<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          常見分布式限流方案

          共 9652字,需瀏覽 20分鐘

           ·

          2021-06-03 22:35

          點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號(hào)”

          優(yōu)質(zhì)文章,第一時(shí)間送達(dá)

          限流分類

          合法性驗(yàn)證限流

          比如驗(yàn)證碼、IP 黑名單等,這些手段可以有效的防止惡意攻擊和爬蟲采集;

          容器限流

          比如 Tomcat、Nginx 等限流手段,其中 Tomcat 可以設(shè)置最大線程數(shù)(maxThreads),當(dāng)并發(fā)超過(guò)最大線程數(shù)會(huì)排隊(duì)等待執(zhí)行;而 Nginx 提供了兩種限流手段:一是控制速率,二是控制并發(fā)連接數(shù);

          服務(wù)端限流

          比如我們?cè)诜?wù)器端通過(guò)限流算法實(shí)現(xiàn)限流,此項(xiàng)也是我們本文介紹的重點(diǎn)。

          常見的6種限流方案

          1.Tomcat 使用 maxThreads 設(shè)置請(qǐng)求線程數(shù)來(lái)實(shí)現(xiàn)限流

          實(shí)現(xiàn)方案:
          進(jìn)入tomcat的conf/server.xml文件配置中,然后找到如下代碼:

          <Connector port="8080" protocol="HTTP/1.1"
                    connectionTimeout="20000"
                    maxThreads="150"
                    redirectPort="8443" />

          maxThreads就是tomcat的最大線程數(shù),當(dāng)請(qǐng)求的并發(fā)大于此值,請(qǐng)求就會(huì)排隊(duì)執(zhí)行。
          此值默認(rèn)為 150(Tomcat 版本 8.5.42),每開啟一個(gè)線程需要耗用 1MB 的 JVM 內(nèi)存空間用于作為線程棧之用,Windows 每個(gè)進(jìn)程中的線程數(shù)不允許超過(guò) 2000,Linux 每個(gè)進(jìn)程中的線程數(shù)不允許超過(guò) 1000。

          2.nginx通過(guò) limit_req_zone 和 burst 來(lái)實(shí)現(xiàn)速率限流

          實(shí)現(xiàn)方案:在nginx主文件配置中寫入如下代碼:

          limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
          server { 
              location / { 
                  limit_req zone=mylimit;
              }
          }

          以上代碼的意思是限制每個(gè)ip訪問(wèn)的速度為每秒2次請(qǐng)求,但是nginx對(duì)時(shí)間是更加嚴(yán)格的,細(xì)分到500ms為單位劃分,其真實(shí)情況是1r/500ms。

          上面的請(qǐng)求限制太過(guò)于苛刻,對(duì)于單位時(shí)間內(nèi)無(wú)法執(zhí)行的請(qǐng)求直接拒絕。真實(shí)情況應(yīng)該用ip繼續(xù)細(xì)分而不是只統(tǒng)計(jì)單位時(shí)間內(nèi)請(qǐng)求次數(shù)。我們可以使用burst配置,代碼如下:

          limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
          server { 
              location / { 
                  limit_req zone=mylimit burst=4;
              }
          }

          burst=4 表示每個(gè) IP 最多允許4個(gè)突發(fā)請(qǐng)求.這意味著500ms內(nèi)一個(gè)ip發(fā)起6次請(qǐng)求,第一個(gè)請(qǐng)求立即執(zhí)行,接下來(lái)4個(gè)請(qǐng)求進(jìn)入隊(duì)列等待執(zhí)行,最后一個(gè)請(qǐng)求直接拒絕。

          3.nginx通過(guò) limit_conn_zone 和 limit_conn 兩個(gè)指令控制并發(fā)連接的總數(shù)

          實(shí)現(xiàn)方案:
          在nginx主配置文件中寫入如下代碼:

          limit_conn_zone $binary_remote_addr zone=perip:10m;
          limit_conn_zone $server_name zone=perserver:10m;
          server {
              ...
              limit_conn perip 10;
              limit_conn perserver 100;
          }

          上面的10M是指允許使用10MB內(nèi)存保存變量值

          $binary_remote_addr和$server_name。
          其中 limit_conn perip 10 表示限制單個(gè) IP 同時(shí)最多能持有 10 個(gè)連接;limit_conn perserver 100 表示 server 同時(shí)能處理并發(fā)連接的總數(shù)為 100 個(gè)。

          只有當(dāng) request header 被后端處理后,這個(gè)連接才進(jìn)行計(jì)數(shù)。

          4.服務(wù)端使用時(shí)間窗口算法借助 Redis 的zset實(shí)現(xiàn)限流

          實(shí)現(xiàn)方案:
          所謂的滑動(dòng)時(shí)間算法指的是以當(dāng)前時(shí)間為截止時(shí)間,往前取一定的時(shí)間,比如往前取 60s 的時(shí)間,在這 60s 之內(nèi)運(yùn)行最大的訪問(wèn)數(shù)為 100,此時(shí)算法的執(zhí)行邏輯為,先清除 60s 之前的所有請(qǐng)求記錄,再計(jì)算當(dāng)前集合內(nèi)請(qǐng)求數(shù)量是否大于設(shè)定的最大請(qǐng)求數(shù) 100,如果大于則執(zhí)行限流拒絕策略,否則插入本次請(qǐng)求記錄并返回可以正常執(zhí)行的標(biāo)識(shí)給客戶端。
          滑動(dòng)時(shí)間窗口如下圖所示:

          其中每一小個(gè)表示 10s,被紅色虛線包圍的時(shí)間段則為需要判斷的時(shí)間間隔,比如 60s 秒允許 100 次請(qǐng)求,那么紅色虛線部分則為 60s。
          下面是算法實(shí)現(xiàn),每3秒最多請(qǐng)求10次,代碼如下:
          借助 Jedis 包來(lái)操作 Redis,實(shí)現(xiàn)在 pom.xml 添加 Jedis 框架的引用,配置如下:

          <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
          <dependency>
              <groupId>redis.clients</groupId>
              <artifactId>jedis</artifactId>
              <version>3.3.0</version>
          </dependency>

          代碼:

          import redis.clients.jedis.Jedis;

          public class RedisLimit {
              // Redis 操作客戶端
              static Jedis jedis = new Jedis("127.0.0.1", 6379);

              public static void main(String[] args) throws InterruptedException {
                  for (int i = 0; i < 15; i++) {
                      boolean res = isPeriodLimiting("java", 3, 10);
                      if (res) {
                          System.out.println("正常執(zhí)行請(qǐng)求:" + i);
                      } else {
                          System.out.println("被限流:" + i);
                      }
                  }
                  // 休眠 4s
                  Thread.sleep(4000);
                  // 超過(guò)最大執(zhí)行時(shí)間之后,再?gòu)陌l(fā)起請(qǐng)求
                  boolean res = isPeriodLimiting("java", 3, 10);
                  if (res) {
                      System.out.println("休眠后,正常執(zhí)行請(qǐng)求");
                  } else {
                      System.out.println("休眠后,被限流");
                  }
              }

              /**
               * 限流方法(滑動(dòng)時(shí)間算法)
               * @param key      限流標(biāo)識(shí)
               * @param period   限流時(shí)間范圍(單位:秒)
               * @param maxCount 最大運(yùn)行訪問(wèn)次數(shù)
               * @return
               */
              private static boolean isPeriodLimiting(String key, int period, int maxCount) {
                  long nowTs = System.currentTimeMillis(); // 當(dāng)前時(shí)間戳
                  // 刪除非時(shí)間段內(nèi)的請(qǐng)求數(shù)據(jù)(清除老訪問(wèn)數(shù)據(jù),比如 period=60 時(shí),標(biāo)識(shí)清除 60s 以前的請(qǐng)求記錄)
                  jedis.zremrangeByScore(key, 0, nowTs - period * 1000);
                  long currCount = jedis.zcard(key); // 當(dāng)前請(qǐng)求次數(shù)
                  if (currCount >= maxCount) {
                      // 超過(guò)最大請(qǐng)求次數(shù),執(zhí)行限流
                      return false;
                  }
                  // 未達(dá)到最大請(qǐng)求數(shù),正常執(zhí)行業(yè)務(wù)
                  jedis.zadd(key, nowTs, "" + nowTs); // 請(qǐng)求記錄 +1
                  return true;
              }
          }

          此實(shí)現(xiàn)方式存在的缺點(diǎn)有兩個(gè):
          1.使用 ZSet 存儲(chǔ)有每次的訪問(wèn)記錄,如果數(shù)據(jù)量比較大時(shí)會(huì)占用大量的空間,比如 60s 允許 100W 訪問(wèn)時(shí);

          2.此代碼的執(zhí)行非原子操作,先判斷后增加,中間空隙可穿插其他業(yè)務(wù)邏輯的執(zhí)行,最終導(dǎo)致結(jié)果不準(zhǔn)確。

          時(shí)間窗口算法的缺點(diǎn):在一定范圍內(nèi),比如 60s 內(nèi)只能有 10 個(gè)請(qǐng)求,當(dāng)?shù)谝幻霑r(shí)就到達(dá)了 10 個(gè)請(qǐng)求,那么剩下的 59s 只能把所有的請(qǐng)求都給拒絕掉。

          5.服務(wù)端使用漏桶算法借助 Redis-Cell 來(lái)實(shí)現(xiàn)限流

          實(shí)現(xiàn)方案:

          漏洞算法的實(shí)現(xiàn)步驟是,先聲明一個(gè)隊(duì)列用來(lái)保存請(qǐng)求,這個(gè)隊(duì)列相當(dāng)于漏斗,當(dāng)隊(duì)列容量滿了之后就放棄新來(lái)的請(qǐng)求,然后重新聲明一個(gè)線程定期從任務(wù)隊(duì)列中獲取一個(gè)或多個(gè)任務(wù)進(jìn)行執(zhí)行,這樣就實(shí)現(xiàn)了漏桶算法。

          我們可以使用 Redis 4.0 版本中提供的 Redis-Cell 模塊,該模塊使用的是漏斗算法,并且提供了原子的限流指令,而且依靠 Redis 這個(gè)天生的分布式程序就可以實(shí)現(xiàn)比較完美的限流了。

          我們可以使用 Redis 4.0 版本中提供的 Redis-Cell 模塊,該模塊使用的是漏斗算法,并且提供了原子的限流指令,而且依靠 Redis 這個(gè)天生的分布式程序就可以實(shí)現(xiàn)比較完美的限流了。
          Redis-Cell 實(shí)現(xiàn)限流的方法也很簡(jiǎn)單,只需要使用一條指令 cl.throttle 即可,使用示例如下:

          > cl.throttle mylimit 15 30 60
          1)(integer)0 # 0 表示獲取成功,1 表示拒絕
          2)(integer)15 # 漏斗容量
          3)(integer)14 # 漏斗剩余容量
          4)(integer)-1 # 被拒絕之后,多長(zhǎng)時(shí)間之后再試(單位:秒)-1 表示無(wú)需重試
          5)(integer)2 # 多久之后漏斗完全空出來(lái)

          其中 15 為漏斗的容量,30 / 60s 為漏斗的速率。

          6.服務(wù)端使用令牌算法借助Google 的 guava 包來(lái)實(shí)現(xiàn)限流

          實(shí)現(xiàn)方案:
          在令牌桶算法中有一個(gè)程序以某種恒定的速度生成令牌,并存入令牌桶中,而每個(gè)請(qǐng)求需要先獲取令牌才能執(zhí)行,如果沒有獲取到令牌的請(qǐng)求可以選擇等待或者放棄執(zhí)行,如下圖所示:

          我們可以使用 Google 開源的 guava 包,很方便的實(shí)現(xiàn)令牌桶算法,首先在 pom.xml 添加 guava 引用,配置如下:

          <!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
          <dependency>
              <groupId>com.google.guava</groupId>
              <artifactId>guava</artifactId>
              <version>28.2-jre</version>
          </dependency>

          具體實(shí)現(xiàn)代碼如下:

          import com.google.common.util.concurrent.RateLimiter;

          import java.time.Instant;

          /**
           * Guava 實(shí)現(xiàn)限流
           */
          public class RateLimiterExample {
              public static void main(String[] args) {
                  // 每秒產(chǎn)生 10 個(gè)令牌(每 100 ms 產(chǎn)生一個(gè))
                  RateLimiter rt = RateLimiter.create(10);
                  for (int i = 0; i < 11; i++) {
                      new Thread(() -> {
                          // 獲取 1 個(gè)令牌
                          rt.acquire();
                          System.out.println("正常執(zhí)行方法,ts:" + Instant.now());
                      }).start();
                  }
              }
          }

          從以上結(jié)果可以看出令牌確實(shí)是每 100ms 產(chǎn)生一個(gè),而 acquire() 方法為阻塞等待獲取令牌,它可以傳遞一個(gè) int 類型的參數(shù),用于指定獲取令牌的個(gè)數(shù)。它的替代方法還有 tryAcquire(),此方法在沒有可用令牌時(shí)就會(huì)返回 false 這樣就不會(huì)阻塞等待了。當(dāng)然 tryAcquire() 方法也可以設(shè)置超時(shí)時(shí)間,未超過(guò)最大等待時(shí)間會(huì)阻塞等待獲取令牌,如果超過(guò)了最大等待時(shí)間,還沒有可用的令牌就會(huì)返回 false。

          注意事項(xiàng):Redis 實(shí)現(xiàn)的限流方案可用于分布式系統(tǒng),而 guava 實(shí)現(xiàn)的限流只能應(yīng)用于單機(jī)環(huán)境。


          版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 BY-SA 版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接和本聲明。

          本文鏈接:

          https://blog.csdn.net/qq_36427244/article/details/117388495








          瀏覽 52
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  tt天天干.vip www.逼特逼视频 | 99这里只有精品热 | 成人伊人影视在线 | 免费成人一级片 | 成人中文字幕在线 |