其实业务被攻击过一次之后,我就概览过限流算法一次,当时发现所用的库主要是利用了Golang现成的标准库来做的,没很深入继续研究下去。
前几周回头看这个问题,发现这个库的Readme赫然写着“以前的版本弄错啦,根本提供不了之前说的那些功能, 大家赶紧改改吧”。顿时心里一惊,决定这次抽空好好研究一下。
简单点说,常用的限流算法有两种:令牌桶(token bucket)和漏桶(leaky bucket)。
漏桶:比较像街边小摊的叫号,甭管排了多少人,只能隔一小段时间买一次商品。
令牌桶:比较像餐馆的叫号,本来餐馆有一定的容量, 填满之前,顾客都可以进,填满之后,就跟漏桶差不多了。
一般情况下,令牌桶就够用了。你可以定义最初有多少个令牌(burst),并且定义令牌增加的频率(每秒多少个),每次请求来的时候看有没有令牌可用。Golang库里一个好的实现是tollbooth,除了实现限流算法外,还附带了很多方便的方法取ip,header域等。另外,要说明一点,虽然令牌桶算法是一定时间放一个令牌,但是实现的时候,并不需要新开一个goroutine去隔一段时间增加计数(事实上,用户量很小的情况下,完全可以这么做,见官方例子),而是逻辑上计算时间差对应的令牌差额即可,详细可见golang.org/x/time/rate
的实现,对CPU和Memory非常友好,所以不用担心并发量大了要怎么办。
不过,上述包里的令牌桶算法有几个限制:
- 从写法上看,频率规则必须是以秒为单位。其实他们都会转化成令牌增加速度。例如,1秒10次,那么逻辑上令牌桶里面每0.1秒会增加一个令牌(除非已经满了)。你当然可以设置成1分钟60次,但最终都会转化成1秒1次。
- 从定义上可以看出,它并不能精确限制每个时间单位的个数。例如,定义桶里3个令牌, 每秒新增2个令牌, 那么一秒内(闭区间的话,意味着首尾跨越了一个间隔),可能有2(0 + 2,对应开始时是空桶)到 5(3 + 2, 对应开始时是满桶)个令牌,设定值时,需要注意这点。
有点迷糊?不要慌,下面举个栗子。比如,需要实现“每分钟60个请求”,可能令牌桶并不能特别好的胜任。 假设桶满是20个(相当于一个buffer), 每分钟新增40, 那么就是60个了。但是,爆发的最大值(几乎同一时刻请求)其实达不到60, 因为桶最多就20。同时,如果桶空了,其后的一分钟最多只能接受40个请求。
所以, 爆发值太大,会造成每分钟的请求限制抖动很大。爆发值设置太小,又可能扛不住突然的大量访问。
这该如何是好?下面要讲的东西还有一些,那就下回分解吧。
更新:
重读文章的时候发现之前的理解有误。
对于每分钟60次这样的限制,其实就是设置每秒1次的速率即可。桶大小(Burst)只是用来处理突发的大请求的,即最多一次处理满桶那么多请求。
网友评论