制全解析:全量同步、增量同步與PSYNC協(xié)議實(shí)踐)
1. 先想明白主從模式到底是在解決什么問題有一回我和同事排查一個(gè)線上告警從節(jié)點(diǎn)斷了幾十秒重連之后主節(jié)點(diǎn)的 CPU 和帶寬突然一起飆升日志里反復(fù)出現(xiàn)全量數(shù)據(jù)同步的記錄。當(dāng)時(shí)第一反應(yīng)是網(wǎng)絡(luò)出了問題但查來查去根子其實(shí)出在復(fù)制積壓緩沖區(qū)太小斷線期間的增量數(shù)據(jù)已經(jīng)保不住了所以被迫退化成一次完整的全量數(shù)據(jù)同步。那次之后我越來越覺得Redis 主從模式不難搭但要把全量數(shù)據(jù)同步和增量數(shù)據(jù)同步這兩套機(jī)制吃透才談得上在故障面前不手忙腳亂。主從模式是 Redis 高可用的地基。你可以先把它理解成一個(gè)“一個(gè)老師帶多個(gè)學(xué)生”的結(jié)構(gòu)主節(jié)點(diǎn)負(fù)責(zé)接收寫請(qǐng)求從節(jié)點(diǎn)實(shí)時(shí)跟著主節(jié)點(diǎn)的數(shù)據(jù)走平時(shí)主要承擔(dān)讀請(qǐng)求。這樣做最直接的好處有三個(gè)讀寫分離把讀流量分散到多個(gè)從節(jié)點(diǎn)上主節(jié)點(diǎn)壓力變小。數(shù)據(jù)冗余主節(jié)點(diǎn)掛了從節(jié)點(diǎn)手里還有一份完整數(shù)據(jù)能頂上。高可用基礎(chǔ)哨兵模式、集群模式底層都依賴主從復(fù)制這套數(shù)據(jù)同步機(jī)制。搭建主從的方式也很簡(jiǎn)單無非兩種。一種是配置文件中寫好replicaof指令另一種是在命令行動(dòng)態(tài)執(zhí)行# 在從節(jié)點(diǎn)上執(zhí)行把當(dāng)前實(shí)例變成 master 的從節(jié)點(diǎn) REPLICAOF 10.0.0.11 6379注意命令是REPLICAOF不是老寫法SLAVEOF。新版本里雖然SLAVEOF還能用但官方已經(jīng)不推薦了主從這兩個(gè)詞在 Redis 的運(yùn)維語義里正逐步被替換掉。很多初學(xué)者會(huì)有個(gè)疑問主從復(fù)制到底是推數(shù)據(jù)還是拉數(shù)據(jù)答案是“拉”。從節(jié)點(diǎn)主動(dòng)找主節(jié)點(diǎn)要數(shù)據(jù)主節(jié)點(diǎn)被動(dòng)響應(yīng)。這個(gè)設(shè)計(jì)很關(guān)鍵因?yàn)槿绻侵鞴?jié)點(diǎn)主動(dòng)推送主節(jié)點(diǎn)還得維護(hù)一張從節(jié)點(diǎn)清單誰掉線了、誰新加入管理起來非常麻煩。改成從節(jié)點(diǎn)主動(dòng)拉取之后主節(jié)點(diǎn)只需要在收到請(qǐng)求時(shí)干活整個(gè)鏈路的控制權(quán)就交給了從節(jié)點(diǎn)。另一個(gè)容易誤解的點(diǎn)是主從模式并不保證強(qiáng)一致性。Redis 的復(fù)制默認(rèn)是異步的主節(jié)點(diǎn)執(zhí)行完寫命令后就返回客戶端了不會(huì)等著所有從節(jié)點(diǎn)都確認(rèn)寫完。所以從節(jié)點(diǎn)上的數(shù)據(jù)一定比主節(jié)點(diǎn)“慢一點(diǎn)點(diǎn)”這個(gè)滯后是常態(tài)。理解了這一點(diǎn)你再去看全量同步和增量同步的設(shè)計(jì)就會(huì)有更清晰的主線一切努力都是為了縮小這個(gè)滯后窗口同時(shí)讓斷線重連后的追平成本盡可能低。2. 全量同步第一次握手是怎么把整份數(shù)據(jù)搬過去的2.1 初次建立復(fù)制的入口PSYNC ? -1不管是從零搭建一主一從還是從節(jié)點(diǎn)數(shù)據(jù)已經(jīng)被污染需要重建第一次建立復(fù)制時(shí)從節(jié)點(diǎn)對(duì)主節(jié)點(diǎn)說的一句話永遠(yuǎn)是PSYNC ? -1翻譯成人話就是“我對(duì)自己要復(fù)制的這段數(shù)據(jù)歷史一無所知從哪個(gè)偏移量開始追我也不知道干脆把我需要的所有數(shù)據(jù)都發(fā)給我吧。”主節(jié)點(diǎn)收到這個(gè)請(qǐng)求之后如果同意全量同步會(huì)回復(fù)FULLRESYNC replid offsetreplid是主節(jié)點(diǎn)當(dāng)前復(fù)制流的身份標(biāo)識(shí)你可以把它理解成這部“數(shù)據(jù)連續(xù)劇”的劇目編號(hào)offset是主節(jié)點(diǎn)當(dāng)前已經(jīng)寫到的字節(jié)位置相當(dāng)于播放進(jìn)度。拿到這兩個(gè)信息之后從節(jié)點(diǎn)才正式開始全量同步。這里有個(gè)值得拆解的細(xì)節(jié)為什么第一次同步必須用全量而不是把主節(jié)點(diǎn)上的每條寫命令都重新發(fā)給從節(jié)點(diǎn)執(zhí)行一遍原因很樸素——主節(jié)點(diǎn)可能已經(jīng)跑了幾個(gè)月寫操作記錄太多了如果從頭開始逐條“補(bǔ)作業(yè)”網(wǎng)絡(luò)開銷和耗時(shí)都不可控。而 RDB 快照把當(dāng)前所有數(shù)據(jù)壓縮成一個(gè)文件一次性發(fā)過去雖然過程重但終點(diǎn)明確。2.2 全量同步的完整時(shí)序BGSAVE、積壓緩沖區(qū)、清空裝載全量同步不是主節(jié)點(diǎn)一個(gè)BGSAVE然后把文件丟給從節(jié)點(diǎn)就完事兒的中間藏著好幾個(gè)容易忽略的環(huán)節(jié)。我把完整流程拆開來看從節(jié)點(diǎn)發(fā)送PSYNC ? -1主節(jié)點(diǎn)決定走全量同步返回FULLRESYNC。主節(jié)點(diǎn)執(zhí)行BGSAVE生成 RDB 快照文件。在BGSAVE執(zhí)行期間主節(jié)點(diǎn)依然對(duì)外服務(wù)所有新的寫命令會(huì)被同時(shí)記錄到復(fù)制積壓緩沖區(qū)replication backlog里。RDB 文件生成完畢主節(jié)點(diǎn)把文件發(fā)給從節(jié)點(diǎn)。從節(jié)點(diǎn)收到 RDB 之前會(huì)先把自己的舊數(shù)據(jù)清空然后加載 RDB 文件。RDB 發(fā)送結(jié)束后主節(jié)點(diǎn)再把積壓緩沖區(qū)里這段時(shí)間積攢的寫命令補(bǔ)發(fā)給從節(jié)點(diǎn)。從節(jié)點(diǎn)執(zhí)行完這些補(bǔ)發(fā)的命令主從數(shù)據(jù)追平進(jìn)入后續(xù)的增量同步階段。注意第 5 步“清空舊數(shù)據(jù)”不是在拿到 RDB 之前就做的。從節(jié)點(diǎn)會(huì)先把舊數(shù)據(jù)留著等 RDB 文件完整接收且校驗(yàn)通過后再清空再加載這樣即使 RDB 傳壞了至少從節(jié)點(diǎn)還能保留原來的數(shù)據(jù)不至于兩邊都變成空殼。這里就是全量同步最容易出性能問題的環(huán)節(jié)了。主節(jié)點(diǎn)執(zhí)行BGSAVE需要 fork 出一個(gè)子進(jìn)程來生成快照。fork 本身很快但如果實(shí)例里堆積了大量大 key子進(jìn)程在生成 RDB 時(shí)會(huì)觸發(fā)內(nèi)存頁的復(fù)制Copy-On-Write內(nèi)存和 CPU 都會(huì)有一波明顯消耗。我在生產(chǎn)環(huán)境里見過上百 GB 的實(shí)例做全量同步主節(jié)點(diǎn)的瞬時(shí) CPU 直接飆到多核打滿耗時(shí)是以分鐘計(jì)的。所以 Redis 提供了一些調(diào)節(jié)閥比如# 開啟無盤復(fù)制主節(jié)點(diǎn)不生成 RDB 文件落盤通過 socket 直接發(fā)給從節(jié)點(diǎn) config set repl-diskless-sync yes # 等待多個(gè)從節(jié)點(diǎn)一起請(qǐng)求延遲幾秒后只生成一次 RDB服務(wù)所有從節(jié)點(diǎn) config set repl-diskless-sync-delay 5無盤復(fù)制適合主節(jié)點(diǎn)磁盤 IO 比較吃緊、但網(wǎng)卡帶寬還寬裕的場(chǎng)景。如果從節(jié)點(diǎn)很多我會(huì)建議開啟repl-diskless-sync-delay讓多個(gè)從節(jié)點(diǎn)集合到一起共享一次 RDB 生成結(jié)果避免每個(gè)從節(jié)點(diǎn)各 fork 一次。2.3 從節(jié)點(diǎn)裝載 RDB 時(shí)是阻塞的別忽視從節(jié)點(diǎn)加載 RDB 文件的過程是阻塞的。也就是說加載期間從節(jié)點(diǎn)沒法對(duì)外提供讀服務(wù)INFO replication里會(huì)看到master_link_status:down同時(shí)在日志里出現(xiàn)SYNC with master in progress。這個(gè)狀態(tài)持續(xù)多久取決于 RDB 文件的大小和機(jī)器的磁盤速度。如果你的業(yè)務(wù)對(duì)從節(jié)點(diǎn)的讀可用性要求比較高可以關(guān)注一下 Redis 7 引入的repl-diskless-load配置# 可選on-empty-db / swapdb降低裝載 RDB 期間的阻塞影響 config set repl-diskless-load on-empty-db這個(gè)配置的大體思路是從節(jié)點(diǎn)先把新 RDB 數(shù)據(jù)加載到臨時(shí)內(nèi)存再切換視圖讓舊數(shù)據(jù)盡可能多服務(wù)一會(huì)兒。不過它也有自己的副作用比如臨時(shí)內(nèi)存消耗會(huì)翻倍。所以要不要開還得看你的內(nèi)存余量不能照單全收。再補(bǔ)一個(gè)容易踩的坑如果從節(jié)點(diǎn)配置了 AOF 持久化那么從節(jié)點(diǎn)在完成 RDB 裝載之后還會(huì)基于新數(shù)據(jù)集做一次 AOF 重寫。換句話說磁盤上會(huì)先出現(xiàn)短暫的“舊 AOF 和文件新內(nèi)容并存”的狀態(tài)如果你監(jiān)控的是磁盤空間這段時(shí)間要做好心理準(zhǔn)備別把磁盤告警閾值卡得太死。3. 增量同步復(fù)制偏移量、積壓緩沖區(qū)與命令傳播3.1 復(fù)制的“里程表”offset 是怎么記錄進(jìn)度的全量同步結(jié)束之后主從之間就進(jìn)入增量數(shù)據(jù)同步階段也叫命令傳播階段。這時(shí)候機(jī)制變得輕量很多主節(jié)點(diǎn)每執(zhí)行一條寫命令除了寫自己的內(nèi)存數(shù)據(jù)還會(huì)把這條命令追加到復(fù)制積壓緩沖區(qū)同時(shí)把自身的master_repl_offset往后推進(jìn)。從節(jié)點(diǎn)每收到一條命令并執(zhí)行完也會(huì)把自己這邊的slave_repl_offset往前推進(jìn)。也就是說主節(jié)點(diǎn)和從節(jié)點(diǎn)各自維護(hù)著一個(gè)“進(jìn)度條”。雖然它們都是朝著同一個(gè)方向前進(jìn)但異步復(fù)制決定了這兩個(gè)進(jìn)度條不可能完全同步一定存在一個(gè)差值這個(gè)差值就是我前面提到的數(shù)據(jù)滯后量。你可以把復(fù)制流想象成一條兩端不斷增長(zhǎng)的長(zhǎng)河offset就是河上的里程標(biāo)。主節(jié)點(diǎn)在上游從節(jié)點(diǎn)在下游河水每流一米兩邊的里程標(biāo)都會(huì)更新。這里的“河水”就是命令字節(jié)流每個(gè)字節(jié)都有自己唯一的偏移量。命令傳播階段其實(shí)就兩件事主節(jié)點(diǎn)往“河”里注入新的命令流并記錄master_repl_offset。從節(jié)點(diǎn)從“河”里消費(fèi)命令流執(zhí)行后上報(bào)自己的slave_repl_offset。從節(jié)點(diǎn)會(huì)每秒上報(bào)一次自己的offset通過REPLCONF ACK offset命令告訴主節(jié)點(diǎn)“我追到哪兒了”。主節(jié)點(diǎn)收到之后不但能算出滯后量還能判斷這個(gè)從節(jié)點(diǎn)是否還活著——如果超過timeout時(shí)間沒收到 ACK主節(jié)點(diǎn)就會(huì)認(rèn)為這個(gè)從節(jié)點(diǎn)已經(jīng)斷開了。3.2 斷線重連后主節(jié)點(diǎn)怎么決定從哪接著傳從節(jié)點(diǎn)斷線之后重連時(shí)發(fā)送的就不再是PSYNC ? -1了而是PSYNC replid offset意思變成“我?guī)е洗蔚纳矸莺瓦M(jìn)度來了你看看能不能從斷點(diǎn)接著給我傳。”主節(jié)點(diǎn)收到這個(gè)請(qǐng)求后會(huì)做兩個(gè)判斷第一這個(gè)replid是不是我當(dāng)前的身份第二這個(gè)offset是否還落在我的復(fù)制積壓緩沖區(qū)里。如果兩個(gè)條件都滿足主節(jié)點(diǎn)返回CONTINUE然后從offset對(duì)應(yīng)的位置開始把后面的命令流繼續(xù)發(fā)給從節(jié)點(diǎn)。這個(gè)過程就是增量數(shù)據(jù)同步也就是官方文檔里說的“部分重同步”partial resynchronization。代價(jià)非常小網(wǎng)絡(luò)開銷只等于斷線期間積攢的那部分命令。如果條件不滿足主節(jié)點(diǎn)返回FULLRESYNC整個(gè)流程退回全量同步。具體來說以下兩種情況最常見從節(jié)點(diǎn)發(fā)送的replid和主節(jié)點(diǎn)當(dāng)前replid對(duì)不上。從節(jié)點(diǎn)發(fā)送的offset太舊了已經(jīng)不在積壓緩沖區(qū)里主節(jié)點(diǎn)找不到那個(gè)位置的數(shù)據(jù)了。3.3 backlog 大小是增量同步的命門對(duì)象積壓緩沖區(qū)的官方配置名是repl-backlog-size默認(rèn)只有 1MB。1MB 聽著不小但放在寫流量大的主節(jié)點(diǎn)上可能不到一分鐘就被環(huán)形緩沖區(qū)覆蓋掉了。為什么說是環(huán)形緩沖區(qū)因?yàn)樗拇鎯?chǔ)區(qū)域是固定大小循環(huán)使用的。新命令不斷寫入最老的命令就被頂?shù)?。主?jié)點(diǎn)只會(huì)保留最近 N 字節(jié)的命令流。舉個(gè)例子復(fù)制積壓緩沖區(qū)當(dāng)前范圍是 offset 10000 到 11024。從節(jié)點(diǎn)重連后上報(bào) offset 10500落在這個(gè)區(qū)間內(nèi)可以增量同步。如果從節(jié)點(diǎn)上報(bào) offset 9900說明它需要的命令已經(jīng)有一部分被頂?shù)袅酥荒苋客?。這個(gè)例子能直觀看出backlog 越大能容忍的斷線時(shí)間越長(zhǎng)增量同步的成功率越高。生產(chǎn)環(huán)境里怎么定這個(gè)大小我一般會(huì)先做個(gè)粗估算repl-backlog-size 主節(jié)點(diǎn)每秒寫入量(bytes) x 預(yù)期最長(zhǎng)斷線時(shí)間(秒)比如主節(jié)點(diǎn)平均每秒產(chǎn)生 2MB 寫命令我希望從節(jié)點(diǎn)斷線 120 秒后重連還能走增量同步那就得給到 256MB 以上。注意這里算的是寫命令的字節(jié)量不是業(yè)務(wù)寫入的數(shù)據(jù)量。一條更新 100 字節(jié)字段的命令在復(fù)制流里可能也要占幾十到上百字節(jié)所以用粗略值乘個(gè) 1.5 系數(shù)更穩(wěn)妥。設(shè)置方式config set repl-backlog-size 268435456如果實(shí)例已經(jīng)運(yùn)行一段時(shí)間了Redis 會(huì)直接從當(dāng)前offset開始填充新的環(huán)形區(qū)間不需要重啟。生效后可以通過INFO replication里的repl_backlog_active和repl_backlog_histlen確認(rèn)。我踩過的一個(gè)真實(shí)教訓(xùn)是某業(yè)務(wù)從節(jié)點(diǎn)數(shù)量有七八個(gè)平時(shí)增量同步都正常但只要某個(gè)從節(jié)點(diǎn)所在的網(wǎng)絡(luò)偶爾抖動(dòng)超過一兩分鐘重連后就必然是FULLRESYNC。后來把repl-backlog-size從默認(rèn) 1MB 調(diào)到 128MB復(fù)制風(fēng)暴立刻消失。查日志的時(shí)候看到的關(guān)鍵字一般是Trying a partial resynchronization (request ...) Full resync requested前面的說明在嘗試增量后面說明失敗了、退化成全量。如果你在日志里看到這兩種狀態(tài)交替出現(xiàn)第一反應(yīng)就應(yīng)該是去看 backlog 大小而不是去懷疑網(wǎng)絡(luò)。4. PSYNC 協(xié)議演進(jìn)斷點(diǎn)續(xù)傳能力是怎樣一步步變強(qiáng)的4.1 SYNC 時(shí)代的無奈每次重連都是一次全量現(xiàn)在大家接觸 Redis 基本都是 4.0 以上版本PSYNC 已經(jīng)是默認(rèn)行為。但如果你翻過老代碼或者看過更早的運(yùn)維文檔會(huì)發(fā)現(xiàn)最初的復(fù)制協(xié)議只有一個(gè)命令SYNC。SYNC的邏輯非常簡(jiǎn)單粗暴從節(jié)點(diǎn)一發(fā)SYNC主節(jié)點(diǎn)就BGSAVE生成 RDB 發(fā)過去。在這個(gè)協(xié)議下從節(jié)點(diǎn)只要斷開重連不管斷了幾秒、缺了多少數(shù)據(jù)主節(jié)點(diǎn)都得重新生成一份完整快照再全量發(fā)一遍。那個(gè)時(shí)期的 Redis從節(jié)點(diǎn)數(shù)量稍微多一點(diǎn)只要趕上一次網(wǎng)絡(luò)抖動(dòng)就可能產(chǎn)生連鎖反應(yīng)每個(gè)從節(jié)點(diǎn)都觸發(fā)一次全量同步每個(gè)全量同步都會(huì)讓主節(jié)點(diǎn) fork 并發(fā)送 RDB主節(jié)點(diǎn) CPU、內(nèi)存、帶寬同時(shí)被打滿。可以說SYNC協(xié)議是把“簡(jiǎn)單”寫在了臉上代價(jià)全留給了運(yùn)維。4.2 PSYNC 與 replid第一代斷點(diǎn)續(xù)傳是怎么實(shí)現(xiàn)的后來 Redis 引入了PSYNC核心變化就是給復(fù)制流增加了兩個(gè)“坐標(biāo)”replid和offset。從節(jié)點(diǎn)可以在斷線重連時(shí)帶上自己上次記住的坐標(biāo)主節(jié)點(diǎn)只要發(fā)現(xiàn)自己手里還留著這段數(shù)據(jù)就能從斷點(diǎn)繼續(xù)傳不必每次都全量。這背后的實(shí)現(xiàn)就是我前面講的復(fù)制積壓緩沖區(qū)??梢哉fPSYNC協(xié)議的價(jià)值和 backlog 是綁定的沒有 backlog 就沒有斷點(diǎn)續(xù)傳。平時(shí)用INFO replication能看到主節(jié)點(diǎn)上這么一行master_replid:8e0d1f3c...這就是當(dāng)前復(fù)制流的身份 ID。只要主節(jié)點(diǎn)維護(hù)的這個(gè) ID 不變從節(jié)點(diǎn)帶著舊的replid和offset回來就有機(jī)會(huì)繼續(xù)增量同步。4.3 PSYNC2主從切換后不再強(qiáng)制全量但第一代 PSYNC 有一個(gè)明顯的硬傷一旦發(fā)生主從切換原本的從節(jié)點(diǎn)被提升為新主節(jié)點(diǎn)它自己的replid是全新的原來那批從節(jié)點(diǎn)帶著舊主節(jié)點(diǎn)的replid再連過來兩邊身份對(duì)不上統(tǒng)統(tǒng)退化為全量同步。設(shè)想一個(gè)場(chǎng)景A 是主節(jié)點(diǎn)B 和 C 是 A 的從節(jié)點(diǎn)。某天 A 宕機(jī)哨兵把 B 提升為新主節(jié)點(diǎn)。C 節(jié)點(diǎn)還在正常運(yùn)行它手里的數(shù)據(jù)就差 A 掛掉那一刻沒來得及同步的一小段而已。理論上B 和 C 都從 A 復(fù)制了一份幾乎相同的數(shù)據(jù)C 連到 B 時(shí)完全有資格做增量同步。但舊協(xié)議不認(rèn)賬C 會(huì)被迫從頭開始全量拉一次 B 的數(shù)據(jù)浪費(fèi)大量帶寬。第二代 PSYNC常被稱為 PSYNC2解決的就是這個(gè)問題。它引入了一個(gè)replid2的概念當(dāng)一個(gè)從節(jié)點(diǎn)被提升為主節(jié)點(diǎn)時(shí)它會(huì)保留自己作為從節(jié)點(diǎn)時(shí)期的主節(jié)點(diǎn)replid把它存到replid2里。C 節(jié)點(diǎn)再連過來時(shí)B 會(huì)檢查你的replid雖然跟我不一樣但和我保存的replid2匹配而且你的offset還在我的 backlog 范圍內(nèi)那就允許增量同步。這樣一來主從切換后的大部分從節(jié)點(diǎn)都能繼續(xù)走增量數(shù)據(jù)同步只有極少情況下才需要全量。這在大型集群里的收益非常明顯少了全量同步等于少了 N 次 RDB 文件傳輸主節(jié)點(diǎn)的 fork 壓力和網(wǎng)絡(luò)壓力都大大緩解??梢杂靡粡埿”戆讶鷧f(xié)議的區(qū)別說清楚能力SYNCPSYNCPSYNC2初次全量同步支持支持支持?jǐn)嗑€重連續(xù)傳不支持支持支持主從切換后續(xù)傳不支持不支持支持需要 backlog不需要需要需要5. 主從復(fù)制鏈路中的坑與排查經(jīng)驗(yàn)5.1 反反復(fù)復(fù)的全量同步先別怪網(wǎng)絡(luò)如果你的從節(jié)點(diǎn)日志里一直出現(xiàn)“先嘗試部分重同步、緊接著又變成全量同步”的狀態(tài)第一排查項(xiàng)一定是 backlog 容量。環(huán)型緩沖區(qū)的特性決定了它只認(rèn)“最近發(fā)過多少字節(jié)的命令”不認(rèn)“你斷線了多長(zhǎng)時(shí)間”。哪怕只斷了幾秒只要這幾秒內(nèi)主節(jié)點(diǎn)的寫命令字節(jié)量超過了 backlog 的上限重連照樣得全量。排查步驟也不復(fù)雜# 主節(jié)點(diǎn)上查看 backlog 實(shí)際使用情況 redis-cli info replication | grep -E backlog|offset重點(diǎn)看幾個(gè)字段master_repl_offset主節(jié)點(diǎn)當(dāng)前寫入進(jìn)度。slave_repl_offset從節(jié)點(diǎn)當(dāng)前進(jìn)度。repl_backlog_histlenbacklog 里目前保存的有效命令流長(zhǎng)度。如果你的repl_backlog_histlen已經(jīng)等于repl-backlog-size說明環(huán)形緩沖已經(jīng)被寫滿里面的內(nèi)容時(shí)刻在滾動(dòng)淘汰。這種狀態(tài)下斷線重連增量同步的成功率非常低。解決辦法也很直接把 backlog 調(diào)大同時(shí)結(jié)合業(yè)務(wù)寫流量估算出合理值。調(diào)整完不是結(jié)束還應(yīng)該盯兩輪第一輪觀察斷線重連的日志確認(rèn)出現(xiàn)的是CONTINUE而不是FULLRESYNC第二輪觀察主節(jié)點(diǎn) CPU確認(rèn)沒有因?yàn)轭l繁全量同步而持續(xù)走高。5.2 過期 Key 不會(huì)在從節(jié)點(diǎn)上主動(dòng)刪除這是個(gè)非常經(jīng)典的主從一致性細(xì)節(jié)。在主從復(fù)制中從節(jié)點(diǎn)默認(rèn)不會(huì)主動(dòng)刪除過期的 key。聽起來是不是反直覺如果從節(jié)點(diǎn)自己能判斷 key 過期直接刪掉不就行了為什么還要等主節(jié)點(diǎn)通知原因在于兩個(gè)實(shí)例的“時(shí)間感”不完全一致如果在同一秒內(nèi)主節(jié)點(diǎn)認(rèn)為 key 過期、從節(jié)點(diǎn)認(rèn)為還沒過期兩邊刪除時(shí)機(jī)不一致就會(huì)導(dǎo)致數(shù)據(jù)分叉。所以 Redis 的策略是過期刪除的決策權(quán)只在主節(jié)點(diǎn)。主節(jié)點(diǎn)的處理方式有兩類惰性刪除客戶端請(qǐng)求某個(gè) key 時(shí)發(fā)現(xiàn)已經(jīng)過期直接刪除并返回空。定期刪除后臺(tái)周期掃描一批 key刪除其中過期的。不管是哪種方式刪除主節(jié)點(diǎn)都會(huì)把刪除行為轉(zhuǎn)成一條DEL命令通過復(fù)制流轉(zhuǎn)發(fā)給從節(jié)點(diǎn)。從節(jié)點(diǎn)收到這條DEL后才會(huì)真正把本地這個(gè) key 刪掉。這里有個(gè)副作用從節(jié)點(diǎn)在收到主節(jié)點(diǎn)的刪除命令之前如果被讀到了那個(gè)邏輯上已經(jīng)過期的 key會(huì)發(fā)生什么按我的實(shí)測(cè)經(jīng)驗(yàn)Redis 從節(jié)點(diǎn)會(huì)直接把這個(gè) key 當(dāng)不存在處理返回空結(jié)果但底層數(shù)據(jù)仍然占著內(nèi)存。也就是說從節(jié)點(diǎn)會(huì)“看不到”這個(gè) key但內(nèi)存遲遲不釋放。如果你發(fā)現(xiàn)從節(jié)點(diǎn)的內(nèi)存占用長(zhǎng)時(shí)間高于預(yù)期而大 key 掃描也沒掃出什么東西可以懷疑是過期 key 堆積。解決辦法并不復(fù)雜檢查主從復(fù)制是否正常確認(rèn)沒有積壓很久的延遲因?yàn)閺墓?jié)點(diǎn)必須追平主節(jié)點(diǎn)、消化掉積壓的 DEL 命令才會(huì)把這些過期 key 清空。另外要注意從節(jié)點(diǎn)一定要開啟只讀模式config set replica-read-only yes如果有人在從節(jié)點(diǎn)上手動(dòng)寫入或修改數(shù)據(jù)主從數(shù)據(jù)立刻就會(huì)分叉而且很難追回來。這類問題往往比 backlog 問題更隱蔽因?yàn)闃I(yè)務(wù)表現(xiàn)只是一些偶發(fā)的數(shù)據(jù)“對(duì)不上”。5.3 復(fù)制風(fēng)暴與從節(jié)點(diǎn)級(jí)聯(lián)控制所謂復(fù)制風(fēng)暴是指短時(shí)間內(nèi)大量從節(jié)點(diǎn)同時(shí)觸發(fā)全量同步導(dǎo)致主節(jié)點(diǎn)資源被沖垮。典型誘因有兩種一個(gè)是主節(jié)點(diǎn)重啟或切換導(dǎo)致所有從節(jié)點(diǎn)同時(shí)掉線再重連另一個(gè)是從節(jié)點(diǎn)全網(wǎng)段重啟全部在同一時(shí)刻重連主節(jié)點(diǎn)。應(yīng)對(duì)思路有幾個(gè)我按優(yōu)先級(jí)說調(diào)大 backlog讓盡量多的斷線重連走增量同步。這是最便宜、最基礎(chǔ)的措施。錯(cuò)峰重連靠運(yùn)維工具控制從節(jié)點(diǎn)分批重啟不要一把梭。用鏈?zhǔn)綇?fù)制即主-從-從結(jié)構(gòu)。中間的從節(jié)點(diǎn)既當(dāng)從節(jié)點(diǎn)又當(dāng)下一級(jí)的主節(jié)點(diǎn)減輕頂層主節(jié)點(diǎn)的壓力。開啟無盤復(fù)制并設(shè)置 delay多個(gè)從節(jié)點(diǎn)可以共享一次 RDB 生成結(jié)果。我自己的傾向是從節(jié)點(diǎn)數(shù)量超過 5 個(gè)的架構(gòu)鏈?zhǔn)綇?fù)制是值得認(rèn)真考慮的尤其是跨機(jī)房場(chǎng)景??鐧C(jī)房帶寬本來就緊張如果每臺(tái)從節(jié)點(diǎn)都直接從主節(jié)點(diǎn)拉全量 RDB那點(diǎn)帶寬瞬間就會(huì)被吃光。中間建一層“中繼從節(jié)點(diǎn)”讓它在某個(gè)機(jī)房?jī)?nèi)再向下一層復(fù)制能顯著降低跨機(jī)房的復(fù)制流量。需要注意鏈?zhǔn)綇?fù)制會(huì)讓最下端從節(jié)點(diǎn)的數(shù)據(jù)滯后時(shí)間變大因?yàn)槊钜?jīng)過兩次轉(zhuǎn)發(fā)。這屬于典型的時(shí)間換帶寬是不是劃算得結(jié)合業(yè)務(wù)對(duì)讀取延遲的要求來權(quán)衡。5.4 平時(shí)怎么判斷主從狀態(tài)是健康的我養(yǎng)成了一個(gè)習(xí)慣每次接手一套 Redis 環(huán)境第一件事先把INFO replication的輸出完整看一遍。重點(diǎn)關(guān)注這么幾項(xiàng)role是主還是從別搞反了。master_link_status:up復(fù)制鏈路是否正常。狀態(tài)不是 up 就得馬上看日志。master_sync_in_progress:0是否正在全量同步。長(zhǎng)時(shí)間為 1 說明有大 RDB 在傳。slave_repl_offset和主節(jié)點(diǎn)的master_repl_offset兩者差值是不是在合理范圍內(nèi)長(zhǎng)期偏大就說明從節(jié)點(diǎn)追不上。master_last_io_seconds_ago最后一次和主節(jié)點(diǎn)通信是在多久之前超過timeout值就要警惕掉線。這里還有一個(gè)經(jīng)驗(yàn)之談從節(jié)點(diǎn)追不上主節(jié)點(diǎn)不一定是網(wǎng)絡(luò)慢也可能是主節(jié)點(diǎn)發(fā)給它的命令本身就重。比如一條命令操作大集合的幾百個(gè)元素復(fù)制流里這條命令的體積很大從節(jié)點(diǎn)執(zhí)行時(shí)又會(huì)觸發(fā)內(nèi)存分配和哈希表擴(kuò)容耗時(shí)遠(yuǎn)超主節(jié)點(diǎn)。這種情況下光是調(diào)大 backlog 沒用還得優(yōu)化業(yè)務(wù)側(cè)的寫命令模式比如把大集合的批量寫拆開。最后分享一條我個(gè)人反復(fù)驗(yàn)證過的經(jīng)驗(yàn)當(dāng)復(fù)制鏈路出現(xiàn)詭異問題時(shí)不要急著改配置。先把redis-cli info replication的快照保存一份再把主從兩端的日志級(jí)別臨時(shí)調(diào)到 debug復(fù)現(xiàn)一次問題把MASTER - REPLICA sync相關(guān)日志完整收集起來。八成以上的復(fù)制問題從這幾行日志里就能看出真正的走向剩下的兩成才輪到網(wǎng)絡(luò)抓包和內(nèi)核參數(shù)排查。這套笨辦法幫我在好幾次故障里省下了大量半夜時(shí)間也建議你下次遇到類似問題時(shí)先試試。