http://www.open-open.com/lib/view/open1469933226345.html
吳建超,優酷土豆廣告基礎平臺開發工程師,對互聯網基礎產品及大數據產品有興趣。
在優酷,我們使用 Redis Cluster 構建了一套內存存儲系統,項目代號為藍鯨。藍鯨的設計目標是高效讀寫,所有數據都在內存中。藍鯨的主要應用場景是 cookie 和大數據團隊計算的數據,都具有較強的時效性,因此所有的數據都有過期時間。更準確的說藍鯨是一個全內存的臨時存儲系統。
到目前為止集群規模逐漸增長到 700+ 節點,即將達到 作者建議的最大集群規模 1,000 節點 。我們發現隨著集群規模的擴大,帶寬壓力不斷突出,并且響應時間 RT 方面也會略微升高。與一致性哈希構建的 Redis 集群不一樣,Redis Cluster 不能做成超大規模的集群,它比較適合作為中等規模集群的解決方案。
運維期間, 吞吐量與 RT 一直作為衡量集群穩定性的重要指標 ,這里在本文中,我們碰到的影響集群吞吐量與 RT 的一些問題與探索記錄下來,希望對大家有所幫助。
Redis Cluster 工作原理
Redis 采用單進程模型,除去 bgsave 與 aof rewrite 會另外新建進程外,所有的請求與操作都在主進程內完成。其中比較重量級的請求與操作類型有:
客戶端請求
集群通訊
從節同步
AOF 文件
其它定時任務
Redis 服務端采用 Reactor 設計模式,它是一種基于事件的編程模型,主要思想是將請求的處理流程劃分成有序的事件序列,比如對于網絡請求通常劃分為:Accept new connections、Read input to buffer、Process request、 Response 等幾個事件。并在一個無限循環的 EventLoop 中不斷的處理這些事件。更多關于Reactor,請參考 https://en.wikipedia.org/wiki/Reactor 。
比較特別的是,Redis 中還存在一種時間事件,它其實是定時任務,與請求事件一樣,它同樣在 EventLoop 中處理。Redis 主線程的主要處理流程如下圖:
Redis main process playload overview
理解了 Redis 的單進程模型與主要負載情況,很容易明白,想要增加 Redis 吞吐量,只需要盡量降低其它任務的負載量就行了,所以提高 Redis 集群吞吐量的方式主要有:
提高 Redis 集群吞吐的方法
1. 適當調大 cluster-node-timeout 參數
我們發現當集群規模達到一定程度時,集群間消息通訊開銷的帶寬是極其可觀的。
集群通信機制
Redis 集群采用無中心的方式,為了維護集群狀態統一,節點之間需要互相交換消息。Redis采用交換消息的方式被稱為 Gossip ,基本思想是節點之間互相交換信息最終所有節點達到一致,更多關于 Gossip 可參考 https://en.wikipedia.org/wiki/Gossip_protocol 。

Gossip in Redis Cluster
總結集群通信機制的一些要點:
如何理解集群中節點狀態的十分之一?假如集群中有 700 個節點,十分之一就是 70 個節點狀態,節點狀態具體數據結構見下邊代碼:

我們將注意力放在數據包大小與流量上,每個節點狀態大小為 104 byte,所以對于 700 個節點的集群,這部分消息的大小為 70 * 104 = 7280,大約為 7KB。另外每個 Gossip 消息還需要攜帶一個長度為 16,384 的 Bitmap,大小為 2KB,所以每個 Gossip 消息大小大約為 9KB。
隨著集群規模的不斷擴大,每臺主機的流量不斷增長,我們懷疑集群間通信的流量已經大于前端請求產生的流量,所以做了以下實驗以明確集群流量狀況。
實驗過程
實驗環境為:節點 704,物理主機 40 臺,每臺物理主機有 16 個節點,集群采用一主一從模式,集群中節點 cluster-node-timeout 設置為 30 秒。
實驗的大概思路為,分別截取一分鐘時間內一個節點,在集群通信端口上,進入方向與出去方向的流量,并統計出消息條數,并最終計算出臺主機因為集群間通訊產生的帶寬開銷。實驗具體過程如下:

通過實驗能看到進入方向與出去方向在 60s 內收到的數據包數量為 2,700 多個。因為 Redis 規定每個節點每一秒只向一個節點發送數據包,所以正常情況每個節點平均 60s 會收到 60 個數據包,為什么會有這么大的差距?
原來考慮到 Redis 發送對象節點的選取是隨機的,所以存在兩個節點很久都沒有交換消息的情況,為了保證集群狀態能在較短時間內達到一致性,Redis 規定當兩個節點超過 cluster-node-timeout 的一半時間沒有交換消息時,下次心跳交換消息。
解決了這個疑惑,接下來看帶寬情況。先看 Redis Cluster 集群通信端口進入方向每臺主機的每秒帶寬為:
再看 Redis Cluster 集群通信端口出去方向每臺主機的每秒帶寬為:
所以每臺主機進入方向的帶寬為:
為什么需要加和
我們以節點 A 主動與節點 B 發生消息交換為例進行說明,交換過程如下圖:

Redis Cluster msg exchange
首先 A 隨機一個端口向節點 B 的集群通訊端 17,380 發送 PING 消息,之后節點 B 通過 17,380 端口向節點 A 發送 PONG 消息,PONG 消息的內容與 PING 消息的內容相似,每個消息的大小也一樣(9KB)。同理當節點 B 主動與節點 A 發生消息交換時也是同樣的過程。
可以看出對于節點 A 進入方向的帶寬不僅包含集群通訊端口的還包含隨機端口的帶寬。而對于節點 A 進入方向隨機端口的帶寬,正是其它節點出去方向的帶寬。所以每臺主機進入方向的帶寬為上邊公式計算的加和。同理出去方帶寬與進入方帶寬一樣為 107.5MBit / s。
cluster-node-timeout 對帶寬的影響
集群中每臺主機的帶寬狀況如下圖:

集群帶寬圖 (http://image.cnthrowable.com/upload/throwable_blog/itbroblog/blog/1468919903506_309.png)
每臺主機的進出口帶寬都大概在 150MBit / s 左右,其中集群通信帶寬占 107.5MBit / s,所以前端請求的帶寬占用大概為 45MBit / s。再來看當把 cluster-node-timeout 從 20s 調整到 30s 時,主機的帶寬變化情況:

帶寬變化 (http://image.cnthrowable.com/upload/throwable_blog/itbroblog/blog/1468920308772_304.jpg)
從圖中,可以看到帶寬下降 50MBit / s,效果非常明顯。
經過以上實驗我們能得出兩個結論:
集群間通信占用大量帶寬資源
調整 cluster-node-timeout 參數能有效降低帶寬
Redis Cluster 判定節點為 fail 的機制
但是并不是 cluster-node-timeout 越大越好。當 cluster-node-timeou 增大的時候集群判斷節點 fail 的時間會增加,從而 failover 的時間窗口會增加。集群判定節點為fail所需時間的計算公式如下:
當節點向失敗節點發出 PING 消息,并且在 cluster-node-timeout 時間內還沒有收到失敗節點的 PONG 消息,此時判定它為 pfail 。pfail 即部分失敗,它是一種中間狀態,該狀態隨著集群心跳不斷傳播。再經過一半 cluster-node-timeout 時間后,所有節點都與失敗的節點發生過心跳并且把它標記為 pfail 。當然也可能不需要這么長時間,因為其它節點之間的心跳同樣會傳遞 pfail 狀態,這里姑且以最大時間計算。
Redis Cluster 規定當集群中超過一半以上節點認為一個節點為 pfail 狀態時,會把它標記為 fail 狀態,并廣播給其他所有節點。對于每個節點而言平均一秒鐘收到一個心跳包,每次心跳都會攜帶隨機的十分之一的節點個數。所以現在問題抽像為經過多長時間一個節點會積累到一半的 pfail 狀態數。這是一個概率問題,因為個人并不擅長概率計算,這里直接取了一個較大概率能滿足條件的數值 10。
所以上述公式不是達到這么長時間一定會判定節點為 fail,而是經過這么長時間集群有很大概率會判定節點 fail 。
Redis Cluster 默認 cluster-node-timeout 為 15s,我們將它設置成了 30s。也就是說 700 節點的集群,集群間帶寬開銷為 104.5MBit / s,判定節點失敗時間窗口大概為 55s,實際上大多數情況都小于 55s,因為上邊的計算都是按照高位時間估算的。
總而言之, 對于大的 Redis 集群 cluster-node-timeout 參數的需要謹慎設定 。
提高 Redis 集群吞吐的方法
2. 控制主節點寫命令傳播
Redis 中主節點的每個寫命令傳播到以下三個地方:

其中 repl_backlog 部分傳播在 replicationFeedSlaves 函數中完成。
減少從節點的數量
高可用的集群不應該出現單點,所以 Redis 集群一般都會是主從模式。Redis 的主從同步機制是所有的主節點的寫請求,會同步到所有的從節點。如果沒有從節點,對于主節點來說,它只需要處理該請求即可。但對于有 N 個從節點的主節點來說,它需要額外的將請求傳播給 N 個從節點。請注意這里是對于每個寫請求都會這樣處理。顯而易見從節點的數量對主節點的吞吐量的影響是比較大的,我們采用的是一主一從模式。
因為從節點不需要同步數據,生產環境中觀察主節點的 CPU 占用率要比從節點機器要高,這對這條結論起到了佐證的作用。
關閉 AOF 功能
如果開啟 AOF 功能,每個寫請求都會 Append 到本地 AOF 文件中,雖然 Linux 中寫文件操作會利用到操作系統緩存機制,但是如果關閉 AOF 功能主線程中省去了寫 AOF 文件的操作,顯然會對吞吐量的增加有幫助。
AOF 是 Redis 的一種持久化方式,如果關閉了 AOF 功能怎么保證數據的安全性。我們的做法是定時在從節點 BGSAVE。當然具體采用何種策略需要結合具體情況來決定。
去掉頻繁的 Cluster nodes 命令
在運維過程中發現前端請求的平均 RT 增加不少,大概 50% 左右。通過一番調研,發現是 頻繁的 cluster nodes 命令導致 。
當時集群規模為 500+ 節點,cluster nodes 命令返回的結果大小有 103KB。cluster nodes 命令的頻率為:每隔 20s 向集群所有節點發送。
提高 Redis 集群吞吐的方法
3. 調優 hz 參數
Redis 會定時做一些任務,任務頻率由 hz 參數規定,定時任務主要包含:
主動清除過期數據
對數據庫進行漸式Rehash
處理客戶端超時
更新請求統計信息
發送集群心跳包
發送主從心跳
以下是作者對于 hz 參數的介紹:

我們沒有修改 hz 參數的經驗,由于其復雜性,并且在 hz 默認值 10 的情況下,理論上不會對 Redis 吞吐量產生太大影響,建議沒有經驗的情況下不要修改該參數。
參考資料
關于 Redis Cluster 可以參考官方的兩篇文檔: