- 原文地址:As bad as anything else: Part 1
- 原文做者:Fred T-H
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:steinliber
- 校對者:kasheemlew, SergeyChang
我在由 Genetec 組織的 ConnectDev'16 會上受邀演講,這個是我所介紹部分的一個鬆散的文字記錄(或者也能夠說是較長的釋義)。html
我假定這裏的大多數人都沒有使用過 Erlang,或許可能已經據說過它,或許就只是知道這個名字。在這種狀況下,這個介紹只會涵蓋 Erlang 中的高層次理念,使用這種方式來說述的話即便你從未接觸過這個語言,它也會對你的工做或副項目有所幫助。前端
若是你以前曾經瞭解過 Erlang,那你應該已經聽過「就讓它奔潰」的箴言。當我第一次遇到這句話時就在想這究竟是什麼鬼玩意。Erlang 應該對併發和容錯有着很好的支持,可是我卻被告知就讓它奔潰吧,這和我想要這個系統發生的徹底相反。這個主張讓人難以想象,可是 Erlang 之禪卻與之息息相關。android
在必定程度上,在 Erlang 中使用「就讓它奔潰」這句話就和在火箭科學中使用「就讓它爆炸」同樣可笑。在火箭科學中「就讓它爆炸」也許是你最不想發生的事 - 挑戰者號空難就是一個鮮明的提醒。相對的若是你用不一樣的方式來看待這件事,火箭和它的整個推動機制都是要處理危險且會爆炸的可燃物(這是其中危險之處),可是它經過可控的方式來使用這種能量來驅動空間旅行或者把載荷送上軌道。ios
這裏的重點在於控制;你能夠嘗試把火箭科學看做是一種如何正確駕馭爆炸的方式 - 或者至少是駕馭其中包含的能量 - 用於作咱們想要的事情。從同一個角度看就讓它奔潰也是同樣的道理:它全部一切都是關於容錯。這個想法並非說讓不可控制的錯誤遍及各處,而是把失敗,異常和奔潰轉化爲咱們可使用的工具。git
回燃法和受控制的燃燒是真實世界中用火來滅火的真實例子。在我出生的加拿大薩格內-聖約翰,藍莓田會例行以受控的方式被燒燬,以幫助支持和延續它們以後的生長。爲了防止森林火災,使用火來清除森林中已經枯萎了的部分是很常見的行爲,這樣森林才能被適當地監管和控制。這裏的主要目的是用這種方式來去除可燃材料,這樣真實的火災就不能進一步蔓延。github
在全部這些狀況下,火對莊稼或者森林的破壞力被用於確保莊稼的健康或者是防止森林地區發生更大的沒法控制的火災。編程
我認爲這就是「就讓它奔潰」所想表達的。若是咱們能夠經過一種很是好的控制方式來擁抱失敗,奔潰和異常,它們就不會是須要避免的可怕事物,而能成爲構建大型可靠系統的強大基石。後端
因此這個問題就變成想出一個辦法確保奔潰是促進者而不是毀滅者。對於這個,Erlang 使用進程做爲它的基本棋子。Erlang 的進程是徹底獨立的,進程之間不共享任何東西。任何進程都不能訪問其它進程的內存,或者經過修改它所操做的數據來影響它的工做。這樣就很棒了,由於這意味着咱們能夠保證一個進程死亡只會把問題保留在進程內,從而爲你的系統帶來了很是強大的故障隔離。安全
Erlang 的進程也很是輕量,你就算運行成千上萬個也不是問題。這裏的想法是使用你__須要__運行數量的進程,而不是你__能__運行數量的進程。這裏一般的比較就是說,若是你有一個面向對象的語言,該語言在任何運行時間中只能有 32 個對象,你很快就會發現使用這種語言構建程序會受到過多的限制並且是很是荒謬的。擁有許多小的進程能夠確保在拆分事物中有更高的粒度,並且在一個咱們想要利用這些失敗力量的世界中,這很棒!服務器
如今想象這樣 Erlang 中進程是如何工做的可能會有些奇怪。當你寫一個 C 程序時,你有一個大的 main()
函數作大量的事情。這個函數是你程序的入口點。在 Erlang 中就沒有這樣的東西。沒有進程是這個程序指定的主進程。每個進程都運行一個函數,這個函數在對應單個進程中扮演着 main()
函數的角色。
咱們如今有一羣蜜蜂,可是若是它們之間不能經過任何方式溝通,那可能就很難管理它們加固蜂巢。蜜蜂是經過舞蹈進行溝通,而 Erlang 的進程是經過消息傳遞。
在並行環境中,進程間消息傳遞是最直觀的通訊形式。這是咱們工做過最古老的通訊方式,從咱們開始寫信經過郵差騎馬來送到目的地的日子,到這個幻燈片中展現的拿破崙信號塔。在這種狀況下,你只須要帶着一羣人進入塔樓,給他們一個消息,他們就會揮舞旗幟以比騎馬更快的方式把消息傳遞到遠方,但這很容易讓人疲勞。最終這些都被電報取代了,以後又被電話和收音機取代了,如今咱們擁有全部這些很棒的技術來傳遞消息,而且能夠傳的很遠、很快。
全部這些消息轉遞方式特別是過去的其中一個關鍵在於它們都是異步的,並且傳輸的消息都會被複制。沒有人會爲了等寄信的信使回來而在門廊上站好幾天,也沒有人(至少我認爲)會坐在信號塔中等消息的響應發回來。你只是會把消息發送出去,而後回去作你的平常工做,最終會有人告訴你有回信了。
這很合理由於若是另外一邊沒有迴應,你不會什麼事都不作而是傻傻的在門廊前一直等到死去。相反,若是你死了,消息交流通道另外一邊的收信人也不會奇蹟般的立刻收到或改變你的消息。數據__應該__在被髮送出以前被複制一遍。這兩個原則確保通訊過程當中的失敗不會致使一個損壞或者沒法恢復的狀態。Erlang 實現了這兩個原則。
爲了能閱讀消息,每一個進程都有一個郵箱。任何一個進程均可以寫消息到一個進程的郵箱,可是隻有擁有這個郵箱的進程能查看它。這些消息會默認以它們到達的順序被讀取,可是也有可能經過模式匹配[咱們在先前的演講中討論過這個]之類的特性來讓進程臨時只關注一種,從而驅動不一樣優先級消息的執行順序。
大家之中的部分人會注意到我剛纔所提到的一些事項;我一再重申隔離和獨立性是偉大的,這樣一個系統的組件就能夠在不影響其它組件的狀況下死亡和奔潰,同時我也提到了在多個進程或者代理之間進行交流。
每當有兩個進程開始交流,咱們在它們之間就建立了一個隱性的依賴。系統中會有一些隱性的狀態將它們綁定在一塊兒。若是進程 A 向進程 B 發送了一個消息,可是 B 進程已經死亡而沒有響應 A,那麼 A 進程所能作的要麼就是永遠等着,要麼就是在一段時間後放棄和 B 進程的交流。後者是一個有效的策略,可是它也是一個十分模糊的策略:它並不知道遠端的那個進程是已經死了還只是處理時間比較長,解除綁定以後遠端進程的消息才發送到你的郵箱。
相反,Erlang 爲咱們提供了兩種機制來處理這種狀況:監視器和連接
監視器所作的所有就是做爲一個觀察者,一個攀緣植物。你決定去留意一個進程,若是該進程出於任何緣由死亡了,你就能夠在你的信箱中獲取到關於這個的消息。你就能夠對此做出反應而且經過你新發現的信息來作出決策。其它的進程永遠也不會知道你對它作了什麼。若是你是一個觀察者或者關注對等進程的狀態,那麼監視器能夠是很是棒的工具。
連接是雙向的,創建一個連接就會將其所相連的兩個進程命運綁定。當其中一個死亡時,任何與之連接的進程都會收到退出信號,這個退出信號會殺死這些進程。
如今這就真變得頗有趣了,由於咱們使用監控器來快速地檢測到失敗,並且我還能使用連接做爲一個架構構造夠把多個進程綁定在一塊兒做爲一個共同的失敗單元。不管什麼時候我獨立構建的模塊開始有相互之間的依賴,我可以開始把這些依賴寫入個人代碼中。這頗有用,由於這樣就能夠防止個人系統意外奔潰進入到一個不穩定的局部狀態。連接是一種工具,它可讓開發人員確保當其中一件事失敗時,最終會徹底失敗並留下一個空的白板,而不會影響這個運行中沒有牽涉到的組件。
在這個幻燈片中,我選了一張爬山者經過繩子連在一塊兒的照片。如今若是爬山者之間只有這個連接,他們恐怕會陷入一個糟糕的境地。任什麼時候間你隊伍裏的一個爬山者滑倒,隊裏的其餘人也會立刻滑倒死去。這並非一個作事情的好辦法。
相反,Erlang 可讓你指定某些進程是特殊的,這些進程會使用 trap_exit
選項做爲標記。而後他們能夠接受連接發送過來的退出信號而且將它們轉化爲消息。這可讓它們從錯誤中恢復過來而且可能啓動一個新的進程來作以前死掉進程的工做。不像爬山者那樣,這種特殊的進程不能阻止一個對等進程奔潰;這是這個對等進程的責任來確保本身不會掛掉,好比說經過 try ... catch
表達式。一個收到退出信號的進程仍是沒有辦法進入另外一個進程的內存而後保存這些內存,可是它能夠避免由於這個而死去。
這成爲了實施監督者的關鍵特性。若是你歷來沒有據說過這些,咱們很快就會接觸到這些。
在進入監督者這部分以前,咱們仍須要一點調料才能成功地烘焙出一個系統,這個系統利用奔潰來得到自身的優點。其中之一與進程如何調度有關。對於這方面,我想提到的真實世界例子是阿波羅 11 號的登月計劃。
阿波羅 11 號是在 1969 年的登月任務。在這個幻燈片中,咱們看到 Buzz Aldrin 和 Neil Armstrong 的登月艙,這張照片我認爲是 Michael Collins 拍的,在此次任務中他留在了指揮艙中。
在他們登月的途中,登月艙將由 Apollo PGNCS(主要指揮,導航和控制系統) 所引導。這個指導系統有多個任務在上面運行,它們的運行週期數是被仔細斟酌過的。NASA 也指出全部任務運行只佔用了處理器 85% 的容量,還剩下 15% 的空間。
如今,爲了在應對須要終止計劃的狀況,宇航員們須要制定一個完善的備份計劃。因而他們還用處理器運行了一個交會雷達以防萬一它能派上用場,這會用掉 CPU 所剩容量中的一大部分。當 Buzz Aldrin 輸入指令時會出現大量關於溢出和容量耗盡的報錯信息。若是控制系統所以失控,它將沒法正常工做,而且害死兩名宇航員
這主要是因爲雷達存在已知的硬件錯誤會使它的運行頻率和指揮計算機不匹配,這就致使它竊取了比其原本所應該有的更多的運行週期。固然 NASA 的人也不是白癡,在這種關鍵的任務中他們重用了他們所知道以前用過不多發生錯誤的組件,而不是研發一個新的技術。可是更重要的是,他們設計了優先級調度。
這意味着即便由於這種雷達或者輸入命令致使處理器過載的狀況下,若是它們的運行優先級與性命攸關的事情相比很低,那麼這些任務將被殺死,從而把 CPU 運行週期給真正迫切須要它的任務。那是在 1969 年;在今天仍然有大量的語言或者框架給你的__只是__合做調度,除此以外別無他有。
Erlang 並非一種用於構建生命攸關係統的語言 - 它只遵循軟實時時間約束,而不是實時時間約束因此在這些場景中使用它並非一個好主意。可是 Erlang 爲你提供了搶先試調度以及相應的進程優先級。這就意味着做爲一個開發者或者系統設計人員,你並不__須要__去關心確保每一個人都仔細統計了他們全部組件的(包括使用的庫)CPU 使用量以避免使整個系統變慢。他們並無這個能力。並且若是你須要一些重要任務在它必須運行時總能運行,你也能實現這個。
這彷佛並非一個大的或者通用的需求,人們仍是能經過協做式的併發任務開發真正成功的項目,可是它確實十分有價值,由於它可使你免受他人和你本身錯誤的影響。它還爲像自動負載平衡,懲罰和獎勵好或者壞的進程或者給予須要作大量工做的進程更高優先級提供了實現機制。這些東西最終都會使你的系統更好的適應生產環境負載和處理意外事件。
我想討論得到優雅容錯性的最後一個調料是網絡意識。在咱們開發的任何須要長時間運行的系統中,讓多臺計算機快速的運行這個系統是一個先決條件。你不會想坐在由鈦門鎖在裏面的金色機器旁邊,卻不能忍受任何方式引發的中斷影響到你的用戶。
因此最終你須要兩臺計算機,這樣一臺機器能夠在另外一臺破環時繼續提供服務,若是你想要在損壞計算機仍是你係統一部分的時候部署,那麼你或許就須要第三臺。
這個幻燈片中的飛機是 F-82 雙生野馬,這是一架在第二次世界大戰期間設計的飛機,用於護送大多數其它戰機沒法覆蓋範圍內的轟炸機。它有兩個駕駛艙,這樣隨着時間推移,當一個駕駛員累的時候另外一個能夠互相替換;在一些狀況下他們也能相互配合,其中一我的飛行的時候,另外一個能夠操做雷達做爲攔截者的角色。現代飛機仍然在作相似的一些事情;他們有數不清的備用方案,常常有機組人員在飛行期間的途中睡覺,以確保總有人能時刻警戒準備好駕駛飛機。
當這個說法用於編程語言或者開發環境,它們中大多數的設計都徹底忽略了分佈式,儘管人們都知道若是你寫的是服務器棧,那麼你須要的就不止一臺服務器。然而,若是你要使用文件,這些變成語言就會有標準庫幫你完成這些事情。大多數語言更進一步就是給你一個套接字庫或者 HTTP 客戶端。
Erlang 意識到了分佈式這個事實而且爲你提供了一個實現,這個實現是有文檔記錄並且透明的。這可讓人們爲故障轉移設或者是接管奔潰的應用配置所想要的邏輯從而提供更高的容錯性,甚至可讓其它語言僞裝它們是 Erlang 的節點來構建多邊形系統。
全部這些就是 Erlang 之禪食譜的基本調料。這整個語言的目的在於得到崩潰和失敗,並使它們如此易於管理,從而有可能將它們看成工具。就讓它崩潰開始有道理起來了,這裏看到的原則大部分都是能夠在非 Erlang 系統中做爲靈感重用的。
如何將它們組合在一塊兒是下一個挑戰。
監管樹描述的是如何實施你 Erlang 程序的架構。它們源自於一個簡單的觀念,有一個監管者,它惟一的工做就是啓動進程,關注進程的運行,而後在它們運行失敗時重啓它們。順便提下,監管者是 ‘ OTP ’ 的核心組件之一,它是被普遍使用的開發框架,名字叫作 ‘ Erlang/OTP ’。
這樣作的目的是建立一個層級結構,在這個結構中全部重要必須穩定運行的東西越接近樹的根部,而全部易變或正在轉移的部分則會積累在葉子部分。事實上,這就是現實生活中大多數樹木的樣子:樹葉不是固定的,樹上會有不少樹葉,在秋天它們都會飄落下來,而這個樹仍然活着。
這意味着當你構建 Erlang 程序時,任何你以爲脆弱的容許運行失敗的進程應該處於這個層級的更深處,而穩定並且可靠性要求很高的應該移到層級上面。
監管者是經過使用連接和捕獲退出來實現這個功能的。它們的工做從一次啓動它們的子進程開始,從上往下,從左到右。只有當一個子進程徹底開始以後它纔會返回上個層級開始建立下一個子進程。每個子進程都會被自動連接。
每當一個子進程死亡時,有如下三個策略可供選擇。第一個策略在這個幻燈片中就是 ‘一對一’,經過替換死去的子進程來實現。這是用於監管者的全部子進程相互之間都獨立時的策略。
第二個策略是‘一便是所有’。這個策略用於子進程之間存在相互依賴關係。當它們中的任何一個死去時,監管者就會在把它們所有從新啓動以前把其它全部子進程都殺掉。當失去一個特殊的子進程會使其它進程陷入一個不肯定的狀態時,你就可使用這個策略。讓咱們想象三個進程進行一個對話,該對話以投票結束的。若是在投票過程當中其中一個進程死亡,那麼可能咱們並無編寫任何代碼來處理這個問題。用一個新的替換死去的進程會在表格上帶來一個新的同伴,而其中的全部進程徹底不知道接下來該作什麼。
若是咱們沒有真正定義當一個進程在投票過程當中形成嚴重出故障時要怎麼作,那麼這種不一致的狀態多是有危險的。相比於這個,殺死全部的進程可能會更安全,而後從已知穩定的狀態從新開始。經過這樣作咱們就能夠限制錯誤的範圍:在錯誤發生時早點及時奔潰會比慢慢且長時間毀壞數據要更好。
當進程之間根據它們的啓動順序有依賴關係時一般能夠用最後這種策略。它的命名叫作‘一個所剩下的’,當一個子進程死亡時,在以後它後面啓動的進程會被殺死。而後進程就會像以前預期的那樣從新啓動。
每一個監管者還額外有可配置的控制和忍耐級別。一些監管者可能中斷以前天天只能忍受一個故障,而其它的或許能夠每秒承受 150 個故障。
在我提到監管者以後你們一般都會說起的評論就是「可是若是個人配置文件就是錯的,重啓並不能解決任何問題!」。
這徹底正確。重啓有效的緣由在於生產環境系統中所遇到的錯誤性質。爲了討論這個問題,我必須說起 Jim Gray 在 1985 年提出的 ‘ Bohrbug ’ 和 ‘ Heisenbug ’ 這兩個術語(我建議你儘量多讀下 Jim Gray 的論文,它們都寫的很棒!)。
基本上來看,一個 bohrbug 是一個穩定的,可觀察的並且可復現的錯誤。它們傾向於能夠被開發者容易地推測出問題的緣由。相反 Heisenbug 具備不可靠的行爲,它不會在肯定的條件中出現,並且若是隻是採起簡單的行爲嘗試去觀測這些問題時它們可能會被隱藏起來。好比說在系統中使用每一個操做都會被循序執行的調試器時,併發錯誤就沒法查找出來。
Heisenbugs 是這些在一千次,百萬次,十億次或者萬億次錯誤中才會出現一次的使人討厭的錯誤。當你看到有人打印了一頁又一頁的代碼以及在它們中填上一大堆標記時,你就知道他已經處理這種類型的錯誤有一段時間了。
定義了這些術語以後,讓咱們來看看它們的出現頻率應該是多少。
在這裏,我把 bohrbugs 列爲可重複的錯誤類型,把 heisenbugs 列爲暫時的錯誤類型。
若是你在你係統的核心功能中有 bohrbugs,那麼當這個系統到達生產環境以前它們應該能很容易被找出來。經過可重複性,以及這類錯誤一般在程序運行的關鍵路徑上,你應該早晚會遇到它們,並且在到達下一個階段以前修復它們。
那些發生在次要的,更少使用的功能上的錯誤,更像是提醒和錯過的事。每一個人都認可的是修復軟件中的所有錯誤是一件艱苦的戰爭,爲此獲得的收益是遞減的;隨着你繼續編寫代碼,除去其中的小缺陷可能要花愈來愈多的時間。一般狀況下,這些次要功能每每會收到較少的關注,不只由於較少的客戶會使用它們,還由於它們對滿意度的影響並無那麼重要。或者也許它們只是要晚些時候被安排修復並且把時間表拖後最終會下降開發人員處理這個的重要度。
在任何狀況下,它們在必定程度上都挺容易找到的,咱們只是沒有時間或者資源來作這件事。
Heisenbugs 幾乎不可能在開發過程當中發現它們。像形式證實,模型檢查,窮舉測試或者基於屬性的測試這些很棒的技術可能會增長髮現其中一部分或者所有問題(取決於所用方法)的可能性,可是坦白講,除非手頭上的任務是很是關鍵的,不然咱們中不多有人使用這些技術。在數十億次中出現一次的問題就須要大量的測試和驗證才能發現,並且若是你已經看到過這個錯誤,那麼極可能沒那麼好運氣再次產生這個錯誤。
更多內容請見本文第二部分:Erlang 之禪:第二部分。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。