WindChaser 2019-06-27
小编所在的部门作为公司的基础服务部门,支撑着上层业务的正常运行,当有业务举办活动、遭遇攻击,或者是写土了代码,都会或多或少给我们的服务带来流量上的冲击。我们通常说缓存、降级,以及限流技术是高并发服务的三大利器,为保证集团其它业务不受影响,限流往往是服务端接口的必要特性之一,用于对抗大规模恶意请求,保护有限的计算和存储资源。
关于接口限流有很多成熟的算法可供使用,包括:计数器、漏桶,以及令牌桶等,这些算法都为实际项目中的限流器设计提供了理论支撑。
计数器应该是最简单、最容易想到的限流策略,毕竟限流的本质就是限制一个接口在某个维度上单位时间的响应次数。我们可以设置一个计数器对某一时间段内的请求进行计数,当请求量超过某个事先设定的阈值时则触发饱和策略,拒绝用户的请求。比如我们事先设定某个接口单一 IP 维度在 1 分钟内只能正常响应 100 次用户请求,那么如果范围内某个 IP 请求超过该阈值,就会拒绝后续请求。
上述方法存在的一个缺点是计数不够平滑,考虑一个 10 点开放抢购的场景,如果一个恶意用户如果在 09:59:30 ~ 09:59:59
之间请求了 100 次,然后等到 10 点整时计数器被清空,这个时候该用户在 10:00:00 ~ 10:00:30
之间又可以再次请求 100 次,实际上在 09:59:30 ~ 10:00:30
这一分钟内该恶意用户请求了 200 次,成功绕过了限流策略。
针对计数器算法存在的上述缺陷,一种典型的解决方法就是采用 __滑动窗口策略__,本质上就是多级计数。上面我们介绍的 1 分钟 100 次的请求上限可以看做是一个大窗口,我们还可以将该窗口进一步细分,比如每 10 秒一个小窗口,该窗口的频率上限同样设置为 100,大窗口中包含了 6 个小窗口,并且没过 10 秒大窗口就往前移动一个小窗口长度。这样的设计下,如果一个恶意用户在 09:59:30 ~ 09:59:59
之间请求了 100 次,等到 10:00:00 ~ 10:00:30
时这 100 次计数仍然是有效的,所以这个时间段该用户新的请求仍然会被拒绝。
漏桶(Leaky Bucket)算法是限流方面比较经典的算法,该算法最早应用于网络拥塞控制方面。理解该算法可以联想一个具体的漏桶模型,不管进水量有多大,漏桶始终以恒定的速率往外排水,如果桶被装满则后来涌入的水会漫出去。对应接口限流来说,用户的请求可以看做是这里的水,不管用户的请求量有多大多不均衡,能够被处理的请求速率是恒定的,而且能够被接受的请求数也是有上限的,超出上限的请求会被拒绝,典型的我们可以采用队列作为这里的漏桶实现。
由上面的解释我们应该能够感觉到漏桶算法非常适用于秒杀系统的限流,漏桶在这种应用场景下可以起到一定的削峰填谷的作用,并且漏桶的设计从根本上能够应对集中访问的问题,同时具备平滑策略,但是始终恒定的处理速率有时候并不一定是好事情,对于突发的请求洪峰,在保证服务安全的前提下,应该尽最大努力去响应,这个时候漏桶算法显得有些呆滞。
令牌桶(Token Bucket)算法可以看作是计数器算法的逆过程,不过相对于计数器来说更加平滑。该算法要求系统以一定的速率发放访问令牌,用户的请求必须在持有合法令牌的前提下才能够被响应,我们可以按照权重设置一类请求被响应所需持有的令牌数,只有当桶中的令牌数目满足当前请求所需时才授予令牌,对于其他情况则拒绝该请求。
由于发放令牌的速率是恒定的,所以对于集中请求来说,令牌桶算法能够很好的做平滑,比如前面列举的在 09:59:30 ~ 09:59:59
之间有 100 次的突发请求,那么等到 10 点整的时候系统并不会立即容忍 100 次新的请求,这个时候服务的响应受限于当前桶中的令牌数量。实际项目中我们可以基于当前服务能力动态调整令牌的发放速率,此外我们还需要为桶设置大小上限(或者为令牌设置生命周期),以防止大量令牌累积导致的 “伪限流失效” 现象。
Guava 中的 RateLimiter 类对该算法进行了实现,基于 RateLimiter 我们可以设置每秒生成的令牌数,并允许以阻塞、尝试,以及超时等策略获取令牌。相关实现和使用方式比较简单,可以参考官方文档。
此前在写项目时发现组内的限流模块散落在各个具体项目中,并且各个项目都根据自己的业务特点对限流实现做了一定的定制化,这样在一定程度上增加了代码的实现和维护成本,于是就抽空闲时间对这一块进行了改造,抽取出了一个公共的 throttle 基础模块。考虑到时间成本和兼容性,这一块的实现还是采用了平滑版本的计数器,并且在本地和全局维度上进行计数,以应对服务的分布式部署。
如上图所示,限流模块以拦截器的方式进行工作,针对用户的请求会在本地和 IDC 范围内进行计数,当任何一个计数器达到阈值即触发饱和策略。采用计数器的好处是在分布式限流方面实现上比较简单,且除全局缓存以外不依赖于第三方服务,不过远程通信这一块的性能损耗不能忽略,关于远程通信这一块还是有一些优化手段的,比如在本地计数并定时与远端执行同步,没有必要每一次用户请求都进行请求一次远程计数。另外,考虑到各个业务的限流维度不尽相同,在限流器设计上可以引入 SPI 机制来提升可扩展性和可配置性。
总的说来限流器在实现上并没有太多复杂的逻辑,不过正如以前听一位长者所说,__限流的难点在于配置__,如何让限流在不误伤的前提下尽量发挥硬件的最大性能是一个富有经验的问题,而压测是一个基础且行之有效的途径。