共計 14029 個字符,預計需要花費 36 分鐘才能閱讀完成。
丸趣 TV 小編今天帶大家了解如何分析 redis 中的高可用方案,文中知識點介紹的非常詳細。覺得有幫助的朋友可以跟著丸趣 TV 小編一起瀏覽文章的內容,希望能夠幫助更多想解決這個問題的朋友找到問題的答案,下面跟著丸趣 TV 小編一起深入學習“如何分析 redis 中的高可用方案”的知識吧。
主從復制
用戶可以通過 SLAVEOF 命令或者配置,讓一個服務器去復制另一個服務器。被復制的服務器稱為主服務器,進行復制的服務器稱為從服務器。這樣你在主服務器上增加鍵值,同時可以在從服務器上讀取。【相關推薦:Redis 視頻教程】
復制的過程又分為同步和命令傳播兩個步驟。
同步
同步將從服務器的數據庫狀態更新到主服務器當前的數據庫狀態。
客戶端向從服務器發送 SLAVEOF 命令時,從服務器會向主服務器發生 SYNC 命令進行同步,步驟如下:
從服務器向主服務器發生 SYNC 命令。
收到 SYNC 命令的主服務器執行 BGSAVE 命令,在后臺生成一個 RDB 文件,并用一個緩沖區記錄從現在開始執行的所有寫命令。
主服務器的 BGSAVE 命令執行完畢后,主服務器將 BGSAVE 生成的 RDB 文件發送給從服務器,從服務器接收并載入這個 RDB 文件,將從服務器的數據庫狀態更新到主服務器執行 BGSAVE 命令時的數據庫狀態
主服務器將緩沖區的所有寫命令發送給從服務器,從服務器執行這些寫命令,將數據庫狀態更新至主服務器當前數據庫狀態。
命令傳播
同步操作完成之后,主服務器和從服務器的數據庫狀態是一致的,但主服務器又接收到客戶端寫命令后,主從數據庫之間又產生了數據不一致,這時通過命令傳播達到數據庫一致。
PSYNC 同步的優化
2.8 之前的同步每次都是全量同步,而如果是從服務器只是斷開連接了一會,事實上是不用從頭開始同步的,只需要將斷開連接這會的數據同步即可。所以 2.8 版本開始使用 PSYNC 來代替 SYNC 命令。
PSYNC 分成全量同步和部分同步兩種情況,全量同步就是處理初次同步的狀態,而部分同步就是處理斷線重連這種情況。
部分同步的實現
部分同步主要使用了以下三部分:
主服務器的復制偏移量和從服務器的復制偏移量
主服務器的復制積壓緩沖區
服務器的運行 ID
復制偏移量
主服務器的復制偏移量:主服務器每次向從服務器傳播 N 個字節的數據時,就將自己的復制偏移量 + N 從服務器的復制偏移量:從服務器每次收到主服務器傳播的 N 個字節數據,就將自己的復制偏移量 +N
如果主從服務器處于一致狀態,那么它們的偏移量總是相同的,如果偏移量不相等,那么說明它們處于不一致狀態。
復制積壓緩沖區
復制積壓緩沖區由主服務器維護的一個固定長度的 FIFO 隊列,默認大小 1MB,達到最大長度后,最先入隊的會被彈出,給新入隊的元素讓位置。
redis 命令傳播的時候不但會發送給從服務器,還會發送給復制積壓緩沖區。
當從服務器重連上主服務器時,從服務器會通過 PSYNC 命令將自己的復制偏移量 offset 發送給主服務器,主服務器根據復制偏移量來決定使用部分同步還是全量同步。
如果 offset 偏移量之后的數據還在復制積壓緩沖區,那么使用部分同步,反之使用全量同步。
(書上沒說是怎么判斷的,我猜測應該是拿主復制偏移量減去從復制偏移量,如果大于 1MB 就說明有數據不在緩沖積壓區?)
服務器的運行 ID
服務器啟動時會生成一個 40 位隨機的字符作為服務器運行 ID。
從服務器對主服務器初次復制時,主服務器會將自己的運行 ID 傳送給從服務器,而從服務器會將這個運行 ID 保存下來。從服務器斷線重連的時候,會將保存的運行 ID 發送過去,如果從服務器保存的運行 ID 和當前主服務器的運行 ID 相同,那么會嘗試部分同步,如果不同會執行全量同步。
PSYNC 的整體流程
心跳檢測
在命令傳播階段,從服務器會默認以每秒一次的頻率,向主服務器發送命令:
REPLICONF ACK replication_offset
其中 replication_offset 就是從服務器當前的復制偏移量。
發送 REPLICONF ACK 命令對于主從服務器有三個作用:
檢測主從服務器的網絡連接狀態。
輔助實現 min-slaves 選項。
檢測命令丟失。
檢測主從服務器的網絡連接狀態
主從服務器可以通過發送和接收 REPLICONF ACK 命令來檢查兩者之間的網絡連接是否正常:如果主服務器超過一秒鐘沒有收到從服務器發來的 REPLICONF ACK 命令,那么主服務器就知道主從之間出現問題了。
輔助實現 min-slaves 選項
redis 的 min-slaves-to-write 和 min-slaves-max-lag 兩個選項可以防止主從服務器在不安全的情況下執行寫命令。
min-slaves-to-write 3
min-slaves-max-lag 10
如果配置如上,就表示如果從服務器的數量少于 3 個,或者 3 個從服務器的延遲都大于或等于 10 秒時,那么主服務器就將拒絕執行寫命令。
檢測命令丟失
如果因為網絡故障,主服務器傳播給從服務器的寫命令在半路丟失,那么從服務器向主服務器發送 REPLICONF ACK 命令時,主服務器將發覺從服務器當前的復制偏移量少于自己的偏移量,那么主服務器可以根據從服務器的復制偏移量,在復制緩沖區當中找到從服務器缺少的數據,將這些數據重寫發送給從服務器。
主從復制總結
其實主從復制就是多備份了一份數據,因為即使有 RDB 和 AOF 進行持久化,但是可能主服務器上整個機器掛掉了,而主從復制可以將主從服務器部署在兩臺不同的機器上,這樣即使主服務器的機器掛掉了,也可以手動切換到從服務器繼續服務。
sentinel
主從雖然實現了數據的備份,但當主服務器掛掉時,需要手動的將從服務器切換成主服務器。而 sentinel 就可以實現當主服務器掛掉時,自動將從服務器切換成主服務器。
sentinel 系統可以監視所有的主從服務器,假設 server1 現在下線。當 server1 的下線時長超過用戶設定的下線時長上限時,sentinel 系統就會對 server1 執行故障轉移:
首先 sentinel 系統會挑選 server1 下的其中一個從服務器,并將這個選中的從服務器升級成新的主服務器。
之后,sentinel 系統會向 server1 屬下的所有從服務器發送新的復制命令,讓他們成為新主服務器的從服務器。當所有從服務器復制新的主服務器時,故障轉移操作執行完畢。
另外,sentinel 還會監視已下線的 server1,在它重新上線時,將它設置為新的主服務器的從服務器。
初始化 sentinel 狀態
struct sentinelState { char myid[CONFIG_RUN_ID_SIZE+1];
// 當前紀元,用于實現故障轉移
uint64_t current_epoch;
// 保存了所有被這個 sentinel 監視的主服務器
// 字典的鍵是主服務器的名字
// 字典的值是指向 sentinelRedisInstance 結構的指針
dict *masters;
// 是否進入了 TILT 模式
int tilt;
// 目前正在執行的腳本數量
int running_scripts;
// 進入 TILT 模式的時間
mstime_t tilt_start_time;
// 最后一次執行時間處理器的時間
mstime_t previous_time;
// 一個 fifo 隊列,包含了所有需要執行的用戶腳本
list *scripts_queue;
char *announce_ip;
int announce_port;
unsigned long simfailure_flags;
int deny_scripts_reconfig;
char *sentinel_auth_pass;
char *sentinel_auth_user;
int resolve_hostnames;
int announce_hostnames;
} sentinel;
初始化 sentinel 狀態的 masters 屬性
masters 記錄了所有被 sentinel 監視的主服務器的相關信息,其中字典的鍵是被監視服務器的名字,而值是被監視服務器對應著 sentinelRedisInstance 結構。sentinelRedisInstance 被 sentinel 服務器監視的實例,可以是主服務器、從服務器或其他 sentinel 實例。
typedef struct sentinelRedisInstance {
// 標識值,記錄實例的類型,以及該實例的當前狀態
int flags;
// 實例的名字
// 主服務器名字在配置文件中設置
// 從服務器和 sentinel 名字由 sentinel 自動設置,格式是 ip:port
char *name;
// 運行 id
char *runid;
// 配置紀元,用于實現故障轉移
uint64_t config_epoch;
// 實例的地址
sentinelAddr *addr; /* Master host. */
// 實例無響應多少毫秒之后,判斷為主觀下線
mstime_t down_after_period;
// 判斷這個實例為客觀下線所需的支持投票數量
unsigned int quorum;
// 執行故障轉移,可以同時對新的主服務器進行同步的從服務器數量
int parallel_syncs;
// 刷新故障遷移狀態的最大時限
mstime_t failover_timeout;
// 除了自己外,其他監視主服務器的 sentinel
// 鍵是 sentinel 的名字,格式是 ip:port
// 值是鍵對應的 sentinel 的實例結構
dict *sentinels;
// ...
} sentinelRedisInstance;
創建連向主服務器的網絡連接
初始化 sentinel 的最后一步是創建連向被監視主服務器的網絡連接,會創建兩個連向主服務器的連接。
命令連接:專門向主服務器發送命令,并接收命令回復。
訂閱連接:專門用于訂閱主服務器的_sentinel_:hello 頻道。
獲取主服務器信息
sentinel 默認會每 10 秒,通過命令連接向被監視的主服務器發送 INFO 命令,并通過回復獲取主服務器當前的信息。回復可以獲得以下信息。
主服務器的 run_id
主服務器下所有從服務器的信息。
根據這些信息可以更新 sentinelRedisInstance 下的 name 字典和 runid 字段。
獲取從服務器信息
sentinel 也會創建連接到從服務器的命令連接和訂閱連接。
sentinel 默認會每 10 秒,通過命令連接向從服務器發送 INFO 命令,并通過回復獲取從服務器當前的信息。回復如下:
從服務器的運行 ID
從服務器的角色 role
主服務器的 ip 和端口
主服務器的連接狀態 master_link_status
從服務器的優先級 slave_priority
從服務器的復制偏移變量
根據 info 的回復信息,sentinel 可以更新從服務器的實例結構。
向主服務器和從服務器的訂閱連接發送信息
默認情況下,sentinel 會每 2 秒一次,向被監視的主服務器和從服務器發送命令。
s_ip:sentinel 的 ip 地址
s_port:sentinel 的端口號
s_runid:sentinel 的運行 id
s_epoch:sentinel 當前的配置紀元
m_name: 主服務器的名字
m_ip: 主服務器的 ip 地址
m_port: 主服務器的端口號
m_epoch: 主服務器當前的配置紀元
向 sentinel_:hello 頻道發送信息,也會被監視同一個服務器的其他 sentinel 監聽到(包括自己)。
創建連向其他 sentinel 的命令連接
sentinel 之間會互相創建命令連接。監視同一個囑咐其的多個 sentinel 將形成相互連接的網絡。
sentinel 之間不會創建訂閱連接。
檢測主觀下線狀態
sentinel 會每秒一次向所有與它創建了命令連接的實例(主服務器、從服務器、其他 sentinel)發送 ping 命令,通過實例的回復來判斷實例是否在線。
有效回復:實例返回 +PONG、-LOADING、-MASTERDOWN 其中一種。
無效回復:以上三種回復之外的其他回復,或者指定時長內沒回復。
某個實例在 down-after-milliseconds 毫秒內,連續向 sentinel 返回無效回復。那么 sentinel 就會修改這個實例對應的實例結構,在結構的 flags 屬性中打開 SRI_S_DOWN 標識,標識該實例進入主觀下線狀態。(down-after-milliseconds 可以在 sentinel 的配置文件中配置)
檢測客觀下線狀態
當 sentinel 將一個主服務器判斷為主觀下線后,為了確認這個主服務器是否真的下線,還會想其他同樣監視這個主服務器的其他 sentinel 詢問,看其他 sentinel 是否也認為該主服務器下線了。超過一定數量就將主服務器判斷為客觀下線。
詢問其他 sentinel 是否同意該服務器下線
SENTINEL is-master-down-by-addr ip port current_epoch runid
通過 SENTINEL is-master-down-by-addr 命令詢問,參數意義如下圖:
接收 SENTINEL is-master-down-by-addr 命令
其他 sentinel 接收到 SENTINEL is-master-down-by-addr 命令后,會根據其中主服務器的 ip 和端口,檢查主服務器是否下線,然后返回包含三個參數的 Multi Bulk 的回復。
sentinel 統計其他 sentinel 同意主服務器已下線的數量,達到配置的數量后,則將主服務器的 flags 屬性的 SRI_O_DOWN 標識打開,表示主服務器已經進入客觀下線狀態。
選舉領頭 sentinel
當一個主服務器被判斷成客觀下線時,監視這個下線主服務器的各個 sentinel 就會協商選舉一個新的領頭 sentinel,由這個 sentinel 進行故障轉移操作。
確認主服務器進入客觀下線狀態后,會再次發送 SENTINEL is-master-down-by-addr 命令來選舉出領頭 sentinel。
選舉規則
監視同一個主服務器的多個在線 sentinel 中每一個都可能成為領頭 sentinel。
每次進行領頭 sentinel 選舉之后,無論選舉是否成功,所有 sentinel 的配置紀元(configuration epoch)的值都會自增一次。(配置紀元,其實就是一個計數器)
在一個配置紀元里,所有 sentinel 都有將某個 sentinel 設置成局部 sentinel 的機會,一旦設置在這個配置紀元里就不能再更改。
所有發現主服務器客觀下線的 sentinel 都會要求其他 sentinel 將自己設置為局部領頭 sentinel,也就是都會發送 SENTINEL is-master-down-by-addr 命令,嘗試讓其他 sentinel 將自己設置成局部領頭 sentinel。
當一個 sentinel 向另一個 sentinel 發送 SENTINEL is-master-down-by-addr 命令時,如果 runid 參數的值不是 *,而是源 sentinel 的 runid, 就表示要目標 sentinel 將自己設置成領頭 sentinel。
sentinel 設置局部領頭的規則是先到先得,第一個設置為局部領頭 sentinel 后,其他的請求都被拒絕。
目標 sentinel 在接收到一條 SENTINEL is-master-down-by-addr 命令后,將向源 sentinel 返回一個命令回復。回復中 leader_runid 參數和 leader_epoch 參數分別記錄了目標 sentinel 的局部領頭 sentinel 的 runid 和配置紀元。
源 sentinel 接收到回復之后,會比較返回的配置紀元是否和自己的配置紀元相同,如果一樣再繼續比較返回的局部領頭 sentinel 的 runid 是否和自己的 runid 相同,如果一致就表示目標 sentinel 將自己設置成了局部領頭 sentinel。
如果某個 sentinel 被半數以上的 sentinel 設置成了局部領頭 sentinel,那么它就成為領頭 sentinel。
領頭 sentinel 需要半數以上支持,并且每個配置紀元內只能設置一次,那么一個配置紀元里,只會出現一個領頭 sentinel
如果在一定時限內,每一個 sentinel 被選舉成領頭 sentinel(沒人沒獲取半數以上選票),那么各個 sentinel 在一段時間之后再次選舉,直到選出領頭 sentinel
故障轉移
故障轉移包含以下三個步驟:
在已下線的主服務器下所有從服務器里,挑選出一個從服務器轉換成主服務器。
讓已下線的主服務器屬下的所有從服務器改為復制新的主服務器。
將已經下線的主服務器設置為新服務器的從服務器,舊的主服務器重新上線后,它就成為新的主服務器的從服務器。
選出新的主服務器
已下線的主服務器下所有從服務器里,挑選出一個從服務器,向這個從服務器發送 SLAVEOF no one 命令,將這個從服務器轉換成主服務器。
挑選新主服務器的規則
領頭的 sentinel 會將已下線主服務器的所有從服務器保存到一個列表里面,然后對這個列表進行過濾,挑選出新的主服務器。
刪除列表中所有處于下線或者斷線狀態的從服務器。
刪除列表中所有最近五秒內沒有回復過領頭 sentinel 的 INFO 命令的從服務器
刪除所有與已下線服務器連接斷開超過 dwon-after-milliseconds * 10 毫秒的服務器
然后根據從服務器的優先級,對列表中剩余的從服務器進行排序,并選出其中優先級最高的服務器。
如果有多個相同最高優先級的從服務器,那么就根據復制偏移量進行排序,選出最大偏移量的從服務器(復制偏移量最大也代表它保存的數據最新)
如果復制偏移量也相同,那么就根據 runid 進行排序,選其中 runid 最小的從服務器
發送 slaveof no one 命令之后,領頭 sentinel 會每秒一次向被升級的從服務器發送 info 命令(平常是每 10 秒一次),如果返回的回復 role 從原來的 slave 變成了 master,那么領頭 sentinel 就知道從服務器已經升級成主服務器了。
修改從服務器的復制目標
通過 SLAVEOF 命令來使從服務器復制新的主服務器。當 sentinel 監測到舊的主服務器重新上線后,也會發送 SLAVEOF 命令使它成為新的主服務器的從服務器。
sentinel 總結
sentinel 其實就是一個監控系統,,而 sentinel 監測到主服務器下線后,可以通過選舉機制選出一個領頭的 sentinel,然后由這個領頭的 sentinel 將下線主服務器下的從服務器挑選一個切換成主服務器,而不用人工手動切換。
集群
哨兵模式雖然做到了主從自動切換,但是還是只有一臺主服務器進行寫操作(當然哨兵模式也可以監視多個主服務器,但需要客戶端自己實現負載均衡)。官方也提供了自己的方式實現集群。
節點
每個 redis 服務實例就是一個節點,多個連接的節點組成一個集群。
CLUSTER MEET ip port
向另一個節點發送 CLUSTER MEET 命令,可以讓節點與目標節點進行握手,握手成功就能將該節點加入到當前集群。
啟動節點
redis 服務器啟動時會根據 cluster-enabled 配置選項是否為 yes 來決定是否開啟服務器集群模式。
集群數據結構
每個節點都會使用一個 clusterNode 結構記錄自己的狀態,并為集群中其他節點都創建一個相應的 clusterNode 結構,記錄其他節點狀態。
typedef struct clusterNode {
// 創建節點的時間
mstime_t ctime;
// 節點的名稱
char name[CLUSTER_NAMELEN];
// 節點標識
// 各種不同的標識值記錄節點的角色(比如主節點或從節點) // 以及節點目前所處的狀態(在線或者下線) int flags;
// 節點當前的配置紀元,用于實現故障轉移
uint64_t configEpoch;
// 節點的 ip 地址
char ip[NET_IP_STR_LEN];
// 保存建立連接節點的有關信息
clusterLink *link;
list *fail_reports;
// ...
} clusterNode;
clusterLink 保存著連接節點所需的相關信息
typedef struct clusterLink {
// ...
// 連接的創建時間
mstime_t ctime;
// 與這個連接相關聯的節點,沒有就為 null
struct clusterNode *node;
// ...
} clusterLink;
每個節點還保存著一個 clusterState 結構,它記錄了在當前節點視角下,集群目前所處的狀態,例如集群在線還是下線,集群包含多少個節點等等。
typedef struct clusterState {
// 指向當前節點 clusterNode 的指針
clusterNode *myself;
// 集群當前的配置紀元,用于實現故障轉移
uint64_t currentEpoch;
// 集群當前的狀態,上線或者下線
int state;
// 集群中至少處理一個槽的節點數量
int size;
// 集群節點的名單(包括 myself 節點) // 字典的鍵是節點的名字,字典的值為節點對應的 clusterNode 結構
dict *nodes;
} clusterState;
CLUSTER MEET 命令的實現
CLUSTER MEET ip port
節點 A 會為節點 B 創建一個 clusterNode 結構,并將該結構添加到自己的 clusterState.nodes 字典里面。
之后,節點 A 將根據 CLUSTER MEET 命令給定的 IP 地址和端口號,向節點 B 發送一條 MEET 消息。
如果一切順利,節點 B 將接收到節點 A 發送的 MEET 消息,節點 B 會為節點 A 創建一個 clusterNode 結構,并將該結構添加到自己的 clusterState.nodes 字典里面。
之后,節點 B 將向節點 A 返回一條 PONG 消息。
如果一切順利,節點 A 將接收到節點 B 返回的 PONG 消息,通過這條 PONG 消息節點 A 可以知道節點 B 已經成功地接收到了自己發送的 MEET 消息。
之后,節點 A 將向節點 B 返回一條 PING 消息。
如果一切順利,節點 B 將接收到節點 A 返回的 PING 消息,通過這條 PING 消息節點 B 知道節點 A 已經成功接收到自己返回的 PONG 消息,握手完成。
槽指派
集群的整個數據庫被分為 16384 個槽,每個鍵都屬于 16384 個槽的其中一個,集群中每個節點處理 0 個或 16384 個槽。當所有的槽都有節點在處理時,集群處于上線狀態,否則就是下線狀態。
CLUSTER ADDSLOTS
CLUSTER ADDSLOTS slot …
通過 CLUSTER ADDSLOTS 命令可以將指定槽指派給當前節點負責,例如:CLUSTER ADDSLOTS 0 1 2 3 4 可以將 0 至 4 的槽指派給當前節點
記錄節點的槽指派信息
clusterNode 結構的 slots 屬性和 numslot 屬性記錄了節點負責處理哪些槽:
typedef struct clusterNode {
unsigned char slots[CLUSTER_SLOTS/8];
int numslots;
// ...
} clusterNode;
slots:是一個二進制數組,一共包含 16384 個二進制位。當二進制位的值是 1,代表節點負責處理該槽,如果是 0,代表節點不處理該槽 numslots:numslots 屬性則記錄節點負責處理槽的數量,也就是 slots 中值為 1 的二進制位的數量。
傳播節點的槽指派信息
節點除了會將自己負責的槽記錄在 clusterNode 中,還會將 slots 數組發送給集群中的其他節點,以此告知其他節點自己目前負責處理哪些槽。
typedef struct clusterState { clusterNode *slots[CLUSTER_SLOTS];
} clusterState;
slots 包含 16384 個項,每一個數組項都是指向 clusterNode 的指針,表示被指派給該節點,如果未指派給任何節點,那么指針指向 NULL。
CLUSTER ADDSLOTS 命令的實現
在集群中執行命令
客戶端向節點發送與數據庫有關的命令時,接收命令的節點會計算出命令要處理的數據庫鍵屬于哪個槽,并檢查該槽是否指派給了自己。
如果指派給了自己,那么該節點直接執行該命令。如果沒有,那么該節點會向客戶端返回一個 MOCED 的錯誤,指引客戶端轉向正確的節點,并再次發送執行的命令。
計算鍵屬于那個槽
CRC16(key)是計算出鍵 key 的 CRC16 的校驗和,而 16383 就是取余,算出 0 -16383 之間的整數作為鍵的槽號。
判斷槽是否由當前節點負責處理
計算出鍵所屬的槽號 i 后,節點就能判斷該槽號是否由自己處理。
如果 clusterState.slots[i]等于如果 clusterState.myself, 那么由自己負責該節點可以直接執行命令。
如果不相等,那么可以獲取 clusterState.slots[i]指向如果 clusterNode 的 ip 和端口,向客戶端返回 MOVED 錯誤,指引客戶端轉向負責該槽的節點。
集群模式下不會打印 MOVED 錯誤,而是直接自動轉向。
重新分片
redis 集群重新分配可以將任意數量已經指派給某個節點的槽改為指派給另一個節點,相關槽所屬的鍵值對也會從源節點移動到目標節點。
重新分片操作是在線進行的,在重新分片的過程中,集群不用下線,源節點和目標節點都可以繼續處理命令請求。
redis 集群的重新分片操作是由 redis-trib 負責執行。重新分片執行步驟如下:
redis-trib 對目標節點發送 CLUSTER SETSLOT slot IMPORTING source_id 命令,讓目標節點準備好從源節點導入槽 slot 的鍵值對。
redis-trib 對源節點發送 CLUSTER SETSLOT slot MIGRTING target_id 命令,讓源節點準備好將屬于槽 slot 的鍵值對遷移至目標節點。
redis-trib 向源節點發送 CLUSTER GETKEYSINSLOT slot count 命令,獲取最多 count 個屬于槽的鍵值對的鍵名稱。
對于步驟 3 獲取的每個鍵名,redis-trib 都向源節點發送一個 MIGRTING target_ip target_port key_name 0 timeout 命令,將被選中的鍵值對從源節點遷移至目標節點。
重復執行步驟 3 和步驟 4,直到源節點保存的所以屬于槽 slot 的鍵值對都被遷移至目標節點。
redis-trib 向集群中任何一個節點發送 CLUSTER SETSLOT slot NODE target_id 命令,將槽指派給目標節點。這一信息最終會通過消息發送至整個集群。
CLUSTER SETSLOT IMPORTING 命令實現
typedef struct clusterState {
// ...
clusterNode *importing_slots_from[CLUSTER_SLOTS];
} clusterState;
importing_slots_from 記錄了當前節點正在從其他節點導入的槽。importing_slots_from[i]不為 null,則指向 CLUSTER SETSLOT slot IMPORTING source_id 命令,source_id 所代表的 clusterNode 結構。
CLUSTER SETSLOT MIGRTING 命令實現
typedef struct clusterState {
// ...
clusterNode *migrating_slots_to[CLUSTER_SLOTS];
} clusterState;
migrating_slots_to 記錄了當前節點正在遷移至其他節點的槽。migrating_slots_to[i]不為 null,則指向遷移至目標節點所代表的 clusterNode 結構。
ASK 錯誤
在重新分片期間,源節點向目標節點遷移槽的過程中,可能屬于這個槽的一部分鍵值對一部分保存在源節點當中,而另一部分保存在目標節點當中。
客戶端向源節點發送一個與數據庫鍵有關的命令,恰好這個槽正在被遷移。
源節點現在自己的數據庫中查找指定的鍵,如果找到,直接執行。
如果沒有找到,節點會檢查 migrating_slots_to[i]查看鍵是否正在遷移,如果在遷移就返回一個 ask 錯誤,引導客戶端轉向目標節點。
ASKING
客戶端收到 ask 錯誤之后,會先執行 ASKING 命令,再向目標節點發送命令。ASKING 命令就是打開發送該命令的客戶端的 REDIS_ASKING 標識。一般來說客戶端發送的鍵如果不屬于自己負責會返回 MOVED 錯誤(槽只遷移部分,這時槽還不屬于目標節點負責),但還會檢查 importing_slots_from[i],如果顯示節點正在導入槽 i,并且發送命令的客戶端帶有 REDIS_ASKING 標識,那么它就會破例執行一次該命令。
集群的故障轉移
集群的故障轉移效果和哨兵模式類似,也是將從節點升級成主節點。舊的主節點重新上線后將會成為新主節點的從節點。
故障檢測
集群中每個節點會定期的向集群中其他節點發送 PING 消息,檢測對方是否在線,如果指定時間內沒有收到 PONG 消息,那么就將該節點標記為疑似下線。clusterState.nodes 字典中找到該節點的 clusterNode 結構,將 flags 屬性修改成 REDIS_NODE_PFAIL 標識。
集群中各個節點會互相發送消息來交換集群中各個節點的狀態,例如:主節點 A 得知主節點 B 認為主節點 C 進入了疑似下線狀態,主節點 A 會在 clusterState.nodes 字典中找到節點 C 的 clusterNode 結構,并將主節點 B 的下線報告添加到 clusterNode 結構的 fail_reports 鏈表當中。
每一個下線報告由一個 clusterNodeFailReport 結構表示
typedef struct clusterNodeFailReport {
struct clusterNode *node;
// 最后一次收到下線報告的時間
mstime_t time;
} clusterNodeFailReport;
如果一個集群當中,半數以上負責處理槽的主節點都將某個主節點 X 報告為疑似下線。那么這個主節點 X 將被標記為已下線。將主節點 X 標記成已下線的節點會向集群廣播一條關于主節點 X 的 FAIL 消息。所有收到這條 FAIL 消息的節點都會將主節點 X 標記成已下線。
故障轉移
當一個從節點發現自己正在復制的主節點進入了已下線狀態,從節點將開始對下線主節點進行故障轉移。
復制下線主節點的所有從節點,會有一個主節點被選中。
被選中的從節點會執行 SLAVEOF no one 命令,成為新的主節點。
新的主節點會撤銷所有對已下線主節點的槽指派,并將這些槽全部指派給自己。
新的主節點向集群廣播一條 PONG 消息,這條 PONG 消息可以讓集群中的其他節點立即知道這個節點已經由從節點變成主節點。這個主節點已經接管了已下線節點負責處理的槽。
新的主節點開始接收和自己負責處理的槽有關的命令請求,故障轉移完成。
選舉新的主節點
新的主節點通過選舉產生
集群的配置紀元是一個自增計數器,它的初始值為 0。
當集群的某個節點開始一次故障轉移操作,集群的配置紀元的值加 1。
對于每個配置紀元,集群里每個負責處理槽的主節點都有一次投票的機會,第一個想主節點要求投票的從節點將獲得主節點的投票。
當從節點發現自己正在復制的主節點進入已下線狀態時,從節點會向集群廣播一條 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息,要求所有收到這條消息,并具有投票權的主節點向這個從節點投票。
如果一個主節點具有投票權(它正在負責處理槽),并且這個主節點尚未投票給其他從節點,那么主節點將向要求投票的從節點返回一條 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,表示這個主節點支持從節點成為新的主節點。
每個參與選舉的從節點都會接收 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,并根據自己收到了多少條這種消息來統計自己獲得了多少主節點的支持。
如果集群里有 N 個具有投票權的主節點,那么當一個從節點收集到大于等于 N / 2 + l 張支持票時,這個從節點就會當選為新的主節點。
因為在每一個配置紀元里面,每個具有投票權的主節點只能投一次票,所以如果有 N 個主節點進行投票,那么具有大于等于 N / 2 + l 張支持票的從節點只會有一個,這確保了新的主節點只會有一個。
如果在一個配置紀元里面沒有從節點能收集到足夠多的支持票,那么集群進人一個新的配置紀元,并再次進行選舉,直到選出新的主節點為止。
主節點選舉的過程和選舉領頭 sentinel 的過程非常相似。
數據丟失
主從復制數據丟失
主從復制之間是異步執行的,有可能 master 的部分數據還沒來得及同步到從數據庫,然后 master 就掛了,這時這部分未同步的數據就丟失了。
腦裂
腦裂就是說,某個 master 所在機器突然脫離了正常的網絡,跟其他 slave 機器不能連接,但是實際上 master 還運行著。此時哨兵可能就會認為 master 宕機了,然后開啟選舉,將其他 slave 切換成了 master,這個時候,集群里面就會有 2 個 master,也就是所謂的腦裂。
此時雖然某個 slave 被切換成了 master,但是可能 client 還沒來得及切換到新的 master,還繼續向舊 master 的寫數據。
master 再次恢復的時候,會被作為一個 slave 掛到新的 master 上去,自己的數據將會清空,重新從新的 master 復制數據,導致數據丟失。
減少數據丟失的配置
min-slaves-to-writ 1
min-slaves-max-lag 10
上述配置表示,如果至少有 1 個從服務器超過 10 秒沒有給自己 ack 消息,那么 master 不再執行寫請求。
主從數據不一致
當從數據庫因為網絡原因或者執行復雜度高命令阻塞導致滯后執行同步命令,導致數據同步延遲,造成了主從數據庫不一致。
感謝大家的閱讀,以上就是“如何分析 redis 中的高可用方案”的全部內容了,學會的朋友趕緊操作起來吧。相信丸趣 TV 丸趣 TV 小編一定會給大家帶來更優質的文章。謝謝大家對丸趣 TV 網站的支持!