微服務如今辣麼火,業界流行的對比的卻都是所謂的Monolithic單體應用,而大量的系統在十幾年前都是已是分佈式系統了,那麼微服務做爲新的理念和原來的分佈式系統,或者說SOA(面向服務架構)是什麼區別呢?html
咱們先看 相同點 :前端
須要Registry,實現動態的服務註冊發現機制;程序員
須要考慮分佈式下面的事務一致性,CAP原則下,兩段式提交不能保證性能,事務補償機制須要考慮;sql
同步調用仍是異步消息傳遞,如何保證消息可靠性?SOA由ESB來集成全部的消息;數據庫
都須要統一的Gateway來匯聚、編排接口,實現統一認證機制,對外提供APP使用的RESTful接口;編程
一樣的要關注如何再分佈式下定位系統問題,如何作日誌跟蹤,就像咱們電信領域作了十幾年的信令跟蹤的功能;後端
是持續集成、持續部署?對於CI、CD(持續集成、持續部署),這自己和敏捷、DevOps是交織在一塊兒的,我認爲這更傾向於軟件工程的領域而不是微服務技術自己;設計模式
使用不一樣的通信協議是否是區別?微服務的標杆通信協議是RESTful,而傳統的SOA通常是SOAP,不過目前來講採用輕量級的RPC框架Dubbo、Thrift、gRPC很是多,在Spring Cloud中也有Feign框架將標準RESTful轉爲代碼的API這種仿RPC的行爲,這些通信協議不該該是區分微服務架構和SOA的核心差異;架構
是流行的基於容器框架仍是虛擬機爲主?Docker和虛擬機仍是物理機都是架構實現的一種方式,不是核心區別;併發
服務的切分上有比較大的區別,SOA本來是以一種「集成」技術出現的,不少技術方案是將原有企業內部服務封裝爲一個獨立進程,這樣新的業務開發就可重用這些服務,這些服務極可能是相似供應鏈、CRM這樣的很是大的顆粒;而微服務這個「微」,就說明了他在切分上有講究,不妥協。無數的案例證實,若是你的切分是錯誤的,那麼你得不到微服務承諾的「低耦合、升級不影響、可靠性高」之類的優點,而會比使用Monolithic有更多的麻煩。
不拆分存儲的微服務是僞服務:在實踐中,咱們經常見到一種架構,後端存儲是所有和在一個數據庫中,僅僅把前端的業務邏輯拆分到不一樣的服務進程中,本質上和一個Monolithic同樣,只是把模塊之間的進程內調用改成進程間調用,這種切分不可取,違反了分佈式第一原則,模塊耦合沒有解決,性能卻受到了影響。
分佈式設計第一原則 — 「不要分佈你的對象」
微服務的「Micro」這個詞並非越小越好,而是相對SOA那種粗粒度的服務,咱們須要更小更合適的粒度,這種Micro不是無限制的小。
若是咱們將兩路(同步)通訊與小/微服務結合使用,並根據好比「1個類=1個服務」的原則,那麼咱們實際上回到了使用Corba、J2EE和分佈式對象的20世紀90年代。遺憾的是,新生代的開發人員沒有使用分佈式對象的經驗,所以也就沒有認識到這個主意多麼糟糕,他們正試圖重複歷史,只是此次使用了新技術,好比用HTTP取代了RMI或IIOP。
一個簡單的圖書管理系統確定無需微服務架構。既然採用了微服務架構,那麼面對的問題空間必然是比較宏大,好比整個電商、CRM。
使用什麼樣的方法拆解服務?業界流行1個類=1個服務、1個方法=1個服務、2 Pizza團隊、2周能重寫完成等方法,可是這些都缺少實施基礎。咱們必須從一些軟件設計方法中尋找,面向對象和 設計模式 適用的問題空間是一個模塊,而函數式編程的理念更多的是在代碼層面的微觀上起做用。
Eric Evans 的《領域驅動設計》這本書對微服務架構有很大借鑑意義,這本書提出了一個能將一個大問題空間拆解分爲領域和實體之間的關係和行爲的技術。目前來講,這是一個最合理的解決拆分問題的方案,透過限界上下文(Bounded Context,下文簡稱爲BC)這個概念,咱們能將實現細節封裝起來,讓BC都可以實現SRP(單一職責)原則。而每一個微服務正是BC在實際世界的物理映射,符合BC思路的微服務互相獨立鬆耦合。
微服務架構是一件好事,逼着你們關注設計軟件的合理性,若是原來在Monolithic中領域分析、面向對象設計作很差,換微服務會把這個問題成倍的放大
以電商中的訂單和商品兩個領域舉例,按照DDD拆解,他們應該是兩個獨立的限界上下文,可是訂單中確定是包含商品的,若是貿然拆爲兩個BC,查詢、調用關係就耦合在一塊兒了,甚至有了麻煩的分佈式事務的問題,這個關聯如何拆解?BC理論認爲在不一樣的BC中,即便是一個術語,他的關注點也不同,在商品BC中,關注的是屬性、規格、詳情等等(實際上商品BC這個領域有價格、庫存、促銷等等,把他做爲單獨一個BC也是不合理的,這裏爲了簡化例子,你們先認爲商品BC就是商品基礎信息), 而在訂單BC中更關注商品的庫存、價格。因此在實際編碼設計中,訂單服務每每將關注的商品名稱、價格等等屬性冗餘在訂單中,這個設計解脫了和商品BC的強關聯,兩個BC能夠獨立提供服務,獨立數據存儲
微服務架構首先要關注的不是RPC/ServiceDiscovery/Circuit Breaker這些概念,也不是Eureka/Docker/SpringCloud/Zipkin這些技術框架,而是服務的邊界、職責劃分,劃分錯誤就會陷入大量的服務間的相互調用和分佈式事務中,這種狀況微服務帶來的不是便利而是麻煩。
DDD給咱們帶來了合理的劃分手段,可是DDD的概念衆多,晦澀難以理解,如何抓住重點,合理的運用到微服務架構中呢?
我認爲以下的幾個架構思想是重中之重
充血模型
事件驅動
DDD那麼複雜的理論,聚合根、值對象、事件溯源,到底咱們該怎麼入手呢?
實際上DDD和麪向對象設計、 設計模式 等等理論有千絲萬縷的聯繫,若是不熟悉OOA、OOD,DDD也是使用很差的。不過學習這些OO理論的時候,你們每每感受到無用武之地,由於大部分的Java程序員開發生涯是從學習J2EE經典的分層理論開始的(Action、Service、Dao),在這種分層理論中,咱們基本沒有啥機會使用那些所謂的「行爲型」的設計模式,這裏的核心緣由,就是J2EE經典分層的開發方式是「貧血模型」。
Martin Fowler在他的《企業應用架構模式》這本書中提出了兩種開發方式「事務腳本」和「領域模型」,這兩種開發分別對應了「貧血模型」和「充血模型」。
事務腳本的核心是過程,能夠認爲大部分的業務處理都是一條條的SQL,事務腳本把單個SQL組織成爲一段業務邏輯,在邏輯執行的時候,使用事務來保證邏輯的ACID。最典型的就是存儲過程。固然咱們在平時J2EE經典分層架構中,常常在Service層使用事務腳本。
使用這種開發方式,對象只用於在各層之間傳輸數據用,這裏的對象就是「貧血模型」,只有數據字段和Get/Set方法,沒有邏輯在對象中。
咱們以一個庫存扣減的場景來舉例:
業務場景
首先談一下業務場景,一個下訂單扣減庫存(鎖庫存),這個很簡單
先判斷庫存是否足夠,而後扣減可銷售庫存,增長訂單佔用庫存,而後再記錄一個庫存變更記錄日誌(做爲憑證)
貧血模型的設計
首先設計一個庫存表 Stock,有以下字段
設計一個Stock對象(Getter和Setter省略)
1
2
3
4
5
6
|
public
class
Stock {
private
String spuId;
private
String skuId;
private
int
stockNum;
private
int
orderStockNum;
}
|
Service入口
設計一個StockService,在其中的lock方法中寫邏輯
入參爲(spuId, skuId, num)
實現僞代碼
1
2
3
4
5
6
7
|
count = select stocknum from stock where spuId=xx and skuid=xx
if
count>num {
update stock set stocknum=stocknum-num, orderstocknum=orderstocknum+num where skuId=xx and spuId=xx
}
else
{
//庫存不足,扣減失敗
}
insert stock_log set xx=xx, date=
new
Date()
|
ok,打完收工,若是作的好一些,能夠把update和select count合一,這樣能夠利用一條語句完成自旋,解決併發問題(高手)。
小結一下:
有沒有發現,在這個業務領域很是重要的核心邏輯 — 下訂單扣減庫存中操做過程當中,Stock對象根本不用出現,所有是數據庫操做SQL,所謂的業務邏輯就是由多條SQL構成。Stock只是CRUD的數據對象而已,沒邏輯可言。
馬丁福勒定義的「貧血模型」是反模式,面對簡單的小系統用事務腳本方式開發沒問題,業務邏輯複雜了,業務邏輯、各類狀態散佈在大量的函數中,維護擴展的成本一會兒就上來,貧血模型沒有實施微服務的基礎。
雖然咱們用Java這樣的面嚮對象語言來開發,可是其實和過程型語言是同樣的,因此不少狀況下你們用數據庫的存儲過程來替代Java寫邏輯反而效果會更好,(ps:用了Spring boot也不是微服務),
領域模型是將數據和行爲封裝在一塊兒,並與現實世界的業務對象相映射。各種具有明確的職責劃分,使得邏輯分散到合適對象中。這樣的對象就是「充血模型」 。
在具體實踐中,咱們須要明確一個概念,就是領域模型是有狀態的,他表明一個實際存在的事物。仍是接着上面的例子,咱們設計Stock對象須要表明一種商品的實際庫存,並在這個對象上面加上業務邏輯的方法
這樣作下單鎖庫存業務邏輯的時候,每次必須先從Repository根據主鍵load還原Inventory這個對象,而後執行對應的lock(num)方法改變這個Inventory對象的狀態(屬性也是狀態的一種),而後再經過Repository的save方法把這個對象持久化到存儲去。
完成上述一系列操做的是Application,Application對外提供了這種集成操做的接口
領域模型開發方法最重要的是把扣減形成的狀態變化的細節放到了Inventory對象執行,這就是對業務邏輯的封裝。
Application對象的lock方法能夠和事務腳本方法的StockService的lock來作個對比,StockService是徹底掌握全部細節,一旦有了變化(好比庫存爲0也能夠扣減),Service方法要跟着變;而Application這種方式不須要變化,只要在Inventory對象內部計算就能夠了。代碼放到了合適的地方,計算在合適層次,一切都很合理。這種設計能夠充分利用各類OOD、OOP的理論把業務邏輯實現的很漂亮。
充血模型的缺點
從上面的例子,在Repository的load 到執行業務方法,再到save回去,這是須要耗費必定時間的,可是這個過程當中若是多個線程同時請求對Inventory庫存的鎖定,那就會致使狀態的不一致,麻煩的是針對庫存的併發不只難處理並且很常見。
貧血模型徹底依靠數據庫對併發的支撐,實現能夠簡化不少,但充血模型就得本身實現了,無論是在內存中經過鎖對象,仍是使用Redis的遠程鎖機制,都比貧血模型複雜並且可靠性降低,這是充血模型帶來的挑戰。更好的辦法是能夠經過事件驅動的架構來取消併發。
上面講了領域模型的實現,可是他和微服務是什麼關係呢?在實踐中,這個Inventory是一個限界上下文的聚合根,咱們能夠認爲一個聚合根就是一個微服務進程。
不過問題又來了,一個庫存的Inventory必定和商品信息是有關聯的,僅僅靠Inventory中的冗餘那點商品ID是不夠的,商品的上下架狀態等等都是業務邏輯須要的,那不是又把商品Sku這樣的重型對象引入了這個微服務?兩個重型的對象在一個服務中?這樣的微服務拆不開啊,仍是必須依靠商品庫?!
咱們採用了領域驅動的開發方式,使用了充血模型,享受了他的好處,可是也不得不面對他帶來的弊端。這個弊端在分佈式的微服務架構下面又被放大。
事務一致性的問題在Monolithic下面不是大問題,在微服務下面倒是很致命,咱們回顧一下所謂的ACID原則
Atomicity – 原子性,改變數據狀態要麼是一塊兒完成,要麼一塊兒失敗
Consistency – 一致性,數據的狀態是完整一致的
Isolation – 隔離線,即便有併發事務,互相之間也不影響
Durability – 持久性, 一旦事務提交,不可撤銷
在單體服務和關係型數據庫的時候,咱們很容易經過數據庫的特性去完成ACID。可是一旦你按照DDD拆分聚合根-微服務架構,他們的數據庫就已經分離開了,你就要獨立面對分佈式事務,要在本身的代碼裏面知足ACID。
對於分佈式事務,你們通常會想到之前的JTA標準,2PC兩段式提交。我記得當年在Dubbo羣裏面,基本每週都會有人詢問Dubbo啥時候支撐分佈式事務。實際上根據分佈式系統中CAP原則,當P(分區容忍)發生的時候,強行追求C(一致性),會致使(A)可用性、吞吐量降低,此時咱們通常用最終一致性來保證咱們系統的AP能力。固然不是說放棄C,而是在通常狀況下CAP都能保證,在發生分區的狀況下,咱們能夠經過最終一致性來保證數據一致。
例:
在電商業務的下訂單凍結庫存場景。須要根據庫存狀況肯定訂單是否成交。
假設你已經採用了分佈式系統,這裏訂單模塊和庫存模塊是兩個服務,分別擁有本身的存儲(關係型數據庫),
在一個數據庫的時候,一個事務就能搞定兩張表的修改,可是微服務中,就無法這麼作了。
在DDD理念中,一次事務只能改變一個聚合內部的狀態,若是多個聚合之間須要狀態一致,那麼就要經過最終一致性。訂單和庫存明顯是分屬於兩個不一樣的限界上下文的聚合,這裏須要實現最終一致性,就須要使用事件驅動的架構。
事件驅動架構在領域對象之間經過異步的消息來同步狀態,有些消息也能夠同時發佈給多個服務,在消息引發了一個服務的同步後可能會引發另外消息,事件會擴散開。嚴格意義上的事件驅動是沒有同步調用的。
例子:
在訂單服務新增訂單後,訂單的狀態是「已開啓」,而後發佈一個Order Created事件到消息隊列上
庫存服務在接收到Order Created 事件後,將庫存表格中的某sku減掉可銷售庫存,增長訂單佔用庫存,而後再發送一個Inventory Locked事件給消息隊列
訂單服務接收到Inventory Locked事件,將訂單的狀態改成「已確認」
有人問,若是庫存不足,鎖定不成功怎麼辦? 簡單,庫存服務發送一個Lock Fail事件, 訂單服務接收後,把訂單置爲「已取消」。
好消息,咱們能夠不用鎖 !事件驅動有個很大的優點就是取消了併發,全部請求都是排隊進來,這對咱們實施充血模型有很大幫助,咱們能夠不須要本身來管理內存中的鎖了。取消鎖,隊列處理效率很高,事件驅動能夠用在高併發場景下,好比搶購。
是的,用戶體驗有改變 ,用了這個事件驅動,用戶的體驗有可能會有改變,好比原來同步架構的時候沒有庫存,就立刻告訴你條件不知足沒法下單,不會生成訂單;可是改了事件機制,訂單是當即生成的,極可能過了一會系統通知你訂單被取消掉。 就像搶購「小米手機」同樣,幾十萬人在排隊,排了好久告訴你沒貨了,明天再來吧。若是但願用戶當即獲得結果,能夠在前端想辦法,在BFF(Backend For Frontend)使用CountDownLatch這樣的鎖把後端的異步轉成前端同步,固然這樣BFF消耗比較大。
沒辦法,產品經理不接受 ,產品經理說用戶的體驗必須是沒有庫存就不會生成訂單,這個方案會不斷的生成取消的訂單,他不能接受,怎麼辦?那就在訂單列表查詢的時候,略過這些cancel狀態的訂單吧,也許須要一個額外的視圖來作。我並非一個理想主義者,解決當前的問題是我首先要考慮的,咱們設計微服務的目的是本想是解決業務併發量。而如今面臨的倒是用戶體驗的問題,因此架構設計也是須要妥協的:( 可是至少分析完了,我知道我妥協在什麼地方,爲何妥協,將來還有可能改變。
多個領域多表Join查詢
我我的認爲聚合根這樣的模式對修改狀態是特別合適,可是對搜索數據的確是不方便,好比篩選出一批符合條件的訂單這樣的需求,自己聚合根對象不能承擔批量的查詢任務,由於這不是他的職責。那就必須依賴「領域服務(Domain Service)」這種設施。
當一個方法不便放在實體或者值對象上,使用領域服務即是最佳的解決方法,請確保領域服務是無狀態的。
咱們的查詢任務每每很複雜,好比查詢商品列表,要求按照上個月的銷售額進行排序; 要按照商品的退貨率排序等等。可是在微服務和DDD以後,咱們的存儲模型已經被拆離開,上述的查詢都是要涉及訂單、用戶、商品多個領域的數據。如何搞? 此時咱們要引入一個視圖的概念。好比下面的,查詢用戶名下訂單的操做,直接調用兩個服務本身在內存中join效率無疑是很低的,再加上一些filter條件、分頁,無法作了。因而咱們將事件廣播出去,由一個單獨的視圖服務來接收這些事件,並造成一個物化視圖(materialized view),這些數據已經join過,處理過,放在一個單獨的查詢庫中,等待查詢,這是一個典型的以空間換時間的處理方式。
通過分析,除了簡單的根據主鍵Find或者沒有太多關聯的List查詢,咱們大部分的查詢任務能夠放到單獨的查詢庫中,這個查詢庫能夠是關係數據庫的ReadOnly庫,也能夠是 NoSQL 的數據庫,實際上咱們在項目中使用了ElasticSearch做爲專門的查詢視圖,效果很不錯
除了多領域join的問題,咱們在業務中還會常常碰到一些場景,好比電商中的商品信息是基礎信息,屬於單獨的BC,而其餘BC,無論是營銷服務、價格服務、購物車服務、訂單服務都是須要引用這個商品信息的。可是須要的商品信息只是所有的一小部分而已,營銷服務須要商品的id和名稱、上下架狀態;訂單服務須要商品id、名稱、目錄、價格等等。這比起商品中心定義一個商品(商品id、名稱、規格、規格值、詳情等等)只是一個很小的子集。這說明不一樣的限界上下文的一樣的術語,可是所指的概念不同。 這樣的問題映射到咱們的實現中,每次在訂單、營銷模塊中直接查詢商品模塊,確定是不合適,由於
商品中心須要適配每一個服務須要的數據,提供不一樣的接口
併發量必然很大
服務之間的耦合嚴重,一旦宕機、升級影響的範圍很大。
特別是最後一條,嚴重限制了咱們得到微服務提供的優點「鬆耦合、每一個服務本身能夠頻繁升級不影響其餘模塊」。這就須要咱們經過事件驅動方法,適當冗餘一些數據到不一樣的BC去,把這種耦合拆解開。這種耦合有時候是經過Value Object嵌入到實體中的方式,在生成實體的時候就冗餘,好比訂單在生成的時候就冗餘了商品的信息;有時候是經過額外的Value Object列表方式,營銷中心冗餘一部分相關的商品列表數據,並隨時關注監聽商品的上下級狀態,同步替換掉本限界上下文的商品列表。
下圖一個下單場景分析,在電商系統中,咱們能夠認爲會員和商品是全部業務的基礎數據,他們的變動應該是經過廣播的方式發佈到各個領域,每一個領域保留本身須要的信息。
最終一致性成功依賴不少條件
依賴消息傳遞的可靠性,可能A系統變動了狀態,消息發到B系統的時候丟失了,致使AB的狀態不一致
依賴服務的可靠性,若是A系統變動了本身的狀態,可是還沒來得及發送消息就掛了。也會致使狀態不一致
我記得JavaEE規範中的JMS中有針對這兩種問題的處理要求,一個是JMS經過各類確認消息(Client Acknowledge等)來保證消息的投遞可靠性,另外是JMS的消息投遞操做能夠加入到數據庫的事務中-即沒有發送消息,會引發數據庫的回滾(沒有查資料,不是很準確的描述,請專家指正)。不過如今符合JMS規範的MQ沒幾個,特別是保一致性須要下降性能,如今標榜高吞吐量的MQ都把問題拋給了咱們本身的應用解決。因此這裏介紹幾個常見的方法,來提高最終一致性的效果。
仍是以上面的訂單扣取信用的例子
訂單服務開啓本地事務,首先新增訂單;
而後將Order Created事件插入一張專門Event表,事務提交;
有一個單獨的定時任務線程,按期掃描Event表,掃出來須要發送的就丟到MQ,同時把Event設置爲「已發送」。
方案的優點是使用了本地數據庫的事務,若是Event沒有插入成功,那麼訂單也不會被建立;線程掃描後把event置爲已發送,也確保了消息不會被漏發(咱們的目標是寧肯重發,也不要漏發,由於Event處理會被設計爲冪等)。
缺點是須要單獨處理Event發佈在業務邏輯中,繁瑣容易忘記;Event發送有些滯後;定時掃描性能消耗大,並且會產生數據庫高水位隱患;
咱們稍做改進,使用數據庫特有的MySQL Binlog跟蹤(阿里的Canal)或者Oracle的GoldenGate技術能夠得到數據庫的Event表的變動通知,這樣就能夠避免經過定時任務來掃描了
不過用了這些數據庫日誌的工具,會和具體的數據庫實現(甚至是特定的版本)綁定,決策的時候請慎重。
事件溯源對咱們來講是一個特別的思路,他並不持久化Entity對象,而是隻把初始狀態和每次變動的Event記錄下來,並在內存中根據Event還原Entity對象的最新狀態,具體實現很相似數據庫的Redolog的實現,只是他把這種機制放到了應用層來。
雖然事件溯源有不少宣稱的優點,引入這種技術要特別當心,首先他不必定適合大部分的業務場景,一旦變動不少的狀況下,效率的確是個大問題;另一些查詢的問題也是困擾。
咱們僅僅在個別的業務上探索性的使用Event Souring和AxonFramework,因爲實現起來比較複雜,具體的狀況還須要等到實踐一段時間後再來總結,也許須要額外的一篇文章來詳細描述
以上是對事件驅動在微服務架構中一些個人理解。