為什麼要分散式
Redis是一款開源的基於記憶體的K-V型資料庫,因為記憶體訪問速度快,一般被用來做系統的快取。
Redis作為單機部署能夠支援業務簡單,資料量不大的系統需求,但在實際應用中,一旦系統規模上來,單機的Redis就會遇到下面的挑戰:
- 伸縮性。系統隨著長期執行與業務增長,對Redis儲存的資料量需求也越來越大,單機必然受限於伺服器的記憶體與磁碟大小。
- 高效能。系統規模變大後,對Redis的吞吐量需求也會提高,而單機的吞吐量必然有限,這種情況會影響整體系統的效能。
- 高可用。Redis持久化機制一定程度上能緩解單點問題,但是需要花費時間去恢復,在恢復的過程中服務可能不可用,或者資料會有丟失。
分散式解決方案
分散式的解決方案對於業內是通用的:
- 水平拆分。單點的一個重要挑戰就是資料量大的時候單點儲存不夠。直接的想法就是部署多個例項,將需要儲存的資料分散儲存到各個例項中。當例項儲存空間不夠時,繼續擴大例項個數就可以解決資料伸縮性問題。同時這種資料水平拆分的方法也可以解決單機效能問題,因為不同的資料讀寫可以分配到不同的例項。
- 主從複製。如果只有水平拆分,如果其中一個例項出現了問題,那該例項上儲存的資料都不可訪問,還是存在可用問題。為此可以進行所有資料的主備複製,同一份資料可以有多個副本,當某個例項出現問題時,可以啟用該例項的副本,達到高可用的目的。同時副本也可以幫助提高系統的吞吐能力,因為對資料的訪問也可以分發到副本上。
儲存的分散式解決方案大抵如上,不同的是各種系統的實現方法不同。下面我們來看下Redis的分散式解決方案是怎樣的。
歷史發展
分散式解決方案不是一蹴而就的,也有個發展的過程,不同歷史時期提供的方案可能不同,最新的解決方案也可能在不遠的將來被替換。
主從複製(replication)是Redis分散式的基礎。Redis中包含兩種節點:master節點與slave節點。同一份資料存放在master與多個slave節點上,
master對外提供讀寫,slave不對外提供寫操作。當master當機時,slave節點還能繼續提供服務。主從複製中最重要的就是主從資料如何同步。
Redis的主從同步分為全量同步與增量同步,在下面的章節會詳細介紹。
如果只有主從複製,當master當機時,需要運維人工將slave節點切換成master。這在生產環境是不可接受的,在這過程中系統可能無法使用。所以需要有一個機制,當master當機時能自動進行主從切換,應用程式無感知,繼續提供服務。於是Redis官方提供了一種方案: Redis Sentinel(哨兵模式)。
簡單的說,哨兵模式就是在主從基礎上增加了哨兵節點,哨兵節點不儲存業務資料,它負責監控主從節點的健康,當主節點當機時,它能及時發現,並自動選擇一個從節點,將其切換成主節點。為了避免哨兵本身成為單點,哨兵一般也由多個節點組成。
主從複製與哨兵模式解決了高可用問題,但資料的伸縮性還不行,還需要進行水平拆分。而這個時期大資料高併發的需求在快速增長,哪裡有需求,哪裡就有市場。在Redis官方自己的叢集方案出來之前,Codis應運而生並得到快速發展。
Codis是中國人開發並開源的,來自前豌豆莢中介軟體團隊。Codis和後面的Redis Cluster相似,將特定的key分發到特定的Redis例項上,預設將key化為1024個槽位;另外Codis採用zookeeper來維護節點間資料的一致性;更方便的是Codis提供了非常友好的後臺管理介面。更深入的大家可以自行去了解。但是當Redis官方的叢集方案 Redis Cluster 釋出後,Codis的境遇就有些尷尬了。畢竟不是親生的,很多新的特性總比Redis官方慢一拍。
正是因為分散式是剛需,所以Redis官方在3.0版本推出了自己的分散式方案,這就是Redis Cluster。這個名詞可能有些歧義,它不是簡單地代表Redis叢集,而是Redis的一種分散式方案,該方案的名稱就叫Redis Cluster。下面我們就重點介紹下該方案。
Redis Cluster
Redis Cluster 提供了一種去中心化的分散式方案,該方案可以實現水平拆分,故障轉移等需求。
拓撲結構
一個Redis Cluster由多個Redis節點組成,這裡的節點指的是Redis例項,一臺伺服器上可能有多個例項。這些節點可按節點組劃分,每個節點組裡的節點存放相同的資料,裡面有且只有一個是master節點,同時有0到多個slave節點。不同節點組存放的資料沒有交集,而所有節點組的資料組成這個Redis Cluster的全部資料集合。
如圖所示,這裡有3臺伺服器,每臺伺服器上有2個 Redis 例項。標號相同的節點組成一個節點組,他們存放相同的資料,如標號為1的2個節點組成一個節點組,其中深色的為master節點,另外一個為slave節點。也就是全量資料被劃分為3份,分別標號1/2/3。每份資料又有1個 slave 節點,他們通過主從複製儲存和 master 節點相同的資料。這裡主要有兩方面:一是主從複製,二是資料橫向劃分,在 Redis 中稱為分片 Sharding。
主從複製 Replication
主從節點最重要的是如何保證主從節點資料的一致性。只有 master 節點提供資料的寫操作,資料被寫到 master 節點後,再同步到 slave 節點。
主從之間的資料同步可以分為全量同步與增量同步。
全量同步
全量同步也稱為快照同步,主節點上進行一次 bgsave 操作,將當前記憶體的資料全部快照寫到磁碟檔案中,然後將檔案同步給從節點。從節點清空當前記憶體全部資料後全量載入該檔案,這樣達到與主節點資料同步的目的。
但是在從節點載入快照檔案的過程中,主節點還在對外提供寫服務。所以當從節點載入完快照後,依舊可能與主節點資料不一致,這時就需要增量同步上場了。
增量同步
增量同步的不是資料,而是指令流。主節點會將對當前資料狀態產生修改的指令記錄在記憶體的一個buffer中,然後非同步地將buffer同步到從節點,從節點通過執行buffer中的指令,達到與主節點資料一致的目的。
Redis的buffer是一個定長的環形結構,當指令流滿的時候,會覆蓋最前面的內容。所以當從節點上次增量同步由於各種原因,導致花費時間較長時,再次同步指令流時,就有可能前面沒有同步的指令被覆蓋掉了。這種情況就需要進行全量同步了。
所以全量同步與增量同步是相輔相成的關係。全量同步時一個很耗資源與時間的操作,如果單靠全量同步,同步操作會很重,在同步的長時間過程中不能提供服務。而增量同步有buffer容量限制,僅僅靠增量同步可能造成資料丟失,導致主從資料不一致。
分片 Sharding
所謂分片,就是將資料集按照一定規則,分散儲存在各個節點上。這裡涉及兩個問題:
1.分片規則是什麼?
2.如何儲存在各個節點上?
Redis將所有資料分為16384個hash slot(槽),每條資料(key-value)根據key值通過演算法對映到其中一個slot上,這條資料就儲存在該slot中。對映演算法是:
slotId=crc16(key)%16384
Redis的每個key都會基於該分片規則,落到特定的slot上。而在叢集部署完成時,slot的分佈就已經確定了。
172.16.190.78:7001> cluster nodes 08a5e808d2e6f6b231d73519bd4f05f74614c2a2 172.16.190.77:7000@17000 master - 0 1592218859545 3 connected 10923-16383 2c75029ab638a48537a4c02ed0ca77a19fc4106b 172.16.190.78:7000@17000 master - 0 1592218860448 1 connected 0-5460 50884e234c5f1ccf03f5a6d1cc4e6e6dc4779752 172.16.242.36:7000@17000 master - 0 1592218860000 2 connected 5461-10922 d0381ef4aad364c42e08bf1c2d78168f4901bd90 172.16.190.78:7001@17001 myself,slave 08a5e808d2e6f6b231d73519bd4f05f74614c2a2 0 1592218859000 4 connected e5c154dcf02526b807c67bebf0a63b4c98118ffe 172.16.190.77:7001@17001 slave 50884e234c5f1ccf03f5a6d1cc4e6e6dc4779752 0 1592218861048 6 connected 4a1b49cd77dbd18eba677663777f211be6f68dae 172.16.242.36:7001@17001 slave 2c75029ab638a48537a4c02ed0ca77a19fc4106b 0 1592218860000 5 connected
如上圖,3個master節點,172.16.190.77:7000節點存放10923-16383 slot,172.16.190.78:7000節點存放0-5460 slot,172.16.242.36:7000存放5461-10922 slot。
對於一個穩定的叢集,slot的分佈也是固定的。但在一些情況下,slot的分佈需要發生改變:
- 新的master加入
- 節點分組退出叢集
- slot分佈不均勻
這些情況下就需要進行slot的遷移。slot遷移的觸發與過程控制都是由外部系統完成,Redis只提供能力,但不自動進行slot遷移。
MOVE & ASKING
MOVED/ASKING 類似http的重定向碼3xx,是操作的一種錯誤返回。
當客戶端向某個節點發出指令,該節點發現指令的key對應的slot不在當前節點上,這時Redis會向客戶端傳送一個MOVED指令,告訴它正確的節點,然後客戶端去連這個正確的節點並進行再次操作。如下,15495為key a所在的slot id。
172.16.190.78:7001> get a (error) MOVED 15495 172.16.190.77:7000
ASKING 是在slot遷移過程中的一種錯誤返回。當某個slot在遷移過程中,客戶端發了一個位於該slot的某個key的操作請求,請求被路由到舊的節點。此時該key如果在舊節點上存在,則正常操作;如果在舊的節點上找不到,那麼可能該key已被遷移到新的節點上,也可能就沒有該key,此時會返回ASKING,讓客戶端跳轉到新的節點上去執行。
MOVED 與 ASKING 的共同點是兩者都是重定向,區別在於 MOVED 是永久重定向,下次對同樣的key
進行操作,客戶端就將請求傳送到正確的節點,而 ASKING 是臨時重定向,它只對這次操作起作用,不會更新客戶端的槽位關係表。
故障恢復
前面提到Redis Cluster是一個去中心化的叢集方案。比如Codis採用zookeeper來維護節點間狀態一致性,Redis哨兵模式是哨兵來管理節點的狀態,這些都是中心化的例子。Redis Cluster沒有專門用於維護節點狀態的節點,而是所有節點通過Gossip協議相互通訊,廣播自己的狀態以及自己對整個叢集認知的改變。
如果一個節點宕掉了,其他節點和它進行通訊時,會發現改節點失聯。當某個節點發現其他節點失聯時,會將這個失聯節點狀態變成PFail(possible fail),並廣播給其他節點。當一個節點收到某個節點PFail的數量達到了主節點的大多數,就標記該節點為Fail,並進行廣播,通過這種方式確認節點故障。
當slave發現其master狀態為Fail後,它會發起選舉,如果其他master節點都同意,則該slave進行從主切換,變成master節點。同時會將自己的狀態廣播給其他節點,達到大家資訊一致性。
根據上面的原理,下面情況下是無法自動從主切換,達到叢集繼續可用目的的,在實際部署時應避免:
- 如果一個節點組中的master與slave部署在同一伺服器上,當伺服器發生故障時,master與salve同時Fail。
- 比如總共3個master,其中2個master部署在同一伺服器上,當伺服器發生故障時,這兩個master同時Fail,slave無法完成從主切換,因為PFail的數量無法達到主節點的大多數。
當叢集節點發生變化時,需要將變化同步到客戶端,客戶端才能根據新的叢集拓撲來向正確的節點傳送請求。
比如Redis客戶端Lettuce,在連線配置中配置了多個節點,Lettuce會選擇其中一個可用節點進行連線。在連線斷開之前,如果該節點掛掉,Lettuce不會自動進行節點切換,此時會不斷地拋連線異常,無法繼續讀寫。Lettuce提供了在連線過程中自適應重新整理叢集拓撲,即在連線失敗時自動重新整理,也可以設計定時重新整理。
ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder() .enablePeriodicRefresh(Duration.ofMinutes(10)) .enableAllAdaptiveRefreshTriggers() .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(30)).build();
參考:
1.《深入分散式快取 從原理到實戰》於君澤 曹洪偉 邱碩等著
2.《Redis 深度歷險:核心原理與應用實踐》老錢
3.Redis官網:https://lettuce.io/core/release/reference/index.html
更多分享,?