閱讀929 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Redis4.0新特性(三)-PSYNC2

redis部分重新同步:是指redis因某種原因引起複製中斷後,從庫重新同步時,隻同步主實例的差異數據(寫入指令),不進行bgsave複製整個RDB文件。

本文的名詞規約:
部分重新同步:後文簡稱
psync
全量重新同步:後文簡稱fullsync
redis2.8第一版部分重新同步:後文簡稱psync1
redis4.0第二版本部分重新同步:後文簡稱psync2

在說明psync2功能前,先簡單闡述redis2.8版本發布的psync1

Redis2.8 psync1解決什麼問題

在psync1功能出現前,redis複製秒級中斷,就會觸發從實例進行fullsync。
每一次的fullsync,集群的性能和資源使用都可能帶來抖動;如果redis所處的網絡環境不穩定,那麼fullsync的出步頻率可能較高。為解決此問題,redis2.8引入psync1, 有效地解決這種複製閃斷,帶來的影響。redis的fullsync對業務而言,算是比較“重”的影響;對性能和可用性都有一定危險。

這裏列舉幾個fullsync常見的影響:

  • master需運行bgsave,出現fork(),可能造成master達到毫秒或秒級的卡頓(latest_fork_usec狀態監控);
  • redis進程fork導致Copy-On-Write內存使用消耗(後文簡稱COW),最大能導致master進程內存使用量的消耗。(eg 日誌中輸出 RDB: 5213 MB of memory used by copy-on-write)
  • redis slave load RDB過程,會導致複製線程的client output buffer增長很大;增大Master進程內存消耗;
  • redis保存RDB(不考慮disless replication),導致服務器磁盤IO和CPU(壓縮)資源消耗
  • 發送數GB的RDB文件,會導致服務器網絡出口爆增,如果千兆網卡服務器,期間會影響業務正常請求響應時間(以及其他連鎖影響)

psync1的基本實現

因為psync2是在psync1基礎上的增強實現,介紹psync2之前,簡單分析psync1的實現。

redis2.8為支持psync1,引入了replication backlog buffer(後文稱:複製積壓緩衝區);複製積壓緩衝區是redis維護的固定長度緩衝隊列(由參數repl-backlog-size設置,默認1MB),master的寫入命令在同步給slaves的同時,會在緩衝區中寫入一份(master隻有1個積壓緩衝區,所有slaves共享)。

當redis複製中斷後,slave會嚐試采用psync, 上報原master runid + 當前已同步master的offset(複製偏移量,類似mysql的binlog file和position);

如果runid與master的一致,且複製偏移量在master的複製積壓緩衝區中還有(即offset >= min(backlog值),master就認為部分重同步成功,不再進行全量同步。

部分重同步成功,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的部分同步機製,有效解決了網絡環境不穩定、redis執行高時間複雜度的命令引起的複製中斷,從而導致全量同步。但在應對slave重啟和Master故障切換的場景時,psync1還是需進行全量同步。

psync1的不足

從上文可知,psync1需2個條件同時滿足,才能成功psync: master runid不變 和複製偏移量在master複製積緩衝區中
那麼在redis slave重啟,因master runid和複製偏移量都會丟失,需進行全量重同步;redis master發生故障切換,因master runid發生了變化;故障切換後,新的slave需進行全量重同步。而slave維護性重啟、master故障切換都是redis運維常見場景,為redis的psync1是不能解決這兩類場景的成功部分重同步問題。

因此redis4.0的加強版部分重同步功能-psync2,主要解決這兩類場景的部分重新同步。

2 psync2的實現簡述

在redis cluster的實際生產運營中,實例的維護性重啟、主實例的故障切換(如cluster failover)操作都是比較常見的(如實例升級、rename command和釋放實例內存碎片等)。而在redis4.0版本前,這類維護性的處理,redis都會發生全量重新同步,導到性能敏感的服務有少量受損。
如前文所述,psync2主要讓redis在從實例重啟和主實例故障切換場景下,也能使用部分重新同步。本節主要簡述psync2在這兩種場景的邏輯實現。

名詞解釋:

  • master_replid : 複製ID1(後文簡稱:replid1),一個長度為41個字節(40個隨機串+’\0’)的字符串。redis實例都有,和runid沒有直接關聯,但和runid生成規則相同,都是由getRandomHexChars函數生成。當實例變為從實例後,自己的replid1會被主實例的replid1覆蓋。
  • master_replid2:複製ID2(後文簡稱:replid2),默認初始化為全0,用於存儲上次主實例的replid1

實例的replid信息,可通過info replication進行查看; 示例如下:

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 //這裏是主實例的replid1相同
master_replid2:0000000000000000000000000000000000000000 //未發生切換,即主實例未發生過變化,所以是初始值全"0"master_repl_offset:119750
second_repl_offset:-1

在之前的版本,redis重啟後,複製信息是完全丟失;所以從實例重啟後,隻能進行全量重新同步。

redis4.0為實現重啟後,仍可進行部分重新同步,主要做以下3點:

  • redis關閉時,把複製信息作為輔助字段(AUX Fields)存儲在RDB文件中;以實現同步信息持久化;
  • redis啟動加載RDB文件時,會把複製信息賦給相關字段;
  • redis重新同步時,會上報repl-id和repl-offset同步信息,如果和主實例匹配,且offset還在主實例的複製積壓緩衝區內,則隻進行部分重新同步。

接下來,我們詳細分析每步的簡單實現

redis關閉時,持久化複製信息到RDB

redis在關閉時,通過shutdown save,都會調用rdbSaveInfoAuxFields函數,
把當前實例的repl-id和repl-offset保存到RDB文件中。
說明:當前的RDB存儲的數據內容和複製信息是一致性的。熟悉MySQL的同學,可以認為MySQL中全量備份數和binlog信息是一致的。
rdbSaveInfoAuxFields函數實現在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;

 //把實例的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中複製信息

redis實例啟動讀取RDB文件,通過rdb.c文件中rdbLoadRio()函數實現。
redis加載RDB文件,會專門處理文件中輔助字段(AUX fields)信息,把其中repl_id和repl_offset加載到實例中,分別賦給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從實例嚐試部分重新同步

redis實例重啟後,從RDB文件中加載(注:此處不討論AOF和RDB加載優先權)master_replid和master_repl_offset;相當於實例的server.cached_master。當我們把它作為某個實例的從庫時(包含如被動的cluster slave或主動執行slaveof指令),實例向主實例上報master_replid和master_repl_offset+1;從實例同時滿足以下兩條件,就可以部分重新同步:
1.從實例上報master_replid串,與主實例的master_replid1或replid2有一個相等
2. 從實例上報的master_repl_offset+1字節,還存在於主實例的複製積壓緩衝區中

從實例嚐試部分重新同步函數slaveTryPartialResynchronization(replication.c文件中);
主實例判斷能否進行部分重新同步函數masterTryPartialResynchronization(replication.c文件中)。

redis重啟時,臨時調整主實例的複製積壓緩衝區大小

redis的複製積壓緩衝區是通過參數repl-backlog-size設置,默認1MB;為確保從實例重啟後,還能部分重新同步,需設置合理的repl-backlog-size值。
1 計算合理的repl-backlog-size值大小
通過主庫每秒增量的master複製偏移量master_repl_offset(info replication指令獲取)大小,
如每秒offset增加是5MB,那麼主實例複製積壓緩衝區要保留最近60秒寫入內容,backlog_size設置就得大於300MB(60*5)。而從實例重啟加載RDB文件是較耗時的過程,如重啟某個重實例需120秒(RDB大小和CPU配置相關),那麼主實例backlog_size就得設置至少600MB.

計算公式:backlog_size = 重啟從實例時長 * 主實例offset每秒寫入量

2 重啟從實例前,調整主實例的動態調整repl-backlog-size的值。
因為通過config set動態調整redis的repl-backlog-size時,redis會釋放當前的積壓緩衝區,重新分配一個指定大小的緩衝區。 所以我們必須在從實例重啟前,調整主實例的repl-backlog-size。
調整backlog_size處理函數resizeReplicationBacklog,代碼邏輯如下:

void resizeReplicationBacklog(long long newsize) {
 if (newsize < CONFIG_REPL_BACKLOG_MIN_SIZE) //如果設置新值小於16KB,則修改為16KB
 newsize = CONFIG_REPL_BACKLOG_MIN_SIZE;
 if (server.repl_backlog_size == newsize) return; //如果新值與原值相同,則不作任何處理,直接返回。

 server.repl_backlog_size = newsize; //修改backlog參數大小
 if (server.repl_backlog != NULL) { //當backlog內容非空時,釋放當前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內容長度和首字節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;
 }
}

為解決主實例故障切換後,重新同步新主實例數據時使用psync,而分fullsync;

1 redis4.0使用兩組replid、offset替換原來的master runid和offset.

2 redis slave默認開啟複製積壓緩衝區功能;以便slave故障切換變化master後,其他落後從可以從緩衝區中獲取寫入指令。

第一組:master_replid和master_repl_offset

如果redis是主實例,則表示為自己的replid和複製偏移量; 如果redis是從實例,則表示為自己主實例的replid1和同步主實例的複製偏移量。

第二組:master_replid2和second_repl_offset
無論主從,都表示自己上次主實例repid1和複製偏移量;用於兄弟實例或級聯複製,主庫故障切換psync.
初始化時, 前者是40個字符長度為0,後者是-1; 隻有當主實例發生故障切換時,redis把自己replid1和master_repl_offset+1分別賦值給master_replid2和second_repl_offset。
這個交換邏輯實現在函數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);
}

這樣發生主庫故障切換,以下三種常見結構,都能進行psync:

  1. 一主一從發生切換,A->B 切換變成 B->A ;
  2. 一主多從發生切換,兄弟節點變成父子節點時;
  3. 級別複製發生切換, A->B->C 切換變成 B->C->A

主實例判斷能否進行psync的邏輯函數在masterTryPartialResynchronization()

int masterTryPartialResynchronization(client *c) {

 //如果slave提供的master_replid與master的replid不同,且與master的replid2不同,或同步速度快於master; 就必須進行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;
 }

原文發布時間為:2017-11-13
本文作者:RogerZhuo
本文來自雲棲社區合作夥伴“老葉茶館”,了解相關信息可以關注“老葉茶館”微信公眾號

最後更新:2017-11-14 11:04:52

  上一篇:go  python單元測試之unittest
  下一篇:go  如絲般順滑的2017阿裏雙11黑科技曝光