還能再漲23%!AI寵兒NVIDIA成大摩明年首選AMD FSR 4.0將與RX 9070 XT顯卡同步登場(chǎng)羅永浩細(xì)紅線最新進(jìn)展,暫別AR,迎來(lái)AI Jarvis構(gòu)建堅(jiān)實(shí)數(shù)據(jù)地基,南京打造可信數(shù)據(jù)空間引領(lǐng)數(shù)字城市建設(shè)下單前先比價(jià)不花冤枉錢(qián) 同款圖書(shū)京東價(jià)低于抖音6折日媒感慨中國(guó)電動(dòng)汽車/智駕遙遙領(lǐng)先:本田、日產(chǎn)、三菱合并也沒(méi)戲消委會(huì)吹風(fēng)機(jī)品質(zhì)檢測(cè)結(jié)果揭曉 徠芬獨(dú)占鰲頭 共話新質(zhì)營(yíng)銷力,2024梅花數(shù)據(jù)峰會(huì)圓滿落幕索尼影像專業(yè)服務(wù) PRO Support 升級(jí),成為會(huì)員至少需注冊(cè) 2 臺(tái) α 全畫(huà)幅相機(jī)、3 支 G 大師鏡頭消息稱vivo加碼電池軍備競(jìng)賽:6500mAh 旗艦機(jī)+7500mAh中端機(jī)寶馬M8雙門(mén)轎跑車明年年初將停產(chǎn),后續(xù)無(wú)2026款車型比亞迪:2025 款漢家族車型城市領(lǐng)航智駕功能開(kāi)啟內(nèi)測(cè)雷神預(yù)告2025年首次出席CES 將發(fā)布三款不同技術(shù)原理智能眼鏡realme真我全球首發(fā)聯(lián)發(fā)科天璣 8400 耐玩戰(zhàn)神共創(chuàng)計(jì)劃iQOO Z9 Turbo長(zhǎng)續(xù)航版手機(jī)被曝電池加大到6400mAh,搭驍龍 8s Gen 3處理器普及放緩 銷量大跌:曝保時(shí)捷將重新評(píng)估電動(dòng)汽車計(jì)劃來(lái)京東參與榮耀Magic7 RSR 保時(shí)捷設(shè)計(jì)預(yù)售 享365天只換不修國(guó)補(bǔ)期間電視迎來(lái)?yè)Q機(jī)潮,最暢銷MiniLED品牌花落誰(shuí)家?美團(tuán)旗下微信社群團(tuán)購(gòu)業(yè)務(wù)“團(tuán)買(mǎi)買(mǎi)”宣布年底停運(yùn)消息稱微軟正與第三方廠商洽談,試圖合作推出Xbox游戲掌機(jī)設(shè)備
  • 首頁(yè) > 數(shù)據(jù)存儲(chǔ)頻道 > 數(shù)據(jù)庫(kù)頻道 > 軟件架構(gòu)

    Replication(上):常見(jiàn)的復(fù)制模型&分布式系統(tǒng)的挑戰(zhàn)

    2022年08月29日 11:00:13   來(lái)源:美團(tuán)技術(shù)團(tuán)隊(duì)

      分布式系統(tǒng)設(shè)計(jì)是一項(xiàng)十分復(fù)雜且具有挑戰(zhàn)性的事情。其中,數(shù)據(jù)復(fù)制與一致性更是其中十分重要的一環(huán)。數(shù)據(jù)復(fù)制領(lǐng)域概念龐雜、理論性強(qiáng),如果對(duì)應(yīng)的算法沒(méi)有理論驗(yàn)證大概率會(huì)出錯(cuò)。如果在設(shè)計(jì)過(guò)程中,不了解對(duì)應(yīng)理論所解決的問(wèn)題以及不同理論之間的聯(lián)系,勢(shì)必?zé)o法設(shè)計(jì)出一個(gè)合理的分布式系統(tǒng)。

      本系列文章分上下兩篇,以《數(shù)據(jù)密集型應(yīng)用系統(tǒng)設(shè)計(jì)(DDIA)》(下文簡(jiǎn)稱《DDIA》)為主線,文中的核心理論講解與圖片來(lái)自于此書(shū)。在此基礎(chǔ)上,加入了日常工作中對(duì)這些概念的理解與個(gè)性化的思考,并將它們映射到Kafka中,跟大家分享一下如何將具體的理論應(yīng)用于實(shí)際生產(chǎn)環(huán)境中。

      1. 簡(jiǎn)介

      1.1 簡(jiǎn)介——使用復(fù)制的目的

      在分布式系統(tǒng)中,數(shù)據(jù)通常需要被分散在多臺(tái)機(jī)器上,主要為了達(dá)到以下目的:

      擴(kuò)展性,數(shù)據(jù)量因讀寫(xiě)負(fù)載巨大,一臺(tái)機(jī)器無(wú)法承載,數(shù)據(jù)分散在多臺(tái)機(jī)器上可以有效地進(jìn)行負(fù)載均衡,達(dá)到靈活的橫向擴(kuò)展。

      容錯(cuò)、高可用,在分布式系統(tǒng)中,單機(jī)故障是常態(tài),在單機(jī)故障下仍然希望系統(tǒng)能夠正常工作,這時(shí)候就需要數(shù)據(jù)在多臺(tái)機(jī)器上做冗余,在遇到單機(jī)故障時(shí)其他機(jī)器就可以及時(shí)接管。

      統(tǒng)一的用戶體驗(yàn),如果系統(tǒng)客戶端分布在多個(gè)地域,通?紤]在多個(gè)地域部署服務(wù),以方便用戶能夠就近訪問(wèn)到他們所需要的數(shù)據(jù),獲得統(tǒng)一的用戶體驗(yàn)。

      數(shù)據(jù)的多機(jī)分布的方式主要有兩種,一種是將數(shù)據(jù)分片保存,每個(gè)機(jī)器保存數(shù)據(jù)的部分分片(Kafka中稱為Partition,其他部分系統(tǒng)稱為Shard),另一種則是完全的冗余,其中每一份數(shù)據(jù)叫做一個(gè)副本(Kafka中稱為Replica),通過(guò)數(shù)據(jù)復(fù)制技術(shù)實(shí)現(xiàn)。在分布式系統(tǒng)中,兩種方式通常會(huì)共同使用,最后的數(shù)據(jù)分布往往是下圖的樣子,一臺(tái)機(jī)器上會(huì)保存不同數(shù)據(jù)分片的若干個(gè)副本。本系列博文主要介紹的是數(shù)據(jù)如何做復(fù)制,分區(qū)則是另一個(gè)主題,不在本文的討論范疇。

      圖1 常見(jiàn)數(shù)據(jù)分布

      復(fù)制的目標(biāo)需要保證若干個(gè)副本上的數(shù)據(jù)是一致的,這里的“一致”是一個(gè)十分不確定的詞,既可以是不同副本上的數(shù)據(jù)在任何時(shí)刻都保持完全一致,也可以是不同客戶端不同時(shí)刻訪問(wèn)到的數(shù)據(jù)保持一致。一致性的強(qiáng)弱也會(huì)不同,有可能需要任何時(shí)候不同客端都能訪問(wèn)到相同的新的數(shù)據(jù),也有可能是不同客戶端某一時(shí)刻訪問(wèn)的數(shù)據(jù)不相同,但在一段時(shí)間后可以訪問(wèn)到相同的數(shù)據(jù)。因此,“一致性”是一個(gè)值得單獨(dú)抽出來(lái)細(xì)說(shuō)的詞。在下一篇文章中,我們將重點(diǎn)介紹這個(gè)詞在不同上下文之間的含義。

      此時(shí),大家可能會(huì)有疑問(wèn),直接讓所有副本在任意時(shí)刻都保持一致不就行了,為啥還要有各種不同的一致性呢?我們認(rèn)為有兩個(gè)考量點(diǎn),第一是性能,第二則是復(fù)雜性。

      性能比較好理解,因?yàn)槿哂嗟哪康牟煌耆菫榱烁呖捎茫有延遲和負(fù)載均衡這類提升性能的目的,如果只一味地為了地強(qiáng)調(diào)數(shù)據(jù)一致,可能得不償失。復(fù)雜性是因?yàn)榉植际较到y(tǒng)中,有著比單機(jī)系統(tǒng)更加復(fù)雜的不確定性,節(jié)點(diǎn)之間由于采用不大可靠的網(wǎng)絡(luò)進(jìn)行傳輸,并且不能共享統(tǒng)一的一套系統(tǒng)時(shí)間和內(nèi)存地址(后文會(huì)詳細(xì)進(jìn)行說(shuō)明),這使得原本在一些單機(jī)系統(tǒng)上很簡(jiǎn)單的事情,在轉(zhuǎn)到分布式系統(tǒng)上以后就變得異常復(fù)雜。這種復(fù)雜性和不確定性甚至?xí)屛覀儜岩,這些副本上的數(shù)據(jù)真的能達(dá)成一致嗎?下一篇文章會(huì)專門(mén)詳細(xì)分析如何設(shè)計(jì)算法來(lái)應(yīng)對(duì)這種復(fù)雜和不確定性。

      1.2 文章系列概述

      本系列博文將分為上下兩篇,第一篇將主要介紹幾種常見(jiàn)的數(shù)據(jù)復(fù)制模型,然后介紹分布式系統(tǒng)的挑戰(zhàn),讓大家對(duì)分布式系統(tǒng)一些稀奇古怪的故障有一些感性的認(rèn)識(shí)。

      第二篇文章將針對(duì)本篇中提到的問(wèn)題,分別介紹事務(wù)、分布式共識(shí)算法和一致性,以及三者的內(nèi)在聯(lián)系,再分享如何在分布式系統(tǒng)中保證數(shù)據(jù)的一致性,進(jìn)而讓大家對(duì)數(shù)據(jù)復(fù)制技術(shù)有一個(gè)較為全面的認(rèn)識(shí)。此外,本系列還將介紹業(yè)界驗(yàn)證分布式算法正確性的一些工具和框架。接下來(lái),讓我們一起開(kāi)始數(shù)據(jù)復(fù)制之旅吧!

      2. 數(shù)據(jù)復(fù)制模式

      總體而言,最常見(jiàn)的復(fù)制模式有三種,分別為主從模式、多主節(jié)點(diǎn)模式、無(wú)主節(jié)點(diǎn)模式,下面分別進(jìn)行介紹。

      2.1 最簡(jiǎn)單的復(fù)制模式——主從模式

      簡(jiǎn)介

      對(duì)復(fù)制而言,最直觀的方法就是將副本賦予不同的角色,其中有一個(gè)主副本,主副本將數(shù)據(jù)存儲(chǔ)在本地后,將數(shù)據(jù)更改作為日志,或者以更改流的方式發(fā)到各個(gè)從副本(后文也會(huì)稱節(jié)點(diǎn))中。在這種模式下,所有寫(xiě)請(qǐng)求就全部會(huì)寫(xiě)入到主節(jié)點(diǎn)上,讀請(qǐng)求既可以由主副本承擔(dān)也可以由從副本承擔(dān),這樣對(duì)于讀請(qǐng)求而言就具備了擴(kuò)展性,并進(jìn)行了負(fù)載均衡。但這里面存在一個(gè)權(quán)衡點(diǎn),就是客戶端視角看到的一致性問(wèn)題。這個(gè)權(quán)衡點(diǎn)存在的核心在于,數(shù)據(jù)傳輸是通過(guò)網(wǎng)絡(luò)傳遞的,數(shù)據(jù)在網(wǎng)絡(luò)中傳輸?shù)臅r(shí)間是不能忽略的。

      圖2 同步復(fù)制與異步復(fù)制

      如上圖所示,在這個(gè)時(shí)間窗口中,任何情況都有可能發(fā)生。在這種情況下,客戶端何時(shí)算寫(xiě)入完成,會(huì)決定其他客戶端讀到數(shù)據(jù)的可能性。這里我們假設(shè)這份數(shù)據(jù)有一個(gè)主副本和一個(gè)從副本,如果主副本保存后即向客戶端返回成功,這樣叫做異步復(fù)制(1)。而如果等到數(shù)據(jù)傳送到從副本1,并得到確認(rèn)之后再返回客戶端成功,稱為同步復(fù)制(2)。這里我們先假設(shè)系統(tǒng)正常運(yùn)行,在異步同步下,如果從副本承擔(dān)讀請(qǐng)求,假設(shè)reader1和reader2同時(shí)在客戶端收到寫(xiě)入成功后發(fā)出讀請(qǐng)求,兩個(gè)reader就可能讀到不一樣的值。

      為了避免這種情況,實(shí)際上有兩種角度的做法,第一種角度是讓客戶端只從主副本讀取數(shù)據(jù),這樣,在正常情況下,所有客戶端讀到的數(shù)據(jù)一定是一致的(Kafka當(dāng)前的做法);另一種角度則是采用同步復(fù)制,假設(shè)使用純的同步復(fù)制,當(dāng)有多個(gè)副本時(shí),任何一個(gè)副本所在的節(jié)點(diǎn)發(fā)生故障,都會(huì)使寫(xiě)請(qǐng)求阻塞,同時(shí)每次寫(xiě)請(qǐng)求都需要等待所有節(jié)點(diǎn)確認(rèn),如果副本過(guò)多會(huì)極大影響吞吐量。而如果僅采用異步復(fù)制并由主副本承擔(dān)讀請(qǐng)求,當(dāng)主節(jié)點(diǎn)故障發(fā)生切換時(shí),一樣會(huì)發(fā)生數(shù)據(jù)不一致的問(wèn)題。

      很多系統(tǒng)會(huì)把這個(gè)決策權(quán)交給用戶,這里我們以Kafka為例,首先提供了同步與異步復(fù)制的語(yǔ)義(通過(guò)客戶端的acks參數(shù)確定),另外提供了ISR機(jī)制,而只需要ISR中的副本確認(rèn)即可,系統(tǒng)可以容忍部分節(jié)點(diǎn)因?yàn)楦鞣N故障而脫離ISR,那樣客戶端將不用等待其確認(rèn),增加了系統(tǒng)的容錯(cuò)性。當(dāng)前Kafka未提供讓從節(jié)點(diǎn)承擔(dān)讀請(qǐng)求的設(shè)計(jì),但在高版本中已經(jīng)有了這個(gè)Feature。這種方式使系統(tǒng)有了更大的靈活性,用戶可以根據(jù)場(chǎng)景自由權(quán)衡一致性和可用性。

      主從模式下需要的一些能力

      增加新的從副本(節(jié)點(diǎn))

      1. 在Kafka中,我們所采取的的方式是通過(guò)新建副本分配的方式,以追趕的方式從主副本中同步數(shù)據(jù)。

      2. 數(shù)據(jù)庫(kù)所采用的的方式是通過(guò)快照+增量的方式實(shí)現(xiàn)。

      a.在某一個(gè)時(shí)間點(diǎn)產(chǎn)生一個(gè)一致性的快照。

      b.將快照拷貝到從節(jié)點(diǎn)。

      c.從節(jié)點(diǎn)連接到主節(jié)點(diǎn)請(qǐng)求所有快照點(diǎn)后發(fā)生的改變?nèi)罩尽?/p>

      d.獲取到日志后,應(yīng)用日志到自己的副本中,稱之為追趕。

      e.可能重復(fù)多輪a-d。

      處理節(jié)點(diǎn)失效

      從節(jié)點(diǎn)失效——追趕式恢復(fù)

      針對(duì)從節(jié)點(diǎn)失效,恢復(fù)手段較為簡(jiǎn)單,一般采用追趕式恢復(fù)。而對(duì)于數(shù)據(jù)庫(kù)而言,從節(jié)點(diǎn)可以知道在崩潰前所執(zhí)行的最后一個(gè)事務(wù),然后連接主節(jié)點(diǎn),從該節(jié)點(diǎn)將拉取所有的事件變更,將這些變更應(yīng)用到本地記錄即可完成追趕。

      對(duì)于Kafka而言,恢復(fù)也是類似的,Kafka在運(yùn)行過(guò)程中,會(huì)定期項(xiàng)磁盤(pán)文件中寫(xiě)入checkpoint,共包含兩個(gè)文件,一個(gè)是recovery-point-offset-checkpoint,記錄已經(jīng)寫(xiě)到磁盤(pán)的offset,另一個(gè)則是replication-offset-checkpoint,用來(lái)記錄高水位(下文簡(jiǎn)稱HW),由ReplicaManager寫(xiě)入,下一次恢復(fù)時(shí),Broker將讀取兩個(gè)文件的內(nèi)容,可能有些被記錄到本地磁盤(pán)上的日志沒(méi)有提交,這時(shí)就會(huì)先截?cái)?Truncate)到HW對(duì)應(yīng)的offset上,然后從這個(gè)offset開(kāi)始從Leader副本拉取數(shù)據(jù),直到認(rèn)追上Leader,被加入到ISR集合中

      主節(jié)點(diǎn)失效——節(jié)點(diǎn)切換

      主節(jié)點(diǎn)失效則會(huì)稍稍復(fù)雜一些,需要經(jīng)歷三個(gè)步驟來(lái)完成節(jié)點(diǎn)的切換。

      確認(rèn)主節(jié)點(diǎn)失效,由于失效的原因有多種多樣,大多數(shù)系統(tǒng)會(huì)采用超時(shí)來(lái)判定節(jié)點(diǎn)失效。一般都是采用節(jié)點(diǎn)間互發(fā)心跳的方式,如果發(fā)現(xiàn)某個(gè)節(jié)點(diǎn)在較長(zhǎng)時(shí)間內(nèi)無(wú)響應(yīng),則會(huì)認(rèn)定為節(jié)點(diǎn)失效。具體到Kafka中,它是通過(guò)和Zookeeper(下文簡(jiǎn)稱ZK)間的會(huì)話來(lái)保持心跳的,在啟動(dòng)時(shí)Kafka會(huì)在ZK上注冊(cè)臨時(shí)節(jié)點(diǎn),此后會(huì)和ZK間維持會(huì)話,假設(shè)Kafka節(jié)點(diǎn)出現(xiàn)故障(這里指被動(dòng)的掉線,不包含主動(dòng)執(zhí)行停服的操作),當(dāng)會(huì)話心跳超時(shí)時(shí),ZK上的臨時(shí)節(jié)點(diǎn)會(huì)掉線,這時(shí)會(huì)有專門(mén)的組件(Controller)監(jiān)聽(tīng)到這一信息,并認(rèn)定節(jié)點(diǎn)失效。

      選舉新的主節(jié)點(diǎn)。這里可以通過(guò)通過(guò)選舉的方式(民主協(xié)商投票,通常使用共識(shí)算法),或由某個(gè)特定的組件指定某個(gè)節(jié)點(diǎn)作為新的節(jié)點(diǎn)(Kafka的Controller)。在選舉或指定時(shí),需要盡可能地讓新主與原主的差距最小,這樣會(huì)最小化數(shù)據(jù)丟失的風(fēng)險(xiǎn)(讓所有節(jié)點(diǎn)都認(rèn)可新的主節(jié)點(diǎn)是典型的共識(shí)問(wèn)題)--這里所謂共識(shí),就是讓一個(gè)小組的節(jié)點(diǎn)就某一個(gè)議題達(dá)成一致,下一篇文章會(huì)重點(diǎn)進(jìn)行介紹。

      重新配置系統(tǒng)是新的主節(jié)點(diǎn)生效,這一階段基本可以理解為對(duì)集群的元數(shù)據(jù)進(jìn)行修改,讓所有外界知道新主節(jié)點(diǎn)的存在(Kafka中Controller通過(guò)元數(shù)據(jù)廣播實(shí)現(xiàn)),后續(xù)及時(shí)舊的節(jié)點(diǎn)啟動(dòng),也需要確保它不能再認(rèn)為自己是主節(jié)點(diǎn),從而承擔(dān)寫(xiě)請(qǐng)求。

      問(wèn)題

      雖然上述三個(gè)步驟較為清晰,但在實(shí)際發(fā)生時(shí),還會(huì)存在一些問(wèn)題:

      假設(shè)采用異步復(fù)制,在失效前,新的主節(jié)點(diǎn)與原主節(jié)點(diǎn)的數(shù)據(jù)存在Gap,選舉完成后,原主節(jié)點(diǎn)很快重新上線加入到集群,這時(shí)新的主節(jié)點(diǎn)可能會(huì)收到?jīng)_突的寫(xiě)請(qǐng)求,此時(shí)還未完全執(zhí)行上述步驟的第三步,也就是原主節(jié)點(diǎn)沒(méi)有意識(shí)到自己的角色發(fā)生變化,還會(huì)嘗試向新主節(jié)點(diǎn)同步數(shù)據(jù)。這時(shí),一般的做法是,將原主節(jié)點(diǎn)上未完成復(fù)制的寫(xiě)請(qǐng)求丟掉,但這又可能會(huì)發(fā)生數(shù)據(jù)丟失或不一致,假設(shè)我們每條數(shù)據(jù)采用MySQL的自增ID作為主鍵,并且使用Redis作為緩存,假設(shè)發(fā)生了MySQL的主從切換,從節(jié)點(diǎn)的計(jì)數(shù)器落后于主節(jié)點(diǎn),那樣可能出現(xiàn)應(yīng)用獲取到舊的自增ID,這樣就會(huì)與Redis上對(duì)應(yīng)ID取到的數(shù)據(jù)不一致,出現(xiàn)數(shù)據(jù)泄露或丟失。

      假設(shè)上面的問(wèn)題,原主節(jié)點(diǎn)因?yàn)橐恍┕收嫌肋h(yuǎn)不知道自己角色已經(jīng)變更,則可能發(fā)生“腦裂”,兩個(gè)節(jié)點(diǎn)同時(shí)操作數(shù)據(jù),又沒(méi)有相應(yīng)解決沖突(沒(méi)有設(shè)計(jì)這一模塊),就有可能對(duì)數(shù)據(jù)造成破壞。

      此外,對(duì)于超時(shí)時(shí)間的設(shè)定也是個(gè)十分復(fù)雜的問(wèn)題,過(guò)長(zhǎng)會(huì)導(dǎo)致服務(wù)不可用,設(shè)置過(guò)短則會(huì)導(dǎo)致節(jié)點(diǎn)頻繁切換,假設(shè)本身系統(tǒng)處于高負(fù)載狀態(tài),頻繁角色切換會(huì)讓負(fù)載進(jìn)一步加重(團(tuán)隊(duì)內(nèi)部對(duì)Kafka僵尸節(jié)點(diǎn)的處理邏輯)。

      異步復(fù)制面臨的主要問(wèn)題——復(fù)制滯后

      如前文所述,如果我們使用純的同步復(fù)制,任何一臺(tái)機(jī)器發(fā)生故障都會(huì)導(dǎo)致服務(wù)不可寫(xiě)入,并且在數(shù)較多的情況下,吞吐和可用性都會(huì)受到比較大的影響。很多系統(tǒng)都會(huì)采用半步復(fù)制或異步復(fù)制來(lái)在可用性和一致性之間做權(quán)衡。

      在異步復(fù)制中,由于寫(xiě)請(qǐng)求寫(xiě)到主副本就返回成功,在數(shù)據(jù)復(fù)制到其他副本的過(guò)程中,如果客戶端進(jìn)行讀取,在不同副本讀取到的數(shù)據(jù)可能會(huì)不一致,《DDIA》將這個(gè)種現(xiàn)象稱為復(fù)制滯后(Replication Lag),存在這種問(wèn)題的復(fù)制行為所形成的數(shù)據(jù)一致性統(tǒng)稱為最終一致性。未來(lái)還會(huì)重點(diǎn)介紹一下一致性和共識(shí),但在本文不做過(guò)多的介紹,感興趣的同學(xué)可以提前閱讀《Problems with Replication Lag》這一章節(jié)。

      2.2 多主節(jié)點(diǎn)復(fù)制

      前文介紹的主從復(fù)制模型中存在一個(gè)比較嚴(yán)重的弊端,就是所有寫(xiě)請(qǐng)求都需要經(jīng)過(guò)主節(jié)點(diǎn),因?yàn)橹淮嬖谝粋(gè)主節(jié)點(diǎn),就很容易出現(xiàn)性能問(wèn)題。雖然有從節(jié)點(diǎn)作為冗余應(yīng)對(duì)容錯(cuò),但對(duì)于寫(xiě)入請(qǐng)求實(shí)際上這種復(fù)制方式是不具備擴(kuò)展性的。

      此外,如果客戶端來(lái)源于多個(gè)地域,不同客戶端所感知到的服務(wù)相應(yīng)時(shí)間差距會(huì)非常大。因此,有些系統(tǒng)順著傳統(tǒng)主從復(fù)制進(jìn)行延伸,采用多個(gè)主節(jié)點(diǎn)同時(shí)承擔(dān)寫(xiě)請(qǐng)求,主節(jié)點(diǎn)接到寫(xiě)入請(qǐng)求之后將數(shù)據(jù)同步到從節(jié)點(diǎn),不同的是,這個(gè)主節(jié)點(diǎn)可能還是其他節(jié)點(diǎn)的從節(jié)點(diǎn)。復(fù)制模式如下圖所示,可以看到兩個(gè)主節(jié)點(diǎn)在接到寫(xiě)請(qǐng)求后,將數(shù)據(jù)同步到同一個(gè)數(shù)據(jù)中心的從節(jié)點(diǎn)。此外,該主節(jié)點(diǎn)還將不斷同步在另一數(shù)據(jù)中心節(jié)點(diǎn)上的數(shù)據(jù),由于每個(gè)主節(jié)點(diǎn)同時(shí)處理其他主節(jié)點(diǎn)的數(shù)據(jù)和客戶端寫(xiě)入的數(shù)據(jù),因此需要模型中增加一個(gè)沖突處理模塊,最后寫(xiě)到主節(jié)點(diǎn)的數(shù)據(jù)需要解決沖突。

      圖3 多主節(jié)點(diǎn)復(fù)制

      使用場(chǎng)景

      a. 多數(shù)據(jù)中心部署

      一般采用多主節(jié)點(diǎn)復(fù)制,都是為了做多數(shù)據(jù)中心容災(zāi)或讓客戶端就近訪問(wèn)(用一個(gè)高大上的名詞叫做異地多活),在同一個(gè)地域使用多主節(jié)點(diǎn)意義不大,在多個(gè)地域或者數(shù)據(jù)中心部署相比主從復(fù)制模型有如下的優(yōu)勢(shì):

      性能提升:性能提升主要表現(xiàn)在兩個(gè)核心指標(biāo)上,首先從吞吐方面,傳統(tǒng)的主從模型所有寫(xiě)請(qǐng)求都會(huì)經(jīng)過(guò)主節(jié)點(diǎn),主節(jié)點(diǎn)如果無(wú)法采用數(shù)據(jù)分區(qū)的方式進(jìn)行負(fù)載均衡,可能存在性能瓶頸,采用多主節(jié)點(diǎn)復(fù)制模式下,同一份數(shù)據(jù)就可以進(jìn)行負(fù)載均衡,可以有效地提升吞吐。另外,由于多個(gè)主節(jié)點(diǎn)分布在多個(gè)地域,處于不同地域的客戶端可以就近將請(qǐng)求發(fā)送到對(duì)應(yīng)數(shù)據(jù)中心的主節(jié)點(diǎn),可以最大程度地保證不同地域的客戶端能夠以相似的延遲讀寫(xiě)數(shù)據(jù),提升用戶的使用體驗(yàn)。

      容忍數(shù)據(jù)中心失效:對(duì)于主從模式,假設(shè)主節(jié)點(diǎn)所在的數(shù)據(jù)中心發(fā)生網(wǎng)絡(luò)故障,需要發(fā)生一次節(jié)點(diǎn)切換才可將流量全部切換到另一個(gè)數(shù)據(jù)中心,而采用多主節(jié)點(diǎn)模式,則可無(wú)縫切換到新的數(shù)據(jù)中心,提升整體服務(wù)的可用性。

      b. 離線客戶端操作

      除了解決多個(gè)地域容錯(cuò)和就近訪問(wèn)的問(wèn)題,還有一些有趣的場(chǎng)景,其中一個(gè)場(chǎng)景則是在網(wǎng)絡(luò)離線的情況下還能繼續(xù)工作,例如我們筆記本電腦上的筆記或備忘錄,我們不能因?yàn)榫W(wǎng)絡(luò)離線就禁止使用該程序,我們依然可以在本地愉快的編輯內(nèi)容(圖中標(biāo)記為Offline狀態(tài)),當(dāng)我們連上網(wǎng)之后,這些內(nèi)容又會(huì)同步到遠(yuǎn)程的節(jié)點(diǎn)上,這里面我們把本地的App也當(dāng)做其中的一個(gè)副本,那么就可以承擔(dān)用戶在本地的變更請(qǐng)求。聯(lián)網(wǎng)之后,再同步到遠(yuǎn)程的主節(jié)點(diǎn)上。

      圖4 Notion界面

      c. 協(xié)同編輯

      這里我們對(duì)離線客戶端操作進(jìn)行擴(kuò)展,假設(shè)我們所有人同時(shí)編輯一個(gè)文檔,每個(gè)人通過(guò)Web客戶端編輯的文檔都可以看做一個(gè)主節(jié)點(diǎn)。這里我們拿美團(tuán)內(nèi)部的學(xué)城(內(nèi)部的Wiki系統(tǒng))舉例,當(dāng)我們正在編輯一份文檔的時(shí)候,基本上都會(huì)發(fā)現(xiàn)右上角會(huì)出現(xiàn)“xxx也在協(xié)同編輯文檔”的字樣,當(dāng)我們保存的時(shí)候,系統(tǒng)就會(huì)自動(dòng)將數(shù)據(jù)保存到本地并復(fù)制到其他主節(jié)點(diǎn)上,各自處理各自端上的沖突。

      另外,當(dāng)文檔出現(xiàn)了更新時(shí),學(xué)城會(huì)通知我們有更新,需要我們手動(dòng)點(diǎn)擊更新,來(lái)更新我們本地主節(jié)點(diǎn)的數(shù)據(jù)。書(shū)中說(shuō)明,雖然不能將協(xié)同編輯完全等同于數(shù)據(jù)庫(kù)復(fù)制,但卻是有很多相似之處,也需要處理沖突問(wèn)題。

      沖突解決

      通過(guò)上面的分析,我們了解到多主復(fù)制模型最大挑戰(zhàn)就是解決沖突,下面我們簡(jiǎn)單看下《DDIA》中給出的通用解法,在介紹之前,我們先來(lái)看一個(gè)典型的沖突。

      a. 沖突實(shí)例

      圖5 沖突實(shí)例

      在圖中,由于多主節(jié)點(diǎn)采用異步復(fù)制,用戶將數(shù)據(jù)寫(xiě)入到自己的網(wǎng)頁(yè)就返回成功了,但當(dāng)嘗試把數(shù)據(jù)復(fù)制到另一個(gè)主節(jié)點(diǎn)時(shí)就會(huì)出問(wèn)題,這里我們?nèi)绻僭O(shè)主節(jié)點(diǎn)更新時(shí)采用類似CAS的更新方式時(shí)更新時(shí),都會(huì)由于預(yù)期值不符合從而拒絕更新。針對(duì)這樣的沖突,書(shū)中給出了幾種常見(jiàn)的解決思路。

      b. 解決思路

      1. 避免沖突

      所謂解決問(wèn)題最根本的方式則是盡可能不讓它發(fā)生,如果能夠在應(yīng)用層保證對(duì)特定數(shù)據(jù)的請(qǐng)求只發(fā)生在一個(gè)節(jié)點(diǎn)上,這樣就沒(méi)有所謂的“寫(xiě)沖突”了。繼續(xù)拿上面的協(xié)同編輯文檔舉例,如果我們把每個(gè)人的都在填有自己姓名表格的一行里面進(jìn)行編輯,這樣就可以最大程度地保證每個(gè)人的修改范圍不會(huì)有重疊,沖突也就迎刃而解了

      2. 收斂于一致?tīng)顟B(tài)

      然而,對(duì)更新標(biāo)題這種情況而言,沖突是沒(méi)法避免的,但還是需要有方法解決。對(duì)于單主節(jié)點(diǎn)模式而言,如果同一個(gè)字段有多次寫(xiě)入,那么最后寫(xiě)入的一定是最新的。ZK、KafkaController、KafkaReplica都有類似Epoch的方式去屏蔽過(guò)期的寫(xiě)操作,由于所有的寫(xiě)請(qǐng)求都經(jīng)過(guò)同一個(gè)節(jié)點(diǎn),順序是絕對(duì)的,但對(duì)于多主節(jié)點(diǎn)而言,由于沒(méi)有絕對(duì)順序的保證,就只能試圖用一些方式來(lái)決策相對(duì)順序,使沖突最終收斂,這里提到了幾種方法:

      給每個(gè)寫(xiě)請(qǐng)求分配Uniq-ID,例如一個(gè)時(shí)間戳,一個(gè)隨機(jī)數(shù),一個(gè)UUID或Hash值,最終取最高的ID作為最新的寫(xiě)入。如果基于時(shí)間戳,則稱作最后寫(xiě)入者獲勝(LWW),這種方式看上去非常直接且簡(jiǎn)單,并且非常流行。但很遺憾,文章一開(kāi)始也提到了,分布式系統(tǒng)沒(méi)有辦法在機(jī)器間共享一套統(tǒng)一的系統(tǒng)時(shí)間,所以這個(gè)方案很有可能因?yàn)檫@個(gè)問(wèn)題導(dǎo)致數(shù)據(jù)丟失(時(shí)鐘漂移)。

      每個(gè)副本分配一個(gè)唯一的ID,ID高的更新優(yōu)先級(jí)高于地域低的,這顯然也會(huì)丟失數(shù)據(jù)。

      當(dāng)然,我們可以用某種方式做拼接,或利用預(yù)先定義的格式保留沖突相關(guān)信息,然后由用戶自行解決。

      3. 用戶自行處理

      其實(shí),把這個(gè)操作直接交給用戶,讓用戶自己在讀取或?qū)懭肭斑M(jìn)行沖突解決,這種例子也是屢見(jiàn)不鮮,Github采用就是這種方式。

      這里只是簡(jiǎn)單舉了一些沖突的例子,其實(shí)沖突的定義是一個(gè)很微妙的概念!禗DIA》第七章介紹了更多關(guān)于沖突的概念,感興趣同學(xué)可以先自行閱讀,在下一篇文章中也會(huì)提到這個(gè)問(wèn)題。

      c. 處理細(xì)節(jié)介紹

      此外,在書(shū)中將要結(jié)束《復(fù)制》這一章時(shí),也詳細(xì)介紹了如何進(jìn)行沖突的處理,這里也簡(jiǎn)單進(jìn)行介紹。

      這里我們可以思考一個(gè)問(wèn)題,為什么會(huì)發(fā)生沖突?通過(guò)閱讀具體的處理手段后,我們可以嘗試這樣理解,正是因?yàn)槲覀儗?duì)事件發(fā)生的先后順序不確定,但這些事件的處理主體都有重疊(比如都有設(shè)置某個(gè)數(shù)據(jù)的值)。通過(guò)我們對(duì)沖突的理解,加上我們的常識(shí)推測(cè),會(huì)有這樣幾種方式可以幫我們來(lái)判斷事件的先后順序。

      1. 直接指定事件順序

      對(duì)于事件發(fā)生的先后順序,我們一個(gè)最直觀的想法就是,兩個(gè)請(qǐng)求誰(shuí)新要誰(shuí)的,那這里定義“最新”是個(gè)問(wèn)題,一個(gè)很簡(jiǎn)單的方式是使用時(shí)間戳,這種算法叫做最后寫(xiě)入者獲勝LWW。

      但分布式系統(tǒng)中沒(méi)有統(tǒng)一的系統(tǒng)時(shí)鐘,不同機(jī)器上的時(shí)間戳無(wú)法保證精確同步,那就可能存在數(shù)據(jù)丟失的風(fēng)險(xiǎn),并且由于數(shù)據(jù)是覆蓋寫(xiě),可能不會(huì)保留中間值,那么最終可能也不是一致的狀態(tài),或出現(xiàn)數(shù)據(jù)丟失。如果是一些緩存系統(tǒng),覆蓋寫(xiě)看上去也是可以的,這種簡(jiǎn)單粗暴的算法是非常好的收斂沖突的方式,但如果我們對(duì)數(shù)據(jù)一致性要求較高,則這種方式就會(huì)引入風(fēng)險(xiǎn),除非數(shù)據(jù)寫(xiě)入一次后就不會(huì)發(fā)生改變。

      2. 從事件本身推斷因果關(guān)系和并發(fā)

      上面直接簡(jiǎn)單粗暴的制定很明顯過(guò)于武斷,那么有沒(méi)有可能時(shí)間里面就存在一些因果關(guān)系呢,如果有我們很顯然可以通過(guò)因果關(guān)系知道到底需要怎樣的順序,如果不行再通過(guò)指定的方式呢?

      例如:

      圖6 違背因果關(guān)系示例

      這里是書(shū)中一個(gè)多主節(jié)點(diǎn)復(fù)制的例子,這里ClientA首先向Leader1增加一條數(shù)據(jù)x=1,然Leader1采用異步復(fù)制的方式,將變更日志發(fā)送到其他的Leader上。在復(fù)制過(guò)程中,ClientB向Leader3發(fā)送了更新請(qǐng)求,內(nèi)容則是更新Key為x的Value,使Value=Value+1。

      原圖中想表達(dá)的是,update的日志發(fā)送到Leader2的時(shí)間早于insert日志發(fā)送到Leader2的時(shí)間,會(huì)導(dǎo)致更新的Key不存在。但是,這種所謂的事件關(guān)系本身就不是完全不相干的,書(shū)中稱這種關(guān)系為依賴或者Happens-before。

      我們可能在JVM的內(nèi)存模型(JMM)中聽(tīng)到過(guò)這個(gè)詞,在JMM中,表達(dá)的也是多個(gè)線程操作的先后順序關(guān)系。這里,如果我們把線程或者請(qǐng)求理解為對(duì)數(shù)據(jù)的操作(區(qū)別在于一個(gè)是對(duì)本地內(nèi)存數(shù)據(jù),另一個(gè)是對(duì)遠(yuǎn)程的某處內(nèi)存進(jìn)行修改),線程或客戶端都是一種執(zhí)行者(區(qū)別在于是否需要使用網(wǎng)絡(luò)),那這兩種Happens-before也就可以在本質(zhì)上進(jìn)行統(tǒng)一了,都是為了描述事件的先后順序而生。

      書(shū)中給出了檢測(cè)這類事件的一種算法,并舉了一個(gè)購(gòu)物車的例子,如圖所示(以餐廳掃碼點(diǎn)餐的場(chǎng)景為例):

      圖7 掃碼點(diǎn)餐示例

      圖中兩個(gè)客戶端同時(shí)向購(gòu)物車?yán)锓艝|西,事例中的數(shù)據(jù)庫(kù)假設(shè)只有一個(gè)副本。

      首先Client1向購(gòu)物車中添加牛奶,此時(shí)購(gòu)物車為空,返回版本1,Value為[牛奶]。

      此時(shí)Client2向其中添加雞蛋,其并不知道Client1添加了牛奶,但服務(wù)器可以知道,因此分配版本號(hào)為2,并且將雞蛋和牛奶存成兩個(gè)單獨(dú)的值,最后將兩個(gè)值和版本號(hào)2返回給客戶端。此時(shí)服務(wù)端存儲(chǔ)了[雞蛋] 2 [牛奶]1。

      同理,Client1添加面粉,這時(shí)候Client1只認(rèn)為添加了[牛奶],因此將面粉與牛奶合并發(fā)送給服務(wù)端[牛奶,面粉],同時(shí)還附帶了之前收到的版本號(hào)1,此時(shí)服務(wù)端知道,新值[牛奶,面粉]可以替換同一個(gè)版本號(hào)中的舊值[牛奶],但[雞蛋]是并發(fā)事件,分配版本號(hào)3,返回值[牛奶,面粉] 3 [雞蛋]2。

      同理,Client2向購(gòu)物車添加[火腿],但在之前的請(qǐng)求中,返回了[雞蛋][牛奶],因此和火腿合并發(fā)送給服務(wù)端[雞蛋,牛奶,火腿],同時(shí)附帶了版本號(hào)2,服務(wù)端直接將新值覆蓋之前版本2的值[雞蛋],但[牛奶,面粉]是并發(fā)事件,因此存儲(chǔ)值為[牛奶,面粉] 3 [雞蛋,牛奶,火腿] 4并分配版本號(hào)4。

      最后一次Client添加培根,通過(guò)之前返回的值里,知道有[牛奶,面粉,雞蛋],Client將值合并[牛奶,面粉,雞蛋,培根]聯(lián)通之前的版本號(hào)一起發(fā)送給服務(wù)端,服務(wù)端判斷[牛奶,面粉,雞蛋,培根]可以覆蓋之前的[牛奶,面粉]但[雞蛋,牛奶,火腿]是并發(fā)值,加以保留。

      通過(guò)上面的例子,我們看到了一個(gè)根據(jù)事件本身進(jìn)行因果關(guān)系的確定。書(shū)中給出了進(jìn)一步的抽象流程:

      服務(wù)端為每個(gè)主鍵維護(hù)一個(gè)版本號(hào),每當(dāng)主鍵新值寫(xiě)入時(shí)遞增版本號(hào),并將新版本號(hào)和寫(xiě)入值一起保存。

      客戶端寫(xiě)主鍵,寫(xiě)請(qǐng)求比包含之前讀到的版本號(hào),發(fā)送的值為之前請(qǐng)求讀到的值和新值的組合,寫(xiě)請(qǐng)求的相應(yīng)也會(huì)返回對(duì)當(dāng)前所有的值,這樣就可以一步步進(jìn)行拼接。

      當(dāng)服務(wù)器收到有特定版本號(hào)的寫(xiě)入時(shí),覆蓋該版本號(hào)或更低版本號(hào)的所有值,保留高于請(qǐng)求中版本號(hào)的新值(與當(dāng)前寫(xiě)操作屬于并發(fā))。

      有了這套算法,我們就可以檢測(cè)出事件中有因果關(guān)系的事件與并發(fā)的事件,而對(duì)于并發(fā)的事件,仍然像上文提到的那樣,需要依據(jù)一定的原則進(jìn)行合并,如果使用LWW,依然可能存在數(shù)據(jù)丟失的情況。因此,需要在服務(wù)端程序的合并邏輯中需要額外做些事情。

      在購(gòu)物車這個(gè)例子中,比較合理的是合并新值和舊值,即最后的值是[牛奶,雞蛋,面粉,火腿,培根],但這樣也會(huì)導(dǎo)致一個(gè)問(wèn)題,假設(shè)其中的一個(gè)用戶刪除了一項(xiàng)商品,但是union完還是會(huì)出現(xiàn)在最終的結(jié)果中,這顯然不符合預(yù)期。因此可以用一個(gè)類似的標(biāo)記位,標(biāo)記記錄的刪除,這樣在合并時(shí)可以將這個(gè)商品踢出,這個(gè)標(biāo)記在書(shū)中被稱為墓碑(Tombstone)。

      2.3 無(wú)主節(jié)點(diǎn)復(fù)制

      之前介紹的復(fù)制模式都是存在明確的主節(jié)點(diǎn),從節(jié)點(diǎn)的角色劃分的,主節(jié)點(diǎn)需要將數(shù)據(jù)復(fù)制到從節(jié)點(diǎn),所有寫(xiě)入的順序由主節(jié)點(diǎn)控制。但有些系統(tǒng)干脆放棄了這個(gè)思路,去掉了主節(jié)點(diǎn),任何副本都能直接接受來(lái)自客戶端的寫(xiě)請(qǐng)求,或者再有一些系統(tǒng)中,會(huì)給到一個(gè)協(xié)調(diào)者代表客戶端進(jìn)行寫(xiě)入(以Group Commit為例,由一個(gè)線程積攢所有客戶端的請(qǐng)求統(tǒng)一發(fā)送),與多主模式不同,協(xié)調(diào)者不負(fù)責(zé)控制寫(xiě)入順序,這個(gè)限制的不同會(huì)直接影響系統(tǒng)的使用方式。

      處理節(jié)點(diǎn)失效

      假設(shè)一個(gè)數(shù)據(jù)系統(tǒng)擁有三個(gè)副本,當(dāng)其中一個(gè)副本不可用時(shí),在主從模式中,如果恰好是主節(jié)點(diǎn),則需要進(jìn)行節(jié)點(diǎn)切換才能繼續(xù)對(duì)外提供服務(wù),但在無(wú)主模式下,并不存在這一步驟,如下圖所示:

      圖8 Quorum寫(xiě)入處理節(jié)點(diǎn)失效

      這里的Replica3在某一時(shí)刻無(wú)法提供服務(wù),此時(shí)用戶可以收到兩個(gè)Replica的寫(xiě)入成功的確認(rèn),即可認(rèn)為寫(xiě)入成功,而完全可以忽略那個(gè)無(wú)法提供服務(wù)的副本。當(dāng)失效的節(jié)點(diǎn)恢復(fù)時(shí),會(huì)重新提供讀寫(xiě)服務(wù),此時(shí)如果客戶端向這個(gè)副本讀取數(shù)據(jù),就會(huì)請(qǐng)求到過(guò)期值。

      為了解決這個(gè)問(wèn)題,這里客戶端就不是簡(jiǎn)單向一個(gè)節(jié)點(diǎn)請(qǐng)求數(shù)據(jù)了,而是向所有三個(gè)副本請(qǐng)求,這時(shí)可能會(huì)收到不同的響應(yīng),這時(shí)可以通過(guò)類似版本號(hào)來(lái)區(qū)分?jǐn)?shù)據(jù)的新舊(類似上文中并發(fā)寫(xiě)入的檢測(cè)方式)。這里可能有一個(gè)問(wèn)題,副本恢復(fù)之后難道就一直讓自己落后于其他副本嗎?這肯定不行,這會(huì)打破一致性的語(yǔ)義,因此需要一個(gè)機(jī)制。有兩種思路:

      客戶端讀取時(shí)對(duì)副本做修復(fù),如果客戶端通過(guò)并行讀取多個(gè)副本時(shí),讀到了過(guò)期的數(shù)據(jù),可以將數(shù)據(jù)寫(xiě)入到舊副本中,以便追趕上新副本。

      反熵查詢,一些系統(tǒng)在副本啟動(dòng)后,后臺(tái)會(huì)不斷查找副本之間的數(shù)據(jù)diff,將diff寫(xiě)到自己的副本中,與主從復(fù)制模式不同的是,此過(guò)程不保證寫(xiě)入的順序,并可能引發(fā)明顯的復(fù)制滯后。

      讀寫(xiě)Quorum

      上文中的實(shí)例我們可以看出,這種復(fù)制模式下,要想保證讀到的是寫(xiě)入的新值,每次只從一個(gè)副本讀取顯然是有問(wèn)題的,那么需要每次寫(xiě)幾個(gè)副本呢,又需要讀取幾個(gè)副本呢?這里的一個(gè)核心點(diǎn)就是讓寫(xiě)入的副本和讀取的副本有交集,那么我們就能夠保證讀到新值了。

      直接上公式:

      。其中N為副本的數(shù)量,w為每次并行寫(xiě)入的節(jié)點(diǎn)數(shù),r為每次同時(shí)讀取的節(jié)點(diǎn)數(shù),這個(gè)公式非常容易理解,就不做過(guò)多贅述。不過(guò)這里的公式雖然看著比較直白也簡(jiǎn)單,里面卻蘊(yùn)含了一些系統(tǒng)設(shè)計(jì)思考:

      一般配置方法,取

      w,r與N的關(guān)系決定了能夠容忍多少的節(jié)點(diǎn)失效

      假設(shè)N=3, w=2, r=2,可以容忍1個(gè)節(jié)點(diǎn)故障。

      假設(shè)N=5,w=3, r=3 可以容忍2個(gè)節(jié)點(diǎn)故障。

      N個(gè)節(jié)點(diǎn)可以容忍可以容忍

      個(gè)節(jié)點(diǎn)故障。

      在實(shí)際實(shí)現(xiàn)中,一般數(shù)據(jù)會(huì)發(fā)送或讀取所有節(jié)點(diǎn),w和r決定了我們需要等待幾個(gè)節(jié)點(diǎn)的寫(xiě)入或讀取確認(rèn)。

      Quorum一致性的局限性

      看上去這個(gè)簡(jiǎn)單的公式就可以實(shí)現(xiàn)很強(qiáng)大的功能,但這里有一些問(wèn)題值得注意:

      首先,Quorum并不是一定要求多數(shù),重要的是讀取的副本和寫(xiě)入副本有重合即可,可以按照讀寫(xiě)的可用性要求酌情考慮配置。

      另外,對(duì)于一些沒(méi)有很強(qiáng)一致性要求的系統(tǒng),可以配置w+r <= N,這樣可以等待更少的節(jié)點(diǎn)即可返回,這樣雖然有可能讀取到一個(gè)舊值,但這種配置可以很大提升系統(tǒng)的可用性,當(dāng)網(wǎng)絡(luò)大規(guī)模故障時(shí)更有概率讓系統(tǒng)繼續(xù)運(yùn)行而不是由于沒(méi)有達(dá)到Quorum限制而返回錯(cuò)誤。

      假設(shè)在w+r>N的情況下,實(shí)際上也存在邊界問(wèn)題導(dǎo)致一些一致性問(wèn)題:

      首先假設(shè)是Sloppy Quorum(一個(gè)更為寬松的Quorum算法),寫(xiě)入的w和讀取的r可能完全不相交,因此不能保證數(shù)據(jù)一定是新的。

      如果兩個(gè)寫(xiě)操作同時(shí)發(fā)生,那么還是存在沖突,在合并時(shí),如果基于LWW,仍然可能導(dǎo)致數(shù)據(jù)丟失。

      如果寫(xiě)讀同時(shí)發(fā)生,也不能保證讀請(qǐng)求一定就能取到新值,因?yàn)閺?fù)制具有滯后性(上文的復(fù)制窗口)。

      如果某些副本寫(xiě)入成功,其他副本寫(xiě)入失敗(磁盤(pán)空間滿)且總的成功數(shù)少于w,那些成功的副本數(shù)據(jù)并不會(huì)回滾,這意味著及時(shí)寫(xiě)入失敗,后續(xù)還是可能讀到新值。

      雖然,看上去Quorum復(fù)制模式可以保證獲取到新值,但實(shí)際情況并不是我們想象的樣子,這個(gè)協(xié)議到最后可能也只能達(dá)到一個(gè)最終的一致性,并且依然需要共識(shí)算法的加持。

      2.4 本章小結(jié)

      以上我們介紹了所有常見(jiàn)的復(fù)制模式,我們可以看到,每種模式都有一定的應(yīng)用場(chǎng)景和優(yōu)缺點(diǎn),但是很明顯,光有復(fù)制模式遠(yuǎn)遠(yuǎn)達(dá)不到數(shù)據(jù)的一致性,因?yàn)榉植际较到y(tǒng)中擁有太多的不確定性,需要后面各種事務(wù)、共識(shí)算法的幫忙才能去真正對(duì)抗那些“稀奇古怪”的問(wèn)題。

      到這里,可能會(huì)有同學(xué)就會(huì)問(wèn),到底都是些什么稀奇古怪的問(wèn)題呢?相比單機(jī)系統(tǒng)又有那些獨(dú)特的問(wèn)題呢?下面本文先來(lái)介紹分布式系統(tǒng)中的幾個(gè)最典型的挑戰(zhàn)(Trouble),讓一些同學(xué)小小地“絕望”一下,然后我們會(huì)下一篇文章中再揭曉答案。

      3. 分布式系統(tǒng)的挑戰(zhàn)

      這部分存在的意義主要想讓大家理解,為什么一些看似簡(jiǎn)單的問(wèn)題到了分布式系統(tǒng)中就會(huì)變得異常復(fù)雜。順便說(shuō)一聲,這一章都是一些“奇葩”現(xiàn)象,并沒(méi)有過(guò)于復(fù)雜的推理和證明,希望大家能夠較為輕松愉悅地看完這些內(nèi)容。

      3.1 部分失效

      這是分布式系統(tǒng)中特有的一個(gè)名詞,這里先看一個(gè)現(xiàn)實(shí)當(dāng)中的例子。假設(shè)老板想要處理一批文件,如果讓一個(gè)人做,需要十天。但老板覺(jué)得有點(diǎn)慢,于是他靈機(jī)一動(dòng),想到可以找十個(gè)人來(lái)搞定這件事,然后自己把工作安排好,認(rèn)為這十個(gè)人一天正好干完,于是向他的上級(jí)信誓旦旦地承諾一天搞定這件事。他把這十個(gè)人叫過(guò)來(lái),把任務(wù)分配給了他們,他們彼此建了個(gè)微信群,約定每個(gè)小時(shí)在群里匯報(bào)自己手上的工作進(jìn)度,并強(qiáng)調(diào)在晚上5點(diǎn)前需要通過(guò)郵件提交最后的結(jié)果。于是老版就去愉快的喝茶去了,但是現(xiàn)實(shí)卻讓他大跌眼鏡。

      首先,有個(gè)同學(xué)家里信號(hào)特別差,報(bào)告進(jìn)度的時(shí)候只成功報(bào)告了3個(gè)小時(shí)的,然后老板在微信里問(wèn),也收不到任何回復(fù),最后結(jié)果也沒(méi)法提交。另一個(gè)同學(xué)家的表由于長(zhǎng)期沒(méi)換電池,停在了下午四點(diǎn),結(jié)果那人看了兩次表都是四點(diǎn),所以一點(diǎn)都沒(méi)著急,中間還看了個(gè)電影,慢慢悠悠做完交上去了,他還以為老板會(huì)表?yè)P(yáng)他,提前了一小時(shí)交,結(jié)果實(shí)際上已經(jīng)是晚上八點(diǎn)了。還有一個(gè)同學(xué)因?yàn)榍耙惶鞗](méi)睡好,效率極低,而且也沒(méi)辦法再去高強(qiáng)度的工作了。結(jié)果到了晚上5點(diǎn),只有7個(gè)人完成了自己手頭上的工作。

      這個(gè)例子可能看起來(lái)并不是非常恰當(dāng),但基本可以描述分布式系統(tǒng)特有的問(wèn)題了。在分布式的系統(tǒng)中,我們會(huì)遇到各種“稀奇古怪”的故障,例如家里沒(méi)信號(hào)(網(wǎng)絡(luò)故障),不管怎么叫都不理你,或者斷斷續(xù)續(xù)的理你。另外,因?yàn)槊總(gè)人都是通過(guò)自己家的表看時(shí)間的,所謂的5點(diǎn)需要提交結(jié)果,在一定程度上舊失去了參考的絕對(duì)價(jià)值。因此,作為上面例子中的“老板”,不能那么自信的認(rèn)為一個(gè)人干工作需要10天,就可以放心交給10個(gè)人,讓他們一天搞定。

      我們需要有各種措施來(lái)應(yīng)對(duì)分派任務(wù)帶來(lái)的不確定性,回到分布式系統(tǒng)中,部分失效是分布式系統(tǒng)一定會(huì)出現(xiàn)的情況。作為系統(tǒng)本身的設(shè)計(jì)人員,我們所設(shè)計(jì)的系統(tǒng)需要能夠容忍這種問(wèn)題,相對(duì)單機(jī)系統(tǒng)來(lái)說(shuō),這就帶來(lái)了特有的復(fù)雜性。

      3.2 分布式系統(tǒng)特有的故障

      不可靠的網(wǎng)絡(luò)

      對(duì)于一個(gè)純的分布式系統(tǒng)而言,它的架構(gòu)大多為Share Nothing架構(gòu),即使是存算分離這種看似的Share Storage,它的底層存儲(chǔ)一樣是需要解決Share Nothing的。所謂Nothing,這里更傾向于叫Nothing but Network,網(wǎng)絡(luò)是不同節(jié)點(diǎn)間共享信息的唯一途徑,數(shù)據(jù)的傳輸主要通過(guò)以太網(wǎng)進(jìn)行傳輸,這是一種異步網(wǎng)絡(luò),也就是網(wǎng)絡(luò)本身并不保證發(fā)出去的數(shù)據(jù)包一定能被接到或是何時(shí)被收到。這里可能發(fā)生各種錯(cuò)誤,如下圖所示:

      圖9 不可靠的網(wǎng)絡(luò)

      請(qǐng)求丟失

      請(qǐng)求正在某個(gè)隊(duì)列中等待

      遠(yuǎn)程節(jié)點(diǎn)已經(jīng)失效

      遠(yuǎn)程節(jié)點(diǎn)無(wú)法響應(yīng)

      遠(yuǎn)程節(jié)點(diǎn)已經(jīng)處理完請(qǐng)求,但在ack的時(shí)候丟包

      遠(yuǎn)程接收節(jié)點(diǎn)已經(jīng)處理完請(qǐng)求,但回復(fù)處理很慢

      本文認(rèn)為,造成網(wǎng)絡(luò)不可靠的原因不光是以太網(wǎng)和IP包本身,其實(shí)應(yīng)用本身有時(shí)候異常也是造成網(wǎng)絡(luò)不可靠的一個(gè)誘因。因?yàn),我們所采用的?jié)點(diǎn)間傳輸協(xié)議大多是TCP,TCP是個(gè)端到端的協(xié)議,是需要發(fā)送端和接收端兩端內(nèi)核中明確維護(hù)數(shù)據(jù)結(jié)構(gòu)來(lái)維持連接的,如果應(yīng)用層發(fā)生了下面的問(wèn)題,那么網(wǎng)絡(luò)包就會(huì)在內(nèi)核的Socket Buffer中排隊(duì)得不到處理,或響應(yīng)得不到處理。

      應(yīng)用程序GC。

      處理節(jié)點(diǎn)在進(jìn)行重的磁盤(pán)I/O,導(dǎo)致CPU無(wú)法從中斷中恢復(fù)從而無(wú)法處理網(wǎng)絡(luò)請(qǐng)求。

      由于內(nèi)存換頁(yè)導(dǎo)致的顛簸。

      這些問(wèn)題和網(wǎng)絡(luò)本身的不穩(wěn)定性相疊加,使得外界認(rèn)為的網(wǎng)絡(luò)不靠譜的程度更加嚴(yán)重。因此這些不靠譜,會(huì)極大地加重上一章中的 復(fù)制滯后性,進(jìn)而帶來(lái)各種各樣的一致性問(wèn)題。

      應(yīng)對(duì)之道

      網(wǎng)絡(luò)異常相比其他單機(jī)上的錯(cuò)誤而言,可能多了一種不確定的返回狀態(tài),即延遲,而且延遲的時(shí)間完全無(wú)法預(yù)估。這會(huì)讓我們寫(xiě)起程序來(lái)異常頭疼,對(duì)于上一章中的問(wèn)題,我們可能無(wú)從知曉節(jié)點(diǎn)是否失效,因?yàn)槟惆l(fā)的請(qǐng)求壓根可能不會(huì)有人響應(yīng)你。因此,我們需要把上面的“不確定”變成一種確定的形式,那就是利用“超時(shí)”機(jī)制。這里引申出兩個(gè)問(wèn)題:

      1. 假設(shè)能夠檢測(cè)出失效,我們應(yīng)該如何應(yīng)對(duì)?

      a. 負(fù)載均衡需要避免往失效的節(jié)點(diǎn)上發(fā)數(shù)據(jù)(服務(wù)發(fā)現(xiàn)模塊中的健康檢查功能)。

      b. 如果在主從復(fù)制中,如果主節(jié)點(diǎn)失效,需要出發(fā)選舉機(jī)制(Kafka中的臨時(shí)節(jié)點(diǎn)掉線,Controller監(jiān)聽(tīng)到變更觸發(fā)新的選舉,Controller本身的選舉機(jī)制)。

      c. 如果服務(wù)進(jìn)程崩潰,但操作系統(tǒng)運(yùn)行正常,可以通過(guò)腳本通知其他節(jié)點(diǎn),以便新的節(jié)點(diǎn)來(lái)接替(Kafka的僵尸節(jié)點(diǎn)檢測(cè),會(huì)觸發(fā)強(qiáng)制的臨時(shí)節(jié)點(diǎn)掉線)。

      d. 如果路由器已經(jīng)確認(rèn)目標(biāo)節(jié)點(diǎn)不可訪問(wèn),則會(huì)返回ICMP不可達(dá)(ping不通走下線)。

      2. 如何設(shè)置超時(shí)時(shí)間是合理的?

      很遺憾地告訴大家,這里面實(shí)際上是個(gè)權(quán)衡的問(wèn)題,短的超時(shí)時(shí)間會(huì)更快地發(fā)現(xiàn)故障,但同時(shí)增加了誤判的風(fēng)險(xiǎn)。這里假設(shè)網(wǎng)絡(luò)正常,那么如果端到端的ping時(shí)間為d,處理時(shí)間為r,那么基本上請(qǐng)求會(huì)在2d+r的時(shí)間完成。但在現(xiàn)實(shí)中,我們無(wú)法假設(shè)異步網(wǎng)絡(luò)的具體延遲,實(shí)際情況可能會(huì)更復(fù)雜。因此這是一個(gè)十分靠經(jīng)驗(yàn)的工作。

      3.2 不可靠的時(shí)鐘

      說(shuō)完了“信號(hào)”的問(wèn)題,下面就要說(shuō)說(shuō)每家的“鐘表”——時(shí)鐘了,它主要用來(lái)做兩件事:

      描述當(dāng)前的絕對(duì)時(shí)間

      描述某件事情的持續(xù)時(shí)間

      在DDIA中,對(duì)于這兩類用途給出了兩種時(shí)間,一類成為墻上時(shí)鐘,它們會(huì)返回當(dāng)前的日期和時(shí)間,例如clock_gettime(CLOCK_REALTIME) 或者System.currentTimeMills,但這類反應(yīng)精確時(shí)間的API,由于時(shí)鐘同步的問(wèn)題,可能會(huì)出現(xiàn)回?fù)艿那闆r。因此,作為持續(xù)時(shí)間的測(cè)量通常采用單調(diào)時(shí)鐘,例如clock_gettime(CLOCK_MONOTONIC) 或者System.nanoTime。高版本的Kafka中把請(qǐng)求的相應(yīng)延遲計(jì)算全部換成了這個(gè)API實(shí)現(xiàn),應(yīng)該也是這個(gè)原因。

      這里時(shí)鐘同步的具體原理,以及如何會(huì)出現(xiàn)不準(zhǔn)確的問(wèn)題,這里就不再詳細(xì)介紹了,感興趣的同學(xué)可以自行閱讀書(shū)籍。下面將介紹一下如何使用時(shí)間戳來(lái)描述事件順序的案例,并展示如何因時(shí)鐘問(wèn)題導(dǎo)致事件順序判斷異常的:

      圖10 不可靠的時(shí)鐘

      這里我們發(fā)現(xiàn),Node1的時(shí)鐘比Node3快,當(dāng)兩個(gè)節(jié)點(diǎn)在處理完本地請(qǐng)求準(zhǔn)備寫(xiě)Node2時(shí)發(fā)生了問(wèn)題,原本ClientB的寫(xiě)入明顯晚于ClientA的寫(xiě)入,但最終的結(jié)果,卻由于Node1的時(shí)間戳更大而丟棄了本該保留的x+=1,這樣,如果我們使用LWW,一定會(huì)出現(xiàn)數(shù)據(jù)不符合預(yù)期的問(wèn)題。

      由于時(shí)鐘不準(zhǔn)確,這里就引入了統(tǒng)計(jì)學(xué)中的置信區(qū)間的概念,也就是這個(gè)時(shí)間到底在一個(gè)什么樣的范圍里,一般的API是無(wú)法返回類似這樣的信息的。不過(guò),Google的TrueTime API則恰恰能夠返回這種信息,其調(diào)用結(jié)果是一個(gè)區(qū)間,有了這樣的API,確實(shí)就可以用來(lái)做一些對(duì)其有依賴的事情了,例如Google自家的Spanner,就是使用TrueTime實(shí)現(xiàn)快照隔離。

      如何在這艱難的環(huán)境中設(shè)計(jì)系統(tǒng)

      上面介紹的問(wèn)題是不是挺“令人絕望”的?你可能發(fā)現(xiàn),現(xiàn)在時(shí)間可能是錯(cuò)的,測(cè)量可能是不準(zhǔn)的,你的請(qǐng)求可能得不到任何響應(yīng),你可能不知道它是不是還活著......這種環(huán)境真的讓設(shè)計(jì)分布式系統(tǒng)變得異常艱難,就像是你在100個(gè)人組成的大部門(mén)里面協(xié)調(diào)一些工作一樣,工作量異常的巨大且復(fù)雜。

      但好在我們并不是什么都做不了,以協(xié)調(diào)這件事為例,我們肯定不是武斷地聽(tīng)取一個(gè)人的意見(jiàn),讓我們回到學(xué)生時(shí)代。我們需要評(píng)選一位班長(zhǎng),肯定我們都經(jīng)歷過(guò)投票、唱票的環(huán)節(jié),最終得票最多的那個(gè)人當(dāng)選,有時(shí)可能還需要設(shè)置一個(gè)前提,需要得票超過(guò)半數(shù)。

      映射到分布式系統(tǒng)中也是如此,我們不能輕易地相信任何一臺(tái)節(jié)點(diǎn)的信息,因?yàn)樗刑嗟牟淮_定,因此更多的情況下,在分布式系統(tǒng)中如果我們需要就某個(gè)事情達(dá)成一致,也可以采取像競(jìng)選或議會(huì)一樣,大家協(xié)商、投票、仲裁決定一項(xiàng)提議達(dá)成一致,真相由多數(shù)人商議決定,從而達(dá)到大家的一致和統(tǒng)一,這也就是后面要介紹的分布式共識(shí)協(xié)議。這個(gè)協(xié)議能夠容忍一些節(jié)點(diǎn)的部分失效,或者莫名其妙的故障帶來(lái)的問(wèn)題,讓系統(tǒng)能夠正常地運(yùn)行下去,確保請(qǐng)求到的數(shù)據(jù)是可信的。

      下面給出一些實(shí)際分布式算法的理論模型,根據(jù)對(duì)于延遲的假設(shè)不同,這里介紹三種系統(tǒng)模型。

      1. 同步模型

      該模型主要假設(shè)網(wǎng)絡(luò)延遲是有界的,我們可以清楚地知道這個(gè)延遲的上下界,不管出現(xiàn)任何情況,它都不會(huì)超出這個(gè)界限。

      2. 半同步模型(大部分模型都是基于這個(gè)假設(shè))

      半同步模型認(rèn)為大部分情況下,網(wǎng)絡(luò)和延遲都是正常的,如果出現(xiàn)違背的情況,偏差可能會(huì)非常大。

      3. 異步模型

      對(duì)延遲不作任何假設(shè),沒(méi)有任何超時(shí)機(jī)制。

      而對(duì)于節(jié)點(diǎn)失效的處理,也存在三種模型,這里我們忽略惡意謊言的拜占庭模型,就剩下兩種。

      1.崩潰-終止模型(Crash-Stop):該模型中假設(shè)一個(gè)節(jié)點(diǎn)只能以一種方式發(fā)生故障,即崩潰,可能它會(huì)在任意時(shí)刻停止響應(yīng),然后永遠(yuǎn)無(wú)法恢復(fù)。

      2.崩潰-恢復(fù)模型:節(jié)點(diǎn)可能在任何時(shí)刻發(fā)生崩潰,可能會(huì)在一段時(shí)間后恢復(fù),并再次響應(yīng),在該模型中假設(shè),在持久化存儲(chǔ)中的數(shù)據(jù)將得以保存,而內(nèi)存中的數(shù)據(jù)會(huì)丟失。

      而多數(shù)的算法都是基于半同步模型+崩潰-恢復(fù)模型來(lái)進(jìn)行設(shè)計(jì)的。

      Safety and Liveness

      這兩個(gè)詞在分布式算法設(shè)計(jì)時(shí)起著十分關(guān)鍵的作用,其中安全性(Safety)表示沒(méi)有意外發(fā)生,假設(shè)違反了安全性原則,我們一定能夠指出它發(fā)生的時(shí)間點(diǎn),并且安全性一旦違反,無(wú)法撤銷。而活性(Liveness)則表示“預(yù)期的事情最終一定會(huì)發(fā)生”,可能我們無(wú)法明確具體的時(shí)間點(diǎn),但我們期望它在未來(lái)某個(gè)時(shí)間能夠滿足要求。

      在進(jìn)行分布式算法設(shè)計(jì)時(shí),通常需要必須滿足安全性,而活性的滿足需要具備一定的前提。

      4. 總結(jié)

      以上就是第一篇文章的內(nèi)容,簡(jiǎn)單做下回顧,本文首先介紹了復(fù)制的三種常見(jiàn)模型,分別是主從復(fù)制、多主復(fù)制和無(wú)主復(fù)制,然后分別介紹了這三種模型的特點(diǎn)、適用場(chǎng)景以及優(yōu)缺點(diǎn)。接下來(lái),我們用了一個(gè)現(xiàn)實(shí)生活中的例子,向大家展示了分布式系統(tǒng)中常見(jiàn)的兩個(gè)特有問(wèn)題,分別是節(jié)點(diǎn)的部分失效以及無(wú)法共享系統(tǒng)時(shí)鐘的問(wèn)題,這兩個(gè)問(wèn)題為我們?cè)O(shè)計(jì)分布式系統(tǒng)帶來(lái)了比較大的挑戰(zhàn)。如果沒(méi)有一些設(shè)計(jì)特定的措施,我們所設(shè)計(jì)的分布式系統(tǒng)將無(wú)法很好地滿足設(shè)計(jì)的初衷,用戶也無(wú)法通過(guò)分布式系統(tǒng)來(lái)完成自己想要的工作。

      以上這些問(wèn)題,我們會(huì)下篇文章《Replication(下):事務(wù),一致性與共識(shí)》中逐一進(jìn)行解決,而事務(wù)、一致性、共識(shí)這三個(gè)關(guān)鍵詞,會(huì)為我們?cè)谠O(shè)計(jì)分布式系統(tǒng)時(shí)保駕護(hù)航。

      文章內(nèi)容僅供閱讀,不構(gòu)成投資建議,請(qǐng)謹(jǐn)慎對(duì)待。投資者據(jù)此操作,風(fēng)險(xiǎn)自擔(dān)。

    即時(shí)

    新聞

    明火炊具市場(chǎng):三季度健康屬性貫穿全類目

    奧維云網(wǎng)(AVC)推總數(shù)據(jù)顯示,2024年1-9月明火炊具線上零售額94.2億元,同比增加3.1%,其中抖音渠道表現(xiàn)優(yōu)異,同比有14%的漲幅,傳統(tǒng)電商略有下滑,同比降低2.3%。

    企業(yè)IT

    重慶創(chuàng)新公積金應(yīng)用,“區(qū)塊鏈+政務(wù)服務(wù)”顯成效

    “以前都要去窗口辦,一套流程下來(lái)都要半個(gè)月了,現(xiàn)在方便多了!”打開(kāi)“重慶公積金”微信小程序,按照提示流程提交相關(guān)材料,僅幾秒鐘,重慶市民曾某的賬戶就打進(jìn)了21600元。

    3C消費(fèi)

    華碩ProArt創(chuàng)藝27 Pro PA279CRV顯示器,高能實(shí)力,創(chuàng)

    華碩ProArt創(chuàng)藝27 Pro PA279CRV顯示器,憑借其優(yōu)秀的性能配置和精準(zhǔn)的色彩呈現(xiàn)能力,為您的創(chuàng)作工作帶來(lái)實(shí)質(zhì)性的幫助,雙十一期間低至2799元,性價(jià)比很高,簡(jiǎn)直是創(chuàng)作者們的首選。