稳定性五件套-限流的原理和实现
背景
最近了解到很多朋友对限流、熔断、降级、隔离、超时重试的概念和应用场景理解的不是很到位,所以想用五篇的篇幅稍微系统的介绍一下。
本篇是第一篇,是限流做详解,如果反馈好的话,我会继续写下面四篇。不好的话就算了,算我理解不够,再自己总结总结。
限流的概念
有朋友问我限流和熔断有什么区别,我的理解很简单。限流作用是防御上游流量超过处理能力的手段,熔断作用是容错下游的快速失败手段。
举个生活中的限流例子:
小A最近打算找个女朋友,他拜托了很多朋友帮自己介绍,朋友们也很给力,很多姑娘都愿意和小A聊一聊。小A发现时间忙不开了,他就制定了一个计划,一天见2个。这就是限流。
举个生活中的熔断例子:
小A在见这些姑娘的时候,如果有的姑娘不守时,超过约定时间半小时还没有出现,那小A就会离开。不然会耽误见下一位姑娘,这是一种熔断手段。另外,如果有的姑娘特别能说,聊天超过了3小时,小A也会打断姑娘,把姑娘先送走,不然也会耽误见下一位姑娘。这也是需要的熔断措施。
限流的原理
不管任何编程语言的实现,目前主流的底层就是基于令牌桶算法和漏斗算法。这两种算法达到的效果有所不同。
令牌桶算法
令牌桶算法是先有个固定容量的桶,一个任务会以固定的速率往桶里放token,请求来了会去取token。如果桶满了,token就溢出了。多出来的token就不要了。如果请求太快,token生产速度跟不上消费速率,桶空了,有的请求取不到token,这时候就会直接返回错误而不继续处理。
举个例子:
比如小A最后找到了心仪的女朋友小C。他俩相处融洽,一起包饺子吃。小A负责擀皮,小C负责包。小A会把擀好的皮放到一块案板上。这个案板可以放20张皮。如果皮擀多了,就放不下,这时候小A就会停下来等。如果皮擀的慢,小C没的包,也就只能停下来。这里的皮就相当于是token,包饺子就相当于是处理业务的请求。用图表示如下:
漏斗算法
漏斗算法也是先有个固定容量的桶,请求来了先经过桶,从桶里出去的速率是一定的。如果请求量让桶满了,多出来的请求就不处理了。如果桶是空的,新来的请求就能马上处理。
事实上,各种MQ比如kafka就是典型漏斗算法。broker就是这个固定容量的桶,生产者会不断的将数据写到broker里,消费者是采用的拉取模式,总是以固定的速率来消费。
令牌桶算法和漏洞算法的比较
限流的实现
基础实现
在Java中业界用的比较多的是Google出品的Guava RateLimiter和另外的一款resilience4j-ratelimiter来实现限流。原理差不多。
下面以RateLimiter为例进行讲解。要实现一个限流总共需要用到RateLimiter的两个方法:
1>RateLimiter.create() 静态方法创建对象,初始化桶容量
2>acquire()或者tryAcquire() 获取请求token,两者使用一个即可。acquire方法是阻塞式的,用来实现漏斗算法;tryAcquire是非阻塞式的,用来实现令牌桶算法。
阻塞式是如果到达指定条件前一直不返回结果,通过下面的源码可看到内部实际上是用sleep来实现的阻塞。因为所有的请求获取权限时都会sleep固定的时间才返回,就达到了匀速的目的。
非阻塞是立即返回是否获取到权限(token)。这时候请求如果获取权限成功就处理请求,获取权限失败就直接返回一个自定义的快速失败处理方式。平时请求速率小于token产生速率,桶渐渐满了。一旦有突发流量,因为桶里有存量token,也可以直接获取到权限,就是为什么令牌桶算法可以应对突发流量的原理。
高阶实现
上面实现里讲的是工具组件,如果只使用工具组件有个问题。限流实际上需要定期进行容量评估,是一个动态的过程,如果只使用工具组件就需要每次修改代码。当然也可以将每个值写到一个统一配置里,比如zookeeper来进行管理。
如果规模大的情况下更好的一个解决方法是使用专门的平台。这个平台可以支撑更多维度的配置,比如集群维度的限流。集群维度和单机维度的区别是如果设置了一个总的阈值,系统可以根据机器资源情况自动计算出每台机器的限流情况。
在业界,阿里有个sentinel,有人称为微服务哨兵。它是一套更完整的生态,除了我上面提到的功能之外,还提供了动态系统保护、热点限流等功能。