你能區分熔斷和降級有什麼區別嗎?

2023年04月28日20:47:10 財經 1906

熔斷和降級都是系統自我保護的一種機制,但二者又有所不同,它們的區別主要體現在以下幾點:

  1. 概念不同
  2. 觸發條件不同
  3. 歸屬關係不同

1.概念不同

1.1 熔斷概念

「熔斷」一詞早期來自股票市場。熔斷(Circuit Breaker)也叫自動停盤機制,是指當股指波幅達到規定的熔斷點時,交易所為控制風險採取的暫停交易措施。比如 2020 年 3 月 9 日,紐約股市開盤出現暴跌,隨後跌幅達到 7% 上限,觸發熔斷機制,停止交易 15 分中,恢復交易後跌幅有所減緩。

熔斷在程序中,表示「斷開」的意思。如發生了某事件,程序為了整體的穩定性,所以暫時(斷開)停止服務一段時間,以保證程序可用時再被使用。

如果沒有熔斷機制的話,會導致聯機故障和服務雪崩等問題,如下圖所示:

你能區分熔斷和降級有什麼區別嗎? - 天天要聞

1.2 降級概念

降級(Degradation)降低級別的意思,它是指程序在出現問題時,仍能保證有限功能可用的一種機制。

比如電商交易系統在雙 11 時,使用的人比較多,此時如果開放所有功能,可能會導致系統不可用,所以此時可以開啟降級功能,優先保證支付功能可用,而其他非核心功能,如評論、物流、商品介紹等功能可以暫時關閉。

所以,從上述信息可以看出:降級是一種退而求其次的選擇,而熔斷卻是整體不可用

2.觸發條件不同

不同框架的熔斷和降級的觸發條件是不同的,本文咱們以經典的 Spring Cloud 組件 Hystrix 為例,來說明觸發條件的問題。

2.1 Hystrix 熔斷觸發條件

默認情況 hystrix 如果檢測到 10 秒內請求的失敗率超過 50%,就觸發熔斷機制。之後每隔 5 秒重新嘗試請求微服務,如果微服務不能響應,繼續走熔斷機制。如果微服務可達,則關閉熔斷機制,恢復正常請求。

你能區分熔斷和降級有什麼區別嗎? - 天天要聞

2.2 Hystrix 降級觸發條件

默認情況下,hystrix 在以下 4 種條件下都會觸發降級機制:

  1. 方法拋出 HystrixBadRequestException
  2. 方法調用超時
  3. 熔斷器開啟攔截調用
  4. 線程池或隊列或信號量已滿

雖然 hystrix 組件的觸發機制,不能代表所有的熔斷和降級機制,但足矣說明此問題。

3.歸屬關係不同

**熔斷時可能會調用降級機制,而降級時通常不會調用熔斷機制。**因為熔斷是從全局出發,為了保證系統穩定性而停用服務,而降級是退而求其次,提供一種保底的解決方案,所以它們的歸屬關係是不同(熔斷 > 降級)。

題外話

當然,某些框架如 Sentinel,它早期在 Dashboard 控制台中可能叫「降級」,但在新版中新版本又叫「熔斷」,如下圖所示:

你能區分熔斷和降級有什麼區別嗎? - 天天要聞

但在兩個版本中都是通過同一個異常類型 DegradeException 來監聽的,如下代碼所示:

你能區分熔斷和降級有什麼區別嗎? - 天天要聞

所以,在 Sentinel 中,熔斷和降級功能指的都是同一件事,也側面證明了「熔斷」和「降級」概念的相似性。但我們要知道它們本質上是不同的,就像兩個雙胞胎,不能因為他們長得像,就說他們是同一個人。


你能區分熔斷和降級有什麼區別嗎? - 天天要聞

當用戶請求 A、P、H、I 四個服務獲取數據時,在正常流量下系統穩定運行,如果某天系統進來大量流量,其中服務 I 出現 CPU、內存佔用過高等問題,結果導致服務 I 出現延遲、響應過慢,隨著請求的持續增加,服務 I 承受不住壓力導致內部錯誤或資源耗盡,一直不響應,此時更糟糕的是其他服務對 I 有依賴,那麼這些依賴 I 的服務一直等待 I 的響應,也會出現請求堆積、資源佔用,慢慢擴散到所有微服務,引發雪崩效應。

基本的容錯模式

常見的容錯模式主要包含以下幾種方式:

1.主動超時:Http請求主動設置一個超時時間,超時就直接返回,不會造成服務堆積

2.限流:限制最大並發數

3.熔斷:當錯誤數超過閾值時快速失敗,不調用後端服務,同時隔一定時間放幾個請求去重試後端服務是否能正常調用,如果成功則關閉熔斷狀態,失敗則繼續快速失敗,直接返回。(此處有個重試,重試就是彈性恢復的能力)

4.隔離:把每個依賴或調用的服務都隔離開來,防止級聯失敗引起整體服務不可用

5.降級:服務失敗或異常後,返回指定的默認信息

你能區分熔斷和降級有什麼區別嗎? - 天天要聞

服務降級

由於爆炸性的流量衝擊,對一些服務進行有策略的放棄,以此緩解系統壓力,保證目前主要業務的正常運行。它主要是針對非正常情況下的應急服務措施:當此時一些業務服務無法執行時,給出一個統一的返回結果。

服務熔斷

熔斷這一概念來源於電子工程中的斷路器(Circuit Breaker)。在互聯網系統中,當下游服務因訪問壓力過大而響應變慢或失敗,上游服務為了保護系統整體的可用性,可以暫時切斷對下游服務的調用。

服務熔斷與服務降級比較

服務熔斷對服務提供了proxy,防止服務不可能時,出現串聯故障(cascading failure),導致雪崩效應。

服務熔斷一般是某個服務(下游服務)故障引起,而服務降級一般是從整體負荷考慮

1.共性:

目的 -> 都是從可用性、可靠性出發,提高系統的容錯能力。

最終表現->使某一些應用不可達或不可用,來保證整體系統穩定。

粒度 -> 一般都是服務級別,但也有細粒度的層面:如做到數據持久層、只許查詢不許增刪改等。

自治 -> 對其自治性要求很高。都要求具有較高的自動處理機制。

2.區別:

觸發原因 -> 服務熔斷通常是下級服務故障引起;服務降級通常為整體系統而考慮。

管理目標 -> 熔斷是每個微服務都需要的,是一個框架級的處理;而服務降級一般是關注業務,對業務進行考慮,抓住業務的層級,從而決定在哪一層上進行處理:比如在IO層,業務邏輯層,還是在外圍進行處理。

實現方式 -> 代碼實現中的差異。

服務熔斷恢復需注意的問題

如果服務是冪等性的,則恢復重試不會有問題;而如果服務是非冪等性的,則重試會導致數據出現問題。

Java 實現限流

1 滑動窗口

public class WindowLimiterComponent implements LimiterComponent {

    /**
     * 隊列id和隊列的映射關係,隊列裡面存儲的是每一次通過時候的時間戳,這樣可以使得程序里有多個限流隊列
     */
    private final Map<String, List<Long>> MAP = new ConcurrentHashMap<>();

    /**
     * 限制次數
     */
    private final int count;

    /**
     * 時間窗口大小
     */
    private final long timePeriod;

    public WindowLimiterComponent(int count, long timePeriod) {
        this.count = count;
        this.timePeriod = timePeriod;
    }

    /**
     * 滑動時間窗口限流演算法
     * 在指定時間窗口,指定限制次數內,是否允許通過
     *
     * @param id 隊列id
     * @return 是否被限流
     */
    @Override
    public synchronized boolean isLimited(String id) {
        // 獲取當前時間
        long nowTime = System.currentTimeMillis();
        // 根據隊列id,取出對應的限流隊列,若沒有則創建
        List<Long> list = MAP.computeIfAbsent(id, k -> new LinkedList<>());
        // 如果隊列還沒滿,則允許通過,並添加當前時間戳到隊列開始位置
        if (list.size() < count) {
            list.add(0, nowTime);
            return false;
        }
        // 隊列已滿(達到限制次數),則獲取隊列中最早添加的時間戳
        Long lastTime = list.get(count - 1);
        // 用當前時間戳 減去 最早添加的時間戳
        if (nowTime - lastTime <= timePeriod) {
            // 若結果小於等於timePeriod,則說明在timePeriod內,通過的次數大於count
            // 不允許通過
            return true;
        } else {
            // 若結果大於timePeriod,則說明在timePeriod內,通過的次數小於等於count
            // 允許通過,並刪除最早添加的時間戳,將當前時間添加到隊列開始位置
            list.remove(count - 1);
            list.add(0, nowTime);
            return false;
        }
    }

}
複製代碼
    @Test
    public void test() throws InterruptedException {
        // 任意10秒內,只允許2次通過
        LimiterComponent component = new WindowLimiterComponent(2, 10000L);
        while (true) {

            System.out.println(LocalTime.now().toString() + component.isLimited("1"));
            // 睡眠0-10秒
            Thread.sleep(1000 * new Random().nextInt(10));
        }
    }
複製代碼

2 Redis zset

public class RedisZSetLimiterComponent implements LimiterComponent{

    private final redissonComponent redissonComponent;

    /**
     * 限制次數
     */
    private final int count;

    /**
     * 時間窗口大小,單位毫秒
     */
    private final long timePeriod;


    public RedisZSetLimiterComponent(RedissonComponent component) {
        this.redissonComponent = component;
        this.count = 5;
        this.timePeriod = 1000;
    }

    public RedisZSetLimiterComponent(RedissonComponent component, int count, long timePeriod) {
        this.redissonComponent = component;
        this.count = count;
        this.timePeriod = timePeriod;
    }

    /**
     * 基於 zset 的滑動時間窗口限流演算法
     * 在指定時間窗口,指定限制次數內,是否允許通過
     *
     * @param key 隊列key
     * @return 是否允許通過
     */
    @Override
    public synchronized boolean isLimited(String key) {
        // 獲取當前時間
        long nowTime = System.currentTimeMillis();
        RScoredSortedSet<String> set = redissonComponent.getRScoredSortedSet(key);
        // 移除一個時間段以前的
        set.removeRangeByScore(0, true, (double) (nowTime - timePeriod), true);
        // 獲取集合內元素總數
        int size = set.count((double) (nowTime - timePeriod), true, nowTime, true);
        // 如果隊列沒滿
        if (size < count) {
            // 當前時間加入集合
            set.add((double) nowTime, String.valueOf(nowTime));
            return false;
        }
        return true;
    }


}
複製代碼
    @Test
    public void test() throws InterruptedException {
        // 任意10秒內,只允許2次通過
        LimiterComponent component = new RedisZSetLimiterComponent(redissonComponent, 2, 10000L);
        while (true) {

            System.out.println(LocalTime.now().toString() + component.isLimited("1"));
            // 睡眠0-10秒
            Thread.sleep(1000 * new Random().nextInt(10));
        }
    }
複製代碼

3 guava RateLimiter

@SuppressWarnings("UnstableApiUsage")
public class GuavaLimiterComponent implements LimiterComponent {

    private final int count;

    private final long timePeriod;

    private final Map<String, RateLimiter> MAP = new ConcurrentHashMap<>();

    public GuavaLimiterComponent(int count, long timePeriod) {
        this.count = count;
        this.timePeriod = timePeriod;
    }

    /**
     * 令牌桶演算法
     *
     * @param key 鍵值
     * @return 是否被限流
     */
    @Override
    public synchronized boolean isLimited(String key) {
        RateLimiter rateLimiter = MAP.computeIfAbsent(key, k -> RateLimiter.create(count, timePeriod, TimeUnit.MILLISECONDS));
        return !rateLimiter.tryAcquire();
    }


}
複製代碼
    @Test
    public void test() throws InterruptedException {
        // 任意10秒內,只允許2次通過
        LimiterComponent component = new GuavaLimiterComponent(2, 10000L);
        while (true) {
            System.out.println(LocalTime.now().toString() + component.isLimited("1"));
            // 睡眠0-10秒
            Thread.sleep(1000 * new Random().nextInt(10));
        }
    }
複製代碼

4 redisson RRateLimiter

public class RedisRateLimiterComponent implements LimiterComponent {

    private final RedissonComponent redissonComponent;

    /**
     * 限制次數
     */
    private final int count;

    /**
     * 時間窗口大小,單位毫秒
     */
    private final long timePeriod;


    public RedisRateLimiterComponent(RedissonComponent component) {
        this.redissonComponent = component;
        this.count = 5;
        this.timePeriod = 1000;
    }

    public RedisRateLimiterComponent(RedissonComponent component, int count, long timePeriod) {
        this.redissonComponent = component;
        this.count = count;
        this.timePeriod = timePeriod;
    }

    /**
     * 基於 rateLimiter 的滑動時間窗口限流演算法
     * 在指定時間窗口,指定限制次數內,是否允許通過
     *
     * @param key 隊列key
     * @return 是否允許通過
     */
    @Override
    public synchronized boolean isLimited(String key) {
        RRateLimiter rateLimiter = redissonComponent.getRateLimiter(key);
        rateLimiter.trySetRate(RateType.PER_CLIENT, count, timePeriod, RateIntervalUnit.MILLISECONDS);
        return !rateLimiter.tryAcquire();
    }


}
複製代碼
    @Test
    public void test() throws InterruptedException {
        // 任意10秒內,只允許2次通過
        LimiterComponent component = new RedisRateLimiterComponent(redissonComponent, 2, 10000L);
        while (true) {

            System.out.println(LocalTime.now().toString() + component.isLimited("1"));
            // 睡眠0-10秒
            Thread.sleep(1000 * new Random().nextInt(10));
        }
    }


總結

熔斷和降級都是程序在我保護的一種機制,但二者在概念、觸發條件、歸屬關係上都是不同的。熔斷更偏向於全局視角的自我保護(機制),而降級則偏向於具體模塊「退而請其次」的解決方案。



財經分類資訊推薦

NAS慎用指南:RAID0+無備份+不備電,翻車別怪我沒提前勸 - 天天要聞

NAS慎用指南:RAID0+無備份+不備電,翻車別怪我沒提前勸

大學同學某君,喜看美女高清視頻,存於雲盤每日觀瞻,豈料平台開展綠色行動,平日珍藏悉數被嗶,後在馬首富家直購大容量硬碟,盤內裝滿學習資料,繼續夜夜歡樂。前年同學小聚笑談此事,老哥表示已經搞定NAS私有雲服務,不但實現多設備數據互通,而且居家、遠程都能獨享快樂後花
Deep seek分析:未來3年內,普通家庭貶值最快的7項資產 - 天天要聞

Deep seek分析:未來3年內,普通家庭貶值最快的7項資產

近些年,很多人在買了某些東西之後,剛開始心裡美滋滋的。但沒過幾年,這些商品卻成為了「賠錢貨」。最明顯的就是房子、車子。於是,有人問Deep Seek:未來三年,普通家庭最容易貶值的東西有哪些?而Deep Seek給出的答案,除了房子和車子之外,還有手機、小眾收
智慧玩梗 · 網路安全警鐘長鳴 - 天天要聞

智慧玩梗 · 網路安全警鐘長鳴

網警蜀黍在此提醒大家,在「整蠱」「搞怪」的同時,在網路安全這件事上,也別忘了保持警惕。接下來,和大家分享一些關於如何在網路世界中保護自己的建議。時刻記住:網路安全,不容玩笑!
OPPO「開卷」,影像旗艦淪為千元機,Reno12 Pro開始收尾了 - 天天要聞

OPPO「開卷」,影像旗艦淪為千元機,Reno12 Pro開始收尾了

以前,攝影是一項門檻較高的愛好。那時候,手機拍照效果十分有限,功能機的拍照質量差得讓人無法忍受,所以出門旅遊時,很多人要麼選擇攜帶專業的單反相機,要麼乾脆找當地的攝影師幫忙拍攝。但是現在,隨著智能手機攝影技術的飛速發展,攝影已不再是只有專業人士才能掌握的技能。
蘋果給Windows筆記本重力一擊,24GB+512GB+M4晶元,突降到7199元 - 天天要聞

蘋果給Windows筆記本重力一擊,24GB+512GB+M4晶元,突降到7199元

在如今的筆記本市場,各大品牌都在爭奪用戶的注意力,競爭異常激烈。我們看到越來越多的品牌推出了性價比非常高的產品,就連蘋果也在這場競爭中加大了力度。特別是2024款的MacBook Air,以其強大的性能和合理的價格,成為了許多用戶的首選。相比於以往的MacBo