Redis設計與實現讀書筆記 - 複製&Sentinel&Cluster叢集
阿新 • • 發佈:2021-09-03
複製
概述
- 複製:是將某事物通過某種方式製作成相同的一份或多份的行為(維基百科)
- redis的複製就是讓一個伺服器(從)通過某種方式去獲得另一臺伺服器(主)的資料
Redis-2.8版本舊版複製
- 舊版複製有同步(sync)和命令傳播(command propagate)操作
- 怎麼保證主伺服器和從伺服器資料一致呢?那就首先主伺服器把所有的資料都給從伺服器都發一份,這樣在某個時間點資料就保證一致了。但是主伺服器肯定還會寫入啊,一寫入就又不一致了,那就主伺服器把之後寫入的那些命令實時同步給從伺服器(增量傳送),這樣又一致啦。
同步
- SYNC命令
- 作用:把主伺服器所有的資料都給從伺服器發一份
主從伺服器互動步驟
- 從伺服器收到
SLAVEOF
命令後,會給主傳送SYNC
命令 - 主伺服器收到
SYNC
命令,執行BGSAVE
命令,利用fork()來生成一個RDB檔案 - 主伺服器使用一個緩衝區記錄從
BGSAVE
開始執行的所有寫命令- 生成的RDB檔案只是執行
BGSAVE
那個時間點的,萬一在命令執行過程中,或者傳給從伺服器之前有命令寫入,儘管把RDB檔案給了從伺服器,還是不一致
- 生成的RDB檔案只是執行
- 主伺服器生成完RDB檔案,把RDB檔案給從伺服器,從伺服器載入這個RDB檔案
- 主伺服器把緩衝區的寫命令發給從伺服器,從伺服器執行這些寫命令使得保持一致
命令傳播
- 作用:主一直會有命令寫入,這些命令會造成主從不一致,所以主伺服器需要把一些會造成不一致實時的傳播給從伺服器,讓從伺服器也執行保持一致
舊版複製缺陷
- 斷線後重複製
- 主從正常執行,忽然網路抖動,從伺服器斷開了5秒,5秒後重新連上
- 由於在這斷開的5秒期間也會有資料寫入,主從不一致了,從伺服器需要採取某些措施,讓主從再次保持一致。
- 最直觀的想法,從伺服器斷開了5秒,我把丟失的5秒發給從伺服器就可以了啊。
- 當舊版的複製從斷開重連後,會向主伺服器傳送SYNC命令,前面說了SYNC命令會生成RDB,全量同步一遍,只斷了5秒,非得給我來一波全量同步,開銷極大。
新版複製
- 老版本斷線重連後,得SYNC,重新同步RDB檔案,低效!Redis2.8版本開始使用PSYNC命令代替SYNC命令
- 同樣思路還是沒變,先全量同步一次,然後增量同步。
- 全量同步在2.8叫完整重同步 , 增量同步叫部分重同步
完整重同步
- 和老版本一樣,主伺服器生成和傳送RDB檔案,傳送緩衝區中的命令,主從保持一致。
部分重同步
部分重同步功能主要由以下三個部分構成:
- 主伺服器的複製偏移量和從伺服器的複製偏移量
- 主伺服器的複製積壓緩衝區
- 伺服器的執行ID(run ID)
複製偏移量
- 主伺服器在向從伺服器傳播N個位元組的資料時,自己的複製偏移量就加上N
- 從伺服器收到主伺服器傳播來的N個位元組的資料時,就將自己的複製偏移量加上N
複製積壓緩衝區
- 一個固定長度的先進先出FIFO佇列 , 預設1MB
- 固定長度是指如果大小滿了,那麼後面的資料就會把前面的資料給頂替掉
- 將資料傳播給所有從伺服器時,還會將寫命令入隊到複製積壓緩衝區
- 緩衝區中還維護了每個位元組的偏移量
執行ID
- 每個Redis伺服器,無論是主伺服器還是從伺服器,都有自己的執行ID
- 執行ID在伺服器啟動時自動生成,由40個隨機的十六進位制字元組成。
- 當從伺服器對主伺服器進行初次複製時,主伺服器會把自己的執行ID傳給從伺服器,從伺服器會把這個執行ID儲存起來
部分重同步步驟
- 從伺服器把自己的offset給主伺服器 , 比如是從100開始斷的 , 把100告訴主
- 主伺服器已經到150了 , 主伺服器會先去看100及其之後的偏移量是不是在自己的複製積壓緩衝區內,如果在就執行部分重同步操作。如果不在就需要執行完整重同步。
- 所以根據業務場景來調整複製積壓緩衝區的大小非常重要哦 ,
repl-backlog-size
來進行調整
PSYNC命令完整步驟
- 如果從伺服器之前沒有複製過任何主伺服器,那麼從伺服器開始時會向主伺服器傳送
PSYNC ? -1
- 如果從伺服器之前複製主伺服器,那麼就傳送
PSYNC <runid> <offset>
runid就是上次儲存的主伺服器的runid , offset表示上次複製到的偏移量 - 如果主伺服器發現runid跟自己對不上,說明這個從上次複製的不是自己,主伺服器返回
+ FULLRESYNC <runid> <offset>
執行完整重同步 - 如果主伺服器發現runid對得上,看看offset及其之後的資料是否在複製積壓緩衝區內,如果在,返回
+CONTINUE
執行部分重同步 , 如果不在執行完整重同步 - 執行完整重同步,從伺服器會把主伺服器傳送過來的runid和offset儲存起來
複製的實現
客戶端執行SLAVEOF master_ip master_port
- 從伺服器收到命令後,將ip + port儲存起來,返回OK,非同步執行復制
- 從伺服器向主伺服器建立套接字連線
- 從伺服器給主伺服器傳送PING命令
- 檢查套接字讀寫狀態是否正常
- 檢查主伺服器目前是否可以正常處理命令請求
- 主伺服器如果可以正常執行,返回PONG,如果暫時沒法處理從伺服器的命令請求,就返回一個錯誤,從會斷開重連
- 身份驗證
- 從伺服器告訴主伺服器自己的埠
- 執行PSYNC命令
- 命令傳播
心跳檢測
- 在命令傳播階段,從伺服器預設每秒一次的頻率,向主伺服器傳送
Reolconf ACK offset
- 如果命令傳播丟失了,主伺服器可以根據心跳中的offset進行資料補發
Sentinel
- 主從複製存在的問題:如果主下線了,整個叢集將處於崩潰狀態。需要有個角色來幫我們及時發現有主服務區下線了,並且及時讓下面的從伺服器變為新的主伺服器,這個角色就是sentinel(哨兵)
啟動sentinel
通過命令 redis-sentinel /path/to/you/sentinel.conf
啟動
- sentinel本質是執行在特殊模式下的redis伺服器
- 啟動的時候會把一部分普通redis伺服器程式碼替換成Sentinel專用程式碼
- 啟動時需要初始化SentinelState
初始化SentinelState
初始化masters屬性
dict *master
- masters屬性記錄了所有被Sentinel監視的主伺服器資訊
- sentinel會載入使用者指定的配置檔案,將主伺服器的名字當作key , 並且建立一個 sentinelRedisInstance 結構當作value,配置檔案可以使用如下模版
- 初始化後的masters屬性
獲取主伺服器資訊
- 上一步已經初始化了masters屬性,所以知道了master的ip和port,這樣就可以建立連線了(建立兩個連線,命令連線和訂閱連線)
- sentinel預設會以每十秒一次的頻率,向主伺服器傳送info命令
- info命令中會有主的一些資訊,比如runid , 更新到對應的instance結構體中
- info命令中會有所有從伺服器的資訊,把從伺服器的資訊儲存到主伺服器的例項結構中的slves字典中 (如果存在就更新,不存在就建立)
- key:ip:port
- value:也是sentinelRedisInstance型別,不過flags會區分這個例項是主還是從
獲取從伺服器資訊
- 根據主伺服器發現了所有的從伺服器之後,sentinel會為所有的從伺服器建立連線。
- 建立連線之後,sentinel會每10秒傳送一次info命令,更新從伺服器的例項結構,比如runid
更新sentinels字典
- 主伺服器和從伺服器都建立連線後,Sentinel預設以每兩秒一次的頻率,向每個伺服器的__sentinel__:hello頻道中傳送自己本身sentinel的資訊和監視的主伺服器資訊
- sentinel同時又會訂閱__sentinel__:hello頻道 , 所以如果有sentinel1 , sentinel2 , sentinel3 同時監控一個伺服器,sentinel1往頻道傳送一條訊息,sentinel2,sentinel3是能收到這條訊息的
- sentinel收到訊息後,會先根據runid檢查是否是自己發的,如果自己發的就直接丟棄即可,如果不是自己發的,就會開始更新sentinels字典(sentinels字典位於主伺服器的例項結構中)
- sentinels字典的key是一個sentinel的ip port 格式ip:port
- sentinels字典的value就是對應的sentinel例項結構
- 首先接收到別的sentinel訊息的sentinel,會先根據傳送的主伺服器資訊,去master找到對應的主伺服器例項結構,然後去主伺服器例項結構的sentinels字典中查詢是否有sentinel例項,沒有就建立,有則更新。
- sentinel可以通過接收頻道資訊來獲知其他Sentinel的存在,監視同一個主伺服器的多個Sentinel可以自動發現對方 , 並且互相建立連線進行通訊
檢查主觀下線狀態
- 初始化之後,sentinel與監視的主伺服器,主伺服器下面的從伺服器,監視同個主伺服器的其他sentinel都建立了連線。
- Sentinel會以每秒一次的頻率向所有與它建立了連線的例項傳送PING命令
- 如果在
down-after-milliseconds
,例項連續向sentinel返回無效恢復,那麼sentinel就會把這個例項標識為主觀下線狀態- 無效回覆為除返回 +PONG , -LOADING , -MASTERDOWN三種回覆之外的其他回覆或者指定時限內沒有返回任何回覆
檢查客觀下線狀態
- sentinel向其他監控主伺服器的sentinel詢問,看看其他人是否也認為主伺服器已經進入了下線狀態
- 其他sentinel會將自己是否認為主伺服器已經下線回覆回去
- 當認為主伺服器已經下線的數量到達了配置指定的數量時,sentinel就會將這個主伺服器在自己的實力結構中標識為客觀下線狀態。
選舉領頭Sentinel
- 當一個主伺服器被判斷為客觀下線,監視這個下線主伺服器的各個Sentinel會進行協商,選舉出一個領頭Sentinel。Raft協議
故障轉移
- 領頭的Sentinel將對已經下線的主伺服器執行故障轉移操作。
- 從主伺服器屬下的從伺服器選擇一個新的做為主伺服器。
- 其他從伺服器開始複製新的主伺服器。
- 監視舊的主伺服器,如果重新上線了,就會讓它成為新的主伺服器的從伺服器。
總結
叢集
節點
- 剛開始每個節點都是相互獨立的,為了組建叢集,需要將各個獨立的節點連線起來。
- 使用
CLUSTER MEET <ip> <port>
讓各個節點連線起來,當向一個節點發送這個命令時,這個節點會跟指定的ip + port(目標節點)進行握手,握手成功後,就會讓目標節點加入自己的叢集 - 每個節點都會儲存一個clusterState結構,這個結構記錄了在當前節點的視角下,叢集目前所處的狀態,叢集包含多少個節點,叢集當前的配置紀元等
clusterState.nodes
: 叢集節點字典,key:節點的名字 value : 節點的例項(clusterNode)
CLUSTER MEET命令的實現
- 給A節點發送 cluster meet ip port , ip 和 port 為B節點
- A節點建立一個B節點的clusterNode結構,放入clusterState.nodes字典中。
- A節點和B節點建立連線,給B節點發送MEET訊息
- B節點收到MEET訊息,在自己的clusterNode.nodes字典中加入A節點
- B節點返回PONG訊息
- A節點返回一條PING命令
- 完成握手,A節點通過Gossip協議傳播給叢集中的其他節點,讓其他節點和B節點握手,互相加入對方的nodes字典中,這樣叢集中所有的節點的nodes字典都是全量的節點
槽指派
-
Redis叢集把整個資料庫分為16384個槽,每個key通過求hash取餘的方式都會落到一個槽中
-
當資料庫的16384個槽都有節點在處理的時候,叢集處於上線狀態,否則叢集處於下線狀態。
- 使用
CLUSTER ADDSLOTS
命令,將槽指定給節點
- 使用
-
給節點分配槽後
- 節點首先檢查分配的槽是不是指派過給別人了,如果已經指派給別人,直接報錯
- 初始化clusterState.slots陣列,這個陣列每個項都是一個指標, 表示這個槽由哪個節點進行負責,使用者快速定位這個槽是哪個節點負責
- 初始化clusterNode中有一個slots陣列,16384個bit,把這個陣列的槽初始化,如果1表示是自己負責的槽,如果0表示不是自己負責的槽
-
初始化自己的slots陣列之後,就需要把這個陣列傳給叢集中其他的節點,其他節點收到了這個陣列,更新culsterState.slots陣列並且去自己的clusterState.nodes找到這個節點,並且更新裡面的slots陣列
-
每個節點最終會知道哪個槽是由哪個節點負責的
叢集處理命令
- 16384個槽都進行指派之後,叢集就會進入上線狀態。
- 客戶端往叢集任意一個節點發送命令,節點會計算key對應哪個槽,如果這個槽是自己負責的就處理這個命令,如果不是自己負責的,就給客戶端發一個MOVED錯誤,指引客戶端轉向正確的節點(每個節點的clusterState中儲存了全量節點和知道每個槽該由哪個節點負責)
節點儲存資料
- 除了正常儲存之外,還會在clusterState使用跳錶維護每個槽下面有哪些key,為了方便快速批量操作某個槽下面所有的key
重新分片
- 重新分片的作用:將任意數量的槽線上(online)從某個節點遷移到另一個節點
- redis-trib會把槽遷移的資訊發給叢集中所有的節點,所有節點都會知道槽slot已經指派給新的節點了
- 使用redis-trib工具(類似於redis-cli客戶端 都是官方提供的命令列工具)
遷移一個槽的步驟,遷移多個槽就是每個槽都執行如下的步驟 - redis-trib給目標節點發送命令,讓目標節點準備好從源節點匯入slot的所有鍵值對
- redis-trib給源節點發送命令,讓源節點準備好傳送slot的所有鍵值對
- redis-trib給源節點發送命令,一次性獲得count數量的slot的key名 (直接從之前說的維護了slot和key的跳錶中取)
- 獲得多個key之後,每個key都給源節點發送migrate命令,把每個key都原子的遷移到目標節點
- 依次重複,直到slot中所有的key都遷移到目標節點
ASK錯誤
- 遷移過程中,可能一個槽的部分key在源節點裡面,另一部分在目標節點中。
- 源節點首先會在自己的資料庫中查詢key,如果找不到就傳送ASK錯誤,指引客戶端向目標節點查詢。
- 之前槽遷移的步驟中說了,redis-trib會給目標節點發送命令,讓他準備好槽匯入。收到命令後,目標節點會在clusterState中的importing_slots_form陣列中記錄當前節點正在從其他節點匯入槽
- redis-trib會給源節點發送命令,讓他準備好槽遷移。收到命令後,目標節點會在clusterState結構的migrating_slots_to陣列中記錄當前節點正在槽遷移
- 如果節點收到了一個key請求,會先去自己的資料庫中找是否存在這個key,如果沒找到,會去看這個key對應的槽是否正在遷移中,如果在遷移中,會給客戶端傳送一個ACK錯誤,引導客戶端去目標節點查詢key