在分散式系統中通過客戶端庫包提高可用性

banq發表於2022-01-21

在客戶端應用程式中設定一個庫,我們可以一致地處理故障,從而提高系統的感知可用性。

在開發在我們自己的公司內部或外部使用的 API 時,除了記錄和公開端點之外,我們還可以選擇交付客戶端庫。這種方法對使用者有很多好處:更容易實現(有時它甚至是單線),更容易遷移(通常只是增加一個依賴版本),並且可能更容易設定安全性。

雖然開發和維護此類客戶端庫需要付出努力,但它們也可以為 API 開發人員帶來巨大的好處,其中包括增加系統的正常執行時間/可用性。當出現瞬態故障時,重試操作通常可以解決問題。這個邏輯可以內建到客戶端庫中,效果很好。

本文的目的是瞭解在哪些情況下在普通 API 定義之上新增客戶端庫可以顯著提高正常執行時間。首先,我們將介紹分散式系統架構中的一些最佳實踐,這些是從藉助客戶端庫獲得的正常執行時間收益中受益的必要先決條件。然後,我們將討論如何準確地開發客戶端庫以增加我們系統的正常執行時間,同時又不會花費太多維護成本。在結束本文時,我將提到客戶端庫如何讓 API 開發人員的生活變得更好的其他幾種方式。

在開始之前,讓我們在本文的上下文中定義可用性/正常執行時間。它是“從客戶端的角度來看成功操作的百分比”,意思是在純 HTTP 實現的情況下,它是沒有超時或返回 5xx 響應程式碼的請求的百分比。在庫實現的情況下,它是未導致異常或返回伺服器錯誤的成功方法呼叫的百分比。

同樣,我們試圖通過重試來減少客戶端感知到的錯誤數量,而不是網路請求失敗或服務重新啟動的數量。讓我們首先了解一些最重要的先決條件。

 

高可用和DNS

為了獲得較高的重試成功率,我們需要沒有單點故障的基礎設施。如果其中一個物理位置的應用程式停止響應,並且我們有理由相信該位置存在問題,則高可用性設定將允許客戶端開始將請求路由到不同物理位置的負載平衡器。

如果應用程式完全託管在單個物理位置上,客戶端重試仍然可以在臨時網路故障的情況下提供幫助,但在物理位置出現故障的情況下整個應用程式將變得不可用。

要使用高可用性設定,請確保您的 DNS 解析為多個 IP 地址供客戶端選擇(例如,每個負載均衡器一個)。有趣的事實:大多數瀏覽器實際上都有內建機制來嘗試與 DNS 響應不同的 IP 地址,以防他們一直使用的 IP 地址沒有響應。

 

負載均衡器配置

負載均衡器通過將請求路由到叢集中的可用例項,在提高軟體系統的可靠性方面發揮著至關重要的作用。它們使用執行狀況檢查確定哪些例項可用,並維護一個執行狀況良好且能夠服務請求的例項的活動列表。

如果負載均衡器沒有收到來自認為健康的例項的預期響應(或任何響應),它應該將該例項標記為不健康,並確保在健康檢查成功之前沒有進一步的請求被路由到該例項。負載均衡器檢測失敗例項的速度越快,失敗的請求就越少,後續重試被路由到另一個健康例項的機會就越大。

雖然負載均衡器通常可以檢測到故障(L4 和 L7 負載均衡器在這裡的工作方式略有不同,如果您不熟悉,我強烈推薦Hussein Nasser 的解釋),它不會為我們重試對不同例項的相同請求——這個責任仍然在客戶端應用程式中。

 

超時

在為我們的客戶端公開 API 時,我們指定最大服務響應時間,即我們的客戶端超時。這就是為什麼以後端總最大超時小於客戶端超時的方式設計系統很重要的原因。

請記住:我們正在使用客戶端庫通過重試失敗的操作來提高系統的感知可用性。理想情況下,為了讓客戶端庫能夠在放棄並通知客戶端應用程式失敗之前“在後臺”重試一次,差異應該至少是 2 倍,這意味著後端超時必須小於 客戶端超時的一半。換句話說,當承諾某些最大響應時間時,我們需要考慮至少一次重試的時間。

通常,用例中涉及的順序操作越多,每個單獨的超時時間應該越短。我在關於 API 故障的文章中從不同的角度提到了超時,並在我[url=https://betterprogramming.pub/architecting-distributed-systems-random-code-8db0cd9b87d1]關於隨機數的文章[/url]中解釋了為什麼應該在超時中新增一些抖動。

 

冪等性

最後但同樣重要的是,如果我們第一次沒有得到預期的結果,我們希望能夠在客戶端安全地重試相同的操作。請求可能已經超時,但操作實際上可能已經成功,在這種情況下重試必須返回初始操作的結果,而不是再次執行。換句話說,如果我們不知道我們的第一次嘗試是否成功,我們希望能夠安全地重試操作。

 

客戶端庫實現

當涉及到客戶端庫的實現時,首先要了解需求是很重要的。我通常會問兩個問題:

  1. 我們的整合重讀還是寫?
  2. 我們可以在傳送之前在客戶端可靠地儲存記錄嗎?

如果客戶端應用程式主要將資料寫入我們的系統,並且這些客戶端可以訪問非易失性記憶體,我會鼓勵使用記憶體來緩衝伺服器尚未成功接收的資料。這是物聯網用例中的常見模式,如果伺服器暫時無法接收從裝置傳送的資料,我們不希望丟失任何遙測資料。一旦伺服器確認它們成功攝取,就可以刪除本地儲存的記錄。

然後,我問第三個問題:客戶端是否需要先離線?由於這不是一個常見的要求,而且出了名的很難做到正確,我假設我們的客戶大部分時間都可以訪問網際網路。

由於大多數時候,客戶端的工作不是寫繁重的,它們不需要離線優先支援,並且是無狀態的,我將在進一步的解釋中重點關注這個場景。

根據最新的行業標準,API 要麼作為HTTP端點公開,要麼使用gRPC框架。對於 HTTP API,事實來源是OpenAPI規範檔案,而如果您要公開 gRPC API,那將是帶有服務和訊息定義的.proto檔案。OpenAPI 和 gRPC 都帶有程式碼生成器生態系統,它們是我們客戶端庫的完美起點。事實上,我們可能想要做的是使用 OpenAPI 和 gRPC 附帶的久經考驗的生成器,並編寫我們自己的帶有額外重試邏輯的外掛。

基本上,我們採用的是這樣的東西,它將由預設編譯器(虛擬碼)生成:

function getWeather(city) {
  return transport.callGetWeather(city); // throws Error
}

並將其包裝在我們自己的錯誤處理邏輯中,例如:

function getWeather(city) {
  try {
    return transport.callGetWeather(city);
  } catch (error) {
    if error.code in (500, 502, 503, 504)
      // perhaps try a different load balancer
      return transport.callGetWeather(city, retry = true);
    else
      throw error;
  }
}

在後臺,客戶端可以決定使用與先前執行的 DNS 解析不同的 IP 地址,以避免將請求重試到相同的物理位置。

不幸的是,我們需要在外掛中為客戶端使用的每種支援的程式語言複製此程式碼生成邏輯。幸運的是,如果操作正確,只需為所有服務執行一次。Google 傾向於使用與生成的庫相同的語言編寫這些外掛,即 Java 生成器外掛是用 Java 編寫的,允許語言社群為專案做出貢獻。

如果某些操作不能自動重試,例如因為它們不是冪等的,則可以在您的 API 規範檔案中將它們標記為這樣。然後,程式碼生成器外掛可以考慮此資訊並有條件地將程式碼包裝在附加重試邏輯中。由於我們可以控制規範檔案和程式碼生成器,因此我們可以根據具體用例獲得所需的靈活性。

 

好處: 

除了遮蔽故障使應用程式感覺更可用之外,客戶端庫還有其他優點,我想在這裡列出其中的一些優點。如果您希望我在單獨的文章中擴充套件這些要點中的一個或多個,請告訴我。

  1. 產品質量。您可以提供有史以來最快、最可靠的服務,但如果客戶犯了整合錯誤並且沒有獲得廣告宣傳的 99.95% 可用性,那麼從客戶的角度來看,整體質量仍然會下降。擁有一個庫而不是普通的 API 文件可以減少整合錯誤的機會,並使整個產品看起來更好。此外,程式碼執行的時間越長(想象成百上千的客戶端執行它),發現和修復錯誤的速度就越快。
  2. 故障排除。如果客戶可以接受資料共享,則可以將錯誤日誌直接從客戶端庫傳送到您的系統,這樣您就可以全面瞭解所發生的情況,同時擁有客戶端和伺服器日誌。
  3. 安全與遷移。我將這兩個分組是因為它們對客戶意味著同樣的事情:簡單。實現安全性可能並非易事,而庫可以隱藏這種複雜性的很大一部分。在遷移方面,對依賴項進行簡單的更新通常就足夠了,並且在發生重大更改的情況下,不同的方法簽名通常比文件更容易理解。
  4. 升級。零停機升級或重新啟動可能很難做到正確,並且開發人員通常傾向於在較小的停機時間而不是額外的複雜性上進行權衡。通過在客戶端遮蔽錯誤,您可以讓升級感覺像是零停機時間,而無需背後的實際工程。

相關文章