如何让系统在汹涌澎湃的流量面前谈笑风生?我们的策略是不要让系统超负荷工作。如果现有的系统扛不住业务目标怎么办?加机器!机器不够怎么办?业务降级,服务限流!
正所谓「他强任他强,清风拂山岗;他横任他横,明月照大江」,降级和限流是系统可用性保障中必不可少的神兵利器,丢卒保车,以暂停边缘业务为代价保障核心业务的资源,以系统不被突发流量压挂为第一要务。
现状
JPush API 频率控制
JPush API对访问次数,具有频率控制。即一定的时间窗口内,API允许调用的次数是有限制的。免费版:1分钟600次。
超出后:
各业务组直接对接极光API
目前各业务线都在使用极光推送服务,并且是直接调用极光API。水归一源,终汇聚一处。想实现流量管控,只有将调用出口整合到一处,方可达到限流管控的目的。
解决方案
令牌桶算法
令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。令牌桶属于控制速率类型的。令牌桶算法最初来源于计算机网络。在网络传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送。令牌桶算法就实现了这个功能,可控制发送到网络上数据的数目,并允许突发数据的发送。
在 Wikipedia 上,令牌桶算法是这么描述的:
- 每秒会有 r 个令牌放入桶中,或者说,每过 1/r 秒桶中增加一个令牌。
- 桶中最多存放 b 个令牌,如果桶满了,新放入的令牌会被丢弃。
- 当一个 n 字节的数据包到达时,消耗 n 个令牌,然后发送该数据包。
- 如果桶中可用令牌小于 n,则该数据包将被缓存或丢弃。
大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。传送到令牌桶的数据包需要消耗令牌。不同大小的数据包,消耗的令牌数量不一样。
令牌桶这种控制机制基于令牌桶中是否存在令牌来指示什么时候可以发送流量。令牌桶中的每一个令牌都代表一个字节。如果令牌桶中存在令牌,则允许发送流量;而如果令牌桶中不存在令牌,则不允许发送流量。因此,如果突发门限被合理地配置并且令牌桶中有足够的令牌,那么流量就可以以峰值速率发送。
基于令牌桶算法改造消息中心,流程图如下:
单机限流
Google Guava RateLimiter就是令牌桶算法的实现,速率限制器会在可配置的速率下分配许可证。如果必要的话,每个acquire() 会阻塞当前线程直到许可证可用后获取该许可证。一旦获取到许可证,不需要再释放许可证。RateLimiter经常用于限制对一些物理资源或者逻辑资源的访问速率。与Semaphore 相比,Semaphore 限制了并发访问的数量而不是使用速率。
|
|
通过设置许可证的速率来定义RateLimiter。在默认配置下,许可证会在固定的速率下被分配,速率单位是每秒多少个许可证。为了确保维护配置的速率,许可会被平稳地分配,许可之间的延迟会做调整。可能存在配置一个拥有预热期的RateLimiter的情况,在这段时间内,每秒分配的许可数会稳定地增长直到达到稳定的速率。
分布式限流
Remote Dictionary Server(Redis)是一个基于 key-value 键值对的持久化数据库存储系统。支持多种数据结构,包括 string (字符串)、list (链表)、set (集合)、zset (sorted set –有序集合)和 hash(哈希类型)。这些数据类型都支持 push/pop、add/remove 及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。
想实现分布式限流,非Redis莫属。Redis限流的大体思路是设置一个带有过期时间的key,每次申请令牌时,将 key 所储存的值加上增量 increment, 判断key 所存储的值是否达到上限,未达到上限,则获得许可。如有需要,亦可阻塞当前线程直到获得许可。
限流相关的命令:
事务
MULTI、EXEC、DISCARD和WATCH命令是Redis事务功能的基础,Redis事务允许在一次单独的步骤中执行一组命令,并且可以保证如下两个重要事项:
Redis会将一个事务中的所有命令序列化,然后按顺序执行。
Redis不可能在一个Redis事务的执行过程中插入执行另一个客户端发出的请求。这样便能保证Redis将这些命令作为一个单独的隔离操作执行。
在一个Redis事务中,Redis要么执行其中的所有命令,要么什么都不执行。
因此,Redis事务能够保证原子性。EXEC命令会触发执行事务中的所有命令。因此,当某个客户端正在执行一次事务时,如果它在调用MULTI命令之前就从Redis服务端断开连接,那么就不会执行事务中的任何操作;相反,如果它在调用EXEC命令之后才从Redis服务端断开连接,那么就会执行事务中的所有操作。当Redis使用只增文件(AOF:Append-only File)时,Redis能够确保使用一个单独的write(2)系统调用,这样便能将事务写入磁盘。然而,如果Redis服务器宕机,或者系统管理员以某种方式停止Redis服务进程的运行,那么Redis很有可能只执行了事务中的一部分操作。Redis将会在重新启动时检查上述状态,然后退出运行,并且输出报错信息。使用redis-check-aof工具可以修复上述的只增文件,这个工具将会从上述文件中删除执行不完全的事务,这样Redis服务器才能再次启动。
从2.2版本开始,除了上述两项保证之外,Redis还能够以乐观锁(Watch)的形式提供更多的保证,这种形式非常类似于”检查再设置”(CAS:Check And Set)操作。
从2.6版本开始提供脚本(Lua scripting)能力,一种更灵活的批量命令组织方式用于取代目前的事务机制。脚本提供了更强大和灵活的编程能力,但也是一把双刃剑,由于 Redis 需要保证脚本执行的原子性和隔离性,脚本执行期间会阻塞其他命令的执行,因此建议使用高效的脚本完成业务。
SET
SET key value [EX seconds] [PX milliseconds] [NX|XX]
将字符串值 value 关联到 key 。如果 key 已经持有其他值, SET 就覆写旧值,无视类型。对于某个原本带有生存时间(TTL)的键来说, 当 SET 命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。
从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:
EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
XX :只在键已经存在时,才对键进行设置操作。
INCRBY
将 key 所储存的值加上增量 increment 。如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCRBY 命令。如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。
Redis官方基于上述命令提供的限流代码:https://redis.io/commands/incr#pattern-rate-limiter-1
代码验证
使用Jedis client进行限流方案验证:
|
|
|
|
|
|
JedisTemplate.java
验证代码
|
|
测试用例
10个线程20次请求,限制5s内10次请求。
测试结果
10次请求在5s内正常通过,剩下10次请求等待下个时间窗口轮询。