1. 分布式限流介绍

1.1 限流的定义

限流就是某个时间窗口对资源访问做限制,比如设定每秒最多100个访问请求,但是在真正的场景里,一般设置不只一种限流规则,而是使用多种限流规则,共同作用。

1.2 常见限流规则

1.2.1 QPS和连接数控制

  1. 访问评率:QPS(query per second)
  2. 可以设置IP维度的限流,例如:直接对yihua.com这个IP进行限制
  3. 也可以设置单个服务器的限流,例如:对192.168.73.131这个IP进行限制
  4. 生产实践中通常会设置多个维度的限流规则
  5. 设置举例

    1. 设置一个IP每秒访问评率小于10,连接数小于5
    2. 再设定每台机器的QPS最高1000,连接数最大200
    3. 甚至可以把某个服务器组或者整个机房的服务器当做一个整体,设置high-level的限流规则

      1.2.2 传输速率

  6. 传输速率:例如资源下载的速度

  7. 有的网站对这方面的限流逻辑做的更细致,例如百度云盘
  8. 可以使用Nginx限制传输速度

    1.2.3 黑白名单

  9. 黑白名单是各大企业常用的限流和方形的手段,而且黑白名单往往是动态变化的

  10. 不解释了,在自己的经历

    1.3 限流的思考

    未处理的请求可以设置消息队列缓冲,超过队列长度就丢弃,限流本身就是要丢弃过多请求,没有问题
    理解:限流本来就是丢弃过多的请求

    2. 限流算法

    https://class.imooc.com/lesson/1238#mid=29659

    2.1 令牌桶算法

    image.png
    image.png

    2.2 漏桶算法

    2.3 滑动窗口

    3. 常用解决方案

    3.1分布式环境的限流思考

    image.png

    3.2 网关层限流

    image.png
    image.png

    3.3 中间件限流

    image.png

    3.4 限流组件

    image.png

    3.5 各种限流比较

  11. 网关层限流可以避免你的流量冲击到服务器

  12. 中间件、限流组件在应用层面才会拦截住,但是它们在限流措施上更加灵活,例如最后的Redis注解方式
  13. 实战中一定是多种限流手段共同作用

    4. 基于Nginx的网关层限流

    image.png
    image.png

    4.1 IP限流

    完全的是一些配置,暂时不记,主要IP限流是限制的服务器的ip不是用户的ip地址
    image.png
    image.png

    4.2 配置单机限流

    image.png

    4.3 连接数限制

    一些配置

    4.4 限制下载速度

    image.png

    5. 基于Redis和Lua的分布式限流

    5.1 为什么选择redis

  14. 性能:Redis作为缓存组件,如果不考虑持久化方案的话,Redis的大部分操作都是基于内存的

  15. 线程安全:只用单线程承接网络请求,其它模块任然是多线程,天然具有线程安全的特性,且对原子性的操作支持非常到位
  16. 限流服务不仅需要承担超高的QPS,还要保证限流层面具备线程安全的特性,所以利用Redis的天然特性做限流既能保证线程安全,也能保持良好性能

    5.2 架构模式

    5.2.1 流程架构

    image.png

  17. 访问请求:需要被限流的对象

  18. 限流规则:定义一段程序或者脚本,当请求来的时候执行
  19. 存储介质:用来存储限流信息的地方,比如令牌的个数或者访问请求的计数

    5.2.2 问题:限流逻辑应该放在哪里?

    5.2.2.1 放在java程序中

  20. 性能压力:代码中执行一段限流的程序并不会带来很大的性能压力

  21. 网络开销
    1. 在一个限流逻辑中,往往需要发起多个Redis命令和修改指令
    2. 比如获取令牌这一步,就涉及到查询令牌、发放令牌等步骤
    3. 这些步骤发起的Redis请求指令,造成了更多的网络开销
  22. 线程安全
    1. 上述的操作还需要保证线程安全
    2. 如此一来,在程序中就会涉及资源锁定等复杂操作

      5.2.2.2 为什么使用Lua

      Lua脚本语言

image.png

  1. -- 不同的请求传入不同的key,这样才知道你的请求是来自同一个服务,还是不同的服务
  2. -- 这里声明一个本地的变量key,它其实应该是外面传递的,这里模拟
  3. -- 用作限流的key
  4. local key = "lua key"
  5. -- 当前限流规定的上线是多少
  6. -- 限流的最大阈值
  7. local limit = 2
  8. -- 当前的流量大小
  9. local current_limit = 2
  10. -- 判断逻辑: 超出了为false,没超过是true
  11. if current_limit + 1 > limit then
  12. print "reject"
  13. return false
  14. else
  15. print "accept"
  16. return true
  17. end

5.3 Redis预编译Lua

先通过Java程序将Lua脚本上传到Redis进行编译,后续的流程只需要使用编译好的脚本即可

  1. 第一个是预加载
  2. 第二个是执行
  3. 总的来说就是生成一个脚本ID,后续执行都是使用脚本ID+传递的参数

image.png

5.4 限流组件封装

最终效果:通过在方法上加注解即可对这个方法实现限流,借用AOP机制

5.5 SpringCloud Gateway组件的限流脚本

  1. local tokens_key = KEYS[1]
  2. local timestamp_key = KEYS[2]
  3. --redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)
  4. local rate = tonumber(ARGV[1])
  5. local capacity = tonumber(ARGV[2])
  6. local now = tonumber(ARGV[3])
  7. local requested = tonumber(ARGV[4])
  8. local fill_time = capacity/rate
  9. local ttl = math.floor(fill_time*2)
  10. --redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
  11. --redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
  12. --redis.log(redis.LOG_WARNING, "now " .. ARGV[3])
  13. --redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
  14. --redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
  15. --redis.log(redis.LOG_WARNING, "ttl " .. ttl)
  16. local last_tokens = tonumber(redis.call("get", tokens_key))
  17. if last_tokens == nil then
  18. last_tokens = capacity
  19. end
  20. --redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)
  21. local last_refreshed = tonumber(redis.call("get", timestamp_key))
  22. if last_refreshed == nil then
  23. last_refreshed = 0
  24. end
  25. --redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)
  26. local delta = math.max(0, now-last_refreshed)
  27. local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
  28. local allowed = filled_tokens >= requested
  29. local new_tokens = filled_tokens
  30. local allowed_num = 0
  31. if allowed then
  32. new_tokens = filled_tokens - requested
  33. allowed_num = 1
  34. end
  35. --redis.log(redis.LOG_WARNING, "delta " .. delta)
  36. --redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
  37. --redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
  38. --redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)
  39. redis.call("setex", tokens_key, ttl, new_tokens)
  40. redis.call("setex", timestamp_key, ttl, now)
  41. return { allowed_num, new_tokens }