服务化体系之—限流
08月 29, 2016 | Filed under 技术
(上)设计篇
在实现算法之前,先临时客串一下产品经理,尝试用最少的字,把“限流”这简单二字所展开的种种需求给说清楚。
1.各种目的
1. 保护每个服务节点。
2. 保护服务集群背后的资源,比如数据库。
3. 避免单个调用者过度使用服务,影响其他调用者。
2. 各种设定维度
2.1. 节点级别 vs 集群级别
如果以保护每个服务节点为目的,可以简单的在本地做节点级别的限流。
但如果以保护服务集群背后的资源为目的,就需要做集群级别的限流。
集群级别的一个做法是使用Redis, Memcached之类做一个集群级别的计数器。但额外多一次访问Redis的消耗,代价有点大。
而另一个做法是把集群限流总数分摊到每个节点上,但一是不够精准,二是如果使用Docker动态缩扩容,需要动态更新这个分摊数。
2.2 客户端 vs 服务端
当以避免单个调用者过度使用服务为目的,可以针对客户端设定限流。
此时限流可以在客户端实现,节约了网络往返,但同样有调用者的节点 or 集群之惑。
也可以在服务端实现,让所有限流逻辑集中于一处发生。
2.3 服务级别 vs 方法级别
可以对消耗特别大的方法专门配置,比如复杂的查询,昂贵的写操作。
然后其他方法使用统一的值,或者配一个所有方法加起来的总和。
3. 各种触发条件
触发条件的设定,难点在于服务的容量,受着本服务节点的能力,背后的资源的能力,下游服务的响应的多重约束。
3.1 静态配置固定值
当然,这个固定值可以被动态更新。
3.2 根据预设规则触发
规则的条件可以是服务平均时延,可以是背后数据库的CPU情况等。
比如平时不限流,当服务时延大于100ms,则触发限流500 QPS。
还可以是多级条件,比如>100ms 限流500 QPS, >200ms 限流200 QPS。
3.3 全动态自动增减调控
这个诱人的想法,永远存在于老板的心里。
4. 各种处理
4.1 立刻返回拒绝错误
由客户端进行降级处理。
4.2 进行短暂的等待
短暂等待,期待有容量空余,直到超时,依然是客户端降级。
4.3 触发服务降级,调用服务端的降级方法
服务端的降级方法,走服务端的简单路径与预设值,则代表了服务端这边的态度和逻辑,各有适用的场景,等下一篇《服务降级》 再详述。
(下)实现篇
开涛的《聊聊高并发系统之限流特技》 已讲得非常好,这里再简单补充两句。
另一篇《接口限流算法总结》 也非常好非常详细,差别只在于对令牌桶的突发量的描述上,我有我自己的理解。
1. 并发控制
并发控制本身就是一种最简单的限流,包括:
框架本身的连接/线程限制
各种数据库连接池,Http连接池
服务/方法级别的信号量计数器限制
2. 窗口流量控制
窗口流量控制有几种做法。
一种最简单的计数器,维护一个单位时间内的Counter,如判断单位时间已经过去,则将Counter重置零。开涛文章里利用guava cache也是一种做法。
但此做法有时被认为粒度太粗,没有把QPS平摊到一秒的各个毫秒里,同时也没有很好的处理单位时间的边界,比如在前一秒的最后一毫秒里和下一秒的第一毫秒都触发了最大的请求数,将目光移动一下,就看到在两毫秒内发生了两倍的TPS。
于是,另一个改进版的做法是把单位时间拆分,比如把1秒分成5个200毫秒宽的桶,然后以滑动窗口来计算限流,好像就能很大程度上解决上面的两个问题了。。。。滑动窗口的算法,统一到《熔断篇》再来讨论。
最后一组就是漏桶算法或令牌桶算法了,下面会详述。
另外,集群规模的计数器,基于Memcachd或Redis的实现,也见开涛的博客。
3. 令牌桶算法(Token Bucket )
- 1. 随着时间流逝,系统会按速率 1/rate 的时间间隔(如果rate=100,则间隔是10ms)往桶里加入Token
- 2. 如果桶满了(burst),则丢弃新加入的令牌
- 3. 每个请求进来,都要消耗一个Token,如果桶空了,则丢弃请求或等待有新的令牌放入。
非常形象,好像不需要更多解析
4. 漏桶算法(Leaky Bucket )
简单的想象有一个木桶,有新请求就是不断的倒水进来,然后桶底下有个洞,按照固定的速率把水漏走,如果水进来的速度比漏走的快,桶可能就会满了,然后就拒绝请求。
可见,两个算法的基本描述上,只是方向不一样,其他没什么不同的。所以WikiMedia里说, 两个算法实现可以一样,对于相同的参数得到的限流效果是一样的。
5. Guava版的令牌桶实现 -- RateLimiter
Guava已实现了一个性能非常好的RateLimiter,基本不需要我们再费心实现。但其中有一些实现的细节,需要我们留意:
1. 支持桶外预借的突发
突发,原本是指如果单位时间的前半段流量较少,桶里会积累一些令牌,然后支持来一波大的瞬时流量,将前面积累的令牌消耗掉。
但在RateLimiter的实现里,还多了个桶外预借(我自己给他的命名),就是即使桶里没有多少令牌,你也可以消耗一波大的,然后桶里面在时间 段内都没有新令牌。比如桶的容量是5,桶里面现在只有1个令牌,如果你要拿5个令牌,也可以,清了桶里的一个令牌,再预借4个。然后再过800毫秒,桶里 才会出现新令牌。
可见,Guava版的RateLimiter对突发的支持,比原版的两种算法都要大,你几乎随时都可以一次过消费burst个令牌,不管现在桶里有没有积累的令牌。
不过有个副作用,就是如果前面都没什么流量,桶里累积了5个令牌,则你其实可以一次过消费10个令牌。。。不过那么一下,超借完接下来还是固定速率的,直到还清了旧账,才可能再来那么一下。
2. 支持等待可用令牌与立刻返回两种接口
3. 单位时段是秒,这有点不太好用,不支持设定5分钟的单位。
4. 发令牌的计算粒度是MicroSeconds,也就是最多支持一百万的QPS。