文档参考:书名:《从程序员到架构师:大数据量、缓存、高并发、微服务、多团队协同等核心场景实战》-王伟杰
前文如下:
1.业务场景:如何保障服务器承受亿级流量
在某次秒杀活动中,总计有100个特价商品,且每个商品的价格都非常低,活动计划于当年10月10日晚上10点10分0秒开启。当时,服务架构如图11-1所示,所有客户端的API请求先进入Nginx层,再由Nginx层转发至网关层(Java,使用Spring Cloud Zuul),最后转发至后台服务(Java)。
公司预测到秒杀开始那一瞬间会有海量用户涌入,致使系统无法处理所有用户请求。为保障服务器承受住大流量,只能通过限流的方式将部分流量放入后台服务中。
那什么是限流呢?一说到限流,有些人会把它与熔断混在一起讨论,其实它们是有区别的。
熔断一般发生在服务调用方,比如服务A需要调用服务B,调用几次后发现服务B出现了问题且无法再调用,此时服务A必须立即触发熔断,在一段时间内不再调用服务B。
限流一般发生在服务被调用方,且主要在网关层做限流操作。比如一个电商网站的后台服务一秒内只能处理10万个请求,这时突然涌入了100万个请求,该怎么办?此时,可以把90%的请求全部抛弃且不做处理,然后重点处理其余10%的请求,以此保证至少10万人能正常操作(这个比例看起来有点夸张,但是在实际秒杀场景中,即使把99%的流量抛弃掉也不要紧)。
再回到这一章的业务场景中,这次项目的需求是在某个层级通过限流的方式将秒杀活动的交易TPS控制在100笔/秒(因为秒杀活动总计100个商品,也就是说最终的交易只有100笔,希望100笔交易在一秒内完成)。此时应该怎么做呢?这就需要用到限流的一些常用算法了。
2 限流算法
关于限流的算法分为固定时间窗口计数、滑动时间窗口计数、漏桶、令牌桶4种,下面分别进行说明。
2.1 固定时间窗口计数算法
假设需求是后台服务每5秒钟处理500个请求(以5秒为单位方便举例),那么每5秒钟就需要一个时间窗口来统计请求,见表11-1。
此时固定时间窗口算法看起来是可以满足需求的,不过它会存在一个问题。假设14秒有200个请求,5秒时有300个请求,69秒有499个请求,10秒时有1个请求,通过计算得知:15秒总计500个请求,610秒也是总计500个请求。通过以上统计,流量并没有超出阈值。
但是如果计算一下59秒这个区间的请求数,会发现它已经达到了300+499=799个,也就是说59秒的请求数超标了299个,服务器明显支撑不住。因此,固定时间窗口计数算法在现实中并不实用。
2.2 滑动时间窗口计数算法
假设项目需求是后台服务在一秒内处理100个请求,滑动时间窗口计数算法就是每100毫秒设置一个时间区间,每个时间区间统计该区间内的请求数量,然后每10个时间区间合并计算请求总数,请求数超出最大数量时就把多余的请求数据抛弃,当时间节点进入下一个区间(比如第11个区间)时,便不再统计第1个区间的请求数量,而是将第2~11个区间的请求数量进行合并来计算出一个总数,并以此类推,如图11-2所示。
虽然滑动时间窗口计数算法并不能保证每秒的统计请求数都是精准的,但是可以大大减少单位时间内请求数超出阈值且检测不出来的概率。比如,请求都堆积在前100毫秒的尾端与后100毫秒的首端时,才可能出现请求数超出最大数量且不被发现的情况。
当然,可以将这个区间分得更细,比如设置10毫秒为一个区间。区间分得越细,计算数据就越精准,但是资源损耗也越多。
这个算法目前看来似乎已经能满足需求了。不过,场景是这样的:库存中只有100个商品,如果想把TPS控制在100笔/秒,将滑动时间窗口设置为1秒即可,即分成10个区间,每个区间100毫秒,此时就会出现在第一个100毫秒请求已经超出100个的情况,也就是说商品已经被秒光。
这时就有个问题,什么人能在100毫秒内完成点击购买、下单、提交订单的整个流程?可能只有机器人可以做到,也就是说秒杀商品基本是给机器人准备的,这并不是公司想要的结果。
2.3 漏桶算法(请求出口限流)
漏桶算法的实现思路如图11-3所示。
从图11-3中可以看到,漏桶算法的实现分为3个步骤。
1)任意请求进来后直接进入漏桶排队。
2)以特定的速度处理漏桶队列里面的请求。
3)超出漏桶负载范围的新请求直接抛弃掉,无法进入排队队列。
结合上一小节在一秒内控制100个请求的例子,可以把输出速度设置为100个/秒(即每10毫秒处理一次请求),再把桶的大小设置为100。因为漏桶算法是按先进先出的原则处理请求的,所以会出现最终被处理的请求还是前面那100个请求的情况,这就与滑动时间窗口计数算法遇到的问题一样了,最终商品都会被机器人买走。那如果把桶的大小设置为1,不就可以达到目的了吗?
2.4 令牌桶算法(请求入口限流)
令牌桶算法的实现思路如图11-4所示。
1)按照特定的速度产生令牌(Token)并存放在令牌桶中,如果令牌桶满了,新的令牌将不再产生。
2)新进来的请求如果需要处理,则需要消耗桶中的一个令牌。
3)如果桶中有令牌,直接消耗一个。
4)如果桶中没有令牌,进入一个队列中等待新的令牌。
5)如果等待令牌的队列满了,新请求就会直接被抛弃。
再结合上面在一秒内控制100个请求的例子,如果使用令牌桶算法,则需要先把令牌的产生速度设置为100个/秒,等待令牌的排队队列设为0,这样就能满足秒杀限流的需求了。
那令牌桶数量到底设置为多少呢?如果设置为100,假设令牌在秒杀前已经产生,那么秒杀开始时请求数已经是100了,前100个请求就会被放行,也就是说机器人又抢到了所有商品。此时可以设置令牌桶数量为10,这样可以保证最多只有10个机器人抢到商品。
3.方案实现
理解限流的常见算法后,就可以进行方案实现了,需要考虑以下4个问题。
3.1 使用令牌桶还是漏桶模式
刚刚提到令牌桶算法与漏桶算法都可以满足需求,但是做限流时,项目组希望这个算法不仅可以用于秒杀功能,还可以用于其他限流场景。
而使用漏桶算法存在一个缺陷:比如服务器空闲时,理论上服务器可以直接处理一次洪峰,但是漏桶的机制是请求的处理速度恒定,因此,前期服务器资源只能根据恒定的漏水速度逐步处理请求,无法用于其他限流场景。
如果使用令牌桶算法就不存在这个问题了,因为可以把令牌桶一下子装满。因此,针对这个项目,最终使用的是令牌桶。
3.2 在Nginx中实现限流还是在网关层中实现限流
在上述业务场景中,最终决定在网关层实现限流,原因有两点。
1)Nginx中有一个限流插件,它可以对单个用户的请求数做限制,不过它基于漏桶算法,而前面提过,这里希望使用令牌桶算法。
2)当时希望可以动态调整限流的相关配置,就是有一个界面,可以直接管理Nginx的配置。一般这种做法是通过Nginx+Lua实现的,但是因为团队对Lua不熟悉,所以配置人员无法直接操作Nginx中的数据。
而团队对Java是很熟悉的。基于以上两个原因,项目组最终选择在Java的网关层做限流。
3.3 使用分布式限流还是统一限流
网关层也是有负载均衡的,多个网关服务器节点可以共享一个令牌桶(统一限流),也可以每个节点有自己的令牌桶(分布式限流)。
如果使用分布式限流的方式,就需要提前计算服务器的数量,然后把100的TPS平分到各个服务器上进行一层换算。
如果使用统一限流的方式,可以把令牌桶的数据存放在Redis中,即每次请求都需要访问Redis,因秒杀开始时下单的请求数往往很大,Redis未必能承受住如此大的QPS。
所以统一限流有一个风险,就是一旦Redis崩溃,限流就会失效,那后台的服务器就会被拖垮。
如果是分布式限流,假设有些节点失效了,那么其他节点还是可以正常工作的,这样导致的问题有两个。
1)部分网关层的负载增加。不管是统一限流还是分布式限流其实都有这个风险,因为在统一限流中网关服务器也可能崩溃。
2)后台处理100个请求的时间拉长。比如有10个网关,每个网关每秒通过10个请求,这样1秒内就有100个请求到后台服务器。假设其中5台失效,那么每秒只能通过50个请求,2秒才能放行100个请求。不过这对当前的业务来说影响不大。通过对以上问题的衡量,项目组最终决定使用分布式限流方式。
3.4 使用哪个开源技术
项目组最终使用开源库Google-Guava中RateLimiter的相关类来实现限流,它是基于令牌桶算法的实现库。这个库在限流场景中还是比较常用的。使用Google-Guava时,先定义一个Zuul的过滤器(filter),再使用Guava的RateLimiter对提交订单的API请求进行过滤。在使用RateLimiter的过程中,需要配置以下3项。
1)permitsPerSecond:每秒允许的请求数。
2)warmupPeriod:令牌桶多久满。
3)tryAcquire的超时时间:当令牌桶为空时,可以等待新的令牌多久。
分别配置如下。
1)permitsPerSecond设置为100/10=10,100代表想达到的TPS,10代表网关节点为10台,说明每秒可以产生10个令牌。
2)warmupPeriod设置为100毫秒,代表从开始到令牌桶塞满需要100毫秒,即令牌桶的大小是1,如果有10台网关服务器,那么总令牌桶的大小就是10(前面提到过,为防止抢到物品的都是机器人,需要把令牌桶设置为10)。
3)tryAcquire的超时时间设置为0,即拿不到令牌的请求直接抛弃,无须等待。
4. 限流方案的注意事项
4.1 限流返回给客户端的错误代码
为了给用户带来好的体验,用户界面上尽量不要出现错误,因此限流后被抛弃的请求应该返回一个特制的HTTPCODE,供客户端进行特殊处理。而客户端拿到这个错误代码时,就可以展示专门的信息给用户,比如:很遗憾,商品已经秒光,您可以关注下次的秒杀活动。这是第一次秒杀活动的信息。针对第二次秒杀活动,项目组又增加了如下提示:您可以在10分钟后过来,有些秒杀成功但是没有在10分钟内付款的用户,他们锁定的商品会被释放出来。
4.2 实时监控
在实际工作中,最好对限流日志随时做好记录并实时统计,这样有助于实时监控限流情况,一旦出现意外,可以及时处理。
4.3 实时配置
因为限流功能还需要应用到秒杀以外的场景,所以最好在配置中心就可以实现对令牌桶的动态管理+实时设置,这样也方便管理其他的限流场景。
4.4 秒杀以外的场景限流配置
在这次秒杀活动中,可以简单换算出需要控制数值为100的TPS,而在平时的限流场景中,TPS或QPS(其他场景可能不使用TPS)需要根据实际的压力测试结果来计算,从而进行限流的正确配置。