限流:计数器、漏桶、令牌桶 三大算法的原理与实战(史上最全) ...

打印 上一主题 下一主题

主题 580|帖子 580|积分 1740

限流

限流是面试中的常见的面试题(尤其是大厂面试、高P面试)


   注:本文以 PDF 连续更新,最新尼恩 架构条记、面试题 的PDF文件,请到文末《技能自由圈》公号获取
  为什么要限流

   简朴来说:
  限流在很多场景中用来限制并发和请求量,好比说秒杀抢购,保护自身材系和下游体系不被巨型流量冲垮等。
  以微博为例,例如某某明星公布了恋情,访问从寻常的50万增加到了500万,体系的规划能力,最多可以支撑200万访问,那么就要执行限流规则,包管是一个可用的状态,不至于服务器崩溃,所有请求不可用。
参考图谱

体系架构知识图谱(一张价值10w的体系架构知识图谱)
https://www.processon.com/view/link/60fb9421637689719d246739
秒杀体系的架构
https://www.processon.com/view/link/61148c2b1e08536191d8f92f
限流的思想

   在包管可用的情况下尽可能多增加进入的人数,其余的人在排队等待,或者返回友好提示,包管里面的进行体系的用户可以正常使用,防止体系雪崩。
  日常生活中,有哪些需要限流的地方?

像我旁边有一个国家景区,寻常可能根本没什么人前往,但是一到五一或者春节就人满为患,这时候景区管理人员就会实行一系列的政策来限制进入人流量,
为什么要限流呢?
假如景区能容纳一万人,如今进去了三万人,势必摩肩接踵,整欠好还会有变乱发生,这样的结果就是所有人的体验都欠好,假如发生了变乱景区可能还要关闭,导致对外不可用,这样的后果就是所有人都觉得体验糟糕透了。
限流的算法

限流算法很多,常见的有三类,分别是计数器算法、漏桶算法、令牌桶算法,下面逐一解说。
限流的手段通常有计数器、漏桶、令牌桶。注意限流和限速(所有请求都会处理处罚)的差异,视
业务场景而定。
(1)计数器:
在一段时间间隔内(时间窗/时间区间),处理处罚请求的最大数量固定,凌驾部分不做处理处罚。
(2)漏桶:
漏桶大小固定,处理处罚速率固定,但请求进入速率不固定(在突发情况请求过多时,会抛弃过多的请求)。
(3)令牌桶:
令牌桶的大小固定,令牌的产生速率固定,但是消耗令牌(即请求)速率不固定(可以应对一些某些时间请求过多的情况);每个请求都会从令牌桶中取出令牌,假如没有令牌则抛弃该次请求。
计数器算法

计数器限流定义:

在一段时间间隔内(时间窗/时间区间),处理处罚请求的最大数量固定,凌驾部分不做处理处罚。
简朴粗暴,好比指定线程池大小,指定命据库连接池大小、nginx连接数等,这都属于计数器算法。
计数器算法是限流算法里最简朴也是最容易实现的一种算法。
举个例子,好比我们规定对于A接口,我们1分钟的访问次数不能凌驾100个。
那么我们可以这么做:


  • 在一开 始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,假如counter的值大于100并且该请求与第一个请求的间隔时间还在1分钟之内,那么说明请求数过多,拒绝访问;
  • 假如该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter,就是这么简朴粗暴。

盘算器限流的实现

  1. package com.crazymaker.springcloud.ratelimit;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.junit.Test;
  4. import java.util.concurrent.CountDownLatch;
  5. import java.util.concurrent.ExecutorService;
  6. import java.util.concurrent.Executors;
  7. import java.util.concurrent.atomic.AtomicInteger;
  8. import java.util.concurrent.atomic.AtomicLong;
  9. // 计速器 限速
  10. @Slf4j
  11. public class CounterLimiter
  12. {
  13.     // 起始时间
  14.     private static long startTime = System.currentTimeMillis();
  15.     // 时间区间的时间间隔 ms
  16.     private static long interval = 1000;
  17.     // 每秒限制数量
  18.     private static long maxCount = 2;
  19.     //累加器
  20.     private static AtomicLong accumulator = new AtomicLong();
  21.     // 计数判断, 是否超出限制
  22.     private static long tryAcquire(long taskId, int turn)
  23.     {
  24.         long nowTime = System.currentTimeMillis();
  25.         //在时间区间之内
  26.         if (nowTime < startTime + interval)
  27.         {
  28.             long count = accumulator.incrementAndGet();
  29.             if (count <= maxCount)
  30.             {
  31.                 return count;
  32.             } else
  33.             {
  34.                 return -count;
  35.             }
  36.         } else
  37.         {
  38.             //在时间区间之外
  39.             synchronized (CounterLimiter.class)
  40.             {
  41.                 log.info("新时间区到了,taskId{}, turn {}..", taskId, turn);
  42.                 // 再一次判断,防止重复初始化
  43.                 if (nowTime > startTime + interval)
  44.                 {
  45.                     accumulator.set(0);
  46.                     startTime = nowTime;
  47.                 }
  48.             }
  49.             return 0;
  50.         }
  51.     }
  52.     //线程池,用于多线程模拟测试
  53.     private ExecutorService pool = Executors.newFixedThreadPool(10);
  54.     @Test
  55.     public void testLimit()
  56.     {
  57.         // 被限制的次数
  58.         AtomicInteger limited = new AtomicInteger(0);
  59.         // 线程数
  60.         final int threads = 2;
  61.         // 每条线程的执行轮数
  62.         final int turns = 20;
  63.         // 同步器
  64.         CountDownLatch countDownLatch = new CountDownLatch(threads);
  65.         long start = System.currentTimeMillis();
  66.         for (int i = 0; i < threads; i++)
  67.         {
  68.             pool.submit(() ->
  69.             {
  70.                 try
  71.                 {
  72.                     for (int j = 0; j < turns; j++)
  73.                     {
  74.                         long taskId = Thread.currentThread().getId();
  75.                         long index = tryAcquire(taskId, j);
  76.                         if (index <= 0)
  77.                         {
  78.                             // 被限制的次数累积
  79.                             limited.getAndIncrement();
  80.                         }
  81.                         Thread.sleep(200);
  82.                     }
  83.                 } catch (Exception e)
  84.                 {
  85.                     e.printStackTrace();
  86.                 }
  87.                 //等待所有线程结束
  88.                 countDownLatch.countDown();
  89.             });
  90.         }
  91.         try
  92.         {
  93.             countDownLatch.await();
  94.         } catch (InterruptedException e)
  95.         {
  96.             e.printStackTrace();
  97.         }
  98.         float time = (System.currentTimeMillis() - start) / 1000F;
  99.         //输出统计结果
  100.         log.info("限制的次数为:" + limited.get() +
  101.                 ",通过的次数为:" + (threads * turns - limited.get()));
  102.         log.info("限制的比例为:" + (float) limited.get() / (float) (threads * turns));
  103.         log.info("运行的时长为:" + time);
  104.     }
  105. }
复制代码
计数器限流的严重问题

这个算法虽然简朴,但是有一个非常致命的问题,那就是临界问题,我们看下图:

从上图中我们可以看到,假设有一个恶意用户,他在0:59时,刹时发送了100个请求,并且1:00又刹时发送了100个请求,那么其实这个用户在 1秒里面,刹时发送了200个请求。
我们刚才规定的是1分钟最多100个请求(规划的吞吐量),也就是每秒钟最多1.7个请求,用户通过在时间窗口的重置节点处突发请求, 可以刹时凌驾我们的速率限制。
用户有可能通过算法的这个漏洞,刹时压垮我们的应用。
   说明:本文会连续更新,更多最新尼恩3高条记PDF,请从下面的链接获取: 码云
  漏桶算法

漏桶算法限流的根本原理为:水(对应请求)从进水口进入到漏桶里,漏桶以肯定的速率出水(请求放行),当水流入速率过大,桶内的总水量大于桶容量会直接溢出,请求被拒绝,如图所示。
大致的漏桶限流规则如下:
(1)进水口(对应客户端请求)以任意速率流入进入漏桶。
(2)漏桶的容量是固定的,出水(放行)速率也是固定的。
(3)漏桶容量是稳定的,假如处理处罚速率太慢,桶内水量会超出了桶的容量,则背面流入的水滴会溢出,表示请求拒绝。
漏桶算法原理

漏桶算法思路很简朴:
   水(请求)先辈入到漏桶里,漏桶以肯定的速率出水,当水流入速率过大会凌驾桶可采取的容量时直接溢出。
  可以看出漏桶算法能强行限制数据的传输速率。

漏桶算法其实很简朴,可以粗略的以为就是注水漏水过程,往桶中以任意速率流入水,以肯定速率流出水,当水凌驾桶容量(capacity)则抛弃,因为桶容量是稳定的,包管了团体的速率。
以肯定速率流出水,

削峰:有大量流量进入时,会发生溢出,从而限流保护服务可用
缓冲:不至于直接请求到服务器,缓冲压力
消耗速率固定 因为盘算性能固定
漏桶算法实现

  1. package com.crazymaker.springcloud.ratelimit;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.junit.Test;
  4. import java.util.concurrent.CountDownLatch;
  5. import java.util.concurrent.ExecutorService;
  6. import java.util.concurrent.Executors;
  7. import java.util.concurrent.atomic.AtomicInteger;
  8. // 漏桶 限流
  9. @Slf4j
  10. public class LeakBucketLimiter {
  11.     // 计算的起始时间
  12.     private static long lastOutTime = System.currentTimeMillis();
  13.     // 流出速率 每秒 2 次
  14.     private static int leakRate = 2;
  15.     // 桶的容量
  16.     private static int capacity = 2;
  17.     //剩余的水量
  18.     private static AtomicInteger water = new AtomicInteger(0);
  19.     //返回值说明:
  20.     // false 没有被限制到
  21.     // true 被限流
  22.     public static synchronized boolean isLimit(long taskId, int turn) {
  23.         // 如果是空桶,就当前时间作为漏出的时间
  24.         if (water.get() == 0) {
  25.             lastOutTime = System.currentTimeMillis();
  26.             water.addAndGet(1);
  27.             return false;
  28.         }
  29.         // 执行漏水
  30.         int waterLeaked = ((int) ((System.currentTimeMillis() - lastOutTime) / 1000)) * leakRate;
  31.         // 计算剩余水量
  32.         int waterLeft = water.get() - waterLeaked;
  33.         water.set(Math.max(0, waterLeft));
  34.         // 重新更新leakTimeStamp
  35.         lastOutTime = System.currentTimeMillis();
  36.         // 尝试加水,并且水还未满 ,放行
  37.         if ((water.get()) < capacity) {
  38.             water.addAndGet(1);
  39.             return false;
  40.         } else {
  41.             // 水满,拒绝加水, 限流
  42.             return true;
  43.         }
  44.     }
  45.     //线程池,用于多线程模拟测试
  46.     private ExecutorService pool = Executors.newFixedThreadPool(10);
  47.     @Test
  48.     public void testLimit() {
  49.         // 被限制的次数
  50.         AtomicInteger limited = new AtomicInteger(0);
  51.         // 线程数
  52.         final int threads = 2;
  53.         // 每条线程的执行轮数
  54.         final int turns = 20;
  55.         // 线程同步器
  56.         CountDownLatch countDownLatch = new CountDownLatch(threads);
  57.         long start = System.currentTimeMillis();
  58.         for (int i = 0; i < threads; i++) {
  59.             pool.submit(() ->
  60.             {
  61.                 try {
  62.                     for (int j = 0; j < turns; j++) {
  63.                         long taskId = Thread.currentThread().getId();
  64.                         boolean intercepted = isLimit(taskId, j);
  65.                         if (intercepted) {
  66.                             // 被限制的次数累积
  67.                             limited.getAndIncrement();
  68.                         }
  69.                         Thread.sleep(200);
  70.                     }
  71.                 } catch (Exception e) {
  72.                     e.printStackTrace();
  73.                 }
  74.                 //等待所有线程结束
  75.                 countDownLatch.countDown();
  76.             });
  77.         }
  78.         try {
  79.             countDownLatch.await();
  80.         } catch (InterruptedException e) {
  81.             e.printStackTrace();
  82.         }
  83.         float time = (System.currentTimeMillis() - start) / 1000F;
  84.         //输出统计结果
  85.         log.info("限制的次数为:" + limited.get() +
  86.                 ",通过的次数为:" + (threads * turns - limited.get()));
  87.         log.info("限制的比例为:" + (float) limited.get() / (float) (threads * turns));
  88.         log.info("运行的时长为:" + time);
  89.     }
  90. }
复制代码
漏桶的问题

漏桶的出水速率固定,也就是请求放行速率是固定的。
网上抄来抄去的说法:
   漏桶不能有效应对突发流量,但是能起到平滑突发流量(整流)的作用。
  现实上的问题:
   漏桶出口的速率固定,不能机动的应对后端能力提拔。好比,通过动态扩容,后端流量从1000QPS提拔到1WQPS,漏桶没有办法。
  令牌桶限流

令牌桶算法以一个设定的速率产生令牌并放入令牌桶,每次用户请求都得申请令牌,假如令牌不足,则拒绝请求。
令牌桶算法中新请求到来时会从桶里拿走一个令牌,假如桶内没有令牌可拿,就拒绝服务。当然,令牌的数量也是有上限的。令牌的数量与时间和发放速率强相干,时间流逝的时间越长,会不绝往桶里加入越多的令牌,假如令牌发放的速率比申请速率快,令牌桶会放满令牌,直到令牌占满整个令牌桶,如图所示。
令牌桶限流大致的规则如下:
(1)进水口按照某个速率,向桶中放入令牌。
(2)令牌的容量是固定的,但是放行的速率不是固定的,只要桶中还有剩余令牌,一旦请求过来就能申请成功,然后放行。
(3)假如令牌的发放速率,慢于请求到来速率,桶内就无牌可领,请求就会被拒绝。
总之,令牌的发送速率可以设置,从而可以对突发的出口流量进行有效的应对。
令牌桶算法

令牌桶与漏桶相似,差别的是令牌桶桶中放了一些令牌,服务请求到达后,要获取令牌之后才会得到服务,举个例子,我们寻常去食堂用饭,都是在食堂内窗口前排队的,这就好比是漏桶算法,大量的人员聚集在食堂内窗口外,以肯定的速率享受服务,假如涌进来的人太多,食堂装不下了,可能就有一部分人站到食堂外了,这就没有享受到食堂的服务,称之为溢出,溢出可以继承请求,也就是继承排队,那么这样有什么问题呢?
假如这时候有特殊情况,好比有些赶时间的志愿者啦、或者高三要高考啦,这种情况就是突发情况,假如也用漏桶算法那也得逐步排队,这也就没有解决我们的需求,对于很多应用场景来说,除了要求可以或许限制数据的平均传输速率外,还要求答应某种程度的突发传输。这时候漏桶算法可能就不符合了,令牌桶算法更为适合。如图所示,令牌桶算法的原理是体系会以一个恒定的速率往桶里放入令牌,而假如请求需要被处理处罚,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。


令牌桶算法实现

  1. package com.crazymaker.springcloud.ratelimit;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.junit.Test;
  4. import java.util.concurrent.CountDownLatch;
  5. import java.util.concurrent.ExecutorService;
  6. import java.util.concurrent.Executors;
  7. import java.util.concurrent.atomic.AtomicInteger;
  8. // 令牌桶 限速
  9. @Slf4j
  10. public class TokenBucketLimiter {
  11.     // 上一次令牌发放时间
  12.     public long lastTime = System.currentTimeMillis();
  13.     // 桶的容量
  14.     public int capacity = 2;
  15.     // 令牌生成速度 /s
  16.     public int rate = 2;
  17.     // 当前令牌数量
  18.     public AtomicInteger tokens = new AtomicInteger(0);
  19.     ;
  20.     //返回值说明:
  21.     // false 没有被限制到
  22.     // true 被限流
  23.     public synchronized boolean isLimited(long taskId, int applyCount) {
  24.         long now = System.currentTimeMillis();
  25.         //时间间隔,单位为 ms
  26.         long gap = now - lastTime;
  27.         //计算时间段内的令牌数
  28.         int reverse_permits = (int) (gap * rate / 1000);
  29.         int all_permits = tokens.get() + reverse_permits;
  30.         // 当前令牌数
  31.         tokens.set(Math.min(capacity, all_permits));
  32.         log.info("tokens {} capacity {} gap {} ", tokens, capacity, gap);
  33.         if (tokens.get() < applyCount) {
  34.             // 若拿不到令牌,则拒绝
  35.             // log.info("被限流了.." + taskId + ", applyCount: " + applyCount);
  36.             return true;
  37.         } else {
  38.             // 还有令牌,领取令牌
  39.             tokens.getAndAdd( - applyCount);
  40.             lastTime = now;
  41.             // log.info("剩余令牌.." + tokens);
  42.             return false;
  43.         }
  44.     }
  45.     //线程池,用于多线程模拟测试
  46.     private ExecutorService pool = Executors.newFixedThreadPool(10);
  47.     @Test
  48.     public void testLimit() {
  49.         // 被限制的次数
  50.         AtomicInteger limited = new AtomicInteger(0);
  51.         // 线程数
  52.         final int threads = 2;
  53.         // 每条线程的执行轮数
  54.         final int turns = 20;
  55.         // 同步器
  56.         CountDownLatch countDownLatch = new CountDownLatch(threads);
  57.         long start = System.currentTimeMillis();
  58.         for (int i = 0; i < threads; i++) {
  59.             pool.submit(() ->
  60.             {
  61.                 try {
  62.                     for (int j = 0; j < turns; j++) {
  63.                         long taskId = Thread.currentThread().getId();
  64.                         boolean intercepted = isLimited(taskId, 1);
  65.                         if (intercepted) {
  66.                             // 被限制的次数累积
  67.                             limited.getAndIncrement();
  68.                         }
  69.                         Thread.sleep(200);
  70.                     }
  71.                 } catch (Exception e) {
  72.                     e.printStackTrace();
  73.                 }
  74.                 //等待所有线程结束
  75.                 countDownLatch.countDown();
  76.             });
  77.         }
  78.         try {
  79.             countDownLatch.await();
  80.         } catch (InterruptedException e) {
  81.             e.printStackTrace();
  82.         }
  83.         float time = (System.currentTimeMillis() - start) / 1000F;
  84.         //输出统计结果
  85.         log.info("限制的次数为:" + limited.get() +
  86.                 ",通过的次数为:" + (threads * turns - limited.get()));
  87.         log.info("限制的比例为:" + (float) limited.get() / (float) (threads * turns));
  88.         log.info("运行的时长为:" + time);
  89.     }
  90. }
复制代码
令牌桶的好处

令牌桶的好处之一就是可以方便地应对 突发出口流量(后端能力的提拔)。
好比,可以改变令牌的发放速率,算法能按照新的发送速率调大令牌的发放数量,使得出口突发流量能被处理处罚。
Guava RateLimiter

Guava是Java领域优秀的开源项目,它包含了Google在Java项目中使用一些核心库,包含聚集(Collections),缓存(Caching),并发编程库(Concurrency),常用注解(Common annotations),String操作,I/O操作方面的众多非常实用的函数。 Guava的 RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)宁静滑预热限流(SmoothWarmingUp)实现。

RateLimiter的类图如上所示,
Nginx漏桶限流

Nginx限流的简朴演示

每六秒才处理处罚一次请求,如下
  1.   limit_req_zone  $arg_sku_id  zone=skuzone:10m      rate=6r/m;
  2.   limit_req_zone  $http_user_id  zone=userzone:10m      rate=6r/m;
  3.   limit_req_zone  $binary_remote_addr  zone=perip:10m      rate=6r/m;
  4.   limit_req_zone  $server_name        zone=perserver:1m   rate=6r/m;
复制代码
这是从请求参数里边,提前参数做限流

这是从请求参数里边,提前参数,进行限流的次数统计key。
在http块里边定义限流的内存区域 zone。
  1.   limit_req_zone  $arg_sku_id  zone=skuzone:10m      rate=6r/m;
  2.   limit_req_zone  $http_user_id  zone=userzone:10m      rate=6r/m;
  3.   limit_req_zone  $binary_remote_addr  zone=perip:10m      rate=6r/m;
  4.   limit_req_zone  $server_name        zone=perserver:1m   rate=10r/s;
复制代码
在location块中使用 限流zone,参考如下:
  1. #  ratelimit by sku id
  2.     location  = /ratelimit/sku {
  3.       limit_req  zone=skuzone;
  4.       echo "正常的响应";
  5. }
复制代码
测试
  1. [root@cdh1 ~]# /vagrant/LuaDemoProject/sh/linux/openresty-restart.sh
  2. shell dir is: /vagrant/LuaDemoProject/sh/linux
  3. Shutting down openrestry/nginx:  pid is 13479 13485
  4. Shutting down  succeeded!
  5. OPENRESTRY_PATH:/usr/local/openresty
  6. PROJECT_PATH:/vagrant/LuaDemoProject/src
  7. nginx: [alert] lua_code_cache is off; this will hurt performance in /vagrant/LuaDemoProject/src/conf/nginx-seckill.conf:90
  8. openrestry/nginx starting succeeded!
  9. pid is 14197
  10. [root@cdh1 ~]# curl  http://cdh1/ratelimit/sku?sku_id=1
  11. 正常的响应
  12. root@cdh1 ~]#  curl  http://cdh1/ratelimit/sku?sku_id=1
  13. 正常的响应
  14. [root@cdh1 ~]#  curl  http://cdh1/ratelimit/sku?sku_id=1
  15. 限流后的降级内容
  16. [root@cdh1 ~]#  curl  http://cdh1/ratelimit/sku?sku_id=1
  17. 限流后的降级内容
  18. [root@cdh1 ~]#  curl  http://cdh1/ratelimit/sku?sku_id=1
  19. 限流后的降级内容
  20. [root@cdh1 ~]#  curl  http://cdh1/ratelimit/sku?sku_id=1
  21. 限流后的降级内容
  22. [root@cdh1 ~]#  curl  http://cdh1/ratelimit/sku?sku_id=1
  23. 限流后的降级内容
  24. [root@cdh1 ~]#  curl  http://cdh1/ratelimit/sku?sku_id=1
  25. 正常的响应
复制代码
从Header头部提前参数

1、nginx是支持读取非nginx标准的用户自定义header的,但是需要在http或者server下开启header的下划线支持:
underscores_in_headers on;
2、好比我们自定义header为X-Real-IP,通过第二个nginx获取该header时需要这样:
$http_x_real_ip; (一律采取小写,而且前面多了个http_)
  1. underscores_in_headers on;
  2.   limit_req_zone  $http_user_id  zone=userzone:10m      rate=6r/m;
  3.   server {
  4.     listen       80 default;
  5.     server_name  nginx.server *.nginx.server;
  6.     default_type 'text/html';
  7.     charset utf-8;
  8. #  ratelimit by user id
  9.     location  = /ratelimit/demo {
  10.       limit_req  zone=userzone;
  11.       echo "正常的响应";
  12.     }
  13.   
  14.     location = /50x.html{
  15.       echo "限流后的降级内容";
  16.     }
  17.     error_page 502 503 =200 /50x.html;
  18.   }
复制代码
测试
  1. [root@cdh1 ~]# curl -H "USER-ID:1" http://cdh1/ratelimit/demo
  2. 正常的响应
  3. [root@cdh1 ~]# curl -H "USER-ID:1" http://cdh1/ratelimit/demo
  4. 限流后的降级内容
  5. [root@cdh1 ~]# curl -H "USER-ID:1" http://cdh1/ratelimit/demo
  6. 限流后的降级内容
  7. [root@cdh1 ~]# curl -H "USER-ID:1" http://cdh1/ratelimit/demo
  8. 限流后的降级内容
  9. [root@cdh1 ~]# curl -H "USER-ID:1" http://cdh1/ratelimit/demo
  10. 限流后的降级内容
  11. [root@cdh1 ~]# curl -H "USER-ID:1" http://cdh1/ratelimit/demo
  12. 限流后的降级内容
  13. [root@cdh1 ~]# curl -H "USER-ID:1" http://cdh1/ratelimit/demo
  14. 限流后的降级内容
  15. [root@cdh1 ~]# curl -H "USER_ID:2" http://cdh1/ratelimit/demo
  16. 正常的响应
  17. [root@cdh1 ~]# curl -H "USER_ID:2" http://cdh1/ratelimit/demo
  18. 限流后的降级内容
  19. [root@cdh1 ~]#
  20. [root@cdh1 ~]# curl -H "USER_ID:2" http://cdh1/ratelimit/demo
  21. 限流后的降级内容
  22. [root@cdh1 ~]# curl -H "USER-ID:3" http://cdh1/ratelimit/demo
  23. 正常的响应
  24. [root@cdh1 ~]# curl -H "USER-ID:3" http://cdh1/ratelimit/demo
  25. 限流后的降级内容
复制代码
Nginx漏桶限流的三个细分类型,即burst、nodelay参数详解

每六秒才处理处罚一次请求,如下
  1. limit_req_zone  $arg_user_id  zone=limti_req_zone:10m      rate=10r/m;
复制代码
不带缓冲队列的漏桶限流

limit_req zone=limti_req_zone;


  • 严格依照在limti_req_zone中设置的rate来处理处罚请求
  • 凌驾rate处理处罚能力范围的,直接drop
  • 表现为对收到的请求无延时
   假设1秒内提交10个请求,可以看到一共10个请求,9个请求都失败了,直接返回503,
  接着再检察 /var/log/nginx/access.log,印证了只有一个请求成功了,其它就是都直接返回了503,即服务器拒绝了请求。

带缓冲队列的漏桶限流

limit_req zone=limti_req_zone burst=5;


  • 依照在limti_req_zone中设置的rate来处理处罚请求
  • 同时设置了一个大小为5的缓冲队列,在缓冲队列中的请求会等待逐步处理处罚
  • 凌驾了burst缓冲队列长度和rate处理处罚能力的请求被直接抛弃
  • 表现为对收到的请求有延时
   假设1秒内提交10个请求,则可以发如今1s内,**在服务器吸收到10个并发请求后,先处理处罚1个请求,同时将5个请求放入burst缓冲队列中,等待处理处罚。而凌驾(burst+1)数量的请求就被直接抛弃了,即直接抛弃了4个请求。**burst缓存的5个请求每隔6s处理处罚一次。
  接着检察 /var/log/nginx/access.log日志

带瞬时处理处罚能力的漏桶限流

limit_req zone=req_zone burst=5 nodelay;
假如设置nodelay,会在瞬时提供处理处罚(burst + rate)个请求的能力,请求数量凌驾**(burst + rate)**的时候就会直接返回503,峰值范围内的请求,不存在请求需要等待的情况
   假设1秒内提交10个请求,则可以发如今1s内,服务器端处理处罚了6个请求(峰值速率:burst+10s内一个请求)。对于剩下的4个请求,直接返回503,在下一秒假如继承向服务端发送10个请求,服务端会直接拒绝这10个请求并返回503。
  接着检察 /var/log/nginx/access.log日志

可以发如今1s内,服务器端处理处罚了6个请求(峰值速率:burst+原来的处理处罚速率)。对于剩下的4个请求,直接返回503。
   但是,总数额度和速率*时间保持一致, 就是额度用完了,需要比及一个有额度的时间段,才开始吸收新的请求。假如一次处理处罚了5个请求,相称于占了30s的额度,6*5=30。因为设定了6s处理处罚1个请求,所以直到30
s 之后,才可以再处理处罚一个请求,即假如此时向服务端发送10个请求,会返回9个503,一个200
    说明:本文会以pdf格式连续更新,更多最新尼恩3高pdf条记,请从下面的链接获取:语雀 或者 码云
  分布式限流组件

why

   但是Nginx的限流指令只能在同一块内存区域有效,而在生产场景中秒杀的外部网关每每是多节点部署,所以这就需要用到分布式限流组件。
  高性能的分布式限流组件可以使用Redis+Lua来开发,京东的抢购就是使用Redis+Lua完成的限流。并且无论是Nginx外部网关还是Zuul内部网关,都可以使用Redis+Lua限流组件。
理论上,接入层的限流有多个维度:
(1)用户维度限流:在某一时间段内只答应用户提交一次请求,好比可以采取客户端IP或者用户ID作为限流的key。
(2)商品维度的限流:对于同一个抢购商品,在某个时间段内只答应肯定命量的请求进入,可以采取秒杀商品ID作为限流的key。
什么时候用nginx限流:
用户维度的限流,可以在ngix 上进行,因为使用nginx限流内存来存储用户id,比用redis 的key,来存储用户id,服从高。
什么时候用redis+lua分布式限流:
商品维度的限流,可以在redis上进行,不需要大量的盘算访问次数的key,另外,可以控制所有的接入层节点的访问秒杀请求的总量。
redis+lua分布式限流组件

  1. --- 此脚本的环境: redis 内部,不是运行在 nginx 内部
  2. ---方法:申请令牌
  3. --- -1 failed
  4. --- 1 success
  5. --- @param key key 限流关键字
  6. --- @param apply  申请的令牌数量
  7. local function acquire(key, apply)
  8.     local times = redis.call('TIME');
  9.     -- times[1] 秒数   -- times[2] 微秒数
  10.     local curr_mill_second = times[1] * 1000000 + times[2];
  11.     curr_mill_second = curr_mill_second / 1000;
  12.     local cacheInfo = redis.pcall("HMGET", key, "last_mill_second", "curr_permits", "max_permits", "rate")
  13.     --- 局部变量:上次申请的时间
  14.     local last_mill_second = cacheInfo[1];
  15.     --- 局部变量:之前的令牌数
  16.     local curr_permits = tonumber(cacheInfo[2]);
  17.     --- 局部变量:桶的容量
  18.     local max_permits = tonumber(cacheInfo[3]);
  19.     --- 局部变量:令牌的发放速率
  20.     local rate = cacheInfo[4];
  21.     --- 局部变量:本次的令牌数
  22.     local local_curr_permits = 0;
  23.     if (type(last_mill_second) ~= 'boolean' and last_mill_second ~= nil) then
  24.         -- 计算时间段内的令牌数
  25.         local reverse_permits = math.floor(((curr_mill_second - last_mill_second) / 1000) * rate);
  26.         -- 令牌总数
  27.         local expect_curr_permits = reverse_permits + curr_permits;
  28.         -- 可以申请的令牌总数
  29.         local_curr_permits = math.min(expect_curr_permits, max_permits);
  30.     else
  31.         -- 第一次获取令牌
  32.         redis.pcall("HSET", key, "last_mill_second", curr_mill_second)
  33.         local_curr_permits = max_permits;
  34.     end
  35.     local result = -1;
  36.     -- 有足够的令牌可以申请
  37.     if (local_curr_permits - apply >= 0) then
  38.         -- 保存剩余的令牌
  39.         redis.pcall("HSET", key, "curr_permits", local_curr_permits - apply);
  40.         -- 为下次的令牌获取,保存时间
  41.         redis.pcall("HSET", key, "last_mill_second", curr_mill_second)
  42.         -- 返回令牌获取成功
  43.         result = 1;
  44.     else
  45.         -- 返回令牌获取失败
  46.         result = -1;
  47.     end
  48.     return result
  49. end
  50. --eg
  51. -- /usr/local/redis/bin/redis-cli  -a 123456  --eval   /vagrant/LuaDemoProject/src/luaScript/redis/rate_limiter.lua key , acquire 1  1
  52. -- 获取 sha编码的命令
  53. -- /usr/local/redis/bin/redis-cli  -a 123456  script load "$(cat  /vagrant/LuaDemoProject/src/luaScript/redis/rate_limiter.lua)"
  54. -- /usr/local/redis/bin/redis-cli  -a 123456  script exists  "cf43613f172388c34a1130a760fc699a5ee6f2a9"
  55. -- /usr/local/redis/bin/redis-cli -a 123456  evalsha   "cf43613f172388c34a1130a760fc699a5ee6f2a9" 1 "rate_limiter:seckill:1"  init 1  1
  56. -- /usr/local/redis/bin/redis-cli -a 123456  evalsha   "cf43613f172388c34a1130a760fc699a5ee6f2a9" 1 "rate_limiter:seckill:1"  acquire 1
  57. --local rateLimiterSha = "e4e49e4c7b23f0bf7a2bfee73e8a01629e33324b";
  58. ---方法:初始化限流 Key
  59. --- 1 success
  60. --- @param key key
  61. --- @param max_permits  桶的容量
  62. --- @param rate  令牌的发放速率
  63. local function init(key, max_permits, rate)
  64.     local rate_limit_info = redis.pcall("HMGET", key, "last_mill_second", "curr_permits", "max_permits", "rate")
  65.     local org_max_permits = tonumber(rate_limit_info[3])
  66.     local org_rate = rate_limit_info[4]
  67.     if (org_max_permits == nil) or (rate ~= org_rate or max_permits ~= org_max_permits) then
  68.         redis.pcall("HMSET", key, "max_permits", max_permits, "rate", rate, "curr_permits", max_permits)
  69.     end
  70.     return 1;
  71. end
  72. --eg
  73. -- /usr/local/redis/bin/redis-cli -a 123456 --eval   /vagrant/LuaDemoProject/src/luaScript/redis/rate_limiter.lua key , init 1  1
  74. -- /usr/local/redis/bin/redis-cli -a 123456 --eval   /vagrant/LuaDemoProject/src/luaScript/redis/rate_limiter.lua  "rate_limiter:seckill:1"  , init 1  1
  75. ---方法:删除限流 Key
  76. local function delete(key)
  77.     redis.pcall("DEL", key)
  78.     return 1;
  79. end
  80. --eg
  81. -- /usr/local/redis/bin/redis-cli  --eval   /vagrant/LuaDemoProject/src/luaScript/redis/rate_limiter.lua key , delete
  82. local key = KEYS[1]
  83. local method = ARGV[1]
  84. if method == 'acquire' then
  85.     return acquire(key, ARGV[2], ARGV[3])
  86. elseif method == 'init' then
  87.     return init(key, ARGV[2], ARGV[3])
  88. elseif method == 'delete' then
  89.     return delete(key)
  90. else
  91.     --ignore
  92. end
复制代码
  在redis中,为了避免重复发送脚本数据浪费网络资源,可以使用script load命令进行脚本数据缓存,并且返回一个哈希码作为脚本的调用句柄,
  每次调用脚本只需要发送哈希码来调用即可。
  分布式令牌限流实战

可以使用redis+lua,实战一票下边的简朴案例:
令牌按照1个每秒的速率放入令牌桶,桶中最多存放2个令牌,那体系就只会答应连续的每秒处理处罚2个请求,
或者每隔2 秒,等桶中2 个令牌攒满后,一次处理处罚2个请求的突发情况,包管体系稳定性。
商品维度的限流

当秒杀商品维度的限流,当商品的流量,远远大于涉及的流量时,开始随机抛弃请求。
Nginx的令牌桶限流脚本getToken_access_limit.lua执行在请求的access阶段,但是,该脚本并没有实现限流的核心逻辑,仅仅调用缓存在Redis内部的rate_limiter.lua脚本进行限流。
getToken_access_limit.lua脚本和rate_limiter.lua脚本的关系,具体如图10-17所示。

图10-17 getToken_access_limit.lua脚本和rate_limiter.lua脚本关系
什么时候在Redis中加载rate_limiter.lua脚本呢?
和秒杀脚本一样,该脚本是在Java程序启动商品秒杀时,完成其在Redis的加载和缓存的。
还有一点非常紧张,Java程序会将脚本加载完成之后的sha1编码,去通过自定义的key(具体为"lua:sha1:rate_limiter")缓存在Redis中,以方便Nginx的getToken_access_limit.lua脚本去获取,并且在调用evalsha方法时使用。
注意:使用redis集群,因此每个节点都需要各自缓存一份脚本数据
  1. /**
  2. * 由于使用redis集群,因此每个节点都需要各自缓存一份脚本数据
  3. * @param slotKey 用来定位对应的slot的slotKey
  4. */
  5. public void storeScript(String slotKey){
  6. if (StringUtils.isEmpty(unlockSha1) || !jedisCluster.scriptExists(unlockSha1, slotKey)){
  7.    //redis支持脚本缓存,返回哈希码,后续可以继续用来调用脚本
  8.     unlockSha1 = jedisCluster.scriptLoad(DISTRIBUTE_LOCK_SCRIPT_UNLOCK_VAL, slotKey);
  9.    }
  10. }
复制代码
常见的限流组件

redission分布式限流采取令牌桶思想和固定时间窗口,trySetRate方法设置桶的大小,使用redis key过期机制到达时间窗口目的,控制固定时间窗口内答应通过的请求量。
spring cloud gateway集成redis限流,但属于网关层限流
技能自由的实现路径:

实现你的 架构自由:

《吃透8图1模板,大家可以做架构》
《10Wqps批评中台,如何架构?B站是这么做的!!!》
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》
《100亿级订单怎么调度,来一个大厂的极品方案》
《2个大厂 100亿级 超大流量 红包 架构方案》
… 更多架构文章,正在添加中
实现你的 相应式 自由:

《相应式圣经:10W字,实现Spring相应式编程自由》
这是老版本 《Flux、Mono、Reactor 实战(史上最全)》
实现你的 spring cloud 自由:

《Spring cloud Alibaba 学习圣经》 PDF
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》
实现你的 linux 自由:

《Linux命令大全:2W多字,一次实现Linux自由》
实现你的 网络 自由:

《TCP协议详解 (史上最全)》
《网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!》
实现你的 分布式锁 自由:

《Redis分布式锁(图解 - 秒懂 - 史上最全)》
《Zookeeper 分布式锁 - 图解 - 秒懂》
实现你的 王者组件 自由:

《队列之王: Disruptor 原理、架构、源码 一文穿透》
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》
《缓存之王:Caffeine 的使用(史上最全)》
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》
实现你的 面试题 自由:

4000页《尼恩Java面试宝典 》 40个专题
以上尼恩 架构条记、面试题 的PDF文件更新,请到《技能自由圈》公号获取↓↓↓

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

数据人与超自然意识

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表