Redis集群slot遷移改造實踐
一、背景介紹
Redis 集群服務在互聯網公司被廣泛使用,眾所周知服務集群化可以突破單節點的能力瓶頸,帶來規模、可用性、擴展性等多方面的收益。在實際使用 Redis 集群的過程中,發現在進行涉及集群數據遷移的水平擴縮容操作時,業務側多次反饋 Redis 請求的時延升高問題,甚至發生過擴容操作導致集群節點下線的可用性故障,并進一步引發遷移流程中斷、節點間數據腦裂等一系列嚴重影響,給運維同事帶來極大困擾,嚴重影響線上服務的穩定。
二、問題分析
2.1 原生遷移介紹
Redis 集群功能采用無中心架構設計,集群中各個節點都維護各自視角的集群拓撲并保存自有的分片數據,集群節點間通過 gossip 協議進行信息協調和變更通知。具體來說 Redis 集群數據管理上采用虛擬哈希槽分區機制,將數據的鍵通過哈希函數映射到 0~16383 整數槽內,此處的槽位在 Redis 集群設計中被稱為 slot。這樣實際上每一個節點只需要負責維護一部分 slot 所映射的鍵值數據,slot 就成為 Redis 集群管理數據的基本單位,集群擴縮容本質上就是 slot 信息和 slot 對應數據數據在節點之間的轉移。Redis 集群水平擴展的能力就是基于 slot 維度進行實現,具體流程如下圖所示。
上圖所示的遷移步驟中,步驟1-2是對待遷移 slot 進行狀態標記,方便滿足遷移過程中數據訪問,步驟3-4是遷移的核心步驟,這兩個步驟操作會在步驟5調度下持續不斷進行,直到待遷移 slot 的鍵值數據完全遷移到了目標節點,步驟6會在數據轉移完成后進行,主要是發起集群廣播消息更新集群內節點 slot 拓撲。
由于正常的遷移時一個持續的處理過程,不可避免地會出現正在遷移 slot 數據分布于遷移兩端地“分裂”狀態,這種狀態會隨著 slot 遷移的流程進行而持續存在。為了保證遷移期間正在遷移的 slot 數據能夠正常讀寫,Redis 集群實現了下圖所示的一種 ask-move 機制,如果請求訪問正在遷移的 slot 數據,請求首先會按照集群拓撲正常訪問到遷移的源節點,如果在源節點查詢到數據則正常處理響應請求;如果在源節點沒有找到請求所需數據,則會給客戶端回復 ASK {ip}:{port} 消息回包。
Redis 集群智能客戶端收到該回包后會按照包內節點信息找到新節點重試命令,但是由于此時目標節點還沒有遷移中 slot 的所屬權,所以在重試具體命令之前智能客戶端會首先向目的節點發送一個 asking 命令,以此保證接下來訪問遷移中 slot 數據的請求能被接受處理。由于原生遷移時按照 key 粒度進行的,一個 key 的數據要不存在源節點,要不存在目的節點,所以Redis 集群可以通過實現上述 ask-move 機制,保證遷移期間數據訪問的一致性和完整性。
2.2 遷移問題分析
(1)時延分析
根據上述原生 Redis 集群遷移操作步驟的了解,可以總結出原生遷移功能按照 key 粒度進行的,即不斷掃描源節點上正在遷移的 slot 數據并發送數據給目的節點,這是集群數據遷移的核心邏輯。微觀來說遷移單個 key 數據對于服務端來說包含以下操作:
- 序列化待遷移鍵值對數據;
- 通過網絡連接發送序列化的數據包;
- 等待回復(目標端接收完包并加載成功才會返回);
- 刪除本地殘留的副本,釋放內存。
上述操作中涉及多個耗費線程處理時長的操作,首先序列化數據是非常耗費 CPU 時間的操作,如果遇到待遷移 key 比較大線程占用時長也會隨之惡化,這對于單工作線程的 Redis 服務來說是不可接受的,進一步地網絡發送數據到目標節點時會同步等待結果返回,而遷移目的端又會在進行數據反序列化和入庫操作后才會向源節點進行結果返回。需要注意的是在遷移期間會不斷循環進行以上步驟的操作,而且這些步驟是在工作線程上連續處理的,期間無法對正常請求進行處理,所以此處就會導致服務響應時延持續突刺,這一點可以通過 slowlog 的監控數據得到驗證,遷移期間會在 slowlog 抓取到大量的 migrate 和 restore 命令。
(2)ask-move 開銷
正常情況下每個正在遷移的 slot 數據都會一段時間內存在數據分布在遷移的兩端的情況,遷移期間該 slot 數據訪問請求可以通過 ask-move 機制來保證數據一致性,但是不難看出這樣的機制會導致單個請求網絡訪問次數出現成倍的增加,對客戶端也存在一定的開銷壓力。另外,對于可能存在的用戶采用 Lua 或者 Pipline 這種需要對單個 slot 內多 key 連續訪問的場景,目前大部分集群智能客戶端支持有限,可能會遇到遷移期間相關請求不能正常執行的報錯。另外需要說明的是,由于 ask-move 機制的只在遷移兩端的主節點上能觸發,所以遷移期間從節點是不能保證數據請求結果一致性的,這對于采用讀寫分離方式訪問集群數據的用戶也非常不友好。
(3)拓撲變更開銷
為了降低遷移期間數據 ask-move 的機制對請求的影響,正常情況下原生遷移每次只會操作一個 slot 遷移,這就導致對每一個遷移完成的 slot 都會觸發集群內節點進行一次拓撲更新,而每次集群拓撲的更新都會觸發正在執行指令的業務客戶端幾乎同時發送請求尋求更新集群拓撲,拓撲刷新請求結果計算開銷高、結果集大,大大增加了節點的處理開銷,也會造成正常服務請求時延的突刺,尤其對于連接數較大、集群節點多的集群,集中的拓撲刷新請求很容易造成節點計算資源緊張和網絡擁塞,容易觸發出各種服務異常告警。
(4)遷移無高可用
原生的遷移的 slot 標記狀態只存在于遷移雙端的主節點,其對應的從節點并不知道遷移狀態,這也就導致一旦在遷移期間發生節點的 failover,遷移流程將會中斷和出現 slot 狀態殘留,也將進一步導致遷移 slot 數據的訪問請求無法正常觸發 ask-move 機制而發生異常。例如遷移源節點異常,那么其 slave 節點 failover 上線,由于新主節點并不能同步到遷移狀態信息,那么對于遷移中 slot 的請求就不能觸發 ask 回復,如果是一個對已經遷移至目標節點的數據的寫請求,新主節點會直接在本節點新增 key,導致數據出現腦裂,類似地如果處理的是已經遷移數據的讀取請求也無法保證返回正確結果。
三、優化方案
3.1 優化方向思考
通過原生數據遷移機制分析,可以發現由于遷移操作涉及大量的同步阻塞操作會長時間占用工作線程,以及頻繁的拓撲刷新操作,會導致請求時延不斷出現上升。那么是否可以考慮將阻塞工作線程的同步操作改造成為異步線程處理呢?這樣改造有非常大的風險,因為原生遷移之所以能夠保證遷移期間數據訪問的正確性,正是這些同步接口進行了一致性保證,如果改為異步操作將需要引入并發控制,還要考慮遷移數據請求與 slave 節點的同步協調問題,此方案也無法解決拓撲變動開銷問題。所以 vivo 自研 Redis 放棄了原生按照 key 粒度進行遷移的邏輯,結合線上真實擴容需求,采用了類似主從同步的數據遷移邏輯,將遷移目標節點偽裝成遷移源節點的從節點,通過主從協議來轉移數據。
3.2 功能實現原理
Redis 主從同步機制是指在 Redis 主節點(Master)和從節點(Slave)之間進行數據同步和復制的過程,主從同步機制可以提高 Redis 集群的可用性,避免單點故障和數據丟失等問題。Redis 目前主從同步有全量同步和部分同步兩種方式,從節點發送同步位點給主節點,如果是首次同步則需要走全量同步邏輯,主節點通過發送 RDB 基礎數據文件和傳播增量命令方式將數據同步給從節點;如果不是首次同步,主節點則會通過從節點同步請求中的位點等信息判斷是否滿足增量同步條件,優先進行增量同步以控制同步開銷。由于主節點在同步期間也在持續處理新的命令請求,所以從節點對主節點的數據同步是一個動態追齊的過程,正常情況下,主節點會持續發送寫命令給從節點。
基于同步機制,我們設計實現了一套如下圖所示的 Redis 集群數據遷移的功能。遷移數據邏輯主要走的全量同步邏輯,遷移數據和同步數據最大的區別在于,正常情況下需要遷移的是源節點部分 slot 數據,目標節點并不需要復制源節點的全量數據,完全復用同步機制會產生不必要的開銷,需要對主從同步邏輯進行修改適配。為了解決該問題,我們對相關邏輯做了一些針對性的改造。首先在同步命令交互上,針對遷移場景增加了遷移節點間 slot 信息交互,從而讓遷移源節點獲知需要遷移哪些 slot 到哪個節點。另外,我們還對 RDB 文件文件結構按照 slot 順序進行了調整改造,并且將各個 slot 數據的文件起始偏移量數據作為元數據記錄到 RDB 文件尾部固定位置,這樣在進行遷移操作的 RDB 傳輸步驟時就可以方便地索引到 RDB 文件中目標 slot 數據片段。
3.3 改造效果分析
(1)時延影響小
對于 slot 遷移操作而言,主要涉及遷移源和目的兩端的開銷,對于基于主從同步機制實現的新 slot 遷移,其源節點主要開銷在于生成 RDB 和傳送網絡包,正常對于請求時延影響不大。但是因為目的節點需要對較大的 RDB 文件片段數據進行接收、加載,由于目的節點遷移時也需要對正常服務請求響應,此時不再能采用類似 slave 節點將所有數據收取完以后保存本地文件,然后進行阻塞式數據加載的方案,所以新 slot 遷移功能對遷移目的節點的數據加載流程進行了針對性改造,目的節點會按照接收到的網絡包粒度將數據按照下圖所示進行遞進式加載,即 slot 遷移目標節點每接收完一個 RDB 數據網絡包就會嘗試加載,每次只加載本次網絡包內包含的完整元素,這樣復合類型數據就可以按照 field 粒度加載,從而降低多元素大 key 數據遷移對訪問時延的劇烈影響。通過這樣的設計保持原來單線程簡潔架構的同時,有效地控制了時延影響,所有數據變更操作都保持在工作線程進行,不需要進行并發控制。通過以上改造,基本消除了遷移大 key 對遷移目的節點時延影響。
(2)數據訪問穩定
新 slot 遷移操作期間,正在遷移的數據還是存儲在源節點上沒有變,請求繼續在源節點上正常處理,用戶側的請求不會觸發 ask-move 轉發機制。這樣用戶就不需要擔心讀寫分離會出現數據不一致現象,在進行事務、pipeline 等方式封裝執行命令時也不會出現大量請求報錯的問題。遷移動作一旦完成,殘留在源端的已遷移 slot 數據將成為節點的殘留數據,這部分數據不會再被訪問,對上述殘留數據的清理被設計在 serverCron 中逐步進行,這樣每一次清理多少數據可以參數化控制,可以根據需要進行個性化設置,保證數據清理對正常服務請求影響完全可控。
(3)拓撲變更少
原生的遷移功能為了降低 ask-move 機制對正常服務請求的影響,每次僅會對一個 slot 進行數據遷移,遷移完了會立即發起拓撲變更通知來集群節點轉換 slot 的屬主,這就導致拓撲變化的次數隨著遷移 slot 的數量增加而變多,客戶端也會在每一次感知到拓撲變化后發送命令請求進行拓撲更新。更新拓撲信息的命令計算開銷較大,如果多條查詢拓撲的命令集中處理,就會導致節點資源的緊張。新的 slot 遷移按照節點進行數據同步,可以支持同時遷移源節點的多個 slot 甚至全部數據,最后可以通過一次拓撲變更轉換多個 slot 的屬主,大大降低了拓撲刷新的影響。
(4)支持高可用
集群的數據遷移是一個持續的過程,這個過程可能長達幾個小時,期間服務可能發生各種異常情況。正常情況下的 Redis 集群具有 failover 機制,從節點可感知節點異常以代替舊主節點進行服務。新 slot 遷移功能為了應對這樣的可用性問題,將 slot 遷移狀態同步給從節點,這樣遷移期間如果集群遷移節點發生 failover,其從節點就可以代替舊主節點繼續推進數據遷移流程,保證了遷移流程的高可用能力,避免人工干預,大大簡化運維操作復雜度。
四、功能測試對比
為了驗證改造后遷移功能的效果,對比自研遷移和原生遷移對請求響應的影響,在三臺同樣配置物理機上部署了原生和自研兩套相同拓撲的集群,選擇后對 hash 數據類型的 100k 和 1MB 兩種大小數據分別進行了遷移測試,每輪在節點間遷移內存用量 5G 左右的數據。測試主要目的是對比改造前后數轉移對節點服務時延影響,所以在實際測試時沒有對集群節點進行背景流量操作,節點的時延數據采用每秒鐘 ping 10次節點的方式進行采集,遷移期間源節點和目的節點的時延監控數據入下表所示(縱軸數值單位:ms)。
通過對比以上原生和自研集群 slot 遷移期間的時延監控數據,可以看出自研 slot 遷移功能遷移數據期間遷移兩端節點的請求響應時延表現非常平穩,也可以表現出經過主從復制原理改造的 Redis 集群 slot 遷移功能具備的優勢和價值。
五、總結和展望
原生 Redis 集群的擴縮容功能按照 key 粒度進行數據轉移,較大的 key 會造成工作線程的長時間占用,進而引起正常服務請求時延飆高問題,甚至導致節點長時間無法回復心跳包而被判定下線的情況,存在穩定性風險。通過同步機制改造實現的新 slot 遷移功能,能顯著降低數據遷移對用戶訪問時延的影響,提升線上 Redis 集群穩定性和運維效率,同時新的 slot 遷移功能還存在一些問題,例如新的遷移造成節點頻繁的 bgsave 壓力,遷移期間節點內存占用增加等問題,未來我們將圍繞這些具體問題,繼續不斷優化總結。