Web应用开发和部署场景中,对于大规模的网络请求处理,经常会遇到需要进行访问请求流量控制(限流)的情况。这种操作本质上是通过临时降低服务质量的方式,保护应用系统,不会由于过量处理和系统超载而崩溃,从而完全失去服务能力,或者可能在请求规模恢复正常时,无法从崩溃中恢复。其基本的原理是设置一个合理的系统处理规模的上限,如果处理请求超过这个上限,则直接拒绝这些请求,不会真正的进行处理,从而保护系统处在一个比较合理的负载水平之下;并且这个机制可以进行动态调整,当请求规模正常时,就能够恢复,并进行正常的处理。
当然,相关的解决方案和实现方式有很多,本文讨论的是其中的一种比较常用的访问流量控制算法-令牌桶(Token Bucket)。另外本文主要讨论请求-响应这一应用模式(典型的如HTTP)的情况,其他的应用场景如数据传输、队列处理等类似的数据流控制的情况,还有其他算法实现如令牌桶泄露算法、滑动窗口(TCP协议使用)等,不在本文中讨论。
设计和思路
令牌桶算法实现的基本构思和处理逻辑过程如下(如图):
- 先设置一个有一定容量的令牌桶
- 定时向桶里面注入一定数量的令牌,直到令牌桶注满,这里注入的速度其实就是希望控制的最大流量
- 访问请求来临的时候,向令牌桶申请相应数量的令牌
- 如果桶里面有足够数量的令牌,则确认提供令牌,并修改桶中的令牌数量
- 如果桶里面没有足够数量的令牌,则告知没有令牌,从而拒绝请求(后期的改进可以提供部分令牌)
代码实现
下面是基于设计构想和思路,使用Javascript语言实现的令牌桶算法的代码和使用示例,并结合代码的分析和说要:
算法使用时,需要先初始化一个实例,初始参数包括令牌桶的容量,令牌生成的速率(生成间隔和数量)。
这里的令牌,实际上是一个抽象的概念,代码实现并不维护一个真实的令牌数据和结构,而是使用一个计数器。另外在算法中进行了计数处理的改进,不是定时补充令牌,而是在请求时,根据时间计算应该补充的令牌数量(这样可以节省定时器和处理)。
参考的原始代码,只判断桶里是否有足够数量的令牌,直接返回是或者否,作为成功获取令牌的依据。笔者对其进行了一些改进,如果需要一定数量的令牌,会尽力提供(部分满足)。
后面的测试代码,是为了便于观察和理解。 就是令牌生成的速度小于消耗的速度,可以展示获取失败的情况。在真正的业务环节中,需要根据业务的特点设置和调整。
特点和优势
在了解和熟悉了令牌桶算法以后,我们会很自然的理解到其特点,并更好的将其适配到我们的开发和应用场景中。
- 简单
可以看到,这个算法的实现、扩展和使用都是非常简单的。详后。
- 更好的控制和细粒度
可以通过控制令牌注入间隔和数量,可以提供比简单请求计数器等流控方式更好的控制粒度和弹性。
- 业务解耦
这个算法和业务本身进行了很好的解耦,部署和集成非常简单。基本上不需要侵入和修改业务代码(只需要在业务调用之前判断能否获得令牌)。 有的算法实现相对就比较复杂,比如需要业务调用回报处理状态和结果等等。
- 合理的规划
这个令牌桶算法在实际使用过程中,需要预先评估系统实际的处理能力,然后设置合理的初始参数。比如初始评估接口的处理能力为3000,则可以将参数设为容量为1500,每10ms生成30个令牌。即生成速度和间隔就应该和处理能力相等。
至于令牌桶容量,笔者的理解它是一个缓冲池,即给了系统一点弹性,可以短时间超载,不至于一上来就开始拒绝服务。这个值的大小,暂时设置为500ms的处理能力。
- 算法改进
基于基本的实现,我们可以考虑如下的改进方向:
-- 可以在运行当中调整参数,做到自动适应,因为和业务状态解耦,所以初始设置的参数不一定合理,可以根据一些反馈数据来进行调整(已实现setBucket方法)
-- 还有一种可以主动调整令牌桶使用的方法,就是在业务完成后,向桶里归还令牌(已实现returnToken方法)
-- 如果有切实的需求,也可以实现真实的令牌桶数据结构。比如使用一个队列维护一系列令牌,然后业务系统使用令牌来开展业务等等。在原型代码的基础上改进,也是比较简单的。