banner
田野放空

田野放空

认真扮演一个擅长白日做梦的普通人

如何設計一個精壯的服務

最近辭職以後,在準備面試和簡歷期間,回顧了之前在工作期間出現的服務問題。有一些事故中,是因為某個或某幾個服務被高峰期的流量打趴,可能是資料庫問題,亦有可能是內存等等各種問題。其深層次的背後,是服務本身不夠健壯,服務的某個或某幾個節點被打趴以後,導致重試風暴,繼而又導致雪崩的出現。如果這些服務是主流程上的服務的話,那就有可能導致全系統的雪崩。

image-20230401221427201

我們的很多服務在壓力下的表現如上圖所示,在到達最大負載之前還是可以正常提供服務的,但隨著流量一旦突破服務所能承受的最大極限後,對外的服務能力急轉直下,即使這時候流量沒有過載了,甚至把流量完全降級為 0,其服務能力也久久不能恢復。

甚至有的情況下好不容易等到服務能力恢復了,上層流量切回來以後,服務瞬間又被大量人工或自動的重試請求給打趴下了。

如果我們的後臺服務表現如此的 “脆”,只要過載立刻就嘎嘣倒下,久久不爬起來,那我們的穩定性保障工作是沒法做的。在大型的應用中,全鏈路有幾千個服務,每個服務又有很多的節點。這種情況下,是很難保證,以及很難監控到所有的節點的穩定,系統不被流量打趴。這種情況也是做不到的。因為想不到的局部的異常是永遠都會出現的。

因此,我們要整個應用做好健壯性,應用的每個服務都應該具備一定的健壯性。對於單個服務本身 (無論是單機還是分佈式),其實這個要求並不高,也不太難做到。

image-20230401222233706

我們要做到,一個 “精壯” 的服務,在壓力下的表現應該如上圖所示,當負載加大的時候輸出線性提升,當外部負載超過服務理論能承受的最大負載後時,經過一個短暫的抖動後,不管外部負載有多高,服務都能穩定的對外輸出,並且這個輸出最好能比較貼近理論的最大負載。也就是說,一個 “精壯” 的服務即便在超負荷的情況下,也能夠對外提供穩定輸出的服務,超過處理能力的請求,應該果斷的拒絕服務

  • 一個服務的每個節點都需要有能夠實時表徵自己服務負載的指標,當指標超出服務能力上限的時候,拒絕超出服務能力的請求。
  • 服務端要和客戶端(這裡的客戶端不只是指 APP,而是這個服務自己的客戶端,一般來說是集成夠知曉後端的負載情況,調整自己的策略。在另一個服務裡的 SDK,當然,也可能是 APP 裡的 SDK,下同)之間要有反饋機制,客戶端要能知曉後端的負載情況,調整自己的策略。

通俗來講,對於每個服務的每個節點:1. 你得清楚自己現在忙不忙,是不是快到極限了。2. 你忙你得跟你的客戶端說一聲,別它不知道,一不小心把你給弄趴下了。。。

接下來展開講一下這兩點,如何找到表徵自己服務負載的指標,以及如何在過載的時候和客戶端協商。

表徵服務負載的指標#

表徵服務負載的指標,指的是當這一指標越過某個閾值的時候,該服務節點的服務能力會急劇惡化並久久不能恢復。

我們可以通過壓測,去找到標準伺服器環境下的較合理的 QPS、TPS 等值。然後通過限流、熔斷等工具去進行峰值限制。但是可以根據實際情況,將限流峰值限制在一個區間內:

  1. 只要這個服務節點是可以水平擴展的,就不需要太精確,不需要過度追求讓服務壓著 CPU 或其他資源極限跑,根據壓測結果,設定一個不那麼激進的值即可,可以通過混佈甚至超售進一步壓榨伺服器資源利用率。
  2. 要考慮超時時間,否則就算處理完了請求也沒用,客戶端可能已經超時了,請求返回也只會被丟棄。
  3. 要考慮一些機器是否會運行其他功能的服務,比如說 機器監控工具、主備節點同步功能、心跳功能。因此,在閾值的設定上,要留有餘地。更精細的服務甚至可以考慮請求分級,針對每個優先級設定不同的計數器。
  4. 有人可能會想這個計數器的閾值是不是搞成自適應的,即根據服務節點內部資源情況動態調整閾值大小,是不是更牛逼。說實話,絕大多數情況下,不需要搞成這樣子,越簡單越不容易出錯,很少的情況下需要死扣單節點的性能,在如今機器爆炸的年代,服務穩定比機器更貴。

客戶端與服務端的協同#

說實話我對客戶端不是很了解,但是這個問題實際上還是後端繁忙情況下的重試邏輯的一個優化。

客戶端的重試策略其實應該是一個很精細的邏輯,只不過特別容易被人忽略而已。一般的重試策略,大家很容易想到的都是簡單的等間隔重試,周到一些的會考慮按照等比數列或者斐波那契數列等,逐步拉長間隔的重試。

但仔細去想,重試實際上是為了解決兩類問題。一個是對網絡丟包或瞬斷的 fail over,一個是對服務端節點故障的 fail over。對於前者,重試間隔其實越小越好,最好是瞬時重試;但對於後者,這個間隔如果太小,很容易引起重試風暴,讓後端節點死的更慘。然而,悲劇就在於,站在客戶端的角度,我們其實區分不了到底發生的是哪種情況。

因此,最簡單的解決辦法是,當服務端的節點繁忙的時候,並不是簡單的丟棄收到的請求,而是對於這些請求,都向客戶端返回 RET_BUSY。客戶端一旦收到 RET_BUSY,都應該跳出當前重試邏輯,直接拉長重試的間隔,避免造成重試風暴。更進一步的,服務端可以根據自己情況對返回值進行分級,例如 RET_RETRY,讓客戶端立刻發起重試,RET_BUSY 和 RET_VERY_BUSY 讓客戶端拉長到不同的重試周期。這裡其實有非常多精細的玩法,能夠解決很多複雜的問題,大家可以結合自己的工程場景實踐一下。

到此為止,我們就講完了如何做一個 “精壯” 的服務。本質上是用非常簡單的工程方法,去實現能初步識別自我繁忙程度的服務端,和能夠根據服務端繁忙程度自適應重試的客戶端。從而做到不管外部請求壓力有多大,都能提供可控輸出。

在實際的工程實踐中,要做到服務端節點,能夠在被壓到平均 CPU 利用率 70% 以上,瞬時 CPU 利用率超過 90%的情況下,提供持續穩定的對外服務。在測試環境裡能夠做到在極限情況下的持續穩定輸出。

能不能做到服務無感實現的情況下上述健壯性#

這套機制能不能通過外部監控,結合流量降級方案實現在外圍,這樣是不是就不需要動服務本身了?

首先,外部監控 + 流量降級方案,是無論如何一定要有的,它是一切的兜底方案。但是,監控是有滯後的,並且,流量降級是有損的,這導致降級需要謹慎。這些因素都決定了,外部監控 + 流量降級這套機制,是用來解決故障,而非讓服務更加健壯的。服務的健壯性本身,要依賴服務自身來解決。

哪些服務需要改造#

上述這套機制是可以做進 RPC 框架裡的,對於絕大多數要求沒那麼高的服務,也是足夠用了。

分佈式存儲系統會更高要求性能問題。存儲系統在穩定性上的工程難度確實是最複雜的,最基本的,存儲節點不能通過簡單重啟來解決問題;並且資源消耗維度複雜,除了 CPU 外,還有內存以及 page cache,網絡吞吐和中斷,磁盤 iops 和吞吐等諸多資源考量維度;更進一步的,流量降級以後通常只有存儲系統是不能快速恢復服務的(例如,需要等待主從同步,需要等待 minor 或 major compaction, 需要等待刷盤等等),因此,在線存儲系統的健壯性,需要考慮的因素通常是要更多的。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。