Token-Bucket-II
昨天公司年会上中了一部iPhoneX,感觉用尽了积攒了二十多年的运气。
上回说到用ticker的方式后台fill令牌桶的效率是最高的,然后鄙人就很奇怪,所以就又刨根问底测试了一下,发现在单核的情况下(用docker的方式绑定CPU到容器),如果有大于100个协程在run的话,性能的确会受影响。
代码: https://gist.github.com/wrfly/3f2b23b20d53fbe5f9fee8e8f89fe861
CPU: Intel(R) Core(TM) i7-7600U CPU @ 2.80GHz
多核情况下的性能对比:
➜ ratelimit ./ratelimit
2018/01/20 15:10:28 start testing...
2018/01/20 15:10:28 5s test
2018/01/20 15:10:33 juju[lock]: 5.000108905
2018/01/20 15:10:33 take: 6000
2018/01/20 15:10:33 drop: 35740301
2018/01/20 15:10:33
2018/01/20 15:10:38 bsm[atomic]: 5.000146318
2018/01/20 15:10:38 take: 6000
2018/01/20 15:10:38 drop: 57423059
2018/01/20 15:10:38
2018/01/20 15:10:43 wrfly: 5.000100057
2018/01/20 15:10:43 take: 5000
2018/01/20 15:10:43 drop: 116287439
2018/01/20 15:10:43
2018/01/20 15:10:48 tb: 5.000113946
2018/01/20 15:10:48 take: 6000
2018/01/20 15:10:48 drop: 117678990
2018/01/20 15:10:48
2018/01/20 15:10:48 range test: 10000000
2018/01/20 15:10:48 dry run: 0.002578619
2018/01/20 15:10:48
2018/01/20 15:10:49 juju[lock]: 0.976654044
2018/01/20 15:10:49 take: 1976
2018/01/20 15:10:49 drop: 9998024
2018/01/20 15:10:49
2018/01/20 15:10:50 bsm[atomic]: 0.516987935
2018/01/20 15:10:50 take: 1516
2018/01/20 15:10:50 drop: 9998484
2018/01/20 15:10:50
2018/01/20 15:10:50 wrfly: 0.023622615
2018/01/20 15:10:50 take: 25
2018/01/20 15:10:50 drop: 9999975
2018/01/20 15:10:50
2018/01/20 15:10:50 tb: 0.029078959
2018/01/20 15:10:50 take: 1000
2018/01/20 15:10:50 drop: 9999000
2018/01/20 15:10:50
➜ ratelimit
可以看到,使用对比时间戳的方式实现的令牌桶和使用ticker的差别很大,这是因为大部分的时间都耗在获取当前时间上了:
func testTimeGet() {
num := 0
ctx, cancel := context.WithDeadline(context.Background(),
time.Now().Add(time.Second*5))
defer cancel()
for ctx.Err() == nil {
num++
time.Now().UnixNano()
}
log.Println("num:", num)
}
[Running] go run "/home/mr/test/go/ratelimit/main.go"
2018/01/20 15:18:01 num: 63049350
单单只有获取时间,五秒钟也只range了63049350个而已。所以要想提升性能,就要抛弃这种方法。当然也不是说这种不好,这种方法只是没能显现出多核的优势而已。
单核情况下的性能对比:
➜ ratelimit docker run --memory=1G --cpuset-cpus 0 --rm -ti -v `pwd`:/tmp --name test golang bash
WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap.
root@c4a0eeb6a504:/go# cd /tmp/
root@c4a0eeb6a504:/tmp# ls
main main.go main.go.2 main.go.bak ratelimit
root@c4a0eeb6a504:/tmp# ./ratelimit
2018/01/20 07:21:54 start testing...
2018/01/20 07:21:54 5s test
2018/01/20 07:21:59 juju[lock]: 5.041641557
2018/01/20 07:21:59 take: 6041
2018/01/20 07:21:59 drop: 35580348
2018/01/20 07:21:59
2018/01/20 07:22:04 bsm[atomic]: 5.045806507
2018/01/20 07:22:04 take: 6045
2018/01/20 07:22:04 drop: 57940620
2018/01/20 07:22:04
2018/01/20 07:22:09 wrfly: 5.065803119
2018/01/20 07:22:09 take: 124
2018/01/20 07:22:09 drop: 117384667
2018/01/20 07:22:09
2018/01/20 07:22:14 tb: 5.045892976
2018/01/20 07:22:14 take: 6000
2018/01/20 07:22:14 drop: 117920987
2018/01/20 07:22:14
2018/01/20 07:22:14 range test: 10000000
2018/01/20 07:22:14 dry run: 0.002730106
2018/01/20 07:22:14
2018/01/20 07:22:15 juju[lock]: 0.984049551
2018/01/20 07:22:15 take: 1984
2018/01/20 07:22:15 drop: 9998016
2018/01/20 07:22:15
2018/01/20 07:22:15 bsm[atomic]: 0.51509471
2018/01/20 07:22:15 take: 1515
2018/01/20 07:22:15 drop: 9998485
2018/01/20 07:22:15
2018/01/20 07:22:15 wrfly: 0.027949863
2018/01/20 07:22:15 take: 2
2018/01/20 07:22:15 drop: 9999998
2018/01/20 07:22:15
2018/01/20 07:22:15 tb: 0.030819544
2018/01/20 07:22:15 take: 1000
2018/01/20 07:22:15 drop: 9999000
2018/01/20 07:22:15
root@c4a0eeb6a504:/tmp#
可以看出,我的方法是比较垃圾的,在单核情况下五秒钟才通过了124个。这是因为,每次我都需要调度一个goroutine出去,等待一段时间然后将标志位清零,再接受take请求。频繁的goroutine让效率如此低下。所以,在资源有限的情况下(单核),goroutine多了并不是好事,更可能会碍事。
之前遇到的情况就是,单核的资源限制,一个程序运行了1w+的协程,最后内存爆炸。其实这是一连串的反映,因为协程越多,越有可能却换不到核心函数,然后又引发goroutine的增长。
然后加上后台运行goroutine,单独测试一下tb的实现:
func routine() {
x := 0
for {
x++
time.Sleep(time.Duration(x))
}
}
func getTime() {
num := 0
ctx, cancel := context.WithDeadline(context.Background(),
time.Now().Add(time.Second*5))
defer cancel()
for ctx.Err() == nil {
num++
time.Now().UnixNano()
}
log.Println("num:", num)
}
func testTB() {
drop = 0
take = 0
ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(duration))
start = time.Now()
for ctx.Err() == nil {
if tbBkt.Take(1) == 1 {
go routine()
take++
continue
}
drop++
}
cancel()
log.Println("tb: ", time.Since(start).Seconds())
log.Println("take:", take)
log.Println("drop:", drop)
log.Println()
time.Sleep(time.Second)
drop = 0
take = 0
start = time.Now()
for i := 0; i < end; i++ {
if tbBkt.Take(1) == 1 {
go routine()
take++
continue
}
drop++
}
log.Println("tb: ", time.Since(start).Seconds())
log.Println("take:", take)
log.Println("drop:", drop)
log.Println()
}
root@30831db642cd:/go# /tmp/ratelimit
2018/01/20 07:57:35 tb: 5.032092409
2018/01/20 07:57:35 take: 5000
2018/01/20 07:57:35 drop: 95187340
2018/01/20 07:57:35
2018/01/20 07:57:36 tb: 0.025567881
2018/01/20 07:57:36 take: 1000
2018/01/20 07:57:36 drop: 9999000
2018/01/20 07:57:36
竟然毫无压力。。。
所以得出两个结论,要么是我之前测试错了,要么就是当前测试方法不对。因为实际业务比较复杂,不单单是time.Sleep
这么简单,而且sleep会使CPU切换,更能够触发ticker。
然后,还用bsm的方法测试过,发现有内存飙涨:
CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
test 81.92% 1024MiB / 1GiB 99.99% 5.67kB / 0B 4.62GB / 8.26GB 5
^C
真……
这几天晚上一直被这货困扰,想着搞一个很牛逼的tokenbucket出来,然而越弄越乱,不了了之。
想过用channel的方式取代原子操作,但一比较发现,channel的效率很低: https://gist.github.com/wrfly/63f56519020494d68aebc9fad713daa5
所以最终得到的方案还是上一篇的方案,自己实现的tokenbucket没什么好处也。