微服務設計關鍵的難點:微服務架構的數據庫是如何設計的?

單獨的數據庫:

微服務設計的一個關鍵是數據庫設計,基本原則是每一個服務都有本身單獨的數據庫,並且只有微服務自己能夠訪問這個數據庫。它是基於下面三個緣由。數據庫

  • 優化服務接口:微服務之間的接口越小越好,最好只有服務調用接口(RPC或消息),沒有其餘接口。若是微服務不能獨享本身的數據庫,那麼數據庫也變成了接口的一部分,這大大拓展了接口範圍。
  • 錯誤診斷:生產環境中的錯誤大部分都是和數據庫有關的,要麼是數據出了問題,要麼是數據庫的使用方式出了問題。當你不能徹底控制數據庫的訪問時,會有各類各樣的錯誤發生。它多是別的程序直接連到你的數據庫或者是其餘部門直接用客戶端訪問數據庫的數據,而這些都是在程序中查不到的,增長了錯誤排查難度。若是是程序中的問題,只要修改了代碼,那麼這個錯誤就不會再有。而上面提到的錯誤,你永遠都無法預測它們何時還會再次發生。
  • 性能調優:性能調優也是同樣,你須要對數據庫有全權控制才能保證它的性能。若是其餘部門必定要訪問數據庫,並且只是查詢的話,那麼能夠另外建立一份只讀數據庫,讓他們在另外一個庫中查詢,這樣纔不會影響到你的庫。

理想的設計是你的數據庫只有你的服務能訪問,你也只調用本身數據庫中的數據,全部對別的微服務的訪問都經過服務調用來實現。固然,在實際應用中,單純的服務調用可能不能知足性能或其餘要求,不一樣的微服務都多少須要共享一些數據。網絡

共享數據:

微服務之間的數據共享能夠有下四種方式。架構

靜態表:

有一些靜態的數據庫表,例如國家,可能會被不少程序用到,並且程序內部須要對國家這個表作鏈接(join)生成最終用戶展現數據,這樣用微服務調用的方式就效率不高,影響性能。一個辦法是在每一個微服務中配置一個這樣的表,它是隻讀的,這樣就能夠作數據庫鏈接了。固然你須要保證數據同步。這個方案在多數狀況下都是能夠接受的,由於如下兩點:app

  1. 靜態的數據庫表結構基本不變:由於一旦表結構變了,你不但要更改全部微服務的數據庫表,還要修改全部微服務的程序。
  2. 數據庫表中的數據變化不頻繁:這樣數據同步的工做量不大。另外當你同步數據庫時總會有延遲,若是數據變化不頻繁那麼你有不少同步方式可供選擇。

只讀業務數據訪問:

若是你須要讀取別的數據庫裏的動態業務數據, 理想的方式是服務調用。若是你只是調用其餘微服務作一些計算,通常狀況下性能都是能夠接受的。若是你須要作數據的鏈接,那麼你能夠用程序代碼來作,而不是用SQL語句。若是測試以後性能不能知足要求,那你能夠考慮在本身的數據庫裏建一套只讀數據表。數據同步方式大體有兩種。若是是事件驅動方式,就用發消息的方式進行同步,若是是RPC方式,就用數據庫自己提供的同步方式或者第三方同步軟件。負載均衡

一般狀況下,你可能只須要其餘數據庫的幾張表,每張表只須要幾個字段。這時,其餘數據庫是數據的最終來源,控制全部寫操做以及相應的業務驗證邏輯,咱們叫它主表。你的只讀庫能夠叫從表。 當一條數據寫入主表後,會發一條廣播消息,全部擁有從表的微服務監聽消息並更新只讀表中的數據。但這時你要特別當心,由於它的危險性要比靜態表大得多。第一它的表結構變動會更頻繁,並且它的變動徹底不受你控制。第二業務數據不像靜態表,它是常常更新的,這樣對數據同步的要求就比較高。要根據具體的業務需求來決定多大的延遲是能夠接受的。數據庫設計

另外它還有兩個問題:ide

  1. 數據的容量:數據庫中的數據量是影響性能的主要因素。由於這個數據是外來的,不利於掌握它的流量規律,很難進行容量規劃,也不能更好地進行性能調優。
  2. 接口外泄:微服務之間的接口原本只有服務調用接口,這時你能夠對內部程序和數據庫作任何更改,而不影響其餘服務。如今數據庫表結構也變成了接口的一部分。接口一旦發佈以後,基本是不能更改的,這大大限制了你的靈活性。幸運的是由於另外建了一套表,有了一個緩衝,當主表修改時,從表也許不須要同步更新。

除非你能用服務調用(沒有本地只讀數據庫)的方式完成全部功能,否則無論你是用RPC方式仍是事件驅動方式進行微服務集成,上面提到的問題都是不可避免的。可是你能夠經過合理規劃數據庫更改,來減小上面問題帶來的影響,下面將會詳細講解。微服務

讀寫業務數據訪問:

這是最複雜的一種狀況。通常狀況下,你有一個表是主表,而其餘表是從表。主表包含主要信息,並且這些主要信息被複制到從表,但微服務會有額外字段須要寫入從表。這樣本地微服務對從表就既有讀也有寫的操做。並且主表和從表有一個前後次序的關係。從表的主鍵來源於主表,所以必定先有主表,再有從表。性能

微服務設計關鍵的難點:微服務架構的數據庫是如何設計的?

上圖是例子。假設咱們有兩個與電影有關的微服務,一個是電影論壇,用戶能夠發表對電影的評論。另外一個是電影商店。「movie」是共享表,左邊的一個是電影論壇庫,它的「movie」表是主表。右邊的是電影商店庫,它的「movie」表是從表。它們共享「id」字段(主鍵)。主表是數據的主要來源,但從表裏的「quantity」和「price」字段主表裏面沒有。主表插入數據後,發消息,從表接到消息,插入一條數據到本地「movie」表。而且從表還會修改表裏的「quantity」和「price」字段。在這種狀況下,要給每個字段分配一個惟一源頭(微服務),只有源頭纔有權利主動更改字段,其餘微服務只能被動更改(接收源頭髮出的更改消息以後再改)。在本例子中, 「quantity」和「price」字段的源頭是右邊的表,其餘的字段的源頭都是左邊的表。本例子中「quantity」和「price」只在從表中存在,所以數據寫入是單向的,方向是主表到從表。若是主表也須要這些字段,那麼它們還要被回寫,那數據寫入就變成雙向的。測試

直接訪問其它數據庫:

這種方式是要絕對禁止的。生產環境中的許多程序錯誤和性能問題都是由這種方式產生的。上面的三種方式因爲是另外新建了本地只讀數據庫表,產生了數據庫的物理隔離,這樣一個數據庫的性能問題不會影響到另外一個。另外,當主庫中的表結構更改時,你能夠暫時保持從庫中的表不變,這樣程序還能夠運行。若是直接訪問別人的庫,主庫一修改,別的微服務程序立刻就會報錯。

向後兼容的數據庫更新:

從上面的論述能夠看出,數據庫表結構的修改是一個影響範圍很廣的事情。在微服務架構中,共享的表在別的服務中也會有一個只讀的拷貝。如今當你要更改表結構時,還須要考慮到對別的微服務的影響。當在單體(Monolithic)架構中,爲了保證程序部署可以回滾,數據庫的更新是向後兼容的。須要兼容性的另外一個緣由是支持藍綠髮布(Blue-Green Deployment)。在這種部署方式中,你同時擁有新舊版本的代碼,由負載均衡來決定每個請求指向那個版本。它們能夠共享一個數據庫(這就要求數據庫是向後兼容的),也可使用不一樣的數據。數據庫的更新簡單來說有如下幾種類型:

  • 增長表或字段:若是字段可取空值,這個操做是向後兼容的。若是是非空值就要插入一個缺省值。
  • 刪除表或字段:可先暫時保留被刪除表或字段,通過幾個版本以後再刪除。
  • 修改字段名:新增長一個字段,把數據從舊字段拷貝到新字段,用數據庫觸發器(或程序)同步舊字段和新字段(供過渡時期使用)。 而後再在幾個版本以後把原來的字段刪除。
  • 修改表名:若是數據庫支持可更新視圖,最簡單的辦法是先修改表的名字,而後建立一個可更新視圖指向原來的表。若是數據庫不支持可更新視圖,使用的方法與修改字段名類似,須要建立新的表並作數據同步。
  • 修改字段類型:與修改字段名幾乎相同,只是在拷貝數據時,須要作數據類型轉換。

向後兼容的數據庫更新的好處是,當程序部署出現問題時,如需進行回滾。只要回滾程序就好了,而沒必要回滾數據庫。回滾時通常只回滾一個版本。凡是須要刪除的表或字段在本次部署時都不作修改,等到一個或幾個版本以後,確認沒有問題了再刪除。它的另外一個好處就是不會對其餘微服務中的共享表產生馬上的直接影響。當本微服務升級後,其餘微服務能夠評估這些數據庫更新帶來的影響再決定是否須要作相應的程序或數據庫修改。

跨服務事物:

微服務的一個難點是如何實現跨服務的事物支持。兩階段提交(Two-Phase Commit)已被證實性能上不能知足需求,如今基本上沒有人用。被一致承認的方法叫Saga。它的原理是爲事物中的每一個操做寫一個補償操做(Compensating Transaction),而後在回滾階段挨個執行每個補償操做。示例以下圖,在一個事物中共有3個操做T1,T2,T3。每個操做要定義一個補償操做,C1,C2,C3。事物執行時是按照正向順序先執行T1,當回滾時是按照反向順序先執行C3。 事物中的每個操做(正向操做和補償操做)都被包裝成一個命令(Command),Saga執行協調器(Saga Execution Coordinator (SEC))負責執行全部命令。在執行以前,全部的命令都會按順序被存入日誌中,而後Saga執行協調器從日誌中取出命令,依次執行。當某個執行出現錯誤時,這個錯誤也被寫入日誌,而且全部正在執行的命令被中止,開始回滾操做。

微服務設計關鍵的難點:微服務架構的數據庫是如何設計的?

Saga放鬆了對一致性(Consistency)的要求,它能保證的是最終一致性(Eventual Consistency),所以在事物執行過程當中數據是不一致的,而且這種不一致會被別的進程看到。在生活中,大多數狀況下,咱們對一致性的要求並無那麼高,短暫的不一致性是能夠接收的。例如銀行的轉帳操做,它們在執行過程當中都不是在一個數據庫事物裏執行的,而是用記帳的方式分紅兩個動做來執行,保證的也是最終一致性。

Saga的原理看起來很簡單,但要想正確的實施仍是有必定難度的。它的核心問題在於對錯誤的處理,要把它徹底講明白鬚要另寫一遍文章,我如今只講一下要點。網絡環境是不可靠的,正在執行的命令可能很長時間都沒有返回結果,這時,第一,你要設定一個超時。第二,由於你不知道沒有返回值的緣由是,已經完成了命令但網絡出了問題,仍是沒完成就犧牲了,所以不知道是否要執行補償操做。這時正確的作法是重試原命令,直到獲得完成確認,而後再執行補償操做。但這對命令有一個要求,那就是這個操做必須是冪等的(Idempotent),也就是說它能夠執行屢次,但最終結果仍是同樣的。

另外,有些操做的補償操做比較容易生成,例如付款操做,你只要把錢款退回就能夠了。但有些操做,像發郵件,完成以後就沒有辦法回到以前的狀態了,這時就只能再發一個郵件更正之前的信息。所以補償操做不必定非要返回到原來的狀態,而是抵消掉原來操做產生的效果。

微服務的拆分:

咱們原來的程序大多數都是單體程序,但如今要把它拆分紅微服務,應該怎樣作才能下降對現有應用的影響呢?

微服務設計關鍵的難點:微服務架構的數據庫是如何設計的?

咱們用上面的圖來作例子。它共有兩個程序,一個是「Styling app」,另外一個是「Warehouse app」,它們共享圖中下面的數據庫,庫裏有三張表,「core client」,「core sku」,「core item」。

微服務設計關鍵的難點:微服務架構的數據庫是如何設計的?

假設咱們要拆分出來一個微服務叫「client-service」,它須要訪問「core client」表。第一步,咱們先把程序從原來的代碼裏拆分出來,變成一個服務. 數據庫不動,這個服務仍然指向原來的數據庫。其餘程序再也不直接訪問這個服務管理的表,而是經過服務調用或另建共享表來獲取數據。

微服務設計關鍵的難點:微服務架構的數據庫是如何設計的?

第二步,再把服務的數據庫表拆分出來,這時微服務就擁有它本身的數據庫了,而再也不須要原來的共享數據庫了。這時就成了一個真正意義上的的微服務。

上面只講了拆分一個微服務,若是有多個須要拆分,則需一個一個按照上面講的方法依次進行。

另外,Martin Fowler有一個很好的建議。那就是,當你把服務從單體程序裏拆分時,不要只想着把代碼拆分出來。由於如今的需求可能已經跟原來有所不一樣,原先的設計可能也不太適用了。並且,技術也已更新,代碼也要做相應的改造。更好的辦法是重寫原來的功能(而不是重寫原來的代碼),把重點放在拆分業務功能上,而不是拆分代碼上,用新的設計和技術來實現這個業務功能。

結論:

數據庫設計是微服務設計的一個關鍵點,基本原則是每一個微服務都有本身單獨的數據庫,並且只有微服務自己能夠訪問這個數據庫。微服務之間的數據共享能夠經過服務調用,或者主、從表的方式實現。在共享數據時,要找到合適的同步方式。在微服務架構中,數據庫的修改影響普遍,須要保證這種修改是向後兼容的。實現跨服務事物的標準方法是Saga。當把單體程序拆分紅微服務時,能夠分步進行,以減小對現有程序的影響。

相關文章
相關標籤/搜索