Node.js 斷路器簡介

ES2049 發表於 2021-10-16
Node.js

架構演變帶來的問題

當我們使用傳統的 CS 架構時,服務端由於故障等原因將請求堵塞,可能會導致客戶端的請求失去響應,進而在一段時間後導致一批使用者無法獲得服務。而這種情況可能影響範圍是有限,可以預估的。然而,在微服務體系下,您的伺服器可能依賴了若干其他微服務,而這些微服務又依賴其它更多的微服務,這種情況下,某個服務對於下游的堵塞,可能會瞬間(數秒內)因為級聯的資源消耗造成整條鏈路上災難性的後果,我們稱之為“服務血崩”。

image.png

image.png

解決問題的幾種方式

  1. 熔斷模式:顧名思義,就如同家用電路一樣,如果一條線路電壓過高,保險絲會熔斷,防止火災。在使用熔斷模式的系統中,如果發現上游服務呼叫慢,或者有大量超時的時候,直接中止對於該服務的呼叫,直接返回資訊,快速釋放資源。直至上游服務好轉時再恢復呼叫。
  2. 隔離模式:將不同的資源或者服務的呼叫分割成幾個不同的請求池,一個池子的資源被耗盡並不會影響其它資源的請求,防止某個單點的故障消耗完全部的資源。這是非常傳統的一種容災設計。
  3. 限流模式:熔斷和隔離都是一種事後處置的方式,限流模式則可以在問題出現之前降低問題出現的概率。限流模式可以對某些服務的請求設定一個最高的 QPS 閾值,超出閾值的請求直接返回,不再佔用資源處理。但是限流模式,並不能解決服務血崩的問題,因為往往引起血崩並不是因為請求的數量大,而是因為多個級聯層數的放大。

斷路器的機制和實現

斷路器的存在,相當於給了我們一層保障,在呼叫穩定性欠佳,或者說很可能會呼叫失敗的服務和資源時,斷路器可以監視這些錯誤並且在達到一定閾值之後讓請求失敗,防止過度消耗資源。並且,斷路器還擁有自動識別服務狀態並恢復的功能,當上遊服務恢復正常時,斷路器可以自動判斷並恢復正常請求。

讓我們看一下一個沒有斷路器的請求過程:
使用者依賴 ServiceA 來提供服務,ServiceA 又依賴 ServiceB 提供的服務,假設 ServiceB 此時出現了故障,在 10 分鐘內,對於每個請求都會延遲 10 秒響應。
image.png

那麼假設我們有 N 個 User 在請求 ServiceA 的服務時,幾秒鐘內,ServiceA 的資源就會因為對 ServiceB 發起的請求被掛起而消耗一空,從而拒絕 User 之後的任何請求。對於使用者來說,這就等於 ServiceA 和 ServiceB 同時都出現了故障,引起了整條服務鏈路的崩潰。

而當我們在 ServiceA 上裝上一個斷路器後會怎麼樣呢?

  1. 斷路器在失敗次數達到一定閾值後會發現對 ServiceB 的請求已經無效,那麼此時 ServiceA 就不需要繼續對 ServiceB 進行請求,而是直接返回失敗,或者使用其他Fallback 的備份資料。此時,斷路器處於 開路 狀態。
  2. 在一段時間過後,斷路器會開始定時查詢 ServiceB 是否已經恢復,此時,斷路器處於 半開 狀態。
  3. 如果 ServiceB 已經恢復,那麼斷路器會置於 關閉 狀態,此時 ServiceA 會正常呼叫 ServiceB 並且返回結果。

image.png

斷路器的狀態圖如下:
image.png

由此可見,斷路器的幾個核心要點如下:

  1. 超時時間:請求達到多久,算引起了一次失敗
  2. 失敗閾值:即斷路器觸發開路之前,需要達到的失敗次數
  3. 重試超時:當斷路器處於開路狀態後,隔多久開始重新嘗試請求,即進入半開狀態

有了這些知識,我們可以嘗試建立一個斷路器:

class CircuitBreaker {
  constructor(timeout, failureThreshold, retryTimePeriod) {
    // We start in a closed state hoping that everything is fine
    this.state = 'CLOSED';
    // Number of failures we receive from the depended service before we change the state to 'OPEN'
    this.failureThreshold = failureThreshold;
    // Timeout for the API request.
    this.timeout = timeout;
    // Time period after which a fresh request be made to the dependent
    // service to check if service is up.
    this.retryTimePeriod = retryTimePeriod;
    this.lastFailureTime = null;
    this.failureCount = 0;
  }
}

構造斷路器的狀態機:

async call(urlToCall) {
    // Determine the current state of the circuit.
    this.setState();
    switch (this.state) {
      case 'OPEN':
      // return  cached response if no the circuit is in OPEN state
        return { data: 'this is stale response' };
      // Make the API request if the circuit is not OPEN
      case 'HALF-OPEN':
      case 'CLOSED':
        try {
          const response = await axios({
            url: urlToCall,
            timeout: this.timeout,
            method: 'get',
          });
          // Yay!! the API responded fine. Lets reset everything.
          this.reset();
          return response;
        } catch (err) {
          // Uh-oh!! the call still failed. Lets update that in our records.
          this.recordFailure();
          throw new Error(err);
        }
      default:
        console.log('This state should never be reached');
        return 'unexpected state in the state machine';
    }
  }

補充剩餘功能:

// reset all the parameters to the initial state when circuit is initialized
  reset() {
    this.failureCount = 0;
    this.lastFailureTime = null;
    this.state = 'CLOSED';
  }

  // Set the current state of our circuit breaker.
  setState() {
    if (this.failureCount > this.failureThreshold) {
      if ((Date.now() - this.lastFailureTime) > this.retryTimePeriod) {
        this.state = 'HALF-OPEN';
      } else {
        this.state = 'OPEN';
      }
    } else {
      this.state = 'CLOSED';
    }
  }

  recordFailure() {
    this.failureCount += 1;
    this.lastFailureTime = Date.now();
  }

使用斷路器時,只需要將請求包裹在斷路器例項中的 Call 方法裡呼叫即可:

...
const circuitBreaker = new CircuitBreaker(3000, 5, 2000);

const response = await circuitBreaker.call('http://0.0.0.0:8000/flakycall');

成熟的 Node.js 斷路器庫

Red Hat 很早就建立了一個名叫 Opossum 的成熟 Node.js 斷路器實現,連結在此:Opossum 。對於分散式系統來說,使用這個庫可以極大提升你的服務的容錯能力,從根本上解決服務血崩的問題。

作者:ES2049 / 尋找奇點

文章可隨意轉載,但請保留此原文連結。
非常歡迎有激情的你加入 ES2049 Studio,簡歷請傳送至 [email protected]