每秒超一百萬次請求,Netflix如何做負載均衡?
原創(chuàng)【51CTO.com原創(chuàng)稿件】Netflix 云網關團隊一直致力于幫助系統(tǒng)減少錯誤、提高可用性,增強 Netflix 應對故障的能力。
這么做是因為在每秒超過一百萬次請求的這等規(guī)模下,哪怕很低的錯誤率也會影響會員的體驗,所以降低錯誤率是必要的。
因此,我們開始向 Zuul 和其他團隊取經,改進我們的負載均衡機制,進一步減少服務器過載導致的錯誤。
在 Zuul 中,我們歷來使用 Ribbon 負載均衡器(https://github.com/Netflix/ribbon/),另外使用輪詢調度(round-robin)算法和用于將連接故障率高的服務器列入黑名單的一些過濾機制。
這些年來,我們做了多次改進和定制,旨在向最近啟動的服務器發(fā)送較少的流量以免過載。
這些取得了顯著的成效,但對于一些特別麻煩的源集群,我們仍會看到與負載有關的錯誤率遠高于預期。
如果集群中所有服務器過載,我們選擇某一臺服務器而不是另一臺幾乎沒什么改進。
但我們常看到只有一部分服務器過載的情況,比如:
- 服務器在啟動后(在紅黑部署和自動擴展事件期間)。
- 服務器因交錯的動態(tài)屬性/腳本/數(shù)據(jù)更新或大型垃圾回收(GC)事件而暫時減速/阻塞。
- 壞的服務器硬件。我們會??吹揭恍┓掌鞯倪\行速度永遠不如其他服務器,無論原因是嘈雜的相鄰系統(tǒng)還是不同的硬件。
指導原則
開始做項目時有必要恪守幾個原則,以下是該項目遵循的幾個原則。
注重現(xiàn)有負載均衡器框架的約束
我們將之前的負載均衡定制與 Zuul 代碼庫相結合,因而無法與 Netflix 的其他團隊共享這些定制。
于是這回我們決定接受約束和所需的額外投入,一開始就牢記重用性。因而更容易被其他系統(tǒng)所采用,減小了重新發(fā)明輪子的機會。
向別人借鑒經驗
試著借鑒別人的想法和技術。比如,之前在 Netflix 的其他 IPC 堆棧中考察過的 choice-of-2 和考察(probation)算法。
避免分布式狀態(tài)
優(yōu)先考慮本地決策,避免跨集群協(xié)調狀態(tài)帶來的彈性問題、復雜性和延滯。
避免客戶端配置和手動調整
多年來我們在 Zuul 方面的運營經驗表明,將服務配置的一部分放在不屬于同一團隊的客戶端服務中會導致問題。
一個問題是,這些客戶端配置往往與不斷變化的服務器端配置不同步,或者需要結合屬于不同團隊的服務之間的變更管理。
比如說,升級用于服務 X 的 EC2 實例類型,導致該集群所需的節(jié)點更少。因此,現(xiàn)在應增加服務 Y 中“每個主機的***連接數(shù)”客戶端配置,以體現(xiàn)新增的容量。
應該先進行客戶端更改?還是進行服務器端更改?還是同時進行?設置很有可能完全被遺忘,導致更多的問題。
如果可能的話,使用根據(jù)當前流量、性能和環(huán)境來更改的自適應機制,而不是配置靜態(tài)閾值。
若確實需要靜態(tài)閾值,讓服務在運行時傳達這一切,避免跨團隊推送變更帶來的問題,而不是讓服務團隊協(xié)調每個客戶端的閾值配置。
負載均衡方法
一個總體思路是,雖然對服務器端延遲而言***的數(shù)據(jù)源是客戶端視圖,但服務器利用率方面的***數(shù)據(jù)源來自服務器本身。結合這兩個數(shù)據(jù)源可為我們提供最有效的負載均衡。
我們結合使用了相互補充的機制,大多數(shù)機制之前由別人開發(fā)和使用:
- 在服務器之間選擇的 choice-of-2 算法。
- 主要根據(jù)負載均衡器了解服務器利用率的情況進行均衡。
- 其次根據(jù)服務器了解利用率的情況進行均衡。
- 基于考察和服務器年限的機制,避免剛啟動的服務器過載。
- 收集的服務器統(tǒng)計數(shù)據(jù)慢慢衰減為零。
結合加入最短隊列和服務器報告的利用率
我們選擇結合常用的加入最短隊列(JSQ)算法和基于服務器報告的利用率的 choice-of-2 算法,試圖集兩者之所長。
JSQ 的問題
加入最短隊列非常適用于單個負載均衡器,但如果單獨用在負載均衡器集群中會有嚴重問題。
問題是,負載均衡器往往會放牧(herd),同時選擇相同的低利用率服務器,因而過載,然后進入到下一臺利用率***的服務器并使它過載,依次類推.....
這可以通過結合使用 JSQ 和 choice-of-2 算法來解決。這基本上解決了放牧問題。
JSQ 一般通過計算僅從本地負載均衡器到服務器的使用中連接的數(shù)量來實現(xiàn),但是負載均衡器節(jié)點有幾十個至幾百個時,本地視圖可能會誤導。
圖 1:單個負載均衡器的視圖可能與實際情況大不相同
比如在此圖中,負載均衡器 A 對服務器 X 有 1 個正在處理(inflight)的請求,對服務器 Z 有 1 個請求,但對服務器 Y 沒有請求。
因此它收到新請求、要選擇哪臺服務器的利用率***時,從可用數(shù)據(jù)來看,它會選擇服務器 Y。
雖然這不是正確的選擇,服務器 Y 實際上是利用率***的,因為另外兩個負載均衡器目前都有正在處理的請求,但負載均衡器 A 無法知道這點。這表明了單個負載均衡器的視圖如何與實際情況完全不同。
我們僅依賴客戶端視圖遇到的另一個問題是,對于龐大集群(尤其是結合低流量)而言,負載均衡器常常與幾百臺服務器中的一部分服務器只有幾條正在使用的連接。
因此,當它選擇哪臺服務器的負載最小時,通常只能在零和零之間選擇,即它沒有它要選擇的任何一臺服務器的利用率方面的數(shù)據(jù),因此只好隨機猜測。
解決這個問題的一個辦法是,與所有其他負載均衡器共享每個負載均衡器的正在處理的請求數(shù)量的狀態(tài),但那樣就要解決分布式狀態(tài)問題。
我們通常采用分布式可變狀態(tài)作為***的手段,因為獲得的好處需要壓倒涉及的實際成本:
- 給部署和金絲雀測試(canarying)等任務增添了運營開銷和復雜性。
- 與數(shù)據(jù)損壞的影響范圍有關的彈性風險(即 1% 負載均衡器上的壞數(shù)據(jù)很煩人,100% 負載均衡器上的壞數(shù)據(jù)是故障)。
- 在負載均衡器之間實現(xiàn) P2P 分布式狀態(tài)系統(tǒng)的成本,或運行擁有處理這龐大讀寫流量所需的性能和彈性的單獨數(shù)據(jù)庫的成本。
我們選擇了另一個更簡單的解決方案,改而依賴向每個負載均衡器報告服務器的利用率。
服務器報告的利用率
使用每臺服務器了解的利用率有這個優(yōu)點:它整合了使用該服務器的所有負載均衡器的情況,因此避免了 JSQ 的問題:了解的情況不完整。
我們有兩種方法可以實現(xiàn)這一點:
- 使用健康狀況檢查端點,主動輪詢每臺服務器的當前利用率。
- 被動跟蹤來自用當前利用率數(shù)據(jù)標注的服務器的響應。
我們選擇了第二種方法,原因很簡單,便于頻繁更新這些數(shù)據(jù),還避免了給服務器增添額外的負擔:每隔幾秒就讓 N 個負載均衡器輪詢 M 臺服務器。
這種被動策略的一個影響是,負載均衡器向一臺服務器發(fā)送請求的頻次越高,它獲得的該服務器利用率的視圖越新。
因此,每秒請求數(shù)(RPS)越高,負載均衡的效果越好。但反過來,RPS 越低,負載均衡效果越差。
這對我們來說不是問題,但對于通過一個特定負載均衡器收到低 RPS(同時通過另一個負載均衡器收到高 RPS)的服務來說,主動輪詢健康狀況檢查可能更有效。
轉折點出現(xiàn)在負載均衡器向每臺服務器發(fā)送的 RPS 低于用來健康狀況檢查的輪詢頻次。
服務器端實現(xiàn)
我們在服務器端實現(xiàn)了這個機制,只需跟蹤正在處理的請求數(shù),將其轉換成該服務器的配置***值百分比,然后將其作為 HTTP 響應頭來寫出:
- X-Netflix.server.utilization: [, target=]
可以由服務器指定可選的目標利用率,表明它們在正常條件下打算以怎樣的百分比利用率來運行。然后負載均衡器將其用于稍后介紹的粗粒度過濾。
我們用正在處理的請求數(shù)之外的指標進行了一番實驗,比如操作系統(tǒng)報告的 CPU 利用率和平均負載,但發(fā)現(xiàn)它們引起了變動。因此,我們決定使用相對簡單的實現(xiàn)方法:只計算正在處理的請求。
choice-of-2 算法而不是輪詢調度算法
由于我們希望能夠通過比較統(tǒng)計數(shù)據(jù)來選擇服務器,因此棄用了現(xiàn)有的簡單的輪詢調度算法。
我們嘗試的另一種方法是 JSQ 結合 ServerListSubsetFilter 減少 distributed-JSQ 的放牧問題。這得到了合理的結果,但是因而在目標服務器上的請求分布仍然太寬。
因此,我們改而運用了 Netflix 的另一個團隊汲取的早期經驗,實現(xiàn)了 choice-of-2 算法。優(yōu)點是易于實現(xiàn),使負載均衡器上的 CPU 成本保持很低,且請求分布良好。
基于多個因素進行選擇
要在服務器之間進行選擇,我們針對三個不同的因素來比較:
- 客戶端健康狀況:該服務器的連接相關錯誤的滾動百分比。
- 服務器利用率:該服務器提供的***分數(shù)。
- 客戶端利用率:從該負載均衡器到該服務器的正在處理的請求的當前數(shù)量。
這三個因素用于為每臺服務器分配分數(shù),然后比較總分數(shù)來選擇獲勝者。使用這樣的多個因素確實加大了實現(xiàn)的復雜性,但可以防范僅僅依賴一個因素而出現(xiàn)的極端情況問題。
比如說,如果一臺服務器開始失效、拒絕所有請求,它報告利用率會低得多(由于拒絕請求比接受請求更快);如果這是唯一所用的因素,那么所有負載均衡器將開始向這臺壞的服務器發(fā)送更多的請求??蛻舳私】禒顩r因素可緩解這種情形。
過濾
隨機選擇要比較的 2 臺服務器時,我們過濾掉高于針對利用率和健康狀況而保守配置的閾值的任何服務器。
對每個請求進行這種過濾,避免只是定期過濾的過時問題。為避免在負載均衡器上造成高 CPU 負載,我們只盡力而為:作 N 次嘗試以找到一臺隨機選擇的可行服務器,然后必要時回退到非過濾的服務器。
服務器池中大部分服務器存在持久性問題時,這種過濾大有幫助。在這種情況下,隨機選擇 2 臺服務器會經常導致選擇 2 臺糟糕的服務器進行比較,盡管有許多好的服務器可用。
但這么做的缺點是依賴靜態(tài)配置的閾值,我們試圖避免這種情況。不過,測試結果讓我們相信值得添加這種方法,即便使用了一些通用的閾值。
考察
對于負載均衡器尚未收到響應的任何服務器而言,我們只允許每次只有一個正在處理的請求。我們過濾掉這些考察期(in-probation)的服務器,直至收到來自它們的響應。
這有助于在讓剛啟動的服務器有機會表明利用率如何之前,避免它們因大量請求而過載。
基于服務器年限的預熱
我們利用服務器年限在前 90 秒內逐步加大發(fā)送到剛啟動的服務器的流量。這是另一個類似考察的機制,進一步提醒在有時微妙的啟動后狀態(tài)避免服務器過載。
統(tǒng)計數(shù)據(jù)衰減
為了確保服務器不被***性列入黑名單,我們對收集用于負載均衡中的所有統(tǒng)計數(shù)據(jù)使用了衰減率(decay rate,目前是 30 秒內的線性衰減)。
比如說,如果服務器的錯誤率上升到 80%、我們停止向它發(fā)送流量,我們使用的值將在 30 秒內衰減到零(即 15 秒后錯誤率將是 40%)。
運營影響
更寬的請求分布
不將輪詢調度用于負載均衡的負面影響時,之前我們跨集群服務器有非常緊湊的請求分布,現(xiàn)在服務器之間的變化(delta)更大。
使用 choice-of-2 算法有助于大大緩解這種情況(與跨集群中所有或部分服務器的 JSQ 相比),但不可能完全避免。
因此需要在運營方面考慮這一點,對于我們通常比較請求數(shù)、錯誤率和 CPU 等指標的絕對值的金絲雀分析而言更是如此。
較慢的服務器收到較少的流量
很顯然這是預期的效果,但對于過去常用輪詢調度(流量平均分配)的團隊來說,這在運營方面帶來了一些連鎖反應。
由于跨源服務器的流量分布現(xiàn)在依賴其利用率,如果一些服務器在運行效率更高或更低的不同構建(build),會收到更多或更少的流量,所以:
- 集群采用紅黑部署時,如果新的服務器組出現(xiàn)了性能衰退,發(fā)送到該組的流量其比例就會小于 50%。
- 金絲雀系統(tǒng)方面可以看到相同的結果――基線系統(tǒng)可能收到與金絲雀集群不同大小的流量。因此查看指標時,***看看 RPS 和 CPU 的組合(比如金絲雀系統(tǒng)中 RPS 可能較低,而 CPU 相同)。
- 不太高效的異常檢測――我們通常有自動化技術來監(jiān)控集群中的異常服務器(通常是啟動后因某個硬件問題而立即慢下來的虛擬機),并終止它們。那些異常服務器因負載均衡而收到較少的流量時,這種檢測更難了。
滾動動態(tài)數(shù)據(jù)更新
從輪詢調度改用這種新負載均衡器的好處是,可以與分階段部署動態(tài)數(shù)據(jù)和屬性很好地結合起來。
我們的***實踐是每次一個區(qū)域(數(shù)據(jù)中心)部署數(shù)據(jù)更新,限制意外問題的影響范圍。
即使沒有數(shù)據(jù)更新本身引起的任何問題,服務器進行更新這個行為也可能導致短暫的負載峰值(通常與垃圾回收有關)。
如果集群中所有服務器上同時出現(xiàn)該峰值,可能導致負載分流(load-shedding)出現(xiàn)較大的峰值,錯誤向上游傳播。這種情況下,負載均衡器基本上沒多大幫助,因為所有服務器都遇到高負載。
不過有一個解決辦法(若與這樣的自適應負載均衡器結合使用)是在集群服務器上進行滾動數(shù)據(jù)更新。
如果只有一小部分服務器同時進行更新,負載均衡器可暫時減少發(fā)送到它們的流量,只要集群中有足夠的其他服務器來承擔轉移的流量。
合成負載測試結果
我們廣泛使用合成負載測試場景,同時開發(fā)、測試和調整該負載均衡器的不同方面。
這對于驗證實際集群和網絡的效果非常有用,作為單元測試之上的可重現(xiàn)步驟,但尚未使用實際的用戶流量。
圖 2:結果比較
關于該測試的更多詳細信息,要點總結如下:
- 與輪詢調度方法相比,新負載均衡器在啟用所有功能后,負載分流和連接錯誤減少了好幾個數(shù)量級。
- 平均和長尾延遲有了實質性改進(與輪詢調度方法相比減少 3 倍)。
- 單單添加服務器利用率特性就大有好處,錯誤減少了一個數(shù)量級,并大大降低了延遲。
對實際生產流量的影響
我們發(fā)現(xiàn)新負載均衡器在為每個源服務器分配盡可能多的流量這方面非常有效。
這么做的好處是,路由繞過性能間歇性降級的服務器和持續(xù)性降級的服務器,無需任何人工干預,這避免了半夜叫醒工程師、嚴重影響白天工作效率的問題。
正常運行期間很難表明這種影響,但在生產事故期間就能看清;對于一些服務而言,甚至在正常的穩(wěn)態(tài)運行期間就能看清。
事故期間
最近發(fā)生了一起事故:服務中的一個 Bug 導致越來越多的服務器線程慢慢阻塞,即從已啟動服務器的角度來看,幾個線程每小時就會阻塞,直到服務器最終開始達到***值并分流負載。
在顯示每臺服務器 RPS 的下圖中,可以看到凌晨 3 點之前,服務器之間的分布很寬。這是由于負載均衡器向阻塞線程數(shù)較多的服務器發(fā)送的流量較少。
然后凌晨 3:25 之后,自動擴展機制開始啟動更多的服務器,每臺服務器收到的 RPS 是現(xiàn)有服務器的約兩倍,因為它們還沒有任何阻塞線程,因此可以成功地處理更多流量。
圖 3:每臺服務器的每秒請求數(shù)
現(xiàn)在如果看看同一時間范圍內每臺服務器的錯誤率這張圖,可以看到整個事件中所有服務器上的錯誤分布呈均勻分布,盡管我們知道一些服務器的容量比其他服務器低得多。
這表明負載均衡器在有效運行。由于集群中總可用容量太少,所有服務器都在略高于有效容量的負載下運行。
然后自動擴展機制啟動新服務器時,向這些服務器發(fā)送盡可能多的流量,直到出現(xiàn)的錯誤與集群中其余服務器一樣少。
圖 4:每臺服務器的每秒錯誤數(shù)
總而言之,負載均衡在向服務器分配流量方面非常有效,但在這種情況下,并沒有啟動足夠的新服務器將總錯誤量一路降低到零。
穩(wěn)態(tài)
我們還看到,因垃圾回收事件而出現(xiàn)幾秒負載分流的服務器的一些服務中的穩(wěn)態(tài)噪聲大幅減少。可以看到啟用新負載均衡器后,錯誤大大減少:
圖 5:啟用新負載均衡器前后的幾周內與負載有關的錯誤率
提醒不足
意想不到的是,我們的自動警報機制的一些不足暴露出來。一些基于服務錯誤率的現(xiàn)有警報(之前問題只影響集群的一小部分時就會觸發(fā))現(xiàn)在很久之后才觸發(fā),或根本不觸發(fā),因為錯誤率保持較低。
這意味著有時嚴重的問題在影響集群,團隊卻未收到相關通知。解決辦法是,添加利用率指標(而不僅僅是錯誤指標)偏差方面的其他警報,以填補不足。
結束語
本文無意為 Zuul 作廣告,雖然它是出色的系統(tǒng),主要是為代理/服務網格/負載均衡社區(qū)補充另一種有價值的方法。
Zuul 是很好的系統(tǒng),可測試、實現(xiàn)和改進這些負載均衡方案;結合 Netflix 的需求和規(guī)模來運行 Zuul 使我們能夠證明和改進這些方法。
有許多不同的方法可以用來改善負載均衡,而這個方法對我們來說效果很好,大幅降低了與負載有關的錯誤率,并大大改善了服務器上實際負載的均衡效果。
不過與任何軟件系統(tǒng)一樣,你應根據(jù)貴企業(yè)的條件和目標做決策,盡量避免追求盡善盡美。
【51CTO原創(chuàng)稿件,合作站點轉載請注明原文作者和出處為51CTO.com】