优酷蓝鲸近千节点的Redis集群运维经验总结
http://www.infoq.com/cn/news/2016/08/youku-Redis-nosql
Redis是時下比較流行的Nosql技術。在優酷我們使用Redis Cluster構建了一套內存存儲系統,項目代號藍鯨。到目前為止集群有700+節點,即將達到作者推薦的最大集群規模1000節點。集群從Redis Cluster發布不久就開始運行,到現在已經將近兩年時間。在運維集群過程中遇到了很多問題,記錄下來希望對他人有所幫助。
主從重同步問題
問題描述
服務器宕機并恢復后,需要重啟Redis實例,因為集群采用主從結構并且宕機時間比較長,此時宕機上的節點對應的節點都是主節點,宕掉的節點重啟后都應該是從節點。啟動Redis實例,我們通過日志發現節點一直從不斷的進行主從同步。我們稱這種現象為主從重同步。
主從同步機制
為了分析以上問題,我們首先應該搞清楚Redis的主從同步機制。以下是從節點正常的主從同步流程日志:
17:22:49.763 * MASTER <-> SLAVE sync started17: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主從同步的流程,主要步驟為:
到此一次全量主從同步完成。等等日志中“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命令能查看內存及碎片情況:
# Memoryused_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:4854837632used_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進程并重啟。
寫在最后
運維是一個理論落地的過程,對于運維集群而言任何微小的異常背后都是有原因的,了解系統內部運行機制,并且著手去探究,才能更好的解釋問題,消除集群的隱患。
轉載于:https://www.cnblogs.com/davidwang456/articles/9254085.html
總結
以上是生活随笔為你收集整理的优酷蓝鲸近千节点的Redis集群运维经验总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Hbase Replication 介绍
- 下一篇: Redis 如何分布式,来看京东金融的设