Redis Cluster探索與思考
作者:張冬洪,微博研發中心高階DBA,Redis中國使用者組主席,多年Linux和資料庫運維經驗,專注於MySQL和NoSQL架構設計與運維以及自動化平臺的開發;目前在微博主要負責Feed核心系統相關業務的資料庫運維和業務保障工作。
責編:仲培藝,關注資料庫領域,尋求報道或者投稿請發郵件[email protected]。
本文為《程式設計師》原創文章,未經允許不得轉載,更多精彩文章請訂閱2017年《程式設計師》
Redis Cluster的基本原理和架構
Redis Cluster是分散式Redis的實現。隨著Redis版本的更替,以及各種已知bug的fixed,在穩定性和高可用性上有了很大的提升和進步,越來越多的企業將Redis Cluster實際應用到線上業務中,通過從社群獲取到反饋社群的迭代,為Redis Cluster成為一個可靠的企業級開源產品,在簡化業務架構和業務邏輯方面都起著積極重要的作用。下面從Redis Cluster的基本原理為起點開啟Redis Cluster在業界的分析與思考之旅。
基本原理
Redis Cluster的基本原理可以從資料分片、資料遷移、叢集通訊、故障檢測以及故障轉移等方面進行了解,Cluster相關的程式碼也不是很多,註釋也很詳細,可自行檢視,地址是:https://github.com/antirez/redis/blob/unstable/src/cluster.c。這裡由於篇幅的原因,主要從資料分片和資料遷移兩方面進行詳細介紹:
資料分片
Redis Cluster在設計中沒有使用一致性雜湊(Consistency Hashing),而是使用資料分片(Sharding)引入雜湊槽(hash slot)來實現;一個 Redis Cluster包含16384(0~16383)個雜湊槽,儲存在Redis Cluster中的所有鍵都會被對映到這些slot中,叢集中的每個鍵都屬於這16384個雜湊槽中的一個,叢集使用公式slot=CRC16(key)/16384來計算key屬於哪個槽,其中CRC16(key)語句用於計算key的CRC16 校驗和。
叢集中的每個主節點(Master)都負責處理16384個雜湊槽中的一部分,當叢集處於穩定狀態時,每個雜湊槽都只由一個主節點進行處理,每個主節點可以有一個到N個從節點(Slave),當主節點出現宕機或網路斷線等不可用時,從節點能自動提升為主節點進行處理。
如圖1,ClusterNode資料結構中的slots和numslots屬性記錄了節點負責處理哪些槽。其中,slot屬性是一個二進位制位陣列(bitarray),其長度為16384/8=2048 Byte,共包含16384個二進位制位。叢集中的Master節點用bit(0和1)來標識對於某個槽是否擁有。比如,對於編號為1的槽,Master只要判斷序列第二位(索引從0開始)的值是不是1即可,時間複雜度為O(1)。
叢集中所有槽的分配資訊都儲存在ClusterState資料結構的slots陣列中,程式要檢查槽i是否已經被分配或者找出處理槽i的節點,只需要訪問clusterState.slots[i]的值即可,複雜度也為O(1)。ClusterState資料結構如圖2所示。
查詢關係如圖3所示。
資料遷移
資料遷移可以理解為slot和key的遷移,這個功能很重要,極大地方便了叢集做線性擴充套件,以及實現平滑的擴容或縮容。那麼它是一個怎樣的實現過程?下面舉個例子:現在要將Master A節點中編號為1、2、3的slot遷移到Master B節點中,在slot遷移的中間狀態下,slot 1、2、3在Master A節點的狀態表現為MIGRATING,在Master B節點的狀態表現為IMPORTING。
MIGRATING狀態
這個狀態如圖4所示是被遷移slot在當前所在Master A節點中出現的一種狀態,預備遷移slot從Mater A到Master B的時候,被遷移slot的狀態首先變為MIGRATING狀態,當客戶端請求的某個key所屬的slot的狀態處於MIGRATING狀態時,會出現以下幾種情況:
- 如果key存在則成功處理。
- 如果key不存在,則返回客戶端ASK,客戶端根據ASK首先發送ASKING命令到目標節點,然後傳送請求的命令到目標節點。
- 當key包含多個命令時:
- 如果都存在則成功處理
- 如果都不存在,則返回客戶端ASK
- 如果一部分存在,則返回客戶端TRYAGAIN,通知客戶端稍後重試,這樣當所有的key都遷移完畢,客戶端重試請求時會得到ASK,然後經過一次重定向就可以獲取這批鍵
- 此時並不重新整理客戶端中node的對映關係
IMPORTING狀態
這個狀態如圖2所示是被遷移slot在目標Master B節點中出現的一種狀態,預備遷移slot從Mater A到Master B的時候,被遷移slot的狀態首先變為IMPORTING狀態。在這種狀態下的slot對客戶端的請求可能會有下面幾種影響:
- 如果key不存在則新建。
- 如果key不在該節點上,命令會被MOVED重定向,重新整理客戶端中node的對映關係。
- 如果是ASKING命令則命令會被執行,從而key沒在被遷移的節點,已經被遷移到目標節點的情況命令可以被順利執行。
鍵空間遷移
這是完成資料遷移的重要一步,鍵空間遷移是指當滿足了slot遷移前提的情況下,通過相關命令將slot 1、2、3中的鍵空間從Master A節點轉移到Master B節點,這個過程由MIGRATE命令經過3步真正完成資料轉移。步驟示意如圖5。
經過上面三步可以完成鍵空間資料遷移,然後再將處於MIGRATING和IMPORTING狀態的槽變為常態即可,從而完成整個重新分片的過程。
架構
實現細節:
Redis Cluster中節點負責儲存資料,記錄叢集狀態,叢集節點能自動發現其他節點,檢測出節點的狀態,並在需要時剔除故障節點,提升新的主節點。
Redis Cluster中所有節點通過PING-PONG機制彼此互聯,使用一個二級制協議(Cluster Bus) 進行通訊,優化傳輸速度和頻寬。發現新的節點、傳送PING包、特定情況下發送叢集訊息,叢集連線能夠釋出與訂閱訊息。
客戶端和叢集中的節點直連,不需要中間的Proxy層。理論上而言,客戶端可以自由地向叢集中的所有節點發送請求,但是每次不需要連線叢集中的所有節點,只需要連線叢集中任何一個可用節點即可。當客戶端發起請求後,接收到重定向(MOVED\ASK)錯誤,會自動重定向到其他節點,所以客戶端無需儲存叢集狀態。不過客戶端可以快取鍵值和節點之間的對映關係,這樣能明顯提高命令執行的效率。
Redis Cluster中節點之間使用非同步複製,在分割槽過程中存在視窗,容易導致丟失寫入的資料,叢集即使努力嘗試所有寫入,但是以下兩種情況可能丟失資料:
- 命令操作已經到達主節點,但在主節點回復的時候,寫入可能還沒有通過主節點複製到從節點那裡。如果這時主節點宕機了,這條命令將永久丟失。以防主節點長時間不可達而它的一個從節點已經被提升為主節點。
- 分割槽導致一個主節點不可達,然而叢集傳送故障轉移(failover),提升從節點為主節點,原來的主節點再次恢復。一個沒有更新路由表(routing table)的客戶端或許會在叢集把這個主節點變成一個從節點(新主節點的從節點)之前對它進行寫入操作,導致資料徹底丟失。
Redis叢集的節點不可用後,在經過叢集半數以上Master節點與故障節點通訊超過cluster-node-timeout時間後,認為該節點故障,從而叢集根據自動故障機制,將從節點提升為主節點。這時叢集恢復可用。
Redis Cluster的優勢和不足
優勢
- 無中心架構。
- 資料按照slot儲存分佈在多個節點,節點間資料共享,可動態調整資料分佈。
- 可擴充套件性,可線性擴充套件到1000個節點,節點可動態新增或刪除。
- 高可用性,部分節點不可用時,叢集仍可用。通過增加Slave做standby資料副本,能夠實現故障自動failover,節點之間通過gossip協議交換狀態資訊,用投票機制完成Slave到Master的角色提升。
- 降低運維成本,提高系統的擴充套件性和可用性。
不足
- Client實現複雜,驅動要求實現Smart Client,快取slots mapping資訊並及時更新,提高了開發難度,客戶端的不成熟影響業務的穩定性。目前僅JedisCluster相對成熟,異常處理部分還不完善,比如常見的“max redirect exception”。
- 節點會因為某些原因發生阻塞(阻塞時間大於clutser-node-timeout),被判斷下線,這種failover是沒有必要的。
- 資料通過非同步複製,不保證資料的強一致性。
- 多個業務使用同一套叢集時,無法根據統計區分冷熱資料,資源隔離性較差,容易出現相互影響的情況。
- Slave在叢集中充當“冷備”,不能緩解讀壓力,當然可以通過SDK的合理設計來提高Slave資源的利用率。
Redis Cluster在業界有哪些探索
通過調研瞭解,目前業界使用Redis Cluster大致可以總結為4類:
直連型
直連型,又可以稱之為經典型或者傳統型,是官方的預設使用方式,架構圖見圖6。這種使用方式的優缺點在上面的介紹中已經有所說明,這裡不再過多重複贅述。但值得一提的是,這種方式使用Redis Cluster需要依賴Smart Client,諸如連線維護、快取路由表、MultiOp和Pipeline的支援都需要在Client上實現,而且很多語言的Client目前都還是沒有的(關於Clients的更多介紹請參考https://redis.io/clients)。雖然Client能夠進行定製化,但有一定的開發難度,客戶端的不成熟將直接影響到線上業務的穩定性。
帶Proxy型
在Redis Cluster還沒有那麼穩定的時候,很多公司都已經開始探索分散式Redis的實現了,比如有基於Twemproxy或者Codis的實現,下面舉一個唯品會基於Twemproxy架構的例子(不少公司分散式Redis的叢集架構都經歷過這個階段),如圖7所示。
這種架構的優點和缺點也比較明顯。
優點:
- 後端Sharding邏輯對業務透明,業務方的讀寫方式和操作單個Redis一致;
- 可以作為Cache和Storage的Proxy,Proxy的邏輯和Redis資源層的邏輯是隔離的;
- Proxy層可以用來相容那些目前還不支援的Clients。
缺點:
- 結構複雜,運維成本高;
- 可擴充套件性差,進行擴縮容都需要手動干預;
- failover邏輯需要自己實現,其本身不能支援故障的自動轉移;
- Proxy層多了一次轉發,效能有所損耗。
正是因此,我們知道Redis Cluster和基於Twemproxy結構使用中各自的優缺點,於是就出現了下面的這種架構,糅合了二者的優點,儘量規避二者的缺點,架構如圖8。
目前業界Smart Proxy的方案瞭解到的有基於Nginx Proxy和自研的,自研的如餓了麼開源部分功能的Corvus,優酷土豆是則通過Nginx來實現,滴滴也在展開基於這種方式的探索。選用Nginx Proxy主要是考慮到Nginx的高效能,包括非同步非阻塞處理方式、高效的記憶體管理、和Redis一樣都是基於epoll事件驅動模式等優點。優酷土豆的Redis服務化就是採用這種結構。
優點:
- 提供一套HTTP Restful介面,隔離底層資源,對客戶端完全透明,跨語言呼叫變得簡單;
- 升級維護較為容易,維護Redis Cluster,只需平滑升級Proxy;
- 層次化儲存,底層儲存做冷熱異構儲存;
- 許可權控制,Proxy可以通過金鑰管理白名單,把一些不合法的請求都過濾掉,並且也可以對使用者請求的超大value進行控制和過濾;
- 安全性,可以遮蔽掉一些危險命令,比如keys *、save、flushall等,當然這些也可以在Redis上進行設定;
- 資源邏輯隔離,根據不同使用者的key加上字首,來實現動態路由和資源隔離;
- 監控埋點,對於不同的介面進行埋點監控。
缺點:
- Proxy層做了一次轉發,效能有所損耗;
- 增加了運維成本和管理成本,需要對架構和Nginx Proxy的實現細節足夠了解,因為Nginx Proxy在批量介面呼叫高併發下可能會瞬間向Redis Cluster發起幾百甚至上千的協程去訪問,導致Redis的連線數或系統負載的不穩定,進而影響叢集整體的穩定性。
雲服務型
這種型別典型的案例就是企業級的PaaS產品,如亞馬遜和阿里雲提供的Redis Cluster服務,使用者無需知道內部的實現細節,只管使用即可,降低了運維和開發成本。當然也有開源的產品,國內如搜狐的CacheCloud,它提供一個Redis雲管理平臺,實現多種型別(Redis Standalone、Redis Sentinel、Redis Cluster)自動部署,解決Redis例項碎片化現象,提供完善統計、監控、運維功能,減少開發人員的運維成本和誤操作,提高機器的利用率,提供靈活的伸縮性,提供方便的接入客戶端,更多細節請參考:https://cachecloud.github.io。儘管這還不錯,如果是一個新業務,到可以嘗試一下,但若對於一個穩定的業務而言,要遷移到CacheCloud上則需要謹慎。如果對分散式框架感興趣的可以看下Twitter開源的一個實現Memcached和Redis的分散式快取框架Pelikan,目前國內並沒有看到這樣的應用案例,它的官網是http://twitter.github.io/pelikan/。
自研型
這種型別在眾多型別中更顯得孤獨,因為這種型別的方案更多是現象級,僅僅存在於為數不多的具有自研能力的公司中,或者說這種方案都是各公司根據自己的業務模型來進行定製化的。這類產品的一個共同特點是沒有使用Redis Cluster的全部功能,只是借鑑了Redis Cluster的某些核心功能,比如說failover和slot的遷移。作為國內使用Redis較早的公司之一,新浪微博就基於內部定製化的Redis版本研發出了微博Redis服務化系統Tribe。它支援動態路由、讀寫分離(從節點能夠處理讀請求)、負載均衡、配置更新、資料聚集(相同字首的資料落到同一個slot中)、動態擴縮容,以及資料落地儲存。同類型的還有百度的BDRP系統。
Redis Cluster運維開發最佳實踐經驗
根據公司的業務模型選擇合適的架構,適合自己的才是最好的;
做好容錯機制,當連線或者請求異常時進行連線retry或reconnect;
重試時間可設定大於cluster-node-time (預設15s),增強容錯性,減少不必要的failover;
避免產生hot-key,導致節點成為系統的短板;
避免產生big-key,導致網絡卡打爆和慢查詢;
設定合理的TTL,釋放記憶體。避免大量key在同一時間段過期,雖然Redis已經做了很多優化,仍然會導致請求變慢;
避免使用阻塞操作(如save、flushall、flushdb、keys *等),不建議使用事務;
Redis Cluster不建議使用pipeline和multi-keys操作(如mset/mget. multi-key操作),減少max redirect的產生;
當資料量很大時,由於複製積壓緩衝區大小的限制,主從節點做一次全量複製導致網路流量暴增,建議單例項容量不要分配過大或者借鑑微博的優化採用增量複製的方式來規避;
資料持久化建議在業務低峰期操作,關閉aofrewrite機制,aof的寫入操作放到bio執行緒中完成,解決磁碟壓力較大時Redis阻塞的問題。設定系統引數vm.overcommit_memory=1,也可以避免bgsave/aofrewrite的失敗;
client buffer引數調整
client-output-buffer-limit normal 256mb 128mb 60
client-output-buffer-limit slave 512mb 256mb 180對於版本升級的問題,修改原始碼,將Redis的核心處理邏輯封裝到動態庫,記憶體中的資料儲存在全域性變數裡,通過外部程式來呼叫動態庫裡的相應函式來讀寫資料。版本升級時只需要替換成新的動態庫檔案即可,無須重新載入資料,可毫秒級完成;
對於實現異地多活或實現資料中心級災備的要求(即實現叢集間資料的實時同步),可以參考搜狐的實現:Redis Cluster => Redis-Port => Smart proxy => Redis Cluster;
-
- 加速key->hashslot的分配
- 更好更多的資料中心儲存
- redis-trib的C程式碼將移植到redis-cli,瘦身包體積
- 叢集的備份/恢復
- 非阻塞的Migrate
- 更快的resharding
- 隱藏一個只Cache模式,當沒有Slave時,Masters當在有一個失敗後能夠自動重新分配slot
- Cluster API和Redis Modules的改進,並且Disque分散式訊息佇列將作為Redis Module加入Redis。
致謝:感謝好友陳群、李航和劉東輝的協助審稿和寶貴建議。
SDCC 2017·深圳站之架構&大資料技術實戰峰會將於2017年6月10-11日於深圳南山區中南海濱大酒店舉行,集阿里、騰訊、百度、滴滴出行、Intel、微博、唯品會的資深架構師和一線實踐者,納知名研發案例,遇見蘇寧雲商大資料中心總監陳敏敏、Apache RocketMQ聯合創始人馮嘉、餓了麼大資料平臺部總監畢洪宇等大牛。
目前八折優惠售票倒計時1天,五人團購立減1000元,更多嘉賓和詳細議題關注大會官網和票務點選註冊參會。