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