Redis PSYNC2 錯誤事後檢討
四天前,一位使用者在 Redis 的 GitHub 儲存庫回報了一個重大問題。這個問題與 Redis 4.0 全新的 PSYNC2 複寫協定有關,且非常嚴重。PSYNC2 為 Redis 複寫帶來了許多好處,包含在 failover(容錯移轉) 之後,甚至在由從節點自行控制的重啟之後,僅需交換差異資料即可重新同步,而無需傳送整個資料集。而這次的問題就出在後者這項功能上:在 PSYNC2 中,RDB 檔案會被擴增以包含複寫資訊。從節點重新啟動後,會重新載入複寫中繼資料,並能夠嘗試執行 PSYNC,試圖與主節點交握以接收自上次中斷連線以來的差異資料。
從 Redis 維運的角度來看,這些全都是好消息,然而,雖然自 Redis 4.0.0 正式版推出以來,PSYNC2 本身相當穩定,但涉及從節點重啟的這項功能,其可靠性明顯不足。這項功能有兩個問題:第一,它是 PSYNC2 最後一刻才加入的功能,並未包含在原始設計文件中。它比較像是我們在 PSYNC2 上所做工作的自然延伸,但並未像規格中其他部分那樣,經過同等程度的嚴格審視,以找出潛在的錯誤與問題。第二個問題在於,這項功能比起初看起來要複雜得多,因為要在重啟後完整還原從節點端複寫的所有狀態,其實相當棘手。此外,若未能還原某些狀態位元,多數情況下並不會產生明顯的錯誤,因此很難透過整合測試發現。只有在特定條件發生時,缺少某些狀態才會導致問題。舉例來說,若未能在從節點的複寫狀態中正確重建當前選定的資料庫,只有當不同 Redis 資料庫中同時有寫入操作時才會產生問題,而這類錯誤也只有在特殊條件下才會無法正確儲存當前選定的資料庫。
感謝眾多 Redis 貢獻者的協助,尤其是 GitHub 使用者 @soloestoy(一位任職於 Alibaba 的 Redis 開發者),我們近期致力於改善許多 PSYNC2 的潛在問題。所有完成的工作預計在幾天內,經過對迄今為止所有修補程式的充分測試後,一併納入 Redis 4.0.3。然而,在收到 issue #4483 之後,我意識到必須趕緊發布 Redis 4.0.3,因為這個問題遠比我們到目前為止發現的其他 Redis 4 PSYNC2 問題嚴重得多。
issue #4483 中描述的錯誤相當單純,卻非常棘手。在從節點重啟並從 RDB 重新載入複寫狀態後,主節點的 replication backlog(複寫積壓佇列) 中可能包含以 EVALSHA 指令形式存在的 Lua 腳本執行紀錄。然而,從節點上的 Lua 腳本引擎在重啟後會清空所有腳本,因此無法處理這類指令。這導致從節點無法處理源自 Lua 腳本的寫入操作,除非這些腳本使用的是「commands replication(指令複寫)」,但這並非預設值。預設行為是直接複寫腳本本身。
我有點陷入恐慌狀態……並撰寫了幾個替代修補程式提交到該 issue。最終,選擇了不需要與舊版 RDB 產生不相容性的那個方案,因此我便盡快發布了 Redis 4.0.3。然而我犯了一個錯誤……這幾週我都在辦公室工作,而非在家工作。平常我晚上不工作,但為了 4.0.3,我回到家後打開家裡的筆記型電腦,將修補程式合併到 4.0 分支,並進行了一些測試。隔天回到辦公室後,我改用另一台電腦繼續工作,卻沒意識到自己漏掉了一個在家中合併、但尚未推送到儲存庫的 commit。
所以,我實際上發布的 4.0.3 包含了所有 PSYNC2 修正,卻唯獨少了針對該複寫錯誤最關鍵的一項修正。我盡快發布了一個新的修補版本 Redis 4.0.4,補上了這個複寫修正。這已經夠糟了:升級 Redis 是一項需要排程的工作,沒有人會想因為我在準備發行版本時出錯而需要升級兩次……但更糟的還在後頭。我在 4.0.4 中加入的修正,也就是關於執行 PSYNC2 的重啟從節點上的腳本複寫的修正,存在一個完全未被所有複寫整合測試發現的錯誤:該修正的做法是將從節點記憶體中的 Lua 腳本直接儲存到 RDB 中,以便之後重新載入。然而,卻沒有考慮到載入腳本的函式在腳本已存在於記憶體中時會觸發 assert,因此當從節點從主節點接收到完整同步時,便會開始載入 RDB 檔案,並因為記憶體中已存在重複的腳本而立即當機。有使用者很快透過 Twitter 回報了這個問題,我在不到 45 分鐘內就從接獲回報到發布 4.0.5 完成了修復,但這並無法彌補這一系列發布修正失敗所可能造成的各種問題。
上述問題是由多種原因所造成:
- 考量到 PSYNC2 的複雜度與程式碼異動幅度,Redis 4.0 太早被標示為穩定版本。下一次,即使會延後發布時程,我也會讓 Redis 的新主要版本在最後的候選版階段停留更久,以便我們能在發布正式版本(GA 版本)之前找出這類錯誤。
- 為了發布 issue #4483 的修正,我過於倉促。即使該錯誤非常嚴重,也應該花些時間釐清實際狀況,以及修正本身可能帶來的潛在問題。此外,我因為太過匆忙,未仔細檢查組成 4.0.3 的整組 commit,導致遺漏了其中一項修正,進而需要再發布一個新版本。
- 雖然 Redis 4.0 針對 PSYNC2 擁有非常嚴格的單元測試與整合測試,包含模擬連續 failover 並在每個步驟檢查資料一致性的測試,但因為這次的錯誤,我才發現這些測試從未嘗試將 PSYNC2 與 Lua 腳本的複寫混合測試。這部分必須改進。此外,透過從節點 RDB 重啟後的 PSYNC2 複寫測試也必須加強。
我會先從「3」的改進開始,未來在每次新版本發布前,也會謹記「1」與「2」的教訓。在此向所有因我的失誤而受到影響的人致上誠摯的歉意,並由衷感謝 @soloestoy 提供了大量的協助與支持。
隨機一篇部落格