1. 程式人生 > 其它 >Redis設計與實現讀書筆記 - 複製&Sentinel&Cluster叢集

Redis設計與實現讀書筆記 - 複製&Sentinel&Cluster叢集

複製

概述

  • 複製:是將某事物通過某種方式製作成相同的一份或多份的行為(維基百科)
  • redis的複製就是讓一個伺服器(從)通過某種方式去獲得另一臺伺服器(主)的資料

Redis-2.8版本舊版複製

  • 舊版複製有同步(sync)和命令傳播(command propagate)操作
  • 怎麼保證主伺服器和從伺服器資料一致呢?那就首先主伺服器把所有的資料都給從伺服器都發一份,這樣在某個時間點資料就保證一致了。但是主伺服器肯定還會寫入啊,一寫入就又不一致了,那就主伺服器把之後寫入的那些命令實時同步給從伺服器(增量傳送),這樣又一致啦。

同步

  • SYNC命令
  • 作用:把主伺服器所有的資料都給從伺服器發一份
主從伺服器互動步驟
  • 從伺服器收到SLAVEOF命令後,會給主傳送SYNC命令
  • 主伺服器收到SYNC命令,執行BGSAVE命令,利用fork()來生成一個RDB檔案
  • 主伺服器使用一個緩衝區記錄從BGSAVE開始執行的所有寫命令
    • 生成的RDB檔案只是執行BGSAVE那個時間點的,萬一在命令執行過程中,或者傳給從伺服器之前有命令寫入,儘管把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