Google 怎麼解決長尾延遲問題

cch123發表於2021-05-10

The Tail at Scale,是 Google 2013 年發表的一篇論文,大規模線上服務的長尾延遲問題。

要知道怎麼解決長尾問題,先要理解長尾延遲是個什麼問題,在開發線上服務的時候,我們都知道要關注服務的 p99/p999 延遲,要讓大部分使用者都能夠在預期的時間範圍內獲得響應。

下面是一個不同響應時間的請求數分佈圖:

tail_latency

大部分系統也都遵循這種分佈規律,現在網際網路的系統規模比較大,一個服務依賴幾十上百個服務的情況都是有可能的。單一模組的長尾延遲會在有大量依賴的情況下,在服務粒度被放大,《The Tail at Scale》論文裡給出了這樣的例子。

考慮一個系統,大部分服務呼叫在 10ms 內響應,但 99 分位數的延遲為 1 秒。如果一個使用者請求只在一個這樣的服務上處理,那麼 100 個使用者請求中只有一個會很慢(一秒鐘)。 這裡的圖表概述了在這種假設的情況下,服務級別的延遲是如何被非常小概率的大延遲值影響的。

tail

如果一個使用者請求必須從 100 個這樣的服務並行收集響應,那麼 63% 的使用者請求將需要超過一秒鐘(圖中標記為 "x")。 即使對於只有萬分之一概率在單臺伺服器上遇到超過一秒的響應延遲的服務,如果服務的規模到達 2000 例項的話,也會觀察到幾乎五分之一的使用者請求需要超過一秒(圖中標記為 "o")。

Xargin 注:

  1. 因為請求是並行的,所以只受最慢的那個影響,100 個請求都落在 99 分位內延遲才會小於 1s,所以延遲小於 1s 的概率是 pow(0.99, 100) = 0.3660323412732292,也就是說一定有 63% 的概率會超過 1s。
  2. 第二個,我們落在 99 分位的概率是 pow(0.9999, 2000) = 0.8187225652655495,也就是將近 20% 的使用者響應會超過 1s。

tail2

上面的表格列出了一個真實的谷歌服務的測量結果,該服務在邏輯上與前文簡化過的場景相似;根服務通過中間服務將一個請求分發到非常多的葉子服務。表展示了大量扇出 (fan-out) 呼叫時,對延遲分佈的影響。

在根伺服器上測量的單個隨機請求完成的 99 分位延遲是 10ms。然而,所有請求完成的 99 分位數延遲是 140ms,95% 的請求 99 分位數延遲是 70ms,這意味著等待最慢的 5% 的慢請求要對總的 99 百分位數延遲的一半負責。對這些慢請求場景進行優化,會對服務的整體延遲產生巨大影響。

為什麼會有長尾延遲?

單個服務元件的長尾高延遲可能由於許多原因產生,包括:

共享資源 (Shared resources,Xargin: 混部現在越來越多了)。機器可能被爭奪共享資源 (如 CPU 核心、處理器快取、記憶體頻寬和網路頻寬) 的不同應用所共享,而在同一應用中,不同的請求可能爭奪資源。 守護程式 (Daemons)。後臺守護程式可能平均只使用有限的資源,但在執行時可能會產生幾毫秒的峰值抖動。 全域性資源共享 (Global resource sharing)。在不同機器上執行的應用程式可能會爭搶全域性資源 (如網路交換機和共享檔案系統)。 維護活動 (Maintenance activities)。後臺活動 (如分散式檔案系統中的資料重建,BigTable 等儲存系統中的定期日誌壓縮,以及垃圾收集語言中的定期垃圾收集) 會導致週期性的延遲高峰。 排隊 (Queueing)。中間伺服器和網路交換機中的多層佇列放大了這種可能性。

同時也是因為當前硬體的趨勢:

功率限制 (Power limits.)。現代 CPU 被設計成可以暫時執行在其平均功率之上 (英特爾睿頻加速技術),如果這種活動持續很長時間,之後又會通過節流 (throttling) 限制發熱; 垃圾收集 (Garbage collection)。固態儲存裝置提供了非常快的隨機讀取訪問,但是需要定期對大量的資料塊進行垃圾收集,即使是適度的寫入活動,也會使讀取延遲增加 100 倍; 能源管理 (Energy management)。許多型別的裝置的省電模式可以節省相當多的能量,但在從非活動模式轉為活動模式時,會增加額外的延遲。

解決方案

模組內儘量降低長尾延遲

服務分級 && 優先順序佇列 (Differentiating service classes and higher-level queuing)。差異化服務類別可以用來優先排程使用者正在等待的請求,而不是非互動式請求。保持低階佇列較短,以便更高階別的策略更快生效。

減少隊頭阻塞 (Reducing head-of-line blocking)。將需要長時間執行的請求打散成一連串小請求,使其可以與其它短時間任務交錯執行;例如,谷歌的網路搜尋系統使用這種時間分割,以防止大請求影響到大量其它計算成本較低的大量查詢延遲。(隊頭阻塞還有協議和連線層面的問題,需要通過使用更新的協議來解決,比如 h1 -> h2 -> h3 的升級思路)

管理後臺活動和同步中斷 (Managing background activities and synchronized disruption)。後臺任務可能產生巨大的 CPU、磁碟或網路負載;例子是面向日誌的儲存系統的日誌壓縮和垃圾收集語言的垃圾收集器活動。可以結合限流功能,把重量級操作分解成成本較低的操作,並在整體負載較低的時候觸發這些操作 (比如半夜),以減少後臺活動對互動式請求延遲的影響。

請求期間內的一些自適應手段

對衝請求 (Hedged requests)。 抑制延遲變化的一個簡單方法是向多個副本發出相同的請求 (Go 併發模式中的 or channel),並使用首先響應的結果。一旦收到第一個結果,客戶端就會取消剩餘的未處理請求。不過直接這麼實現會造成額外的多倍負載。所以需要考慮優化。

一個方法是推遲傳送第二個請求,直到第一個請求到達 95 分位數還沒有返回。這種方法將額外的負載限制在 5% 左右,同時大大縮短了長尾時間。

捆綁式請求 (Tied requests)。不像對衝一樣等待一段時間傳送,而是同時發給多個副本,但告訴副本還有其它的服務也在執行這個請求,副本任務處理完之後,會主動請求其它副本取消其正在處理的同一個請求。需要額外的網路同步。

跨請求的自適應手段

微分割槽 (Micro-partition)。把伺服器區分成很多小分割槽,比如一臺大伺服器分成 20 個 partition,這樣無論是流量調整或者故障恢復都可以快非常多。以細粒度來調整負載便可以儘量降低負載不均導致的延遲影響。

選擇性的複製 (Selective replication)。為你檢測到的或預測到的會很熱的分割槽增加複製因子。然後,負載均衡器可以幫助分散負載。 谷歌的主要網路搜尋系統採用了這種方法,在多個微分割槽中對流行和重要的檔案進行額外的複製。(就是熱點分割槽多準備些例項啦,感覺應該需要按具體業務去做一些預測)

將慢速機器置於考察期 (Put slow machines on probation)。當檢測到一臺慢速機器時,暫時將其排除在操作之外 (circuit breaker)。由於緩慢往往是暫時的,監測何時使受影響的系統重新上線。繼續向這些被排除的伺服器發出影子請求,收集它們的延遲統計資料,以便在問題緩解時將它們重新納入服務中。(這裡就是簡單的熔斷的實現)

一些其它的權衡

考慮 “足夠好” 的響應 (Consider 'good enough' responses)。一旦所有的伺服器中有足夠的一部分做出了響應,使用者可能會得到最好的服務,即得到輕微的不完整的結果,以換取更好的端到端延遲。(這裡應該是大家常說的降級,如果不完整的結果對於使用者來說已經足夠有用,那不重要的不展示也可以)

使用金絲雀請求 (canary requests)。在具有非常高扇出的系統中可能發生的另一個問題是,一個特定的請求觸發了未經測試的程式碼路徑,導致崩潰或同時在成千上萬的伺服器上出現極長的延遲。為了防止這種相關的崩潰情況,谷歌的一些 IR 系統採用了一種叫做 “金絲雀請求” 的技術;根伺服器並不是一開始就把一個請求傳送給成千上萬的葉子伺服器,而是先把它傳送給一個或兩個葉子伺服器。其餘的伺服器只有在根伺服器在合理的時間內從金絲雀那裡得到成功的響應時才會被查詢。

更多原創文章乾貨分享,請關注公眾號
  • Google 怎麼解決長尾延遲問題
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章