[譯] Erlang 之禪第二部分

Uncho發表於2019-02-25

就像其它的任何事物一樣糟糕

Erlang 之禪

如果你還沒看本文的第一部分,請先閱讀第一部分:Erlang 之禪:第一部分

生產環境中的 Bug

我在這部分想要闡述的是以我的經驗去說說在生產中每種型別錯誤的出現頻率。沒有任何明顯的證據表明利用查詢錯誤和錯誤的發生概率有聯絡。但是我的直覺告訴我,這種關係是存在的。

首先,在核心特性中容易復現的錯誤不應該出現在產品中。如果這些(容易復現的)bugs 確實在生產環境中出現,那麼你實際上已經發布了一個破產品,再多的重新啟動或者技術支援都不會幫到你的使用者。這種問題需要修改程式碼,並且可能是生產該產品的組織內部一些根深蒂固的問題的後果。

邊緣特性中的可復現 bugs 很可能會流入生產環境,我認為這是沒有花足夠的時間去合理測試它們的結果。但當涉及到部分重構時,次要功能往往會被擺在次要位置,或者設計者沒有充分考慮這些功能要與系統的其他部分保持一致。

另一方面,瞬變錯誤一直存在。吉姆·格雷發明了這些術語,他報告說,在給定的客戶站點中,132 個錯誤裡只有一個是波爾 Bug(可復現的 bug)。生產環境中遇到的 bug 中有 131/132 是海森堡Bug(不可復現的 bug)。它們很難被觸發,如果它們是真正的錯誤,可能每一百萬次就只出現一次,那麼你的系統就會一直需要一些負載來捕捉它們;在一個每秒處理 10 萬請求的系統中,10 億之一的 bug 每 3 小時出現一次,百萬分之一的 bug 每 10s 就會出現一次,但在測試環境中,類似 bug 很少出現。

如果處理不當,就將會有很多的錯誤和失敗。

通過重啟來處理 bug

那麼,重啟作為一種策略有多有效呢?

對於核心功能上的可復現的 bug,重新啟動是沒用的。對於不經常使用的程式碼路徑中的可復現的 bug,這取決於不同情況;如果這個功能對於非常少的使用者來說非常重要,那麼重新啟動不會有太大的作用。如果這是每個人都使用的一個小功能,但在某種程度上他們並不太在意,那麼重新開始或忽略失敗就夠了。例如,如果facebook 的 ‘poke’ 功能失效(不知道這個問題是否還存在),也不會對很多使用者的體驗有影響。

對於瞬態錯誤,重啟是非常有效的,而且它們往往是我們遇到的常見錯誤。由於它們難以復現,所以它們的出現通常依賴於特定情況或系統中狀態的交織,並且它們的出現往往只佔所有操作的一小部分,重啟往往會使它們消失。

回滾到已知的穩定狀態,再重複一次相同的操作,不太可能碰到導致這種情況的奇怪上下文。因此,可能發生的災難只不過是系統的一個小插曲,使用者很快就學會適應了。

然後,你可以使用日誌記錄、跟蹤或各種自檢工具(這些工具在 Erlang 中都是現成的)來查詢、理解和修復問題,以保證它們不再發生。或者你可以決定容忍它們,因為解決問題需要付出巨大的努力。

臭名昭著的 bsd

這個問題是在一個論壇上提出來的,當時我正在討論程式設計內容和 Erlang 模型。我一字不差地摘錄了它,因為這是一個很好的例子,很多人聽到重啟和 Erlang 的特性時都會問這個問題。

我想通過一個現實的例子來具體說明如何在 Erlang 中設計一個系統,這更能突出它的特性。

監控樹示例

通過監督者(圓角矩形),我們可以開始建立深層次的流程。這裡我們有一個選舉系統,有兩棵樹:一棵計數樹和一棵實時報告樹。計數樹負責計數和儲存結果,而實時報告樹則是讓人們連線到它以檢視結果。

通過定義子節點的順序可以知道,計數樹啟動後實時報告樹才會開始執行。除非儲存層可用,否則分割槽子樹(關於每個分割槽的計數結果)將不會執行。如果儲存工作者池(將連線到資料庫)可用,則只能啟動儲存的快取。

我前面提到的監督策略讓我們在程式結構中對這些需求進行編碼,並且它們在執行時仍然存在的,而不僅僅是在啟動時。例如,管理人員可能會採用一對一策略,這意味著各區域可能各自失敗,而不會影響彼此之間的計數。相比之下,每個地區(魁北克和安大略的管理者)都可以採取休息策略。 因此,這一策略可以確保 OCR 程式始終可以將檢測到的投票傳送給“計數”工作人員,並且即使經常崩潰也不會對其造成影響。 另一方面,如果計數工作人員無法儲存和儲存狀態,它的停止會中斷 OCR 程式,確保沒有任何資料丟失。

這個 OCR 程式本身可能是用 C 語言編寫的監視程式碼,作為獨立的代理,並與其連結。 這將進一步隔離該 C 語言程式碼與虛擬機器的故障,以實現更好的隔離或並行化。

我要指出的另一件事是,每個主管都有對失敗的可配置容忍度;區域主管可能非常寬容,每分鐘處理 10 次故障,而儲存層如果預期是正確的,則可能對故障相當不寬容,如果我們希望它是正確的,則在每小時 3 次崩潰後永久關閉。

在這個程式中,關鍵的功能更接近樹的根,這樣能更少的移動和更加堅固。他們不受兄弟節點消亡的影響,但他們自己的失敗影響到其他人。葉子完成了所有的工作,並且可以很好地丟失 —— 一旦它們吸收了資料並在上面進行光合作用,它就可以進入核心。

因此,通過定義所有這些,我們可以將危險的程式碼隔離在一個具有高容忍度或正在被監控的程式中,並在資料進入系統時將資料移至更穩定的程式。 如果 C 語言中的 OCR 程式碼有危險,它可以失敗並安全地重新啟動。 當它工作時,它將其資訊傳輸到 Erlang OCR 程式。 該過程可以進行驗證,也可以自行崩潰,也許不會。 如果資訊是可靠的,則將其移至 Count 過程,該程式的任務是保持非常簡單的狀態,並最終通過儲存子樹將該狀態重新整理到資料庫,這是安全獨立的。

如果 OCR 程式死亡,它會自動重啟。如果它奔潰得太頻繁,它就會將自己的管理器關閉,子樹的那一部分也會重新啟動 —— 不會影響系統的其他部分。如果這能解決問題,很好。如果沒有,這個過程就會不斷重複,直到它工作,直到整個系統停止,因為某些東西顯然出錯了,我們無法通過重新啟動來處理它。

以這種方式構建系統具有巨大的價值,因為錯誤處理被嵌入到系統的結構中。這意味著我可以不用在邊緣節點中編寫噁心的防禦程式碼 —— 如果出了問題,讓其他人(或程式的結構)來決定如何反應。如果我知道如何處理一個錯誤,那麼我可以對那個特定的錯誤這麼做。否則,就讓它崩潰吧!

這種方式傾向於轉換程式碼。慢慢地,你會發現它不再包含大量的 if/else 或 switch 或 try/catch 表示式。相反,它包含了清晰的程式碼,解釋當一切正常時程式碼應該做什麼。它不再包含許多形式的猜測,你的軟體可讀性更強。

監督子樹

當我們退一步看我們的程式結構時,我們可能會發現,在黃色環繞的每個子樹中,在它們所做的事情上似乎都是相互獨立的;它們的依賴關係大多是合乎邏輯的:例如,報表系統需要一個儲存層進行查詢。

例如,如果我可以交換儲存實現或在其他系統中獨立使用它,那也是非常好的。將實時報告系統隔離到不同的節點或開始提供替代手段(例如 SMS)也可能很整潔。

我們現在需要的是找到一種方式來打破這些子樹,並將它們轉化為我們可以組合,重用的邏輯單元,並且我們可以獨立配置,重新啟動或開發。

OTP apps

Erlang 將 OTP 用作解決方案。OTP 應用程式是構建這種子樹的程式碼以及一些後設資料。該後設資料包含基本內容,如版本號和應用程式的描述,以及指定應用程式之間的依賴關係的方法。 這非常有用,因為它可以讓我的儲存應用程式與系統的其他部分保持獨立,但仍然對計數應用程式在執行時的需要進行編碼。我可以保留我在系統中編碼的所有資訊,但現在它是由獨立塊構建的,這些塊更容易理解。

實際上,人們認為 OTP 應用程式是 Erlang 的庫。 如果您的程式碼庫不是 OTP 應用程式,那麼它在其他系統中不可重用。 [旁註:有許多方法可以指定實際上不包含子樹的 OTP 庫,只是由其他庫重用的模組]

搞掂一切後,我們的 Erlang 系統現在已經定義了以下所有屬性:

  • 對系統的生存至關重要或不重要
  • 什麼是可以失敗的,以及在不再可持續之前它能夠以何種頻率這樣做
  • 軟體應該如何根據哪些保證以及以什麼樣的順序啟動
  • 軟體應該如何失敗,這意味著它定義了你所處的部分失敗的合法狀態,以及如何在發生這種情況時回滾到已知的穩定狀態
  • 軟體如何升級(因為它可以根據監督結構進行實時升級)
  • 元件如何相互依賴

這是非常有價值的。更有價值的是迫使每個開發人員在早期就從這種角度去考慮。你的防守程式碼較少,發生奔潰時系統會繼續執行。你只需要檢視日誌或實時系統狀態,並花時間修復問題(如果您覺得這是值得的時間)。

晚上睡覺

完成這一切後,我應該可以安穩的睡大覺了,對吧?希望是的。我這裡展示的是我們幾年前在 Heroku 上部署的一個畫素圖表。

圖的最左邊是在 9 月左右。那時,我們的新代理層(vegur)已經投入生產了大約 3 個月,我們已經解決了其中的大部分問題。使用者沒有問題,過渡進行得很順利,新的功能正在被使用。

在某個時候,一個團隊成員為我們用來聚合異常的日誌記錄服務收到了非常昂貴的信用卡帳單。 那時候我們看了一眼,看到了圖表最左邊的恐怖:我們每天產生 500,000 到 1,200,000 個異常!額滴神,這太多了吧。 但是呢? 如果問題是一個 heisenbug,而我們的系統每秒收到 100,000 個請求,那麼它發生的機率是多少?在 1/17000 到 1/7000 之間。這很頻繁,但是因為它對服務沒有影響,所以直到頻寬和儲存賬單來了我們才注意到它。

我們花了一點時間才弄清錯誤,然後我們修正了錯誤。你可以看到,此後的異常率仍然很低,可能每天幾十萬。他們都是我們所知道的,但是沒有影響。兩年後,我們還沒有著手解決這個問題,因為儘管如此,系統還是可以正常工作的。

預料失敗

與此同時,你不可能總能安穩的睡大覺。儘管你採用了最佳的設計方法,但失敗可能會失控。

幾年前,我乘坐過一趟飛往溫哥華的航班。當飛機下降時,飛行員在廣播裡說道:“這是機長,我們馬上就要著陸了。不要驚慌,因為我們會在停機坪上停留幾分鐘,而消防部門會檢查飛機。我們有一些液壓元件失效了,他們想要確保沒有發生火災的危險。我們有兩個備用系統,我們應該沒問題。”

我們都沒事。在這種情況下,這架飛機設計得非常好。

這張幻燈片上的圖片並不是那個航班,而是我兩週前乘坐的另一架,當時美國東部正被埋在 24 英寸厚的雪中。這架飛機(聯合 734 航班),我確信它同樣可靠,降落在跑道上。但到了休息的時候,它發出了很大的噪音,我猜是 ABS 的飛機,但它還是繼續前進。

我們跑過了跑道盡頭的紅燈,你在照片上看到了,在停機坪的盡頭,飛機滑出跑道,錯過了斜坡,前輪在草地上消失了。每個人都沒事,但這是一個偉大的工程不能每次都能正常運作的例子。

危險區

事實上,操作始終是成功部署系統的一個重要因素。這張幻燈片很受理查德·庫克( Richard Cook )的演講啟發(實際上是被偷了)。如果你不認識他,我建議你去 youtube 上看他演講的視訊,這些視訊非常棒。

正確的系統架構和開發實踐仍然無法被取代,或者可能因不適當的操作而被打破; 工具,劇本,監控,自動化等的效率和有用性,都趨向隱式依賴於知識和操作條件的完全考慮(如吞吐量,負載,過載管理等)。如果定義了這些,這些操作限制會讓你知道什麼時候事情會變壞,什麼時候再變好。

這些限制的問題在於,當操作員習慣了這些限制,並且習慣了頻繁地破壞它們而不產生負面後果,就有可能慢慢地將極限推到危險區域的邊緣,在那裡會發生嚴重的大規模故障。你的反應時間和和餘地將受到更高的負載會的侵蝕,最終被終結在一個不斷被破壞的位置,卻沒有任何喘息的機會。

所以我們必須小心,注意這類事情,以及重視人們使用和操作軟體的重要性。要想擴大一個優秀團隊的規模,總是比擴大一個專案要困難。即使不發生緊急情況也要做好計劃預防它們奔潰,當這樣的事情發生時你可以輕鬆的執行模擬程式並且有完備的方法去修復它們。

飛機應急措施

就像我說的,在我的飛行中沒有人受傷。儘管如此,這仍是一場為了大家而上演的鬧劇:巴士護送乘客返回航站樓,因為運送滯留的飛機可能存在風險。 很多隨車將巴士安全地從跑道護送至碼頭。其中有警車,一大堆消防車,還有那輛我不知道它做什麼的黑色汽車,但我相信它非常有用。

儘管每個人沒事,儘管飛機非常可靠,但他們還是部署了所有這些裝置。他們做了正確的事情。

其他好處

這裡有另外一些你使用 Erlang 獲得的東西。對他們沒什麼好說的,只是我傾向於對切換使用它有一些興趣,所以就是這樣。

最後一點值得評論。在他們的系統設計方法中非常靈活的語言中發生的風險之一是,你使用的庫可能不會按照你認為合理的的方式執行任何操作。這樣的情形下你只好不用庫,又或者用不連貫的設計來操作程式碼庫。這在 Erlang 中不會發生,因為每個人都使用相同的經過驗證的方法來完成任務。

元件如何互動

簡而言之,Erlang 之禪和 “讓它崩潰”,其實就是搞清楚元件如何相互作用,弄明白什麼是關鍵的,什麼不是關鍵的,什麼狀態可以儲存、保留、重新計算或丟失。在所有的情況下,你都必須想出最壞的情況以及如何度過它。通過使用具有隔離,鏈路和監視器以及監視器的故障快速機制來限制所有這些最壞情況的規模和傳播,你將讓它成為一個非常容易理解的常規故障案例。

這聽起來很簡單,但卻有奇效。如果你認為你可以理解的常規失敗案例是可行的,那麼你所有的錯誤處理都可以適用於該案例。你不再需要擔心或編寫防禦程式碼。你只要編寫程式碼應該做什麼,並讓程式的結構決定其餘部分。隨它崩潰去吧。

Erlang 之禪

這就是 Erlang 的精髓:首先建立互動,確保可能發生的最壞情況仍然是可行的。那麼在你的系統中幾乎沒有錯誤或失敗會讓你緊張(當它發生時,你可以在執行時自省一切!),那樣你就可以坐下來放鬆了。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章