Redis Cluster,不再是幻影
在我的 Git 歷史紀錄中,能找到關於 Redis Cluster 的第一筆提交紀錄日期是 2011 年 3 月 29 日,但那其實是一次「copy and commit」形式的合併:Cluster 分支的歷史紀錄已被銷毀,因為當時充滿了混亂的 WIP 提交,只是為了勾勒出 API 以及與系統其他部分互動的初步構想。
基本上,這是一個約 4 年歷史的專案。這大約占了 Redis 專案整體歷史的三分之二。然而,直到今天,我才釋出 Redis 3.0.0 的第一個 Release Candidate(候選版),也就是第一個支援 Cluster 的版本。
曲折的歷程
要理解為何花了這麼久,原因其實很直觀:當初我是在非常倉促的情況下啟動 Cluster 專案的,當時看起來如果沒有自動擴展的方式,Redis 似乎就要變得完全無用。那其實並非啟動 Cluster 專案的正確時機,單純是因為 Redis 本身還太不成熟,我們甚至還沒有一套穩固的「單實例」論述可以提出。
雖然我在錯誤的時機啟動專案犯了錯,但至少我沒有掉入忽視社群需求的陷阱,因此這個專案被無數次地暫停,以便將更多資源投入到其他基礎功能上。Persistence(持久化)、Replication(複寫)、Latency(延遲)、Introspection(內省) 等功能獲得了比 Cluster 更多的關注,單純是因為它們對使用者族群來說更為重要。
專案的另一個限制在於,當我啟動它時,我對 distributed programming(分散式程式設計) 毫無概念。我的第一版設計非常糟糕,唯一掌握得不錯的是「產品」層面的需求:低延遲、線性可擴展性以及小型叢集的低開銷。然而,所有的細節都是錯的,設計也遠比必要的複雜,所使用的演算法並不安全,諸如此類。
在取得一些小幅進展的同時,我開始學習 distributed programming 的基礎,重新設計了 Redis Cluster,並將相同的理念應用到新版的 Sentinel 上。兩個系統所使用的 distributed programming 演算法仍然相當原始,因為它們是 asynchronous replicated(非同步複寫)、eventually consistent(最終一致性) 的系統,所以我不需要處理 consensus(共識) 等非顯而易見的難題。然而,即使你處理的是相對簡單的問題——至少相較於撰寫 CP store(一致性儲存) 而言——你仍需理解自己在做什麼,否則最終的系統可能會完全錯誤。
儘管有這些問題,我仍持續投入這個專案,試圖修正它、修正實作,並使其趨於成熟,因為有一個簡單的事實,就像小房間裡的大象一樣,瀰漫在整個 Redis 社群之中,那就是:人們一再地用自己的力量,而且往往以完全錯誤的方式,在做兩件事:
- 將資料集以 Sharding(分片) 方式分散到 N 個節點上。
- 建立靈敏的 Failover(容錯移轉) 機制以在特定故障中存活。
問題「2」嚴重到某個時刻,我決定在 Cluster 完成之前先啟動 Redis Sentinel 專案,以便盡快提供一套 HA(高可用性) 系統,而且對於大多數只需要「2」而不需要「1」的使用情境來說,它比 Redis Cluster 更為適合。
終於,我開始看到這些努力的第一批實際成果,而現在我們有了一個 release candidate,這是獲得採用、修復剩餘錯誤並以更漸進的方式改進系統所需的基本里程碑。
它實際上做了什麼?
Redis Cluster 基本上是一種 data sharding 策略,具備在叢集運行期間將鍵從一個節點 reshard(重新分片) 到另一個節點的能力,同時搭配一套 Failover 機制,確保系統能夠在特定類型的故障中存活。
從分散式資料庫的角度來看,Redis Cluster 在分區期間提供有限的可用性,以及一種弱一致性。基本上,它既不是 CP 也不是 AP 系統。換句話說,Redis Cluster 並未達到分散式系統理論上可能的極限,而是為了取得某些現實世界的特性。
其一致性模型就是著名的 eventual consistency 模型。基本上,如果節點因分區而失去同步,可以保證當分區癒合時,所有負責某個特定鍵的節點都會對其值達成一致。
然而,其合併策略是「last failover wins」,因此在網路分區期間接收到的寫入可能會遺失。一個常見的例子是,當一個 master 被隔離到少數派分區,而客戶端仍嘗試對其寫入時會發生的情況。若當分區癒合時,在分區的多數派那一側已有一個 slave 被提升來取代這個 master,則舊 master 所接收到的寫入就會遺失。
這反過來意味著 Redis Cluster 不必在資料結構中攜帶詮釋資料來嘗試進行值合併,而且 Redis 所支援的各種精巧指令與資料結構,Redis Cluster 也同樣支援。因此沒有額外的記憶體開銷、沒有 API 限制,也沒有限制單一值所能包含的元素數量,但在分區期間的安全性較低。
很容易理解,在像 Redis Cluster 這樣設計的系統中,節點間產生分歧並不是好事,因此系統試圖透過限制兩個節點分歧的機率(以及分歧的程度)來緩解其缺點。這透過以下幾種方式達成:
- 分區中少數派的一側會變為不可用。
- Replication 的設計使得通常對客戶端的回覆與對 slave 的複寫串流會同時發送。
- 當有多個 slave 可用於對某個 master 進行 Failover 時,系統會嘗試挑選看起來與失效 master 分歧較小的那一個。
這些策略並不會改變系統的理論特性,但為常見的 Redis Cluster 故障模式提供了更多現實世界的保護。
對於 Redis 的 API 與使用情境而言,我認為這樣的設計是合理的,但過去有許多人持不同意見。不過我認為,每位設計師都可以自由地按照自己的意願設計系統,只有一條規則:說實話,因此 Redis Cluster 在官方文件中清楚地記載了其限制與故障模式。
決定一個系統是否有用的,是使用者以及手邊的使用情境。我的感覺是,六年來使用者即使在完全沒有叢集支援的情況下仍持續使用 Redis,因為使用情境使這成為可能,而 Redis 提供了某些特定功能與效能,使其非常適合解決特定問題。我希望 Redis Cluster 能改善許多這類使用者的體驗。
未來的路
終於,我們有了一個足以發布的 minimum viable product(最小可行產品),它已經夠穩定,讓使用者可以認真開始測試,並在某些情況下直接採用。採用的人越多,我們改進得就越多。我從 Redis 與 Sentinel 的經驗中深知這一點:現在是一個將軟體從可用推向成熟的漸進過程。傾聽使用者、修復錯誤、以測試覆蓋更多程式碼……
同時,我也開始思考 Redis Cluster 的下一個版本,希望在 v1 的基礎上加入許多目前無法加入的實用功能,例如 multi data center(多資料中心) 支援、在少數派分區中透過 commands replay(指令重播) 提供更高的寫入安全性、automatic nodes balancing(自動節點平衡)(目前若某些節點過空而其他節點過滿,必須手動 reshard),以及更多功能。
此外,我認為 Redis Cluster 可以受益於一種專為快取設計的特殊執行模式,在該模式下,節點會接受對其不負責的 hash slot(雜湊槽) 的寫入,以便在少數派分區中保持可用。
我們永遠有時間去改進與修正我們的實作與設計,但若過於執著於軟體「應該」是什麼樣子,就有可能讓它被歸入 Vaporware(幻影產品) 的範疇,時間遠比必要的更長。是時候讓它問世了。盡情享受 Redis Cluster 吧!
Redis Cluster RC1 已同時以 GitHub 上的 '3.0.0-rc1' 標籤形式,以及位於 Redis.io 下載頁面 http://redis.io/download 的 tarball 形式提供。
隨機一篇部落格