一個翻掉了卻沒壞任何東西的位元
你一路爬這座階梯所學的一切,都依靠著一個讓人安心的謊言:你寫下的一個位元會一直是你寫下的那個位元。一個暫存器握住你放進去的東西;一個 DRAM 單元記得它的電荷,直到你去改它。通常是真的——但並非總是如此。在矽片底層,單獨一顆 DRAM 電容或一個 SRAM 單元只握著一丁點電荷,而這世界充滿了能擾動它的東西:晶片封裝裡微量放射性放出的一顆 α 粒子,或是宇宙射線撞上大氣層而打鬆的一顆中子。這樣一顆粒子能沉積足夠的電荷,把一個存好的 0 撥成 1,或把 1 撥成 0。
形容這件事的關鍵字是軟(soft)。一個軟錯誤翻掉的是資料,卻沒損壞元件——往同一個單元寫進一個新值,它又完美無缺地運作了。沒有任何東西壞掉;矽片好好的;只有內容說了謊。這正是它和硬(hard)錯誤的分野:硬錯誤是某顆電晶體或某條導線已經實體故障,會一直給出錯誤答案,直到那個零件被換掉。軟錯誤是個幽靈:你一覆寫它,它就不留痕跡,而這恰恰是它如此令人不安、難以除錯的原因。
故障、錯誤、失效——三個不同的詞
工程師對日常語言混為一談的三個詞很講究,而這份精確很值得。故障(fault)是潛伏在系統裡的瑕疵——那一道宇宙射線、那顆卡死的電晶體、那一行有臭蟲的程式碼。一個被觸發的故障會產生一個錯誤(error):一個實際的錯誤狀態,就像那個此刻坐在記憶體裡的翻掉的位元。而一個傳播到要緊之處的錯誤,就成了一場失效(failure):系統把錯誤的結果交給使用者,帳目差了一塊錢,火箭轉向轉錯了。這條故障、錯誤、失效的鏈,是我們推理可信賴度的脊梁。
把它們分開的理由,是你可以在任何一個環節打斷這條鏈。一個故障大可永不被觸發。一個錯誤一旦存在,也不必抵達輸出——只要你及時抓到並修好它,就永遠不會有失效發生。這就是容錯這整場遊戲:不是阻止每一個故障(不可能——宇宙射線會一直來),而是在錯誤變成失效之前攔下它。本篇主角 ECC 正好落在那中間的環節:它讓一個翻掉的位元發生,然後在任何程式讀到那個錯誤值之前,把這個錯誤改正過來。
幾個額外的位元如何抓出謊言:同位與 ECC
訣竅在於資料中的冗餘:存的位元比訊息所需的還多,好讓一個有效的訊息有一個可辨認的形狀,而一個被破壞的訊息會違反這個形狀。最簡單的版本是單獨一個同位(parity)位元。取 8 個資料位元,數一數有幾個是 1,再加上第 9 個位元,使 1 的總數成為偶數。現在若任何一個位元翻掉——資料或同位都算——這個數目就變成奇數,硬體便大喊「這壞了!」。同位既便宜又能偵測單一翻轉,但它無力去修:它知道數目不對,卻不知道這九個位元裡哪一個在說謊。
要改正而不只是偵測,你需要更多檢查位元,巧妙地安排到哪些檢查失敗的模式會直接指向那個有罪的位元。這就是漢明碼(Hamming code)背後的想法,也是多數 ECC 記憶體所採用的家族。靠著多重重疊的同位檢查,每個資料位元都參與一組獨一無二的檢查組合;當一個位元翻掉,恰好那些包含它的檢查會出錯,而那個簽章就是元兇的二進位位址。把那個位元翻回去,錯誤就消失了。伺服器裡的標準保證是 SECDED:單錯誤改正、雙錯誤偵測——它修好任何單一翻掉的位元,並可靠地察覺(但不修)任何兩個。
Plain 64-bit word in memory ECC 64-bit word in memory
--------------------------- -------------------------
[ 64 data bits ] [ 64 data bits | 8 check bits ]
| |
v v
one bit flips -> silent recompute checks, compare
wrong answer |
+- all agree -> data is clean
+- one mismatch pattern
-> points at the flipped bit
-> flip it back, deliver correct data
Cost: 8 extra bits per 64 = +12.5% memory, spent to buy trust (SECDED).注意那張圖裡攤開的這筆交易:每 64 個資料位元配 8 個檢查位元,是多 12.5% 的記憶體,而編碼與檢查的邏輯又在每一次存取上加了一絲延遲。你正花著實實在在的容量和一點點速度,去買可信賴。這就是這整級反覆出現的那筆交易——安全與可靠幾乎總要付出代價,而誠實的工程問題從不是「免不免費」,而是「在這裡,這份保護值不值它的價」。在一支放音樂的手機上,不值;在守護一家銀行資料庫的錯誤更正碼上,絕對值。
超越單一晶片的冗餘
ECC 守護一塊記憶體晶片裡的一個字,但同一個想法——花掉備援資源,好讓單一故障不至於變成整體的失效——能一路往上放大。冗餘(redundancy)就是它的通稱。你在儲存那一級已經見過一種形式:RAID 把資料連同額外的同位攤在好幾顆磁碟上,於是當某一顆磁碟整個掛掉,缺失的資料就從倖存者那裡重建出來,系統照常服務。同一套哲學,更大的顆粒:能被容忍的不是一個位元,而是一整顆磁碟的故障。
把顆粒再往大推,你就會冗餘地跑整台電腦。一個飛行控制器可能拿三顆相同的處理器跑同一份輸入,再投票:若兩顆一致、一顆唱反調,那個落單的就被否決並標記。在你無法遮蔽故障的地方,次好的辦法是優雅降級(graceful degradation)——當某個零件故障,系統卸下能力而不是崩潰。一顆有許多核心的晶片若因瑕疵失去一顆核心,可以當作一個比較便宜、能用核心較少的零件出貨;一個失去一台伺服器的服務,會繼續跑得稍微慢一點,而不是整個暗下去。優雅地降級把一場本將發生的失效,變成僅僅是縮水的服務。
把信任量出來:可靠度、可用度、MTBF
若你打算花錢買保護,你需要能拿來爭論的數字。可靠度問的是:系統在一段時間裡持續正確運作、不發生任何失效的機率有多大?可用度問的是一個微妙不同的問題:在隨機的某個時刻,系統有多大比例的時間是開著並提供服務的?這兩者並不相同。一個常常故障卻能眨眼間恢復的系統,可以可靠度很糟卻可用度極佳;一個幾乎從不故障、可一旦故障要修一週的系統,則可能相反。
把可靠度繫到真實世界的那個數字,是平均故障間隔(MTBF):你預期到下一次故障之前的平均運轉時間。可用度於是有一個乾淨的形狀——大致是 MTBF 除以(MTBF 加上平均修復時間)。藏在那個分數裡的教訓令人豁然開朗:要提高可用度,你可以靠故障得少一點(更大的 MTBF),或靠恢復得快一點(更小的修復時間)。在資料中心的規模上,總有某個元件正在死去,工程師往往放棄「永不故障」,轉而瘋狂投資於快速、自動的恢復——修復時間縮向零,於是即使故障不斷,可用度依然很高。
帶著一個誠實的但書往前走。這些數字把隨機、獨立的故障描述得很漂亮,但最可怕的失效是那些被數學悄悄假設掉的相關故障——一場掀掉整個機架的停電、一次同時推到每一台伺服器的壞軟體更新,或是這一級其餘篇幅的主角:那個蓄意的攻擊者。可靠度的數學保護你不受壞運氣之害;它不保護你不受惡意對手探測你時序之害,而那正是我們接下來要轉向的地方。軟錯誤是大自然的無動於衷;接下來的導覽,講的是有人很聰明。