摘要:1 什么是Redis部分重新同步-psync redis部分重新同步:是指redis因某種原因引起復(fù)制中斷后满着,從庫重新同步時,只同步主實(shí)例的差異數(shù)據(jù)(寫入指令)贯莺,不進(jìn)行bgsave復(fù)制整個RDB文件风喇。
1 什么是Redis部分重新同步-psync
redis部分重新同步:是指redis因某種原因引起復(fù)制中斷后,從庫重新同步時乖篷,只同步主實(shí)例的差異數(shù)據(jù)(寫入指令)响驴,不進(jìn)行bgsave復(fù)制整個RDB文件。
本文的名詞規(guī)約:
部分重新同步:后文簡稱
psync
全量重新同步:后文簡稱fullsync
redis2.8第一版部分重新同步:后文簡稱psync1
redis4.0第二版本部分重新同步:后文簡稱psync2
在說明psync2功能前撕蔼,先簡單闡述redis2.8版本發(fā)布的psync1
Redis2.8 psync1解決什么問題
在psync1功能出現(xiàn)前豁鲤,redis復(fù)制秒級中斷秽誊,就會觸發(fā)從實(shí)例進(jìn)行fullsync。
每一次的fullsync琳骡,集群的性能和資源使用都可能帶來抖動锅论;如果redis所處的網(wǎng)絡(luò)環(huán)境不穩(wěn)定,那么fullsync的出步頻率可能較高楣号。為解決此問題最易,redis2.8引入psync1, 有效地解決這種復(fù)制閃斷,帶來的影響炫狱。redis的fullsync對業(yè)務(wù)而言藻懒,算是比較“重”的影響;對性能和可用性都有一定危險视译。
這里列舉幾個fullsync常見的影響:
master需運(yùn)行bgsave,出現(xiàn)fork()嬉荆,可能造成master達(dá)到毫秒或秒級的卡頓(latest_fork_usec狀態(tài)監(jiān)控);
redis進(jìn)程fork導(dǎo)致Copy-On-Write內(nèi)存使用消耗(后文簡稱COW)酷含,最大能導(dǎo)致master進(jìn)程內(nèi)存使用量的消耗鄙早。(eg 日志中輸出 RDB: 5213 MB of memory used by copy-on-write)
redis slave load RDB過程,會導(dǎo)致復(fù)制線程的client output buffer增長很大椅亚;增大Master進(jìn)程內(nèi)存消耗限番;
redis保存RDB(不考慮disless replication),導(dǎo)致服務(wù)器磁盤IO和CPU(壓縮)資源消耗
發(fā)送數(shù)GB的RDB文件,會導(dǎo)致服務(wù)器網(wǎng)絡(luò)出口爆增,如果千兆網(wǎng)卡服務(wù)器,期間會影響業(yè)務(wù)正常請求響應(yīng)時間(以及其他連鎖影響)
psync1的基本實(shí)現(xiàn)
因?yàn)閜sync2是在psync1基礎(chǔ)上的增強(qiáng)實(shí)現(xiàn)呀舔,介紹psync2之前弥虐,簡單分析psync1的實(shí)現(xiàn)。
redis2.8為支持psync1媚赖,引入了replication backlog buffer(后文稱:復(fù)制積壓緩沖區(qū))躯舔;復(fù)制積壓緩沖區(qū)是redis維護(hù)的固定長度緩沖隊(duì)列(由參數(shù)repl-backlog-size設(shè)置,默認(rèn)1MB)省古,master的寫入命令在同步給slaves的同時,會在緩沖區(qū)中寫入一份(master只有1個積壓緩沖區(qū)丧失,所有slaves共享)豺妓。
當(dāng)redis復(fù)制中斷后,slave會嘗試采用psync, 上報原master runid + 當(dāng)前已同步master的offset(復(fù)制偏移量布讹,類似mysql的binlog file和position)琳拭;
如果runid與master的一致,且復(fù)制偏移量在master的復(fù)制積壓緩沖區(qū)中還有(即offset >= min(backlog值)描验,master就認(rèn)為部分重同步成功白嘁,不再進(jìn)行全量同步。
部分重同步成功膘流,master的日志顯示如下:
30422:M 04 Aug 14:33:48.505 * Slave xxxxx:10005 asks for synchronization
30422:M 04 Aug 14:33:48.506 * Partial resynchronization request from xxx:10005 accepted. Sending 0 bytes of backlog starting from offset 6448313.
redis2.8的部分同步機(jī)制絮缅,有效解決了網(wǎng)絡(luò)環(huán)境不穩(wěn)定鲁沥、redis執(zhí)行高時間復(fù)雜度的命令引起的復(fù)制中斷,從而導(dǎo)致全量同步耕魄。但在應(yīng)對slave重啟和Master故障切換的場景時画恰,psync1還是需進(jìn)行全量同步。
psync1的不足
從上文可知吸奴,psync1需2個條件同時滿足允扇,才能成功psync:master runid不變 和復(fù)制偏移量在master復(fù)制積緩沖區(qū)中。
那么在redis slave重啟,因master runid和復(fù)制偏移量都會丟失则奥,需進(jìn)行全量重同步考润;redis master發(fā)生故障切換,因master runid發(fā)生了變化读处;故障切換后糊治,新的slave需進(jìn)行全量重同步。而slave維護(hù)性重啟档泽、master故障切換都是redis運(yùn)維常見場景俊戳,為redis的psync1是不能解決這兩類場景的成功部分重同步問題。
因此redis4.0的加強(qiáng)版部分重同步功能-psync2馆匿,主要解決這兩類場景的部分重新同步抑胎。
2 psync2的實(shí)現(xiàn)簡述
在redis cluster的實(shí)際生產(chǎn)運(yùn)營中,實(shí)例的維護(hù)性重啟渐北、主實(shí)例的故障切換(如cluster failover)操作都是比較常見的(如實(shí)例升級阿逃、rename command和釋放實(shí)例內(nèi)存碎片等)。而在redis4.0版本前赃蛛,這類維護(hù)性的處理恃锉,redis都會發(fā)生全量重新同步,導(dǎo)到性能敏感的服務(wù)有少量受損呕臂。
如前文所述破托,psync2主要讓redis在從實(shí)例重啟和主實(shí)例故障切換場景下,也能使用部分重新同步歧蒋。本節(jié)主要簡述psync2在這兩種場景的邏輯實(shí)現(xiàn)土砂。
名詞解釋:
master_replid: 復(fù)制ID1(后文簡稱:replid1),一個長度為41個字節(jié)(40個隨機(jī)串+’\0’)的字符串谜洽。redis實(shí)例都有萝映,和runid沒有直接關(guān)聯(lián),但和runid生成規(guī)則相同阐虚,都是由getRandomHexChars函數(shù)生成序臂。當(dāng)實(shí)例變?yōu)閺膶?shí)例后,自己的replid1會被主實(shí)例的replid1覆蓋实束。
master_replid2:復(fù)制ID2(后文簡稱:replid2),默認(rèn)初始化為全0奥秆,用于存儲上次主實(shí)例的replid1
實(shí)例的replid信息逊彭,可通過info replication進(jìn)行查看; 示例如下:
127.0.0.1:6385> info replication
# Replication
role:slave
master_host:xxxx // IP模糊處理
master_port:6382
master_link_status:up
slave_repl_offset:119750master_replid:fe093add4ab71544ce6508d2e0bf1dd0b7d1c5b2 //這里是主實(shí)例的replid1相同
master_replid2:0000000000000000000000000000000000000000 //未發(fā)生切換吭练,即主實(shí)例未發(fā)生過變化诫龙,所以是初始值全"0"master_repl_offset:119750
second_repl_offset:-1
3 Redis從實(shí)例重啟的部分重新同步
在之前的版本,redis重啟后鲫咽,復(fù)制信息是完全丟失;所以從實(shí)例重啟后签赃,只能進(jìn)行全量重新同步。
redis4.0為實(shí)現(xiàn)重啟后分尸,仍可進(jìn)行部分重新同步锦聊,主要做以下3點(diǎn):
redis關(guān)閉時,把復(fù)制信息作為輔助字段(AUX Fields)存儲在RDB文件中箩绍;以實(shí)現(xiàn)同步信息持久化孔庭;
redis啟動加載RDB文件時,會把復(fù)制信息賦給相關(guān)字段材蛛;
redis重新同步時圆到,會上報repl-id和repl-offset同步信息,如果和主實(shí)例匹配卑吭,且offset還在主實(shí)例的復(fù)制積壓緩沖區(qū)內(nèi)芽淡,則只進(jìn)行部分重新同步。
接下來豆赏,我們詳細(xì)分析每步的簡單實(shí)現(xiàn)
redis關(guān)閉時挣菲,持久化復(fù)制信息到RDB
redis在關(guān)閉時,通過shutdown save,都會調(diào)用rdbSaveInfoAuxFields函數(shù)掷邦,
把當(dāng)前實(shí)例的repl-id和repl-offset保存到RDB文件中白胀。
說明:當(dāng)前的RDB存儲的數(shù)據(jù)內(nèi)容和復(fù)制信息是一致性的。熟悉MySQL的同學(xué)抚岗,可以認(rèn)為MySQL中全量備份數(shù)和binlog信息是一致的或杠。
rdbSaveInfoAuxFields函數(shù)實(shí)現(xiàn)在rdb.c源文件中,省略后代碼如下:
/* Save a few default AUX fields with information about the RDB generated. */
int rdbSaveInfoAuxFields(rio *rdb, int flags, rdbSaveInfo *rsi) {
/* Add a few fields about the state when the RDB was created. */
if (rdbSaveAuxFieldStrStr(rdb,"redis-ver",REDIS_VERSION) == -1) return -1;
//把實(shí)例的repl-id和repl-offset作為輔助字段宣蔚,存儲在RDB中
if (rdbSaveAuxFieldStrStr(rdb,"repl-id",server.replid) == -1) return -1;
if (rdbSaveAuxFieldStrInt(rdb,"repl-offset",server.master_repl_offset) == -1) return -1;
return 1;
}
生成的RDB文件廷痘,可以通過redis自帶的redis-check-rdb工具查看輔助字段信息。
其中repl兩字段信息和info中的相同件已;
$shell> /src/redis-check-rdb dump.rdb
[offset 0] Checking RDB file dump.rdb
[offset 26] AUX FIELD redis-ver = '4.0.1'[offset 133] AUX FIELD repl-id = '44873f839ae3a57572920cdaf70399672b842691'
[offset 148] AUX FIELD repl-offset = '0'[offset 167] \o/ RDB looks OK! \o/
[info] 1 keys read
[info] 0 expires
[info] 0 already expired
redis啟動讀取RDB中復(fù)制信息
redis實(shí)例啟動讀取RDB文件,通過rdb.c文件中rdbLoadRio()函數(shù)實(shí)現(xiàn)元暴。
redis加載RDB文件篷扩,會專門處理文件中輔助字段(AUX fields)信息,把其中repl_id和repl_offset加載到實(shí)例中茉盏,分別賦給master_replid和master_repl_offset兩個變量值鉴未。
以下代碼枢冤,是從RDB文件中讀取兩個輔助字段值。
int rdbLoadRio(rio *rdb, rdbSaveInfo *rsi) {
----------省略-----------
else if (!strcasecmp(auxkey->ptr,"repl-id")) {//讀取的aux字段是repl-id
if (rsi && sdslen(auxval->ptr) == CONFIG_RUN_ID_SIZE) {
memcpy(rsi->repl_id,auxval->ptr,CONFIG_RUN_ID_SIZE+1);
rsi->repl_id_is_set = 1;
}
} else if (!strcasecmp(auxkey->ptr,"repl-offset")) {
if (rsi) rsi->repl_offset = strtoll(auxval->ptr,NULL,10);
} else {
/* We ignore fields we don't understand, as by AUX field
* contract. */
serverLog(LL_DEBUG,"Unrecognized RDB AUX field: '%s'",
(char*)auxkey->ptr);
}
}
redis從實(shí)例嘗試部分重新同步
redis實(shí)例重啟后铜秆,從RDB文件中加載(注:此處不討論AOF和RDB加載優(yōu)先權(quán))master_replid和master_repl_offset淹真;相當(dāng)于實(shí)例的server.cached_master。當(dāng)我們把它作為某個實(shí)例的從庫時(包含如被動的cluster slave或主動執(zhí)行slaveof指令)连茧,實(shí)例向主實(shí)例上報master_replid和master_repl_offset+1核蘸;從實(shí)例同時滿足以下兩條件,就可以部分重新同步:
1.從實(shí)例上報master_replid串啸驯,與主實(shí)例的master_replid1或replid2有一個相等
2. 從實(shí)例上報的master_repl_offset+1字節(jié)客扎,還存在于主實(shí)例的復(fù)制積壓緩沖區(qū)中
從實(shí)例嘗試部分重新同步函數(shù)slaveTryPartialResynchronization(replication.c文件中);
主實(shí)例判斷能否進(jìn)行部分重新同步函數(shù)masterTryPartialResynchronization(replication.c文件中)罚斗。
redis重啟時徙鱼,臨時調(diào)整主實(shí)例的復(fù)制積壓緩沖區(qū)大小
redis的復(fù)制積壓緩沖區(qū)是通過參數(shù)repl-backlog-size設(shè)置,默認(rèn)1MB针姿;為確保從實(shí)例重啟后袱吆,還能部分重新同步,需設(shè)置合理的repl-backlog-size值距淫。
1 計(jì)算合理的repl-backlog-size值大小
通過主庫每秒增量的master復(fù)制偏移量master_repl_offset(info replication指令獲取)大小绞绒,
如每秒offset增加是5MB,那么主實(shí)例復(fù)制積壓緩沖區(qū)要保留最近60秒寫入內(nèi)容,backlog_size設(shè)置就得大于300MB(60*5)溉愁。而從實(shí)例重啟加載RDB文件是較耗時的過程处铛,如重啟某個重實(shí)例需120秒(RDB大小和CPU配置相關(guān)),那么主實(shí)例backlog_size就得設(shè)置至少600MB.
計(jì)算公式:backlog_size = 重啟從實(shí)例時長 * 主實(shí)例offset每秒寫入量
2 重啟從實(shí)例前拐揭,調(diào)整主實(shí)例的動態(tài)調(diào)整repl-backlog-size的值撤蟆。
因?yàn)橥ㄟ^config set動態(tài)調(diào)整redis的repl-backlog-size時,redis會釋放當(dāng)前的積壓緩沖區(qū)堂污,重新分配一個指定大小的緩沖區(qū)家肯。 所以我們必須在從實(shí)例重啟前,調(diào)整主實(shí)例的repl-backlog-size盟猖。
調(diào)整backlog_size處理函數(shù)resizeReplicationBacklog讨衣,代碼邏輯如下:
void resizeReplicationBacklog(long long newsize) {
if (newsize < CONFIG_REPL_BACKLOG_MIN_SIZE) //如果設(shè)置新值小于16KB,則修改為16KB
newsize = CONFIG_REPL_BACKLOG_MIN_SIZE;
if (server.repl_backlog_size == newsize) return; //如果新值與原值相同,則不作任何處理式镐,直接返回反镇。
server.repl_backlog_size = newsize; //修改backlog參數(shù)大小
if (server.repl_backlog != NULL) { //當(dāng)backlog內(nèi)容非空時,釋放當(dāng)前backlog娘汞,并按新值分配一個新的backlog
/* What we actually do is to flush the old buffer and realloc a new
* empty one. It will refill with new data incrementally.
* The reason is that copying a few gigabytes adds latency and even
* worse often we need to alloc additional space before freeing the
* old buffer. */
zfree(server.repl_backlog);
server.repl_backlog = zmalloc(server.repl_backlog_size);
server.repl_backlog_histlen = 0; //修改backlog內(nèi)容長度和首字節(jié)offset都為0
server.repl_backlog_idx = 0;
/* Next byte we have is... the next since the buffer is empty. */
server.repl_backlog_off = server.master_repl_offset+1;
}
}
3 psync2實(shí)現(xiàn)Redis Cluster Failover部分全新同步
為解決主實(shí)例故障切換后歹茶,重新同步新主實(shí)例數(shù)據(jù)時使用psync,而分fullsync;
1 redis4.0使用兩組replid惊豺、offset替換原來的master runid和offset.
2 redis slave默認(rèn)開啟復(fù)制積壓緩沖區(qū)功能燎孟;以便slave故障切換變化master后,其他落后從可以從緩沖區(qū)中獲取寫入指令尸昧。
第一組:master_replid和master_repl_offset
如果redis是主實(shí)例揩页,則表示為自己的replid和復(fù)制偏移量; 如果redis是從實(shí)例烹俗,則表示為自己主實(shí)例的replid1和同步主實(shí)例的復(fù)制偏移量爆侣。
第二組:master_replid2和second_repl_offset
無論主從,都表示自己上次主實(shí)例repid1和復(fù)制偏移量衷蜓;用于兄弟實(shí)例或級聯(lián)復(fù)制累提,主庫故障切換psync.
初始化時, 前者是40個字符長度為0,后者是-1磁浇; 只有當(dāng)主實(shí)例發(fā)生故障切換時斋陪,redis把自己replid1和master_repl_offset+1分別賦值給master_replid2和second_repl_offset。
這個交換邏輯實(shí)現(xiàn)在函數(shù)shiftReplicationId中置吓。
void shiftReplicationId(void) {
memcpy(server.replid2,server.replid,sizeof(server.replid)); //replid賦值給replid2
/* We set the second replid offset to the master offset + 1, since
* the slave will ask for the first byte it has not yet received, so
* we need to add one to the offset: for example if, as a slave, we are
* sure we have the same history as the master for 50 bytes, after we
* are turned into a master, we can accept a PSYNC request with offset
* 51, since the slave asking has the same history up to the 50th
* byte, and is asking for the new bytes starting at offset 51. */
server.second_replid_offset = server.master_repl_offset+1;
changeReplicationId();
serverLog(LL_WARNING,"Setting secondary replication ID to %s, valid up to offset: %lld. New replication ID is %s", server.replid2, server.second_replid_offset, server.replid);
}
這樣發(fā)生主庫故障切換无虚,以下三種常見結(jié)構(gòu),都能進(jìn)行psync:
一主一從發(fā)生切換衍锚,A->B 切換變成 B->A ;
一主多從發(fā)生切換友题,兄弟節(jié)點(diǎn)變成父子節(jié)點(diǎn)時;
級別復(fù)制發(fā)生切換戴质, A->B->C 切換變成 B->C->A
主實(shí)例判斷能否進(jìn)行psync的邏輯函數(shù)在masterTryPartialResynchronization()
int masterTryPartialResynchronization(client *c) {
//如果slave提供的master_replid與master的replid不同度宦,且與master的replid2不同,或同步速度快于master告匠; 就必須進(jìn)行fullsync.
if (strcasecmp(master_replid, server.replid) &&
(strcasecmp(master_replid, server.replid2) ||
psync_offset > server.second_replid_offset))
{
/* Run id "?" is used by slaves that want to force a full resync. */
if (master_replid[0] != '?') {
if (strcasecmp(master_replid, server.replid) &&
strcasecmp(master_replid, server.replid2))
{
serverLog(LL_NOTICE,"Partial resynchronization not accepted: "
"Replication ID mismatch (Slave asked for '%s', my "
"replication IDs are '%s' and '%s')",
master_replid, server.replid, server.replid2);
} else {
serverLog(LL_NOTICE,"Partial resynchronization not accepted: "
"Requested offset for second ID was %lld, but I can reply "
"up to %lld", psync_offset, server.second_replid_offset);
}
} else {
serverLog(LL_NOTICE,"Full resync requested by slave %s",
replicationGetSlaveName(c));
}
goto need_full_resync;
}
/* We still have the data our slave is asking for? */
if (!server.repl_backlog ||
psync_offset < server.repl_backlog_off ||
psync_offset > (server.repl_backlog_off + server.repl_backlog_histlen))
{
serverLog(LL_NOTICE,
"Unable to partial resync with slave %s for lack of backlog (Slave request was: %lld).", replicationGetSlaveName(c), psync_offset);
if (psync_offset > server.master_repl_offset) {
serverLog(LL_WARNING,
"Warning: slave %s tried to PSYNC with an offset that is greater than the master replication offset.", replicationGetSlaveName(c));
}
goto need_full_resync;
}
原文發(fā)布時間為:2017-11-13
本文作者:RogerZhuo