單獨的資料庫: 微服務設計的一個關鍵是資料庫設計,基本原則是每個服務都有自己單獨的資料庫,而且只有微服務本身可以訪問這個資料庫。它是基於下麵三個原因。 + 優化服務介面 :微服務之間的介面越小越好,最好只有服務調用介面(RPC或消息),沒有其他介面。如果微服務不能獨享自己的資料庫,那麼資料庫也變成了 ...
單獨的資料庫:
微服務設計的一個關鍵是資料庫設計,基本原則是每個服務都有自己單獨的資料庫,而且只有微服務本身可以訪問這個資料庫。它是基於下麵三個原因。
- 優化服務介面:微服務之間的介面越小越好,最好只有服務調用介面(RPC或消息),沒有其他介面。如果微服務不能獨享自己的資料庫,那麼資料庫也變成了介面的一部分,這大大拓展了介面範圍。
- 錯誤診斷:生產環境中的錯誤大部分都是和資料庫有關的,要麼是數據出了問題,要麼是資料庫的使用方式出了問題。當你不能完全控制資料庫的訪問時,會有各種各樣的錯誤發生。它可能是別的程式直接連到你的資料庫或者是其他部門直接用客戶端訪問資料庫的數據,而這些都是在程式中查不到的,增加了錯誤排查難度。如果是程式中的問題,只要修改了代碼,那麼這個錯誤就不會再有。而上面提到的錯誤,你永遠都沒法預測它們什麼時候還會再次發生。
- 性能調優:性能調優也是一樣,你需要對資料庫有全權控制才能保證它的性能。如果其他部門一定要訪問資料庫,而且只是查詢的話,那麼可以另外創建一份只讀資料庫,讓他們在另一個庫中查詢,這樣才不會影響到你的庫。
理想的設計是你的資料庫只有你的服務能訪問,你也只調用自己資料庫中的數據,所有對別的微服務的訪問都通過服務調用來實現(請參閱“微服務之間調用的最佳設計“)。當然,在實際應用中,單純的服務調用可能不能滿足性能或其他要求,不同的微服務都多少需要共用一些數據。
共用數據:
微服務之間的數據共用可以有下四種方式。
靜態表:
有一些靜態的資料庫表,例如國家,可能會被很多程式用到,而且程式內部需要對國家這個表做連接(join)生成最終用戶展示數據,這樣用微服務調用的方式就效率不高,影響性能。一個辦法是在每個微服務中配置一個這樣的表,它是只讀的,這樣就可以做資料庫連接了。當然你需要保證數據同步。這個方案在多數情況下都是可以接受的,因為以下兩點:
- 靜態的資料庫表結構基本不變:因為一旦表結構變了,你不但要更改所有微服務的資料庫表,還要修改所有微服務的程式。
- 資料庫表中的數據變化不頻繁:這樣數據同步的工作量不大。另外當你同步資料庫時總會有延遲,如果數據變化不頻繁那麼你有很多同步方式可供選擇。
只讀業務數據訪問:
如果你需要讀取別的資料庫里的動態業務數據, 理想的方式是服務調用。如果你只是調用其他微服務做一些計算,一般情況下性能都是可以接受的。如果你需要做數據的連接,那麼你可以用程式代碼來做,而不是用SQL語句。如果測試之後性能不能滿足要求,那你可以考慮在自己的資料庫里建一套只讀數據表。數據同步方式大致有兩種。如果是事件驅動方式,就用發消息的方式進行同步,如果是RPC方式,就用資料庫本身提供的同步方式或者第三方同步軟體。
通常情況下,你可能只需要其他資料庫的幾張表,每張表只需要幾個欄位。這時,其他資料庫是數據的最終來源,控制所有寫操作以及相應的業務驗證邏輯,我們叫它主表。你的只讀庫可以叫從表。 當一條數據寫入主表後,會發一條廣播消息,所有擁有從表的微服務監聽消息並更新只讀表中的數據。但這時你要特別小心,因為它的危險性要比靜態表大得多。第一它的表結構變更會更頻繁,而且它的變更完全不受你控制。第二業務數據不像靜態表,它是經常更新的,這樣對數據同步的要求就比較高。要根據具體的業務需求來決定多大的延遲是可以接受的。
另外它還有兩個問題:
- 數據的容量:資料庫中的數據量是影響性能的主要因素。因為這個數據是外來的,不利於掌握它的流量規律,很難進行容量規劃,也不能更好地進行性能調優。
- ** 介面外泄**:微服務之間的介面本來只有服務調用介面,這時你可以對內部程式和資料庫做任何更改,而不影響其他服務。現在資料庫表結構也變成了介面的一部分。介面一旦發佈之後,基本是不能更改的,這大大限制了你的靈活性。幸運的是因為另外建了一套表,有了一個緩衝,當主表修改時,從表也許不需要同步更新。
除非你能用服務調用(沒有本地只讀資料庫)的方式完成所有功能,不然不管你是用RPC方式還是事件驅動方式進行微服務集成,上面提到的問題都是不可避免的。但是你可以通過合理規劃資料庫更改,來減少上面問題帶來的影響,下麵將會詳細講解。
讀寫業務數據訪問:
這是最複雜的一種情況。一般情況下,你有一個表是主表,而其他表是從表。主表包含主要信息,而且這些主要信息被覆制到從表,但微服務會有額外欄位需要寫入從表。這樣本地微服務對從表就既有讀也有寫的操作。而且主表和從表有一個先後次序的關係。從表的主鍵來源於主表,因此一定先有主表,再有從表。
上圖是例子。假設我們有兩個與電影有關的微服務,一個是電影論壇,用戶可以發表對電影的評論。另一個是電影商店。“movie”是共用表,左邊的一個是電影論壇庫,它的“movie”表是主表。右邊的是電影商店庫,它的“movie”表是從表。它們共用“id”欄位(主鍵)。主表是數據的主要來源,但從表裡的“quantity”和“price”欄位主表裡面沒有。主表插入數據後,發消息,從表接到消息,插入一條數據到本地“movie”表。並且從表還會修改表裡的“quantity”和“price”欄位。在這種情況下,要給每一個欄位分配一個唯一源頭(微服務),只有源頭才有權利主動更改欄位,其他微服務只能被動更改(接收源頭髮出的更改消息之後再改)。在本例子中, “quantity”和“price”欄位的源頭是右邊的表,其他的欄位的源頭都是左邊的表。本例子中“quantity”和“price”只在從表中存在,因此數據寫入是單向的,方向是主表到從表。如果主表也需要這些欄位,那麼它們還要被回寫,那數據寫入就變成雙向的。
直接訪問其它資料庫:
這種方式是要絕對禁止的。生產環境中的許多程式錯誤和性能問題都是由這種方式產生的。上面的三種方式由於是另外新建了本地只讀資料庫表,產生了資料庫的物理隔離,這樣一個資料庫的性能問題不會影響到另一個。另外,當主庫中的表結構更改時,你可以暫時保持從庫中的表不變,這樣程式還可以運行。如果直接訪問別人的庫,主庫一修改,別的微服務程式馬上就會報錯。請參閱ApplicationDatabase。
向後相容的資料庫更新:
從上面的論述可以看出,資料庫表結構的修改是一個影響範圍很廣的事情。在微服務架構中,共用的表在別的服務中也會有一個只讀的拷貝。現在當你要更改表結構時,還需要考慮到對別的微服務的影響。當在單體(Monolithic)架構中,為了保證程式部署能夠回滾,資料庫的更新是向後相容的。需要相容性的另一個原因是支持藍綠發佈(Blue-Green Deployment)。在這種部署方式中,你同時擁有新舊版本的代碼,由負載均衡來決定每一個請求指向那個版本。它們可以共用一個資料庫(這就要求資料庫是向後相容的),也可以使用不同的數據。資料庫的更新簡單來講有以下幾種類型:
- 增加表或欄位:如果欄位可取空值,這個操作是向後相容的。如果是非空值就要插入一個預設值。
- 刪除表或欄位:可先暫時保留被刪除表或欄位,經過幾個版本之後再刪除。
- 修改欄位名:新增加一個欄位,把數據從舊欄位拷貝到新欄位,用資料庫觸發器(或程式)同步舊欄位和新欄位(供過渡時期使用)。 然後再在幾個版本之後把原來的欄位刪除(請參閱Update your Database Schema Without Downtime)。
- 修改表名:如果資料庫支持可更新視圖,最簡單的辦法是先修改表的名字,然後創建一個可更新視圖指向原來的表(請參閱Evolutionary Database Design )。如果資料庫不支持可更新視圖,使用的方法與修改欄位名相似,需要創建新的表並做數據同步。
- 修改欄位類型:與修改欄位名幾乎相同,只是在拷貝數據時,需要做數據類型轉換。
向後相容的資料庫更新的好處是,當程式部署出現問題時,如需進行回滾。只要回滾程式就行了,而不必回滾資料庫。回滾時一般只回滾一個版本。凡是需要刪除的表或欄位在本次部署時都不做修改,等到一個或幾個版本之後,確認沒有問題了再刪除。它的另一個好處就是不會對其他微服務中的共用表產生立刻的直接影響。當本微服務升級後,其他微服務可以評估這些資料庫更新帶來的影響再決定是否需要做相應的程式或資料庫修改。
跨服務事物:
微服務的一個難點是如何實現跨服務的事物支持。兩階段提交(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在他的文章"Break Monolith into Microservices"里有一個很好的建議。那就是,當你把服務從單體程式里拆分時,不要只想著把代碼拆分出來。因為現在的需求可能已經跟原來有所不同,原先的設計可能也不太適用了。而且,技術也已更新,代碼也要作相應的改造。更好的辦法是重寫原來的功能(而不是重寫原來的代碼),把重點放在拆分業務功能上,而不是拆分代碼上,用新的設計和技術來實現這個業務功能。
結論:
資料庫設計是微服務設計的一個關鍵點,基本原則是每個微服務都有自己單獨的資料庫,而且只有微服務本身可以訪問這個資料庫。微服務之間的數據共用可以通過服務調用,或者主、從表的方式實現。在共用數據時,要找到合適的同步方式。在微服務架構中,資料庫的修改影響廣泛,需要保證這種修改是向後相容的。實現跨服務事物的標準方法是Saga。當把單體程式拆分成微服務時,可以分步進行,以減少對現有程式的影響。
索引:
- 微服務之間調用的最佳設計
- ApplicationDatabase
- Update your Database Schema Without Downtime
- Evolutionary Database Design
- Fault-Tolerance and Data Consistency Using Distributed Sagas
- Distributed Sagas: A Protocol for Coordinating Microservices - Caitie McCaffrey - JOTB17
- Managing Data in Microservices
- How to break a Monolith into Microservices
本文由博客一文多發平臺 OpenWrite 發佈!