Facebook 如何避免大規(guī)模線上故障
Fail at Scale 是 Facebook 2015 年在 acm queue 上發(fā)表的一篇文章。主要寫(xiě)了常見(jiàn)的線上故障和應(yīng)對(duì)方法,內(nèi)容還是比較實(shí)在的。
"What Would You Do If You Weren't Afraid?" 和 "Fortune Favors the Bold." 是 FB 公司信條,掛墻上那種。
為了能在快速變更的系統(tǒng)中使 FB 的系統(tǒng)穩(wěn)定,工程師們對(duì)系統(tǒng)故障進(jìn)行了一些總結(jié)和抽象,為了能夠建立可靠的系統(tǒng)必須要理解故障。而為了理解故障,工程師們建立了一些工具來(lái)診斷問(wèn)題,并建立了對(duì)事故進(jìn)行復(fù)盤(pán),以避免未來(lái)再次發(fā)生的文化。
事故的發(fā)生可以被分為三大類(lèi)。
為什么會(huì)發(fā)生故障
單機(jī)故障
通常情況下,單機(jī)遇到的孤立故障不會(huì)影響到基礎(chǔ)設(shè)施的其他部分。這里的單機(jī)故障指的是:一臺(tái)機(jī)器的硬盤(pán)出現(xiàn)了故障,或者某臺(tái)機(jī)器上的服務(wù)遇到了代碼中的錯(cuò)誤,如內(nèi)存損壞或死鎖。
避免單個(gè)機(jī)器故障的關(guān)鍵是自動(dòng)化。通過(guò)總結(jié)已知的故障模式,并與探究未知的故障癥狀相結(jié)合。在發(fā)現(xiàn)未知故障的癥狀(如響應(yīng)緩慢)時(shí),將機(jī)器摘除,線下分析,并將其總結(jié)至已知故障中。
工作負(fù)載變化
現(xiàn)實(shí)世界的重大事件會(huì)對(duì) Facebook 網(wǎng)站基礎(chǔ)設(shè)施帶來(lái)壓力,比如:
-
奧巴馬當(dāng)選總統(tǒng),他的 Facebook 頁(yè)面經(jīng)歷了創(chuàng)紀(jì)錄的活動(dòng)水平。
-
重大體育賽事的高潮部分,如超級(jí)碗或世界杯,會(huì)導(dǎo)致極高的帖子數(shù)量。
-
負(fù)載測(cè)試,以及新功能發(fā)布時(shí)的引流階段會(huì)不向用戶(hù)展示但引入流量。
這些事件中收集到的統(tǒng)計(jì)數(shù)據(jù)會(huì)為系統(tǒng)設(shè)計(jì)提供獨(dú)特的視角。重大事件會(huì)導(dǎo)致用戶(hù)行為變化,這些變化能為系統(tǒng)后續(xù)的決策提供數(shù)據(jù)依據(jù)。
人為錯(cuò)誤
圖 1a 顯示了周六和周日發(fā)生的事故是如何大幅減少的,盡管網(wǎng)站的流量在整個(gè)一周內(nèi)保持一致。圖 1b 顯示了在六個(gè)月的時(shí)間里,只有兩個(gè)星期沒(méi)有發(fā)生事故:圣誕節(jié)的那一周和員工要為對(duì)方寫(xiě)同行評(píng)論的那一周。
上圖說(shuō)明大部分故障都是人為原因,因?yàn)槭录慕y(tǒng)計(jì)與人的活動(dòng)規(guī)律是一致的。
故障的三個(gè)原因
故障的原因很多,不過(guò)有三種最為常見(jiàn)。這里列出的每一種都給出了預(yù)防措施。
快速部署的配置變更
配置系統(tǒng)往往被設(shè)計(jì)為在全球范圍內(nèi)快速?gòu)?fù)制變化??焖倥渲米兏?強(qiáng)大的工具,然而,快速配置變更可能導(dǎo)致部署問(wèn)題配置時(shí)發(fā)生事故。下面是一些防止配置變更故障的手段。
所有人公用同一套配置系統(tǒng)。使用一個(gè)共同的配置系統(tǒng)可以確保程序和工具適用于所有類(lèi)型的配置。
配置變化靜態(tài)檢查。許多配置系統(tǒng)允許松散類(lèi)型的配置,如 JSON 結(jié)構(gòu)。這些類(lèi)型的配置使工程師很容易打錯(cuò)字段的名稱(chēng),在需要使用整數(shù)的地方使用字符串,或犯其他簡(jiǎn)單的錯(cuò)誤。這類(lèi)簡(jiǎn)單的錯(cuò)誤最好用靜態(tài)驗(yàn)證來(lái)捕捉。結(jié)構(gòu)化格式(Facebook 使用 Thrift)可以提供最基本的驗(yàn)證。然而,編寫(xiě)程序來(lái)對(duì)配置進(jìn)行更詳細(xì)的業(yè)務(wù)層面的驗(yàn)證也是應(yīng)該的。
金絲雀部署。將配置先部署到小范圍,防止變更造成災(zāi)難性后果。金絲雀可以有多種形式。最簡(jiǎn)單的是 A/B 測(cè)試,如只對(duì) 1% 的用戶(hù)啟用新的配置。多個(gè) A/B 測(cè)試可以同時(shí)進(jìn)行,可以使用一段時(shí)間的數(shù)據(jù)來(lái)跟蹤指標(biāo)。
就可靠性而言,A/B 測(cè)試并不能滿(mǎn)足所有需求。
-
一個(gè)被部署到少數(shù)用戶(hù)的變更,如果導(dǎo)致相關(guān)服務(wù)器崩潰或內(nèi)存耗盡,顯然會(huì)產(chǎn)生超出測(cè)試中有限用戶(hù)的影響。
-
A/B 測(cè)試也很耗費(fèi)時(shí)間。工程師們經(jīng)常希望在不使用 A/B 測(cè)試的情況下推送小的變化。
為了避免配置導(dǎo)致的明顯問(wèn)題,F(xiàn)acebook 的基礎(chǔ)設(shè)施會(huì)自動(dòng)在一小部分服務(wù)器上測(cè)試新版本的配置。
例如,如果我們希望向 1% 的用戶(hù)部署一個(gè)新的 A/B 測(cè)試,會(huì)首先向 1% 的用戶(hù)部署測(cè)試,保證這些用戶(hù)的請(qǐng)求落在少量服務(wù)器上,并對(duì)這些服務(wù)器進(jìn)行短時(shí)間的監(jiān)控,以確保不會(huì)因?yàn)榕渲酶鲁霈F(xiàn)非常明顯的崩潰問(wèn)題。
堅(jiān)持可以運(yùn)行的配置。配置系統(tǒng)設(shè)計(jì)方案保證在發(fā)生失敗時(shí),保留原有的配置。開(kāi)發(fā)人員一般傾向于配置發(fā)生問(wèn)題時(shí)系統(tǒng)直接崩潰,不過(guò) Facebook 的基礎(chǔ)設(shè)施開(kāi)發(fā)人員認(rèn)為用老的配置,使模塊能運(yùn)行比向用戶(hù)返回錯(cuò)誤好得多。(注:我只能說(shuō)這個(gè)真的要看場(chǎng)景)
配置有問(wèn)題時(shí)回滾要快速。有時(shí),盡管盡了最大努力,有問(wèn)題的配置還是上了線。迅速回滾是解決這類(lèi)問(wèn)題的關(guān)鍵。配置內(nèi)容在版本管理系統(tǒng)中進(jìn)行管理,保證能夠回滾。
強(qiáng)依賴(lài)核心服務(wù)
開(kāi)發(fā)人員傾向于認(rèn)為,核心服務(wù):如配置管理、服務(wù)發(fā)現(xiàn)或存儲(chǔ)系統(tǒng),永遠(yuǎn)不會(huì)失敗。在這種假設(shè)下,這些核心服務(wù)的短暫故障,會(huì)變成大規(guī)模故障。
緩存核心服務(wù)的數(shù)據(jù)。可以在服務(wù)本地緩存一部分?jǐn)?shù)據(jù),這樣可以降低對(duì)緩存服務(wù)的依賴(lài)。 提供專(zhuān)門(mén) SDK 來(lái)使用核心服務(wù)。 核心服務(wù)最好提供專(zhuān)門(mén)的 SDK,這樣保證大家在使用核心服務(wù)時(shí)都能遵循相同的最佳實(shí)踐。同時(shí)在 SDK 中可以考慮好緩存管理和故障處理,使用戶(hù)一勞永逸。 進(jìn)行演練。 只要不進(jìn)行演練,就沒(méi)法知道如果依賴(lài)的服務(wù)掛了自己是不是真的會(huì)掛,所以通過(guò)演練來(lái)進(jìn)行故障注入是必須的。
延遲增加和資源耗盡
有些故障會(huì)導(dǎo)致延遲增加,這個(gè)影響可以很小(例如,導(dǎo)致 CPU 使用量微微增加),也可以很大(服務(wù)響應(yīng)的線程死鎖)。
少量的額外延遲可以由 Facebook 的基礎(chǔ)設(shè)施輕松處理,但大量的延遲會(huì)導(dǎo)致級(jí)聯(lián)故障。幾乎所有的服務(wù)都有一個(gè)未處理請(qǐng)求數(shù)量的限制。這個(gè)限制可能是因?yàn)檎?qǐng)求響應(yīng)類(lèi)服務(wù)的線程數(shù)量有限,也可能是由于基于事件的服務(wù)內(nèi)存有限。如果一個(gè)服務(wù)遇到大量的額外延遲,那么調(diào)用它的服務(wù)將耗盡它的資源。這種故障會(huì)層層傳播,造成大故障。
資源耗盡是特別具有破壞性的故障模式,它會(huì)使請(qǐng)求子集所使用的服務(wù)的故障引起所有請(qǐng)求的故障:
一個(gè)服務(wù)調(diào)用一個(gè)新的實(shí)驗(yàn)性服務(wù),該服務(wù)只向 1% 的用戶(hù)推出。通常情況下,對(duì)這個(gè)實(shí)驗(yàn)性服務(wù)的請(qǐng)求需要 1 毫秒,但由于新服務(wù)的失敗,請(qǐng)求需要 1 秒。使用這個(gè)新服務(wù)的 1% 的用戶(hù)的請(qǐng)求可能會(huì)消耗很多線程,以至于其他 99% 的用戶(hù)的請(qǐng)求都無(wú)法被執(zhí)行。
下面的手段可以避免請(qǐng)求堆積:
-
延遲控制。在分析過(guò)去涉及延遲的事件時(shí),工程師發(fā)現(xiàn)大量請(qǐng)求都是堆積在隊(duì)列中等待處理。服務(wù)一般會(huì)有線程數(shù)或內(nèi)存使用上的限制。由于服務(wù)響應(yīng)速度 < 請(qǐng)求傳入速度,隊(duì)列會(huì)越來(lái)越大,直到達(dá)到閾值。想要在不影響正常運(yùn)行可靠性的前提下限制隊(duì)列大小,F(xiàn)B 工程師研究了 bufferbloat 問(wèn)題,這里的問(wèn)題和 bufferbloat 問(wèn)題很類(lèi)似,都是在擁塞的時(shí)候不引起過(guò)度延遲。這里實(shí)現(xiàn)了 CoDel(controlled delay 縮寫(xiě))算法的一個(gè)變種:
注:雖然里面寫(xiě)著 M 和 N,但其實(shí) M 和 N 是定值,N = 100ms,M = 5ms
- onNewRequest(req, queue):
- if queue.lastEmptyTime() < (now - N seconds) {
- timeout = M ms
- } else {
- timeout = N seconds;
- }
- queue.enqueue(req, timeout)
在這個(gè)算法中,如果隊(duì)列在過(guò)去的 100ms 內(nèi)沒(méi)有被清空,那么在隊(duì)列中花費(fèi)的時(shí)間被限制在 5ms。如果服務(wù)在過(guò)去的 100ms 能夠清空隊(duì)列,那么在隊(duì)列中花費(fèi)的時(shí)間被限制為 100 ms。這種算法可以減少排隊(duì)(因?yàn)?lastEmptyTime 是在遙遠(yuǎn)的過(guò)去,導(dǎo)致 5ms 的排隊(duì)超時(shí)),同時(shí)允許短時(shí)間的排隊(duì)以達(dá)到可靠性的目的。雖然讓請(qǐng)求有這么短的超時(shí)似乎有悖常理,但這個(gè)過(guò)程允許請(qǐng)求被快速丟棄,而不是在系統(tǒng)無(wú)法跟上傳入請(qǐng)求的速度時(shí)堆積起來(lái)。較短的超時(shí)時(shí)間可以確保服務(wù)器接受的工作總是比它實(shí)際能處理的多一點(diǎn),所以它永遠(yuǎn)不會(huì)閑置。
前面也說(shuō)了,這里的 M 和 N 基本上不需要按場(chǎng)景調(diào)整。其他解決排隊(duì)問(wèn)題的方法,如對(duì)隊(duì)列中的項(xiàng)目數(shù)量設(shè)置限制或?yàn)殛?duì)列設(shè)置超時(shí),需要按場(chǎng)景做 tuning。M 固定 5 毫秒,N 值為 100 毫秒,在大多場(chǎng)景下都能很好地工作。Facebook 的開(kāi)源 Wangle 庫(kù)和 Thrift 使用了這種算法。
-
自適應(yīng) LIFO(后進(jìn)先出)。大多數(shù)服務(wù)以 FIFO 的順序處理隊(duì)列。然而,在大量排隊(duì)期間,先入的請(qǐng)求往往已經(jīng)等了很久,用戶(hù)可能已經(jīng)放棄了請(qǐng)求相關(guān)的行為。這時(shí)候處理先入隊(duì)的請(qǐng)求,會(huì)將資源耗費(fèi)在一個(gè)不太可能使用戶(hù)受益的請(qǐng)求上。FB 的服務(wù)使用自適應(yīng)后進(jìn)先出的算法來(lái)處理請(qǐng)求。在正常的操作條件下,請(qǐng)求是 FIFO 處理模式,但是當(dāng)隊(duì)列開(kāi)始任務(wù)堆積時(shí),則切換至 LIFO 模式。如圖 2 所示,自適應(yīng) LIFO 和 CoDel 可以很好地配合。CoDel 設(shè)置了較短的超時(shí)時(shí)間,防止了形成長(zhǎng)隊(duì)列,而自適應(yīng) LIFO 模式將新的請(qǐng)求放在隊(duì)列的前面,最大限度地提高了它們滿(mǎn)足 CoDel 設(shè)置的最后期限的機(jī)會(huì)。HHVM 實(shí)現(xiàn)了這個(gè)后進(jìn)先出算法。
-
并發(fā)控制。CoDel 和自適應(yīng) LIFO 都在服務(wù)器端運(yùn)行。服務(wù)端是降低延遲的最佳場(chǎng)所--server 為大量的 client 服務(wù),且有比 client 端更多的信息。 不過(guò)有些故障較嚴(yán)重, 可能 導(dǎo)致服務(wù)端的控制無(wú)法啟動(dòng)。 為了兜底 FB 還在客戶(hù)端實(shí)施了一個(gè)策略: 每個(gè)客戶(hù)端都會(huì)在每個(gè)服務(wù)的基礎(chǔ)上跟蹤未完成的出站請(qǐng)求的數(shù)量。 當(dāng)新的請(qǐng)求被發(fā)送時(shí),如果對(duì)該服務(wù)的未決請(qǐng)求的數(shù)量超過(guò)可配置的數(shù)量,該請(qǐng)求將立即被標(biāo)記為錯(cuò)誤( 注:應(yīng)該類(lèi)似熔斷 )。 這種機(jī)制可以防止單一服務(wù)壟斷其客戶(hù)的所有資源。
幫助診斷故障的工具
盡管有最好的預(yù)防措施,故障還是會(huì)發(fā)生。故障期間,使用正確的工具可以迅速找到根本原因,最大限度地減少故障的持續(xù)時(shí)間。
使用 Cubism 的高密度儀表板在處理事故時(shí),快速獲取信息很重要。好的儀表盤(pán)可以讓工程師快速評(píng)估可能出現(xiàn)異常的指標(biāo)類(lèi)型,然后利用這些信息來(lái)推測(cè)根本原因。然而儀表盤(pán)會(huì)越來(lái)越大,直到難以快速瀏覽,這些儀表盤(pán)上顯示的圖表有太多線條,無(wú)法一目了然,如圖3所示。
為了解決這個(gè)問(wèn)題,我們使用 Cubism 構(gòu)建了我們的頂級(jí)儀表盤(pán),這是一個(gè)用于創(chuàng)建地平線圖表的框架--該圖表使用顏色對(duì)信息進(jìn)行更密集的編碼,允許對(duì)多個(gè)類(lèi)似的數(shù)據(jù)曲線進(jìn)行輕松比較。例如,我們使用 Cubism 來(lái)比較不同數(shù)據(jù)中心的指標(biāo)。我們圍繞 Cubism 的工具允許簡(jiǎn)單的鍵盤(pán)導(dǎo)航,工程師可以快速查看多個(gè)指標(biāo)。圖 4 顯示了使用面積圖和水平線圖的不同高度的同一數(shù)據(jù)集。在面積圖版本中,30像素高的版本很難閱讀。同一高度下地平線圖非常容易找到峰值。
最近發(fā)生了什么變更?
由于失敗的首要原因之一是人為錯(cuò)誤,調(diào)試失敗的最有效方法之一是尋找人類(lèi)最近的變化。我們?cè)谝粋€(gè)叫做 OpsStream 的工具中收集關(guān)于最近的變化的信息,從配置變化到新版本的部署。然而,我們發(fā)現(xiàn)隨著時(shí)間的推移,這個(gè)數(shù)據(jù)源已經(jīng)變得非常嘈雜。由于數(shù)以千計(jì)的工程師在進(jìn)行改變,在一個(gè)事件中往往有太多的改變需要評(píng)估。
為了解決這個(gè)問(wèn)題,我們的工具試圖將故障與其相關(guān)的變化聯(lián)系起來(lái)。例如,當(dāng)一個(gè)異常被拋出時(shí),除了輸出堆棧跟蹤外,我們還輸出該請(qǐng)求所讀取的任何配置的值最近發(fā)生的變化。通常,產(chǎn)生許多錯(cuò)誤堆棧的問(wèn)題的原因就是這些配置值之一。然后,我們可以迅速地對(duì)這個(gè)問(wèn)題做出反應(yīng)。例如,讓上線該配置的工程師趕緊回滾配置。
從失敗中學(xué)習(xí)
失敗發(fā)生后,我們的事件審查過(guò)程有助于我們從這些事件中學(xué)習(xí)。
事件審查過(guò)程的目的不是為了指責(zé)。沒(méi)有人因?yàn)樗蛩斐傻氖录粚彶槎唤夤汀彶榈哪康氖菫榱肆私獍l(fā)生了什么,補(bǔ)救使事件發(fā)生的情況,并建立安全機(jī)制以減少未來(lái)事件的影響。
審查事件的方法 Facebook 發(fā)明了一種名為 DERP(detection, escalation, remediation, and prevention)的方法,以幫助進(jìn)行富有成效的事件審查。
-
檢測(cè) detection。如何檢測(cè)問(wèn)題--報(bào)警、儀表板、用戶(hù)報(bào)告?
-
升級(jí) escalation。正確的人是否迅速介入?這些人是否可以通過(guò)警報(bào)被拉進(jìn)故障處理流程,而不是人為拉進(jìn)來(lái)?
-
補(bǔ)救 remediation。采取了哪些步驟來(lái)解決這個(gè)問(wèn)題?這些步驟是否可以自動(dòng)化?
-
預(yù)防 prevention。哪些改進(jìn)可以避免同類(lèi)型的故障再次發(fā)生?你如何能優(yōu)雅地失敗,或更快地失敗,以減少這次故障的影響?
在這種模式的幫助下,即使不能防止同類(lèi)型的事件再次發(fā)生,也至少能夠在下一次更快恢復(fù)。
一把梭的時(shí)候少出故障
"move fast" 的心態(tài)不一定與可靠性沖突。為了使這些理念兼容,F(xiàn)acebook 的基礎(chǔ)設(shè)施提供了安全閥:
-
配置系統(tǒng)可以防止不良配置的快速部署;
-
核心服務(wù)為客戶(hù)提供加固 SDK 以防止故障;
-
核心穩(wěn)定性庫(kù)可以防止在延遲發(fā)生的情況下資源耗盡。
為了處理漏網(wǎng)之魚(yú),還建立了易于使用的儀表盤(pán)和工具,以幫助找到與故障關(guān)聯(lián)的最近的變更。
最重要的是,在事件發(fā)生后,通過(guò)復(fù)盤(pán)學(xué)到的經(jīng)驗(yàn)將使基礎(chǔ)設(shè)施更加可靠。
References
-
CoDel (controlled delay) algorithm; http://queue.acm.org/detail.cfm?id=2209336.
-
Cubism; https://square.github.io/cubism/.
-
HipHop Virtual Machine (HHVM); https://github.com/facebook/hhvm/blob/43c20856239cedf842b2560fd768038f52b501db/hphp/util/job-queue.h#L75.
-
Thrift framework; https://github.com/facebook/fbthrift.
-
Wangle library; https://github.com/facebook/wangle/blob/master/wangle/concurrent/Codel.cpp.
-
https://github.com/facebook/folly/blob/bd600cd4e88f664f285489c76b6ad835d8367cd2/folly/executors/Codel.cpp