分布式系統(tǒng)設(shè)計(jì)中的通用方法
之前翻譯過一篇關(guān)于分布式系統(tǒng)的文章 https:// lichuanyang.top/posts/3 914/ ,在各個(gè)平臺(tái)都取得了不錯(cuò)的反響。因此,最近又重新整理了一下相關(guān)的知識(shí),結(jié)合一些這一年多里新的理解,重新整理了下這篇文章。
首先我們需要明確本文要討論的分布式系統(tǒng)是什么,簡單的說,就是滿足多節(jié)點(diǎn)和有狀態(tài)這兩個(gè)條件即可。多節(jié)點(diǎn)很好理解,有狀態(tài)則是指這個(gè)系統(tǒng)要維護(hù)一些數(shù)據(jù),不然的話,其實(shí)我們無腦的水平擴(kuò)容就沒有任何問題,也就不存在分布式系統(tǒng)的問題了。
常見的分布式系統(tǒng), 無論是mysql, cassandra, hbase這些數(shù)據(jù)庫,還是rocketmq, kafka, pulsar這樣的消息隊(duì)列,還是zookeeper之類的基礎(chǔ)設(shè)施,其實(shí)都滿足這兩個(gè)條件。
這些分布式系統(tǒng)的實(shí)現(xiàn)通常來說主要需要關(guān)注兩個(gè)方面:一是自己本身功能的實(shí)現(xiàn),二是在分布式環(huán)境下保持良好的性能與穩(wěn)定性;即便是兩個(gè)功能完全不一樣的系統(tǒng),其對(duì)第二類問題的處理方式也會(huì)有很多相似之處。本文的關(guān)注重點(diǎn)也即在對(duì)第二類問題的處理上。
接下來,我們列舉一下分布式系統(tǒng)都有哪些常見目標(biāo),包括而不限于:
- 大量普通的服務(wù)器通過網(wǎng)絡(luò)互聯(lián),對(duì)外作為整體提供服務(wù);
- 隨著集群規(guī)模增長,系統(tǒng)整體性能表現(xiàn)為線性增長;
- 能夠自動(dòng)容錯(cuò),故障節(jié)點(diǎn)自動(dòng)遷移,不同節(jié)點(diǎn)的數(shù)據(jù)要能保持一致性;
要達(dá)成這些目標(biāo),又有哪些挑戰(zhàn)呢?大概有以下這些:
- 進(jìn)程崩潰: 原因很多,包括硬件故障、軟件故障、正常的例行維護(hù)等等,在云環(huán)境下會(huì)有一些更加復(fù)雜的原因;進(jìn)程崩潰導(dǎo)致的最大問題就是會(huì)丟數(shù)。出于性能的考慮,很多情況下我們不會(huì)進(jìn)行同步的寫磁盤,而是會(huì)將數(shù)據(jù)暫時(shí)放在內(nèi)存的緩沖區(qū),再定期刷入磁盤。而在進(jìn)程崩潰的時(shí)候,內(nèi)存緩沖區(qū)中的數(shù)據(jù)顯然會(huì)丟失。
- 網(wǎng)絡(luò)延遲和中斷: 節(jié)點(diǎn)的通信變到很慢時(shí),一個(gè)節(jié)點(diǎn)如何確認(rèn)另一個(gè)節(jié)點(diǎn)是否正常;
- 網(wǎng)絡(luò)分區(qū): 集群中節(jié)點(diǎn)分裂成兩個(gè)子集,子集內(nèi)通信正常,子集之間斷開(腦裂),這時(shí)候集群要如何提供服務(wù)。
這里插一個(gè)彩蛋,在CAP理論的前提下,現(xiàn)實(shí)中的系統(tǒng)通常只有兩種模式:放棄高可用的CP模式和放棄強(qiáng)一致性的AP模式。為什么沒有一種放棄分區(qū)容忍性的CA模式?就是因?yàn)槲覀儫o法假設(shè)網(wǎng)絡(luò)通信一定正常,而一旦接受了集群變成兩個(gè)分區(qū),再想合并回來就不現(xiàn)實(shí)了。
- 進(jìn)程暫停:比如full gc之類的原因?qū)е逻M(jìn)程出現(xiàn)短暫的不可用后又迅速恢復(fù),不可用期間集群有可能已經(jīng)做出了相關(guān)的反應(yīng),當(dāng)這個(gè)節(jié)點(diǎn)再恢復(fù)的時(shí)候如何維持狀態(tài)的一致性。
- 時(shí)鐘不同步和消息亂序:集群內(nèi)不同節(jié)點(diǎn)的操作,我們希望它的順序是明確的;不同節(jié)點(diǎn)之間的時(shí)鐘不同步,會(huì)導(dǎo)致我們無法利用時(shí)間戳確保這件事。而消息的亂序就給分布式系統(tǒng)的處理帶來了更大的難度。
下面,我們就依次介紹,針對(duì)這些問題,都有什么處理方式。
對(duì)于進(jìn)程崩潰的問題,首先要明確的是,單純實(shí)現(xiàn)進(jìn)程崩潰下不丟數(shù),沒有任何難度,重要的是怎么在保證系統(tǒng)性能的前提下達(dá)到這個(gè)目標(biāo)。
首先要介紹的就是write-ahead log這種模式,服務(wù)器將每個(gè)狀態(tài)更改作為命令存儲(chǔ)在硬盤上的僅附加(append-only)文件中。 append操作由于是順序的磁盤寫,通常是非??斓?,因此可以在不影響性能的情況下完成。 在服務(wù)器故障恢復(fù)時(shí),可以重播日志以再次建立內(nèi)存狀態(tài)。
其關(guān)鍵思路是先以一個(gè)小成本的方式寫入一份持久化數(shù)據(jù),不一定局限于順序?qū)懘疟P,此時(shí)就可以向client端確認(rèn)數(shù)據(jù)已經(jīng)寫入,不用阻塞client端的其他行為。server端再異步的去進(jìn)行接下來高消耗的操作。
典型場景及變體:mysql redo log; redis aof; kafka本身 ;業(yè)務(wù)開發(fā)中的常見行為:對(duì)于耗時(shí)較高的行為,先寫一條數(shù)據(jù)庫記錄,表示這個(gè)任務(wù)將被執(zhí)行,之后再異步進(jìn)行實(shí)際的任務(wù)執(zhí)行;
write-ahead log會(huì)附帶一個(gè)小問題,日志會(huì)越攢越多,要如何處理其自身的存儲(chǔ)問題呢?有兩個(gè)很自然的思路: 拆分和清理。
拆分即將大日志分割成多個(gè)小日志,由于WAL的邏輯一般都很簡單,所以其拆分也不復(fù)雜,比一般的分庫分表要容易很多。這種模式叫做 Segmented Log, 典型的實(shí)現(xiàn)場景就是kafka的分區(qū)。
關(guān)于清理,有一種模式叫做low-water mark(低水位模式), 低水位,即對(duì)于日志中已經(jīng)可以被清理的部分的標(biāo)記。標(biāo)記的方式可以基于其數(shù)據(jù)情況(redolog), 也可以基于預(yù)設(shè)的保存時(shí)間(kafka),也可以做一些更精細(xì)的清理和壓縮(aof)。
再來看網(wǎng)絡(luò)環(huán)境下的問題,首先使用一個(gè)非常簡單的心跳(HeartBeat)模式,就可以解決節(jié)點(diǎn)間狀態(tài)同步的問題。一段時(shí)間內(nèi)沒有收到心跳,就將這個(gè)節(jié)點(diǎn)視為已宕機(jī)處理。
而關(guān)于腦裂的問題,通常會(huì)使用大多數(shù)(Quorum)這種模式,即要求集群內(nèi)存活的節(jié)點(diǎn)數(shù)要能達(dá)到一個(gè)Quorum值,(通常集群內(nèi)有2f+1個(gè)節(jié)點(diǎn)時(shí),最多只能容忍f個(gè)節(jié)點(diǎn)下線,即quorum值為f+1),才可以對(duì)外提供服務(wù)。我們看很多分布式系統(tǒng)的實(shí)現(xiàn)時(shí),比如rocketmq, zookeeper, 都會(huì)發(fā)現(xiàn)需要滿足至少存活多少個(gè)節(jié)點(diǎn)才能正常工作,正是Quorum模式的要求。
Quorum解決了數(shù)據(jù)持久性的問題,也就是說,成功寫入的數(shù)據(jù),在節(jié)點(diǎn)失敗的情況下,是不會(huì)丟失的。但是單靠這個(gè),無法提供強(qiáng)一致性的保證,因?yàn)椴煌?jié)點(diǎn)上的數(shù)據(jù)是會(huì)存在時(shí)間差的,client連接到不同節(jié)點(diǎn)上時(shí),會(huì)產(chǎn)生不同的結(jié)果??梢酝ㄟ^主從模式(Leader and Followers) 解決一致性的問題。其中一個(gè)節(jié)點(diǎn)被選舉為主節(jié)點(diǎn),負(fù)責(zé)協(xié)調(diào)節(jié)點(diǎn)間數(shù)據(jù)的復(fù)制,以及決定哪些數(shù)據(jù)對(duì)client是可見的。
高水位(High-Water Mark)模式是用來決定哪些數(shù)據(jù)對(duì)client可見的模式。一般來說,在quorum個(gè)從節(jié)點(diǎn)上完成數(shù)據(jù)寫入后,這條數(shù)據(jù)就可以標(biāo)記為對(duì)client可見。完成復(fù)制的這條線,就是高水位。
主從模式的應(yīng)用范圍實(shí)在太廣,這里就不做舉例了。分布式選舉算法很多,比如bully, ZAB, paxos, raft等。其中,paxos無論是理解還是實(shí)現(xiàn)難度都太大,bully在節(jié)點(diǎn)頻繁上下線時(shí)會(huì)頻繁的進(jìn)行選舉,而raft可以說是一種穩(wěn)定性、實(shí)現(xiàn)難度等各方面相對(duì)均衡,使用也最廣泛的一種分布式選舉算法。像elastic search, 在7.0版本里,將選主算法由bully更換為raft;kafka 2.8里,也由利用zk的ZAB協(xié)議,修改為raft.
到這兒,我們先總結(jié)一下。實(shí)際上,一個(gè)對(duì)分布式系統(tǒng)的操作,基本上就可以概括為下邊這么幾步:
- 寫主節(jié)點(diǎn)的Write-Ahead Log;
- 寫1個(gè)從節(jié)點(diǎn)的 WAL
- 寫主節(jié)點(diǎn)數(shù)據(jù);
- 寫1個(gè)從節(jié)點(diǎn)數(shù)據(jù)
- 寫quorum個(gè)子節(jié)點(diǎn)WAL
- 寫quorum個(gè)子節(jié)點(diǎn)數(shù)據(jù)
其中,2-5步之間的順序不是固定的。分布式系統(tǒng)平衡性能和穩(wěn)定性的最重要方式,實(shí)質(zhì)上就是決定這幾步操作的順序,以及決定在哪個(gè)時(shí)間點(diǎn)向client端返回操作成功的確認(rèn)信息。例如,mysql的同步復(fù)制、異步復(fù)制、半同步復(fù)制,就是典型的這種區(qū)別的場景。
關(guān)于進(jìn)程暫停,造成的主要的問題場景是這樣的:假如主節(jié)點(diǎn)暫停了,暫停期間如果選出了新的主節(jié)點(diǎn),然后原來的主節(jié)點(diǎn)恢復(fù)了,這時(shí)候該怎么辦。這時(shí)候,使用Generation Clock這種模式就可以,簡單的說,就是給主節(jié)點(diǎn)設(shè)置一個(gè)單調(diào)遞增的代編號(hào),表示是第幾代主節(jié)點(diǎn)。像raft里的term, ZAB里的epoch這些概念,都是generation clock這個(gè)思路的實(shí)現(xiàn)。
再看看時(shí)鐘不同步問題,在分布式環(huán)境下,不同節(jié)點(diǎn)的時(shí)鐘之間必然是會(huì)存在區(qū)別的。在主從模式下,這種問題其實(shí)已經(jīng)被最大限度的減少了。很多系統(tǒng)會(huì)選擇將所有操作都在主節(jié)點(diǎn)上進(jìn)行,主從復(fù)制也是采取復(fù)制日志再重放日志的形式。這樣,一般情況下,就不用考慮時(shí)鐘的事情了。唯一可能出問題的時(shí)機(jī)就是主從切換的過程中,原主節(jié)點(diǎn)和新主節(jié)點(diǎn)給出的數(shù)就有可能存在亂序。
一種解決時(shí)鐘不同步問題的方案就是搞一個(gè)專門的服務(wù)用來做同步,這種服務(wù)叫做NTP服務(wù)。但這種方案也不是完美的,畢竟涉及到網(wǎng)絡(luò)操作,所以難免產(chǎn)生一些誤差。所以想依靠NTP解決時(shí)鐘不同步問題時(shí),系統(tǒng)設(shè)計(jì)上需要能夠容忍一些非常微弱的誤差。
其實(shí),除了強(qiáng)行去把時(shí)鐘對(duì)齊之外,還有一些簡單一些的思路可以考慮。首先思考一個(gè)問題,我們真的需要保證消息絕對(duì)的按照真實(shí)世界物理時(shí)間去排列嗎?其實(shí)不是的,我們需要的只是 一個(gè)自洽、可重復(fù)的確定消息順序的方式,讓各個(gè)節(jié)點(diǎn)對(duì)于消息的順序能夠達(dá)成一致即可。也就是說,消息不一定按照物理上的先后排列,但是不同節(jié)點(diǎn)排出來的應(yīng)該一樣。
有一種叫Lamport Clock的技術(shù)就能達(dá)到這個(gè)目標(biāo)。它的邏輯很簡單,如圖所示:
就是本機(jī)上的操作會(huì)導(dǎo)致本機(jī)上的stamp加1,發(fā)生網(wǎng)絡(luò)通信時(shí),比如C接收到B的數(shù)據(jù)時(shí),會(huì)比較自己當(dāng)前的stamp, 和B的stamp+1, 選出較大的值,變成自己當(dāng)前的戳。 這樣一個(gè)簡單的操作,就可以保證任何有相關(guān)性的兩個(gè)操作(包括出現(xiàn)在同一節(jié)點(diǎn)、有通信兩種情況)的順序在不同節(jié)點(diǎn)之間看來是一致的。
另外,還有一些相對(duì)簡單些的事情,也是分布式系統(tǒng)設(shè)計(jì)中經(jīng)常要考慮的,比如怎么讓數(shù)據(jù)均勻的分布在各個(gè)節(jié)點(diǎn)上。對(duì)于這個(gè)問題,我們可能需要根據(jù)業(yè)務(wù)情況去找一個(gè)合適的分片key, 也可能需要找到一個(gè)合適的hash算法。另外,也有一致性哈希這種技術(shù),讓我們控制起來更自如。
分布式系統(tǒng)設(shè)計(jì)中還需要重點(diǎn)考慮的一塊就是如何衡量系統(tǒng)性能,指標(biāo)包括性能(延遲、吞吐量)、可用性、一致性、可擴(kuò)展性等等,這些說起來都比較好理解,但要是想更完善的去衡量,尤其是想更方便的去觀測這些指標(biāo)的話,也是一個(gè)很大的話題。