缓存、降级和限流是高并发保护高可用的手段。比如某个明星在微博上发了一条动态,短时间内其可能获得上亿次的转发量,这给微博的服务器造成了巨大的压力,甚至有短暂不可用的状态。解决方法无外乎紧急增加缓存,将不常用的特性临时降级,对资源进行限流,避免将后台服务打爆。
限流是我们常用的在高并发下保护有限资源的手段。现在可能没有人去抢火车票了,但在 N 年前过年的时候,大家都会拼命地抢火车票,以至于把 12306 网站刷得不可用了。 12306 通过业务改造,扩容、分片以及排队等限流手段,降低了对服务器的压力,保证了网站可用。
令牌桶和漏桶是常见的限流手段,可以帮助我们处理高并发的请求。
1. 基于令牌桶实现的限流库
令牌樋算法是网络流量整行( traffic shaping) 和速率限制( rate limiting )中最常使用的一种算法。它不只是用来处理网络流量,我们在处理任意请求时,也可以使用它来控制处理请求的速率,并允许对一定范围内的突发请求进行处理。
大小固定的令牌桶以恒定的速率源源不断地产生令牌,如下图所示:
如果令牌不被消耗,或者令牌被消耗的速率小于其产生的速率,那么令牌桶中的令牌就会不断增多,直到把桶填满。后面新产生的令牌会从令牌桶中溢出,最后令牌桶中可以保存的最大令牌数永远不会超过桶的大小。
令牌桶的处理方式如下:
- 假如用户配置的处理速率为 r,则每隔 1/r 秒就会将一个令牌加入令牌桶中。
- 假设令牌桶最多可以存放 n 个令牌,如果新令牌到达时令牌桶已满,那么这个新令牌会被丢弃。
- 当处理一个请求时,就从令牌桶中删除一个令牌。
- 更广泛的,可以一次申请多个令牌,这可能是一次要处理多个请求,也可能是请求处理的权重不同,有的请求需要多个令牌。
- 如果当前令牌桶中的令牌少于请求的令牌,则不会删除令牌,这个请求将被丢弃;或者,更好的方式是不丢弃请求,而是将其放入一个队列中缓存起来,以后再处理。
可以看到,如果某个时间没有请求要处理的话,那么令牌桶中的令牌可能会积攒得非常多,甚至令牌桶满了。这时候,如果有大量突发请求,它们可能都能获取到令牌,一起按并发地处理。假设在短时间内需要处理大量的请求,在极端情况下,如果突发请求把令牌全取走了,那么之后即使是很小的并发请求,也获取不到令牌,它可能会被丢弃或者在队列中等待。这是令牌桶的一个特点,允许处理突发请求,但是从一个长周期来看,令牌桶的处理速率是恒定的。
1.1 x/time/rate
golang.org/x/time/rate 是 Go 官方提供的一个基于令牌桶实现的限流库。
这个库提供的 Limiter 可以控制事件发生的频率。假设令牌桶的容量为 b,每秒以固定的速率 r 填允,并且一开始令牌桶就被填满了。在很长的一段时间内,Limiter 限制令牌产生的速率是每秒 r 个,并且允许有最多 b 个突发事件。如果 b 是无穷大的,那么它就会被忽略。b 既是令牌桶的容量,也是允许一次获取的最大令牌数。
Limiter 的零值也是有效的值,但是基本上没什么意义,因为它表示拒绝所有的事件。
Limiter 有三个主要办法,即 Allow、Reserve、Wait,每个方法都会消耗一个令牌。它们的不同之处在于:
- Allow --如果没有令牌可用,这个方法将直接返回 false,不会被拒绝
- Reserve--如果没有令牌可用,这个方法将返回为未来可用令牌保留的一个对象Reservation,以及调用者要等待的时间。
- Wait--如果没有令牌可用,这个方法将会被阻塞,直到获取到一个令牌,或者 Context 完成(被撤销)。
1.2 juju/ratelimit