RedisSyncer一款通過replication協議模擬slave來獲取源Redis節點數據并寫入目標Redis從而實現數據同步的Redis同步中間件。 該項目主要包括以下子項目:
redis 同步服務引擎 redissyncer-server
redissycner 客戶端 redissyncer-cli
redis 數據校驗工具 redissycner-compare
基于docker-compse的一體化部署方案 redissyncer
本文主要介紹reidssyncer引擎(既redissyncer-server)的設計與實現,以及引擎運行的機制。
原生redis master slave 模式主要分為兩個階段,第一個階段同步rdb鏡像,也就是全量同步部分;全量同步完成后進入命令傳播模式,每個執行成功的數據變更操作會同步給slave節點。redissyncer 的模擬了這一機制并將兩部分拆解,既可以執行完整同步任務也可以單獨執行全量或增量同步。
?
建立socket
發送auth user password (6.0新增user)
OK 成功
其他 error
send->ping
返回:
ERR invalid password 密碼錯誤
NOAUTH Authentication required.沒有發送密碼
operation not permitted 操作沒權限
PONG 密碼成功
作用:
檢測主從節點之間的網絡是否可用。
檢查主從節點當前是否接受處理命令。
發送從節點端口信息
REPLCONF listening-port <port>
-->OK 成功
-->其他 失敗
發送從節點IP
REPLCONF ip-address <IP>
--> OK 成功
--> 其他 失敗
發送EOF能力(capability)
REPLCONF capa eof
--> OK 成功
--> 失敗
作用:
是否支持EOF風格的RDB傳輸,用于無盤復制,就是能夠解析出RDB文件的EOF流格式。用于無盤復制的方式中。
redis4.0支持兩種能力 EOF 和 PSYNC2
redis4.0之前版本僅支持EOF能力
發送PSYNC2能力
REPLCONF capa PSYNC2
--> OK 成功
--> 失敗
作用:
告訴master支持PSYNC2命令 , master 會忽略它不支持的能力. PSYNC2則表示支持Redis4.0最新的PSYN復制操作。
發送PSYNC
PSYNC {replid} {offset}
--> FULLRESYNC {replid} {offset} 完整同步
--> CONTINUE 部分同步
--> -ERR 主服務器低于2.8,不支持psync,從服務器需要發送sync
--> NOMASTERLINK 重試
--> LOADING 重試
--> 超過重試機制閾值宕掉任務
讀取PSYNC命令狀態,判斷是部分同步還是完整同步
PSYNC ---> 啟動heartbeat
REPLCONF ACK <replication_offset>
心跳檢測
在命令傳播階段,從服務器默認會以每秒一次的頻率
發送REPLCONF ACK命令對于主從服務器有三個作用:
作用:
檢測主從服務器的網絡連接狀態;
輔助實現min-slaves選項;
檢測命令丟失。
REPLCONF GETACK
->REPLCONF ACK <replication_offset>
rdb 鏡像同步完成后進入命令傳播,master 會不斷將變化數據推送給slave。
為了保證
RedisSyncer內部有斷點續傳、數據補償、斷線重連等機制來保證數據同步過程中穩定性和可用性,具體的機制如下。
斷點續傳機制
RedisSyncer的斷點續傳機制是基于Redis的replid和offset來實現的,RedisSyncer有兩個版本的斷點續傳機制v1和v2。
v1版本:
v1版本數據寫入到目的端redis后,將offset持久化到本地,這樣下次重啟就從上次的offset拉取。但是由于該方案寫目的端的操作和offset持久化不是一個原子的操作。如果中間發生中斷會導致數據的不一致。 例如,先寫入數據到目的端成功,后持久化offset還沒成功就發生了宕機、重啟等情況,那么再次斷點續傳拉取上一次的offset數據最后就不一致了。 ?
?
v2版本:
在v2版本策略中RedisSyncer會將每一個pipeline批次中不存在事務的的命令通過multi和exec進行包裝,并在事務尾部插入offset檢查點。 當斷點續傳時需要從目標Redis的所以db庫中查找checkpoint并找到所對應源節點當最大offset,再根據該offset進行斷點續傳。目前v2版本只支持目標為單機Redis的情況。 在v2版本中
v2命令事務封裝結構
v2 checkpoint檢查點結構:
HASH ?hset redis-syncer-checkpoint {value} {value}: ? ?* {ip}:{port}-runid ? ? {replid} ? ?* {ip}:{port}-offset ? ?{offset} ? ?* pointcheckVersion ? ? {version}
在Redis的事務機制中雖然不支持回滾,并且如果事務中間命令執行出錯后但是事務還是被執行完成,但是除特殊情況外能夠保證一致性。 在v2的機制中,為了防止'寫放大'會在目標redis的每一個邏輯庫中寫入一個checkpoint,因此在執行斷點續傳操作的時候,同步工具會先掃描目標各個邏輯庫中的checkpoint并選出里面最大offset的checkpoint作為斷點續傳的參數。 ?
數據補償機制
在數據同步過程中,存在由于網絡穩定性或其他因素導致key寫入失敗的情況,為此redissyncer實現了一套補償機制來保證源端與目的端數據的一致性。 數據補償的前提是命令寫入的冪等性,因此在RedisSyncer中會先將INCR、INCRBY、INCRBYFLOAT、APPEND、DECR、DECRBY等部分非冪等命令轉換成冪等命令后再寫入目標端Redis。 RedisSyncer在目標為單機Redis或者Proxy的時候是通過pipeline機制將數據寫入到目標Redis中的,每一個批次的pipeline的提交會返回一個結果列表, 同步工具會驗證pipeline中結果的正確性,如果部分命令寫入失敗,同步工具對該批次與該key相關的命令進行重試。 如果重試超過指定的閥值,將會宕掉任務。對于存在大key的list等非冪等結構,將不會進行數據補償,強制結束任務待人工處理。
斷線重連機制
?由于網絡抖動等原因可能會導致同步工具源端與目標端連接在同步過程中斷開,因此需要斷線重試機制來保證在任務同步的過程中如果出現異常斷開的問題。斷線重連機制存在于與源Redis節點和RedisSyncer、RedisSyncer與目標Redis節點的連接之間,兩者分別有各自的處理機制。 ?
源端重連機制
源Redis與RedisSyncer的斷線重連機制是通過記錄的offset來實現的,當因網絡異常等原因斷開了連接時,RedisSyncer會重新嘗試與源Redis節點建立連接,并通過當前任務記錄的runid、offset等信息去拉取斷開之前的增量數據,連接重新建立成功后RedisSyncer的同步任務將會無感知繼續同步。當斷線重連超過指定重試閥值或者因為offset刷過導致沒有辦法續傳數據時,RedisSyncer會宕掉當前當同步任務,等待人工干預。
目標端重連機制
RedisSyncer與目標Redis之間的斷線重連機制是通過緩存上一批次的pipeline的命令來實現的,當連接斷開異常時RedisSyncer進行重重連回放上一批次寫入失敗的命令。當回放失敗或者超過連續重試次數RedisSyncer會宕掉當前當同步任務,等待人工干預。
RedisSyncer中采用鏈式策略處理同步數據,任何一個策略返回失敗,該key都將不會被同步。鏈式策略流程如圖所示
?每一個key在RedisSyncer都會經過一個策略鏈進行處理,只要有一個策略未通過則這個key將不會同步到目標Redis,比如key過期時間的計算策略如果計算出全量階段key已過期,則將會自動拋棄該key。
策略鏈中的策略包括
類型 | 策略描述 |
---|---|
DataAnalysisStrategy | 命令統計分析 |
KeyFilterStrategy | 命令過濾 |
DbMappingStrategy | Db映射 |
TimeCalculationStrategy | 過期時間計算 |
RdbCommandSendStrategy | 全量數據寫入 |
AofCommandSendStrategy | 增量數據寫入 |
..... | ..... |
?
任務啟動流程
任務停止及清理流程
任務主動停止時,RedisSyncer會先停止源Redis端的數據寫入然后進入數據保護狀態,確??赡苓€處在RedisSyncer中未寫入目標的少部分數據能夠完整的寫入目標端,并且正確的記錄寫入的最后一條數據的offset并持久化,保證斷點續傳時RedisSyncer能夠提供正確的offset。
任務狀態
TYPE | code | description | status |
---|---|---|---|
STOP | 0 | 任務停止 | 已使用 |
CREATING | 1 | 創建中 | 已使用 |
CREATED | 2 | 創建完成 | 已使用 |
RUN | 3 | 運行狀態 | 已使用 |
BROKEN | 5 | 任務異常 | 已使用 |
RDBRUNING | 6 | 全量RDB同步過程中 | 已使用 |
COMMANDRUNING | 7 | 增量同步中 | 已使用 |
FINISH | 8 | 完成狀態 | 已使用(用于文件導入) |
任務異常處理原則
在RedisSycner任務中如果遇到可能會導致數據不一致的錯誤,RedisSyncer都會宕掉任務,等待人工干預。
rdb文件存在向前兼容問題,即高版本的rdb文件無法導入低rdb版本的Redis
跨版本遷移實現機制
對于可能存在大key的結構比如:SET,ZSET,LIST,HASH等結構:
對于其他命令如:String等結構: 為保證其命令冪等性,命令解析器會根據目標REDIS節點的RDB版本進行序列化(實現DUMP),傳輸模塊會使用REPLACE反序列化到目標節點。(其中在redis3.0以下版本REPLACE命令不支持[REPLACE])
對于對數據成員沒有順序性要求的命令如:SET,ZSET,HASH命令解析器將其解析成一個或多個sadd,zadd,hmset等命令進行處理
對于對數據成員有順序性要求的命令如:List等命令,若被命令解析器判斷為大key并將其拆分為多個子命令,此時必須保證按順序發送至目標REDIS節點
REDIS跨版本間存在的問題: 由于REDIS是向下兼容(低版本無法兼容高版本RDB),在其RDB文件協議中存在一個vesion版本號標識,REDIS在RDB導入或者全量同步執行rdbLoad時會先檢測RDB VERSION是否符合向下兼容,如果不符合則會拋出 Can’t handle RDB format version ? 錯誤。
syncer跨版本實現機制 對于全量同步RDB數據部分syncer將其分命令為兩類進行處理
RDB文件協議中關于 RDB VERSION部分
REDIS RDB文件結構開頭部分示例
----------------------------# RDB is a binary format. There are no new lines or spaces in the file.
52 45 44 49 53 # Magic String "REDIS"
30 30 30 37 # 4 digit ASCCII RDB Version Number. In this case, version = "0007" = 7 RDB VERSION字段
----------------------------
FE 00 # FE = code that indicates database selector. db number = 00
關于 RDB VERSION檢查部分偽代碼
def rdbLoad(filename):
rio = rioInitWithFile(filename);
# 設置標記:
# a. 服務器狀態:rdb_loading = 1
# b. 載入時間:loading_start_time = now_time
# c. 載入大小:loading_total_bytes = filename.size
startLoading(rio)
# 1.檢查該文件是否為RDB文件(即文件開頭前5個字符是否為"REDIS")
if !checkRDBHeader(rio):
redislog("error, Wrong signature trying to load DB from file")
return
# 2.檢查當前RDB文件版本是否兼容(向下兼容)
if !checkRDBVersion(rio):
redislog("error, Can't handle RDB format version")
return
.........
//Redis中關于RDB_VERSION檢查的代碼
rdbver = atoi(buf+5);
if (rdbver < 1 || rdbver > RDB_VERSION) {
rdbCheckError("Can't handle RDB format version %d",rdbver);
goto err;
}
?
RedisSyncer在全量同步階段在遇到LIST、SET、ZSET、HASH等結構等時候,當數據大小超過閥值后RedisSyncer會通過迭代器的形式將key拆分成多個子命令寫入目標庫。防止部分超大key一次性讀入內存導致程序產生oom并提高同步的速度。而對于不存在大key的命令同步工具會通過序列化逆序列化的形式寫入目標。
redis RDB Dump 文件格式
----------------------------# RDB is a binary format. There are no new lines or spaces in the file. 52 45 44 49 53 # Magic String "REDIS" 30 30 30 37 # 4 digit ASCCII RDB Version Number. In this case, version = "0007" = 7 ---------------------------- FE 00 # FE = code that indicates database selector. db number = 00 ----------------------------# Key-Value pair starts FD $unsigned int # FD indicates "expiry time in seconds". After that, expiry time is read as a 4 byte unsigned int $value-type # 1 byte flag indicating the type of value - set, map, sorted set etc. $string-encoded-key # The key, encoded as a redis string $encoded-value # The value. Encoding depends on $value-type ---------------------------- FC $unsigned long # FC indicates "expiry time in ms". After that, expiry time is read as a 8 byte unsigned long $value-type # 1 byte flag indicating the type of value - set, map, sorted set etc. $string-encoded-key # The key, encoded as a redis string $encoded-value # The value. Encoding depends on $value-type ---------------------------- $value-type # This key value pair doesn't have an expiry. $value_type guaranteed != to FD, FC, FE and FF $string-encoded-key $encoded-value ---------------------------- FE $length-encoding # Previous db ends, next db starts. Database number read using length encoding. ---------------------------- ... # Key value pairs for this database, additonal database FF ## End of RDB file indicator 8 byte checksum ## CRC 64 checksum of the entire file. RDB文件以魔術字符串“REDIS”開頭。 52 45 44 49 53 # "REDIS" RDB 版本號 接下來的 4 個字節存儲 rdb 格式的版本號。這 4 個字節被解釋為 ascii 字符,然后使用字符串到整數轉換轉換為整數。 00 00 00 03 # Version = 3 Database Selector 一個Redis實例可以有多個數據庫。 單個字節0xFE標記數據庫選擇器的開始。在該字節之后,一個可變長度字段指示數據庫編號。請參閱“長度編碼”部分以了解如何讀取此數據庫編號。 鍵值對 在數據庫選擇器之后,該文件包含一系列鍵值對。 za 每個鍵值對有 4 個部分 - 1.密鑰到期時間戳。 2.指示值類型的一字節標志 3.密鑰,編碼為 Redis 字符串。請參閱“Redis 字符串編碼” 4.根據值類型編碼的值。參見“Redis 值編碼”
?
附錄二 Redis RESP協議
Redis RESP協議
RESP 協議是在 Redis 1.2 中引入的,但它成為了 Redis 2.0 中與 Redis 服務器通信的標準方式。是在Redis 客戶端中實現的協議。 RESP 實際上是一種序列化協議,它支持以下數據類型:簡單字符串、錯誤、整數、批量字符串和數組。
RESP 在 Redis 中用作請求-響應協議的方式如下:
客戶端將命令作為批量字符串的 RESP 數組發送到 Redis 服務器。
服務器根據命令實現以其中一種 RESP 類型進行回復。
在 RESP 中,某些數據的類型取決于第一個字節:
對于簡單字符串,回復的第一個字節是“+”
對于錯誤,回復的第一個字節是“-”
對于整數,回復的第一個字節是“:”
對于批量字符串,回復的第一個字節是“$”
對于數組,回復的第一個字節是“ *”
RESP 能夠使用稍后指定的批量字符串或數組的特殊變體來表示 Null 值。在 RESP 中,協議的不同部分總是以“\r\n”(CRLF)終止。
'+' 字符開頭,后跟不能包含 CR 或 LF 字符(不允許換行)的字符串,以 CRLF 結尾(即“\r\n”)。如:
"+OK\r\n"
"-Error message\r\n" ?
如:
-ERR unknown command 'foobar' -WRONGTYPE Operation against a key holding the wrong kind of value
Integers只是一個 CRLF 終止的字符串,代表一個整數,以“:”字節為前綴。 例如
":0\r\n" ":1000\r\n"
用于表示長度最大為 512 MB 的單個二進制安全字符串。批量字符串按以下方式編碼:
“$”字節后跟組成字符串的字節數(前綴長度),以 CRLF 結尾。
實際的字符串數據。
最后的 CRLF。
“foobar”的編碼如下:
"$6\r\nfoobar\r\n"
當字符串為空
"$0\r\n\r\n"
Bulk Strings還可以用于表示 Null 值的特殊格式來表示值不存在。在這種特殊格式中,長度為 -1,并且沒有數據,因此 Null 表示為:
"$-1\r\n"
格式:
一個'*'字符作為第一個字節,然后是數組中元素的數量作為十進制數,然后是 CRLF。
Array 的每個元素的附加 RESP 類型。 空數組表示為:
"*0\r\n"
“foo”和“bar”的數組表示為
"*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"
["foo",nil,"bar"] (Null elements in Arrays)
*3\r\n$3\r\nfoo\r\n$-1\r\n$3\r\nbar\r\n
作者: 賈世聞 展恩強
|