文章目錄
- 1. 分布式
- 1.1 什么是CAP原則?
- 1.2 說一說你對高并發的理解
- 1.3 如何實現分布式存儲?
- 1.4 說一說你對分布式事務的了解
- 1.5 分布式系統如何保證最終一致性?
- 1.6 談談你對分布式的單點問題的了解
- 1.7 HTTP和RPC有什么區別?
- 1.7 HTTP和RPC有什么區別?
1. 分布式
1.1 什么是CAP原則?
參考答案
CAP定理又稱CAP原則,指的是在一個分布式系統中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分區容錯性),最多只能同時三個特性中的兩個,三者不可兼得。
-
Consistency (一致性):
“all nodes see the same data at the same time”,即更新操作成功并返回客戶端后,所有節點在同一時間的數據完全一致,這就是分布式的一致性。一致性的問題在并發系統中不可避免,對于客戶端來說,一致性指的是并發訪問時更新過的數據如何獲取的問題。從服務端來看,則是更新如何復制分布到整個系統,以保證數據最終一致。
-
Availability (可用性):
可用性指“Reads and writes always succeed”,即服務一直可用,而且是正常響應時間。好的可用性主要是指系統能夠很好的為用戶服務,不出現用戶操作失敗或者訪問超時等用戶體驗不好的情況。
-
Partition Tolerance (分區容錯性):
即分布式系統在遇到某節點或網絡分區故障的時候,仍然能夠對外提供滿足一致性和可用性的服務。分區容錯性要求能夠使應用雖然是一個分布式系統,而看上去卻好像是在一個可以運轉正常的整體。比如現在的分布式系統中有某一個或者幾個機器宕掉了,其他剩下的機器還能夠正常運轉滿足系統需求,對于用戶而言并沒有什么體驗上的影響。
1.2 說一說你對高并發的理解
參考答案
\1. 如何理解高并發?
高并發意味著大流量,需要運用技術手段抵抗流量的沖擊,這些手段好比操作流量,能讓流量更平穩地被系統所處理,帶給用戶更好的體驗。我們常見的高并發場景有:淘寶的雙11、春運時的搶票、微博大V的熱點新聞等。除了這些典型事情,每秒幾十萬請求的秒殺系統、每天千萬級的訂單系統、每天億級日活的信息流系統等,都可以歸為高并發。很顯然,上面談到的高并發場景,并發量各不相同,那到底多大并發才算高并發呢?
- 不能只看數字,要看具體的業務場景。不能說10W QPS的秒殺是高并發,而1W QPS的信息流就不是高并發。信息流場景涉及復雜的推薦模型和各種人工策略,它的業務邏輯可能比秒殺場景復雜10倍不止。因此,不在同一個維度,沒有任何比較意義。
- 業務都是從0到1做起來的,并發量和QPS只是參考指標,最重要的是:在業務量逐漸變成原來的10倍、100倍的過程中,你是否用到了高并發的處理方法去演進你的系統,從架構設計、編碼實現、甚至產品方案等維度去預防和解決高并發引起的問題?而不是一味的升級硬件、加機器做水平擴展。
此外,各個高并發場景的業務特點完全不同:有讀多寫少的信息流場景、有讀多寫多的交易場景,那是否有通用的技術方案解決不同場景的高并發問題呢?我覺得大的思路可以借鑒,別人的方案也可以參考,但是真正落地過程中,細節上還會有無數的坑。另外,由于軟硬件環境、技術棧、以及產品邏輯都沒法做到完全一致,這些都會導致同樣的業務場景,就算用相同的技術方案也會面臨不同的問題,這些坑還得一個個趟。
\2. 高并發系統設計的目標是什么?
先搞清楚高并發系統設計的目標,在此基礎上再討論設計方案和實踐經驗才有意義和針對性。
2.1 宏觀目標
高并發絕不意味著只追求高性能,這是很多人片面的理解。從宏觀角度看,高并發系統設計的目標有三個:高性能、高可用,以及高可擴展。
- 高性能:性能體現了系統的并行處理能力,在有限的硬件投入下,提高性能意味著節省成本。同時,性能也反映了用戶體驗,響應時間分別是100毫秒和1秒,給用戶的感受是完全不同的。
- 高可用:表示系統可以正常服務的時間。一個全年不停機、無故障;另一個隔三差五出線上事故、宕機,用戶肯定選擇前者。另外,如果系統只能做到90%可用,也會大大拖累業務。
- 高擴展:表示系統的擴展能力,流量高峰時能否在短時間內完成擴容,更平穩地承接峰值流量,比如雙11活動、明星離婚等熱點事件。
這3個目標是需要通盤考慮的,因為它們互相關聯、甚至也會相互影響。比如說:考慮系統的擴展能力,你會將服務設計成無狀態的,這種集群設計保證了高擴展性,其實也間接提升了系統的性能和可用性。再比如說:為了保證可用性,通常會對服務接口進行超時設置,以防大量線程阻塞在慢請求上造成系統雪崩,那超時時間設置成多少合理呢?一般,我們會參考依賴服務的性能表現進行設置。
2.2 微觀目標
再從微觀角度來看,高性能、高可用和高擴展又有哪些具體的指標來衡量?為什么會選擇這些指標呢?
2.2.1 性能指標
通過性能指標可以度量目前存在的性能問題,同時作為性能優化的評估依據。一般來說,會采用一段時間內的接口響應時間作為指標。
-
平均響應時間:最常用,但是缺陷很明顯,對于慢請求不敏感。比如1萬次請求,其中9900次是1ms,100次是100ms,則平均響應時間為1.99ms,雖然平均耗時僅增加了0.99ms,但是1%請求的響應時間已經增加了100倍。
-
TP90、TP99等分位值:將響應時間按照從小到大排序,TP90表示排在第90分位的響應時間, 分位值越大,對慢請求越敏感。
-
吞吐量:和響應時間呈反比,比如響應時間是1ms,則吞吐量為每秒1000次。
通常,設定性能目標時會兼顧吞吐量和響應時間,比如這樣表述:在每秒1萬次請求下,AVG控制在50ms以下,TP99控制在100ms以下。對于高并發系統,AVG和TP分位值必須同時要考慮。另外,從用戶體驗角度來看,200毫秒被認為是第一個分界點,用戶感覺不到延遲,1秒是第二個分界點,用戶能感受到延遲,但是可以接受。因此,對于一個健康的高并發系統,TP99應該控制在200毫秒以內,TP999或者TP9999應該控制在1秒以內。
2.2.2 可用性指標
高可用性是指系統具有較高的無故障運行能力,可用性 = 平均故障時間 / 系統總運行時間,一般使用幾個9來描述系統的可用性。
對于高并發系統來說,最基本的要求是:保證3個9或者4個9。原因很簡單,如果你只能做到2個9,意味著有1%的故障時間,像一些大公司每年動輒千億以上的GMV或者收入,1%就是10億級別的業務影響。
2.2.3 可擴展性指標
面對突發流量,不可能臨時改造架構,最快的方式就是增加機器來線性提高系統的處理能力。
對于業務集群或者基礎組件來說,擴展性 = 性能提升比例 / 機器增加比例,理想的擴展能力是:資源增加幾倍,性能提升幾倍。通常來說,擴展能力要維持在70%以上。
但是從高并發系統的整體架構角度來看,擴展的目標不僅僅是把服務設計成無狀態就行了,因為當流量增加10倍,業務服務可以快速擴容10倍,但是數據庫可能就成為了新的瓶頸。
像MySQL這種有狀態的存儲服務通常是擴展的技術難點,如果架構上沒提前做好規劃(垂直和水平拆分),就會涉及到大量數據的遷移。
因此,高擴展性需要考慮:服務集群、數據庫、緩存和消息隊列等中間件、負載均衡、帶寬、依賴的第三方等,當并發達到某一個量級后,上述每個因素都可能成為擴展的瓶頸點。
\3. 高并發的實踐方案有哪些?
了解了高并發設計的3大目標后,再系統性總結下高并發的設計方案,會從以下兩部分展開:先總結下通用的設計方法,然后再圍繞高性能、高可用、高擴展分別給出具體的實踐方案。
3.1 通用的設計方法
通用的設計方法主要是從「縱向」和「橫向」兩個維度出發,俗稱高并發處理的兩板斧:縱向擴展和橫向擴展。
3.1.1 縱向擴展(scale-up)
它的目標是提升單機的處理能力,方案又包括:
- 提升單機的硬件性能:通過增加內存、CPU核數、存儲容量、或者將磁盤升級成SSD等堆硬件的方式來提升。
- 提升單機的軟件性能:使用緩存減少IO次數,使用并發或者異步的方式增加吞吐量。
3.1.2 橫向擴展(scale-out)
因為單機性能總會存在極限,所以最終還需要引入橫向擴展,通過集群部署以進一步提高并發處理能力,又包括以下2個方向:
-
做好分層架構:這是橫向擴展的提前,因為高并發系統往往業務復雜,通過分層處理可以簡化復雜問題,更容易做到橫向擴展。
上面這種圖是互聯網最常見的分層架構,當然真實的高并發系統架構會在此基礎上進一步完善。比如會做動靜分離并引入CDN,反向代理層可以是LVS+Nginx,Web層可以是統一的API網關,業務服務層可進一步按垂直業務做微服務化,存儲層可以是各種異構數據庫。
-
各層進行水平擴展:無狀態水平擴容,有狀態做分片路由。業務集群通常能設計成無狀態的,而數據庫和緩存往往是有狀態的,因此需要設計分區鍵做好存儲分片,當然也可以通過主從同步、讀寫分離的方案提升讀性能。
3.2 具體的實踐方案
下面再結合我的個人經驗,針對高性能、高可用、高擴展3個方面,總結下可落地的實踐方案。
3.2.1 高性能的實踐方案
- 集群部署,通過負載均衡減輕單機壓力。
- 多級緩存,包括靜態數據使用CDN、本地緩存、分布式緩存等,以及對緩存場景中的熱點key、緩存穿透、緩存并發、數據一致性等問題的處理。
- 分庫分表和索引優化,以及借助搜索引擎解決復雜查詢問題。
- 考慮NoSQL數據庫的使用,比如HBase、TiDB等,但是團隊必須熟悉這些組件,且有較強的運維能力。
- 異步化,將次要流程通過多線程、MQ、甚至延時任務進行異步處理。
- 限流,需要先考慮業務是否允許限流(比如秒殺場景是允許的),包括前端限流、Nginx接入層的限流、服務端的限流。
- 對流量進行削峰填谷,通過MQ承接流量。
- 并發處理,通過多線程將串行邏輯并行化。
- 預計算,比如搶紅包場景,可以提前計算好紅包金額緩存起來,發紅包時直接使用即可。
- 緩存預熱,通過異步任務提前預熱數據到本地緩存或者分布式緩存中。
- 減少IO次數,比如數據庫和緩存的批量讀寫、RPC的批量接口支持、或者通過冗余數據的方式干掉RPC調用。
- 減少IO時的數據包大小,包括采用輕量級的通信協議、合適的數據結構、去掉接口中的多余字段、減少緩存key的大小、壓縮緩存value等。
- 程序邏輯優化,比如將大概率阻斷執行流程的判斷邏輯前置、For循環的計算邏輯優化,或者采用更高效的算法。
- 各種池化技術的使用和池大小的設置,包括HTTP請求池、線程池(考慮CPU密集型還是IO密集型設置核心參數)、數據庫和Redis連接池等。
- JVM優化,包括新生代和老年代的大小、GC算法的選擇等,盡可能減少GC頻率和耗時。
- 鎖選擇,讀多寫少的場景用樂觀鎖,或者考慮通過分段鎖的方式減少鎖沖突。
上述方案無外乎從計算和 IO 兩個維度考慮所有可能的優化點,需要有配套的監控系統實時了解當前的性能表現,并支撐你進行性能瓶頸分析,然后再遵循二八原則,抓主要矛盾進行優化。
3.2.2 高可用的實踐方案
- 對等節點的故障轉移,Nginx和服務治理框架均支持一個節點失敗后訪問另一個節點。
- 非對等節點的故障轉移,通過心跳檢測并實施主備切換(比如redis的哨兵模式或者集群模式、MySQL的主從切換等)。
- 接口層面的超時設置、重試策略和冪等設計。
- 降級處理:保證核心服務,犧牲非核心服務,必要時進行熔斷;或者核心鏈路出問題時,有備選鏈路。
- 限流處理:對超過系統處理能力的請求直接拒絕或者返回錯誤碼。
- MQ場景的消息可靠性保證,包括producer端的重試機制、broker側的持久化、consumer端的ack機制等。
- 灰度發布,能支持按機器維度進行小流量部署,觀察系統日志和業務指標,等運行平穩后再推全量。
- 監控報警:全方位的監控體系,包括最基礎的CPU、內存、磁盤、網絡的監控,以及Web服務器、JVM、數據庫、各類中間件的監控和業務指標的監控。
- 災備演練:類似當前的“混沌工程”,對系統進行一些破壞性手段,觀察局部故障是否會引起可用性問題。
高可用的方案主要從冗余、取舍、系統運維3個方向考慮,同時需要有配套的值班機制和故障處理流程,當出現線上問題時,可及時跟進處理。
3.2.3 高擴展的實踐方案
- 合理的分層架構:比如上面談到的互聯網最常見的分層架構,另外還能進一步按照數據訪問層、業務邏輯層對微服務做更細粒度的分層(但是需要評估性能,會存在網絡多一跳的情況)。
- 存儲層的拆分:按照業務維度做垂直拆分、按照數據特征維度進一步做水平拆分(分庫分表)。
- 業務層的拆分:最常見的是按照業務維度拆(比如電商場景的商品服務、訂單服務等),也可以按照核心接口和非核心接口拆,還可以按照請求源拆(比如To C和To B,APP和H5)。
1.3 如何實現分布式存儲?
參考答案
分布式存儲是一個大的概念,其包含的種類繁多,除了傳統意義上的分布式文件系統、分布式塊存儲和分布式對象存儲外,還包括分布式數據庫和分布式緩存等。下面我們探討一下分布式文件系統等傳統意義上的存儲架構,實現這種存儲架構主要有三種通用的形式,其它存儲架構也基本上基于上述架構,并沒有太大的變化。
中間控制節點架構(HDFS)
分布式存儲最早是由谷歌提出的,其目的是通過廉價的服務器來提供使用與大規模,高并發場景下的Web訪問問題。下圖是谷歌分布式存儲(HDFS)的簡化的模型。在該系統的整個架構中將服務器分為兩種類型,一種名為namenode,這種類型的節點負責管理管理數據(元數據),另外一種名為datanode,這種類型的服務器負責實際數據的管理。
上圖分布式存儲中,如果客戶端需要從某個文件讀取數據,首先從namenode獲取該文件的位置(具體在哪個datanode),然后從該位置獲取具體的數據。在該架構中namenode通常是主備部署,而datanode則是由大量節點構成一個集群。由于元數據的訪問頻度和訪問量相對數據都要小很多,因此namenode通常不會成為性能瓶頸,而datanode集群可以分散客戶端的請求。因此,通過這種分布式存儲架構可以通過橫向擴展datanode的數量來增加承載能力,也即實現了動態橫向擴展的能力。
完全無中心架構—計算模式(Ceph)
下圖是Ceph存儲系統的架構,在該架構中與HDFS不同的地方在于該架構中沒有中心節點。客戶端是通過一個設備映射關系計算出來其寫入數據的位置,這樣客戶端可以直接與存儲節點通信,從而避免中心節點的性能瓶頸。
在Ceph存儲系統架構中核心組件有Mon服務、OSD服務和MDS服務等。對于塊存儲類型只需要Mon服務、OSD服務和客戶端的軟件即可。其中Mon服務用于維護存儲系統的硬件邏輯關系,主要是服務器和硬盤等在線信息。Mon服務通過集群的方式保證其服務的可用性。OSD服務用于實現對磁盤的管理,實現真正的數據讀寫,通常一個磁盤對應一個OSD服務。
客戶端訪問存儲的大致流程是,客戶端在啟動后會首先從Mon服務拉取存儲資源布局信息,然后根據該布局信息和寫入數據的名稱等信息計算出期望數據的位置(包含具體的物理服務器信息和磁盤信息),然后該位置信息直接通信,讀取或者寫入數據。
完全無中心架構—一致性哈希(Swift)
與Ceph的通過計算方式獲得數據位置的方式不同,另外一種方式是通過一致性哈希的方式獲得數據位置。一致性哈希的方式就是將設備做成一個哈希環,然后根據數據名稱計算出的哈希值映射到哈希環的某個位置,從而實現數據的定位。
上圖是一致性哈希的基本原理,為了繪制簡單,本文以一個服務器上的一個磁盤為例進行介紹。為了保證數據分配的均勻性及出現設備故障時數據遷移的均勻性,一致性哈希將磁盤劃分為比較多的虛擬分區,每個虛擬分區是哈希環上的一個節點。整個環是一個從0到32位最大值的一個區間,并且首尾相接。當計算出數據(或者數據名稱)的哈希值后,必然落到哈希環的某個區間,然后以順時針,必然能夠找到一個節點。那么,這個節點就是存儲數據的位置。
Swift存儲的整個數據定位算法就是基于上述一致性哈希實現的。在Swift對象存儲中,通過賬戶名/容器名/對象名三個名稱組成一個位置的標識,通過該唯一標識可以計算出一個整型數來。而在存儲設備方面,Swift構建一個虛擬分區表,表的大小在創建集群是確定(通常為幾十萬),這個表其實就是一個數組。這樣,根據上面計算的整數值,以及這個數組,通過一致性哈希算法就可以確定該整數在數組的位置。而數組中的每項內容是數據3個副本(也可以是其它副本數量)的設備信息(包含服務器和磁盤等信息)。也就是經過上述計算,可以確定一個數據存儲的具體位置。這樣,Swift就可以將請求重新定向到該設備進行處理。
上述計算過程是在一個名為Proxy的服務中進行的,該服務可以集群化部署。因此可以分攤請求的負載,不會成為性能瓶頸。
1.4 說一說你對分布式事務的了解
參考答案
分布式事務就是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分別位于不同的分布式系統的不同節點之上。簡單的說,就是一次大的操作由不同的小操作組成,這些小的操作分布在不同的服務器上,且屬于不同的應用,分布式事務需要保證這些小操作要么全部成功,要么全部失敗。本質上來說,分布式事務就是為了保證不同數據庫的數據一致性。
要實現分布式事務,有如下幾種常見的解決方案:
2PC
說到2PC就不得不聊數據庫分布式事務中的 XA Transactions。
如上圖,在XA協議中分為兩階段:
第一階段:事務管理器要求每個涉及到事務的數據庫預提交(precommit)此操作,并反映是否可以提交。
第二階段:事務協調器要求每個數據庫提交數據,或者回滾數據。
優點:
- 盡量保證了數據的強一致,實現成本較低,在各大主流數據庫都有自己實現,對于MySQL是從5.5開始支持。
缺點:
- 單點問題:事務管理器在整個流程中扮演的角色很關鍵,如果其宕機,比如在第一階段已經完成,在第二階段正準備提交的時候事務管理器宕機,資源管理器就會一直阻塞,導致數據庫無法使用。
- 同步阻塞:在準備就緒之后,資源管理器中的資源一直處于阻塞,直到提交完成,釋放資源。
- 數據不一致:兩階段提交協議雖然為分布式數據強一致性所設計,但仍然存在數據不一致性的可能,比如在第二階段中,假設協調者發出了事務commit的通知,但是因為網絡問題該通知僅被一部分參與者所收到并執行了commit操作,其余的參與者則因為沒有收到通知一直處于阻塞狀態,這時候就產生了數據的不一致性。
總的來說,XA協議比較簡單,成本較低,但是其單點問題,以及不能支持高并發依然是其最大的弱點。
TCC
關于TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland于2007年發表的一篇名為《Life beyond Distributed Transactions:an Apostate’s Opinion》的論文提出。 TCC事務機制相比于上面介紹的XA,解決了其幾個缺點:
- 解決了協調者單點,由主業務方發起并完成這個業務活動。業務活動管理器也變成多點,引入集群。
- 同步阻塞:引入超時,超時后進行補償,并且不會鎖定整個資源,將資源轉換為業務邏輯形式,粒度變小。
- 數據一致性,有了補償機制之后,由業務活動管理器控制一致性。
如上圖,對于TCC的解釋:
- Try階段:嘗試執行,完成所有業務檢查(一致性),預留必須業務資源(準隔離性)。
- Confirm階段:確認執行真正執行業務,不作任何業務檢查,只使用Try階段預留的業務資源,Confirm操作滿足冪等性。要求具備冪等設計,Confirm失敗后需要進行重試。
- Cancel階段:取消執行,釋放Try階段預留的業務資源 Cancel操作滿足冪等性Cancel階段的異常和Confirm階段異常處理方案基本上一致。
舉個簡單的例子如果你用100元買了一瓶水,在Try階段你需要向你的錢包檢查是否夠100元并鎖住這100元,水也是一樣的。如果有一個失敗,則進行cancel(釋放這100元和這一瓶水),如果cancel失敗不論什么失敗都進行重試cancel,所以需要保持冪等。如果都成功,則進行confirm,確認這100元扣,和這一瓶水被賣,如果confirm失敗無論什么失敗則重試(會依靠活動日志進行重試)。
對于TCC來說適合以下場景:
- 強隔離性,嚴格一致性要求的活動業務。
- 執行時間較短的業務。
本地消息表
本地消息表這個方案最初是ebay提出的,此方案的核心是將需要分布式處理的任務通過消息日志的方式來異步執行。消息日志可以存儲到本地文本、數據庫或消息隊列,再通過業務規則自動或人工發起重試。人工重試更多的是應用于支付場景,通過對賬系統對事后問題的處理。
對于本地消息隊列來說核心是把大事務轉變為小事務,還是舉上面用100元去買一瓶水的例子:
- 當你扣錢的時候,你需要在你扣錢的服務器上新增加一個本地消息表,你需要把你扣錢和寫入減去水的庫存到本地消息表放入同一個事務(依靠數據庫本地事務保證一致性。
- 這個時候有個定時任務去輪詢這個本地事務表,把沒有發送的消息,扔給商品庫存服務器,叫他減去水的庫存,到達商品服務器之后這個時候得先寫入這個服務器的事務表,然后進行扣減,扣減成功后,更新事務表中的狀態。
- 商品服務器通過定時任務掃描消息表或者直接通知扣錢服務器,扣錢服務器本地消息表進行狀態更新。
- 針對一些異常情況,定時掃描未成功處理的消息,進行重新發送,在商品服務器接到消息之后,首先判斷是否是重復的,如果已經接收,在判斷是否執行,如果執行在馬上又進行通知事務,如果未執行,需要重新執行需要由業務保證冪等,也就是不會多扣一瓶水。
本地消息隊列是BASE理論,是最終一致模型,適用于對一致性要求不高的場景,實現這個模型時需要注意重試的冪等。
MQ事務
在RocketMQ中實現了分布式事務,實際上其實是對本地消息表的一個封裝,將本地消息表移動到了MQ內部,下面簡單介紹一下MQ事務。
基本流程如下:
第一階段Prepared消息,會拿到消息的地址。
第二階段執行本地事務。
第三階段通過第一階段拿到的地址去訪問消息,并修改狀態。消息接受者就能使用這個消息。
如果確認消息失敗,在RocketMq Broker中提供了定時掃描沒有更新狀態的消息,如果有消息沒有得到確認,會向消息發送者發送消息,來判斷是否提交,在rocketmq中是以listener的形式給發送者,用來處理。
如果消費超時,則需要一直重試,消息接收端需要保證冪等。如果消息消費失敗,這個就需要人工進行處理,因為這個概率較低,如果為了這種小概率時間而設計這個復雜的流程反而得不償失。
Saga事務
Saga是30年前一篇數據庫倫理提到的一個概念。其核心思想是將長事務拆分為多個本地短事務,由Saga事務協調器協調,如果正常結束那就正常完成,如果某個步驟失敗,則根據相反順序一次調用補償操作。 Saga的組成:
每個Saga由一系列sub-transaction Ti 組成 每個Ti 都有對應的補償動作Ci,補償動作用于撤銷Ti造成的結果,這里的每個T,都是一個本地事務。 可以看到,和TCC相比,Saga沒有“預留 try”動作,它的Ti就是直接提交到庫。
Saga的執行順序有兩種:
- T1, T2, T3, …, Tn
- T1, T2, …, Tj, Cj,…, C2, C1,其中0 < j < n
Saga定義了兩種恢復策略:
向后恢復,即上面提到的第二種執行順序,其中j是發生錯誤的sub-transaction,這種做法的效果是撤銷掉之前所有成功的sub-transation,使得整個Saga的執行結果撤銷。 向前恢復,適用于必須要成功的場景,執行順序是類似于這樣的:T1, T2, …, Tj(失敗), Tj(重試),…, Tn,其中j是發生錯誤的sub-transaction。該情況下不需要Ci。這里要注意的是,在saga模式中不能保證隔離性,因為沒有鎖住資源,其他事務依然可以覆蓋或者影響當前事務。
還是拿100元買一瓶水的例子來說,這里定義:
- T1=扣100元,T2=給用戶加一瓶水,T3=減庫存一瓶水;
- C1=加100元,C2=給用戶減一瓶水,C3=給庫存加一瓶水;
我們一次進行T1,T2,T3如果發生問題,就執行發生問題的C操作的反向。 上面說到的隔離性的問題會出現在,如果執行到T3這個時候需要執行回滾,但是這個用戶已經把水喝了(另外一個事務),回滾的時候就會發現,無法給用戶減一瓶水了。這就是事務之間沒有隔離性的問題。
可以看見saga模式沒有隔離性的影響還是較大,可以參照華為的解決方案:從業務層面入手加入一 Session 以及鎖的機制來保證能夠串行化操作資源。也可以在業務層面通過預先凍結資金的方式隔離這部分資源, 最后在業務操作的過程中可以通過及時讀取當前狀態的方式獲取到最新的更新。
1.5 分布式系統如何保證最終一致性?
參考答案
國際開放標準組織Open Group定義了DTS(分布式事務處理模型),模型中包含4種角色:應用程序、事務管理器、資源管理器和通信資源管理器。事務管理器是統管全局的管理者,資源管理器和通信資源管理器是事務的參與者。
JEE(Java企業版)規范也包含此分布式事務處理模型的規范,并在所有AppServer中進行實現。在JEE規范中定義了TX協議和XA協議,TX協議定義應用程序與事務管理器之間的接口,XA協議則定義事務管理器與資源管理器之間的接口。在過去使用 AppServer如WebSphere、 WebLogic、JBoss等配置數據源時會看見類似XADatasource的數據源,這就是實現了分布式事務處理模型的關系型數據庫的數據源。在企業級開發JEE中,關系型數據庫、JMS服務扮演資源管理器的角色,而EJB容器扮演事務管理器的角色。
下面我們介紹兩階段提交協議、三階段提交協議及阿里巴巴提出的 TCC,它們都是根據DTS這一思想演變而來的。
兩階段提交協議
兩階段提交協議把分布式事務分為兩個階段,一個是準備階段,另一個是提交階段。準備階段和提交階段都是由事務管理器發起的,為了接下來講解方便,我們將事務管理器稱為協調者,將資源管理器稱為參與者。
兩階段提交協議的流程如下所述。
-
準備階段:協調者向參與者發起指令,參與者評估自己的狀態,如果參與者評估指令可以完成,則會寫redo或者undo日志(Write-Ahead Log的一種),然后鎖定資源,執行操作,但是并不提交。
-
提交階段:如果每個參與者明確返回準備成功,也就是預留資源和執行操作成功,則協調者向參與者發起提交指令,參與者提交資源變更的事務,釋放鎖定的資源;如果任何一個參與者明確返回準備失敗,也就是預留資源或者執行操作失敗,則協調者向參與者發起中止指令,參與者取消已經變更的事務,執行undo日志,釋放鎖定的資源。兩階段提交協議的成功場景如下圖所示。
我們看到兩階段提交協議在準備階段鎖定資源,這是一個重量級的操作,能保證強一致性,但是實現起來復雜、成本較高、不夠靈活,更重要的是它有如下致命的問題。
-
阻塞:從上面的描述來看,對于任何一次指令都必須收到明確的響應,才會繼續進行下一步,否則處于阻塞狀態,占用的資源被一直鎖定,不會被釋放。
-
單點故障:如果協調者宕機,參與者沒有協調者指揮,則會一直阻塞,盡管可以通過選舉新的協調者替代原有協調者,但是如果協調者在發送一個提交指令后宕機,而提交指令僅僅被一個參與者接收,并且參與者接收后也宕機,則新上任的協調者無法處理這種情況。
-
腦裂:協調者發送提交指令,有的參與者接收到并執行了事務,有的參與者沒有接收到事務就沒有執行事務,多個參與者之間是不一致的。
上面的所有問題雖然很少發生,但都需要人工干預處理,沒有自動化的解決方案,因此兩階段提交協議在正常情況下能保證系統的強一致性,但是在出現異常的情況下,當前處理的操作處于錯誤狀態,需要管理員人工干預解決,因此可用性不夠好,這也符合CAP協議的一致性和可用性不能兼得的原理。
三階段提交協議
三階段提交協議是兩階段提交協議的改進版本。它通過超時機制解決了阻塞的問題,并且把兩個階段增加為以下三個階段。
- 詢問階段:協調者詢問參與者是否可以完成指令,協調者只需要回答是或不是,而不需要做真正的操作,這個階段超時會導致中止。
- 準備階段:如果在詢問階段所有參與者都返回可以執行操作,則協調者向參與者發送預執行請求,然后參與者寫redo和undo日志,執行操作但是不提交操作;如果在詢問階段任意參與者返回不能執行操作的結果,則協調者向參與者發送中止請求,這里的邏輯與兩階段提交協議的準備階段是相似的。
- 提交階段:如果每個參與者在準備階段返回準備成功,也就是說預留資源和執行操作成功,則協調者向參與者發起提交指令,參與者提交資源變更的事務,釋放鎖定的資源;如果任何參與者返回準備失敗,也就是說預留資源或者執行操作失敗,則協調者向參與者發起中止指令,參與者取消已經變更的事務,執行 undo 日志,釋放鎖定的資源,這里的邏輯與兩階段提交協議的提交階段一致。
三階段提交協議的成功場景示意圖如下圖所示:
三階段提交協議與兩階段提交協議主要有以下兩個不同點:
- 增加了一個詢問階段,詢問階段可以確保盡可能早地發現無法執行操作而需要中止的行為,但是它并不能發現所有這種行為,只會減少這種情況的發生。
- 在準備階段以后,協調者和參與者執行的任務中都增加了超時,一旦超時,則協調者和參與者都會繼續提交事務,默認為成功,這也是根據概率統計超時后默認為成功的正確性最大。
三階段提交協議與兩階段提交協議相比,具有如上優點,但是一旦發生超時,系統仍然會發生不一致,只不過這種情況很少見,好處是至少不會阻塞和永遠鎖定資源。
TCC
簽名講解了兩階段提交協議和三階段提交協議,實際上它們能解決常見的分布式事務的問題,但是遇到極端情況時,系統會產生阻塞或者不一致的問題,需要運營或者技術人員解決。兩階段及三階段方案中都包含多個參與者、多個階段實現一個事務,實現復雜,性能也是一個很大的問題,因此,在互聯網的高并發系統中,鮮有使用兩階段提交和三階段提交協議的場景。
后來有人提出了TCC協議,TCC協議將一個任務拆分成Try、Confirm、Cancel三個步驟,正常的流程會先執行Try,如果執行沒有問題,則再執行Confirm,如果執行過程中出了問題,則執行操作的逆操作Cancel。從正常的流程上講,這仍然是一個兩階段提交協議,但是在執行出現問題時有一定的自我修復能力,如果任何參與者出現了問題,則協調者通過執行操作的逆操作來Cancel之前的操作,達到最終的一致狀態。
可以看出,從時序上來說,如果遇到極端情況,則TCC會有很多問題,例如,如果在取消時一些參與者收到指令,而另一些參與者沒有收到指令,則整個系統仍然是不一致的。對于這種復雜的情況,系統首先會通過補償的方式嘗試自動修復,如果系統無法修復,則必須由人工參與解決。
從TCC的邏輯上看,可以說TCC是簡化版的三階段提交協議,解決了兩階段提交協議的阻塞問題,但是沒有解決極端情況下會出現不一致和腦裂的問題。然而,TCC通過自動化補償手段,將需要人工處理的不一致情況降到最少,也是一種非常有用的解決方案。某著名的互聯網公司在內部的一些中間件上實現了TCC模式。
我們給出一個使用TCC的實際案例,在秒殺的場景中,用戶發起下訂單請求,應用層先查詢庫存,確認商品庫存還有余量,則鎖定庫存,此時訂單狀態為待支付,然后指引用戶去支付,由于某種原因用戶支付失敗或者支付超時,則系統會自動將鎖定的庫存解鎖以供其他用戶秒殺。
TCC協議的使用場景如下圖所示:
在大規模、高并發服務化系統中,一個功能被拆分成多個具有單一功能的子功能,一個流程會有多個系統的多個單一功能的服務組合實現,如果使用兩階段提交協議和三階段提交協議,則確實能解決系統間的一致性問題。除了這兩個協議的自身問題,其實現也比較復雜、成本比較高,最重要的是性能不好,相比來看,TCC協議更簡單且更容易實現,但是TCC協議由于每個事務都需要執行Try,再執行Confirm,略顯臃腫,因此,現實系統的底線是僅僅需要達到最終一致性,而不需要實現專業的、復雜的一致性協議。實現最終一致性有一些非常有效、簡單的模式,下面就介紹這些模式及其應用場景。
查詢模式
任何服務操作都需要提供一個查詢接口,用來向外部輸出操作執行的狀態。服務操作的使用方可以通過查詢接口得知服務操作執行的狀態,然后根據不同的狀態來做不同的處理操作。
為了能夠實現查詢,每個服務操作都需要有唯一的流水號標識,也可使用此次服務操作對應的資源ID來標識,例如:請求流水號、訂單號等。首先,單筆查詢操作是必須提供的,也鼓勵使用單筆訂單查詢,這是因為每次調用需要占用的負載是可控的。批量查詢則根據需要來提供,如果使用了批量查詢,則需要有合理的分頁機制,并且必須限制分頁的大小,以及對批量查詢的吞吐量有容量評估、熔斷、隔離和限流等措施。
補償模式
有了上面的查詢模式,在任何情況下,我們都能得知具體的操作所處的狀態,如果整個操作都處于不正常的狀態,則我們需要修正操作中有問題的子操作,這可能需要重新執行未完成的子操作,后者取消已經完成的子操作,通過修復使整個分布式系統達到一致。為了讓系統最終達到一致狀態而做的努力都叫作補償。
對于服務化系統中同步調用的操作,若業務操作發起方還沒有收到業務操作執行方的明確返回或者調用超時,業務發起方需要及時地調用業務執行方來獲得操作執行的狀態,這里使用在前面學習的查詢模式。在獲得業務操作執行方的狀態后,如果業務執行方已經完成預設工作,則業務發起方向業務的使用方返回成功;如果業務操作執行方的狀態為失敗或者未知,則會立即告訴業務使用方失敗,也叫作快速失敗策略,然后調用業務操作的逆向操作,保證操作不被執行或者回滾已經執行的操作,讓業務使用方、業務操作發起方和業務操作執行方最終達到一致狀態。
補償模式如下圖所示:
異步確保模式
異步確保模式是補償模式的一個典型案例,經常應用到使用方對響應時間要求不太高的場景中,通常把這類操作從主流程中摘除,通過異步的方式進行處理,處理后把結果通過通知系統通知給使用方。這個方案的最大好處是能夠對高并發流量進行消峰,例如:電商系統中的物流、配送,以及支付系統中的計費、入賬等。
在實踐中將要執行的異步操作封裝后持久入庫,然后通過定時撈取未完成的任務進行補償操作來實現異步確保模式,只要定時系統足夠健壯,則任何任務最終都會被成功執行。
異步確保模式如下圖所示:
定期校對模式
系統在沒有達到一致之前,系統間的狀態是不一致的,甚至是混亂的,需要通過補償操作來達到最終一致性的目的,但是如何來發現需要補償的操作呢?
在操作主流程中的系統間執行校對操作,可以在事后異步地批量校對操作的狀態,如果發現不一致的操作,則進行補償,補償操作與補償模式中的補償操作是一致的。
另外,實現定期校對的一個關鍵就是分布式系統中需要有一個自始至終唯一的ID,生成全局唯一ID有以下兩種方法:
- 持久型:使用數據庫表自增字段或者Sequence生成,為了提高效率,每個應用節點可以緩存一個批次的ID,如果機器重啟則可能會損失一部分ID,但是這并不會產生任何問題。
- 時間型:一般由機器號、業務號、時間、單節點內自增ID組成,由于時間一般精確到秒或者毫秒,因此不需要持久就能保證在分布式系統中全局唯一、粗略遞增等。
可靠消息模式
在分布式系統中,對于主流程中優先級比較低的操作,大多采用異步的方式執行,也就是前面提到的異步確保模型,為了讓異步操作的調用方和被調用方充分解耦,也由于專業的消息隊列本身具有可伸縮、可分片、可持久等功能,我們通常通過消息隊列實現異步化。對于消息隊列,我們需要建立特殊的設施來保證可靠的消息發送及處理機的冪等性。
緩存一致性模式
在大規模、高并發系統中的一個常見的核心需求就是億級的讀需求,顯然,關系型數據庫并不是解決高并發讀需求的最佳方案,互聯網的經典做法就是使用緩存來抗住讀流量。
- 如果性能要求不是非常高,則盡量使用分布式緩存,而不要使用本地緩存。
- 寫緩存時數據一定要完整,如果緩存數據的一部分有效,另一部分無效,則寧可在需要時回源數據庫,也不要把部分數據放入緩存中。
- 使用緩存犧牲了一致性,為了提高性能,數據庫與緩存只需要保持弱一致性,而不需要保持強一致性,否則違背了使用緩存的初衷。
- 讀的順序是先讀緩存,后讀數據庫,寫的順序要先寫數據庫,后寫緩存。
1.6 談談你對分布式的單點問題的了解
參考答案
在分布式系統中,單點問題是一個比較常見的問題,對于單點問題可以分為有狀態服務的單點問題和無狀態服務的單點問題。
無狀態服務的單點問題
對于無狀態的服務,單點問題的解決比較簡單,因為服務是無狀態的,所以服務節點很容易進行平行擴展。比如,在分布式系統中,為了降低各進程通信的網絡結構的復雜度,我們會增加一個代理節點,專門做消息的轉發,其他的業務進行直接和代理節點進行通信,類似一個星型的網絡結構。
參考上面兩個圖,圖中proxy是一個消息轉發代理,業務進程中的消息都會經過該代理,這也是比較場景的一個架構。在上圖中,只有一個proxy,如果該節點掛了,那么所有的業務進程之間都無法進行通信。由于proxy是無狀態的服務,所以很容易想到第二個圖中的解決方案,增加一個proxy節點,兩個proxy節點是對等的。增加新節點后,業務進程需要與兩個Proxy之間增加一個心跳的機制,業務進程在發送消息的時候根據proxy的狀態,選擇一個可用的proxy進行消息的傳遞。從負載均衡的角度來看,如果兩個proxy都是存活狀態的話,業務進程應當隨機選擇一個proxy。
那么該解決方案中會存在什么問題呢?主要存在的問題是消息的順序性問題。一般來說,業務的消息都是發送、應答,再發送、再應答這樣的順序進行的,在業務中可以保證消息的順序性。但是,在實際的應用中,會出現這樣一個情況:在業務進程1中,有個業務需要給業務進程3發送消息A和消息B,根據業務的特性,消息A必須要在消息B之前到達。如果業務進程1在發送消息A的時候選擇了proxy1,在發送消息B的時候選擇了proxy2,那么在分布式環境中,我們并不能確保先發送的消息A一定就能比后發送的消息B先到達業務進程3。那么怎么解決這個問題?其實方案也比較簡單,對于這類對消息順序有要求的業務,我們可以指定對應的proxy進行發送,比如消息A和消息B都是使用proxy1進行發送,這樣就可以保證消息A比消息B先到達業務進程3。
整體來說,對于無狀態的服務的單點問題的解決方案還是比較簡單的,只要增加對應的服務節點即可。
有狀態服務的單點問題
相對無狀態服務的單點問題,有狀態服務的單點問題就復雜多了。如果在架構中,有個節點是單點的,并且該節點是有狀態的服務,那么首先要考慮的是該節點是否可以去狀態,如果可以,則優先選擇去除狀態的方案(比如說把狀態存儲到后端的可靠DB中,可能存在性能的損耗),然后就退化成了一個無狀態服務的單點問題了,這就可以參考上一方案了。
但是,并不是所有的服務都是可以去狀態的,比如說對于一些業務它只能在一個節點中進行處理,如果在不同的節點中處理的話可能會造成狀態的不一致,這類型的業務是無法去除狀態的。對于這種無法去除狀態的單點的問題的解決方案也是有多種,但是越完善的方案實現起來就越復雜,不過整體的思路都是采用主備的方式。
第一個方案就是就是增加一個備用節點,備用節點和業務進程也可以進行通信,但是所有的業務消息都發往Master節點進行處理。Master節點和Slave節點之間采用ping的方式進行通信。Slave節點會定時發送ping包給Master節點,Master節點收到后會響應一個Ack包。當Slave節點發現Master節點沒有響應的時候,就會認為Master節點掛了,然后把自己升級為Master節點,并且通知業務進程把消息轉發給自己。該方案看起來也是挺完美的,好像不存在什么問題,Slave升級為Master后所有的業務消息都會發給它。但是,如果在Master內部有一些自己的業務邏輯,比如說隨機生成一些業務數據,并且定時存檔。那么當Master和Slave之間的網絡出現問題的時候,Slave會認為Master掛了,就會升級為Master,同樣會執行Master的相應的業務邏輯,同樣也會生成一些業務數據回寫到DB。但是,其實Master是沒有掛的,它同樣也在運行對應的業務邏輯(即使業務進程的消息沒有發給舊的Master了),這樣就會出現兩個Master進行寫同一份數據了,造成數據的混亂。所以說,該方案并不是一個很好的方案。
那怎么解決可能會出現多個Master的問題?換個角度看,該問題其實就是怎么去裁決哪個節點是Master的問題。
方案一:引入第三方的服務進行裁決。
我們可以引入ZooKeeper,由ZooKeeper進行裁決。同樣,我們啟動兩個主節點,“節點A”和節點B。它們啟動之后向ZooKeeper去注冊一個節點,假設節點A注冊的節點為master001,節點B注冊的節點為master002,注冊完成后進行選舉,編號小的節點為真正的主節點。那么,通過這種方式就完成了對兩個Master進程的調度。
方案二: 通過選舉算法和租約的方式實現Master的選舉。
對于方案一的缺點主要要多維護一套ZooKeeper的服務,如果原本業務上并沒有部署該服務的話,要增加該服務的維護也是比較麻煩的事情。這個時候我們可以在業務進程中加入Master的選舉方案。目前有比較成熟的選舉算法,比如Paxos和Raft。然后再配合租約機制,就可以實現Master的選舉,并且確保當前只有一個Master的方案。但是,這些選舉算法理解起來并不是那么地容易,要實現一套完善的方案也是挺難的。所以不建議重復造輪子,業內有很多成熟的框架或者組件可以使用,比如微信的PhxPaxos。
比如上圖的方案中,三個節點其實都是對等的,通過選舉算法確定一個Master。為了確保任何時候都只能存在一個Matster,需要加入租約的機制。一個節點成為Master后,Master和非Master節點都會進行計時,在超過租約時間后,三個節點后可以發起“我要成為Master”的請求,進行重新選舉。由于三個節點都是對等的,任意一個都可以成為Master,也就是說租期過后,有可能會出現Master切換的情況,所以為了避免Master的頻繁切換,Master節點需要比另外兩個節點先發起自己要成為Master的請求(續租),告訴其他兩個節點我要繼續成為Master,然后另外兩個節點收到請求后會進行應答,正常情況下另外兩個節點會同意該請求。關鍵點就是,在租約過期之前,非Master節點不能發起“我要成為Master”的請求,這樣就可以解決Master頻繁切換的問題。
1.7 HTTP和RPC有什么區別?
參考答案
傳輸協議
- RPC,可以基于TCP協議,也可以基于HTTP協議。
- HTTP,基于HTTP協議。
傳輸效率
- RPC,使用自定義的TCP協議,可以讓請求報文體積更小,或者使用HTTP2協議,也可以很好的減少報文的體積,提高傳輸效率。
- HTTP,如果是基于HTTP1.1的協議,請求中會包含很多無用的內容,如果是基于HTTP2.0,那么簡單的封裝一下是可以作為一個RPC來使用的,這時標準RPC框架更多的是服務治理。
性能消耗
- RPC,可以基于thrift實現高效的二進制傳輸。
- HTTP,大部分是通過json來實現的,字節大小和序列化耗時都比thrift要更消耗性能。
負載均衡
- RPC,基本都自帶了負載均衡策略。
- HTTP,需要配置Nginx,HAProxy來實現。
服務治理
- RPC,能做到自動通知,不影響上游。
- HTTP,需要事先通知,修改Nginx/HAProxy配置。
可以解決Master頻繁切換的問題。
1.7 HTTP和RPC有什么區別?
參考答案
傳輸協議
- RPC,可以基于TCP協議,也可以基于HTTP協議。
- HTTP,基于HTTP協議。
傳輸效率
- RPC,使用自定義的TCP協議,可以讓請求報文體積更小,或者使用HTTP2協議,也可以很好的減少報文的體積,提高傳輸效率。
- HTTP,如果是基于HTTP1.1的協議,請求中會包含很多無用的內容,如果是基于HTTP2.0,那么簡單的封裝一下是可以作為一個RPC來使用的,這時標準RPC框架更多的是服務治理。
性能消耗
- RPC,可以基于thrift實現高效的二進制傳輸。
- HTTP,大部分是通過json來實現的,字節大小和序列化耗時都比thrift要更消耗性能。
負載均衡
- RPC,基本都自帶了負載均衡策略。
- HTTP,需要配置Nginx,HAProxy來實現。
服務治理
- RPC,能做到自動通知,不影響上游。
- HTTP,需要事先通知,修改Nginx/HAProxy配置。
總之,RPC主要用于公司內部的服務調用,性能消耗低,傳輸效率高,服務治理方便。HTTP主要用于對外的異構環境,瀏覽器接口調用,APP接口調用,第三方接口調用等。