11 | 脑裂: 一次奇怪的数据丢失
1. 前言
腦裂就是指在主從集群中,同時(shí)有兩個(gè)主節(jié)點(diǎn),它們都能接收寫請(qǐng)求。而腦裂最直接的影響,就是客戶端不知道應(yīng)該往哪個(gè)主節(jié)點(diǎn)寫入數(shù)據(jù),結(jié)果就是不同的客戶端會(huì)往不同的主節(jié)點(diǎn)上寫入數(shù)據(jù)。而且,嚴(yán)重的話,腦裂會(huì)進(jìn)一步導(dǎo)致數(shù)據(jù)丟失。
2. 為什么會(huì)發(fā)生腦裂?
第一步:確認(rèn)是不是數(shù)據(jù)同步出現(xiàn)了問(wèn)題
??在主從集群中發(fā)生數(shù)據(jù)丟失,最常見的原因就主庫(kù)的數(shù)據(jù)還沒(méi)有同步到從庫(kù),結(jié)果主庫(kù)發(fā)生了故障,等從庫(kù)升級(jí)為主庫(kù)后,未同步的數(shù)據(jù)就丟失了。
??如果是這種情況的數(shù)據(jù)丟失,我們可以通過(guò)比對(duì)主從庫(kù)上的復(fù)制進(jìn)度差值來(lái)進(jìn)行判斷,也就是計(jì)算 master_repl_offset 和 slave_repl_offset 的差值。如果從庫(kù)上的slave_repl_offset 小于原主庫(kù)的 master_repl_offset,那么,我們就可以認(rèn)定數(shù)據(jù)丟失是由數(shù)據(jù)同步未完成導(dǎo)致的。
第二步:排查客戶端的操作日志,發(fā)現(xiàn)腦裂現(xiàn)象
??在排查客戶端的操作日志時(shí),我們發(fā)現(xiàn),在主從切換后的一段時(shí)間內(nèi),有一個(gè)客戶端仍然在和原主庫(kù)通信,并沒(méi)有和升級(jí)的新主庫(kù)進(jìn)行交互。這就相當(dāng)于主從集群中同時(shí)有了兩個(gè)主庫(kù)。根據(jù)這個(gè)跡象,我們就想到了在分布式主從集群發(fā)生故障時(shí)會(huì)出現(xiàn)的一個(gè)問(wèn)題:腦裂。
??但是,不同客戶端給兩個(gè)主庫(kù)發(fā)送數(shù)據(jù)寫操作,按道理來(lái)說(shuō),只會(huì)導(dǎo)致新數(shù)據(jù)會(huì)分布在不同的主庫(kù)上,并不會(huì)造成數(shù)據(jù)丟失。那么,為什么我們的數(shù)據(jù)仍然丟失了呢?到這里,我們的排查思路又一次中斷了。
??不過(guò),在分析問(wèn)題時(shí),我們一直認(rèn)為“從原理出發(fā)是追本溯源的好方法”。腦裂是發(fā)生在主從切換的過(guò)程中,我們猜測(cè),肯定是漏掉了主從集群切換過(guò)程中的某個(gè)環(huán)節(jié),所以,我們把研究的焦點(diǎn)投向了主從切換的執(zhí)行過(guò)程。
第三步:發(fā)現(xiàn)是原主庫(kù)假故障導(dǎo)致的腦裂
??我們是采用哨兵機(jī)制進(jìn)行主從切換的,當(dāng)主從切換發(fā)生時(shí),一定是有超過(guò)預(yù)設(shè)數(shù)量(quorum 配置項(xiàng))的哨兵實(shí)例和主庫(kù)的心跳都超時(shí)了,才會(huì)把主庫(kù)判斷為客觀下線,然后,哨兵開始執(zhí)行切換操作。哨兵切換完成后,客戶端會(huì)和新主庫(kù)進(jìn)行通信,發(fā)送請(qǐng)求操作。
??但是,在切換過(guò)程中,既然客戶端仍然和原主庫(kù)通信,這就表明,原主庫(kù)并沒(méi)有真的發(fā)生故障(例如主庫(kù)進(jìn)程掛掉)。我們猜測(cè),主庫(kù)是由于某些原因無(wú)法處理請(qǐng)求,也沒(méi)有響應(yīng)哨兵的心跳,才被哨兵錯(cuò)誤地判斷為客觀下線的。結(jié)果,在被判斷下線之后,原主庫(kù)又重新開始處理請(qǐng)求了,而此時(shí),哨兵還沒(méi)有完成主從切換,客戶端仍然可以和原主庫(kù)通信,客戶端發(fā)送的寫操作就會(huì)在原主庫(kù)上寫入數(shù)據(jù)了。
3. 為什么腦裂會(huì)導(dǎo)致數(shù)據(jù)丟失
??主從切換后,從庫(kù)一旦升級(jí)為新主庫(kù),哨兵就會(huì)讓原主庫(kù)執(zhí)行 slave of 命令,和新主庫(kù)重新進(jìn)行全量同步。而在全量同步執(zhí)行的最后階段,原主庫(kù)需要清空本地的數(shù)據(jù),加載新主庫(kù)發(fā)送的 RDB 文件,這樣一來(lái),原主庫(kù)在主從切換期間保存的新寫數(shù)據(jù)就丟失了。
??在主從切換的過(guò)程中,如果原主庫(kù)只是“假故障”,它會(huì)觸發(fā)哨兵啟動(dòng)主從切換,一旦等它從假故障中恢復(fù)后,又開始處理請(qǐng)求,這樣一來(lái),就會(huì)和新主庫(kù)同時(shí)存在,形成腦裂。等到哨兵讓原主庫(kù)和新主庫(kù)做全量同步后,原主庫(kù)在切換期間保存的數(shù)據(jù)就丟失了。
4. 如何應(yīng)對(duì)腦裂問(wèn)題
??既然問(wèn)題是出在原主庫(kù)發(fā)生假故障后仍然能接收請(qǐng)求上,我們就開始在主從集群機(jī)制的配置項(xiàng)中查找是否有限制主庫(kù)接收請(qǐng)求的設(shè)置。Redis 已經(jīng)提供了兩個(gè)配置項(xiàng)來(lái)限制主庫(kù)的請(qǐng)求處理,分別是 min-slaves-to-write 和 min-slaves-max-lag。
- min-slaves-to-write:這個(gè)配置項(xiàng)設(shè)置了主庫(kù)能進(jìn)行數(shù)據(jù)同步的最少?gòu)膸?kù)數(shù)量;
- min-slaves-max-lag:這個(gè)配置項(xiàng)設(shè)置了主從庫(kù)間進(jìn)行數(shù)據(jù)復(fù)制時(shí),從庫(kù)給主庫(kù)發(fā)送ACK 消息的最大延遲(以秒為單位)。
??我們可以把 min-slaves-to-write 和 min-slaves-max-lag 這兩個(gè)配置項(xiàng)搭配起來(lái)使用,分別給它們?cè)O(shè)置一定的閾值,假設(shè)為 N 和 T。這兩個(gè)配置項(xiàng)組合后的要求是,主庫(kù)連接的從庫(kù)中至少有 N 個(gè)從庫(kù),和主庫(kù)進(jìn)行數(shù)據(jù)復(fù)制時(shí)的 ACK 消息延遲不能超過(guò) T 秒,否則,主庫(kù)就不會(huì)再接收客戶端的請(qǐng)求了。
??等到新主庫(kù)上線時(shí),就只有新主庫(kù)能接收和處理客戶端請(qǐng)求,此時(shí),新寫的數(shù)據(jù)會(huì)被直接寫到新主庫(kù)中。而原主庫(kù)會(huì)被哨兵降為從庫(kù),即使它的數(shù)據(jù)被清空了,也不會(huì)有新數(shù)據(jù)丟失。
??假設(shè)我們將 min-slaves-to-write 設(shè)置為 1,把 min-slaves-max-lag 設(shè)置為 12s,把哨兵的 down-after-milliseconds 設(shè)置為 10s,主庫(kù)因?yàn)槟承┰蚩ㄗ×?15s,導(dǎo)致哨兵判斷主庫(kù)客觀下線,開始進(jìn)行主從切換。同時(shí),因?yàn)樵鲙?kù)卡住了 15s,沒(méi)有一個(gè)從庫(kù)能和原主庫(kù)在 12s 內(nèi)進(jìn)行數(shù)據(jù)復(fù)制,原主庫(kù)也無(wú)法接收客戶端請(qǐng)求了。這樣一來(lái),主從切換完成后,也只有新主庫(kù)能接收請(qǐng)求,不會(huì)發(fā)生腦裂,也就不會(huì)發(fā)生數(shù)據(jù)丟失的問(wèn)題了。
小結(jié)
??這節(jié)課,我們學(xué)習(xí)了主從切換時(shí)可能遇到的腦裂問(wèn)題。腦裂是指在主從集群中,同時(shí)有兩個(gè)主庫(kù)都能接收寫請(qǐng)求。在 Redis 的主從切換過(guò)程中,如果發(fā)生了腦裂,客戶端數(shù)據(jù)就會(huì)寫入到原主庫(kù),如果原主庫(kù)被降為從庫(kù),這些新寫入的數(shù)據(jù)就丟失了。
??為了應(yīng)對(duì)腦裂,你可以在主從集群部署時(shí),
通過(guò)合理地配置參數(shù) min-slaves-to-write 和min-slaves-max-lag,來(lái)預(yù)防腦裂的發(fā)生。
??在實(shí)際應(yīng)用中,可能會(huì)因?yàn)榫W(wǎng)絡(luò)暫時(shí)擁塞導(dǎo)致從庫(kù)暫時(shí)和主庫(kù)的 ACK 消息超時(shí)。在這種情況下,并不是主庫(kù)假故障,我們也不用禁止主庫(kù)接收請(qǐng)求。
??所以,我給你的建議是,假設(shè)從庫(kù)有 K 個(gè),可以將 min-slaves-to-write 設(shè)置為K/2+1(如果 K 等于 1,就設(shè)為 1),將 min-slaves-max-lag 設(shè)置為十幾秒(例如 10~20s),在這個(gè)配置下,如果有一半以上的從庫(kù)和主庫(kù)進(jìn)行的 ACK 消息延遲超過(guò)十幾秒,我們就禁止主庫(kù)接收客戶端寫請(qǐng)求。
總結(jié)
以上是生活随笔為你收集整理的11 | 脑裂: 一次奇怪的数据丢失的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 04 | 内存快照:宕机后, Redis
- 下一篇: 05丨深入浅出索引(下)