制原理與Docker實戰(zhàn):全量/增量同步及故障排查)
1. 主從復(fù)制到底在解決什么問題先說結(jié)論Redis 主從復(fù)制replication本質(zhì)上是把一臺實例上的寫命令流按順序、可重放地同步到一臺或多臺實例上讓多份內(nèi)存數(shù)據(jù)保持一致。它不是什么黑魔法也不依賴共享存儲就是一套快照 命令流的組合拳。第一次接觸這個概念的人容易把它和大數(shù)據(jù)領(lǐng)域的副本機(jī)制混在一起其實 Redis 這套設(shè)計非常土味、非常直接主庫把 RDB 文件丟給從庫之后每一條寫命令都轉(zhuǎn)發(fā)一遍就這樣。這套機(jī)制撐起了三件事一是數(shù)據(jù)冗余主庫那臺機(jī)器磁盤掛了、內(nèi)存炸了從庫還有一份完整數(shù)據(jù)二是讀寫分離把讀流量分到從庫上主庫專心寫QPS 能拉高好幾倍三是高可用地基沒有復(fù)制就沒有哨兵Sentinel和 Cluster 的故障自動切換所有主掛了自動頂上去的承諾都是空話。所以別看主從復(fù)制自己不帶自動故障轉(zhuǎn)移它卻是整個 Redis 高可用體系里最底層的那塊磚。這篇文章面向的讀者很具體你可能是剛學(xué)完redis 數(shù)據(jù)類型、裝完redis desktop manager、跑通了一個單機(jī)實例接下來想搞清楚為什么生產(chǎn)環(huán)境非要搞兩臺以上也可能是被redis面試題里的說說主從復(fù)制的原理問懵了只會背全量同步、增量同步六個字。我會從協(xié)議交互細(xì)節(jié)講到 Docker 里搭三節(jié)點的完整操作再把你線上大概率會遇到的坑攤開講??赐昴阒辽倌茏龅絻杉伦约耗艽钜惶啄苡玫闹鲝囊约翱吹絤aster_link_status:down時知道往哪個方向查。1.1 單機(jī) Redis 的三道坎單機(jī) Redis 的問題不在于性能不夠而在于它把所有的賭注都押在一臺機(jī)器和一個進(jìn)程上。第一道坎是容量Redis 是內(nèi)存數(shù)據(jù)庫單機(jī)內(nèi)存受物理內(nèi)存限制一臺 64G 的機(jī)器你撐死給它 40G 左右的數(shù)據(jù)量再往上 fork 和 RDB 就會很難受。第二道坎是吞吐單線程模型下所有命令排隊執(zhí)行讀多寫少的場景里大量 CPU 時間浪費在無意義的讀操作上。第三道坎最要命是可用性進(jìn)程 OOM 被殺、機(jī)器斷電、內(nèi)核 panic任何一個環(huán)節(jié)出問題服務(wù)就直接不可用而且數(shù)據(jù)可能只剩上次 RDB 或 AOF 落盤的快照。主從復(fù)制對這三道坎的回應(yīng)是分別的容量問題它解決不了每臺從庫都是全量副本不是分片吞吐問題它能解決一大半讀請求分散到 N 臺從庫理論讀能力乘以 N可用性問題它提供了有人能接班的前提但接班的動作得靠哨兵或者你自己寫腳本。我見過不少團(tuán)隊在項目初期直接上 Cluster結(jié)果發(fā)現(xiàn)數(shù)據(jù)量才 5G、QPS 才兩千白白背上了分片帶來的運維復(fù)雜度。這種場景老老實實一主兩從加哨兵省事得多。順帶說一句很多人會搞混的主從不是備份。從庫上執(zhí)行的FLUSHALL會被原樣同步到所有從庫主庫誤刪數(shù)據(jù)從庫跟著一起沒。真正的備份是定期把 RDB 文件拷到另一臺不參與復(fù)制的機(jī)器或?qū)ο蟠鎯ι线@件事別偷懶。1.2 復(fù)制、哨兵、集群的分工邊界剛上手的人最容易把這三個概念攪成一鍋粥。我習(xí)慣這樣給同事解釋復(fù)制Replication負(fù)責(zé)數(shù)據(jù)有幾份只管同步不管誰死誰活。哨兵Sentinel負(fù)責(zé)誰掛了誰頂上它自己也是一組進(jìn)程監(jiān)控主從節(jié)點主庫失聯(lián)后選一個從庫提升為新主然后通知其他從庫改跟隨。集群Cluster負(fù)責(zé)數(shù)據(jù)怎么分片把 16384 個哈希槽分給多個主節(jié)點每個主節(jié)點下面還能掛從庫做冗余。三者的關(guān)系是層層疊加的Cluster 內(nèi)部也用復(fù)制來實現(xiàn)主從冗余Sentinel 完全建立在復(fù)制之上。所以你把復(fù)制原理吃透后面兩個都是順藤摸瓜。提示不要用 Sentinel 去管理 Cluster 節(jié)點也不要在 Cluster 模式下用REPLICAOF手動改拓?fù)銫luster 的槽位和復(fù)制關(guān)系由集群自己維護(hù)手動干預(yù)極容易搞出數(shù)據(jù)不一致。還有一點值得提前說主從復(fù)制是異步的。主庫執(zhí)行完寫命令、返回客戶端成功然后才把命令發(fā)給從庫。這意味著主庫在返回成功到命令真正到達(dá)從庫之間存在一個時間窗口如果此時主庫宕機(jī)且沒有做持久化這條數(shù)據(jù)就永遠(yuǎn)丟了。這不是 Redis 的缺陷是異步復(fù)制的固有代價理解了這一點你才能看懂后面講的WAIT命令和min-replicas-to-write參數(shù)到底在防什么。2. 復(fù)制鏈路是怎么建立的從 REPLICAOF 到 PSYNC討論協(xié)議之前先約定叫法。Redis 5.0 之前命令叫SLAVEOF節(jié)點叫 slave5.0 之后官方全面改用REPLICAOF和 replica老命令還保留兼容。但你在INFO replication的輸出里看到的字段名依然是slave0:、slave1:這是歷史包袱別以為是版本裝錯了。下面我統(tǒng)一用從庫/replica。整條鏈路建立起來分兩大階段握手建連和數(shù)據(jù)同步。數(shù)據(jù)同步里又分全量同步full resynchronization和增量同步partial resynchronization具體走哪條路由從庫斷線前記住的 offset 決定。2.1 建連與握手PING、AUTH、REPLCONF從庫在配置里寫了replicaof 192.168.1.10 6379之后或者運行時執(zhí)行了這條命令會立刻做這幾件事順序很固定保存主庫的 IP 和端口建立 TCP 連接。發(fā)送PING期待主庫回PONG。如果超時或者回的是別的說明網(wǎng)絡(luò)或端口有問題重連。如果主庫配了requirepass從庫必須配masterauth它會發(fā)AUTH password認(rèn)證。從庫發(fā)送自己的監(jiān)聽端口REPLCONF listening-port 6380主庫記錄下來INFO replication里顯示的port就是這個值。Redis 4.0 之后還會發(fā)REPLCONF capa eof capa psync2告訴主庫自己支持無盤傳輸?shù)?EOF 標(biāo)記和 PSYNC2 協(xié)議。這幾個步驟看著很啰嗦但每一步都有存在的理由。REPLCONF listening-port尤其關(guān)鍵——主庫需要通過這個端口去連從庫如果你在 Docker 或者 NAT 環(huán)境里沒把端口映射對主庫記下來的是容器內(nèi)端口外部根本連不上后面做故障轉(zhuǎn)移時哨兵會一臉懵。我踩過這個坑容器里從庫上報listening-port 6379主庫和哨兵都按 6379 去連結(jié)果連到的是主庫自己。握手完成后進(jìn)入真正的同步協(xié)商從庫發(fā)PSYNC replid offset第一次連接時是PSYNC ? -1意思是我沒有歷史給我全量。主庫收到后有兩種回復(fù)FULLRESYNC replid offset走全量同步replid是主庫當(dāng)前復(fù)制流的 ID40 位十六進(jìn)制字符串offset是主庫當(dāng)前的復(fù)制偏移量。CONTINUE replid走增量同步只需要把斷線期間丟失的那部分命令補(bǔ)發(fā)。2.2 全量同步RDB 快照與復(fù)制緩沖區(qū)全量同步是整個流程里最重的環(huán)節(jié)也是最容易出問題的地方。主庫收到PSYNC ? -1之后的動作是執(zhí)行BGSAVE或者BGREWRITEAOF如果 AOF 正在重寫會等它結(jié)束生成 RDB 快照文件。同時開啟一個復(fù)制緩沖區(qū)replication buffer把 BGSAVE 期間新進(jìn)來的所有寫命令都追加進(jìn)去。RDB 生成完畢后把文件通過網(wǎng)絡(luò)發(fā)給從庫。從庫收到后先落盤再清空自己的舊數(shù)據(jù)然后載入 RDB。載入完成后主庫把復(fù)制緩沖區(qū)里堆積的命令發(fā)過去從庫執(zhí)行至此雙方數(shù)據(jù)追平進(jìn)入穩(wěn)定的命令傳播階段。第二步是很多人忽略的細(xì)節(jié)BGSAVE 可能要跑幾秒甚至幾十秒這期間主庫不能停止服務(wù)新寫入的命令必須先緩存起來否則從庫拿到的就是一份過期快照。這部分緩存占用的就是復(fù)制緩沖區(qū)內(nèi)存如果從庫接收 RDB 的速度太慢緩沖區(qū)會一直漲漲到超過client-output-buffer-limit replica的限制主庫會強(qiáng)制斷開這個從庫然后從庫重連、再來一次全量同步形成惡性循環(huán)。RDB 的傳輸方式在 Redis 7.x 里默認(rèn)變成了無盤模式repl-diskless-sync yes。有盤模式是主庫先把 RDB 寫到磁盤再讀文件發(fā)送無盤模式是 BGSAVE 的子進(jìn)程直接把 RDB 字節(jié)流寫進(jìn) socket省掉一次磁盤 IO。無盤模式對大內(nèi)存實例特別友好因為省下了幾 GB 的臨時磁盤文件但也有代價它會讓主庫多開一個 socket 連接repl-diskless-sync-delay默認(rèn) 5 秒是為了等更多從庫一起來好讓一次 RDB 喂給多個從庫——這個延遲在小規(guī)模場景下會讓人誤以為從庫怎么半天不同步。從庫載入 RDB 期間是阻塞的這期間的讀請求會被掛起。如果你用的是repl-diskless-load swapdb從庫會先把數(shù)據(jù)加載到一塊臨時空間加載完再原子替換減少阻塞時間但會額外占用一份內(nèi)存。生產(chǎn)環(huán)境里從庫的規(guī)格不要比主庫小太多否則加載 RDB 那幾分鐘你的讀流量會全部超時。2.3 增量同步replid 與 offset 的賬本增量同步是 Redis 2.8 引入 PSYNC 之后才有的能力在這之前斷線重連只能全量非常痛苦。它的核心是兩個變量replid和offset。replid主庫的復(fù)制流標(biāo)識。主庫每次重啟或者被提升為新主都會重新生成一個 replid從庫看到 replid 變了就知道換人了賬本作廢。offset復(fù)制流的字節(jié)偏移量。主庫每傳播 N 個字節(jié)的命令offset 就加 N從庫每收到 N 字節(jié)自己的 offset 也加 N。兩邊一對比就知道差多少數(shù)據(jù)。光有兩個變量還不夠主庫還得把最近發(fā)出去的這些字節(jié)存著這就是復(fù)制積壓緩沖區(qū)repl backlog由repl-backlog-size控制默認(rèn)只有 1MB注意單位是 MB 不是 GB。從庫斷線重連時發(fā)PSYNC replid offset主庫檢查replid 對得上并且 offset 之差小于 backlog 里現(xiàn)存的數(shù)據(jù)量回CONTINUE只補(bǔ)發(fā)差值部分。其他情況一律FULLRESYNC全量重來。所以repl-backlog-size這個參數(shù)直接決定了從庫短暫抖動一下需不需要全量重同步。默認(rèn) 1MB 在高寫入場景下幾乎等于沒有網(wǎng)絡(luò)抖兩秒就撐爆了。這也是后面第 3 章我要專門算一遍的原因。Redis 4.0 把協(xié)議升級到了 PSYNC2解決了主從切換后無法增量同步的老問題。老版本里主庫一重啟replid 變了所有從庫必須全量PSYNC2 引入了replid2和second_replid_offset讓從庫在新主庫上仍有機(jī)會走增量。這個改進(jìn)在哨兵故障轉(zhuǎn)移時價值巨大因為轉(zhuǎn)移過程中最怕的就是新主剛上來所有從庫排隊全量同步把新主壓垮。2.4 心跳、超時與延遲判定數(shù)據(jù)同步完之后鏈路不會閑著雙方一直在對暗號靠的就是心跳主庫每隔repl-ping-replica-period默認(rèn) 10 秒給從庫發(fā)一個PING確認(rèn)連接還活著。從庫每秒給主庫發(fā)一次REPLCONF ACK offset匯報自己處理到哪兒了。主庫如果repl-timeout默認(rèn) 60 秒內(nèi)沒收到從庫的 ACK就判定從庫下線斷開連接從庫同理超時沒收到主庫數(shù)據(jù)就認(rèn)為主庫掛了master_link_status變成down。這里有個很隱蔽的坑repl-timeout管的東西不止心跳還包括 RDB 傳輸、命令傳播等多個階段的超時。如果你的實例內(nèi)存很大、RDB 有幾 GB在慢網(wǎng)絡(luò)下傳 60 秒根本傳不完主庫會誤判超時然后反復(fù)斷開重傳永遠(yuǎn)同步不上。這種情況不是加大repl-timeout就完事的更該做的是排查網(wǎng)絡(luò)帶寬、考慮換無盤模式或者干脆在從庫本地做一份 RDB 冷啟動。INFO replication里的lag字段是從庫視角看的延遲含義是距離上次收到主庫數(shù)據(jù)過了幾秒正常值應(yīng)該是 0 或 1。如果它持續(xù)大于 3說明鏈路有壓力。更精確的延遲要用 offset 差值來算master_repl_offset - slave_repl_offset這個差值乘以平均命令字節(jié)數(shù)大致就是落后的數(shù)據(jù)量。做監(jiān)控的時候我一般兩個都采lag 反映的是卡不卡offset 差反映的是差多少。3. Docker 三節(jié)點主從實操從零到跑通紙上談兵沒意思這一章我們把一主兩從在本地 Docker 里完整搭一遍。選 Docker 不是因為它是唯一方案而是因為它能讓你在五分鐘內(nèi)擁有一套可反復(fù)銷毀重建的環(huán)境練習(xí)故障場景特別方便。docker安裝redis主從這個關(guān)鍵詞能搜出一堆教程但大多數(shù)只告訴你跑起來不告訴你為什么這么配也不告訴你哪些參數(shù)上線前必須改。3.1 環(huán)境準(zhǔn)備與配置文件先拉鏡像用 7.2 這個版本穩(wěn)定且默認(rèn)參數(shù)比較現(xiàn)代docker pull redis:7.2然后準(zhǔn)備三個配置文件放在./conf下分別是master.conf、replica1.conf、replica2.conf。為什么不直接用docker run redis-server --replicaof ...這種命令行傳參因為生產(chǎn)里你要配的東西遠(yuǎn)不止一個replicaof用配置文件能讓你把參數(shù)固化下來也方便版本管理。master.confport 6379 bind 0.0.0.0 protected-mode no appendonly yes appendfsync everysec requirepass redis123456 masterauth redis123456 repl-backlog-size 64mb repl-backlog-ttl 7200 min-replicas-to-write 1 min-replicas-max-lag 10replica1.confport 6380 bind 0.0.0.0 protected-mode no appendonly yes appendfsync everysec requirepass redis123456 masterauth redis123456 replicaof redis-master 6379 replica-read-only yes replica-serve-stale-data yes replica-priority 100 repl-backlog-size 64mb幾個配置項值得單獨解釋。masterauth從庫必須配不然主庫有密碼時握手直接失敗報NOAUTH Authentication required這個錯誤在日志里一點都不明顯很多人查半天以為是網(wǎng)絡(luò)問題。protected-mode no只在容器這種受控網(wǎng)絡(luò)里開物理機(jī)上千萬別這么干。replica-read-only yes是默認(rèn)值但顯式寫出來提醒自己從庫默認(rèn)拒絕寫命令這是保護(hù)機(jī)制不是安全邊界有CONFIG SET權(quán)限的人照樣能關(guān)掉它。min-replicas-to-write 1和min-replicas-max-lag 10這兩個參數(shù)是主庫側(cè)的保險絲意思是至少有 1 個從庫、且它的延遲不超過 10 秒我才接受寫請求否則直接報錯。它的作用是避免主庫網(wǎng)絡(luò)分區(qū)變成孤島之后還在傻傻接寫等網(wǎng)絡(luò)恢復(fù)把臟數(shù)據(jù)同步下去。注意這兩個參數(shù)一開從庫全部掉線時主庫會直接拒寫業(yè)務(wù)會報錯。上線前一定要和業(yè)務(wù)方確認(rèn)這個行為可接受或者把它當(dāng)成降級開關(guān)通過配置中心動態(tài)調(diào)整。3.2 啟動編排與狀態(tài)驗證用 docker-compose 一把梭docker-compose.yml內(nèi)容如下version: 3.8 services: redis-master: image: redis:7.2 container_name: redis-master ports: - 6379:6379 volumes: - ./conf/master.conf:/usr/local/etc/redis/redis.conf - ./data/master:/data command: [redis-server, /usr/local/etc/redis/redis.conf] redis-replica1: image: redis:7.2 container_name: redis-replica1 ports: - 6380:6380 volumes: - ./conf/replica1.conf:/usr/local/etc/redis/redis.conf - ./data/replica1:/data depends_on: - redis-master command: [redis-server, /usr/local/etc/redis/redis.conf] redis-replica2: image: redis:7.2 container_name: redis-replica2 ports: - 6381:6381 volumes: - ./conf/replica2.conf:/usr/local/etc/redis/redis.conf - ./data/replica2:/data depends_on: - redis-master command: [redis-server, /usr/local/etc/redis/redis.conf]replica2.conf把 port 換成 6381、replicaof指向同一個主庫即可內(nèi)容我就不重復(fù)貼了。啟動docker compose up -d docker compose ps三個容器都 Up 之后開始驗證。先看主庫docker exec -it redis-master redis-cli -a redis123456 info replication期望看到role:master connected_slaves:2 slave0:ip172.20.0.3,port6380,stateonline,offset1234,lag0 slave1:ip172.20.0.4,port6381,stateonline,offset1234,lag0 master_failover_state:no-failover master_replid:8f3a1c... master_repl_offset:1234stateonline是關(guān)鍵如果是statewait_bgsave說明 RDB 還在生成等幾秒再刷如果connected_slaves:0八成是masterauth沒配對或者網(wǎng)絡(luò)不通。再看從庫docker exec -it redis-replica1 redis-cli -a redis123456 info replicationrole:slave、master_link_status:up、master_last_io_seconds_ago:0這三項對了就說明鏈路健康。寫條數(shù)據(jù)驗證同步docker exec -it redis-master redis-cli -a redis123456 set hello from-master docker exec -it redis-replica1 redis-cli -a redis123456 get hello docker exec -it redis-replica2 redis-cli -a redis123456 get hello兩個從庫都能讀到from-master最基礎(chǔ)的主從就通了。這時候你再試試從庫寫入docker exec -it redis-replica1 redis-cli -a redis123456 set test 1 # (error) READONLY You cant write against a read only replica.這個報錯是對的說明replica-read-only生效了。3.3 斷線重連與手動切換演練搭完不練等于白搭我建議至少做三個演練。第一個是驗證增量同步。先把某個從庫的網(wǎng)絡(luò)斷掉模擬短暫抖動docker network disconnect redis_default redis-replica1 docker exec -it redis-master redis-cli -a redis123456 set k1 v1 docker exec -it redis-master redis-cli -a redis123456 set k2 v2 docker network connect redis_default redis-replica1Docker 的 bridge 網(wǎng)絡(luò)名一般是目錄名_default用docker network ls查一下。重連后立刻看從庫的INFO replication重點看sync_full和sync_partial_ok兩個計數(shù)器docker exec -it redis-replica1 redis-cli -a redis123456 info stats | grep sync # sync_full:0 # sync_partial_ok:1sync_full:0且sync_partial_ok:1說明走的是增量同步backlog 起作用了。如果你看到sync_full漲了那就是斷線時間太長或者 backlog 太小差值已經(jīng)超出緩沖區(qū)范圍。第二個演練是手動故障轉(zhuǎn)移。假設(shè)主庫真的掛了我們手動把 replica1 提升為新主docker stop redis-master docker exec -it redis-replica1 redis-cli -a redis123456 replicaof no one docker exec -it redis-replica1 redis-cli -a redis123456 info replication # role:master然后讓 replica2 改跟新主docker exec -it redis-replica2 redis-cli -a redis123456 replicaof redis-replica1 6380注意這一步replicaof運行時執(zhí)行不會落盤到配置文件容器重啟就丟了。生產(chǎn)里做這類變更必須同步改配置文件。第三個演練是觀察全量同步的開銷。刪掉一個從庫的 data 目錄再重啟讓它從頭全量同步同時盯著主庫的INFO stats里的rdb_bgsave_in_progress和latest_fork_usec。你會發(fā)現(xiàn) fork 那一瞬間主庫是阻塞的latest_fork_usec會飆到幾百微秒甚至毫秒級內(nèi)存越大越明顯。這就是為什么我前面強(qiáng)調(diào)從庫重建要錯峰別三個從庫同時重啟。3.4 關(guān)鍵參數(shù)的量級估算前面反復(fù)提到repl-backlog-size默認(rèn) 1MB 太小現(xiàn)在來算一遍該怎么定。邏輯很簡單backlog 容量 主庫寫入速率 × 可容忍的最長斷線時間舉個數(shù)主庫 QPS 5000其中寫命令占 40% 也就是 2000 條/秒平均每條命令含命令名、參數(shù)、協(xié)議頭約 200 字節(jié)那么寫入速率約 400 KB/s。假設(shè)你希望從庫斷線 60 秒內(nèi)還能增量恢復(fù)需要的容量是 400 KB/s × 60 s 24 MB??紤]到協(xié)議開銷的波動和突發(fā)流量取 2 到 3 倍余量配 64MB 比較穩(wěn)。這不是拍腦袋是按峰值估算再留冗余。如果你不確定寫入速率可以用這個命令粗算redis-cli -a redis123456 info stats | grep instantaneous_input_kbpsinstantaneous_input_kbps是最近一次采樣周期內(nèi)的入站速率采樣周期由INFO調(diào)用間隔決定。它只反映瞬時值做容量規(guī)劃時要抓業(yè)務(wù)高峰期的數(shù)據(jù)多采幾次取最大值。其他幾個參數(shù)的經(jīng)驗值我整理成表方便你直接抄參數(shù)默認(rèn)值生產(chǎn)建議說明repl-backlog-size1mb64mb 起按寫入速率和容忍斷線時間算高寫入場景可到 256mbrepl-backlog-ttl36007200從庫全斷后 backlog 保留多久太短會導(dǎo)致重連必須全量repl-timeout6060-120大實例慢網(wǎng)絡(luò)適當(dāng)加大但要和網(wǎng)絡(luò)抖動情況匹配repl-ping-replica-period1010改小會增加心跳開銷一般不動repl-diskless-sync-delay50-5從庫少且啟動分散時設(shè) 0避免等待client-output-buffer-limit replica256mb/64mb/60512mb/128mb/60全量同步期間從庫太慢會被踢大實例要放寬最后一行是很多線上事故的根因。默認(rèn)硬限制 256MB一個 5000 萬 key、寫入又猛的主庫在給慢從庫傳 RDB 時復(fù)制緩沖區(qū)幾秒鐘就能頂?shù)?256MB然后主庫主動斷鏈從庫重連再全量無限循環(huán)。加大這個限制是權(quán)宜之計根子上要看從庫為什么慢——是磁盤 IO 不行還是網(wǎng)絡(luò)帶寬不夠。4. 踩坑實錄常見故障與排查手冊這一章是我這些年攢下來的問題清單按癥狀 → 排查方向 → 解決的順序?qū)懩憧梢援?dāng)成速查表用。4.1 反復(fù)全量同步的定位思路最常見的癥狀是從庫看起來連上了但過一會兒又?jǐn)郔NFO stats里sync_full一路漲主庫的 RDB 生成日志刷個不停。這個問題的排查順序應(yīng)該是第一步確認(rèn)是網(wǎng)絡(luò)抖動還是容量不足??磸膸烊罩纠飻噙B的時間間隔如果規(guī)律性地每隔幾十秒斷一次多半是repl-timeout或者緩沖區(qū)溢出如果是隨機(jī)的更可能是網(wǎng)絡(luò)。第二步量 offset 差值。用master_repl_offset - slave_repl_offset算落后字節(jié)數(shù)和repl-backlog-size一比就知道有沒有超。超了就把 backlog 調(diào)大。第三步看主庫是否踢過從庫。主庫日志里搜Client ... scheduled to be closed ASAP for overcoming of output buffer limits有這句就是輸出緩沖區(qū)超限。第四步看從庫加載 RDB 的耗時。從庫日志會有MASTER - REPLICA sync: Loading DB in memory和Done loading RDB兩條中間的時間差如果超過repl-timeout的一半建議用無盤加載repl-diskless-load swapdb或者提升從庫規(guī)格。還有一個容易漏的點主庫是不是在頻繁做 RDB。如果你既有save策略又有從庫全量同步兩個 BGSAVE 撞一起fork 兩次主庫會非??āU_做法是把從庫同步觸發(fā)的 RDB 和定時 RDB 錯開或者干脆在主庫上關(guān)掉定時 RDB讓從庫承擔(dān)持久化職責(zé)。4.2 典型故障速查表我把高頻問題整理成一張表遇到問題先對號入座現(xiàn)象可能原因處理方式master_link_status:down網(wǎng)絡(luò)不通、端口映射錯、masterauth缺失用telnet測端口檢查兩側(cè)密碼配置NOAUTH Authentication required主庫有requirepass從庫沒配masterauth從庫補(bǔ)上masterauth并重啟從庫讀不到數(shù)據(jù)從庫在replica-serve-stale-data no下與主庫失聯(lián)恢復(fù)鏈路或臨時改配置允許讀舊數(shù)據(jù)READONLY You cant write against a read only replica應(yīng)用誤連從庫做寫操作檢查客戶端路由配置寫請求走主庫sync_full持續(xù)增長backlog 太小、從庫處理慢、網(wǎng)絡(luò)抖動加大repl-backlog-size排查從庫性能lag持續(xù)大于 5從庫負(fù)載高、網(wǎng)絡(luò)帶寬不足、主庫寫入過猛排查從庫慢查詢、加帶寬、考慮級聯(lián)復(fù)制主庫 fork 時間過長內(nèi)存大、頁表大、透明大頁未關(guān)關(guān)閉 THP控制單實例內(nèi)存不超過 16-20GB從庫內(nèi)存暴漲repl-diskless-load swapdb造成雙份數(shù)據(jù)關(guān)掉該選項或擴(kuò)容主從數(shù)據(jù)不一致從庫曾被寫入、主庫誤操作被同步用redis-cli --bigkeys等工具比對重建從庫關(guān)于透明大頁THP這是 Linux 上的一個老坑。THP 會把內(nèi)存頁從 4KB 合并成 2MBRedis 的 fork 走的是寫時復(fù)制頁越大復(fù)制的代價越高fork 耗時會從毫秒級變成幾百毫秒。關(guān)掉的方式是echo never /sys/kernel/mm/transparent_hugepage/enabled這條命令我每次部署新機(jī)器都要執(zhí)行一遍寫進(jìn)初始化腳本里別指望運維記得。4.3 生產(chǎn)環(huán)境的幾條經(jīng)驗除了參數(shù)和排錯還有些只有踩過才知道的東西。別讓從庫承擔(dān)唯一備份。我見過團(tuán)隊把從庫當(dāng)成備份策略結(jié)果主庫誤執(zhí)行了FLUSHALL一分鐘內(nèi)所有從庫全空了。備份必須是獨立的、離線的、有版本歷史的。從庫只能算副本。讀寫分離不要生搬硬套。寫后立即讀的場景比如用戶下單后馬上查訂單狀態(tài)走從庫會讀到舊數(shù)據(jù)這類請求必須強(qiáng)制走主庫。Lettuce 客戶端里有ReadFrom.MASTER、ReadFrom.REPLICA_PREFERRED這類策略Spring Boot 里可以通過LettuceClientConfiguration配置。我的做法是默認(rèn)全部走主庫只對明確能容忍延遲的查詢比如排行榜、統(tǒng)計報表開從庫讀寧可少省點資源也別讓業(yè)務(wù)同學(xué)半夜排查數(shù)據(jù)怎么對不上。級聯(lián)復(fù)制可以救命但不能濫用。一主掛八從的時候主庫要為每個從庫維護(hù)一份輸出緩沖全量同步時 fork 一次就要喂八個。這時候可以讓兩三個從庫直接跟主庫剩下的跟從庫即從庫的從庫。代價是多一跳延遲而且鏈路越長越容易斷。我的經(jīng)驗是一層足夠別搞三層。監(jiān)控必須采這幾項。光看 Redis 進(jìn)程活著沒用要采master_link_status、master_repl_offset差值、sync_full增量、latest_fork_usec、connected_slaves數(shù)量。前兩個反映健康度后三個反映成本和風(fēng)險。用 Prometheus redis_exporter 都能拿到配個告警規(guī)則offset 差值超過閾值或者sync_full五分鐘內(nèi)漲了兩次就報警。升級版本的時候先看協(xié)議變化。Redis 6.0 引入了多線程 IO但復(fù)制鏈路還是單線程處理的7.0 把復(fù)制緩沖區(qū)和 backlog 的內(nèi)存模型改成了共享結(jié)構(gòu)多個從庫共用一份 backlog內(nèi)存占用大幅下降。升級前值得讀一遍 release notes 里 Replication 那一段能省掉很多怎么升級完內(nèi)存掉了的困惑。5. 數(shù)據(jù)一致性與延遲應(yīng)用該怎么寫原理和運維講完了最后聊聊應(yīng)用側(cè)。畢竟主從復(fù)制最終是要服務(wù)于業(yè)務(wù)代碼的代碼寫不對前面配得再漂亮也白搭。5.1 復(fù)制延遲的度量與監(jiān)控延遲不是有或沒有的問題是多少的問題。測量延遲有兩個維度字節(jié)維度就是master_repl_offset - slave_repl_offset。這個值最直觀但要注意它只反映還沒處理的字節(jié)數(shù)不等于業(yè)務(wù)上感知的數(shù)據(jù)條數(shù)差異。要換算成條數(shù)得再除以平均命令大小。時間維度從庫上的master_last_io_seconds_ago和lag字段。前者的含義是距離上次收到主庫數(shù)據(jù)過了幾秒后者是主庫視角的最后一次 ACK 距今秒數(shù)。正常應(yīng)該都是 0 到 1。想精確測端到端延遲可以這么干在主庫上寫一個帶時間戳的 key然后在從庫上輪詢讀讀到之后用當(dāng)前時間減時間戳。這個方法我用過很多次比看 INFO 的計數(shù)器更貼近真實業(yè)務(wù)感受因為它把命令傳播、從庫排隊、網(wǎng)絡(luò)往返全算進(jìn)去了。# 主庫 redis-cli -h master -a pass set delay:probe $(date %s%3N) # 從庫 redis-cli -h replica -a pass get delay:probe監(jiān)控的告警閾值我一般設(shè)成這樣slave_repl_offset差值超過 5MB 持續(xù) 30 秒報警master_link_status變成 down 立即報警sync_full十分鐘內(nèi)增長超過 1 次報警。這三個覆蓋了絕大多數(shù)異常。5.2 讀寫分離的坑與兜底策略最后說說讀寫分離真正落地時的問題。理論上寫走主、讀走從很簡單實際上一堆細(xì)節(jié)連接池要分開配。主庫的連接池和從庫的連接池不能共用因為主庫要為每個從庫開一個復(fù)制連接從庫還要處理讀請求混在一起容易互相擠占。Spring Boot 里常見做法是配兩個LettuceConnectionFactory用Primary標(biāo)記主庫的那個再配一個只讀的供特定場景注入。事務(wù)里的讀必須走主庫。Redis 事務(wù)MULTI/EXEC和 Lua 腳本里的讀操作如果被路由到從庫可能讀到主庫還沒同步過來的中間狀態(tài)導(dǎo)致邏輯錯誤。這類操作的封裝層一定要硬編碼走主庫。延遲敏感的業(yè)務(wù)要有降級。從庫掛了的時候如果讀請求全部失敗用戶體驗會很差??梢宰鲆粚佣档讖膸熳x超時自動回落到主庫讀代價是主庫壓力上升但至少服務(wù)不中斷。這個邏輯別寫死在客戶端庫里放在業(yè)務(wù)層的 Repository 里更好控制。別用從庫做限流和計數(shù)。分布式限流的計數(shù)器、秒殺庫存這類強(qiáng)一致需求的場景必須走主庫。從庫的延遲意味著你的計數(shù)器可能不準(zhǔn)導(dǎo)致超賣或者限流失效。用 Lua 腳本做原子操作的場景尤其要注意腳本本身會在主庫執(zhí)行但如果你讀的數(shù)據(jù)是從庫的就是兩回事了。從庫數(shù)量不是越多越好。每加一個從庫主庫就多一份輸出緩沖、多一次網(wǎng)絡(luò)分發(fā)。三個從庫和八個從庫的差別在寫入高峰期會非常明顯。我的經(jīng)驗是單主掛三個從庫是舒服的區(qū)間再多就上級聯(lián)或者考慮 Cluster 分片。提示從庫做只讀查詢時記得避開KEYS、FLUSHALL這類危險命令。生產(chǎn)環(huán)境用rename-command把它們改掉或者禁用比事后追責(zé)有用得多。這套東西我在幾個日活百萬級的項目里跑過從最早的一主一從手動切換到現(xiàn)在一主三從加哨兵自動切中間踩的坑基本都在上面了。主從復(fù)制本身不難難的是把參數(shù)調(diào)到和自己的業(yè)務(wù)量匹配把監(jiān)控做到能提前發(fā)現(xiàn)問題把應(yīng)用側(cè)的路由規(guī)則寫對。這三件事做好了Redis 的可用性會有質(zhì)的提升。