標籤:
之前一篇寫了關於 Redis 的效能,這篇就寫寫我認為比效能更重要的擴充性方面的主題。
如果再給我一次回到好幾年前的機會,對於使用 Redis 我一開始就要好好考慮將來的擴充問題。就像我們做資料庫分庫分表,一旦決策了分庫分表,通常一次就會分到位,比如搞上 8 或 16 個庫,每個庫再分 256 或 1024 個表。不管將來業務再怎麼發展,基本這個量級的分區都足夠應對,而且底層庫可以做成邏輯的,扛不住時再換成物理的,對應用方完全透明,沒有資料移轉的煩惱。
而 Redis 其實也提供了類似的邏輯庫概念,每個 Redis 執行個體都有 0 到 15 號獨立的邏輯庫空間。當我們早期機器資源緊張而業務量又不大時,可以好好根據業務把不同的資料放在的單一執行個體的不同編號邏輯庫上。這是一種垂直切分方式,也可以用水平方式,把 0 到 15 號邏輯庫當成 16 個分區來用,只是這種用法可能對 Client 庫有些要求。
總之好幾年前我們都沒有這樣,當時物理機資源緊張,為了考慮不遠將來的業務擴張,所以在有限的資源下決定儘可能的分區。但也沒分太多,大約 10 片吧,多了營運成本也高。感覺按 Redis 的效能這一組分區最大承載幾十萬每秒的 OPS 估計能支撐很長時間的發展了。那 10 片怎麼部署呢?由於每個 Reids 執行個體只能利用一個核,當時的伺服器大概是 16 核,全放一台機也可以。當時我們正好有 10 台物理機,所以很自然的每台放了一個執行個體,但 Redis 只能用一個核,太浪費了。所以每台物理機上除了部署 Reids 還要部署應用服務,後來領悟到這又是一個錯誤的部署方式(背景音樂:多麼痛的領悟)。
一台 PC Server 的硬體可靠性大約是 99.9%,Redis 作為一個應用全域共用的關鍵服務分成 10 片放在十台 PC Server 上。實際上導致整體系統可靠性還降低了一個量級,變成了兩個 9。因為任何一台 PC Server 掛了都可能導致全域系統故障。然而當初沒有多餘的機器資源,為了提高可靠性,必須對 Redis 做主備,唯一的辦法就是交叉主備,所以部署結構大概類似下面這樣。
後來,隨著業務發展流量變得越來越大,Redis 記憶體佔用越來越多,而且開始出現一些詭異的故障現象。比如出現瞬時 Redis 大量串連和處理逾時,應用業務線程被阻塞,導致服務拒絕,過一段時間可能又自動回復了。這種瞬時故障非常難抓現場,一天來上幾發就會給人業務不穩定的感受,而一般基礎機器指標的監控周期在分鐘級。瞬時故障可能發生在監控的採集間隙,所以只好上指令碼在秒級監控日誌,發現瞬時出現大量 Redis 逾時錯誤,就收集當時應用的 JVM 堆棧、記憶體和機器 CPU Load 等各項指標。終於發現瞬時故障時刻 Redis 機器 CPU Load 出現瞬間飆升幾百的現象,應用和 Redis 混合部署時應用可能瞬間搶佔了全部 CPU 導致 Redis 沒有 CPU 資源可用。而應用處理業務的邏輯又可能需要訪問 Redis,而 Redis 又沒有 CPU 資源可用導致逾時,這不就像一個死結麼。搞清楚了原因其實解決方案也簡單,就是分離應用和 Redis 的部署,各自資源隔離,自此我們的 Redis 叢集發展開始走上一條合縱與連橫的道路。
合縱
分離應用和 Redis 的部署後,關於物理機資源的有一個尷尬點就是 Redis 單線程機制帶來的。當時一台 PC Server 16 核,記憶體 16 G,你想多利用核就要多部署執行個體,但每個執行個體分到的記憶體又不多。最終我們一台物理機只部署 2 個執行個體,因為業務發展對記憶體的需求強過對 CPU 的利用,所以調整後的部署模型變成下面這樣。
這樣每個 Redis 執行個體能分到的記憶體是小於 8G 的(還要給系統留一點不是)。隨著業務發展,一開始是 2G,很快 4G 然後 6G 就到了單機記憶體瓶頸,下一步只能分一個執行個體出去,每個執行個體獨享單機記憶體。縱向擴容在操作性上是最簡單的,在另外一台機器上先掛一個從分區,同步複製完成後,通知 Client 端切換串連而分區 Hash 規則還是不變。這個過程會有短暫的( 2-5 步這個過程程式執行的時間視窗)寫丟失,在業務上是可接受的。
獨享了更大記憶體,我們就可以繼續縱向擴記憶體,但擴到了 12G 後就基本到頂了,即便還有更大記憶體的物理機也不宜再擴大單分區的記憶體了。主要原因是因為 Redis 的主從複製導致的服務中斷,當初 Redis 版本是 2.4,直到 2.8 才有增量的主從複製。即使 2.8 主從複製依然可能在斷鏈長時間後導致全量複製,雖然官方文檔號稱主從複製不中斷服務,但實際每次全量複製 dump 記憶體時是阻斷了主線程執行。這個阻斷時間在 12G 記憶體時大概有一分多鐘, 繼續縱向擴大記憶體會導致更長時間的阻斷,在業務上不可接受,合縱之路也走到頭了。
連橫
為了對業務做到無縫透明的擴容,只能走橫向發展的道路。而 Redis 官方的 Cluster 方案一直跳票,遲遲出不來,大家的業務都在快速發展,等不及啊。所以在橫向擴充上演變出了兩種方案,一種是代理模式,利用引入中間 Proxy 來嚮應用層屏蔽後端的叢集分布。業界最早是 Twitter 開源的 Twemproxy 採用了這種模式,後來豌豆莢開源的 Codis 進一步在營運可操作性上完善了這種模式。主要是在擴容方面儘可能的做到業務無感知,思路就是前端引入 Proxy 隔離應用程式層,後端改造 Redis 引入 Slot(有些也叫 Buket)來分組 key。應用程式層訪問時基於演算法將 key 先映射到 Slot 再映射到具體分區執行個體,大概如下面這樣。
F(key) -> Slot -> Instance
Redis 中的 key 按 Slot 來組織,平滑擴容時比如加分區後,按 Slot 為單位遷移,這通常需要改造 Redis 源碼來支援。這個模式的架構如下所示。
引入 Proxy 是犧牲了少量效能來換取了對應用的透明和更好的擴充性。另一種方案是基於 Smart Client 免代理,但對應用有一定的侵入性,本質上就是把 Proxy 的功能放到了 Client。
至於採用哪種方案就是仁者見仁、智者見智了,需要根據實際情況去考慮。不過個人認為基於代理的方案更靈活些,而且可在 Proxy 層能做的事情比 Client 要多,但對 Proxy 的實現要求也更高。不管上面哪種方案都採用了中心化的控制方式,中心化對簡化營運操作是有好處的,而且能方便做到叢集全域的管理。
Redis Cluster 終於遲遲推出後,採用了與中心化不同的思路,而且設計目標更追求效能,所以是依賴 Smart Client 的方式。而 Redis Server 的實現還是使用了 Slot 方式預設最大 16 * 1024 = 16384 個 Slot,所以理論叢集最大就是這麼多個執行個體,實際不大可能需要這麼大。採用 Gossip 訊息來同步叢集配置,基於投票機制來進行主從 Failover 發現和自動切換。從目前已經推出的版本和功能來看,作者是在往一個純 Smart Cluster 方向發展,但顯然目前的版本還不成熟。比如自動探索、叢集智能重新平衡等功能都沒有,還依賴人工操作。而且非中心化的叢集相比中心化叢集的可預測性和操作性都要差不少,其在真實的應用案例,除了在網易有道有人分享了一個非關鍵類情境,還沒見過比較有份量的成熟案例。所以 Redis Cluster 的道路恐怕還在路漫漫其修遠兮,吾將上下而求索的階段。
總結
前面回顧了 Redis 叢集發展的曆程,從合縱到連橫實際是一個從業務垂直切分到平台服務化的過程。至於到底應該採用什麼樣的叢集模式,可能需要好好結合自身的業務發展階段、團隊能力和企業環境去分析取捨了。沒有最好的,只有合適的。
參考
[1] antirez. Redis Cluster Specification
[2] 黃東旭. 分布式 Redis 架構設計和踩過的那些坑們
[3] 西代零零發. 全面剖析 Redis Cluster 原理和應用
[4] 西代零零發. Redis Cluster 架構最佳化
[5] 楊肉. Redis Cluster 使用經驗
你還可以看
Redis 的效能幻想與殘酷現實
Redis 叢集的合縱與連橫