近千節點的Redis集羣運維經驗總結

分享一篇好文章 redis愈來愈實用了前端

服務器宕機並恢復後,須要重啓Redis實例,由於集羣採用主從結構而且宕機時間比較長,此時宕機上的節點對應的節點都是主節點,宕掉的節點重啓後都應該是從節點。啓動Redis實例,咱們經過日誌發現節點一直從不斷的進行主從同步。咱們稱這種現象爲主從重同步。node

主從同步機制redis

爲了分析以上問題,咱們首先應該搞清楚Redis的主從同步機制。如下是從節點正常的主從同步流程日誌:數據庫

17:22:49.763 * MASTER <-> SLAVE sync started
17:22:49.764 * Non blocking connect for SYNC fired the event.
17:22:49.764 * Master replied to PING, replication can continue...
17:22:49.764 * Partial resynchronization not possible (no cached master)
17:22:49.765 * Full resync from master: c9fabd3812295cc1436af69c73256011673406b9:1745224753247
17:23:42.223 * MASTER <-> SLAVE sync: receiving 1811656499 bytes from master
17:24:04.484 * MASTER <-> SLAVE sync: Flushing old data
17:24:25.646 * MASTER <-> SLAVE sync: Loading DB in memory
17:27:21.541 * MASTER <-> SLAVE sync: Finished with success
17:28:22.818 # MASTER timeout: no data nor PING received...
17:28:22.818 # Connection with master lost.
17:28:22.818 * Caching the disconnected master state.
17:28:22.818 * Connecting to MASTER xxx.xxx.xxx.xxx:xxxx
17:28:22.818 * MASTER <-> SLAVE sync started
17:28:22.819 * Non blocking connect for SYNC fired the event.
17:28:22.824 * Master replied to PING, replication can continue...
17:28:22.824 * Trying a partial resynchronization (request c9fabd3812295cc1436af69c73256011673406b9:1745240101942).
17:28:22.825 * Successful partial resynchronization with master.
以上日誌是以從節點的視角呈現的,由於以從節點的角度更能反映主從同步流程,因此如下的分析也以從節點的視角爲主。日誌很清楚的說明了Redis主從同步的流程,主要步驟爲:緩存

從節點接收RDB文件
從節點清空舊數據
從節點加載RDB文件
到此一次全量主從同步完成。等等日誌中「Connection with master lost」是什麼鬼,爲何接下來又進行了一次主從同步。服務器

「Connection with master lost」的字面意思是從節點與主節點的鏈接超時。在Redis中主從節點須要互相感知彼此的狀態,這種感知是經過從節點定時PING主節點而且主節點返回PONG消息來實現的。那麼當主節點或者從節點由於其餘緣由不能及時收到PING或者PONG消息時,則認爲主從鏈接已經斷開。運維

問題又來了何爲及時,Redis經過參數repl-timeout來設定,它的默認值是60s。Redis配置文件(redis.conf)中詳細解釋了repl-timeout的含義:異步

# The following option sets the replication timeout for:
#
# 1) Bulk transfer I/O during SYNC, from the point of view of slave.
# 2) Master timeout from the point of view of slaves (data, pings).
# 3) Slave timeout from the point of view of masters (REPLCONF ACK pings).
#
# It is important to make sure that this value is greater than the value
# specified for repl-ping-slave-period otherwise a timeout will be detected
# every time there is low traffic between the master and the slave.
#
# repl-timeout 60
咱們回過頭再來看上邊的同步日誌,從節點加載RDB文件花費將近三分鐘的時間,超過了repl-timeout,因此從節點認爲與主節點的鏈接斷開,因此它嘗試從新鏈接並進行主從同步。函數

部分同步工具

這裏補充一點當進行主從同步的時候Redis都會先嚐試進行部分同步,部分同步失敗纔會嘗試進行全量同步。

Redis中主節點接收到的每一個寫請求,都會寫入到一個被稱爲repl_backlog的緩存空間中,這樣當進行主從同步的時候,首先檢查repl_backlog中的緩存是否能知足同步需求,這個過程就是部分同步。

考慮到全量同步是一個很重量級別而且耗時很長的操做,部分同步機制能在不少狀況下極大的減少同步的時間與開銷。

重同步問題

經過上面的介紹大概瞭解了主從同步原理,咱們在將注意力放在加載RDB文件所花費的三分鐘時間上。在這段時間內,主節點不斷接收前端的請求,這些請求不斷的被加入到repl_backlog中,可是由於Redis的單線程特性,從節點是不能接收主節點的同步寫請求的。因此不斷有數據寫入到repl_backlog的同時卻沒有消費。

當repl_backlog滿的時候就不能知足部分同步的要求了,因此部分同步失敗,須要又一次進行全量同步,如此造成無限循環,致使了主從重同步現象的出現。不只侵佔了帶寬,並且影響主節點的服務。

解決方案

至此解決方案就很明顯了,調大repl_backlog。

Redis中默認的repl_backlog大小爲1M,這是一個比較小的值,咱們的集羣中曾經設置爲100M,有時候仍是會出現主從重同步現象,後來改成200M,一切太平。能夠經過如下命令修改repl_backlog的大小:

//200Mredis-cli -h xxx -p xxx config set repl-backlog-size 209715200
內存碎片

首先對於絕大部分系統內存碎片是必定存在的。試想內存是一整塊連續的區域,而數據的長度能夠是任意的,而且會隨時發生變化,隨着時間的推移,在各個數據塊中間必定會夾雜着小塊的難以利用的內存,因此在Redis中內存碎片是存在的。

在Redis中經過info memory命令能查看內存及碎片狀況:

# Memory
used_memory:4221671264 /* 內存分配器爲數據分配出去的內存大小,能夠認爲是數據的大小 */
used_memory_human:3.93G /* used_memoryd的閱讀友好形式 */
used_memory_rss:4508459008 /* 操做系統角度上Redis佔用的物理內存空間大小,注意不包含swap */
used_memory_peak:4251487304 /* used_memory的峯值大小 */
used_memory_peak_human:3.96G /* used_memory_peak的閱讀友好形式 */
used_memory_lua:34816mem_fragmentation_ratio:1.07 /* 碎片率 */
mem_allocator:jemalloc-3.6.0 /* 使用的內存分配器 */
對於每一項的意義請注意查看註釋部分,也能夠參考官網上info命令memory部分。Redis中內存碎片計算公式爲:

mem_fragmentation_ratio = used_memory_rss / used_memory
能夠看出上邊的Redis實例的內存碎片率爲1.07,是一個較小的值,這也是正常的狀況,有正常狀況就有不正常的狀況。發生數據遷移以後的Redis碎片率會很高,如下是遷移數據後的Redis的碎片狀況:

used_memory:4854837632
used_memory_human:4.52G
used_memory_rss:7362924544
used_memory_peak:7061034784
used_memory_peak_human:6.58G
used_memory_lua:39936
mem_fragmentation_ratio:1.52
mem_allocator:jemalloc-3.6.0
能夠看到碎片率是1.52,也就是說有三分之一的內存被浪費掉了。針對以上兩種狀況,對於碎片簡單的分爲兩種:

常規碎片

遷移碎片

常規碎片數量較小,並且必定會存在,能夠不用理會。那麼如何去掉遷移碎片呢?其實方案很簡單,只須要先BGSAVE再從新啓動節點,從新加載RDB文件會去除絕大部分碎片。

可是這種方案有較長的服務不可用窗口期,因此須要另外一種較好的方案。這種方案須要Redis採用主從結構爲前提,主要思路是先經過重啓的方式處理掉從節點的碎片,以後進行主從切換,最後處理老的主節點的碎。這樣經過極小的服務不可用時間窗口爲代價消除了絕大大部分碎片。

Redis Cluster剔除節點失敗

Redis Cluster採用無中心的集羣模式,集羣中全部節點經過互相交換消息來維持一致性。當有新節點須要加入集羣時,只須要將它與集羣中的一個節點創建聯繫便可,經過集羣間節點互相交換消息全部節點都會互相認識。因此當須要剔除節點的時候,須要向全部節點發送cluster forget命令。

而向集羣全部節點發送命令須要一段時間,在這段時間內已經接收到cluster forget命令的節點與沒有接收的節點會發生信息交換,從而致使cluster forget命令失效。

爲了應對這個問題Redis設計了一個黑名單機制。當節點接收到cluster forget命令後,不只會將被踢節點從自身的節點列表中移除,還會將被剔除的節點添加入到自身的黑名單中。當與其它節點進行消息交換的時候,節點會忽略掉黑名單內的節點。因此經過向全部節點發送cluster forget命令就能順利地剔除節點。

可是黑名單內的節點不該該永遠存在於黑名單中,那樣會致使被踢掉的節點不能再次加入到集羣中,同時也可能致使不可預期的內存膨脹問題。因此黑名單是須要有時效性的,Redis設置的時間爲一分鐘。

因此當剔除節點的時候,在一分鐘內沒能向全部節點發出cluster forget命令,會致使剔除失敗,尤爲在集羣規模較大的時候會常常發生。

解決方案是多個進程發送cluster forget命令,是否是很簡單。

遷移數據時的JedisAskDataException異常

問題描述

Redis Cluster集羣擴容,須要將一部分數據從老節點遷移到新節點。在遷移數據過程當中會出現較多的JedisAskDataException異常。

遷移流程

因爲官方提供遷移工具redis-trib在大規模數據遷移上的一些限制,咱們本身開發了遷移工具,Redis Cluster中數據遷移是以Slot爲單位的,遷移一個Slot主要流程以下:

目標節點 cluster setslot <slot> importing <source_id>
源節點 cluster setslot <slot> migrating <target_id>
源節點 cluster getkeysinslot <slot> <count> ==> keys
源節點 migrate <target_ip> <target_port> <key> 0 <timeout>
重複3&4直到遷移完成
任一節點 cluster setslot <slot> node <target_id>
咱們使用Redis中的MIGRATE命令來把數據從一個節點遷移到另一個節點。MIGRATE命令實現機制是先在源節點上DUMP數據,再在目標節點上RESTORE它。

可是DUMP命令並不會包含過時信息,又由於集羣中全部的數據都有過時時間,因此咱們須要額外的設置過時時間。因此遷移一個SLOT有點相似以下:

while (from.clusterCountKeysInSlot(slot) != 0) {
keys = from.clusterGetKeysInSlot(slot, 100); for (String key : keys) { //獲取key的ttl Long ttl = from.pttl(key); if (ttl > 0) {
from.migrate(host, port, key, 0, 2000); to.asking(); to.pexpire(key, ttl); } }
}
可是上邊的遷移工具在運行過程當中報了較多的JedisAskDataException異常,經過堆棧發現是「Long ttl = from.pttl(key)」這一行致使的。爲了解釋上述異常,咱們須要先了解Redis的一些內部機制。

Redis數據過時機制

Redis數據過時混合使用兩種策略

主動過時策略:定時掃描過時表,並刪除過時數據,注意這裏並不會掃描整個過時表,爲了減少任務引發的主線程停頓,每次只掃描一部分數據,這樣的機制致使數據集中可能存在較多已通過期可是並無刪除的數據。

被動過時策略:當客戶端訪問數據的時候,首先檢查它是否已通過期,若是過時則刪掉它,並返回數據不存在標識。

這樣的過時機制兼顧了每次任務的停頓時間與已通過期數據不被訪問的功能性,充分體現了做者優秀的設計能力,詳細參考官網數據過時機制。

Open狀態Slot訪問機制

在遷移Slot的過程當中,須要先在目標節點將Slot設置爲importing狀態,而後在源節點中將Slot設置爲migrating 狀態,咱們稱這種Slot爲Open狀態的Slot。

由於處於Open狀態的Slot中的數據分散在源與目標兩個節點上,因此若是須要訪問Slot中的數據或者添加數據到Slot中,須要特殊的訪問規則。Redis推薦規則是首先訪問源節點再去訪問目標節點。若是源節點不存在,Redis會返回ASK標記給客戶端,詳細參考官網。

問題分析

讓咱們回到問題自己,通過閱讀Redis代碼發現clusterCountKeysInSlot函數不會觸發被動過時策略,因此它返回的數據包含已通過期可是沒有被刪除的數據。當程序執行到「Long ttl = from.pttl(key);」這一行時,首先Redis會觸發觸發被動過時策略刪掉已通過期的數據,此時該數據已經不存在,又由於該節點處於migrating狀態,因此ASK標記會被返回。而ASK標記被Jedis轉化爲JedisAskDataException異常。

這種異常只須要捕獲並跳過便可。

Redis Cluster flush失敗

flush是一個極少用到的操做,不過既然碰到過詭異的現象,也記錄在此。

問題場景是在Reids Cluster中使用主從模式,向主節點發送flush命令,預期主從節點都會清空數據庫。可是詭異的現象出現了,咱們獲得的結果是主從節點發生了切換,而且數據並無被清空。

分析以上case,Redis採用單線程模型,flush操做執行的時候會阻塞全部其它操做,包括集羣間心跳包。當Redis中有大量數據的時候,flush操做會消耗較長時間。因此該節點較長時間不能跟集羣通訊,當達到必定閾值的時候,集羣會斷定該節點爲fail,而且會切換主從狀態。

Redis採用異步的方式進行主從同步,flush操做在主節點執行完成以後,纔會將命令同步到從節點。此時老的從節點變爲了主節點,它不會再接受來自老的主節點的刪除數據的操做。

當老的主節點flush完成的時候,它恢復與集羣中其它節點的通信,得知本身被變成了從節點,所又會把數據同步過來。最終形成了主從節點發生了切換,而且數據沒有被清空的現象。

解決方式是臨時調大集羣中全部節點的cluster-node-timeout參數。

Redis啓動異常問題

這也是個極少碰到的問題,同上也記錄在此。

咱們集羣中每一個物理主機上啓動多個Redis以利用多核主機的計算資源。問題發生在一次主機宕機。恢復服務的過程當中,當啓動某一個Redis實例的時候,Redis實例正常啓動,可是集羣將它標記爲了fail狀態。

衆所周知Redis Cluster中的實例,須要監聽兩個端口,一個服務端口(默認6379),另外一個是集羣間通信端口(16379),它是服務端口加上10000。

通過一番調查發現該節點的服務通信端口,已經被集羣中其它節點佔用了,致使它不能與集羣中其它節點通信,被標記爲fail狀態。

解決方式是找到佔用該端口的Redis進程並重啓。

寫在最後

運維是一個理論落地的過程,對於運維集羣而言任何微小的異常背後都是有緣由的,瞭解系統內部運行機制,而且着手去探究,才能更好的解釋問題,消除集羣的隱患

相關文章
相關標籤/搜索