我們如何在Linkerd 2.2里設計重試

  • 2019 年 12 月 5 日
  • 筆記

作者:Alex Leong

重試是處理分佈式系統中的部分或瞬態故障的基本機制。但重試也可能是危險的,如果做得不好,他們可以迅速將一個小錯誤升級為系統範圍的中斷。在這篇文章中,我們描述了我們如何在Linkerd 2.2里設計重試,使Linkerd能夠在最小化風險的同時,自動提高系統可靠性。

將路由標記為可重試

在Linkerd 2.2里,我們引入了重試,就是Linkerd能夠自動重試失敗的請求。這使Linkerd能夠自動處理服務中的部分或瞬態故障,而無需應用程序知道:如果請求失敗,Linkerd可以再次嘗試!結合Linkerd的請求級負載平衡,這允許Linkerd處理各個pod的故障。

https://linkerd.io/2/features/load-balancing/

在Linkerd里,您將重試指定為服務配置文件的一部分(在之前的博客文章中介紹)。將路由標記為可重試就像添加isRetryable一樣簡單:設定true到相應的服務配置文件條目:

- name: HEAD /authors/{id}.json      condition:        method: HEAD        pathRegex: /authors/[^/]*\.json      isRetryable: true  

當然,在向路由添加重試行為之前,應該確保路由是冪等的(idempotent)。換句話說,對具有相同參數的相同路由的多次調用將沒有不良影響。這很重要,因為重試(根據定義!)可能導致將同一請求的多個副本發送到服務。如果請求做了非冪等的(non-idempotent)事情,例如從您的銀行帳戶中減去一美元,您可能不希望它自動重試。

啟用後,重試有兩個重要參數:預算(budget)和超時(timeout)。讓我們依次考慮這兩個方面。

使用重試預算

將路由標記為可重試後,Linkerd允許您為服務配置重試預算。Linkerd附帶了合理的默認值,但如果您想自定義預算,可以在服務配置文件中進行設置:

retryBudget:    # The retryRatio is the maximum ratio of retries requests to original    # requests.  A retryRatio of 0.2 means that retries may add at most an    # additional 20% to the request load.    retryRatio: 0.2      # This is an allowance of retries per second in addition to those allowed    # by the retryRatio.  This allows retries to be performed, when the request    # rate is very low.    minRetriesPerSecond: 10      # This duration indicates for how long requests should be considered for the    # purposes of calculating the retryRatio.  A higher value considers a larger    # window and therefore allows burstier retries.    ttl: 10s  

Linkerd使用重試預算,較使用最大重試次數配置重試的常規做法,是更好替代方法。我們花一點時間來理解為什麼。

為什麼預算而不是最大重試次數?

首先,一些背景。配置重試的最常用方法,是指定在放棄之前執行的最大重試次數。對於使用網絡瀏覽器的任何人來說,這是一個熟悉的想法:您嘗試加載網頁,如果沒有加載,則再試一次。如果仍然無法加載,則第三次嘗試。最後您放棄了。

不幸的是,以這種方式配置重試至少有兩個問題:

選擇最大重試次數是猜謎遊戲。您需要選擇一個足夠高的數字,以便在出現某種故障時發揮作用,但不要太高,以至於當系統真正失敗時會在系統上產生額外負載。在實踐中,您通常會從帽子中選擇最大重試次數(例如3),並希望獲得最佳效果。

以這種方式配置的系統易受重試風暴的影響。當一個服務開始出現大於正常的故障率時,重試風暴開始。這會導致其客戶端重試這些失敗的請求。重試帶來的額外負載,會導致服務進一步減速,並使更多請求失敗,從而觸發更多重試。如果每個客戶端配置為最多重試3次,則可以將發送的請求數量翻兩番!更糟糕的是,如果任何客戶端的客戶端配置了重試,則重試次數會成倍增加,並且可以將少量錯誤轉化為自我造成的拒絕服務攻擊。

為了避免這些問題,Linkerd使用重試預算。Linkerd不是為每個請求指定固定的最大重試次數,而是跟蹤常規請求和重試之間的比率,並將此數字保持在限制之下。例如,您可以指定要重試最多添加20%的請求。然後,Linkerd將儘可能多地重試,同時保持該比率。

因此,使用重試預算可以明確在提高成功率和額外負載之間進行權衡。您的重試預算正是您的系統願意從重試中接受的額外負載。

(最後,Linkerd的重試預算還包括允許的最小重試次數,這將是唯一允許的,與比率無關。這使得Linkerd可以在非常低的流量系統中重試。)

設置每個請求的超時

除了預算之外,重試還按每個請求的超時參數。超時可確保始終失敗的請求最終會返迴響應,即使該響應失敗也是如此。超時時,Linkerd將取消請求並返回HTTP 504響應。

與重試預算類似,重試超時具有可在服務配置文件中覆蓋的合理默認值:

- name: HEAD /authors/{id}.json      condition:        method: HEAD        pathRegex: /authors/[^/]*\.json      timeout: 50ms  

誰管有重試行為?客戶端還是服務器?

您可能已經注意到上面的配置片段中的有趣內容。在「傳統」重試系統(例如Web瀏覽器)中,是在客戶端上配置重試行為,畢竟,這是重試實際發生的地方。但是在上面的服務配置文件中,我們將在服務器端指定重試政策。

能夠將政策附加到服務器端,但客戶端必須遵守該政策,這是Linkerd服務配置文件方法的基本優勢之一。重試配置在邏輯上屬於服務級別(「這是您應該和我說話的方式」)。由於Linkerd控制客戶端和服務器行為,我們可以正確的方式執行此操作:服務配置文件允許服務準確發佈「這是我希望您與我交談的方式」,通過Linkerd的所有流量,無論來源如何,會尊重這種行為。太酷了!

把它們放在一起

我們已經展示了如何通過組合超時、預算和可重試性來配置Linkerd的重試行為。現在讓我們將它們放在一起進行簡短的演示。如果您有一個終端窗口和一個Kubernetes集群,您可以在家裡跟隨。

我們首先安裝Linkerd和我們的樣本書應用程序:

$ linkerd install | kubectl apply -f -  $ curl https://run.linkerd.io/bookapp.yml | linkerd inject - | kubectl apply -f -  $ linkerd check  

關於這個應用程序,我們可以注意到的一件事是,從書籍服務到作者服務的請求的成功率非常低:

$ linkerd routes deploy/books --to svc/authors  ROUTE       SERVICE   SUCCESS      RPS   LATENCY_P50   LATENCY_P95   LATENCY_P99  [DEFAULT]   authors    54.24%   3.9rps           5ms          14ms          19ms  

為了更好地了解這裡發生了什麼,讓我們為作者服務添加一個服務配置文件,從Swagger定義生成:

$ curl https://run.linkerd.io/booksapp/authors.swagger | linkerd profile --open-api - authors | kubectl apply -f  -  $ linkerd routes deploy/books --to svc/authors  ROUTE                       SERVICE   SUCCESS      RPS   LATENCY_P50   LATENCY_P95   LATENCY_P99  DELETE /authors/{id}.json   authors     0.00%   0.0rps           0ms           0ms           0ms  GET /authors.json           authors     0.00%   0.0rps           0ms           0ms           0ms  GET /authors/{id}.json      authors     0.00%   0.0rps           0ms           0ms           0ms  HEAD /authors/{id}.json     authors    50.85%   3.9rps           5ms          10ms          17ms  POST /authors.json          authors     0.00%   0.0rps           0ms           0ms           0ms  [DEFAULT]                   authors     0.00%   0.0rps           0ms           0ms           0ms  

有一件事是清楚的,從書籍到作者的所有請求都是針對HEAD /authors/{id}.json路線,這些請求在大約50%的時間內失敗。為了糾正這個問題,讓我們編輯作者服務配置文件,並使這些請求可以重試:

$ kubectl edit sp/authors.default.svc.cluster.local  [...]    - condition:        method: HEAD        pathRegex: /authors/[^/]*\.json      name: HEAD /authors/{id}.json      isRetryable: true ### ADD THIS LINE ###  

在編輯服務配置文件後,我們看到成功率幾乎立即有所改善:

$ linkerd routes deploy/books --to svc/authors -o wide  ROUTE                       SERVICE   EFFECTIVE_SUCCESS   EFFECTIVE_RPS   ACTUAL_SUCCESS   ACTUAL_RPS   LATENCY_P50   LATENCY_P95   LATENCY_P99  DELETE /authors/{id}.json   authors               0.00%          0.0rps            0.00%       0.0rps           0ms           0ms           0ms  GET /authors.json           authors               0.00%          0.0rps            0.00%       0.0rps           0ms           0ms           0ms  GET /authors/{id}.json      authors               0.00%          0.0rps            0.00%       0.0rps           0ms           0ms           0ms  HEAD /authors/{id}.json     authors             100.00%          2.8rps           58.45%       4.7rps           7ms          25ms          37ms  POST /authors.json          authors               0.00%          0.0rps            0.00%       0.0rps           0ms           0ms           0ms  [DEFAULT]                   authors               0.00%          0.0rps            0.00%       0.0rps           0ms           0ms           0ms  

成功率看起來很好,但p95和p99延遲有所增加。這是可以預料到的,因為重試需要時間。但是,我們可以通過設置超時,Linkerd 2.x的另一個新功能,在我們願意等待的最長持續時間來限制此操作。出於本演示的目的,我將設置25ms的超時。您的結果將根據系統的特性而有所不同。

$ kubectl edit sp/authors.default.svc.cluster.local  [...]    - condition:        method: HEAD        pathRegex: /authors/[^/]*\.json      isRetryable: true      name: HEAD /authors/{id}.json      timeout: 25ms ### ADD THIS LINE ###  

我們現在看到成功率略有下降,因為有些請求超時,但尾部延遲已大大減少:

$ linkerd routes deploy/books --to svc/authors -o wide  ROUTE                       SERVICE   EFFECTIVE_SUCCESS   EFFECTIVE_RPS   ACTUAL_SUCCESS   ACTUAL_RPS   LATENCY_P50   LATENCY_P95   LATENCY_P99  DELETE /authors/{id}.json   authors               0.00%          0.0rps            0.00%       0.0rps           0ms           0ms           0ms  GET /authors.json           authors               0.00%          0.0rps            0.00%       0.0rps           0ms           0ms           0ms  GET /authors/{id}.json      authors               0.00%          0.0rps            0.00%       0.0rps           0ms           0ms           0ms  HEAD /authors/{id}.json     authors              97.73%          2.9rps           49.71%       5.8rps           9ms          25ms          29ms  POST /authors.json          authors               0.00%          0.0rps            0.00%       0.0rps           0ms           0ms           0ms  [DEFAULT]                   authors               0.00%          0.0rps            0.00%       0.0rps           0ms           0ms           0ms  

請注意,由於直方圖分段工件,p99延遲似乎大於我們的25ms超時。

總結

在這篇文章中,我們描述了Linkerd如何以最小化系統風險的方式自動重試請求。我們描述了為什麼在服務器,而不是客戶端級別,指定了重試行為,我們向您介紹了如何在演示應用程序中部署服務的重試和超時功能。

重試是Linkerd可靠性路線圖中的一大進步。服務配置文件、重試和診斷的交集是Linkerd特別令人興奮的領域,您可以期待未來版本中更酷的功能。敬請期待!

https://github.com/linkerd/linkerd2/issues/2016

喜歡這篇文章?Linkerd是一個社區項目,由CNCF託管。如果您有功能請求、問題或評論,我們很樂意讓您加入我們快速發展的社區!Linkerd的倉庫在GitHub上,我們在Slack、Twitter和郵件列表上擁有一個蓬勃發展的社區。快來加入吧!