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