制與哨兵架構(gòu)詳解:從原理到故障演練)
Redis 學(xué)到現(xiàn)在我最大的感受是單機(jī)再好也扛不住意外。前面幾篇我整理過(guò)數(shù)據(jù)結(jié)構(gòu)、內(nèi)存模型、持久化手段這一篇終于到了高可用層面最關(guān)鍵的一塊——主從復(fù)制和哨兵。簡(jiǎn)單說(shuō)主從復(fù)制負(fù)責(zé)把數(shù)據(jù)從一臺(tái) Redis 復(fù)制到另外幾臺(tái)哨兵負(fù)責(zé)在主庫(kù)掛了之后自動(dòng)把從庫(kù)抬上位。這套組合是 Redis 生產(chǎn)環(huán)境高可用架構(gòu)的基石也是很多人從會(huì)用 Redis跨到會(huì)維護(hù) Redis的分水嶺。下面這些內(nèi)容是我從動(dòng)手搭建、配置調(diào)優(yōu)和故障演練里一條條記下來(lái)的希望能幫正在學(xué)這塊的人少走彎路。1. 單機(jī) Redis 再快也有扛不住的事1.1 一臺(tái) Redis 的三重風(fēng)險(xiǎn)Redis 性能確實(shí)好單實(shí)例讀寫在很多場(chǎng)景下都能頂住但它本質(zhì)上還是一個(gè)進(jìn)程跑在一臺(tái)機(jī)器上。只要機(jī)器出了問(wèn)題Redis 再快也白搭。我總結(jié)下來(lái)單機(jī) Redis 主要面臨三類風(fēng)險(xiǎn)每一類都不罕見(jiàn)。機(jī)器層面的故障斷電、內(nèi)存接觸不良、磁盤損壞、系統(tǒng)崩潰導(dǎo)致重啟。這些不是會(huì)不會(huì)發(fā)生的問(wèn)題而是哪一天發(fā)生的問(wèn)題。人為層面的誤操作FLUSHALL、FLUSHDB、DEL一個(gè)大 key、執(zhí)行了配置錯(cuò)誤導(dǎo)致進(jìn)程退出。很多線上事故不是攻擊者干的就是自己手滑。網(wǎng)絡(luò)層面的分化機(jī)器本身沒(méi)掛但網(wǎng)絡(luò)分區(qū)把主庫(kù)隔離了??蛻舳诉B不到主庫(kù)業(yè)務(wù)照樣受影響而且這類問(wèn)題排查起來(lái)最花時(shí)間。所以只要業(yè)務(wù)對(duì) Redis 的可用性有一點(diǎn)要求單機(jī)部署就屬于裸奔狀態(tài)。1.2 主從復(fù)制解決什么又解決不了什么主從復(fù)制的核心價(jià)值可以拆成三點(diǎn)數(shù)據(jù)熱備份主庫(kù)的數(shù)據(jù)實(shí)時(shí)追加式同步到從庫(kù)主庫(kù)機(jī)器徹底報(bào)廢時(shí)從庫(kù)上仍然有一份完整數(shù)據(jù)不用從 RDB/AOF 冷恢復(fù)。讀能力擴(kuò)展主庫(kù)負(fù)責(zé)寫從庫(kù)分擔(dān)讀Redis 的讀寫分離模式讓整體吞吐量大幅提升。高可用的底座只有先把數(shù)據(jù)復(fù)制到多節(jié)點(diǎn)后面才能談自動(dòng)故障轉(zhuǎn)移也就是哨兵干的事。但有一點(diǎn)必須提前強(qiáng)調(diào)Redis 主從復(fù)制是異步復(fù)制。主庫(kù)執(zhí)行完寫命令后不是等從庫(kù)確認(rèn)再返回客戶端而是直接返回成功復(fù)制過(guò)程在后臺(tái)異步完成。這意味著它天生無(wú)法保證數(shù)據(jù)零丟失。如果主庫(kù)在某個(gè)寫命令還沒(méi)同步給從庫(kù)時(shí)就宕機(jī)了這條最新的寫入就丟了。理解了這一點(diǎn)再看后面的腦裂和數(shù)據(jù)丟失問(wèn)題就容易多了。2. 主從復(fù)制的底層鏈路從一次 psync 聊起2.1 第一次建立關(guān)系時(shí)主從之間發(fā)生了什么很多人配置完主從就完事了不太關(guān)心數(shù)據(jù)到底怎么過(guò)去的。實(shí)際上第一次建立復(fù)制關(guān)系時(shí)走的是一條完整的全量同步鏈路。我把每一步拆出來(lái)看。假設(shè)現(xiàn)在我有一臺(tái)主庫(kù)和一臺(tái)從庫(kù)我在從庫(kù)上執(zhí)行redis-cli -p 6380 REPLICAOF 127.0.0.1 6379從庫(kù)會(huì)做三件事記錄主庫(kù)地址、建立 TCP 連接、發(fā)送PSYNC命令。PSYNC是復(fù)制的握手協(xié)議語(yǔ)法長(zhǎng)這樣PSYNC replid offset如果是初次復(fù)制從庫(kù)不知道主庫(kù)的 replid也不知道自己該從哪個(gè)偏移量開始所以會(huì)帶上?和-1PSYNC ? -1主庫(kù)收到后如果判斷無(wú)法做部分同步就會(huì)回復(fù)FULLRESYNC同時(shí)把自己的replid和當(dāng)前復(fù)制偏移量發(fā)給從庫(kù)然后進(jìn)入全量同步流程主庫(kù)執(zhí)行BGSAVE生成 RDB 快照。與此同時(shí)主庫(kù)把新收到的寫命令寫入復(fù)制積壓緩沖區(qū)和命令傳播緩沖區(qū)防止快照生成期間的數(shù)據(jù)丟失??煺丈赏瓿珊笸ㄟ^(guò) socket 發(fā)給從庫(kù)。從庫(kù)清空舊數(shù)據(jù)加載 RDB 文件。需要注意的是Redis 為了避免磁盤 I/O 成為瓶頸可以改用磁盤無(wú)盤化復(fù)制repl-diskless-sync yesRDB 在內(nèi)存中直接生成并通過(guò)網(wǎng)絡(luò)流式傳輸副本不落盤。這個(gè)參數(shù)在從庫(kù)較多時(shí)能明顯降低主庫(kù)磁盤壓力但要求主從之間網(wǎng)絡(luò)帶寬充足。從庫(kù)加載完 RDB 后主庫(kù)還會(huì)把復(fù)制緩沖區(qū)里的增量命令繼續(xù)發(fā)過(guò)來(lái)確保數(shù)據(jù)追平。之后雙方進(jìn)入命令傳播階段主庫(kù)每次執(zhí)行寫命令都會(huì)把命令分發(fā)給從庫(kù)執(zhí)行。整個(gè)全量同步的安全點(diǎn)在于BGSAVE生成快照的過(guò)程不阻塞主庫(kù)服務(wù)但仍會(huì)消耗 CPU 和內(nèi)存如果數(shù)據(jù)量特別大建議在業(yè)務(wù)低峰期做首次復(fù)制。2.2 斷線重連背后的增量同步機(jī)制全量同步只發(fā)生在首次建立關(guān)系、或者從庫(kù)落后實(shí)在太多的情況下。日常運(yùn)行中從庫(kù)和主庫(kù)之間網(wǎng)絡(luò)抖動(dòng)導(dǎo)致短暫斷線重新連接時(shí)再走一次全量同步就太浪費(fèi)了。Redis 解決這個(gè)問(wèn)題的機(jī)制叫部分重同步。主庫(kù)內(nèi)存里維護(hù)了一塊固定大小的環(huán)形緩沖區(qū)叫復(fù)制積壓緩沖區(qū)repl_backlog。主庫(kù)每執(zhí)行一條寫命令都會(huì)把命令寫入這個(gè)緩沖區(qū)同時(shí)自己的master_repl_offset也往后推進(jìn)。從庫(kù)這邊同步到哪條命令也會(huì)記錄自己的slave_repl_offset。斷線重連時(shí)從庫(kù)發(fā)送PSYNC 主庫(kù)replid 從庫(kù)記錄的offset主庫(kù)接過(guò)請(qǐng)求后會(huì)檢查主庫(kù) replid 是否匹配不匹配就只能全量同步。從庫(kù) offset 之后的數(shù)據(jù)是否還在本地的復(fù)制積壓緩沖區(qū)中。如果 offset 之后的數(shù)據(jù)仍在 backlog 覆蓋范圍內(nèi)主庫(kù)就回復(fù)CONTINUE把從缺口開始的那一段命令重新發(fā)給從庫(kù)。如果 offset 已經(jīng)超出了緩沖區(qū)范圍說(shuō)明落后的太多主庫(kù)才回復(fù)FULLRESYNC走全量同步。這個(gè)機(jī)制的一個(gè)直接結(jié)論就是backlog 緩沖區(qū)的大小要按實(shí)際斷線容忍時(shí)間配置。比如生產(chǎn)環(huán)境平均每秒寫 2000 次每次命令平均 200 字節(jié)每秒就是 400KB 的數(shù)據(jù)量。默認(rèn)的repl-backlog-size是 1MB算下來(lái)只能容忍約 2.5 秒的斷線。如果你的網(wǎng)絡(luò)抖動(dòng)可能持續(xù) 1 分鐘這個(gè)值至少得配 24MB。緩沖區(qū)配大一點(diǎn)代價(jià)是內(nèi)存占用增加配小了斷線稍久就會(huì)退化成全量同步消耗更大。2.3 主從復(fù)制中容易被忽略的三個(gè)細(xì)節(jié)第一主從復(fù)制并不是主庫(kù)主動(dòng)推數(shù)據(jù)。從庫(kù)啟動(dòng)后先打招呼主庫(kù)才把數(shù)據(jù)交給它之后每次命令傳播也是從庫(kù)在 TCP 連接上等待。理解這個(gè)模型故障排查時(shí)就明白為什么網(wǎng)絡(luò)出問(wèn)題后從庫(kù)會(huì)不斷重連而不是主庫(kù)單方面斷開。第二從庫(kù)默認(rèn)只讀配置項(xiàng)是replica-read-only yes。如果強(qiáng)行在從庫(kù)上執(zhí)行寫命令Redis 會(huì)直接報(bào)錯(cuò)。這樣設(shè)計(jì)既保證一致性也防止開發(fā)者把從庫(kù)當(dāng)成寫入口導(dǎo)致數(shù)據(jù)錯(cuò)亂。第三過(guò)期鍵的處理是主庫(kù)說(shuō)了算。主庫(kù)遇到 key 過(guò)期時(shí)會(huì)顯式向從庫(kù)發(fā)送一條 DEL 命令。從庫(kù)不會(huì)自己判斷 key 是否過(guò)期并刪除只等主庫(kù)指令。這樣可以避免主從各自判斷時(shí)出現(xiàn)數(shù)據(jù)不一致。不過(guò)從庫(kù)仍然會(huì)在讀取時(shí)判斷邏輯過(guò)期返回空值給客戶端內(nèi)存里的清理還是靠主庫(kù)指令。3. 建立主從關(guān)系配置實(shí)操與關(guān)鍵參數(shù)3.1 三種建立方式怎么選建立主從關(guān)系有三種方式方式本身并不復(fù)雜但背后的適用場(chǎng)景很有講究。方式操作特點(diǎn)適用場(chǎng)景命令行REPLICAOF host port即時(shí)生效重啟后失效臨時(shí)切換、演練、手動(dòng)故障轉(zhuǎn)移配置文件在 redis.conf 寫replicaof host port持久生效需重啟實(shí)例正式部署、長(zhǎng)期架構(gòu)啟動(dòng)參數(shù)redis-server --replicaof host port持久生效依賴啟動(dòng)腳本容器化、進(jìn)程管理工具拉起我在實(shí)際部署時(shí)偏愛(ài)配置文件為主、命令行臨時(shí)調(diào)整的組合。平時(shí)保持主從關(guān)系寫在配置里需要做故障演練或主從切換時(shí)才用命令行動(dòng)態(tài)操作。有一點(diǎn)要特別注意執(zhí)行REPLICAOF no one可以讓從庫(kù)解除復(fù)制關(guān)系、變?yōu)橹鲙?kù)但這條命令同樣不會(huì)寫入配置文件。如果機(jī)器重啟它會(huì)重新按配置去連原主庫(kù)。生產(chǎn)環(huán)境做切換時(shí)一定要把配置文件一起改掉否則機(jī)器一重啟架構(gòu)又變回原樣事故就埋下了。3.2 五個(gè)值得提前調(diào)好的配置項(xiàng)repl-backlog-size復(fù)制積壓緩沖區(qū)大小。剛才已經(jīng)算過(guò)需要按每秒寫入量和斷線容忍時(shí)間估算一般 16MB 起步。repl-timeout復(fù)制超時(shí)時(shí)間默認(rèn) 60 秒。主從之間如果超過(guò)這個(gè)時(shí)間沒(méi)有心跳從庫(kù)會(huì)判定主庫(kù)不可用嘗試重新連接。網(wǎng)絡(luò)環(huán)境較差時(shí)可以適當(dāng)調(diào)大比如 90 秒。repl-diskless-sync是否啟用無(wú)盤化全量同步。多從庫(kù)首次全量復(fù)制時(shí)建議開啟減少磁盤 I/O但需要確保主從之間的帶寬充足。replica-read-only從庫(kù)只讀開關(guān)生產(chǎn)環(huán)境必須保持 yes。repl-diskless-sync-delay無(wú)盤化同步開始前的等待時(shí)間默認(rèn) 5 秒。這個(gè)設(shè)計(jì)很有意思主庫(kù)會(huì)等一等看這段時(shí)間內(nèi)有沒(méi)有新的從庫(kù)來(lái)請(qǐng)求復(fù)制如果有可以用一次 RDB 覆蓋多個(gè)從庫(kù)避免每個(gè)從庫(kù)重復(fù)生成快照。3.3 如何在命令行驗(yàn)證復(fù)制狀態(tài)配置完成后在從庫(kù)執(zhí)行redis-cli -p 6380 INFO replication關(guān)注這幾項(xiàng)role:slave master_host:127.0.0.1 master_port:6379 master_link_status:up master_last_io_seconds_ago:0 master_repl_offset:10086 slave_repl_offset:10086 master_replid:e5a3f8...如果master_link_status是up說(shuō)明連接正常。master_repl_offset和slave_repl_offset數(shù)值一致時(shí)主從數(shù)據(jù)追平。實(shí)際環(huán)境中這兩個(gè)偏移量會(huì)存在短暫差異只要差值不大、且不斷收斂就是正常的。我還建議把主庫(kù)側(cè)的connected_slaves也盯上。主庫(kù)用INFO replication可以看到當(dāng)前掛著幾個(gè)從庫(kù)以及各自的復(fù)制偏移量。這個(gè)視角才是運(yùn)維真正需要的。4. 主庫(kù)宕機(jī)后哨兵到底在忙什么4.1 沒(méi)有哨兵時(shí)手動(dòng)切換到底有多累先想想看如果只有主從復(fù)制、沒(méi)有哨兵主庫(kù)宕機(jī)后運(yùn)維要干的活確認(rèn)某個(gè)從庫(kù)的數(shù)據(jù)恢復(fù)程度看偏移量是否追平。在從庫(kù)上執(zhí)行REPLICAOF no one把它升為主庫(kù)。修改所有業(yè)務(wù)客戶端的 Redis 地址指向新的主庫(kù)。把原主庫(kù)降級(jí)為從庫(kù)重新接入新主庫(kù)。這一套流程聽(tīng)下來(lái)不難但每一步都要人工判斷而且業(yè)務(wù)中斷時(shí)間取決于人的反應(yīng)速度和操作熟練度。大半夜被叫起來(lái)處理故障能保持清醒做完四步已經(jīng)很不錯(cuò)了。哨兵存在的意義就是把這件事自動(dòng)化。4.2 主觀下線和客觀下線哨兵判斷主庫(kù)掛了的兩層邏輯哨兵判斷主庫(kù)是否宕機(jī)不是一口咬定而是分兩步。第一步叫主觀下線SDOWN。每個(gè)哨兵進(jìn)程每隔一秒會(huì)向所有節(jié)點(diǎn)發(fā)送 PING 命令如果主庫(kù)在down-after-milliseconds配置時(shí)間內(nèi)都沒(méi)有有效回復(fù)當(dāng)前這個(gè)哨兵就會(huì)認(rèn)為主庫(kù)主觀下線。注意這只是單個(gè)哨兵的看法。由于網(wǎng)絡(luò)波動(dòng)、GC 停頓等原因單個(gè)哨兵可能誤判所以需要引入第二步。第二步叫客觀下線ODOWN。當(dāng)一個(gè)哨兵判斷主庫(kù)主觀下線后它會(huì)向其他哨兵發(fā)起詢問(wèn)我認(rèn)為主庫(kù)掛了你們?cè)趺纯?其他哨兵根據(jù)自己的探測(cè)結(jié)果給出回應(yīng)。當(dāng)贊成票數(shù)達(dá)到配置的quorum閾值時(shí)這個(gè)哨兵就確認(rèn)主庫(kù)客觀下線可以進(jìn)入故障轉(zhuǎn)移流程。這里有一個(gè)關(guān)鍵點(diǎn)quorum不是哨兵集群的所有節(jié)點(diǎn)數(shù)只是確認(rèn)下線所需的最低票數(shù)。假如我有 3 個(gè)哨兵quorum配成 2那么至少兩個(gè)哨兵認(rèn)定主庫(kù)下線流程才會(huì)啟動(dòng)。哨兵節(jié)點(diǎn)數(shù)量建議奇數(shù)個(gè)這是為了后續(xù)選主投票時(shí)能形成多數(shù)派避免平票。4.3 故障轉(zhuǎn)移的完整決策鏈確認(rèn)客觀下線后接下來(lái)發(fā)生的事情順序很有意思哨兵節(jié)點(diǎn)之間選出一個(gè)領(lǐng)導(dǎo)者由它負(fù)責(zé)執(zhí)行故障轉(zhuǎn)移。這個(gè)選舉機(jī)制會(huì)讓每個(gè)哨兵自薦或者推薦別人拿到半數(shù)以上票的哨兵成為領(lǐng)導(dǎo)者。領(lǐng)導(dǎo)者從所有從庫(kù)中選出一個(gè)新的主庫(kù)。選擇規(guī)則按優(yōu)先級(jí)依次是slave-priority配置值越小越優(yōu)先優(yōu)先級(jí)相同時(shí)復(fù)制偏移量最大的從庫(kù)優(yōu)先數(shù)據(jù)最完整再相同按 runid 字典序小的優(yōu)先。新主庫(kù)執(zhí)行REPLICAOF no one從庫(kù)身份變?yōu)橹鲙?kù)。其他從庫(kù)執(zhí)行REPLICAOF 新主庫(kù)IP 新主庫(kù)端口重新指向新主庫(kù)。原主庫(kù)恢復(fù)可用后哨兵會(huì)把它降級(jí)為從庫(kù)也指向新主庫(kù)。整個(gè)過(guò)程從故障到切換完成正常情況幾秒到十幾秒。業(yè)務(wù)客戶端需要能感知哨兵切換通常用哨兵的發(fā)布訂閱消息或訪問(wèn)哨兵來(lái)發(fā)現(xiàn)新的主庫(kù)地址。5. 哨兵最小化部署與故障演練全過(guò)程5.1 一套最小但完整的高可用架構(gòu)我建議學(xué)習(xí)階段就搭一主兩從三哨兵不要偷懶只搭一個(gè)哨兵。單個(gè)哨兵自己都可能成為單點(diǎn)故障轉(zhuǎn)移時(shí)也無(wú)法形成多數(shù)派判斷。部署規(guī)劃可以這樣節(jié)點(diǎn)端口作用主庫(kù)6379讀寫入口從庫(kù) A6380冷備 讀從庫(kù) B6381冷備 讀哨兵 126379監(jiān)控和故障轉(zhuǎn)移哨兵 226380監(jiān)控和故障轉(zhuǎn)移哨兵 326381監(jiān)控和故障轉(zhuǎn)移實(shí)驗(yàn)環(huán)境下可以跑在同一臺(tái)機(jī)器上只要端口區(qū)分就行。生產(chǎn)環(huán)境至少需要三臺(tái)機(jī)器哨兵分布在不同的物理節(jié)點(diǎn)上否則機(jī)房整體斷電會(huì)讓哨兵集體失效。5.2 哨兵配置文件逐項(xiàng)說(shuō)明哨兵的配置文件是sentinel.conf核心項(xiàng)如下# 監(jiān)控的主庫(kù)名、地址、端口、判定客觀下線所需票數(shù) sentinel monitor mymaster 127.0.0.1 6379 2 # 主庫(kù)無(wú)響應(yīng)多久后判定主觀下線默認(rèn)30000毫秒 sentinel down-after-milliseconds mymaster 5000 # 故障轉(zhuǎn)移超時(shí)時(shí)間默認(rèn)180000毫秒 sentinel failover-timeout mymaster 180000 # 故障轉(zhuǎn)移時(shí)最多可以同時(shí)向幾個(gè)從庫(kù)發(fā)起同步 sentinel parallel-syncs mymaster 1逐項(xiàng)解讀sentinel monitor里的最后一個(gè)數(shù)字就是quorum我這里是 2意味著至少兩個(gè)哨兵認(rèn)定主庫(kù)下線才切換。down-after-milliseconds設(shè)成 5000即 5 秒無(wú)響應(yīng)就報(bào)主觀下線。生產(chǎn)環(huán)境要根據(jù)網(wǎng)絡(luò)質(zhì)量調(diào)太短容易誤判太長(zhǎng)切換太慢。parallel-syncs建議設(shè) 1意思是新主庫(kù)上位后同一時(shí)間只允許一個(gè)從庫(kù)全量同步。如果同時(shí)讓多個(gè)從庫(kù)一起同步新主庫(kù)可能因?yàn)樗查g負(fù)載過(guò)高被拖垮。5.3 手動(dòng)殺掉主庫(kù)觀察哨兵的完整動(dòng)作搭建完成后我強(qiáng)烈建議做一次故障演練不然哨兵配置到底有沒(méi)有生效心里沒(méi)底。我當(dāng)時(shí)的演練過(guò)程如下。先把所有節(jié)點(diǎn)進(jìn)程拉起確認(rèn)哨兵已經(jīng)發(fā)現(xiàn)了主庫(kù)和從庫(kù)redis-cli -p 26379 sentinel masters redis-cli -p 26379 sentinel slaves mymaster然后模擬主庫(kù)故障直接殺掉主庫(kù)進(jìn)程redis-cli -p 6379 SHUTDOWN NOSAVE接著盯著哨兵日志看變化。典型的日志順序是sdown master mymaster 127.0.0.1 6379 odown master mymaster 127.0.0.1 6379 #quorum 2/2 try-failover master mymaster 127.0.0.1 6379 failover-state-select-slave master mymaster 127.0.0.1 6379 selected-slave slave 127.0.0.1:6380 promoted-slave slave 127.0.0.1:6380 switch-master mymaster 127.0.0.1 6379 127.0.0.1 6380看到switch-master就說(shuō)明切換已經(jīng)完成。此時(shí)再用哨兵查詢主庫(kù)地址返回的應(yīng)該是 6380 端口。同時(shí)原先的從庫(kù) B 也會(huì)自動(dòng)指向 6380整個(gè)集群恢復(fù)可用。演練完之后我把原 6379 重啟觀察哨兵日志會(huì)發(fā)現(xiàn)它被自動(dòng)接入為從庫(kù)指向 6380。這一步很關(guān)鍵因?yàn)樗?yàn)證了原主庫(kù)恢復(fù)后不會(huì)搶占主庫(kù)位置而是老老實(shí)實(shí)做從庫(kù)。6. 復(fù)制延遲、腦裂與哨兵系統(tǒng)的邊界6.1 復(fù)制延遲從哪里來(lái)怎么盯主從復(fù)制是異步的所以延遲必然存在。延遲的來(lái)源主要有三個(gè)網(wǎng)絡(luò)往返主從跨機(jī)房部署時(shí)RTT 本身就有幾十毫秒。大 key 與慢命令主庫(kù)在生成 RDB 或執(zhí)行大集合操作時(shí)可能阻塞復(fù)制鏈路。從庫(kù)自身負(fù)載從庫(kù)如果承擔(dān)了大量讀請(qǐng)求CPU 跑滿時(shí)處理復(fù)制命令也會(huì)變慢。監(jiān)控方式就是在主庫(kù)執(zhí)行INFO replication觀察master_repl_offset和每個(gè)從庫(kù)的slave_repl_offset二者差值就是復(fù)制積壓量。更省事的方式是利用 Redis 的監(jiān)控指標(biāo)工具如命令行--stat或各類可視化監(jiān)控定期采集這個(gè)差值并告警。實(shí)際運(yùn)營(yíng)中延遲長(zhǎng)期超過(guò) 1 秒就需要排查了尤其是讀寫分離場(chǎng)景下延遲會(huì)導(dǎo)致讀到過(guò)期數(shù)據(jù)。6.2 腦裂場(chǎng)景與最后一道防線 min-replicas腦裂是主從復(fù)制模式下最經(jīng)典的數(shù)據(jù)丟得神不知鬼不覺(jué)的場(chǎng)景。場(chǎng)景是這樣的主庫(kù)和哨兵之間的網(wǎng)絡(luò)發(fā)生了分區(qū)哨兵聯(lián)系不上主庫(kù)于是把從庫(kù) A 提升為新主庫(kù)。但原主庫(kù)并沒(méi)有宕機(jī)它還活著而且客戶端仍然能通過(guò)原地址連接到它繼續(xù)寫入數(shù)據(jù)。等網(wǎng)絡(luò)恢復(fù)哨兵發(fā)現(xiàn)原主庫(kù)還活著強(qiáng)制它變成從庫(kù)去同步新主庫(kù)的數(shù)據(jù)。此時(shí)原主庫(kù)在分區(qū)期間寫入的新數(shù)據(jù)就全被覆蓋了因?yàn)檫@些數(shù)據(jù)根本沒(méi)同步給任何節(jié)點(diǎn)。這個(gè)問(wèn)題的防線在 Redis 配置里叫min-replicas-to-write和min-replicas-max-lag。含義是主庫(kù)至少要有 N 個(gè)從庫(kù)連接且數(shù)據(jù)復(fù)制延遲不能超過(guò) M 秒否則拒絕處理寫請(qǐng)求。min-replicas-to-write 1 min-replicas-max-lag 10意思是如果連一個(gè)從庫(kù)的復(fù)制延遲都超過(guò) 10 秒那主庫(kù)就不再接受寫入。在腦裂場(chǎng)景里原主庫(kù)被分區(qū)隔離從庫(kù)全部連不上它復(fù)制延遲迅速超過(guò)閾值寫入被拒絕從而避免了分區(qū)期間的數(shù)據(jù)產(chǎn)生。當(dāng)然代價(jià)是這段時(shí)間業(yè)務(wù)寫入直接失敗屬于犧牲可用性保一致性的選擇。實(shí)際配置時(shí)怎么取舍取決于業(yè)務(wù)特性但min-replicas-to-write至少設(shè)成 1 是常規(guī)建議。6.3 哨兵模式解決不了的問(wèn)題哨兵不等于數(shù)據(jù)零丟失。主庫(kù)宕機(jī)時(shí)還沒(méi)有同步給從庫(kù)的那部分?jǐn)?shù)據(jù)切換后依然會(huì)丟。如果業(yè)務(wù)對(duì)數(shù)據(jù)完整性要求極高需要在客戶端層面做補(bǔ)償或者接受這個(gè)誤差否則就得考慮其他方案。哨兵模式下主庫(kù)仍是單點(diǎn)寫入口。從庫(kù)雖然可以擴(kuò)展讀但寫入請(qǐng)求最終還是要打到主庫(kù)。如果寫并發(fā)很大、單機(jī)內(nèi)存放不下哨兵解決不了這種容量問(wèn)題需要走 Redis Cluster 的分片方案。哨兵集群不是配置完就一勞永逸。哨兵之間的網(wǎng)絡(luò)同樣可能分區(qū)quorum和節(jié)點(diǎn)數(shù)量需要結(jié)合實(shí)際情況認(rèn)真設(shè)計(jì)。我在實(shí)際搭建時(shí)發(fā)現(xiàn)哨兵日志的輸出信息量大但分散最好在前期就把日志接入統(tǒng)一的監(jiān)控平臺(tái)否則真出事時(shí)只能靠人肉翻日志。最后再分享一個(gè)我自己的小習(xí)慣每次搭完主從和哨兵我都會(huì)在從庫(kù)上執(zhí)行幾遍INFO replication和SENTINEL masters確認(rèn)所有節(jié)點(diǎn)視角一致后再交付。高可用這套東西風(fēng)險(xiǎn)都不在配置本身而在配置和現(xiàn)實(shí)狀態(tài)對(duì)不上。多演練、多觀察、多記錄真正出事時(shí)才能心里有數(shù)。