网关限流功能性能优化

本文主要从设计与原理方面分享优化过程中的思考,不涉及具体的代码实现。在分析过程中我会写一些当时思考的问题,在看后续答案时可以自己也先思考一下

老的限流方案

首先讲解一下原本网关限流功能的实现方案,省略其中的白名单,黑名单,令牌桶算法实现等一些细节

  • 限流策略中包含多种策略,比如根据用户维度限流,ip维度限流,接口维度限流等,每种限流策略各自维护自己的计数。任意一个策略触发限流拒绝都会拒绝请求
  • 每种限流策略都会设置好总允许的权重值,每次访问通过redis的lua脚本进行权重的扣除和增加,直到为0,扣除不了的则触发拒绝
  • 简单讲一下限流使用的令牌桶,网关会在redis中记录剩余令牌数和上次补充令牌的时间点,redis的key是限流维度生成的唯一id,比如用户维度则是特殊前缀+用户id。假设我们有一个令牌桶,初始令牌数为10,每5秒钟补充5个令牌,桶的最大容量也是10个令牌。那么令牌使用情况如下图:
  • 老限流的流程图:

限流存在的问题

总体来看,这个限流的方案还是比较简单的,当流量不大的时候也可以正常运行。
但是流量大的时候存在一些问题,大家可以先想一下有哪些问题?

  1. 一次请求由于需要从多个维度进行限流,所以会串行的多次访问redis,每次io都需要消耗时间
  2. lua脚本在redis中执行,redis的性能成为瓶颈,一方面是cpu高,另一方面redis的命令也是串行的,后面的命令需要等待前面的命令
  3. 即使权重已经扣除到0了,当请求过来时,仍然需要访问redis去判断是否可以通过

分析和优化思路

针对这3个问题,我们分析一下,都有哪些处理方案,以及这些方案的优缺点

第一个问题:多次串行IO
  1. 多线程并行访问

    • 并行可以同时进行网络请求,虽然总次数没有少,但是总时间可以减少到最慢的那次请求的耗时。比如,每次1秒,串行访问3次,总耗时3秒,改为并行则只需要1秒。
    • 串行改并行一般来说改动起来会小一些,比较好实现。可以节省时间。但是本质上没有减少资源消耗
    • 同时由于本次请求的是redis,redis仍然需要进行串行执行
    • 所以这个方案并不好
  2. 合并请求批量访问

    • 在这个场景中,时间消耗分为,io耗时+redis执行耗时。
    • 批量访问可以减少io耗时的部分,但是不会减少redis执行耗时。
    • 总体来看这个方案可以减少总耗时,但是由于需要聚合请求,分发响应,因此代码改动会稍微大一些。
    • 目前看是一个可行的方案
  3. 减少redis的访问

    • 首先我们要分析redis的作用是什么,为什么我们需要访问redis?
      • 由于网关是无状态的,限流是全局维度的,所以针对这个场景我们利用redis作为一个中心化的数据保存,也就是每个策略的权重数据。其次由于请求是并发的,所以我们利用了redis来保证权重加减的原子性。在这2个原因中的3个关键点是:数据保存,中心化和原子性。
    • 数据保存通常来讲我们遵循(内存>redis>数据库)。那我们是否可以用内存代替redis实现数据保存?
      • 如果网关只有一台机器,那么是可行的,但是现在由于多台机器,所以中心化这个关键点限制了我们把数据从redis改为内存。
    • 那我们改进一下这个思路,能不能把部分数据改为内存?
      • 数据是权重数据,本质上就是一个计数器。那我们将计数器的一部分值放到内存中,扣除完了之后再来redis扣除一次(这个思路其实就是java中的TLAB机制的启发)。
    • 数据保存和中心化都没问题了,那原子性是否可以保证?
      • 不同机器从redis中扣除这部分逻辑与之前是一致的,所以原子性没问题,保存在内存中的值,只会被单机操作一定可以保证原子性(最差就是加锁)。
    • 那么最终分析之后这个方案也是可行的
第二个问题: redis执行lua脚本成为瓶颈
  1. 优化lua脚本
    • lua脚本实现的是令牌桶算法,在当前场景下没有发现优化空间,因此不考虑
  2. redis分片处理
    • 不同的key路由到不同的redis中,可以解决瓶颈问题,方案可行,但是本质上没有减少资源消耗
  3. 减少redis访问
    • 同上一个问题的解决方案,方案可行
第三个问题:权重扣除为0,仍然访问redis
  1. 还是先分析原因,为什么权重为0时我们需要访问redis?
    • 因为内存中并不知道权重是否已经为0,这个数据只在redis中存在。
  2. 那我们有什么办法可以提前知道redis中权重是否为0?
    • 好像没有办法
  3. 但是如果上一次请求返回了权重为0,那么这一次请求是否可以判断出redis中权重是否为0?
    • 答案是部分可以。
    • 利用令牌桶算法的特点,令牌桶每隔一定时间会增加令牌。如果当前权重已经是0,且在到需要增加令牌的时间之前,权重一定一直为0。
    • 所以当上一次请求返回为0时,同时记录上一次补充令牌的时间点,那么就可以推算出在哪个时间点前,权重一直为0,那么此时就不需要再次访问redis。
    • 通过这种方式可以极大的减少权重为0时的redis访问。不管总请求数是多少,在一个令牌补充周期内,每台机器只会在权重为0时访问一次redis。
    • 这个问题的解决方案与上2个问题的解决方案同时兼容,因此也是可行的

优化后的方案

现在我们总结上面的问题解决方案:

  • 可以看到减少redis访问这个方案可以同时解决第一个和第二个问题,并且真实的减少了资源消耗
  • 第三个问题的解决方案也不冲突,因此优化后的限流方案改为如下流程:
新方案的优点
  • 从原本一次请求多次redis访问变成了,多次请求一次redis操作
  • 大部分扣除权重不经过网络,纯内存操作
  • redis访问极大的减少,redis的消耗变小
  • 权重扣完后不会再请求redis,避免恶意流量打垮redis
新方案有什么缺点?
  • 限流的精确度变差,限流的边界值被模糊了
    • 比如原本限流1w次,现在变成不到1w次就已经触发了限流
    • 已经触发限流后,在补充令牌前,后面的请求可能又可以成功
  • 这个缺点我认为是可以接受的,毕竟我们的限流没有必要那么的精确,可以容忍
  • 提一个问题,假设一个令牌周期内限流1w次,新方案的限流最早从第多少次会触发第一次限流?和哪些因素有关?

优化后的效果

redis的cpu使用率从85% -> 5%
机器的cpu使用率从70% -> 50%

优化后的总结

  • 优化的首要关键因素是能发现问题,也就是优化点
    • 一般最直接的方式就是通过监控发现,所以监控很重要
    • 业务的理解程度也很大程度上会影响你是否能发现问题
  • 其次是问题的解决方案
    • 解决方案跟个人经验有关系,但是大部分的问题的解决方案都是类似的,通过其他人的问题解决方案来积累经验我认为是最值的
    • 一般来讲很难一下子就想到最优方案。想出一种方案后最好再思考一下,有哪些缺点,还有没有其他方案。比较多个方案选一个相对最优的方案
  • 优化时常用的一些思路
    • 通用方案不一定是最好的,可以利用一些功能的特点去针对性的优化
    • 优化到一定程度后,一般很难做到十全十美,通过放弃一部分东西可以换取一些你更需要的东西,比如空间换时间

如果觉得本文的内容有不足之处,欢迎指出

总觉得自己写的文章语言都好生硬,不知道怎么改善