- 原文地址:As bad as anything else: Part 2
- 原文做者:Fred T-H
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:7Ethan
- 校對者:K.Lew, satansk
若是你還沒看本文的第一部分,請先閱讀第一部分:Erlang 之禪:第一部分。html
我在這部分想要闡述的是以個人經驗去說說在生產中每種類型錯誤的出現頻率。沒有任何明顯的證據代表利用查找錯誤和錯誤的發生機率有聯繫。可是個人直覺告訴我,這種關係是存在的。前端
首先,在覈心特性中容易復現的錯誤不該該出如今產品中。若是這些(容易復現的)bugs 確實在生產環境中出現,那麼你實際上已經發布了一個破產品,再多的從新啓動或者技術支持都不會幫到你的用戶。這種問題須要修改代碼,而且多是生產該產品的組織內部一些根深蒂固的問題的後果。android
邊緣特性中的可復現 bugs 極可能會流入生產環境,我認爲這是沒有花足夠的時間去合理測試它們的結果。但當涉及到部分重構時,次要功能每每會被擺在次要位置,或者設計者沒有充分考慮這些功能要與系統的其餘部分保持一致。ios
另外一方面,瞬變錯誤一直存在。吉姆·格雷發明了這些術語,他報告說,在給定的客戶站點中,132 個錯誤裏只有一個是波爾 Bug(可復現的 bug)。生產環境中遇到的 bug 中有 131/132 是海森堡Bug(不可復現的 bug)。它們很難被觸發,若是它們是真正的錯誤,可能每一百萬次就只出現一次,那麼你的系統就會一直須要一些負載來捕捉它們;在一個每秒處理 10 萬請求的系統中,10 億之一的 bug 每 3 小時出現一次,百萬分之一的 bug 每 10s 就會出現一次,但在測試環境中,相似 bug 不多出現。git
若是處理不當,就將會有不少的錯誤和失敗。github
那麼,重啓做爲一種策略有多有效呢?數據庫
對於核心功能上的可復現的 bug,從新啓動是沒用的。對於不常用的代碼路徑中的可復現的 bug,這取決於不一樣狀況;若是這個功能對於很是少的用戶來講很是重要,那麼從新啓動不會有太大的做用。若是這是每一個人都使用的一個小功能,但在某種程度上他們並不太在乎,那麼從新開始或忽略失敗就夠了。例如,若是facebook 的 ‘poke’ 功能失效(不知道這個問題是否還存在),也不會對不少用戶的體驗有影響。編程
對於瞬態錯誤,重啓是很是有效的,並且它們每每是咱們遇到的常見錯誤。因爲它們難以復現,因此它們的出現一般依賴於特定狀況或系統中狀態的交織,而且它們的出現每每只佔全部操做的一小部分,重啓每每會使它們消失。後端
回滾到已知的穩定狀態,再重複一次相同的操做,不太可能碰到致使這種狀況的奇怪上下文。所以,可能發生的災難只不過是系統的一個小插曲,用戶很快就學會適應了。緩存
而後,你可使用日誌記錄、跟蹤或各類自檢工具(這些工具在 Erlang 中都是現成的)來查找、理解和修復問題,以保證它們再也不發生。或者你能夠決定容忍它們,由於解決問題須要付出巨大的努力。
這個問題是在一個論壇上提出來的,當時我正在討論編程內容和 Erlang 模型。我一字不差地摘錄了它,由於這是一個很好的例子,不少人聽到重啓和 Erlang 的特性時都會問這個問題。
我想經過一個現實的例子來具體說明如何在 Erlang 中設計一個系統,這更能突出它的特性。
經過監督者(圓角矩形),咱們能夠開始建立深層次的流程。這裏咱們有一個選舉系統,有兩棵樹:一棵計數樹和一棵實時報告樹。計數樹負責計數和存儲結果,而實時報告樹則是讓人們鏈接到它以查看結果。
經過定義子節點的順序能夠知道,計數樹啓動後實時報告樹纔會開始運行。除非存儲層可用,不然分區子樹(關於每一個分區的計數結果)將不會運行。若是存儲工做者池(將鏈接到數據庫)可用,則只能啓動存儲的緩存。
我前面提到的監督策略讓咱們在程序結構中對這些需求進行編碼,而且它們在運行時仍然存在的,而不只僅是在啓動時。例如,管理人員可能會採用一對一策略,這意味着各區域可能各自失敗,而不會影響彼此之間的計數。相比之下,每一個地區(魁北克和安大略的管理者)均可以採起休息策略。 所以,這一策略能夠確保 OCR 程序始終能夠將檢測到的投票發送給「計數」工做人員,而且即便常常崩潰也不會對其形成影響。 另外一方面,若是計數工做人員沒法保存和存儲狀態,它的中止會中斷 OCR 程序,確保沒有任何數據丟失。
這個 OCR 進程自己多是用 C 語言編寫的監視代碼,做爲獨立的代理,並與其連接。 這將進一步隔離該 C 語言代碼與虛擬機的故障,以實現更好的隔離或並行化。
我要指出的另外一件事是,每一個主管都有對失敗的可配置容忍度;區域主管可能很是寬容,每分鐘處理 10 次故障,而存儲層若是預期是正確的,則可能對故障至關不寬容,若是咱們但願它是正確的,則在每小時 3 次崩潰後永久關閉。
在這個程序中,關鍵的功能更接近樹的根,這樣能更少的移動和更加堅固。他們不受兄弟節點消亡的影響,但他們本身的失敗影響到其餘人。葉子完成了全部的工做,而且能夠很好地丟失 —— 一旦它們吸取了數據並在上面進行光合做用,它就能夠進入核心。
所以,經過定義全部這些,咱們能夠將危險的代碼隔離在一個具備高容忍度或正在被監控的進程中,並在數據進入系統時將數據移至更穩定的進程。 若是 C 語言中的 OCR 代碼有危險,它能夠失敗並安全地從新啓動。 當它工做時,它將其信息傳輸到 Erlang OCR 進程。 該過程能夠進行驗證,也能夠自行崩潰,也許不會。 若是信息是可靠的,則將其移至 Count 過程,該進程的任務是保持很是簡單的狀態,並最終經過存儲子樹將該狀態刷新到數據庫,這是安全獨立的。
若是 OCR 進程死亡,它會自動重啓。若是它奔潰得太頻繁,它就會將本身的管理器關閉,子樹的那一部分也會從新啓動 —— 不會影響系統的其餘部分。若是這能解決問題,很好。若是沒有,這個過程就會不斷重複,直到它工做,直到整個系統中止,由於某些東西顯然出錯了,咱們沒法經過從新啓動來處理它。
以這種方式構建系統具備巨大的價值,由於錯誤處理被嵌入到系統的結構中。這意味着我能夠不用在邊緣節點中編寫噁心的防護代碼 —— 若是出了問題,讓其餘人(或程序的結構)來決定如何反應。若是我知道如何處理一個錯誤,那麼我能夠對那個特定的錯誤這麼作。不然,就讓它崩潰吧!
這種方式傾向於轉換代碼。慢慢地,你會發現它再也不包含大量的 if/else 或 switch 或 try/catch 表達式。相反,它包含了清晰的代碼,解釋當一切正常時代碼應該作什麼。它再也不包含許多形式的猜想,你的軟件可讀性更強。
當咱們退一步看咱們的程序結構時,咱們可能會發現,在黃色環繞的每一個子樹中,在它們所作的事情上彷佛都是相互獨立的;它們的依賴關係大可能是合乎邏輯的:例如,報表系統須要一個存儲層進行查詢。
例如,若是我能夠交換存儲實現或在其餘系統中獨立使用它,那也是很是好的。將實時報告系統隔離到不一樣的節點或開始提供替代手段(例如 SMS)也可能很整潔。
咱們如今須要的是找到一種方式來打破這些子樹,並將它們轉化爲咱們能夠組合,重用的邏輯單元,而且咱們能夠獨立配置,從新啓動或開發。
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 的精髓:首先創建互動,確保可能發生的最壞狀況仍然是可行的。那麼在你的系統中幾乎沒有錯誤或失敗會讓你緊張(當它發生時,你能夠在運行時自省一切!),那樣你就能夠坐下來放鬆了。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。