Redis筆記(3)多數據庫實現
1.前言
本章介紹redis的三種多服務實現方式,盡可能簡單明了總結一下。
2.復制
復制也可以稱為主從模式。假設有兩個redis服務,一個在127.0.0.1:6379,一個在127.0.0.1:12345。我們登陸12345端口的redis,輸入命令slaveof 127.0.0.1:6379就設置好了復制模式。此時6379就是主服務器,12345就是從服務器。
復制模式下,主從服務器保存相同的數據,概念上稱之為數據庫狀態一致。redis2.8版本前後復制模式有些不同。
2.1 舊版復制
redis的復制功能分為兩塊:同步和命令傳播。同步指的是將從服務器的數據庫狀態更新至主服務器當前所處的數據庫狀態,從與主保持一致。命令傳播則用於主服務器的數據庫狀態被修改了,將命令傳遞給從服務器,保證數據庫狀態一致。
2.1.1 同步
從服務器發送slaveof命令,要復制主服務器時,從服務器首先需要執行同步操作,即將從服務器的數據庫狀態更新至主服務器當前所處的數據庫狀態。同步操作是由從服務器對主服務器發送SYNC命令完成的,具體步驟如下:
1.從服務器向主服務器發送SYNC命令
2.主服務器接收到命令執行BGSAVE,生成一個RDB文件,使用一個緩沖區記錄從現在開始執行的所有寫命令(BGSAVE是fork了一個子進程,當前內存的副本,後續修改丟失,所以在BGSAVE之後的寫操作要記錄在緩沖區)
3.BGSAVE執行完畢後,將RDB文件發送給從服務器,從服務器載入整個RDB文件,更新成主服務器一致的狀態。
4.BGSAVE執行之間的緩沖區寫命令發送給從服務器,從服務器同步到當前的主服務器狀態。
2.1.2 命令傳播
同步完成後,主服務器的數據庫狀態並不是保持不變的,隨時會發生變化。在發生變化的時候,主服務器就需要對從服務器執行命令傳播操作,會將寫命令發送給從服務器,這樣就又保持了一致的狀態了。
2.1.3 舊版功能的缺陷
上面的操作步驟簡單明了,但是實際運用中就會產生一個問題。如果是一個新的從服務器沒有復制過主服務器的,從零開始拷貝主服務器的內容沒有太大問題,但是如果從服務器因為網絡中斷,丟失了主服務器的命令傳播,導致再連上需要再進行一次同步,那無疑是糟糕的選項,意味著從零開始再來一次。
SYNC命令又是一個非常消耗資源的操作,如果主服務器有大量的數據,因為網絡中斷重新拷貝數據那太糟糕了。新版的復制解決了這個問題。
2.2 新版復制
redis2.8版本開始使用PSYNC命令取代了SYNC,該命令支持部分同步的能力,用於處理斷線重連。當然也支持完整同步,這個和SYNC命令沒有太大區別。
2.2.1 部分重同步實現
部分重同步由三個部分構成:主從服務器的復制偏移量,主服務器的復制積壓緩沖區,服務器的運行ID。
復制偏移量:
主從服務器都留存了一個復制偏移量,通過這個值可以知道復制到了什麽地步。
主服務器向從服務器發送N個字節的時候,自己的復制偏移量就+N
從服務器接受到主服務器的N個字節的時候,從服務器的復制偏移量+N
這樣通過對比兩者的復制偏移量就能夠很清楚的知道是否狀態一致了。
從服務器斷線重連後只需要發送自己的復制偏移量給主服務器,主服務就能夠決定是否有新的數據發送給從服務器了,將丟失的數據發送給從服務器,增量操作而非全量。
復制積壓緩沖區:
復制偏移量的方式是一個簡單有效的處理手段,但是這裏面存在一個問題,如果從服務器遲遲沒有連上,這個時候主服務器產生了大量的數據,還要全部緩沖下來嗎?這是一個致命的問題,可能導致內存爆炸。如果不緩沖下來,就存在緩沖區起始偏移量就超過了從服務器的丟失位置,從服務器無法更新丟失的數據。
復制積壓緩沖區是主服務器維護的一個固定長度先進先出隊列,默認大小為1MB。這個結構的含義是大小固定,超過大小再放入數據,隊列頭的數據會被移除,比如大小為3的隊列,放入hello,先存放了hel,l進來的時候擠掉了h變成了ell。主服務器進行命令傳播的時候會將命令傳遞給從服務器,並寫入這個復制積壓緩沖區。
從服務器連上的時候,會發送PSYNC命令將自己的偏移量發送給主服務器,主服務器會根據這個偏移量來決定執行哪種同步:
如果從服務器的偏移量後面的數據都在主服務器中,主服務器執行部分重同步。
如果不在,重連太慢,緩沖區被擠出數據,這個時候只能進行完整同步才能恢復數據了。
就像上面說的,緩沖區如果擠掉了要從服務器要同步的偏移量,那麽只能執行完整同步,這個就沒有發揮部分同步的價值了,所以設置緩沖區大小是一個很重要的事情。大小可以通過second * write_size_per_second進行估算,為了安全起見可以增加至2倍。修改緩沖區的配置是reply-backlog-size。
服務器運行ID:
每個redis服務器都有自己的運行ID,由40個隨機十六進制字符組成。從服務器初次復制主服務器時,主服務器會將自己的ID傳遞給從服務器,從服務器會保存這個值。當從服務器斷線並重新連上一個主服務器時,從服務器會將之前保存的ID發送給現在連接的主服務器。如果ID相同則說明是同一臺,可以執行部分重同步,相反地不同意味著連接的不是同一個主服務器,需要進行完整重同步操作。
2.2.2 PSYNC命令的實現
PSYNC命令調用方法有兩種:
1.從服務器沒有復制過主服務器,或者執行過slave no one命令重置。那麽從服務器在開始新的復制時將發送PSYNC ? -1命令,請求完整重同步。
2.復制過了再次同步時發送PSYNC <runid> <offset>命令,runid是上一次復制的主服務器運行ID,offset是當前從服務器的復制偏移量。
主服務器應對PSYNC命令會返回三種可能:
1.+FULLRESYNC <runid> <offset>表明是一個完整重同步,runid是主服務器得到id,offset是主服務器當前的偏移量,從服務器用此值作為初始值
2.+CONTINUE,執行部分重同步,主服務器會將後續的數據發給從服務器。
3.-ERR 表面主服務器版本低於2.8,不支持PSYNC命令,從服務器將向主服務器發送SYNC命令,執行完整重同步。
2.3 復制的完整過程
1.設置主服務器的地址和端口
SLAVEOF 127.0.0.1 6379
從服務器首先會保存主服務器的ip和端口在redisServer的masterhost和masterport屬性中。SLAVEOF是一個異步命令,保存成功後會返回一個ok給客戶端,復制在這之後進行。
2.建立套接字連接
SLAVEOF執行之後,從服務器會創建連向主服務器的套接字連接。創建連接成功,就會關聯一個專門用於處理復制工作的文件事件處理器,而主服務器接收到連接請求,會把其看成一個客戶端,從服務器是主服務器的一個客戶端,執行相關操作。
3.發送PING命令
套接字連接成功後,從服務器會首先發送一個PING命令給主服務器。該命令有兩個作用:一是保證套接字讀寫狀態正常,二是檢測主服務器是否工作正常,能夠正常返回應答。
從服務器發送PING命令後可能遇到3種情況:
主服務器返回了一個命令回復,但從服務器不能在規定時間內timeout,讀取回復內容,意味著網絡不佳,將會斷開連接並重新創建連接。
主服務器返回了一個錯誤,表面主服務不能處理從服務器的請求,從服務器斷開並重新創建主服務器的套接字。比如主服務器在處理一個超時運行腳本,那麽從服務器發送PING命令時,會接受到BUSY Redisis busy running...
從服務器讀取到PONG回復,表示連接正常,將繼續執行復制工作。
4.驗證身份
接收到PONG應答後,就要決定是否進行身份驗證了。從服務器設置了masterauth選項就會發送AUTH命令。有以下幾種情況:
主服務器沒有設置requirepass選項,從服務器也沒設置masterauth,就不需要認證,命令正常執行。
從服務器的AUTH與主服務器的requirepass相同,正常工作,不同返回invalid password
主服務器設置了requirepass選項,從服務器沒有設置masterauth,返回NOAUTH錯誤。此外,主服務器沒設置,從服務器設置了返回no password is set。
所有的錯誤都會讓從服務器中止目前的復制工作,並從創建套接字開始重新執行復制,知道身份驗證通過,或者從服務器放棄執行復制位置。
5.發送端口信息
身份驗證之後,從服務器會發送REPLCONF listening-port <port-number>消息,將自己監聽的端口告訴主服務器,主服務器會存入redisClient的slave_listening_port屬性中。
向主服務器執行INFO REPLICATION就能看見相關信息了。
6.同步
從服務器發送PSYNC命令,將數據更新至主服務器數據庫當前的狀態。執行同步之前,只有從服務器是主服務器的客戶端,執行同步之後,從服務器與主服務器互為客戶端。因為主服務器要將相關數據發送給從服務器執行,只有客戶端才能執行命令
7.命令傳播
完成同步後,主服務器會進入命令傳播階段,這時只要一直將自己執行的命令發送給從服務器,從服務器一直接收寫命令就可以保證主從一致。
2.4 心跳檢測
在命令傳播階段,從服務器會以每秒1次的頻率向主服務器發送命令REPLCONF ACK replication_offset,這個命令有3個作用:1.檢測網絡連接狀態。2.輔助實現min-slaves選項。3.檢測命令丟失。
檢測主從服務器的網絡連接狀態:
主服務器可以通過發送接收REPLCONF ACK命令來檢查兩者之間的網絡連接是否正常,如果超過1秒沒有接收到命令,則認為從服務器的連接出現了問題。INFO replication命令在從服務器列表的lag一欄中,可以看見從服務器最後一次向主服務器發送REPLCONF ACK命令過去了多久。
輔助實現min-slaves配置選項:
redis有兩個參數:
min-slaves-to-write 3
min-slaves-max-lag 10
意味著從服務器小於3個,或者3個延遲都大於等於10秒,主服務器會拒絕執行寫命令。
檢測命令丟失:
如果網絡故障,主服務器傳播給從服務器的寫命令在半路丟失,可以通過REPLCONF ACK命令感知到丟失,主服務器就會補發丟失的數據了。
這個和部分重同步很相似,不同之處在於此時主服務器沒有與從服務器斷開連接,部分重同步是在從服務器斷線重連時進行的。
2.5 適用範圍
主從復制的方式很顯然適用了寫少讀多的情況,讀寫分離。使用上有所局限,並非是一個高可用的方案,主服務器掛掉會產生很多問題。
3. Sentinel(哨兵模式)
上面說到復制模式的使用範圍。Sentinel是redis提供的一個高可用解決方案:由一個或多個Sentinel實例組成的Sentinel系統監視多個主服務器,以及主服務器下的從服務器狀態,並在主服務器下線時,將從服務器器升級為新的主服務器,由新的主服務器代替下線的主服務器繼續處理命令請求。
總體上來說,就是Sentinel系統監控所有節點的狀態,在主服務器掛掉後,挑選一個從服務器升級為主服務器的工作,然後其它從服務器以這個新主服務器為主。就是這麽一個工作完成復制模式在主節點掛掉了之後的尷尬局面。Sentinel多個實例也是為了避免一個Sentinel掛掉無法運行整個監控體系的尷尬局面。
3.1 啟動初始化Sentinel
redis-sentinel sentinel.conf 或者 redis-server sentinel.conf --sentinel
當一個sentinel啟動時,它需要執行以下步驟:
1.初始化服務器
2.將普通redis服務器使用的代碼替換成sentinel專用代碼
3.初始化sentinel狀態
4.根據給定的配置文件,初始化sentinel的監視主服務器列表
5.創建連向主服務器的網絡連接。
初始化服務器:
sentinel是一個特殊的redis服務器,但是其不執行任何數據庫鍵值對相關命令或者是加載RDB、AOF文件。其復制命令只針對sentinel系統內部使用,客戶端不能使用。
使用專用代碼:
因為特殊,所以不能使用普通redis的代碼,會進行替換。比如普通使用redis.c/redisCommandTable作為服務器命令列表,sentinel使用sentinel.c/sentinelcmds。服務器沒有載入不相關的命令。
初始化sentinel狀態:
sentinelState中記錄了以下內容,來維護sentinel運行:
current_epoch 當前紀元,用於實現故障轉移
dict *master 保存了所有被這個sentinel監視的主服務器,鍵是主服務器的名字,字典的值指向了一個sentienlRedisInstance結構
int tilt 是否進入了TILT模式
int running_scripts 目前正在執行的腳本數量
mstime_t tilt_start_time 進入TILT模式的時間
mstime_t previous_time 最後一次執行時間處理器的時間
list *scripts_queue 一個FIFO隊列,包含了所有需要執行的用戶腳本
監控的主機列表sentinelRedisInstance的結構如下:
int flags 標識值,記錄了實例的類型,以及該實例的當前狀態
char *name 實例的名稱,在配置文件中設置,從服務器的名稱由sentinel設置,為ip:port
char *runid 實例運行的id
uint64_t config_epoch 配置紀元,用於實現故障轉移
sentinelAddr *addr 實例的地址,包括ip 端口
mstime_t down_after_period 實例無響應多少毫秒後判斷下線,主觀下線
int quorum 判斷這個實例下線的支持投票數量,客觀下線
int parallel_syncs 在執行故障轉移操作時,可以同時對新的主服務器進行同步的從服務器數量
mstime_t failover_timeout 刷新故障遷移狀態的最大時限
比如配置:
sentinel monitor master1 127.0.0.1 6379 2(quorum)
sentinel down-after-milliseconds master1 30000
sentinel parallel-syncs master1 1
sentinel failover-timeout master1 900000
創建網絡連接:
Sentinel將對被監視的主服務器創建網絡連接,成為其客戶端,發送命令,並從命令回復中獲取相關的信息。
對於每個主服務器而言,sentinel會創建兩個連向主服務器的異步網絡連接:
一個是命令連接,專門用於向主服務器發送命令,接收命令回復。
一個是訂閱連接,用於訂閱主服務器的_sentinel_: hello頻道。
這麽設計的原因在於,sentinel需要發送命令,所以需要一個命令連接。訂閱連接在於,redis的發布訂閱功能中,被發送的信息不會保存在redis服務器裏面,如果客戶端不在線,就會丟失這些信息,所以需要一個專門的訂閱連接。sentinel會與多個redis實例連接,所以使用異步模式。
3.2 獲取主服務器信息
sentinel默認以每10秒一次的頻率,通過命令連接向被監視的主服務器發送INFO命令,並通過INFO命令的回復來獲取主服務器的當前信息。
可以獲取以下兩個方面的信息:
1.主服務器本身的信息,包括run_id和role服務器角色
2.主服務器屬下所有從服務器的信息,ip、port等內容。
sentinel會對這些信息創建或者更新。
3.3 獲取從服務器信息
sentinel發現新的從服務器的時候,除了會創建相應的結構之外,還會創建連接到從服務器的命令連接和訂閱連接。創建命令連接之後,sentinel在默認情況下,會以每10秒一次的頻率發送INFO命令。根據回復消息,sentinel會提取出以下信息:
從服務器運行的ID,從服務器的role,主服務器的IP地址master_host和master_port,主服務器的連接狀態master_link_status,從服務器的優先級slave_priority和從服務器的復制偏移量slave_repl_offset。
3.4 向主從服務器發送信息
Sentinel會默認以2秒一次的頻率,通過命令連接向所有的主從服務器發送以下格式的命令:
PUBLISH _sentinel_: hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>”
這條命令向服務器的_sentinel_:hello頻道發送了一條消息,s_開頭的是sentinel本身的信息,m_開頭的是主服務器的信息。
3.5 接收來自主從服務器的頻道信息
sentinel會訂閱頻道,但是3.4中卻又往頻道裏面推送了消息,這個設計可能會產生疑惑。但是實際上很好理解,因為sentinel系統中不一定只有一個sentinel實例啊。每個sentinel實例都會接收到其它sentinel的狀態,如果判斷是自己發的就丟棄信息,如果是其它sentinel發送的,就需要根據各個參數,對相應主服務器的實例結構進行更新了。
sentinel通過頻道信息發現一個新的sentinel時,它不僅會為新的sentinel在字典中創建相應的實例結構,還會創建一個連向新sentinel的命令連接,新的sentinel也會同樣創建一個連上這個sentinel的命令連接,多個sentinel形成了相互連接的網絡。使用命令連接可以發送命令請求進行信息交換。sentinel之間不會創建訂閱連接,因為主從服務器的訂閱頻道已經足夠用於sentinel相互之間進行發現了。命令連接足夠用於進行通信了。
3.6 檢測主觀下線狀態
默認情況下以每秒一次的頻率向所有與它創建了命令連接的實例(主從,sentinel)發送ping命令,並通過是否回復判斷實例是否在線。有效回復有:+PONG、-LOADING、-MASTERDOWN。如果在設置的down-after-milliseconds毫秒內連續沒有返回有效信息,判斷為主觀下線,flags打開SRI_S_DOWN標識。
主觀下線的設置不只是作用於該主服務器,其所有從服務器,以及所有監控該主服務器的sentinel都會用這個值作為判斷標準。每個sentinel監控相同的主服務器配置的下線時長可能不一樣,各自使用各自的判斷標準即可。
3.7 檢測客觀下線狀態
當一個sentinel判斷一個主服務器下線了,其會詢問其它的sentinel是否下線,接收足夠數量的下線判斷後,就會判斷成客觀下線,開始執行故障轉移。
發送命令詢問其它sentinel的判斷:
SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
接收響應:
<down_state> 判斷結果,1下線 0 未下線
<leader_runid> 可以是*或者目標sentinel局部領頭的sentinel的運行ID,*表示用於檢測主服務器下線狀態,id用於選舉領頭的sentinel
<leader_epoch> 目標sentinel的局部領頭sentinel的配置紀元,用於選舉領頭的sentinel,僅在leader_runid不為*時有效
不同的sentinel的配置不同,所以判斷客觀下線的票數也可能不同
3.8 選舉領頭sentinel
當一個主服務器被判斷成客觀下線時,監視這個下線主服務器的sentinel就要選出一個領頭的sentinel,由其對下線主服務器進行故障轉移操作了。選擇規則如下:
1.所有的sentinel都有可能成為leader
2.每次選舉無論成功失敗,epoch紀元都是+1
3.在一個紀元內,所有sentinel都有一次將某個sentinel設置為局部領頭的機會,一旦設置,本次紀元就不能修改
4.每個sentinel都會要求其它sentinel設置自己為頭
5.當sentinel發送sentinel is-master-down-by-addr時發送的runid不為*表明發送的sentinel要求接收的sentinel選其為局部領頭
6.目標sentinel應答命令時,回復其當前選舉的leader
7.源sentinel接收到回復,會判斷配置紀元是否相同,相同取出leader_runid,如果與自己一致,意味著目標sentinel選它作為leader節點
8.如果某個源sentinel被半數以下的sentinel設置成局部領頭sentinel,則其成為領頭。因為半數以上,所以一個紀元只會出現一個leader
9.一段時間內沒有選出來,會再次進行選舉,直到選出為止。
簡單通俗的說就是:若幹個sentinel確定了主服務器進入了主觀下線狀態,向其它sentinel確認是否下線,一旦確定進入客觀下線。確認的sentinel就會再次發送檢測命令,不過這次會帶上自己的id,其它sentinel接收到帶有id的就會明白要選舉了,如果沒有leader就認其為leader,有leader就返回告訴它,有sentinel更早確認了這一事實,你來晚了。在一個配置紀元內只會有一個leader,所以票選過半之後就自動升級為leader,可以開始進行故障轉移了。如果選不出來,就再進行一次。
這裏有一個問題就是,如何確保每個sentinel的紀元相同呢?不相同的話就無法判斷是否是同一次選舉了,比如A問C一次,B也問C一次,這兩個sentinel問的怎麽判斷是同一次的。另外選不出來是怎麽判斷的?是通過判斷是否有對主服務器進行下線處理嗎?
選舉算法時Raft算法的領頭選舉方法的實現。這篇文章可以理解以下上面的疑問:這裏。
3.9 故障轉移
選出leader之後,領頭的sentinel對下線的主服務器進行執行故障轉移,主要有3個步驟:
1.挑選一個從服務器,設置成主服務器
2.讓其余從服務器改為復制新的主服務器
3.將下線的主服務器設置為新的主服務器的從服務器
挑選主服務器:
sentinel有所有的主服務器的從服務器列表,從中過濾掉:下線或者斷線狀態的從服務器,5秒內沒有回復leader sentinel的INFO命令的從服務器,與已下線的主服務器斷線超過down-after-milliseconds * 10的從服務器,保證剩余的從服務器沒有過早與主服務器斷線,數據較新。之後根據優先級排序,相同優先級的選復制偏移量最大的,表明數據最新,這個指標也相同選runid最小的。
選出來後執行SLAVEOF no one。
這個之後,sentinel會每秒發送一次INFO命令,觀察role角色,如果變成master代表升級正常。
修改從服務器的復制目標:
向所有從服務器發送SLAVEOF 新主服務器地址完成。
將舊的主服務器變為從服務器:
舊的主服務器已下線,當前上線時,sentinel就會發送SLAVEOF命令,讓其成為新的主服務器的從服務器。
4.集群
哨兵模式可以看作是對復制模式的一種改進,讓復制模式可以自動恢復正常工作。其算是解決了高可用性,但是還有一個很糟糕的問題沒有解決,那就是資源消耗了。每個主從服務器裏面的數據完全一致,有大量的冗余,這對數據量很大的應用而言使用哨兵模式就有些浪費資源了。
redis集群是redis提供的分布式數據庫方案,集群通過分片sharding來進行數據共享,並提供復制和故障轉移功能。
4.1 節點
一個redis集群由多個node組成,一開始每個node都是一個集群,相互獨立,必須連接起來構成一個真正可用的工作集群。
連接節點通過命令:CLUSTER MEET <ip> <port>完成
假設有3個redis服務,7000,7001,7002:
首先登陸7000查看狀態 CLUSTER NODES,只會看見一個節點,現在將7001添加到7000集群中:CLUSTER MEET 127.0.0.1 7001
再此查看節點狀態 CLUSTER NODES,就可以看見2個節點了,7002添加方法相同。
啟動節點:
一個節點就是在redis集群中運行的一臺redis服務器,redis服務器會在啟動時根據cluster-enabled配置選項是否為yes決定是否開啟服務器的集群模式。
節點會繼續使用單機模式中使用的服務器組件,比如文件事件處理器,serverCron函數,保存鍵值對,RDB持久化或者AOF持久化模塊,PUBLISH等命令,復制模塊來進行節點的復制。
集群數據結構:
每個節點都會創建集群中所有節點的clusterNode的結構:
mstime_t ctime 創建節點的時間
char name[REDIS_CLUSTER_NAMELEN] 節點的名稱,由40個十六進制字符組成
int flags 節點標識,角色或者狀態
unit64_t configEpoch 節點當前配置的紀元,用於實現故障轉移
char ip[REDIS_IP_STR_LEN] 節點的ip地址
clusterLink *link 保存連接節點所需的有關信息
clusterLink結果如下:
mstime_t ctime 連接創建的時間
int fd TCP套接字描述符
sds sndbuf 輸出緩沖區,保存著等待發送給其他節點的消息
sds rcvbuf 輸入緩沖區,保存著從其他節點接收到的消息
struct clusterNode *node 與這個連接相關聯的節點
CLUSTER MEET命令的實現:
通過節點A發送該命令,將節點B添加進來。收到命令的節點A將與節點B進行握手,確認彼此的存在。
節點A會為節點B創建一個clusterNode結構,添加到自己的clusterState.nodes字典裏面,之後根據命令給的IP端口向B發送meet消息。節點B接收到消息,創建一個A的clusterNode結構,同樣添加到自己的clusterState.nodes字典。節點B返回一條PONG消息。節點A接收到PONG消息,發送一條PING消息,節點B接收到PING消息,握手成功。
4.2 槽指派
redis集群通過分片的方式保存數據庫的鍵值對:整個數據庫被分為16384個槽slot。每個鍵都在這些槽之中,每個節點最多處理0~16384個槽。所有的槽都有節點處理,集群就處於上線狀態,否則,處於下線狀態。
7000服務上執行CLUSTER INFO可以看見當前集群狀態,這就是因為沒有分配槽。
執行CLUSTER ADDSLOTS <slot> [slot...]可以分配槽,比如CLSUTER ADDSLOTS 0 1 2 .. 5000
所有的槽分配完成之後,集群就處於上線狀態了。
記錄節點的槽指派信息:
clusterNode的slots屬性和numslots記錄了節點負責處理哪些槽:
unsigned char slots[16384/8]
int numslots
slots數組在索引i上的二進制為1,那麽節點負責處理槽i。比如slot[0]為11111111則代表節點處理0~7槽。取出和設置操作的復雜度都是O(1)
傳播節點的槽指派信息:
一個節點除了記錄自己的槽位信息,也會將其發送給其他的節點。當接收到其他槽位信息的時候,就會更新相關結構。
記錄集群所有槽的指派信息:
clusterState中有個clusterNode *slot[16384]指向一個數據結構,不為NULL就是指派了節點。這樣可以很快確定某個槽由哪個節點處理。上面clusterNode中的slots信息也是有必要的,這個在傳播槽位指派信息中會很方便。
CLUSTER ADDSLOTS命令的實現:
遍歷所有的設置槽位,如果有一個被指派了節點,返回錯誤信息。只能為未分配的操作分配節點。
設置槽位信息。設置完畢後通知其他節點。
4.3 集群中執行命令
集群上線後,就可以執行命令了。客戶端向節點發送命令,節點會計算出命令要處理的數據庫是哪個槽的,並檢查該槽指派的節點,如果是自己,直接執行返回。如果是其他節點,返回一個MOVED命令,指引客戶端轉到正確的節點,並再次發送執行命令。
計算鍵所屬的槽:
CRC16(key) & 16383 用於計算鍵key的CRC16校驗和,再縮小到0~16383之間
使用CLUSTER KEYSLOT ”xxx" 查看鍵屬於哪個槽
判斷槽是否是當前節點處理:
檢查自己的clusterState.slots數組,如果是自己就執行,不是返回MOVED錯誤
MOVED錯誤:
格式為 MOVED <slot> <ip>:<port>
redis-cli -c -p 7000集群模式redis-cli中不會打印MOVED錯誤信息,使用redis-cli -p 7000單機模式的redis-cli客戶端就可以打印出來
節點數據庫的實現:
與之前的單機數據庫實現完全相同,唯一的區別在於只能使用0號數據庫
4.4 重新分片
redis可以在線進行槽位的重新指派,這個過程可以繼續處理命令請求。 比如添加一個7003節點。將原本的15001~16383指派給節點7003。
重新分片的原理如下:
通過集群管理軟件redis-trib負責執行,向源節點和目標節點發送命令來進行重新分片操作。
1.對目標節點發送CLUSTER SETSLOT <slot> IMPORTING <source_id>命令,讓目標節點準備好從源節點導入槽slot的鍵值對
2.對源節點發送CLUSTER SETSLOT <slot> MIGRATING <target_id>命令,讓源節點準備好將屬於槽slot的鍵值對遷移到目標節點
3.向源節點發送CLUSTER GETKEYSINSLOT <slot> <count>命令,獲得最多count個屬於槽slot的鍵值對的鍵名
4.對於3獲取的鍵名,redis-trib向源節點發送一個MIGRATE <target_ip> <target_port> <key_name> 0 <timeout>命令,將被選中的鍵原子地從源節點遷移至目標節點
5.重復3,4步驟,直到遷移完成。
6.向任一節點發送CLUSTER SETSLOT <slot> NODE <target_id>,指派槽,會通過消息傳至整個集群,最終所有節點都直到了。
4.5 ASK錯誤
重新分片的過程中會導致一部分鍵在原節點,一部分在目標節點,客戶發送請求的鍵正好處於遷移中的槽位時:源節點先檢查鍵,找到就執行執行。沒找到就可能存在遷移的節點了,源節點會返回一個ASK錯誤,指向正在導入的節點,再此發送之前的命令。和MOVED錯誤相似,不過一個是正常情況下沒有命中,一個是在重新分片過程中沒有命中。
CLUSTER SETSLOT IMPORTING命令的實現:
clusterState中的clusterNode *importing_slots_from[16384]的i下標不為null,意味著當前節點在從指向的節點導入槽i。
CULSTER SETSLOT MIGRATING命令的實現:
clusterState中的clusterNode *migrating_slots_to[16384]的i下標不為null,意味著當前節點正在將槽i導入給指向的節點。
ASK錯誤:
如果節點收到一個key,且該key在此節點上,該節點會嘗試找key,找到了執行執行,沒找到會檢查migrating_slots_to[i]查看是否正在遷移,是就會返回ASK錯誤。
客戶端轉向的時候會先發一個ASKING命令,再發送具體的命令。
ASKING命令:
接收到的唯一要做的就是打開客戶端的REDIS_ASKING標識,下次接收到指定的命令時,會判斷這個標識,是否執行命令,否則由於該槽位還沒有指向該節點被拒絕執行。
4.6 復制與故障轉移
redis集群中的節點分為主節點和從節點,主節點用於處理槽,從節點用於復制某個主節點,在主節點下線時,選出一個從節點替代下線主節點繼續處理命令請求。
設置從節點:
CLUSTER REPLICATE <node_id>
可以讓接收命令的節點成為node_id的從節點,並開始對主節點進行復制。
接收到命令的節點會在clusterState.nodes中找到node_id的結構,將clusterState.myself.slaveof指針指向這個結構。之後修改標識myself.flags,表明自己是一個從節點。最後進行復制操作。這個過程會通知集群中所有的節點,有一個節點變成了從節點,並且正在復制。
故障檢測:
集群中每個節點都會定期向集群中的其他節點發送PING消息,來檢測對方是否在線。沒有在規定時間內返回PONG消息,那麽會被標記成疑似下線。集群各個節點會通過交換節點狀態信息的方式,更新相關的下線標識。用個鏈表記錄所以判斷某個節點下線的節點。如果超過半數,這個節點就會被標記成下線,標記成下線的節點會廣播給所有節點某個節點x下線了。之後所有的節點都直到了節點x被半數判斷下線了。
故障轉移:
當一個從節點發現自己正在復制的主節點進入了已下線的狀態,從節點開始對下線主節點進行故障轉移,具體執行步驟如下:
選擇一個從節點作為新的主節點。
被選擇的從節點執行SLAVEOF no one命令,成為新的主節點
新的主節點會撤銷所有對已下線主節點的槽指派,指派給自己
廣播一條PONG消息,讓其他節點直到自己變成了主節點,並接管了下線主節點的槽。
新的主節點開始處理相關槽請求命令。
選擇新的主節點:
新的主節點是通過選舉產生的,集群的配置紀元是一個自增計數器,初始值為0。當集群裏的某個節點開始一次故障轉移操作時,配置紀元就會被+1。每個配置紀元中,集群裏的所有節點都有一次投票機會,而第一個向主節點要求投票的從節點將獲得投票。
從節點直到主節點下線後會廣播一條CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求有投票權的主節點投票給他。
其他主節點沒有投票過,就會設置該從節點為主節點,並返回ACK消息。
每個參選的從節點都會統計自己接收到多少個ACK,一個配置紀元內超過半數贊同即當選主節點。沒有選出進入下一個配置紀元,重新投票。
4.7 消息
集群中各個節點通過發送和接收消息來進行通信,消息主要有5種:
MEET 添加一個節點進入集群
PING 每秒隨機選出5個節點,對這5個節點中最長時間沒有發送過PING消息的節點發送消息,檢測是否在線。此外,節點A接收到節點B發送的PONG消息時間,距當前時間已經超過了節點A的cluster-mode-timeout選項設置的時長一半,節點A也會向節點B發送PING消息
PONG,接收到MEET和PING消息的應答消息。故障轉移後從節點變主節點也廣播PONG消息刷新其他節點對其的認知
FAIL,一個節點判斷另一個節點進入FAIL狀態,廣播給所有節點這個事實
PUBLISH,節點執行這個命令,並廣播一條PUBLISH,所有節點都會執行相同的命令。
集群中各個節點通過Gossip協議交換各自關於不同節點的狀態信息,Gossip協議由MEET、PING、PONG三種消息實現。
Gossip協議要傳遞所有節點在節點數多的情況下會有一定的延遲,所以FAIL這種要盡快處理的消息不是這麽實現的。
PUBLISH命令為什麽要通過一個節點廣播給所有節點都去執行一下這個命令呢?而不直接向所有節點廣播PUBLISH命令,實際上這樣做更簡單,但是不符合Redis集群的"各個節點通過發送和接收消息來進行通信”這一規則,所以節點沒有采取廣播PUBLISH命令的做法。
Redis筆記(3)多數據庫實現