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没什么好处也。

comments powered by Disqus