服務更新的思考

本篇文章總結一下個人所思考的關於分佈式下怎麼去比較好地熱更新一個服務,其中會涉及到一些關於分佈式下一致性的思考以及升級和數據遷移的一些思路。下面進入正題。

分類

  • 從更新的對象角度看,涉及兩個主要方面:一個是程序二進制可執行文件的更新,一個是程序執行時的配置文件的更新。

  • 從更新所造成的可用性影響上看,有冷和熱兩種兩種方式

  1. 冷更新:停服務,更新,恢復服務。從流程上看,是有一段時間服務處於不可用狀態的,這對於遊戲或者一些可用性要求較高的公有服務其實是不可接受的。
  2. 熱更新:服務正常運行,不需要停服務更新。是提升用戶體驗的一個非常有效的手段, 小到單機服務,大到分佈式服務。一旦你支持熱更新,那麼就意味着更新過程中用戶可以基本無感知或者說感受不到失敗(可能會感受到一些延遲)。

面對的問題

由於冷更新很簡單,也不適用於互聯網等要求可用性較高的方向,就不多說了。本文主要討論在分佈式的情況下,無狀態的業務層怎麼做熱更新。

這裏我們拿對象存儲業務來討論,由於其涉及存儲和一致性問題,所以相對來講也比較具有一般性,普適性。一旦掌握理解了,對其他系統是具有良好的參考意義甚至直接拿來主義的。

業務架構

在這裏插入圖片描述
架構上看都很明顯,只說明一下數據層的route layer,這一層的作用就是把數據層做一個抽象,數據層後端即便是多異構,對於業務層來說也是透明的,使用起來和一套同構的沒有任何區別。包括滾動升級、數據遷移等等都是透明的,agent層服務負責管理路由信息,用戶都是無感知的。Agent實例本身對於業務來說是無狀態的,所有數據部分的狀態都是在下面的數據庫中,但是其自身邏輯的路由信息是有狀態的,需要管理。

對於一個寫請求,大致流程爲:用戶通過應用發起請求,服務側的接入層進行分流和導流,進一步請求進入到業務層,業務層收到請求後,對於每一個data block通過data client經data agent寫入到數據服務,成功後得到一個block id,之後將block id作爲metadata通過meta client經meta agent寫入到元數據服務,之後後返回給用戶結果。

對於整個架構和流程,可以看出route layer是至關重要的,它是易用性、一致性、可用性上非常重要的一層。下面着重討論元數據部分,其他的都可以通過這方面的討論類比分析。

一個業務很容易初期採用簡單粗暴的方式快速部署,無論從成本還是易用性角度,都很可能選擇mysql作爲其metadata服務。而假如隨着業務的發展,其儲量和流量可能越來越大,不是一個mysql能hold住的了,就需要擴容或者遷移到一個分佈式數據庫上(這裏忽略proxy的sharding方式,個人不是很喜歡proxy的方式,不太可控,提供的一致性語義相對較弱,比如說事務,而且維護複雜)。一旦你有了這樣的需求,就需要定製一個可靠的計劃做這件事,那麼這裏假設是基於上述架構來做這件事。很容易看出,我們只需把route layer做好,對於meta來說即把meta agent做好就可以了,更進一步,本質就是做好meta agent的狀態管理即可。這個狀態管理的終極目標就是保證數據的一致性、服務的易用性和可用性。下面通過幾個常用的方式去討論具體的作法。

具體做法

1. 藍綠部署

藍綠部署是一個很常見的業務更新方式,其核心思想是一半一半做。先下掉一半,更新,之後上線,另一半重複上述步驟。其有兩個主要問題:

  1. 一致性
    對於一致性要求不高的服務,簡單的按照上述做法,只要數據層client來點重試,那麼業務層除了latency,是沒有其他感知的。但這樣做其實背後隱藏着一致性問題,因爲假設不加任何保護措施,在下掉一半的時候其上有一個用戶寫請求A,假如client立刻重試A到了另一半併成功,之後用戶又發起了一個寫請求B,覆蓋了剛纔A的數據,這時候被下掉的一半其實已經發出了之前的請求A,又執行了且成功了,這時候用戶後面的請求B就被覆蓋了,破壞了線性一致性。那麼狠容易想到,我可以讓被下掉的一般以gracefully的方式把doing的請求做完再下掉啊,期間不接受新請求,這樣client就不會因爲FIN/RST原因重試了啊。這樣的問題就是,如果crush了呢?甚至於說網絡問題導致非FIN/RST而是timeout了呢?Client一樣會重試。
  2. 可用性
    由於需要下掉一半,那麼意味着可提供服務的能力也下降了一半,這樣的話對於持續高壓的業務是沒辦法接受的,因爲一個是你找不到這麼一個時間點流量低去升級,一個是沒那麼土豪搞那麼多機器去臨時擴容,當然這些都比較極端,可能絕大部分業務沒這樣的問題。但還有一個問題,就是這樣實現,那麼下掉的流程基本上就是a.先向S1的所有服務實例發送停止接收新請求的命令,b.之後向服務發現發送請求摘掉這一半agent S1,都成功且處於空閒狀態後,c. 進行更新。這裏a和b不可以調換,否則可能會有線性一致性問題,比如我這個更新是切庫,先一個請求通過更新後的agent把數據寫入到了新集羣,後一個請求通過還未更新的agent讀取數據,讀不到的。另外上述流程看似沒問題,其實這裏隱藏了一個問題,那就是可能影響服務的可用性。因爲業務層都是有緩存的,也就是說不是每個請求都去服務發現那裏問一下都有哪些agent(且不說服務發現能不能扛得住,latency也比較難以接受),既然有緩存,那麼就可能存在緩存不一致,就存在某一個client多次重試都定向到了被下掉的S1中,最終請求失敗了。當然可以通過一定的策略大概率上避免這個問題,就是幾次失敗後刷新緩存,而不是等到緩存失效,甚至服務發現具有通知機制通知更新。但其實還隱藏着另外一個問題,假設第二步向S1所有實例發送停止接受新請求時某一個失敗了呢?比如說它跟administrator所在的操作節點網絡隔離了呢?這些異常情況都需要處理,否則就可能出現數據不一致。

凡此種種,是不是看起來也很麻煩?假如更新binary,這沒有辦法,必須重啓服務(不要想不是能動態加載動態庫中的函數麼,沒必要非的重啓吧?且不說這樣編程有多麻煩,你需要一個可靠機制去通知配置變更了,而且其本質上等價於重啓)。假如只是更新配置呢?這麼折騰一番是不是看起來很麻煩?有沒有一種方式更輕便一些呢?看下面。

2. 配置中心

對於只想做配置更新而非binary的話,通過藍綠部署是很重的,而且對於超大的集羣來說,是很費時費力的,檢查驗證都是很繁重的任務。不過可以通過引入配置中心,去相對地解決這個問題。比如所有agent都緩存着集羣的配置信息,所有配置都持久化在配置中心,變更也通過配置中心的接口做。所有agent都向配置中心訂閱本集羣的配置變更消息,一旦配置有所變更,向所有的agent發送事件(當然agent也有超時重新獲取機制),所有agent都重新獲取配置,更新完畢。顯而易見,這裏有一致性問題,各個agent不可能“同時”做到配置變更,那麼就有可能出現配置不同導致的線性一致性問題。比較naive的解決方式是讓每一個收到變更事件的agent都wait 1s或更多hang住請求,等待其他agent同步,不過這依然可能會因爲時間不夠或者網絡問題導致不一致。那麼有沒有一種辦法能做到完美呢?既能做到熱更新,又能做到簡單、高可用、線性一致呢?看下面。

3. 帶lease的配置項

上面的配置中心方案具有一致性問題,怎麼解決?我們可以通過lease來解決這個問題。我們假設時鐘漂移是有界(ε)的,這是完全可以假定的且實際可以成立的,比如通過NTP來做,一旦agent檢測到NTP在一個有界時間內沒有更新了,則判定時鐘失效,實例不對外提供服務。基於有界時鐘,我們可以通過改造配置中心,使其具有下發帶有效期(deadline)的配置項的功能即可。各個agent獲取到配置項之後,可認爲在[deadline - ε]之前的時間裏配置項是有效的且集羣一致的。配置中心在更新此種配置項的時候不可以隨意更新,需要等到[max_deadline + ε]之後再更新,這樣就保證了agents配置的一致性。不過這裏存在一個問題,那就是配置中心在等待更新期間可能會有某一個agent A的 deadline較大,在等它失效之前其他agent是無法進行服務的,這樣會導致流量都流向了A甚至不斷重試最終失敗了,進而影響可用性。這裏實現上可以通過配置中心發起主動失效請求的方式來加速配置更新的過程。配置中心收到此種配置項的配置變更請求之後先向所有agent發起失效請求(可以有重試),如果全部成功則立即更新配置,否則等待[max_deadline - ε]之後再更新。另外還可以有一個小優化,那就是配置中心在下放lease的時候不是針對每一個生成一個,而是一批一個,這樣的話在這麼短的時間週期內服務器的晶體振盪器的時鐘漂移也不可能導致配置失效點那麼不一致進而出現更新配置過程中的最後單點問題。

4. 藍綠+lease配置項

我們可以通過藍綠部署和lease配置項的結合,簡化和完善藍綠的更新過程。比如說一般服務是一個開關配置項,另一半是另一個,需要下掉某一半的時候,通過配置中心關閉其開關flag即可。這樣他們一定會在某一個deadline時間之後全部處於offline狀態。進而基於這種機制進行更細膩的下掉的一半的配置更新、binary更新等動作。

不確定結果的確定

分佈式系統的話,stale的請求是再所難免的,總可能會因爲網絡、服務hang或者bug等原因導致的stale的請求出現。分佈式的情況下一般請求的響應有兩種可能,一種是可預期的錯誤(服務裏定義的確定性錯誤),一種是不可預期的錯誤(服務裏定義的未知錯誤、發起端判斷請求超時或者網絡錯誤等)。一般對於分佈式存儲來說,一旦出現不可預期錯誤(known error)的時候,就需要等待,等到某一個時間點,系統狀態穩定了,也就是stale請求結束了,等到這麼一個一致的時間點之後,再繼續操作,就能保證線性一致性了。不過這裏就有一個問題了,怎麼能確定一個一致的時間點?這裏有兩個可行的方法:

  1. 通過一箇中心服務管理器跟蹤和管理請求,整個請求的生命週期都被嚴格跟蹤。每一個請求都需要通過管理器來決議執行。顯而易見,這個實現非常麻煩。
  2. 通過給每一個請求賦予一個可比較的id,保存每一條數據的最後因果請求id,且保證只能更大的id纔可以應用,小的id不可以應用,通過這種機制來消除stale請求的影響。比如系統的設計是冪等的(複雜狀態機可以通過事務就可以保證,簡單狀態機比如kv本身就是冪等的),當出現unknown error時,用戶只需要重試請求即可,一旦成功了,就說明得到了一個確定性結果了,因爲用戶串行的情況下後發起的重試請求的id一定比之前得到unknown error請求id大,一旦成功之前的stale請求必定失敗。而假設之前的請求成功了,由於本請求是冪等的,也不會影響狀態的一致性。

一個基於lease配置項的KV狀態機遷移設計

前提假設

DB-A爲mysql集羣,DB-B爲TiKV集羣。假設當前數據都存儲在DB-A中,現在要遷移到DB-B中。

設計實現

假設不考慮雙寫驗證和容錯(比如複寫MQ防止目標存儲不可靠)。
設計思想就是在某一時刻,把所有業務流量都切到DB-B中,DB-A變爲只讀。這時候所有的舊數據都在DB-A中,新數據在DB-B中。所有的數據獲取都需要在DB-A和DB-B之間做歸併,DB-B的所有刪除請求都必須使用標記刪除。讀取數據的時候先看DB-B中是否有記錄(包括刪除),如果沒有則訪問DB-A,如果有則直接返回。後面的話再通過分佈式鎖或者CAS等機制把DB-A的數據導入到DB-B中。

映射到上面的架構圖,整個過程都是數據層特別是meta agent在參與,業務層完全無感知。那麼問題就是怎麼能讓meta agent在某一時刻路由信息的配置項一致?答案就是通過帶lease的配置項。當然也可以通過上述的藍綠方式,只不過會複雜、麻煩一些。如果對於一致性要求特別高,還需要結合不確定結果的確定這種方式來保證線性一致性。

轉載請附鏈接:https://blog.csdn.net/maxlovezyy/article/details/100199461

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章