天天看點

降級限流1、限流和降級2、限流算法3、限流實踐4、小結

目錄

1、限流和降級

1.1、降級

1.2、限流

2、限流算法

2.1、滑動視窗

2.2、漏桶

2.3、令牌桶

3、限流實踐

3.1、單機Guava實作令牌桶和漏桶

3.2、分布式限流器實作

3.2.1、Redis實作

3.2.2、lua腳本實作

3.2.3、Redission的實作

4、小結

前言      對于高可用的服務,除了盡量保證自身的服務的可靠性外, 還的防止不被上遊壓垮,控制上遊的通路一般使用限流政策,避免被下遊服務不可用拖垮,一般采取降級政策,隻有更好的了解和控制上下遊才能使自身的服務的穩定性增強。是以服務降級限流是系統面臨挑戰的最後一道保護屏障,不可忽視,也是在突發情況下保證系統穩定性和可用性的有效手段。

1、限流和降級

目的: 當流量快速增長的時候,一定要保證核心服務的可用,即便是有損服務。方案包括: 有限重試, 快速失敗, 降級限流方案。 對于高并發C端業務系統,一般都會采取相應的手段來保護系統不被意外的請求流量壓垮,進行服務的降級處理, 包括熔斷和限流:

  • 熔斷:類似于電流的保險絲機制,當我們的服務出問題或者影響到核心服務流程的時候需要暫時熔斷某些功能+友好的提示,等高峰或者問題解決後再打開;
  • 限流:當流量快速增長、防止脈沖式流量導緻服務可能會出現問題(響應逾時),或者非核心服務影響到核心流程時, 仍然需要保證服務的可用性, 即便是有損服務。
  • 緩存:目的是提升系統通路速度和增大系統處理的容量,可以說是 抗高并發流量的銀彈;

是以意味着我們在設計服務的時候,在使用緩存和異步處理的同時,還需要通過一些關鍵資料進行 自動降級,或者配置 人工降級的開關。

1.1、降級

對于系統的降級,請求的backup,降級一般有幾種實作手段:

  • 自動降級:達到某一門檻值後自動熔斷降級;
    • 例如達到請求QPS門檻值,下遊服務接口錯誤率達到一定的門檻值;對業務的重要程度可實作服務降級,自動傳回預設資料和通知資訊,或者延後處理;
    • 中間件服務故障等使用降級掉部分功能服務,例如mq故障使用rpc方式;
    • 多級緩存的使用;
  • 人工降級: 通過配置降級開關,人為實作對流程的控制;
    • 前置化降級開關, 基于配置中心實作附加業務的降級;
    • 業務降級,首先可以進行服務隔離,資源隔離,分管道分地域分區進行控制,在業務高峰期,我們會優先保證核心業務的流程可用,對附加業務進行降級處理;

降級方案分為:有損降級和無損降級

  • 無損降級:例如多級緩存,異步發送mq轉rpc調用,全局id轉本地生成後備方案;
  • 有損降級:限制資源,降級服務不可用+友好提示等等;

1.2、限流

限流的目的是防止惡意請求流量、惡意攻擊、或者防止流量超過系統峰值。通過對并發通路/請求進行限速或者一個時間視窗内的請求進行限速來保護系統。 主要是對資源通路做控制,那麼控制這塊主要有兩個功能: 限流政策 限流政策由 限流算法和 可調節的參數兩部份組成,算法和參數都需要結合具體的業務特點和并發量來選擇和設定。 熔斷政策 對于服務的 熔斷政策,不同的系統有不同的熔斷政策訴求,例如:

  • 直接拒絕服務.(友好提示頁面告知資源沒有了);
  • 排隊等待 異步處理(秒殺、搶購下單);
  • 服務降級 (傳回兜底資料或預設資料,如商品詳情頁等等)

2、限流算法

限流算法,對突發流量的整形形成穩定的流量,對系統起到保護作用,将脈沖式流量壓力均衡分攤,減少系統瞬時的壓力。

2.1、滑動視窗協定

是傳輸層進行流控的一種措施,接收方通過通告發送方自己的視窗大小,進而控制發送方的發送速度,進而達到防止發送方發送速度過快而導緻自己被淹沒的目的。

降級限流1、限流和降級2、限流算法3、限流實踐4、小結

動态限流效果示範: https://media.pearsoncmg.com/aw/ecs_kurose_compnetwork_7/cw/content/interactiveanimations/selective-repeat-protocol/index.html

2.2、漏桶

桶本身具有一個恒定的速率(接口的響應時間)往下漏水,而上方時快時慢的會有水進入桶内。當桶還未滿時,上方的水可以加入。一旦水滿,上方的水就無法加入。桶滿正是算法中的一個關鍵的觸發條件(即流量異常判斷成立的條件)。而此條件下如何處理上方流下來的水,有兩種方式在桶滿水之後,常見的兩種處理方式為:

  • 1) 阻塞:暫時攔截住上方水的向下流動,等待桶中的一部分水漏走後,再放行上方水;
  • 2) 抛棄:溢出的上方水直接抛棄,執行拒絕政策;

特點:

  • 1. 漏水的速率是固定的
  • 2. 即使存在注水 burst(突然注水量變大)的情況, 漏水的速率是固定的,不能解決突發流量; 
降級限流1、限流和降級2、限流算法3、限流實踐4、小結

2.3、令牌桶

令牌桶算法是網絡流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最 常使用的一種算法。典型情況下,令牌桶算法用來控制發送到網絡上的資料的數目, 能解決突發請求的流量; 令牌桶是一個存放固定容量令牌(token)的桶,按照固定速率往桶裡添加令牌; 令牌是按一定的速率來生成;1個令牌/10ms; 令牌桶算法實際上由兩部分組成: 

  • 兩個流:分别是 令牌流、資料流
  • 一個桶: 令牌桶
降級限流1、限流和降級2、限流算法3、限流實踐4、小結

令牌桶和漏桶的差別: 令牌桶算法能夠在限制資料的平均傳輸速率的同時還允許某種程度的突發傳輸。 分析:比如我們的目标現在是每秒鐘處理10個請求,使用漏桶法,我們設定桶的大小為10,流出的速率為0.1s流出一個請求,我們可以達成我們的目标;而使用令牌桶的話,我們會設定每1s向桶中放入10個令牌,請求如果是平穩的,每0.1s過來一個請求,來十個,同樣能達到我們的目标,這裡所說的某種程度的突發傳輸是指,比如在一秒内前0.5請求是平穩的,每0.1s來一個,但在0.6s的時候突發請求同時來個5個請求,此時令牌算法是可以承受這個突發流量的,并且讓5個請求成功立刻同時得到處理,完成了所謂的某種程度的突發傳輸,而漏桶算法,也可以應對這種突發流量,但不同的是沒有讓5個請求同時立刻通過,而是緩沖在桶中,然後仍然以固定速率每0.1s處理一個。     這兩種算法,都可以實作流速的控制,1s處理10個請求,多于10個的請求都會被拒絕,但由于漏桶算法流出速率是一定的,是以請求可能會被緩沖在桶中,不能馬上得到處理,進而徒增了等待時間,而對于令牌桶算法,沒有這種等待時間,有令牌則通過,無令牌則抛棄。

3、限流實踐

3.1、單機Guava實作令牌桶和漏桶

具體執行個體如下:

public class GuavaTokenDemo {
    private int qps;
    private int countOfReq;

    private RateLimiter rateLimiter;

    public GuavaTokenDemo(int qps, int countOfReq) {
        this.qps = qps;
        this.countOfReq = countOfReq;
    }

    //初始化一個令牌桶
    public GuavaTokenDemo processWithTokenBucket() {
        rateLimiter = RateLimiter.create(qps);                          //1min鐘的請求量qps;峰值
        return this;
    }

    //初始化一個漏桶
    public GuavaTokenDemo processWithLeakyBucket() {
        rateLimiter = RateLimiter.create(qps, 0, TimeUnit.MILLISECONDS); //預熱時間 0 
        return this;
    }

    private void processRequest() {
        System.out.println("RateLimiter:" + rateLimiter.getClass());
        long start = System.currentTimeMillis();
        for (int i = 0; i < countOfReq; i++) {
            rateLimiter.acquire();//擷取令牌 acquire()是阻塞的,tryAcquire()是非阻塞的;
        }
        long end = System.currentTimeMillis() - start;
        System.out.println("處理的請求數量:" + countOfReq + "," +
                "" + "耗時:" + end + ",qps:" + rateLimiter.getRate() + ",實際qps:" +
                Math.ceil(countOfReq / (end / 1000.00)));
    }

    public void doProcessor() throws InterruptedException {
        for (int i = 0; i < 20; i = i + 5) {
            TimeUnit.SECONDS.sleep(i);
            processRequest();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new GuavaTokenDemo(50, 100).processWithTokenBucket().doProcessor();
        new GuavaTokenDemo(50, 100).processWithLeakyBucket().doProcessor();
    }

}
           

通過運作結果的耗時和處理請求數可以發現,當請求書低于qps的時候,結果是恒定的,對于突發差不多2倍qps的流量請求,令牌桶是可以處理的;但是漏桶的處理速率就是恒定的;

3.2、分布式限流器實作

在分布式環境下,Guava的限流就無法起到作用了,是以必須借助第三方中間件來實作,而redis的高性能使其成了首選。

3.2.1、Redis實作

基于原生Redis的分布式限流器RateLimiter可以用來在分布式環境下進行限流,例如可以用來限制調用接口的次數。

//基于redis實作的限流器
public class RedisLimiter {

    static String key = "keyWords"; //限流關鍵字
    static int limitCount = 10;     //限流通路頻次
    static int limitTimeSecond = 5; //限流機關時間

    public static void main(String[] args) throws InterruptedException {
        Jedis j = new Jedis("127.0.0.1", 6379);
        new Thread(() -> {
            while (true) {
                if (!checkKeyLimit(j)) {
                    System.out.println("超過門檻值,被限流");
                    return;
                }
                incrKey(j);
            }
        }).start();

    }
    //利用逾時時間内的通路頻次門檻值來實作通路限制;
    private static void incrKey(Jedis j) {
        if (j.ttl(key) < 0) {
            j.incr(key); //通路一次,計數自增一次
            j.expire(key, limitTimeSecond);
        } else {
            j.incr(key);
        }
        System.out.println("總通路次數為:" + j.get(key));
    }
    //判斷時間内是否到達門檻值
    private static boolean checkKeyLimit(Jedis j) {
        if (null != j.get(key)) {
            if (Integer.parseInt(j.get(key)) >= limitCount) {
                return false;
            }
        }
        return true;
    }
}
           

3.2.2、lua腳本實作

其實和原生Redis的實作思想一樣,隻是利用了lua腳本執行的原子性,使用lua腳本實作限流代碼如下 :

    -- ip_limit.lua
    -- IP 限流,對某個 IP 頻率進行限制 ,1 秒鐘通路 10 次 
   local num=redis.call('incr',KEYS[1])
   if tonumber(num)==1 then
       redis.call('expire',KEYS[1],ARGV[1])
       return 1
   else if tonumber(num)>tonumber(ARGV[2]) then
       return 0
   else
       return 1
   end
           

例如想實作對ip:192.168.8.111,限制1秒内請求超過10次就拒絕,運作指令:

./redis-cli --eval "ip_limit.lua" app:ip:limit:192.168.8.111 , 1 10
           

3.2.3、Redission的實作

其實使用基于Redis的用戶端Redisson提供的api來實作更加友善。主要實作原理是令牌桶機制,需要先擷取指定的令牌,限流器每秒會産生n個令牌放入令牌桶,調用接口需要先去令牌桶裡面拿令牌,起到限流的作用。 執行個體如下:

public class SingleTest {
    public static void main(String[] args) {
        Config config = new Config();
        config.setCodec(new org.redisson.client.codec.StringCodec());

        //指定使用單節點部署方式
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redisson = Redisson.create(config);
       
        //擷取令牌桶限流
        RRateLimiter rRateLimiter = redisson.getRateLimiter("myRateLimiter");
        //初始化  最大流速 = 每1秒鐘産生100個令牌
        rRateLimiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.SECONDS);
        //需要1個令牌
        if(rateLimiter.tryAcquire(1)){
            //TODO: do something
        }
        //批量擷取10個令牌
        if(rateLimiter.acquire(10)){
            //TODO: do something
        }

        //關閉RedissonClient
        redisson.shutdown();
    }
}
           

4、小結

為了保證系統的可用性,系統設計必須考慮到降級限流。 應用按層劃分限流:

  • 前置化限流:例如Nginx負載均衡的分流與限流;
  • Server層接口限流方法:令牌桶及漏桶算法,hytrix熔斷,請求排隊+異步處理,線程池排隊+拒絕政策;
  • Dao層資料庫資源-池化技術,連接配接池-資料庫資源的合理利用;

單程序的限流:guava中的Ratlimiter.create(10); 分布式下的限流政策:分布式的限流需要借助于第三方才可實作;

  • Mysql: 存儲限流政策的參數等中繼資料
  • 基于 Redis的計數 和 令牌桶算法,或者lua腳本實作計數限流;

OK---待從頭、收拾舊山河,朝天阙。     水滴石穿,積少成多。學習筆記,内容簡單,用于複習,梳理鞏固。    

繼續閱讀