第三部分 多機(jī)數(shù)據(jù)庫的實現(xiàn)
[toc]
1. 復(fù)制
? 在Redis中,用戶可以通過執(zhí)行SLAVEOF命令或者設(shè)置slaveof選項,讓一個服務(wù)器去復(fù)制另一個服務(wù)器,我們把被復(fù)制的服務(wù)器成為主服務(wù)器,對主服務(wù)器進(jìn)行復(fù)制的服務(wù)器成為從服務(wù)器.也就是我們常說的主從復(fù)制.
? 命令使用方式如下:
redis> SLAVEOF xxx.xxx.xxx.xxx 6379
? 進(jìn)行復(fù)制的主從服務(wù)器雙方將==保存相同的數(shù)據(jù)==,概念上稱為==服務(wù)器狀態(tài)一致==.
(1). 舊版復(fù)制功能的實現(xiàn)(Redis 2.8之前)
? Redis的復(fù)制功能分為同步(將從服務(wù)器的狀態(tài)更新至主服務(wù)器)和命令傳播(主服務(wù)器被更改后,讓主從服務(wù)器重新一致)兩個操作.
1). 同步
? 當(dāng)客戶端向發(fā)送了SLAVEOF命令,==進(jìn)入主從復(fù)制模式==的時候,從服務(wù)器首先要進(jìn)行同步操作,也就是==將從服務(wù)器中的數(shù)據(jù)更新為主服務(wù)器的數(shù)據(jù)庫狀態(tài)==.
? 通過SYNC命令完成,執(zhí)行步驟如下:
- 從服務(wù)器向主服務(wù)器發(fā)送SYNC命令
- 主服務(wù)器收到后執(zhí)行BGSAVE命令,生成一個RDB文件后使用一個緩沖區(qū)記錄從現(xiàn)在開始執(zhí)行的所有寫命令
- 主服務(wù)器的BGSAVE執(zhí)行完之后,主服務(wù)器將RDB文件發(fā)給從服務(wù)器,從服務(wù)器進(jìn)行加載,這時從服務(wù)器就會更新成主服務(wù)器執(zhí)行BGSAVE命令前的數(shù)據(jù)庫狀態(tài)
- 主服務(wù)器將命令緩沖中的命令全部發(fā)送給從服務(wù)器,從服務(wù)器執(zhí)行完之后達(dá)到主從一致的效果
2). 命令傳播
? 當(dāng)主服務(wù)器執(zhí)行寫命令的時候,主服務(wù)器的數(shù)據(jù)庫狀態(tài)就可能被改變,導(dǎo)致打破主從一致.
? 為了保持主從一致,主服務(wù)器需要將自己執(zhí)行的寫命令發(fā)送給從服務(wù)器執(zhí)行,從服務(wù)器執(zhí)行后,從新回到主從一致狀態(tài).
(2). 舊版功能的缺陷
? 對于從服務(wù)器從來沒有復(fù)制過任何服務(wù)器的初次復(fù)制情況,這種方案能夠完成任務(wù).但是如果是從服務(wù)器由于網(wǎng)絡(luò)原因斷線后的重新復(fù)制來說,效率就很低了.
? 因為這時還是會使用RDB文件作為同步的媒介,很大一部分?jǐn)?shù)據(jù)是重復(fù)的.
(3). 新版復(fù)制功能的實現(xiàn)
? Redis從2.8版本開始使用PSYNC命令代替SYNC命令來執(zhí)行復(fù)制時的同步操作.
? PSYNC具有完成重同步和部分重同步兩種模式:
- 完整重同步用于初次復(fù)制,和SYNC命令基本一樣
- 部分重同步用于斷線后的重復(fù)制,如果條件允許,主服務(wù)器可以將主從服務(wù)器連接斷開期間執(zhí)行的寫命令發(fā)送給從服務(wù)器
(4). 部分重同步的實現(xiàn)
? 部分重同步功能由一下三部分構(gòu)成:
- 主服務(wù)器的膚質(zhì)偏移量和從服務(wù)器的復(fù)制偏移量
- 從服務(wù)器的復(fù)制積壓緩存
- 服務(wù)器的運行ID
1). 復(fù)制偏移量
? 主服務(wù)器和從服務(wù)器都會維護(hù)一個復(fù)制偏移量.
- 主服務(wù)器每次向從服務(wù)器傳播N個字節(jié)數(shù)據(jù)時,九江自己的復(fù)制偏移量加上N
- 從服務(wù)器每次收到數(shù)據(jù)也會更新復(fù)制偏移量
? 通過==對比==主從服務(wù)器的==復(fù)制偏移量==就可以知道主從服務(wù)器是否處于主從一致狀態(tài)
2). 復(fù)制積壓緩沖區(qū)
? 復(fù)制積壓緩沖區(qū)是主服務(wù)器維護(hù)的一個==固定長度先進(jìn)先出隊列==,默認(rèn)大小1MB.內(nèi)部存儲了一系列命令的字節(jié)數(shù)組表示和其中每一個字節(jié)對應(yīng)的復(fù)制偏移量(也就是說,是可以清楚的知道每一條命令的復(fù)制偏移量).
? 主服務(wù)器進(jìn)行==命令傳播==時,不僅僅會將命令==發(fā)給從服務(wù)器==,還==寫入復(fù)制積壓緩沖區(qū)中==.因此復(fù)制積壓緩沖區(qū)中會保存最新的1MB命令數(shù)據(jù).當(dāng)從服務(wù)器重新連上主服務(wù)器時,從服務(wù)器根據(jù)自己的復(fù)制偏移量決定接下來的操作:
- 如果從服務(wù)器的復(fù)制偏移量不在復(fù)制積壓緩沖區(qū)中,那么進(jìn)行完整重同步操作
- 否則,說明從服務(wù)器中缺失的命令在復(fù)制積壓緩沖區(qū)中都存在,根據(jù)復(fù)制偏移量選擇起始位置進(jìn)行部分重同步操作
3). 服務(wù)器運行ID
? 每一個Redis服務(wù)器,不論主從都有自己的運行ID,這個ID由服務(wù)器啟動時自動生成,是一個40位十六進(jìn)制數(shù).在進(jìn)行主從復(fù)制時,從服務(wù)器會保存主服務(wù)器的運行ID.
? 在進(jìn)行斷線重連之后,從服務(wù)器會將之前自己保存的主服務(wù)器的運行ID發(fā)送給新的主服務(wù)器,如果一致,那么說明這是一次重連,進(jìn)行部分重同步操作.不一致則說明兩次連接的不是一個服務(wù)器,要進(jìn)行完整重同步操作.
(5). PSYNV命令的實現(xiàn)
? PSYNC命令的調(diào)用方法有兩種:
- 如果從服務(wù)器從來沒有連接過主服務(wù)器,或者已經(jīng)主動斷開了,那么在開始第一次新的復(fù)制時,主動請求服務(wù)器進(jìn)行完整重同步.
- 如果從服務(wù)器已經(jīng)父之過某個服務(wù)器,那么將之前復(fù)制的主服務(wù)器的運行ID和自己的復(fù)制偏移量發(fā)送給主服務(wù)器,由主服務(wù)器決定進(jìn)行哪一種同步操作;
? 主服務(wù)器的回應(yīng)會是下面三種之一:
- 如果主服務(wù)器要與從服務(wù)器進(jìn)行完整重同步,那么會把主服務(wù)器的運行ID發(fā)送給從服務(wù)器保存,將主服務(wù)器的復(fù)制偏移量發(fā)送給從服務(wù)器作為復(fù)制偏移量的起始值
- 如果主服務(wù)器要與從服務(wù)器進(jìn)行部分重同步,那么從服務(wù)器只需要等到主服務(wù)器將自己缺少的數(shù)據(jù)發(fā)送過來即可
- 如果主服務(wù)器的版本低于2.8,識別不了PSYNC命令,那么會讓從服務(wù)器重新發(fā)送一個SYNC命令進(jìn)行完整的重同步
(6). 復(fù)制的實現(xiàn)
? 通過向服務(wù)器(這里是從服務(wù)器)發(fā)送SLAVEOF命令也可以讓服務(wù)器去和其他服務(wù)器進(jìn)行主從連接.
1). 步驟1:設(shè)置主服務(wù)器的地址和端口
? 當(dāng)客戶端向服務(wù)器發(fā)送SLAVEOF命令請求時,從服務(wù)器首先要做的就是將發(fā)送來給定的主服務(wù)器IP和端口保存在服務(wù)器狀態(tài)中:
struct redisServer{
// .....
// 主服務(wù)器的地址
char *masterhost;
// 主服務(wù)器端口
int masterport;
// .....
}
? 然后服務(wù)器會向客戶端回復(fù)OK,隨后才開始真正執(zhí)行命令的內(nèi)容.
2). 步驟2:建立連接套接字
? 從服務(wù)器根據(jù)命令設(shè)置的IP和端口,創(chuàng)建連向主服務(wù)器的套接字連接,如果成功創(chuàng)建,那么從服務(wù)器將為這個套接字關(guān)聯(lián)一個專門處理復(fù)制工作的事件處理器,復(fù)制工作的所有內(nèi)容在后面都是由這個套接字完成的,
? 主服務(wù)器在接受這個套接字連接后,會創(chuàng)建響應(yīng)的客戶端狀態(tài),并將從服務(wù)器當(dāng)做鏈接到主服務(wù)器的一個客戶端來看待.
3). 步驟3:發(fā)送PING命令
? 從服務(wù)器成為主服務(wù)器的客戶端后,第一件事就是發(fā)送PING命令,PING命令有兩個作用:
- 檢查套接字的讀寫狀態(tài)是否正常
- 檢查主服務(wù)器能否正常處理命令請求
? 從服務(wù)器會收到三種響應(yīng)之一:
- 主服務(wù)器發(fā)送了一個命令回復(fù),但是從服務(wù)器并沒有在有限時間內(nèi)讀取出回復(fù)的內(nèi)容,那么說明當(dāng)前的網(wǎng)絡(luò)連接狀態(tài)不佳.會進(jìn)行斷開重連
- 主服務(wù)器向從服務(wù)器返回一個錯誤,表示主服務(wù)器暫時沒辦法處理從服務(wù)器的命令請求.從服務(wù)器會進(jìn)行斷開重連
- 從服務(wù)器收到PONG回復(fù),那么說明狀況良好,可以進(jìn)行下面的操作
4). 步驟4:身份驗證
? 從服務(wù)器收到PONG回復(fù)后,如果設(shè)置了masterauth,那么進(jìn)行身份驗證.
? 從服務(wù)器將向主服務(wù)器發(fā)送一條AUTH命令,參數(shù)為從服務(wù)器masterauth選項的值.
? 從服務(wù)器可能遇到的情況:
- 主服務(wù)器沒有設(shè)置requirepass選項,并且從服務(wù)器沒有設(shè)置masterauth,那么跳過這個階段
- 如果從服務(wù)器通過AUTH命令發(fā)送的密碼和主服務(wù)器requirepass選項設(shè)置的相同,那么驗證成功,繼續(xù)進(jìn)行
- 如果主從服務(wù)器只有一個進(jìn)行了設(shè)置,那么返回一個錯誤
5). 步驟5:發(fā)送端口信息
? 從服務(wù)器向主服務(wù)器發(fā)送從服務(wù)器監(jiān)聽的端口號.主服務(wù)器收到后,會記錄在從服務(wù)器的客戶端狀態(tài)中.
6). 步驟6:同步
? 從服務(wù)器向主服務(wù)器發(fā)送PSYNC命令,執(zhí)行同步操作.
? 值得一提的是,在執(zhí)行同步操作過后,主服務(wù)器也將成為從服務(wù)器的客戶端(雙方可以相互發(fā)送請求和回復(fù)),因為無論怎樣,主服務(wù)器都需要向從服務(wù)器發(fā)送命令請求來同步數(shù)據(jù).
7). 步驟7:命令傳播
? 主從服務(wù)器進(jìn)入命令傳播階段,主服務(wù)器一直想自己執(zhí)行的寫命令發(fā)送給從服務(wù)器,從服務(wù)器一直接收并執(zhí)行就可以保持主從一致.
(7). 心跳檢測
? 從服務(wù)器會以每秒一次的頻率向服務(wù)器發(fā)送命令:
// 參數(shù)是從服務(wù)器的復(fù)制偏移量
REPLCONF ACK <replication_offset>
? 有三個作用:
- 檢測主從服務(wù)器的網(wǎng)絡(luò)連接狀態(tài)
- 輔助實現(xiàn)min-slaves選項
- 檢測命令丟失
1). 檢測網(wǎng)絡(luò)連接狀態(tài)
? 如果至服務(wù)器超過一秒沒有接收到REPLCONF ACK命令,那么主服務(wù)器就知道網(wǎng)絡(luò)連接出現(xiàn)問題了.
? 這個計時的值被稱為lag,應(yīng)該在0~1浮動.
2). 輔助實現(xiàn)min-slaves配置選項
? 這個選項可以防止主服務(wù)器在不安全的情況下執(zhí)行寫命令.
? 可以配置從服務(wù)器少于多少,lag值大于多少的情況下,主服務(wù)器不執(zhí)行寫命令.
3). 檢測命令丟失
? 主服務(wù)器接收到REPLCONF ACK命令,取出其中的從服務(wù)器復(fù)制偏移量,與自己的復(fù)制偏移量進(jìn)行對比,如果不一致,那么從復(fù)制積壓緩沖區(qū)中找到服務(wù)器缺少的數(shù)據(jù),重新發(fā)送給從服務(wù)器.
? 這部分和部分重同步操作的原理很像.
? Redis 2.8版本之前沒有檢測丟失的功能.
2. Sentinel
? Sentinel(哨兵)是Redis的高可用性解決方案:==有一個過著多個Sentinel實例組成的Sentinel系統(tǒng)可以監(jiān)視任意多個主服務(wù)器以及每一臺主服務(wù)器下的所有從服務(wù)器,當(dāng)被監(jiān)視的一臺主服務(wù)器下線時,自動將這臺主服務(wù)器下的某個從服務(wù)器升級為新的主服務(wù)器,然后繼續(xù)維持主從一致==.
[圖片上傳失敗...(image-305527-1590395250247)]
穩(wěn)定狀態(tài)1:
[圖片上傳失敗...(image-d22c78-1590395250247)]
主服務(wù)器下線:
[圖片上傳失敗...(image-3e5155-1590395250247)]
哨兵進(jìn)行調(diào)整:
[圖片上傳失敗...(image-2f92d5-1590395250247)]
- Sentinel會挑選原主服務(wù)器下的其中一個從服務(wù)器,將這個從服務(wù)器升級為主服務(wù)器
- Sentinel系統(tǒng)向原主服務(wù)器下的其他所有從服務(wù)器發(fā)送新的復(fù)制指令,建立新的主從復(fù)制
- Sentinel繼續(xù)監(jiān)視那個下線的服務(wù)器,當(dāng)這個服務(wù)器重新上線時,成為一個從服務(wù)器
(1). 啟動并初始化Sentinel
? 啟動Sentinel可以使用命令:
redis-sentinel /path/to/your/sentinel.conf
或者
redis-server /path/to/your/sentinel.conf --sentinel
? 一個Sentinel啟動時,會執(zhí)行以下步驟:
- 初始化服務(wù)器
- 將普通的Redis服務(wù)器使用的代碼轉(zhuǎn)換成Sentinel專用代碼
- 初始化Sentinel狀態(tài)
- 根據(jù)指定的配置文件,初始化Sentinel監(jiān)視的主服務(wù)器列表
- 創(chuàng)建連向主服務(wù)器的網(wǎng)絡(luò)連接
1). 初始化服務(wù)器
? Sentinel本質(zhì)上只是一個運行在特殊模式下的Redis服務(wù)器,但是因為執(zhí)行的工作不相同,所以初始化方式也不完全相同.比如:不進(jìn)行RDB和AOF數(shù)據(jù)恢復(fù)等等.
2). 使用Sentinel專用代碼
? Sentinel使用一些新的代碼代替原先的部分Redis代碼中,比如默認(rèn)端口號.使用不同的服務(wù)器命令表(因為執(zhí)行的命令完全不同)
? 這也解釋了Sentinel不具備有些Redis服務(wù)器的功能.
3). 初始化Sentinel狀態(tài)
? 服務(wù)器會初始化一個sentinelState結(jié)構(gòu)體,也就是Sentinel狀態(tài),保存了服務(wù)器中所有和Sentinel功能相關(guān)的狀態(tài).
struct sentinelState{
// 當(dāng)前紀(jì)元,用于實現(xiàn)故障轉(zhuǎn)移
uint64_t current_epoch;
// 保存了所有被這個Sentinel監(jiān)視的主服務(wù)器
// 鍵是主服務(wù)器的名字,值是sentinelRedisInstance結(jié)構(gòu)體指針
dict *masters;
//是否進(jìn)入TILT模式
int tilt;
//目前正在執(zhí)行的腳本的數(shù)量
int running_scripts;
// 進(jìn)入TILT模式的時間
mstime_t tile_start_time;
// 最后一次執(zhí)行時間處理器的時間
mstime_t previous_time;
// 一個FIFO隊列,包含了所有需要執(zhí)行的用戶腳本
list *scripts_queue;
};
4). 初始化Sentinel狀態(tài)的masters屬性
? 每一個主服務(wù)器都對應(yīng)一個sentinelRedisInstance結(jié)構(gòu)體:
typedef struct sentinelRedisInstance{
// 標(biāo)識值,記錄當(dāng)前實例的狀態(tài)
int flags;
// 實例的名字
// 主服務(wù)器的名字從配置文件中得到
// 從服務(wù)器和Sentinel的名字由Sentinel自動設(shè)置
// 格式為ip:port
char *name;
// 實例的運行ID
char *runid;
// 配置紀(jì)元,用于實現(xiàn)故障轉(zhuǎn)移
uint64_t config_epoch;
// 實例的地址
sentinelAddr *addr;
//主觀下線的判定時間
mstime_t down_after_period;
// 客觀下線的投票數(shù)量
int quorum;
//進(jìn)行故障轉(zhuǎn)移時,可以同時對新的主服務(wù)器進(jìn)行同步的從服務(wù)器的數(shù)量
int parallel_syncs;
// 刷新故障遷移狀態(tài)的最大時限
msmtime_t failover_timeout;
// .....
}sentinelRedisInstance;
typedef struct sentinelAddr{
char *ip;
int port;
}sentinelAddr;
? Sentinel狀態(tài)的masters字典的初始化根據(jù)載入的配置文件進(jìn)行.也就是說,配置文件中應(yīng)該記錄所有要監(jiān)視的主服務(wù)器的屬性.
5). 創(chuàng)建連向主服務(wù)器的網(wǎng)絡(luò)連接
? 對于每個被監(jiān)視的主服務(wù)器來說,Sentinel會創(chuàng)建兩個異步網(wǎng)絡(luò)連接:
- 命令連接:用于專門向主服務(wù)器發(fā)送命令,并接收回復(fù)
- 訂閱連接:專門用于訂閱主服務(wù)器的__sentinel__:hello頻道
為什么需要訂閱連接:
? Redis目前的發(fā)布訂閱功能,發(fā)送的消息不會保存在Redis服務(wù)器中,一旦客戶端不在線就會丟失,所以只用一個專門的訂閱連接來接受該頻道的消息.
(2). 獲取主服務(wù)器信息
? Sentinel默認(rèn)會以每十秒的頻率通過命令連接來向被監(jiān)視的主服務(wù)器發(fā)送INFO命令,通過分析這個命令的回復(fù)來獲得對應(yīng)主服務(wù)器的狀態(tài).
? 通過INFO命令的回復(fù),Sentinel可以得到以下信息:
- 關(guān)于主服務(wù)器本身的信息:服務(wù)器運行ID,服務(wù)器角色等
- 關(guān)于主服務(wù)器屬下所有從服務(wù)器的信息,每個從服務(wù)器由一個"slave"字符串開頭的行記錄,每一行記錄了從服務(wù)器的ip和端口
? 根據(jù)主服務(wù)器本身的信息,Sentinel會對主服務(wù)器的實例結(jié)構(gòu)進(jìn)行更新.而從服務(wù)器的信息將被用于更新主服務(wù)器實例結(jié)構(gòu)的slaves屬性,這個屬性記錄了下屬的所有從服務(wù)器字典(鍵是從服務(wù)器名字ip:port,值是從服務(wù)器對應(yīng)的結(jié)構(gòu)),如果從服務(wù)器不存在,新建
(3). 獲取從服務(wù)器信息
? 當(dāng)Sentinel發(fā)現(xiàn)主服務(wù)器有新的從服務(wù)器出現(xiàn)時,Sentinel除了會建立對應(yīng)的實例結(jié)構(gòu)之外,還會創(chuàng)建鏈接到從服務(wù)器的命令連接和訂閱連接.也就是說,從服務(wù)器和Sentinel也是直接相連的.
? 創(chuàng)建命令連接之后,Sentinel也會每隔十秒型從服務(wù)器發(fā)送依次INFO命令,獲得從服務(wù)器的運行id,角色,所連接的主服務(wù)器的id和端口,連接狀態(tài),從服務(wù)器的優(yōu)先級,復(fù)制偏移量.根據(jù)這些信息,對從服務(wù)器的實例結(jié)構(gòu)進(jìn)行更新.
(4). 向主服務(wù)器和從服務(wù)器發(fā)送信息
? 默認(rèn)情況下,Sentinel會以每兩秒一次的頻率向所有被連接的主從服務(wù)器發(fā)送一條命令,這條命令會向服務(wù)器的__sentinel__:hello頻道發(fā)送一條信息,包含一下內(nèi)容:
參數(shù) | 意義 |
---|---|
s_ip | Sentinel的ip |
s_port | 端口 |
s_runid | 運行id |
s_epoch | 配置紀(jì)元 |
m_name | 主服務(wù)器的名字 |
m_ip | ip |
m_port | 端口 |
m_rpoch | 當(dāng)前配置紀(jì)元 |
? 如果是從服務(wù)器,那么其中的主服務(wù)器屬性指的是當(dāng)前進(jìn)行復(fù)制的主服務(wù)器的屬性.
(5). 接受來自主服務(wù)器和從服務(wù)器的頻道信息
? 當(dāng)Sentinel和一個服務(wù)器建立訂閱連接以后,Sentinel就會通過訂閱連接向服務(wù)器發(fā)送以下命令:
SUBSCRIBE __sentinel__:hello
? Sentinel對__sentinel__:hello頻道的訂閱會一致持續(xù)到Sentinel與服務(wù)器的連接斷開.
? 也就是說,Sentinel不僅僅可以向__sentinel__:hello頻道發(fā)送消息,也可以從__sentinel__:hello頻道讀取消息.
? 對于監(jiān)視同一個服務(wù)器的多個Sentinel來說,一個Sentinel發(fā)送的消息會被其他Sentinel接收到,這些消息會被用于更新其他Sentinel對于這個服務(wù)器的認(rèn)知.
1). 更新sentinels字典
? Sentinel為主服務(wù)創(chuàng)建的實例結(jié)構(gòu)中的sentinels字典會保存除了自己之外其他連接這個主服務(wù)器的Sentinel的資料.這個字典的鍵是其中一個Sentinel的名字(ip:port),值是對應(yīng)的Sentinel的實例結(jié)構(gòu)
? 當(dāng)Sentinel(目標(biāo)Sentinel)接收到其他Sentinel(源Sentinel)發(fā)來的消息時,就會從中得到以下信息:
- 源Sentinel的ip,端口,運行ID和配置紀(jì)元
- 源Sentinel正在監(jiān)視的主服務(wù)器的名字,ip,端口和配置紀(jì)元
? 根據(jù)這些消息,Sentinel會主服務(wù)器的sentinels字典
2). 創(chuàng)建連向其他Sentinel的命令連接
? 當(dāng)Sentinel通過頻道信息發(fā)現(xiàn)一個新的Sentinel是,不僅會更新sentinels字典,還會創(chuàng)建連向這個Sentinel的命令連接.
? ==最終監(jiān)視同一主服務(wù)器的多個Sentinel將形成相互連接的網(wǎng)絡(luò).==
(6). 檢測主觀下線狀態(tài)
? 默認(rèn)情況下,Sentinel會以每秒一次的頻率向所有與它創(chuàng)建了命令連接的實例(主服務(wù)器,從服務(wù)器,其他Sentinel),發(fā)送PING命令,名通過實例返回的PING命令回復(fù)判斷是否在線.
? 返回的回復(fù)中處理+PONG,-LOADING,-MASTERDOWN之外都是無效回復(fù).當(dāng)連續(xù)返回?zé)o效回復(fù)時長達(dá)到設(shè)定的主觀下線時間長度之后,Sentinel就會修改這個實例的實際結(jié)構(gòu)為主觀下線.
? 不同的Sentinel可能設(shè)置的主觀下線時長不同,當(dāng)一個Sentinel認(rèn)為某個實例主觀下線之后,其他的Sentinel可能認(rèn)為這個實例是在線的.
(7). 檢查客觀下線狀態(tài)
? 當(dāng)Sentinel認(rèn)為某個主服務(wù)器主觀下線后,為了進(jìn)行確認(rèn),它會向同樣監(jiān)視這個主服務(wù)器的其他Sentinel進(jìn)行詢問,當(dāng)Sentinel從其他Sentinel收集到足夠數(shù)量的答復(fù)(主觀下線或者客觀下線)能夠做出判斷之后,Sentinel就會判定主服務(wù)器為客觀下線,并進(jìn)行故障轉(zhuǎn)移.
1). 發(fā)送SENTINEL is-master-down-by-addr命令
? 該命令的參數(shù):詢問是否下線的主服務(wù)器的ip,duank,該Sentinel的配置紀(jì)元,Sentinel的運行id(這個也可以是*代替)
2). 接收SENTINEL is-master-down-by-addr命令
? 接收這個命令的Sentinel會提取出其中的各個參數(shù),返回向源Sentinel返回一個回復(fù).回復(fù)包含:該主服務(wù)器是否下線,局部領(lǐng)頭Sentinel的運行id和配置紀(jì)元.
3). 接收SENTINEL is-master-down-by-addr,命令的回復(fù)
? 源Sentinel接收到所有監(jiān)視這個主服務(wù)器的Sentinel的回復(fù)后,就得到了認(rèn)為該服務(wù)器下線的Sentinel的數(shù)量,通過和配置指定的判斷客觀下線的數(shù)量進(jìn)行對比(超過,則下線),就能得出是否客觀下線的結(jié)論.并對這個主服務(wù)器對應(yīng)結(jié)構(gòu)的標(biāo)記屬性進(jìn)行修改.
? 注意,客觀下線的標(biāo)準(zhǔn)在各個Sentinel中也是不一樣的,所以也會出現(xiàn)不同的Sentinel認(rèn)知不一樣的情況.
(8). 選舉領(lǐng)頭Sentinel
? 當(dāng)一個主服務(wù)器被認(rèn)為客觀下線時,監(jiān)視這個主服務(wù)器的各個Sentinel會進(jìn)行協(xié)商,選舉出一個領(lǐng)頭Sentinel,由這和Sentinel對這個下線的主服務(wù)器進(jìn)行故障轉(zhuǎn)移操作.選舉的規(guī)則和方法:
- 每個在線的Sentinel都有資格
- 每次選舉,不論是否成功,配置紀(jì)元都自增1
- 在一個配置紀(jì)元中(選舉之后,自增,一直到下一次自增中間的時間),只有一個Sentinel是領(lǐng)頭Sentinel,不可被更改
- 每個發(fā)現(xiàn)主服務(wù)器客觀下線的Sentinel都會要求其他Sentinel將自己設(shè)置為局部領(lǐng)頭Sentinel
- SENTINEL is-master-down-by-addr命令的參數(shù)中運行id不為*,則表示自己要做局部領(lǐng)頭Sentinel
- 局部Sentinel規(guī)則先到先得
- SENTINEL is-master-down-by-addr命令的回復(fù)中有源Sentinel的局部領(lǐng)頭Sentinel的運行id和配置紀(jì)元,如果配置紀(jì)元相同,且運行id是自己,那么就說明發(fā)送回復(fù)的Sentinel將自己設(shè)置成了局部領(lǐng)頭
- 如果一個Sentinel被半數(shù)以上的Sentinel設(shè)置為局部領(lǐng)頭Sentinel,那么它成為領(lǐng)頭Sentinel
- 如果在給定的時間內(nèi)沒有完成選舉,那么會重新進(jìn)行
進(jìn)行選舉的SENTINEL is-master-down-by-addr命令和進(jìn)行客觀下線判斷的SENTINEL is-master-down-by-addr不重疊.
(9). 故障轉(zhuǎn)移
? 選舉出領(lǐng)頭Sentinel后,由這個Sentinel對已經(jīng)下線的主服務(wù)器進(jìn)行故障轉(zhuǎn)移.包含以下三個步驟:
- 從這個主服務(wù)器的下屬中選出一個,升級為主服務(wù)器
- 讓其他下屬從服務(wù)器對這個新的主服務(wù)器進(jìn)行主從連接
- 將已經(jīng)下線的主服務(wù)器設(shè)置為新主服務(wù)器的從服務(wù)器,當(dāng)它重新上線時,直接得到身份
1). 選出新的主服務(wù)器
? 領(lǐng)頭Sentinel會將下線的主服務(wù)器的從服務(wù)器保存到一個列表中,然后按照以下規(guī)則進(jìn)行過濾:
- 刪除下線或者斷線的從服務(wù)器
- 刪除5秒內(nèi)沒有回復(fù)過領(lǐng)頭Sentinel的INFO命令的從服務(wù)器
- 刪除與主服務(wù)器斷開連接超過設(shè)定值*10毫秒的從服務(wù)器(保證從服務(wù)器中的數(shù)據(jù)較新)
? 然后,按照從服務(wù)器的優(yōu)先級進(jìn)行選擇,選出其中優(yōu)先級最高的從服務(wù)器(相同優(yōu)先級選擇復(fù)制偏移量大的,然后進(jìn)一步選擇運行id小的),充當(dāng)新的主服務(wù)器.
2). 修改從服務(wù)器的復(fù)制目標(biāo)
? 領(lǐng)頭Sentinel向其他所有從服務(wù)器發(fā)送SLAVEOF命令來讓其他的從服務(wù)器都與新的主服務(wù)器建立主從連接.
3). 將舊的主服務(wù)器變?yōu)閺姆?wù)器
? 領(lǐng)頭Sentinel還是發(fā)送SLAVEOF命令完成這一步.
3. 集群
? Redis集群是Redis提供的分布式數(shù)據(jù)庫方案,集群通過分片來進(jìn)行數(shù)據(jù)共享,并提供復(fù)制和故障轉(zhuǎn)移功能.
(1). 節(jié)點
? 一個Redis有多個節(jié)點構(gòu)成.剛開始,每一個Redis服務(wù)器本身都可以被視為一個單個節(jié)點的集群,它們相互聯(lián)通后就構(gòu)成了一個多個節(jié)點的集群.
? 連接各個節(jié)點可以使用以下命令完成:
CLUSTER MEET <ip> <port>
? 向一個服務(wù)器發(fā)送這個命令,會讓接收命令的服務(wù)器向ip:port的服務(wù)器進(jìn)行握手,握手成功時就會將ip:port對應(yīng)的節(jié)點添加到接收命令的服務(wù)器所處的集群中.
1). 啟動節(jié)點
? 一個節(jié)點就是一個運行在集群模式下的Redis服務(wù)器,Redis服務(wù)器會在啟動時根據(jù)ckuster-enabled配置選項是竇唯yes來決定是否開啟服務(wù)器的集群模式.
? 單機(jī)Redis的所有功能,組件和數(shù)據(jù)都沒有發(fā)生改變.Redis會將集群模式下使用到的數(shù)據(jù)保存到cluster.h/clusterNode結(jié)構(gòu),cluster.h/clusterLink結(jié)構(gòu)和cluster.h/clusterState結(jié)構(gòu)中.
2). 集群數(shù)據(jù)結(jié)構(gòu)
? clusterNode結(jié)構(gòu)保存了一個節(jié)點當(dāng)前的狀態(tài),每一個節(jié)點都會創(chuàng)建一個clusterNode結(jié)構(gòu)保存自己的狀態(tài),并為集群中的其他節(jié)點各自創(chuàng)建一個該結(jié)構(gòu)體.
// 節(jié)點信息
struct clusterNode{
// 創(chuàng)建節(jié)點的時間
mstime_t ctime;
// 節(jié)點的名字,字符數(shù)組的長度是固定的
char name[REDIS_CLUSTER_NAMELEN];
// 節(jié)點表示
// 記錄節(jié)點的角色(主從)和所處狀態(tài)(上線,下線)
int falgs;
// 節(jié)點當(dāng)前的配置紀(jì)元
uint64_t configEpoch;
// 節(jié)點的ip
char ip[REDIS_IP_STR_LEN];
// 端口號
int port;
// 保存連接節(jié)點所需的有關(guān)信息
clusterLink *link;
// .....
};
// 與這個節(jié)點連接的相關(guān)信息
typedef struct clusterLink{
// 連接的創(chuàng)建時間
mstime_t ctime;
// TCP套接字描述符
int fd;
// 輸出緩沖區(qū)
sds sndbuf;
// 輸入緩沖區(qū)
sds rcvbuf;
// 與這個節(jié)點所相連的所有節(jié)點
struct clusterNode *node;
}clusterLink;
// 當(dāng)前節(jié)點視角下的集群狀態(tài)
// 每一個節(jié)點中都存儲集群信息
typedef struct clusterState{
// 指向當(dāng)前節(jié)點的指針
clusterNode *myself;
// 結(jié)群當(dāng)前的配置紀(jì)元
uint64_t currentEpoch;
// 集群當(dāng)前的狀態(tài)(在線或則下線)
int state;
// 集群中至少處理著一個槽的節(jié)點的數(shù)量
int size;
// 集群節(jié)點的名單(包括myself節(jié)點)
dict *nodes;
// .....
}clusterState;
3). CLUSTER MEET命令的實現(xiàn)
? 客戶端向節(jié)點A發(fā)送CLUSTER MEET,讓A去和節(jié)點B握手:
- A會為B創(chuàng)建一個clusterNode結(jié)構(gòu),并添加到自己的clusterState.nodes字典中
- 節(jié)點A根據(jù)命令中的ip和端口向B發(fā)送一條MEET消息
- B接收到消息之后為A創(chuàng)建clusterNode結(jié)構(gòu),添加到自己的clusterState.nodes字典中
- B向A返回一條PONG消息,通過這個返回,A可以知道B準(zhǔn)備好了
- A向B返回一條PING消息,通過這個消息B知道A成功收到了自己的反饋
- 握手完成
- A節(jié)點會將B節(jié)點的信息通過Gossip協(xié)議傳播給集群中的其他節(jié)點,讓其他節(jié)點與B進(jìn)行握手
- 一段時間后,B會被集群中的所有節(jié)點認(rèn)識
(2). 槽指派
? Redis集群通過分片的方式保存數(shù)據(jù)庫中的鍵值對:集群的整個數(shù)據(jù)庫被分為16384個槽,數(shù)據(jù)庫中的每一個鍵被分配給這16384個槽中的一個,每個節(jié)點可以處理0~16384個槽.當(dāng)數(shù)據(jù)庫中的所有槽都被處理時,集群處于上線狀態(tài),否則就是下線狀態(tài).
? 以下命令可以將一個或者多個槽指派給接==受命令的節(jié)點==負(fù)責(zé):
CLUSTER ADDSLOTS <slot> [slot ...]
// 下面這個命令可以將0~5000的槽都交給127.9.9.1:7000的節(jié)點處理
127.0.0.1:7000> CLUSTER ADDSLOTS 0 1 2 3 ... 5000
1). 記錄節(jié)點的槽指派信息
? clusterNode結(jié)構(gòu)的slots屬性和numslot屬性記錄了節(jié)點負(fù)責(zé)處理那些事情:
struct clusterNode{
// .....
// 二進(jìn)制為數(shù)組,包含16384個二進(jìn)制位,每一位表示這個槽是否被這個節(jié)點處理
unsigned char slots[16384/8];
// 這個節(jié)點處理的槽的數(shù)量
int nummslots;
// .....
};
2). 傳播節(jié)點的槽指派信息
? 一個節(jié)點除了負(fù)責(zé)記錄自己的slots屬性和numslots屬性之外,還會通過消息將自己的這兩個屬性發(fā)送給集群中的其他所有節(jié)點.當(dāng)其他節(jié)點收到時,就會保存在自己節(jié)點的clusterState結(jié)構(gòu)中的slots數(shù)組中.
? 所以集群中的每一個節(jié)點都能知道任意一個槽被分派的節(jié)點.
3). 記錄集群所有槽的指派信息
? clusterState結(jié)構(gòu)中的slots數(shù)組記錄了所有槽的指派信息:
typedef struct clusterState{
// .....
clusterNode *slots[16384];
// .....
}clusterState;
? slots數(shù)組中包含16384個clusterNode指針,分別代表了每一個槽的分派節(jié)點.
這個數(shù)組存在的意義是:O(1)時間復(fù)雜度獲取某個槽的分派節(jié)點
但是如果只有這個數(shù)組存儲分派信息,那么在分派信息的傳播時,就會一次又一次的遍歷這個數(shù)組,得到每一個節(jié)點的負(fù)責(zé)區(qū)域.很低效.
4). CLUSTER ADDSLOTS命令的實現(xiàn)
? CLUSTER ADDSLOTS命令接受一個或者多個槽作為參數(shù),將這個槽指派給接收命令的節(jié)點負(fù)責(zé)
? 偽代碼實現(xiàn):
def CLUSTER_ADDSLOTS(*all_input_slots):
# 檢查傳入的槽是否有已經(jīng)被分派的
for i in all_input_slots:
if clusterState.slots[i] != NULL:
# 如果有,報錯
reply_error()
return
# 再次遍歷
for i in all_input_slots:
# 更新這個槽的負(fù)責(zé)節(jié)點指針
clusterState.slots[i] = clusterState.myself
# 更新負(fù)責(zé)節(jié)點中slots屬性對應(yīng)位上的值為1
setSlotBit(clusterState.myself.slots, i)
# 告訴其他所有節(jié)點,自己目前正在負(fù)責(zé)的槽
call_all_other_node()
(3). 在集群中執(zhí)行命令
? 對數(shù)據(jù)庫中的16384個槽都進(jìn)行了指派之后,集群就會進(jìn)入上線狀態(tài),這是客戶端就可以向集群中的節(jié)點發(fā)送數(shù)據(jù)命令了.
? 當(dāng)客戶端向一個節(jié)點發(fā)送一個命令請求時,接收命令的節(jié)點會計算出命令要處理的數(shù)據(jù)庫鍵處于的槽,并判斷這個槽是不是自己負(fù)責(zé)的,如果是,那么自己處理這個命令.如果不是,那么向客戶端返回一個MOVED錯誤,引導(dǎo)客戶端轉(zhuǎn)向正確的節(jié)點,再次發(fā)送要執(zhí)行的命令.
1). 計算鍵屬于哪個槽
? 算法同以下偽代碼:
def slot_number(key):
# CRC16()方法用于計算key的CRC-16校驗和
# 然后進(jìn)行除法散列
return CRC16(key) & 16383
2). 判斷槽是否由當(dāng)前節(jié)點負(fù)責(zé)
? 從clusterState.slots中取出這個槽對應(yīng)的節(jié)點指針,和clusterState.myself進(jìn)行對比即可.
3). MOVED錯誤
? MOVED錯誤的格式為:
MOVED <slot> <ip>:<port>
? 內(nèi)容包括當(dāng)前key的槽,和負(fù)責(zé)這個槽的節(jié)點的ip:端口
? 一個連接集群的客戶端通常會與集群中的多個節(jié)點創(chuàng)建連接,這里客戶端就會根據(jù)MOVED返回的信息選擇正確的套接字發(fā)送命令.如果碰巧這時這個節(jié)點未與當(dāng)前客戶端連接,那么會先根據(jù)ip和端口創(chuàng)建套接字連接,然后在進(jìn)行轉(zhuǎn)向.
集群模式的客戶端由以下命令啟動:
redis-cil -c -p <port>
集群模式下MOVED錯誤是被隱藏的,客戶端會自行進(jìn)行節(jié)點的轉(zhuǎn)向,用戶是察覺不到的.單機(jī)模式下就會打印出MOVED錯誤.
4). 節(jié)點數(shù)據(jù)庫的實現(xiàn)
? 節(jié)點只能使用0號數(shù)據(jù)庫.
? 其他保存鍵值對和設(shè)置過期時間的方式和單機(jī)數(shù)據(jù)庫完全一樣.
? 但是節(jié)點數(shù)據(jù)庫會用clusterState結(jié)構(gòu)中的slots_to_keys跳躍表來==存儲槽和鍵==的關(guān)系,其中分值表示槽號,成員是數(shù)據(jù)庫鍵.
? 這樣就可以批量的對某個節(jié)點中的一個槽進(jìn)行統(tǒng)一處理.
typedef struct clusterState{
// .....
zskiplist *slots_to_keys;
// .....
}clusterState;
(4). 重新分片
? Redis的重新分片操作可以將任意的已經(jīng)進(jìn)行了指派的槽重新指派給另一個節(jié)點,這個操作可以在線進(jìn)行.
? Redis集群的重新分派有Redis的集群管理軟件redis-trib負(fù)責(zé)執(zhí)行.Redis提供了命令,redis-trib通過向源節(jié)點和目標(biāo)節(jié)點發(fā)送命令完成.過程如下:
- redis-trib對目標(biāo)節(jié)點發(fā)送命令讓其做好準(zhǔn)備接受節(jié)點(CLUSTER SETSLOT <slot> IMPORTING <source_id>)
- redis-trib對源節(jié)點發(fā)送命令讓其做好遷移槽的準(zhǔn)備(CLUSTER SETSLOT <slot> MIGRATING <target_id>)
- redis-trib對源節(jié)點發(fā)送命令獲得==不小于一定數(shù)量的某個槽中的key==(CLUSTER GETKEYSINSLOT <slot> <count>)
- redis-trib向源節(jié)點發(fā)送命令,將3過程中被選中的鍵值對原子性的遷移到目標(biāo)節(jié)點
- 重復(fù)執(zhí)行3和4,直到全部遷移完成
- 向集群中任意一個節(jié)點發(fā)送命令,告知已經(jīng)將某個槽重新指派給了新的節(jié)點,其他的節(jié)點更新自己內(nèi)部的信息
(5). ASK錯誤
? 在重新分片時,有可能會遇到一個槽中的鍵值對只遷移了一部分的情況,這個時候,如果對這些鍵進(jìn)行訪問,集群會先在源節(jié)點中查找,如果沒有找到,則表示有可能會出現(xiàn)在目標(biāo)節(jié)點中.(也可能這個鍵不存在)
? 這時源節(jié)點會向客戶端返回一個ASK錯誤,引導(dǎo)客戶端轉(zhuǎn)向目標(biāo)節(jié)點再次發(fā)送命令.
1). CLUSTER SETSLOT IMPORTING命令的實現(xiàn)
? clusterState結(jié)構(gòu)中的importing_slots_from數(shù)組記錄了當(dāng)前節(jié)點正在從其他節(jié)點導(dǎo)入的槽
typedef struct clusterState{
// .....
clusterNode *importing_slots_from[16384];
// .....
}clusterState;
? 該數(shù)組中的元素是clusterNode指針,指向源節(jié)點
? 重新分片中這個命令就是更改目標(biāo)節(jié)點中的這個數(shù)組,從而在后面發(fā)生ASK錯誤的時候,可以分辨.
2). CLUSTER SETSLOT MIGRATING命令的實現(xiàn)
? clusterState結(jié)構(gòu)中的migrating_slots_to數(shù)組記錄了當(dāng)前節(jié)點正在遷移至其他節(jié)點的槽.
typedef struct clusterState{
// .....
clusterNode *migrating_slots_from[16384];
// .....
}clusterState;
? 這個命令更新了源節(jié)點中的這個屬性.
3). ASK錯誤
? 如果一個節(jié)點接收到一個鍵key的請求,并且這個鍵所屬的槽i正好被指派給當(dāng)前節(jié)點,那么就會嘗試在自己數(shù)據(jù)庫中查找這個key.如果找到了,就執(zhí)行客戶端發(fā)送的命令.
? 如果沒找到,那么會先檢查自己的migrating_slots_from數(shù)組中是否記錄這個槽正在被遷移.如果在遷移,那么向客戶端發(fā)送一個ASK錯誤,引導(dǎo)其去目標(biāo)節(jié)點中尋找.如果沒在被遷移,那么表示這個key不存在.
ASK <slot_id> <host>:<port>
? 接到ASK錯誤的客戶端根據(jù)努錯誤提供的ip和端口轉(zhuǎn)向目標(biāo)節(jié)點,然后先向目標(biāo)節(jié)點發(fā)送一個ASKING命令,然后重新發(fā)送原先的命令請求.
4). ASKING命令
? ASKING命令的唯一目的就是打開發(fā)送該命令客戶端的REDIS_ASKING標(biāo)識.
? 一般情況下,未完成重新分片的情況下,目標(biāo)節(jié)點是不知道自己負(fù)責(zé)這個槽的.但是當(dāng)自己的importing_slots_from數(shù)組顯示這個槽正在被導(dǎo)入自己,并且發(fā)送來命令的客戶端帶有REDIS_ASKING標(biāo)識.那么這個節(jié)點就會破例執(zhí)行關(guān)于這個沒有完成導(dǎo)入的槽的命令.
? 客戶端的REDIS_ASKING標(biāo)識使用過一次就會被清除.
5). ASK錯誤的MOVED錯誤的區(qū)別
? MOVED錯誤指這個槽的負(fù)責(zé)權(quán)已經(jīng)交給另一個節(jié)點,而ASK錯誤是指當(dāng)前節(jié)點雖然還保存這個槽的負(fù)責(zé)權(quán),但是這個槽正在被轉(zhuǎn)移,并且當(dāng)前要查找的key恰好未找到,懷疑被轉(zhuǎn)移了.
? MOVED錯誤會改變客戶端對槽的認(rèn)知,也就是說,會通知客戶端下次尋找這個槽時,直接去正確的節(jié)點.而ASK錯誤沒有這種功能,只是臨時的進(jìn)行一次處理.
(6). 復(fù)制與故障轉(zhuǎn)移
? Redis集群中的節(jié)點是分主從的,主節(jié)點負(fù)責(zé)處理槽,從節(jié)點用于復(fù)制某個主節(jié)點,并在復(fù)制的主節(jié)點下線時進(jìn)行升級代替.
1). 設(shè)置從節(jié)點
? 向一個節(jié)點發(fā)送命令:
CLUSTER REPLICATE <node_id>
? 可以讓接受命令的節(jié)點成為node_id所指定節(jié)點的從節(jié)點,并開始進(jìn)行復(fù)制.
- 接受帶這個命令的節(jié)點現(xiàn)在自己的clusterState.nodes字典中找到對應(yīng)的主節(jié)點的clusterNode,并將自己的clusterState.myself.slaveof指針指向這個結(jié)構(gòu),表示正在復(fù)制這個主節(jié)點
- 修改自己在clusterState.myself.flags中的屬性,將標(biāo)識由主節(jié)點修改為從節(jié)點
- 調(diào)用復(fù)制代碼,根據(jù)主節(jié)點的clusterNode結(jié)構(gòu)中的ip和端口進(jìn)行復(fù)制
- 然后這個節(jié)點就成為了主節(jié)點的從節(jié)點
- 這個信息會通過消息發(fā)送給集群中的其他節(jié)點,所有的節(jié)點都會在各自的內(nèi)存中的該主節(jié)點的clusterNode中進(jìn)行記錄
2). 故障檢測
? 集群中每個節(jié)點都會定期地向集群中的其他節(jié)點發(fā)送PING命令,當(dāng)其他節(jié)點一定時間內(nèi)沒有返回PONG命令之后,就會認(rèn)為這個節(jié)點疑似下線,并在clusterState的nodes字典中找到疑似下線的節(jié)點的clusterNode結(jié)構(gòu),將其的標(biāo)識位標(biāo)識為疑似下線.
? 集群中的每一個節(jié)點都會通過互相發(fā)送消息來傳遞這些信息.
? 當(dāng)A節(jié)點從別處得知節(jié)點B疑似下線時,會在自己的clusterState的nodes字典中找到節(jié)點B對應(yīng)的clusterNode結(jié)構(gòu),在其內(nèi)部添加一條下線報告(這時A節(jié)點還并不主觀認(rèn)為B節(jié)點疑似下線).
struct clusterNode{
// .....
// 一個鏈表,記錄了其他節(jié)點對該節(jié)點的下線報告
list *fail_reports;
// .....
};
// 下線報告的結(jié)構(gòu)體
struct clusterNodeFailReport{
// 報告這個節(jié)點下線的節(jié)點
struct clusterNode *node;
// 最后一次從node節(jié)點收到下線報告的時間
// 防止報告過期
mstime_t time;
};
? 如果在一個集群中,半數(shù)以上的主節(jié)點都將某個主節(jié)點x標(biāo)記為主觀疑似下線,那么這個主節(jié)點x將被標(biāo)記為==已下線==,并且進(jìn)行標(biāo)記的節(jié)點會向集群發(fā)送廣播告知整個集群這個消息.收到這個廣播的節(jié)點更改下線節(jié)點的狀態(tài)和標(biāo)識.
3). 故障轉(zhuǎn)移
? 當(dāng)一個從節(jié)點發(fā)現(xiàn)自己正復(fù)制的主節(jié)點已進(jìn)入下線狀態(tài)時,從節(jié)點將對下線主節(jié)點進(jìn)行故障轉(zhuǎn)移:
- 選舉新的主節(jié)點
- 被選中的從節(jié)點執(zhí)行SLAVEOF no one命令,成為新的主機(jī)誒單
- 新的主節(jié)點撤銷對已下線主節(jié)點的槽指派,并將這些槽指派給自己
- 新的主節(jié)點向集群廣播一條PONG消息,告知所有節(jié)點自己替代了原來的主節(jié)點,并且接管了原先的槽
- 新的主節(jié)點開始接收和處理有關(guān)于槽的命令請求.
4). 選舉新的主節(jié)點
- 集群的配置是一個自增計數(shù)器,初始為0
- 每次進(jìn)行依次故障轉(zhuǎn)移操作時,集群的配置紀(jì)元會自增1
- 對于每個配置紀(jì)元,每個主節(jié)點都有一次投票的機(jī)會.而第一個向主節(jié)點要求投票的從節(jié)點將獲得主節(jié)點的投票
- 當(dāng)從節(jié)點發(fā)現(xiàn)自己的主節(jié)點下線時,會發(fā)廣播,通知所有主節(jié)點對自己投票
- 如果一個主節(jié)點還沒進(jìn)行過投票時,收到了廣播,那么進(jìn)行相應(yīng)節(jié)點的投票
- 參與選舉的從節(jié)點通過之前廣播的回應(yīng)進(jìn)行統(tǒng)計,得到自己的票
- 當(dāng)一個節(jié)點你的票數(shù)過主節(jié)點數(shù)量的一半時,成為主節(jié)點
- 如果再一次選舉中沒有一個從節(jié)點獲得半數(shù)投票,那么進(jìn)入新的紀(jì)元,從新選舉
(7). 消息
? 集群中各個節(jié)點通過發(fā)送和接受消息來進(jìn)行通信.節(jié)點發(fā)送的消息主要有一下五種:
- MEET消息:申請加入集群的消息
- PING消息:集群例每個節(jié)點每隔一秒就會在自己已知的所有節(jié)點中隨機(jī)選出5個,然后給其中最久沒有發(fā)送過PING消息的節(jié)點發(fā)送PING消息,以此來檢測是否該節(jié)點在線.另外,如果當(dāng)前結(jié)點最后一次收到某個節(jié)點的PONG消息的時長超過了設(shè)置值的一半,就會主動向這個節(jié)點發(fā)送PING消息
- PONG消息:接收到MEET消息或者PING消息時,PONG消息可以認(rèn)為是回應(yīng).另外,一個節(jié)點可以通過向集群廣播自己的PONG消息來讓其他節(jié)點刷新對自己的認(rèn)知(主從節(jié)點身份更換)
- FAIL消息:A節(jié)點向B節(jié)點發(fā)送FAIL消息表示自己認(rèn)為C節(jié)點已經(jīng)下線,得到這個消息的節(jié)點都要更新對下線節(jié)點的認(rèn)知
- PUBLISH消息:當(dāng)一個節(jié)點收到PUBLISH命令是,節(jié)點會執(zhí)行這個命令,并向集群廣播一條PUBLISH消息,所有接收到這個消息的節(jié)點都會執(zhí)行同樣的命令
? 每個消息都由消息頭組成,消息頭又封裝了正文和發(fā)送者自身的一些信息.
1). 消息頭
typedef struct {
// 消息的長度(整個消息頭,包括正文等等)
uint32_t totlen;
// 消息的類型
uint16_t type;
// 正文包含的節(jié)點信息數(shù)量
uint16_t count;
// 發(fā)送者所處的配置紀(jì)元
uint64_t currentEpoch;
// 如果發(fā)送者是一個主節(jié)點,那么這里是發(fā)送者的配置紀(jì)元
// 如果是一個從節(jié)點,這里記錄的是所復(fù)制的主節(jié)點的配置紀(jì)元
uint64_t configEpoch;
// 發(fā)送者的名字
char sender[REDIS_CLUSTER_NAMELEN];
// 發(fā)送者目前的槽指派信息
unsigned char myslots[REDIS_CLUSTER_SLOTS/8];
// 正在復(fù)制的主節(jié)點的名字,如果當(dāng)前節(jié)點是主節(jié)點,那么為某個常量
char slaveof[REDIS_CLUSTER_NAMELEN];
// 發(fā)送者的端口
uint16_t port;
// 發(fā)送這的標(biāo)識
uint16_t flags;
// 發(fā)送者所處的集群狀態(tài)
unsigned char state;
// 消息的正文
union clusterMsgData data;
}clusterMsg;
// 正文的聯(lián)合屬性
union clusterMsgData{
// MEET,PING,PONG消息的正文
struct{
// 每條MEET,PING,PONG消息都包含兩個clusterMsgDataGossip結(jié)構(gòu)
clusterMsgDataGossip gossip[1];
}ping;
// FAIL消息的正文
struct{
clusterMasDataFil about;
}fail;
// PUBLISH消息的正文
struct{
clusterMsgDataPublish msg;
}publish;
// 其他消息的正文
}
? clusterMsg結(jié)構(gòu)的currentEpoch,sender,myslots等屬性記錄了發(fā)送者本身的節(jié)點信息,接受者根據(jù)這些信息,在自己的clusterState中的nodes字典中找到這個節(jié)點,對結(jié)構(gòu)進(jìn)行更新.
2). MEET.PING,PONG消息的實現(xiàn)
? Redis集群中的各個節(jié)點通過Gossip協(xié)議來交換各自冠以不同節(jié)點的狀態(tài)信息,其中Gossip協(xié)議有Meet,Ping,Pong三種消息實現(xiàn).
? MEET.PING,PONG三種消息使用相同的消息頭,所以通過type字段來判斷是這三種中的哪一種.
? 每次發(fā)送這三種消息時,發(fā)送這都從自己已知的列表中隨機(jī)選出兩個節(jié)點(不分主從),將這兩個節(jié)點的信息分別保存到兩個clusterMsgDataGossip結(jié)構(gòu)中,其中記錄了一系列信息
typedef struct{
// 節(jié)點名字
char nodename[REDIS_CLUSTER_NAMELEN];
// 最后一次向該節(jié)點發(fā)送PING消息的時間戳
uint32_t ping_sent;
// 最后一次從該節(jié)點接受PONG消息的時間戳
uint32_t pongReceived;
// 節(jié)點的ip
char ip[16];
// 端口
uint16_t port;
// 標(biāo)示值
uint16_t flags;
}clusterMsgDataGossip;
? 當(dāng)接到這三種命令時,接受者如果不認(rèn)識被選中的節(jié)點,那么說明與接受者進(jìn)行握手,如果認(rèn)識,對節(jié)點的信息進(jìn)行更新.
3). FAIL消息的實現(xiàn)
typedef struct{
// 記錄下線節(jié)點的名字
char nodename[REDIS_CLUSTER_NAMELEN];
}clusterMasDataFil;
4). PUBLISH消息的實現(xiàn)
? PUBLISH消息用于廣播一條命令.
? 命令格式如下:
PUBLISH <channel> <message>
? 意為向channel頻道發(fā)送消息massage,并進(jìn)行廣播,讓其他收到廣播的節(jié)點也向channel頻道發(fā)送message消息.
typedef struct{
uint32_t channel_len;
uint32_t message_len;
// 數(shù)據(jù)內(nèi)容,長度為8字節(jié)是為了對齊,實際長度不一定
// 其中的前channel_len字節(jié)保存的是channel參數(shù)
// 后面的message_len字節(jié)保存的是message參數(shù)
unsigned char bulk_data[8];
}clusterMsgDataPublish;