作者:巴斯蒂安·科歇爾
特爾;博士: 2021 年5 月24 日,Polkadot 節點因塊5,202,216 上的內存不足(OOM) 錯誤而失敗。該區塊包含驗證者選舉的鏈上解決方案,該解決方案通常在鏈外計算,只有在沒有提交鏈下解決方案時才會在鏈上進行。由於提名人眾多,選舉溢出了Wasm環境中分配的內存。
在準備更新以解決該問題時,驗證者被要求暫時將他們的節點軟件降級到包含本機(非Wasm)運行時版本的先前版本。本機版本不受Wasm 內存分配器的限制。網絡在停機1 小時10 分鐘後恢復。
後來,在區塊5,203,204 上,幾個節點因“存儲根不匹配”錯誤而失敗。經過調查,這是由於構建本機運行時和鏈上Wasm 運行時的編譯器版本不同。解決方案是實現一項功能,該功能允許使用具有正確編譯器版本的Wasm 運行時構建覆蓋鏈上Wasm 運行時。
此問題已得到解決,並已採取預防措施以防止將來再次發生這種情況。
2021 年5 月24 日,Polkadot 節點在嘗試構建塊時因內存不足(OOM) 錯誤而失敗 5202216. 節點本身沒有崩潰,但運行時崩潰了(即區塊鏈的狀態轉換函數)。 Polkadot 的運行時是用WebAssembly 編寫的,由Wasm 解釋器或Wasm 編譯器執行。但是,作為運行時執行環境的一部分,始終提供固定數量的內存(當時為64MB),這對於該塊來說還不夠。
該區塊是該時代倒數第二個會話的最後一個區塊,這意味著需要為下一個會話之後開始的新時代選舉一個新的驗證者集。驗證者集的選舉可以在鏈下或鏈上進行,但鏈下是首選,因為選舉算法是一項非常繁重的計算任務。但是,對於本次會議,沒有驗證者提交解決方案(大概是因為他們在進行鏈下選舉時也遇到了相同的OOM),因此需要在鏈上完成,結果是所有驗證者同時獲得了OOM試圖創作這個塊。 OOM 的解決方案相當簡單——將Wasm 運行時的默認內存大小增加到128MB: https://github.com/paritytech/substrate/pull/8892.
要將這一變化帶給所有驗證器,需要削減一個新版本,並且需要更新大量驗證器。然而,在短期內有一個更容易解決這個問題的方法(最重要的是可以更快地部署)。 Polkadot 的運行時不僅編譯為Wasm,還編譯為本地代碼以獲得更好的性能,最重要的是,本地運行時在執行期間不會對內存使用設置任何限制。但是,當運行節點與鏈上運行時來自同一版本時,本機運行時僅匹配鏈上運行時。此時的鏈上運行時是與2021 年4 月8 日發布的v0.8.30 版本匹配的運行時。從那時起,已經有3 個新版本,這意味著大多數驗證器已經在運行最新的節點版本(v0.9.x)。
因此,為了盡快解決有問題的區塊,所有驗證器操作員都被要求將其驗證器降級到v0.8.30,並使用`– execution native` 標誌運行它們以強制使用本機運行時運行。總體而言,從檢測到問題、提出短期解決方案、向驗證者宣布並最終構建新塊並使網絡完全恢復,大約需要1 小時10 分鐘。
網絡恢復後,我們開始準備0.9.3 版本以分發Wasm 最大內存使用量的增加,以便我們可以再次支持使用Wasm 運行時。在這個過程中,我們使用了一個節點,並想檢查將有問題的塊與增加的內存上限同步現在是否適用於Wasm。有問題的塊確實有效,但我們在嘗試導入時遇到存儲根不匹配 5203204.
存儲根不匹配意味著導入塊不會導致塊作者公佈的相同存儲根。一般來說,在區塊鏈中,相同的輸入應該總是導致相同的輸出。但是,在這種情況下,網絡仍在運行並構建塊,這只能意味著本機和Wasm 運行時之間存在不確定性,因為我們已指示所有驗證器使用本機運行時運行。
所以我們開始調查原生和Wasm 運行時之間的不匹配。我們嘗試首先使用相同的版本和本機運行時在本地同步鏈。但是,這也導致了同樣的存儲根不匹配。這更令人擔憂,因為為相同架構編譯的相同代碼應該總是產生相同的結果。當我們編譯Wasm 運行時,我們使用所謂的“no-std”環境來執行此操作,這涉及使用不同的代碼路徑。因此,引入一些不匹配“更容易”,但是兩次編譯本機運行時應該會導致代碼兩次都在做同樣的事情。
這讓我們假設rust 編譯器可能生成了導致我們看到的不匹配的錯誤代碼。由於一些極端的運氣(否則我們的努力可能會花費更長的時間),Parity 的某個人仍然有一個這個版本的二進製文件,它與github 上發布的版本不同。這個二進製文件能夠將鏈與本機運行時同步,沒有任何問題。這個二進製文件與我們之前構建的二進製文件之間的唯一區別是所使用的rust 編譯器版本。所以我們認為最新的編譯器版本和我們當時用來構建節點的版本之間可能發生了一些變化。是的,在降級Rust 編譯器並重新構建發布分支後,該節點現在成功地同步了。
在驗證了用舊的rust 編譯器編譯的原生運行時可以同步鏈後,我們還嘗試用這個rust 編譯器編譯Wasm 運行時。 Polkadot 節點有一個特殊標誌,允許我們使用本地版本覆蓋鏈上Wasm 運行時,我們使用它來驗證同步是否有效。所以問題變成了,為什麼我們在0.8.30 版本的原生和Wasm 運行時之間存在這種不匹配?您需要知道我們使用rust nightly 編譯器來編譯Wasm 運行時(nightly 是必需的,因為並非我們在Wasm 構建中使用的所有內容都在穩定的rust 編譯器中)。用於節點和Wasm 運行時的編譯器版本是 發佈公告.
因此,1.51.0 穩定版Rust 編譯器(於2021 年3 月23 日發布並用於構建本機運行時)和7 月4 日用於構建Wasm 運行時的rust nightly 編譯器之間肯定發生了一些變化。在將這些日期之間的Rust 工具鏈一分為二後,我們發現2021 年3 月5 日的nightly 是第一個打破我們確定性的工具鏈。所以我們只需要檢查在04.03.2021 和05.03.2021 之間合併的提交並發現問題 犯罪.
在沒有提交的情況下編譯rust 編譯器並使用自建編譯器編譯我們的節點表明本機運行時生成了正確的數據,我們可以同步鏈。提交更改了`binary_search_by` 函數,當有多個匹配項時,它可以返回不同的索引。當我們在運行時使用這個函數時,它可能會導致存儲在狀態中的數據的順序略有不同,從而導致不同的存儲根。
因此,這意味著我們現在擁有無法與Wasm 運行時同步的本地運行時構建的塊,並且我們無法更改鏈上Wasm 運行時來解決此問題,因為您無法在不分叉的情況下重寫區塊鏈的歷史記錄。我們想出了一個 拉取請求 將`code_substitute` 引入鏈規範。鏈規範主要用於存儲關於鏈的起源和其他一些信息。這個新字段“code_substitute”是一個映射,它使用塊哈希作為鍵並映射到Wasm 運行時代碼blob。它指示節點使用鏈規範中指定的塊之後的每個塊中的給定塊覆蓋鏈上Wasm 運行時,直到運行時的規範版本不再匹配。
我們還創建了一個 拉取請求 使用具有正確值的“code_substitute”使節點能夠使用Wasm 再次同步。任何人都可以使用`srtool` 重新構建運行時,以確保構建的是v0.8.30 中的代碼並且他們獲得相同的Wasm blob。
隨著 0.9.3 發布 該節點包含使鏈按預期工作所需的所有修復。
未來我們將進一步改善現狀:
- 現在將以更高的優先級尋求棄用本機運行時。使用Wasm 編譯器Wasmtime 已經將我們帶到了與使用原生運行時幾乎相同的性能水平,因此我們不再真正需要原生優化。尤其是所有潛在的缺點。
- 分配器將得到改進以支持更靈活的資源分配,這意味著我們不會將最大分配限制為128MB,並且可能會支持最大的Wasm(4GB)。
- 鏈上選舉將被完全禁用; 選舉現在需要總是在鏈外發生並提交給運行時。
- 在分配器得到改進之前,鏈下工作程序將使用比鏈上Wasm 運行時執行更高的內存限制。這應該有助於確保鏈下選舉不會耗盡內存並且可以成功提交。
- 目前,對於原生和Wasm 運行時,我們將確保對原生和Wasm 構建使用相同的編譯器版本。這應該可以防止遇到因使用不同的工具鏈版本而導致的更改。