網站首頁 編程語言 正文
1 案例
主從集群有1個主庫、5個從庫和3個哨兵實例,突然發現客戶端發送的一些數據丟了,直接影響業務層數據可靠性。
最終排查發現是主從集群中的腦裂問題導致:主從集群中,同時有兩個主節點都能接收寫請求。
影響
客戶端不知道應往哪個主節點寫數據,導致不同客戶端往不同主節點寫數據。嚴重的,腦裂會進一步導致數據丟失。
2 腦裂原因
最初問題:在主從集群中,客戶端發送的數據丟失了。
2.1 為什么數據會丟失?
① 確認數據同步是否異常
在主從集群中發生數據丟失,最常見原因:主庫數據還沒同步到從庫,結果主庫故障,等從庫升級為主庫后,未同步數據丟了。
新寫入主庫的數據a=1、b=3,因為在主庫故障前未同步到從庫,失了。
這種數據丟失case,可直接對比主從庫的復制進度差值:
master_repl_offset - slave_repl_offset
若從庫的slave_repl_offset < 原主庫的master_repl_offset,則可認定數據丟失是由數據同步未完成導致。
部署主從集群時,也監測了:
- 主庫的master_repl_offset
- 從庫上的slave_repl_offset
但發現數據丟失后,檢查了新主庫升級前的slave_repl_offset,以及原主庫的master_repl_offset,一致,說明該升級為新主庫的從庫,在升級時已和原主庫的數據一致。
那為啥還會出現客戶端發的數據丟失?
所有數據操作都是從客戶端發給Redis實例,是否可從客戶端操作日志發現問題?
② 排查客戶端的操作日志,發現腦裂現象
發現主從切換后的一段時間,有個客戶端仍在和原主庫通信,并沒有和升級的新主庫交互。
相當于主從集群中同時有兩個主庫。據此,想到主從集群故障的腦裂。但不同客戶端給兩個主庫發送數據寫操作,應只會導致新數據會分布在不同主庫,而不會造成數據丟失。
思路又斷了。“從原理出發是追本溯源的好方法”。腦裂是發生在主從切換過程,猜測是漏掉了主從集群切換過程中的某環節,所以,聚焦主從切換的執行過程。
③ 發現是原主庫假故障導致的腦裂
我們采用哨兵機制進行主從切換的,主從切換發生時,一定有超過預設數量(quorum配置項)的哨兵實例和主庫的心跳都超時,才會把主庫判斷為客觀下線,然后,哨兵開始執行切換操作。
哨兵切換完成后,客戶端會和新主庫通信,發送請求操作。
但切換過程中,既然客戶端仍和原主庫通信,說明原主庫并未真故障(如主庫進程掛掉)。懷疑主庫某些原因無法處理請求,也沒響應哨兵的心跳,被哨兵錯判客觀下線。
被判下線后,原主庫又重新開始處理請求了,而此時,哨兵還沒完成主從切換,客戶端仍可和原主庫通信,客戶端發送的寫操作就會在原主庫寫數據。
為驗證原主庫只是“假故障”,查看原主庫服務器的資源使用監控。原主庫所在機器有段時間CPU利用率飆升,因某程序把機器CPU用滿,導致Redis主庫無法響應心跳,這期間,哨兵就把主庫判為客觀下線,開始主從切換。這程序很快恢復正常,CPU使用率也下來了。原主庫又繼續正常服務請求。
正因原主庫未真故障,在客戶端操作日志中就看到和原主庫通信記錄。從庫被升級為新主庫后,主從集群里就有兩個主庫,這就是案例腦裂原因。
3 為何腦裂會導致數據丟失?
主從切換后,從庫一旦升級為新主,哨兵就會讓原主庫執行slave of命令,和新主重新進行全量同步。
在全量同步執行最后階段,原主需清空本地數據,加載新主發送的RDB文件,原主在主從切換期間保存的新寫數據就丟了。
主從切換過程中,若原主只是“假故障”,會觸發哨兵啟動主從切換,一旦等它從假故障恢復,又開始處理請求,這就和新主共存,導致腦裂。
等哨兵讓原主和新主做全量同步后,原主在切換期間保存的數據就丟了。
4 腦裂應急方案
主從集群中的數據丟失是因為發生腦裂,必須有應對腦裂方案。
問題出在原主假故障后,仍能接收請求,因此,可在主從集群機制的配置項中查找是否有限制主庫接收請求的設置。Redis提供如下配置項限制主庫的請求處理:
min-replicas-to-write
主庫能進行數據同步的最少從庫數量
min-replicas-max-lag
主從庫間進行數據復制時,從庫給主庫發送ACK消息的最大延遲(單位s)
分別設置閾值N和T,倆配置項組合后的要求是:
- 主庫連接的從庫中至少有N個從庫
- 和主庫進行數據復制時的ACK消息延遲不能超過T秒
否則,主庫就不會再接收客戶端請求。
即使原主假故障,假故障期間也無法響應哨兵心跳,也不能和從庫進行同步,自然就無法和從庫進行ACK確認。這倆配置項組合要求就無法得到滿足,原主庫就會被限制接收客戶端請求,客戶端也就不能在原主庫中寫新數據。
等新主上線,就只有新主能接收和處理客戶端請求,此時,新寫的數據會被直接寫到新主。而原主會被哨兵降為從庫,即使它的數據被清空,也不會有新數據的丟失。
假設
- min-replicas-to-write=1
- min-replicas-max-lag設為12s
- 哨兵的down-after-milliseconds設為10s
主庫因某原因卡住15s,導致哨兵判斷主庫客觀下線,開始進行主從切換。
同時,因原主庫卡住15s,沒有一個從庫能和原主庫在12s內進行數據復制,原主庫也無法接收客戶端請求。
主從切換完成后,也只有新主庫能接收請求,不會發生腦裂,也就不會發生數據丟失。
5 總結
腦裂,主從集群中,同時有兩個主能接收寫請求。Redis主從切換過程中,若發生腦裂,客戶端數據就會寫入原主,若原主被降為從庫,這些新寫入數據就丟了。
腦裂主要是因為原主庫發生了假故障,假故障的原因:
- 和主庫部署在同一臺服務器上的其他程序臨時占用了大量資源(例如CPU資源),導致主庫資源使用受限,短時間內無法響應心跳。其它程序不再使用資源時,主庫又恢復正常
- 主庫自身遇到阻塞,如處理bigkey或是發生內存swap(你可以復習下第19講中總結的導致實例阻塞的原因),短時間內無法響應心跳,等主庫阻塞解除后,又恢復正常的請求處理了。
應對腦裂,你可以在主從集群部署時,通過合理地配置參數min-slaves-to-write和min-slaves-max-lag,來預防腦裂。
在實際應用中,可能會因為網絡暫時擁塞導致從庫暫時和主庫的ACK消息超時。在這種情況下,并不是主庫假故障,我們也不用禁止主庫接收請求。
6 最佳實踐
假設從庫有K個,可將:
- min-slaves-to-write設置為K/2+1(如果K等于1,就設為1)
- min-slaves-max-lag設置為十幾秒(例如10~20s)
這個配置下,如果有一半以上的從庫和主庫進行的ACK消息延遲超過十幾s,我們就禁止主庫接收客戶端寫請求。
這樣一來,我們可以避免腦裂帶來數據丟失的情況,而且,也不會因為只有少數幾個從庫因為網絡阻塞連不上主庫,就禁止主庫接收請求,增加了系統的魯棒性。
假設:
- min-slaves-to-write 置 1
- min-slaves-max-lag 設置為 15s,哨兵的
- down-after-milliseconds 設置為 10s
哨兵主從切換需要 5s,主庫因為某些原因卡住12s,此時,還會發生腦裂嗎?主從切換完成后,數據會丟失嗎?
主庫卡住 12s,達到哨兵設定的切換閾值,所以哨兵會觸發主從切換。但哨兵切換時間5s,即哨兵還未切換完成,主庫就會從阻塞狀態中恢復回來,且沒有觸發 min-slaves-max-lag 閾值,所以主庫在哨兵切換剩下的 3s 內,依舊可以接收客戶端的寫操作,如果這些寫操作還未同步到從庫,哨兵就把從庫提升為主庫了,那么此時也會出現腦裂的情況,之后舊主庫降級為從庫,重新同步新主庫的數據,新主庫也會發生數據丟失。
即使 Redis 配置了 min-slaves-to-write 和 min-slaves-max-lag,當腦裂發生時,還是無法嚴格保證數據不丟失,只是盡量減少數據的丟失。
這種情況下,新主庫之所以會發生數據丟失,是因為舊主庫從阻塞中恢復過來后,收到的寫請求還沒同步到從庫,從庫就被哨兵提升為主庫了。如果哨兵在提升從庫為新主庫前,主庫及時把數據同步到從庫了,那么從庫提升為主庫后,也不會發生數據丟失。但這種臨界點的情況還是有發生的可能性,因為 Redis 本身不保證主從同步的強一致。
還有一種腦裂情況,就是網絡分區:主庫和客戶端、哨兵和從庫被分割成了 2 個網絡,主庫和客戶端處在一個網絡中,從庫和哨兵在另一個網絡中,此時哨兵也會發起主從切換,出現 2 個主庫的情況,而且客戶端依舊可以向舊主庫寫入數據。等網絡恢復后,主庫降級為從庫,新主庫丟失了這期間寫操作的數據。
腦裂本質是,Redis 主從集群內部沒有通過共識算法,來維護多個節點數據的強一致性。不像 Zookeeper,每次寫請求必須大多數節點寫成功后才認為成功。當腦裂發生時,Zookeeper 主節點被孤立,此時無法寫入大多數節點,寫請求會直接返回失敗,因此它可以保證集群數據的一致性。
對于min-slaves-to-write,如果只有 1 個從庫,當把 min-slaves-to-write 設置為 1 時,在運維時需要小心一些,當日常對從庫做維護時,例如更換從庫的實例,需要先添加新的從庫,再移除舊的從庫才可以,或者使用 config set 修改 min-slaves-to-write 為 0 再做操作,否則會導致主庫拒絕寫,影響到業務。
原文鏈接:https://blog.csdn.net/qq_33589510/article/details/121120876
相關推薦
- 2022-10-11 ArrayList源碼中的MAX_ARRAY_SIZE
- 2022-12-05 useReducer?createContext代替Redux原理示例解析_React
- 2022-06-08 FreeRTOS實時操作系統在Cortex-M3上的移植過程_操作系統
- 2022-03-14 關于log4j日志擴展---自定義PatternLayout(log4j自定義日志級別)
- 2023-02-25 一文搞懂Python中is和==的區別_python
- 2022-03-18 C語言回溯法解八皇后問題(八皇后算法)_C 語言
- 2022-04-14 error: failed to push some refs to ‘http://git.tex
- 2022-10-27 React?State與生命周期詳細介紹_React
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支