算法研究和实现-令牌桶TokenBucket

842 阅读5分钟

Web应用开发和部署场景中,对于大规模的网络请求处理,经常会遇到需要进行访问请求流量控制(限流)的情况。这种操作本质上是通过临时降低服务质量的方式,保护应用系统,不会由于过量处理和系统超载而崩溃,从而完全失去服务能力,或者可能在请求规模恢复正常时,无法从崩溃中恢复。其基本的原理是设置一个合理的系统处理规模的上限,如果处理请求超过这个上限,则直接拒绝这些请求,不会真正的进行处理,从而保护系统处在一个比较合理的负载水平之下;并且这个机制可以进行动态调整,当请求规模正常时,就能够恢复,并进行正常的处理。

当然,相关的解决方案和实现方式有很多,本文讨论的是其中的一种比较常用的访问流量控制算法-令牌桶(Token Bucket)。另外本文主要讨论请求-响应这一应用模式(典型的如HTTP)的情况,其他的应用场景如数据传输、队列处理等类似的数据流控制的情况,还有其他算法实现如令牌桶泄露算法、滑动窗口(TCP协议使用)等,不在本文中讨论。

设计和思路

令牌桶算法实现的基本构思和处理逻辑过程如下(如图):

令牌桶.jpg
  • 先设置一个有一定容量的令牌桶
  • 定时向桶里面注入一定数量的令牌,直到令牌桶注满,这里注入的速度其实就是希望控制的最大流量
  • 访问请求来临的时候,向令牌桶申请相应数量的令牌
  • 如果桶里面有足够数量的令牌,则确认提供令牌,并修改桶中的令牌数量
  • 如果桶里面没有足够数量的令牌,则告知没有令牌,从而拒绝请求(后期的改进可以提供部分令牌)

代码实现

下面是基于设计构想和思路,使用Javascript语言实现的令牌桶算法的代码和使用示例,并结合代码的分析和说要:

算法使用时,需要先初始化一个实例,初始参数包括令牌桶的容量,令牌生成的速率(生成间隔和数量)。

这里的令牌,实际上是一个抽象的概念,代码实现并不维护一个真实的令牌数据和结构,而是使用一个计数器。另外在算法中进行了计数处理的改进,不是定时补充令牌,而是在请求时,根据时间计算应该补充的令牌数量(这样可以节省定时器和处理)。

参考的原始代码,只判断桶里是否有足够数量的令牌,直接返回是或者否,作为成功获取令牌的依据。笔者对其进行了一些改进,如果需要一定数量的令牌,会尽力提供(部分满足)。

后面的测试代码,是为了便于观察和理解。 就是令牌生成的速度小于消耗的速度,可以展示获取失败的情况。在真正的业务环节中,需要根据业务的特点设置和调整。

特点和优势

在了解和熟悉了令牌桶算法以后,我们会很自然的理解到其特点,并更好的将其适配到我们的开发和应用场景中。

  • 简单

可以看到,这个算法的实现、扩展和使用都是非常简单的。详后。

  • 更好的控制和细粒度

可以通过控制令牌注入间隔和数量,可以提供比简单请求计数器等流控方式更好的控制粒度和弹性。

  • 业务解耦

这个算法和业务本身进行了很好的解耦,部署和集成非常简单。基本上不需要侵入和修改业务代码(只需要在业务调用之前判断能否获得令牌)。 有的算法实现相对就比较复杂,比如需要业务调用回报处理状态和结果等等。

  • 合理的规划

这个令牌桶算法在实际使用过程中,需要预先评估系统实际的处理能力,然后设置合理的初始参数。比如初始评估接口的处理能力为3000,则可以将参数设为容量为1500,每10ms生成30个令牌。即生成速度和间隔就应该和处理能力相等。

至于令牌桶容量,笔者的理解它是一个缓冲池,即给了系统一点弹性,可以短时间超载,不至于一上来就开始拒绝服务。这个值的大小,暂时设置为500ms的处理能力。

  • 算法改进

基于基本的实现,我们可以考虑如下的改进方向:

-- 可以在运行当中调整参数,做到自动适应,因为和业务状态解耦,所以初始设置的参数不一定合理,可以根据一些反馈数据来进行调整(已实现setBucket方法)

-- 还有一种可以主动调整令牌桶使用的方法,就是在业务完成后,向桶里归还令牌(已实现returnToken方法)

-- 如果有切实的需求,也可以实现真实的令牌桶数据结构。比如使用一个队列维护一系列令牌,然后业务系统使用令牌来开展业务等等。在原型代码的基础上改进,也是比较简单的。