1. 程式人生 > >分散式服務化系統一致性(分散式事務、ACID、BASE、CAP)原理與解決方案

分散式服務化系統一致性(分散式事務、ACID、BASE、CAP)原理與解決方案

1、背景
 一致性是一個抽象的、具有多重含義的計算機術語,在不同應用場景下,有不同的定義和含義。在傳統的IT時代,一致性通常指強一致性,強一致性通常體現在你中有我、我中有你、渾然一體;而在網際網路時代,一致性的含義遠遠超出了它原有的含義,在我們討論網際網路時代的一致性之前,我們先了解一下網際網路時代的特點,網際網路時代資訊量巨大、需要計算能力巨大,不但對使用者響應速度要求快,而且吞吐量指標也要向外擴充套件(既:水平伸縮),於是單節點的伺服器無法滿足需求,服務節點開始池化,想想那個經典的故事,一隻筷子一折就斷,一把筷子怎麼都折不斷,可見人多力量大的思想是多麼的重要,但是人多也不一定能解決所有事情,還得進行有序、合理的分配任務,進行有效的管理,於是網際網路時代談論最多的話題就是拆分,拆分一般分為“水平拆分”和“垂直拆分”(大家不要對應到資料庫或者快取拆分,這裡主要表達一種邏輯)。這裡,“水平拆分”指的是同一個功能由於單機節點無法滿足效能需求,需要擴充套件成為多節點,多個節點具有一致的功能,組成一個服務池,一個節點服務一部分的請求量,團結起來共同處理大規模高併發的請求量。“垂直拆分”指的是按照功能拆分,秉著“專業的人幹專業的事兒”的原則,把一個複雜的功能拆分到多個單一的簡單的元功能,不同的元功能組合在一起,和未拆分前完成的功能是一致的,由於每個元功能職責單一、功能簡單,讓維護和變更都變得更簡單、安全,更易於產品版本的迭代,在這樣的一個網際網路的時代和環境,一致性指分散式服務化系統之間的弱一致性,包括應用系統一致性和資料一致性。
  無論是水平拆分還是垂直拆分,都解決了特定場景下的特定問題,凡事有好的一面,都會有壞的一面,拆分後的系統或者服務化的系統最大的問題就是一致性問題,這麼多個具有元功能的模組,或者同一個功能池中的多個節點之間,如何保證他們的資訊是一致的、工作步伐是一致的、狀態是一致的、互相協調有序的工作呢?
  本文根據作者在網際網路企業的實際專案經驗,對服務化系統中最難解決的一致性問題進行研究和探討,試圖從實踐經驗中找到規律,抽象出模式,分享給大家,希望對大家的專案實施有所幫助,在對實踐的總結中也會對相關的一致性術語做最樸實的解釋,希望能幫助大家徹底理解一致性的本質,並能將其應用到實踐,解決讀者現實中遇到的服務化系統的一致性問題,本文使用理論與實踐相結合的方法,突出在實踐中解決問題的模式,因此叫做《分散式服務化系統一致性的“最佳實幹”》。
  
2、問題


  本節列舉不一致會導致的種種問題,這也包括一例生活中的問題。
  
  案例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:快取資料結構不一致
  這個案例會時有發生,某系統需要種某一資料結構的快取,這一資料結構有多個數據元素組成,其中,某個資料元素都需要從資料庫中或者服務中獲取,如果一部分資料元素獲取失敗,由於程式處理不正確,仍然將不完全的資料結構存入快取,那麼快取的消費者消費的時候很有可能因為沒有合理處理異常情況而出錯。
  
3、模式
  3.1 生活中不一致問題的解決
  大家回顧一下上一節列舉的生活中的案例1-買房,如果置身事外來看,解決這種不一致的辦法有兩個,一個是避免不一致的發生,如果已經是媳婦了就不好辦了:),還有一種方法就是慢慢的補償,先買個兩居,然後慢慢的等資金充裕了再換三居,買比特幣賺了再換帶花園的房子,於是問題最終被解決了,最終大家處於一致的狀態,都開心了。這樣可以解決案例1的問題,很自然由於有了過渡的方法,問題在不經意間就消失了,可見“過渡”也是解決一致性問題的一個模式。
  從案例1的解決方案來看,我們要解決一致性問題,一個最直接最簡單的方法就是保持強一致性,對於案例1的情況,儘量避免在結婚前兩個人能夠互相瞭解達成一致,避免不一致問題的發生;不過有些事情事已至此,發生了就是發生了,出現了不一致的問題,我們應該考慮去補償,盡最大的努力從不一致狀態修復到一致狀態,避免損失全部或者一部分,也不失為一個好方法。
  因此,避免不一致是上策,出現了不一致及時發現及時修復是中策,有問題不積極解決留給他人解決是下策。
  3.2 酸鹼平衡理論
  ACID在英文中的意思是“酸”,BASE的意識是“鹼”,這一段講的是“酸鹼平衡”的故事。
  
  1. 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原理通過記錄事務的中間的臨時狀態,實現最終一致性。
  
  2. CAP(帽子理論)
  如果想深入的學習CAP理論,請參考CAP(https://en.wikipedia.org/wiki/CAP_theorem)。
  由於對系統或者資料進行了拆分,我們的系統不再是單機系統,而是分散式系統,針對分散式系的帽子理論包含三個元素:

C:Consistency,一致性, 資料一致更新,所有資料變動都是同步的
  A:Availability,可用性, 好的響應效能,完全的可用性指的是在任何故障模型下,服務都會在有限的時間處理響應
  P:Partition tolerance,分割槽容錯性,可靠性

  帽子理論證明,任何分散式系統只可同時滿足二點,沒法三者兼顧。關係型資料庫由於關係型資料庫是單節點的,因此,不具有分割槽容錯性,但是具有一致性和可用性,而分散式的服務化系統都需要滿足分割槽容錯性,那麼我們必須在一致性和可用性中進行權衡,具體表現在服務化系統處理的異常請求在某一個時間段內可能是不完全的,但是經過自動的或者手工的補償後,達到了最終的一致性。
  
  3. 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實現更簡單、更快速。

  4. 酸鹼平衡的總結

  • 使用向上擴充套件(強悍的硬體)執行專業的關係型資料庫(例如:Oracle或者DB2)能夠保證強一致性,錢能解決的問題就不是問題
  • 如果錢是問題,可以對廉價硬體執行的開源關係型資料庫(例如:Mysql)進行分片,將相關的資料分到資料庫的同一個片,仍然能夠使用關係型資料庫保證事務
  • 如果業務規則限制,無法將相關的資料分到同一個片,就需要實現最終一致性,通過記錄事務的軟狀態(中間狀態、臨時狀態),一旦處於不一致,可以通過系統自動化或者人工干預來修復不一致的情況

      3.3 分散式一致性協議
      國際開放標準組織Open Group(http://opengroup.org/)定義了DTS(分散式事務處理模型),模型中包含4個角色:應用程式、事務管理器、資源管理器、通訊資源管理器四部分。事務處理器是統管全域性的管理者,資源處理器和通訊資源處理器是事務的參與者。
      J2EE規範也包含此分散式事務處理模型的規範,並在所有的AppServer中進行實現,J2EE規範中定義了TX協議和XA協議,TX協議定義應用程式與事務管理器之間的介面,而XA協議定義了事務管理器與資源處理器之間的介面,在過去,大家使用AppServer,例如:Websphere、Weblogic、Jboss等配置資料來源的時候會看見類似XADatasource的資料來源,這就是實現了DTS的關係型資料庫的資料來源。企業級開發JEE中,關係型資料庫、JMS服務扮演資源管理器的角色,而EJB容器則扮演事務管理器的角色。
      下面我們就介紹兩階段提交協議(https://en.wikipedia.org/wiki/Two-phase_commit_protocol)、三階段提交協議(https://en.wikipedia.org/wiki/Three-phase_commit_protocol)以及阿里巴巴提出的TCC,它們都是根據DTS這一思想演變出來的。
      
      1. 兩階段提交協議
      上面描述的JEE的XA協議就是根據兩階段提交來保證事務的完整性,並實現分散式服務化的強一致性。
      兩階段提交協議把分散式事務分成兩個過程,一個是準備階段,一個是提交階段,準備階段和提交階段都是由事務管理器發起的,為了接下來講解方便,我們把事務管理器稱為協調者,把資管管理器稱為參與者。
      
      兩階段如下:

    1. 準備階段:協調者向參與者發起指令,參與者評估自己的狀態,如果參與者評估指令可以完成,參與者會寫redo或者undo日誌(這也是前面提起的Write-Ahead
      Log的一種),然後鎖定資源,執行操作,但是並不提交
    2. 提交階段:如果每個參與者明確返回準備成功,也就是預留資源和執行操作成功,協調者向參與者發起提交指令,參與者提交資源變更的事務,釋放鎖定的資源;如果任何一個參與者明確返回準備失敗,也就是預留資源或者執行操作失敗,協調者向參與者發起中止指令,參與者取消已經變更的事務,執行undo日誌,釋放鎖定的資源

        兩階段提交協議成功場景示意圖如下:
        
        這裡寫圖片描述
        
        我們看到兩階段提交協議在準備階段鎖定資源,是一個重量級的操作,並能保證強一致性,但是實現起來複雜、成本較高,不夠靈活,更重要的是它有如下致命的問題:

  • 阻塞:從上面的描述來看,對於任何一次指令必須收到明確的響應,才會繼續做下一步,否則處於阻塞狀態,佔用的資源被一直鎖定,不會被釋放

  • 單點故障:如果協調者宕機,參與者沒有了協調者指揮,會一直阻塞,儘管可以通過選舉新的協調者替代原有協調者,但是如果之前協調者在傳送一個提交指令後宕機,而提交指令僅僅被一個參與者接受,並且參與者接收後也宕機,新上任的協調者無法處理這種情況
  • 腦裂:協調者傳送提交指令,有的參與者接收到執行了事務,有的參與者沒有接收到事務,就沒有執行事務,多個參與者之間是不一致的

      2. 三階段提交協議
      三階段提交協議是兩階段提交協議的改進版本。它通過超時機制解決了阻塞的問題,並且把兩個階段增加為三個階段:

  • 詢問階段:協調者詢問參與者是否可以完成指令,協調者只需要回答是還是不是,而不需要做真正的操作,這個階段超時導致中止

  • 準備階段:如果在詢問階段所有的參與者都返回可以執行操作,協調者向參與者傳送預執行請求,然後參與者寫redo和undo日誌,執行操作,但是不提交操作;如果在詢問階段任何參與者返回不能執行操作的結果,則協調者向參與者傳送中止請求,這裡的邏輯與兩階段提交協議的的準備階段是相似的,這個階段超時導致成功
  • 提交階段:如果每個參與者在準備階段返回準備成功,也就是預留資源和執行操作成功,協調者向參與者發起提交指令,參與者提交資源變更的事務,釋放鎖定的資源;如果任何一個參與者返回準備失敗,也就是預留資源或者執行操作失敗,協調者向參與者發起中止指令,參與者取消已經變更的事務,執行undo日誌,釋放鎖定的資源,這裡的邏輯與兩階段提交協議的提交階段一致

      三階段提交協議成功場景示意圖如下:
      
    這裡寫圖片描述

      然而,這裡與兩階段提交協議有兩個主要的不同:

  • 增加了一個詢問階段,詢問階段可以確保儘可能早的發現無法執行操作而需要中止的行為,但是它並不能發現所有的這種行為,只會減少這種情況的發生,在準備階段以後,協調者和參與者執行的任務中都增加了超時,一旦超時,協調者和參與者都繼續提交事務,預設為成功,這也是根據概率統計上超時後預設成功的正確性最大

  • 三階段提交協議與兩階段提交協議相比,具有如上的優點,但是一旦發生超時,系統仍然會發生不一致,只不過這種情況很少見罷了,好處就是至少不會阻塞和永遠鎖定資源。

      3. TCC
      上面兩節講解了兩階段提交協議和三階段提交協議,實際上他們能解決案例2-轉賬和案例3-下訂單和扣庫存中的分散式事務的問題,但是遇到極端情況,系統會發生阻塞或者不一致的問題,需要運營或者技術人工解決。無論兩階段還是三階段方案中都包含多個參與者、多個階段實現一個事務,實現複雜,效能也是一個很大的問題,因此,在網際網路高併發系統中,鮮有使用兩階段提交和三階段提交協議的場景。
      阿里巴巴提出了新的TCC協議,TCC協議將一個任務拆分成Try、Confirm、Cancel,正常的流程會先執行Try,如果執行沒有問題,再執行Confirm,如果執行過程中出了問題,則執行操作的逆操Cancel,從正常的流程上講,這仍然是一個兩階段的提交協議,但是,在執行出現問題的時候,有一定的自我修復能力,如果任何一個參與者出現了問題,協調者通過執行操作的逆操作來取消之前的操作,達到最終的一致狀態。
      可以看出,從時序上,如果遇到極端情況下TCC會有很多問題的,例如,如果在Cancel的時候一些參與者收到指令,而一些參與者沒有收到指令,整個系統仍然是不一致的,這種複雜的情況,系統首先會通過補償的方式,嘗試自動修復的,如果系統無法修復,必須由人工參與解決。
      從TCC的邏輯上看,可以說TCC是簡化版的三階段提交協議,解決了兩階段提交協議的阻塞問題,但是沒有解決極端情況下會出現不一致和腦裂的問題。然而,TCC通過自動化補償手段,會把需要人工處理的不一致情況降到到最少,也是一種非常有用的解決方案,根據線人,阿里在內部的一些中介軟體上實現了TCC模式。
      我們給出一個使用TCC的實際案例,在秒殺的場景,使用者發起下單請求,應用層先查詢庫存,確認商品庫存還有餘量,則鎖定庫存,此時訂單狀態為待支付,然後指引使用者去支付,由於某種原因使用者支付失敗,或者支付超時,系統會自動將鎖定的庫存解鎖供其他使用者秒殺。
      
      TCC協議使用場景示意圖如下:
      
    這裡寫圖片描述

      3.4 保證最終一致性的模式
      在大規模高併發服務化系統中,一個功能被拆分成多個具有單一功能的元功能,一個流程會有多個系統的多個元功能組合實現,如果使用兩階段提交協議和三階段提交協議,確實能解決系統間一致性問題,除了這兩個協議帶來的自身的問題,這些協議的實現比較複雜、成本比較高,最重要的是效能並不好,相比來看,TCC協議更簡單、容易實現,但是TCC協議由於每個事務都需要執行Try,再執行Confirm,略微顯得臃腫,因此,在現實的系統中,底線要求僅僅需要能達到最終一致性,而不需要實現專業的、複雜的一致性協議,實現最終一致性有一些非常有效的、簡單粗暴的模式,下面就介紹這些模式及其應用場景。
      1. 查詢模式
      任何一個服務操作都需要提供一個查詢介面,用來向外部輸出操作執行的狀態。服務操作的使用方可以通過查詢介面,得知服務操作執行的狀態,然後根據不同狀態來做不同的處理操作。
      為了能夠實現查詢,每個服務操作都需要有唯一的流水號標識,也可使用此次服務操作對應的資源ID來標誌,例如:請求流水號、訂單號等。
      首先,單筆查詢操作是必須提供的,我們也鼓勵使用單筆訂單查詢,這是因為每次呼叫需要佔用的負載是可控的,批量查詢則根據需要來提供,如果使用了批量查詢,需要有合理的分頁機制,並且必須限制分頁的大小,以及對批量查詢的QPS需要有容量評估和流控等。
      
      查詢模式的示意圖如下:

這裡寫圖片描述

  對於案例4:同步超時、案例5:非同步回撥超時、案例6:掉單、案例7:系統間狀態不一致,我們都需要使用查詢模式來了解被呼叫服務的處理情況,來決定下一步做什麼:補償未完成的操作還是回滾已經完成的操作。
  2. 補償模式
  有了上面的查詢模式,在任何情況下,我們都能得知具體的操作所處的狀態,如果整個操作處於不正常的狀態,我們需要修正操作中有問題的子操作,這可能需要重新執行未完成的子操作,後者取消已經完成的子操作,通過修復使整個分散式系統達到一致,為了讓系統最終一致而做的努力都叫做補償。
  對於服務化系統中同步呼叫的操作,業務操作發起的主動方在還沒有得到業務操作執行方的明確返回或者呼叫超時,場景可參考案例4:同步超時,這個時候業務發起的主動方需要及時的呼叫業務執行方獲得操作執行的狀態,這裡使用查詢模式,獲得業務操作的執行方的狀態後,如果業務執行方已經完預設的工作,則業務發起方給業務的使用方返回成功,如果業務操作的執行方的狀態為失敗或者未知,則會立即告訴業務的使用方失敗,然後呼叫業務操作的逆向操作,保證操作不被執行或者回滾已經執行的操作,讓業務的使用方、業務發起的主動方、業務的操作方最終達成一致的狀態。
  
  補償模式的示意圖如下:

這裡寫圖片描述

  補償操作根據發起形式分為:

  • 自動恢復:程式根據發生不一致的環境,通過繼續未完成的操作,或者回滾已經完成的操作,自動來達到一致
  • 通知運營:如果程式無法自動恢復,並且設計時考慮到了不一致的場景,可以提供運營功能,通過運營手工進行補償
  • 通知技術:如果很不巧,系統無法自動回覆,又沒有運營功能,那必須通過技術手段來解決,技術手段包括走資料庫變更或者程式碼變更來解決,這是最糟的一種場景

      3. 非同步確保模式
      非同步確保模式是補償模式的一個典型案例,經常應用到使用方對響應時間要求並不太高,我們通常把這類操作從主流程中摘除,通過非同步的方式進行處理,處理後把結果通過通知系統通知給使用方,這個方案最大的好處能夠對高併發流量進行消峰,例如:電商系統中的物流、配送,以及支付系統中的計費、入賬等。
      實踐中,將要執行的非同步操作封裝後持久入庫,然後通過定時撈取未完成的任務進行補償操作來實現非同步確保模式,只要定時系統足夠健壯,任何一個任務最終會被成功執行。
      
      非同步確保模式的示意圖如下:

這裡寫圖片描述

  對於案例5:非同步回撥超時,使用的就是非同步確保模式,這種情況下對於某個操作,如果遲遲沒有收到響應,我們通過查詢模式和補償模式來繼續未完成的操作。
  
  4. 定期校對模式
  既然我們在系統中實現最終一致性,系統在沒有達到一致之前,系統間的狀態是不一致的,甚至是混亂的,需要補償操作來達到一致的目的,但是我們如何來發現需要補償的操作呢?
  在操作的主流程中的系統間執行校對操作,我們可以事後非同步的批量校對操作的狀態,如果發現不一致的操作,則進行補償,補償操作與補償模式中的補償操作是一致的。
  另外,實現定期校對的一個關鍵就是分散式系統中需要有一個自始至終唯一的ID,ID的生成請參考SnowFlake(https://github.com/twitter/snowflake)。
  
  在分散式系統中,全域性唯一ID的示意圖如下:

這裡寫圖片描述

  一般情況下,生成全域性唯一ID有兩種方法:

  • 持久型:使用資料庫表自增欄位或者Sequence生成,為了提高效率,每個應用節點可以快取一批次的ID,如果機器重啟可能會損失一部分ID,但是這並不會產生任何問題
  • 時間型:一般由機器號、業務號、時間、單節點內自增ID組成,由於時間一般精確到秒或者毫秒,因此不需要持久就能保證在分散式系統中全域性唯一、粗略遞增能特點

      實踐中,為了能在分散式系統中迅速的定位問題,一般的分散式系統都有技術支援系統,它能夠跟蹤一個請求的呼叫鏈,呼叫鏈是在二維的維度跟蹤一個呼叫請求,最後形成一個呼叫樹,原理可參考谷歌的論文Dapper, a Large-Scale Distributed Systems Tracing Infrastructure(https://research.google.com/pubs/pub36356.html),一個開源的參考實現為pinpoint(https://github.com/naver/pinpoint)。
      
      在分散式系統中,呼叫鏈的示意圖如下:

這裡寫圖片描述
  
  全域性的唯一流水ID可以把一個請求在分散式系統中的流轉的路徑聚合,而呼叫鏈中的spanid可以把聚合的請求路徑通過樹形結構進行展示,讓技術支援人員輕鬆的發現系統出現的問題,能夠快速定位出現問題的服務節點,提高應急效率。
  關於訂單跟蹤、呼叫鏈跟蹤、業務鏈跟蹤,我們會在後續文章中詳細介紹。
  在分散式系統中構建了唯一ID,呼叫鏈等基礎設施,我們很容易對系統間的不一致進行核對,通常我們需要構建第三方的定期核對系統,以第三方的角度來監控服務執行的健康程度。
  
  定期核對系統示意圖如下:

這裡寫圖片描述
  
  對於案例6:掉單、案例7:系統間狀態不一致通常通過定期校對模式發現問題,並通過補償模式來修復,最後完成系統間的最終一致性。
  定期校對模式多應用在金融系統,金融系統由於涉及到資金安全,需要保證百分之百的準確性,所以,需要多重的一致性保證機制,包括:系統間的一致性對賬、現金對賬、賬務對賬、手續費對賬等等,這些都屬於定期校對模式,順便說一下,金融系統與社交應用在技術上本質的區別在於社交應用在於量大,而金融系統在於資料的準確性。
  到現在為止,我們看到通過查詢模式、補償模式、定期核對模式可以解決案例4到案例7的所有問題,對於案例4:同步超時,如果同步超時,我們需要查詢狀態進行補償,對於案例5:非同步回撥超時,如果遲遲沒有收到回撥響應,我們也會通過查詢狀態進行補償,對於案例6:掉單、案例7:系統間狀態不一致,我們通過定期核對模式可以保證系統間操作的一致性,避免掉單和狀態不一致導致問題。
  
  5. 可靠訊息模式
  在分散式系統中,對於主流程中優先順序比較低的操作,大多采用非同步的方式執行,也就是前面提到的非同步確保型,為了讓非同步操作的呼叫方和被呼叫方充分的解耦,也由於專業的訊息佇列本身具有可伸縮、可分片、可持久等功能,我們通常通過訊息佇列實現非同步化,對於訊息佇列,我們需要建立特殊的設施保證可靠的訊息傳送以及處理機的冪等等。
  
  訊息的可靠傳送
  
  訊息的可靠傳送可以認為是盡最大努力傳送訊息通知,有兩種實現方法:
  第一種,傳送訊息之前,把訊息持久到資料庫,狀態標記為待發送,然後傳送訊息,如果傳送成功,將訊息改為傳送成功。定時任務定時從資料庫撈取一定時間內未傳送的訊息,將訊息傳送。

這裡寫圖片描述
  
  第二種,實現方式與第一種類似,不同的是持久訊息的資料庫是獨立的,並不耦合在業務系統中。傳送訊息之前,先發送一個預訊息給某一個第三方的訊息管理器,訊息管理器將其持久到資料庫,並標記狀態為待發送,傳送成功後,標記訊息為傳送成功。定時任務定時從資料庫撈取一定時間內未傳送的訊息,回查業務系統是否要繼續傳送,根據查詢結果來確定訊息的狀態。

這裡寫圖片描述
  
  一些公司把訊息的可靠傳送實現在了中介軟體裡,通過Spring的注入,在訊息傳送的時候自動持久訊息記錄,如果有訊息記錄沒有傳送成功,定時會補償傳送。
  訊息處理器的冪等性
  如果我們要保證訊息可靠的傳送,簡單來說,要保證訊息一定要傳送出去,那麼就需要有重試機制,有了重試機制,訊息一定會重複,那麼我們需要對重複做處理。
  

處理重複的最佳方式為保證操作的冪等性,冪等性的數學公式為:
  f(f(x)) = f(x)

  保證操作的冪等性常用的幾個方法:

  • 使用資料庫表的唯一鍵進行濾重,拒絕重複的請求
  • 使用分散式表對請求進行濾重
  • 使用狀態流轉的方向性來濾重,通常使用行級鎖來實現(後續在鎖相關的文章中詳細說明)
  • 根據業務的特點,操作本身就是冪等的,例如:刪除一個資源、增加一個資源、獲得一個資源等

      6. 快取一致性模型
      大規模高併發系統中一個常見的核心需求就是億級的讀需求,顯然,關係型資料庫並不是解決高併發讀需求的最佳方案,網際網路的經典做法就是使用快取抗讀需求,下面有一些使用快取的保證一致性的最佳實踐:
      

  • 如果效能要求不是非常的高,儘量使用分散式快取,而不要使用本地快取

  • 種快取的時候一定種完全,如果快取資料的一部分有效,一部分無效,寧可放棄種快取,也不要把部分資料種入快取
  • 資料庫與快取只需要保持弱一致性,而不需要強一致性,讀的順序要先快取,後資料庫,寫的順序要先資料庫,後快取

      這裡的最佳實踐能夠解決案例8:快取和資料庫不一致、案例9:本地快取節點間不一致、案例10:快取資料結構不一致的問題,對於資料儲存層、快取與資料庫、Nosql等的一致性是更深入的儲存一致性技術,將會在後續文章單獨介紹,這裡的資料一致性主要是處理應用層與快取、應用層與資料庫、一部分的快取與資料庫的一致性。
      
      3.5 專題模式
      這一節介紹特殊場景下的一致性問題和解決方案。
      
      遷移開關的設計
      
      在大多數企業裡,新專案和老專案一般會共存,大家都在努力的下掉老專案,但是由於種種原因總是下不掉,如果要徹底的下掉老專案,就必須要有非常完善的遷移方案,遷移是一項非常複雜而艱鉅的任務,我會在將來的文章中詳細探討遷移方案、流程和技術,這裡我們只對遷移中使用的開關進行描述。
      遷移過程必須使用開關,開關一般都會基於多個維度來設計,例如:全域性的、使用者的、角色的、商戶的、產品的等等,如果遷移過程中遇到問題,我們需要關閉開關,遷移回老的系統,這需要我們的新系統相容老的資料,老的系統也相容新的資料,從某種意義上來講,遷移比實現新系統更加困難。
      曾經看過很多簡單的開關設計,有的開關設計在應用層次,通過一個curl語句呼叫,沒有許可權控制,這樣的開關在服務池的每個節點都是不同步的、不一致的;還有的系統把開關配置放在中心化的配置系統、資料庫或者快取等,處理的每個請求都通過統一的開關來判斷是否遷移等等,這樣的開關有一個致命的缺點,服務請求在處理過程中,開關可能會變化,各個節點之間開關可能不同步、不一致,導致重複的請求可能走到新的邏輯又走了老的邏輯,如果新的邏輯和老的邏輯沒有保證冪等性,這個請求就被重複處理了,如果是金融行業的應用,可能會導致資金損失,電商系統可能會導致發貨並退款等問題。
      這裡面我們推薦使用訂單開關,不管我們在什麼維度上設計了開關,接收到服務請求後,我們在請求建立的關聯實體(例如:訂單)上標記開關,以後的任何處理流程,包括同步的和非同步的處理流程,都通過訂單上的開關來判斷,而不是通過全域性的或者基於配置的開關,這樣在訂單建立的時候,開關已經確定,不再變更,一旦一份資料不再發生變化,那麼它永遠是執行緒安全的,並且不會有不一致的問題。
      這個模式在生產中使用比較頻繁,建議每個企業都把這個模式作為設計評審的一項,如果不檢查這一項,很多開發童鞋都會偷懶,直接在配置中或者資料庫中做個開關就上線了。
      
      4、總結
      
      本文從一致性問題的實踐出發,從大規模高併發服務化系統的實踐經驗中進行總結,列舉導致不一致的具體問題,圍繞著具體問題,總結出解決不一致的方法,並且抽象成模式,供大家在開發服務化系統的過程中參考。
      另外,由於篇幅有限,還有一些關於分散式一致性的技術無法在一篇文章中與大家分享,包括:paxos演算法、raft演算法、zab演算法、nwr演算法、一致性雜湊等,我會在後續文章中詳細介紹。