背景 一致性是一個抽象的、具有多重含義的電腦術語,在不同應用場景下,有不同的定義和含義。在傳統的IT時代,一致性通常指強一致性,強一致性通常體現在你中有我、我中有你、渾然一體;而在互聯網時代,一致性的含義遠遠超出了它原有的含義,在我們討論互聯網時代的一致性之前,我們先瞭解一下互聯網時代的特點,互聯 ...
背景
一致性是一個抽象的、具有多重含義的電腦術語,在不同應用場景下,有不同的定義和含義。在傳統的IT時代,一致性通常指強一致性,強一致性通常體現在你中有我、我中有你、渾然一體;而在互聯網時代,一致性的含義遠遠超出了它原有的含義,在我們討論互聯網時代的一致性之前,我們先瞭解一下互聯網時代的特點,互聯網時代信息量巨大、需要計算能力巨大,不但對用戶響應速度要求快,而且吞吐量指標也要向外擴展(既:水平伸縮),於是單節點的伺服器無法滿足需求,服務節點開始池化,想想那個經典的故事,一隻筷子一折就斷,一把筷子怎麼都折不斷,可見人多力量大的思想是多麼的重要,但是人多也不一定能解決所有事情,還得進行有序、合理的分配任務,進行有效的管理,於是互聯網時代談論最多的話題就是拆分,拆分一般分為“水平拆分”和“垂直拆分”(大家不要對應到資料庫或者緩存拆分,這裡主要表達一種邏輯)。這裡,“水平拆分”指的是同一個功能由於單機節點無法滿足性能需求,需要擴展成為多節點,多個節點具有一致的功能,組成一個服務池,一個節點服務一部分的請求量,團結起來共同處理大規模高併發的請求量。“垂直拆分”指的是按照功能拆分,秉著“專業的人乾專業的事兒”的原則,把一個複雜的功能拆分到多個單一的簡單的元功能,不同的元功能組合在一起,和未拆分前完成的功能是一致的,由於每個元功能職責單一、功能簡單,讓維護和變更都變得更簡單、安全,更易於產品版本的迭代,在這樣的一個互聯網的時代和環境,一致性指分散式服務化系統之間的弱一致性,包括應用系統一致性和數據一致性。
無論是水平拆分還是垂直拆分,都解決了特定場景下的特定問題,凡事有好的一面,都會有壞的一面,拆分後的系統或者服務化的系統最大的問題就是一致性問題,這麼多個具有元功能的模塊,或者同一個功能池中的多個節點之間,如何保證他們的信息是一致的、工作步伐是一致的、狀態是一致的、互相協調有序的工作呢?
本文根據作者在互聯網企業的實際項目經驗,對服務化系統中最難解決的一致性問題進行研究和探討,試圖從實踐經驗中找到規律,抽象出模式,分享給大家,希望對大家的項目實施有所幫助,在對實踐的總結中也會對相關的一致性術語做最朴實的解釋,希望能幫助大家徹底理解一致性的本質,並能將其應用到實踐,解決讀者現實中遇到的服務化系統的一致性問題,本文使用理論與實踐相結合的方法,突出在實踐中解決問題的模式,因此叫做《分散式服務化系統一致性的“最佳實幹”》。
實例,產生不一致的例子
- 案例1:買房
假如你想要享受生活的隨意,只想買個兩居,不想讓房貸有太大壓力,而你媳婦卻想要買個三居,還得帶花園的,那麼你們就不一致了,不一致導致生活不愉快、不協調,嚴重情況下還會吵架,可見生活中的不一致問題影響很大。 - 案例2:轉賬
轉賬是經典的不一致案例,設想一下銀行為你處理一筆轉賬,扣減你賬戶上的餘額,然後增加別人賬戶的餘額;如果扣減你的賬戶餘額成功,增加別人賬戶餘額失敗,那麼你就會損失這筆資金。反過來,如果扣減你的賬戶餘額失敗,增加別人賬戶餘額成功,那麼銀行就會損失這筆資金,銀行需要賠付。對於資金處理系統來說,上面任何一種場景都是不允許發生的,一旦發生就會有資金損失,後果是不堪設想的,嚴重情況會讓一個公司瞬間倒閉,可參考案例(http://blog.jobbole.com/50392/)。 - 案例3:下訂單和扣庫存
電商系統中也有一個經典的案例,下訂單和扣庫存如何保持一致,如果先下訂單,扣庫存失敗,那麼將會導致超賣;如果下訂單沒有成功,扣庫存成功,那麼會導致少賣。兩種情況都會導致運營成本的增加,嚴重情況下需要賠付。 - 案例4:同步超時
服務化的系統間調用常常因為網路問題導致系統間調用超時,即使是網路很好的機房,在億次流量的基數下,同步調用超時也是家常便飯。系統A同步調用系統B超時,系統A可以明確得到超時反饋,但是無法確定系統B是否已經完成了預定的功能或者沒有完成預定的功能。於是,系統A就迷茫了,不知道應該繼續做什麼,如何反饋給使用方。(曾經的一個B2B產品的客戶要求介面超時重新通知他們,這個在技術上是難以實現的,因為伺服器本身可能並不知道自己超時,可能會繼續正常的返回數據,只是客戶端並沒有接受到結果罷了,因此這不是一個合理的解決方案)。 - 案例5:非同步回調超時
此案例和上一個同步超時案例類似,不過這個場景使用了非同步回調,系統A同步調用系統B發起指令,系統B採用受理模式,受理後則返回受理成功,然後系統B非同步通知系統A。在這個過程中,如果系統A由於某種原因遲遲沒有收到回調結果,那麼兩個系統間的狀態就不一致,互相認知不同會導致系統間發生錯誤,嚴重情況下會影響核心事務,甚至會導致資金損失。 - 案例6:掉單
分散式系統中,兩個系統協作處理一個流程,分別為對方的上下游,如果一個系統中存在一個請求,通常指訂單,另外一個系統不存在,則導致掉單,掉單的後果很嚴重,有時候也會導致資金損失。 - 案例7:系統間狀態不一致
這個案例與上面掉單案例類似,不同的是兩個系統間都存在請求,但是請求的狀態不一致。 - 案例8:緩存和資料庫不一致
交易相關係統基本離不開關係型資料庫,依賴關係型資料庫提供的ACID特性(後面介紹),但是在大規模高併發的互聯網系統里,一些特殊的場景對讀的性能要求極高,服務於交易的資料庫難以抗住大規模的讀流量,通常需要在資料庫前墊緩存,那麼緩存和資料庫之間的數據如何保持一致性?是要保持強一致呢還是弱一致性呢? - 案例9:本地緩存節點間不一致
一個服務池上的多個節點為了滿足較高的性能需求,需要使用本地緩存,使用了本地緩存,每個節點都會有一份緩存數據的拷貝,如果這些數據是靜態的、不變的,那永遠都不會有問題,但是如果這些數據是半靜態的或者常被更新的,當被更新的時候,各個節點更新是有先後順序的,在更新的瞬間,各個節點的數據是不一致的,如果這些數據是為某一個開關服務的,想象一下重覆的請求走進了不同的節點(在failover或者補償導致的場景下,重覆請求是一定會發生的,也是服務化系統必須處理的),一個請求走了開關打開的邏輯,同時另外一個請求走了開關關閉的邏輯,這導致請求被處理兩次,最壞的情況下會導致災難性的後果,就是資金損失。 - 案例10:緩存數據結構不一致
這個案例會時有發生,某系統需要種某一數據結構的緩存,這一數據結構有多個數據元素組成,其中,某個數據元素都需要從資料庫中或者服務中獲取,如果一部分數據元素獲取失敗,由於程式處理不正確,仍然將不完全的數據結構存入緩存,那麼緩存的消費者消費的時候很有可能因為沒有合理處理異常情況而出錯。
ACID(酸)
如何保證強一致性呢?電腦專業的童鞋在學習關係型資料庫的時候都學習了ACID原理,這裡對ACID做個簡單的介紹。如果想全面的學習ACID原理,請參考ACID(https://en.wikipedia.org/wiki/ACID)。
關係型資料庫天生就是解決具有複雜事務場景的問題,關係型資料庫完全滿足ACID的特性。
ACID指的是:
- A: Atomicity,原子性
- C: Consistency,一致性
- I: Isolation,隔離性
- D: Durability,持久性
具有ACID的特性的資料庫支持強一致性,強一致性代表資料庫本身不會出現不一致,每個事務是原子的,或者成功或者失敗,事物間是隔離的,互相完全不影響,而且最終狀態是持久落盤的,因此,資料庫會從一個明確的狀態到另外一個明確的狀態,中間的臨時狀態是不會出現的,如果出現也會及時的自動的修複,因此是強一致的。
3個典型的關係型資料庫Oracle、Mysql、Db2都能保證強一致性,Oracle和Mysql使用多版本控制協議實現,而DB2使用改進的兩階段提交協議來實現。
如果你在為交易相關係統做技術選型,交易的存儲應該只考慮關係型資料庫,對於核心系統,如果需要較好的性能,可以考慮使用更強悍的硬體,這種向上擴展(升級硬體)雖然成本較高,但是是最簡單粗暴有效的方式,另外,Nosql完全不適合交易場景,Nosql主要用來做數據分析、ETL、報表、數據挖掘、推薦、日誌處理等非交易場景。
前面提到的案例2-轉賬和案例3-下訂單和扣庫存都可以利用關係型資料庫的強一致性解決。
然而,前面提到,互聯網項目多數具有大規模高併發的特性,必須應用拆分的理念,對高併發的壓力採取“大而化小、小而化了”的方法,否則難以滿足動輒億級流量的需求,即使使用關係型資料庫,單機也難以滿足存儲和TPS上的需求。為了保證案例2-轉賬可以利用關係型資料庫的強一致性,在拆分的時候儘量的把轉賬相關的賬戶放入一個資料庫分片,對於案例3,儘量的保證把訂單和庫存放入同一個資料庫分片,這樣通過關係型資料庫自然就解決了不一致的問題。
然而,有些時候事與願違,由於業務規則的限制,無法將相關的數據分到同一個資料庫分片,這個時候我們就需要實現最終一致性。
對於案例2-轉賬場景,假設賬戶數量巨大,對賬戶存儲進行了拆分,關係型資料庫一共分了8個實例,每個實例8個庫,每個庫8個表,共512張表,假如要轉賬的兩個賬戶正好落在了一個庫里,那麼可以依賴關係型資料庫的事務保持強一致性。
如果要轉賬的兩個賬戶正好落在了不同的庫里,轉賬操作是無法封裝在同一個資料庫事務中的,這個時候會發生一個庫的賬戶扣減餘額成功,另外一個庫的賬戶增加餘額失敗的情況。
對於這種情況,我們需要繼續探討解決之道,CAP原理和BASE原理,BASE原理通過記錄事務的中間的臨時狀態,實現最終一致性。
CAP(帽子理論)
如果想深入的學習CAP理論,請參考CAP(https://en.wikipedia.org/wiki/CAP_theorem)。
由於對系統或者數據進行了拆分,我們的系統不再是單機系統,而是分散式系統,針對分散式系的帽子理論包含三個元素:
- C:Consistency,一致性, 數據一致更新,所有數據變動都是同步的
- A:Availability,可用性, 好的響應性能,完全的可用性指的是在任何故障模型下,服務都會在有限的時間處理響應
- P:Partition tolerance,分區容錯性,可靠性
帽子理論證明,任何分散式系統只可同時滿足二點,沒法三者兼顧。關係型資料庫由於關係型資料庫是單節點的,因此,不具有分區容錯性,但是具有一致性和可用性,而分散式的服務化系統都需要滿足分區容錯性,那麼我們必須在一致性和可用性中進行權衡,具體表現在服務化系統處理的異常請求在某一個時間段內可能是不完全的,但是經過自動的或者手工的補償後,達到了最終的一致性。
BASE(鹼)
BASE理論解決CAP理論提出了分散式系統的一致性和可用性不能兼得的問題,如果想全面的學習BASE原理,請參考Eventual consistency(https://en.wikipedia.org/wiki/Eventual_consistency)。
BASE在英文中有“鹼”的意思,對應本節開頭的ACID在英文中“酸”的意思,基於這兩個名詞提出了酸鹼平衡的結論,簡單來說是在不同的場景下,可以分別利用ACID和BASE來解決分散式服務化系統的一致性問題。
BASE模型與ACID模型截然不同,滿足CAP理論,通過犧牲強一致性,獲得可用性,一般應用在服務化系統的應用層或者大數據處理系統,通過達到最終一致性來儘量滿足業務的絕大部分需求。
BASE模型包含個三個元素:
- BA:Basically Available,基本可用
- S:Soft State,軟狀態,狀態可以有一段時間不同步
- E:Eventually Consistent,最終一致,最終數據是一致的就可以了,而不是時時保持強一致
BASE模型的軟狀態是實現BASE理論的方法,基本可用和最終一致是目標。按照BASE模型實現的系統,由於不保證強一致性,系統在處理請求的過程中,可以存在短暫的不一致,在短暫的不一致視窗請求處理處在臨時狀態中,系統在做每步操作的時候,通過記錄每一個臨時狀態,在系統出現故障的時候,可以從這些中間狀態繼續未完成的請求處理或者退回到原始狀態,最後達到一致的狀態。
以案例1-轉賬為例,我們把用戶A給用戶B轉賬分成四個階段,第一個階段用戶A準備轉賬,第二個階段從用戶A賬戶扣減餘額,第三個階段對用戶B增加餘額,第四個階段完成轉賬。系統需要記錄操作過程中每一步驟的狀態,一旦系統出現故障,系統能夠自動發現沒有完成的任務,然後,根據任務所處的狀態,繼續執行任務,最終完成任務,達到一致的最終狀態。
在實際應用中,上面這個過程通常是通過持久化執行任務的狀態和環境信息,一旦出現問題,定時任務會撈取未執行完的任務,繼續未執行完的任務,直到執行完成為止,或者取消已經完成的部分操作回到原始狀態。這種方法在任務完成每個階段的時候,都要更新資料庫中任務的狀態,這在大規模高併發系統中不會有太好的性能,一個更好的辦法是用Write-Ahead Log(寫前日誌),這和資料庫的Bin Log(操作日誌)相似,在做每一個操作步驟,都先寫入日誌,如果操作遇到問題而停止的時候,可以讀取日誌按照步驟進行恢復,並且繼續執行未完成的工作,最後達到一致。寫前日誌可以利用機械硬碟的追加寫而達到較好性能,因此,這是一種專業化的實現方式,多數業務繫系統還是使用資料庫記錄的欄位來記錄任務的執行狀態,也就是記錄中間的“軟狀態”,一個任務的狀態流轉一般可以通過資料庫的行級鎖來實現,這比使用Write-Ahead Log實現更簡單、更快速。