聊聊微服務(wù)物理設(shè)計

前言

這些年颠印,微服務(wù)架構(gòu)大行其道纲岭,我們每天或多或少的都在開發(fā)微服務(wù)抹竹。有一個問題,或許會時不時的困擾著你止潮,那就是怎樣設(shè)計微服務(wù)代碼的目錄結(jié)構(gòu)窃判,也就是如何分層分包,筆者更習(xí)慣叫物理設(shè)計喇闸。

關(guān)于微服務(wù)物理設(shè)計袄琳,沒有標(biāo)準(zhǔn)答案,在團隊內(nèi)能達成一致即可燃乍。然而唆樊,如果能遵從某種標(biāo)準(zhǔn),那么會讓團隊小伙伴們更信服刻蟹。筆者經(jīng)過多年探索逗旁,發(fā)現(xiàn)基于六邊形架構(gòu)的物理設(shè)計,較容易溝通和落地舆瘪,是一種值得推廣的標(biāo)準(zhǔn)化實踐片效。

六邊形架構(gòu)

六邊形架構(gòu)是 Alistair Cockburn 在 2005 年提出的,在這種架構(gòu)中英古,不同的客戶通過“平等”的方式與系統(tǒng)交互淀衣。需要新的客戶嗎?不是問題召调。只需要添加一個新的適配器將客戶輸入轉(zhuǎn)化成能被系統(tǒng) API 所理解的參數(shù)就行膨桥。同時蛮浑,對于每種特定的輸出,都有一個新建的適配器負責(zé)完成相應(yīng)的轉(zhuǎn)化功能只嚣。

六邊形架構(gòu)也稱為端口與適配器陵吸,如下圖所示:


ddd-hex

六邊形每條不同的邊代表了不同類型的端口,端口要么處理輸入介牙,要么處理輸出壮虫。對于每種外界類型,都有一個適配器與之對應(yīng)环础,外界通過應(yīng)用層 API 與內(nèi)部進行交互囚似。上圖中有 3 個客戶請求均抵達相同的輸入端口(適配器 A、B 和 C)线得,另一個客戶請求使用了適配器 D 饶唤。假設(shè)前 3 個請求使用了 HTTP 協(xié)議(瀏覽器、REST 和 SOAP 等)贯钩,而后一個請求使用了 AMQP 協(xié)議(比如 RabbitMQ)募狂。端口并沒有明確的定義,它是一個非常靈活的概念角雷。無論采用哪種方式對端口進行劃分祸穷,當(dāng)客戶請求到達時,都應(yīng)該有相應(yīng)的適配器對輸入進行轉(zhuǎn)化勺三,然后端口將調(diào)用應(yīng)用程序的某個操作或者向應(yīng)用程序發(fā)送一個事件雷滚,控制權(quán)由此交給內(nèi)部區(qū)域。

應(yīng)用程序通過公共 API 接收客戶請求吗坚,使用領(lǐng)域模型來處理請求祈远。我們可以將倉儲的實現(xiàn)看作是持久化適配器,該適配器用于訪問先前存儲的聚合實例或者保存新的聚合實例商源。正如圖中的適配器 E车份、F 和 G 所展示的,我們可以通過不同的方式實現(xiàn)資源庫牡彻,比如關(guān)系型數(shù)據(jù)庫扫沼、基于文檔的存儲、分布式緩存或內(nèi)存存儲等讨便。如果應(yīng)用程序向外界發(fā)送領(lǐng)域事件消息充甚,我們將使用適配器 H 進行處理。該適配器處理消息輸出霸褒,而上面提到的處理 AMQP 消息的適配器則是處理消息輸入的伴找,因此應(yīng)該使用不同的端口。

用戶界面層去哪里了废菱?

Eric Evans 在《領(lǐng)域驅(qū)動設(shè)計-軟件核心復(fù)雜性應(yīng)對之道》這本書中提出了經(jīng)典的四層架構(gòu)技矮,如下圖所示:


ddd-l4.png

上圖中抖誉,應(yīng)用層和領(lǐng)域?qū)釉诹呅渭軜?gòu)中也有,基礎(chǔ)設(shè)施層(Infrastructure)對應(yīng)六邊形架構(gòu)中的適配層衰倦,但用戶界面層(User Interface)在六邊形架構(gòu)中卻找不到對應(yīng)項袒炉。

你禁不住想問:用戶界面層去哪里了?

其實樊零,自從前后端架構(gòu)分離后我磁,用戶界面層就被徹底的拆分出去了,形成一個完全松耦合的前端層驻襟。應(yīng)用層的調(diào)用者不再僅限于前端層夺艰,還可以是其他微服務(wù)。即使是前端層沉衣,也可能需要不同的用戶交互方式與呈現(xiàn)界面郁副,例如 Web 和移動端 App。

基礎(chǔ)的物理設(shè)計

遵從六邊形架構(gòu)豌习,微服務(wù)物理設(shè)計的第一級為 adapter存谎、app 和 domain。

這個很好理解:

  • ms 是微服務(wù)肥隆,對應(yīng)整個六邊形既荚;
  • adapter 是適配器層,對應(yīng)六邊形最外層巷屿;
  • app 是應(yīng)用層固以,對應(yīng)六邊形中間層;
  • domain 是領(lǐng)域?qū)又鼋恚瑢?yīng)六邊形最內(nèi)層。

在六邊形架構(gòu)中诫钓,越是內(nèi)層就越穩(wěn)定旬昭,越是外層相對就越容易變化。根據(jù)軟件架構(gòu)中的一個重要原則:代碼中不穩(wěn)定的部分菌湃,應(yīng)該依賴穩(wěn)定的部分问拘。就是說,外層依賴內(nèi)層惧所,但內(nèi)層不能依賴外層骤坐。

然而,還有一些代碼我們沒有考慮到下愈,比如日志纽绍,還有用于字符串和日期處理的工具類。這些代碼可能會被適配器層势似、應(yīng)用層和領(lǐng)域?qū)拥娜我粚诱{(diào)用拌夏,理應(yīng)放在最內(nèi)層僧著,但 DDD 強調(diào)領(lǐng)域?qū)邮呛诵模仨毼挥谧顑?nèi)層障簿,且外層要依賴內(nèi)層盹愚,這是否存在矛盾?事實上站故,這些代碼和六邊形中的這幾層代碼根本不在同一個維度皆怕,是對這幾層代碼起公共的支撐作用的,通常叫做公共層( common )西篓。

我們給出基礎(chǔ)的物理設(shè)計全景:


basic-physical-design.png

接下來端逼,我們具體聊聊每一層的物理設(shè)計。

適配器層物理設(shè)計

適配器會把和具體技術(shù)有關(guān)的請求污淋,翻譯成和技術(shù)無關(guān)的請求顶滩,再調(diào)用應(yīng)用層來實現(xiàn)業(yè)務(wù)功能;在接收到應(yīng)用層的返回值以后寸爆,又轉(zhuǎn)化成技術(shù)相關(guān)的響應(yīng)礁鲁,返回給外界。也就是說適配器層屏蔽了輸入輸出技術(shù)的差異赁豆,從而使應(yīng)用層與具體技術(shù)無關(guān)仅醇,這樣就達到了分離關(guān)注點的目的。

適配器分為入口適配器(inbound adapter魔种,又稱 driving adapter)和出口適配器(outbound adapter析二,又稱 driven adapter)。

入口適配器處理的是外界向系統(tǒng)的調(diào)用节预,常見的外部調(diào)用包括 REST 請求叶摄,RPC 請求 和 MQ 請求。REST 請求的處理包括路由設(shè)置和控制器對象執(zhí)行兩部分安拟,所以在 rest 目錄下蛤吓,又增加了 controller 和 router 兩個子目錄(也可以直接通過文件來隔離)。在微服務(wù)中糠赦,REST 消息最為常見会傲,但 RPC 消息和 MQ 消息也時而出現(xiàn),當(dāng)遇到時遵從與 REST 同樣的物理設(shè)計風(fēng)格即可拙泽。

出口適配器處理的是系統(tǒng)向外界的調(diào)用淌山,可以分為對其他微服務(wù)的調(diào)用和對持久化資源的調(diào)用兩部分,對應(yīng)的目錄分別為 gateway 和 persistence顾瞻。一般對緩存泼疑、文件系統(tǒng)和對象存儲服務(wù)等的訪問,都會算作對持久化資源的訪問朋其,我們將這些資源統(tǒng)稱為數(shù)據(jù)庫王浴,所以要在 persistence 目錄(包)下封裝訪問數(shù)據(jù)庫資源的客戶端(client)脆炎,并且還要實現(xiàn)在數(shù)據(jù)庫中持久化的領(lǐng)域?qū)ο蟮膫}儲(repository,簡稱 repo)接口氓辣。同理秒裕,在 gateway 目錄下,我們需要封裝訪問其他微服務(wù)的客戶端钞啸,并且還要實現(xiàn)從其他微服務(wù)獲取的領(lǐng)域?qū)ο蟮膫}儲接口几蜻。

可以看出,倉儲接口的實現(xiàn)在 gateway 和 persistence 目錄下都有体斩,這恰巧說明了我們在實現(xiàn)層面考慮了不同的技術(shù)梭稚。數(shù)據(jù)庫和其他微服務(wù)屬于外部資源,都可以用于領(lǐng)域?qū)ο蟮某志没醭场H绻L問數(shù)據(jù)庫弧烤,就叫 persistence;如果訪問其他微服務(wù)蹬敲,就叫 gateway暇昂。

綜上,我們整體看一下適配層的物理設(shè)計:


adapter-physical-design.png

應(yīng)用層物理設(shè)計

應(yīng)用層作為領(lǐng)域?qū)拥摹伴T面”伴嗡,接受來自客戶端的請求急波,本身并不包含領(lǐng)域邏輯,而是對領(lǐng)域?qū)又械倪壿嬤M行封裝和編排瘪校。領(lǐng)域?qū)臃庋b的邏輯通常是細粒度的澄暮,并不適合直接作為 API 暴露給外部。應(yīng)用層將領(lǐng)域?qū)拥奶幚斫Y(jié)果封裝為更簡單的粗粒度對象阱扬,作為對外 API 的參數(shù)泣懊。這里說的粗粒度對象一般是 DTO(Data Transfer Object),也就是沒有邏輯的數(shù)據(jù)傳輸對象价认,應(yīng)用層負責(zé) DTO 和領(lǐng)域?qū)ο蟮臄?shù)據(jù)轉(zhuǎn)換嗅定。需要強調(diào)的是,這里的 DTO 僅僅是應(yīng)用層的 DTO用踩,是和技術(shù)無關(guān)的,而適配層的 DTO 是和技術(shù)有關(guān)的忙迁。適配層收到消息后脐彩,經(jīng)過反序列化得到適配層 DTO,然后將適配層 DTO 轉(zhuǎn)化成應(yīng)用層 DTO姊扔,最后再調(diào)用應(yīng)用服務(wù)完成業(yè)務(wù)邏輯惠奸。另外,還有一些不屬于領(lǐng)域?qū)拥臋M切關(guān)注點恰梢,比如像事務(wù)佛南、日志和權(quán)限等梗掰,也應(yīng)該放在應(yīng)用層處理。

應(yīng)用層主要包括應(yīng)用服務(wù)和 DTO 對象嗅回,其中應(yīng)用服務(wù)對應(yīng)用例(Use Case)或用戶故事(User Story)及穗,還會處理一些橫切關(guān)注點。

綜上绵载,我們整體看一下應(yīng)用層的物理設(shè)計:


app-physical-design.png

領(lǐng)域?qū)游锢碓O(shè)計

領(lǐng)域?qū)幼詈诵牡氖悄P凸÷剑ǎ?/p>

  • 系統(tǒng)中有哪些領(lǐng)域?qū)ο螅▽嶓w和值對象)
  • 領(lǐng)域?qū)ο笾g的關(guān)系是什么
  • 領(lǐng)域?qū)ο蟮纳芷诠芾恚河镁酆蟻矸庋b,用工廠來創(chuàng)建和銷毀娃豹,用倉儲來查找和持久化

模型通過 model 目錄呈現(xiàn)焚虱,model 目錄下是領(lǐng)域?qū)ο蟮姆纸M,也用領(lǐng)域?qū)ο竺麃肀磉_懂版,這里有三種情況:

  • 分組是聚合鹃栽,聚合下有聚合根(實體)、其他實體躯畴、值對象民鼓、工廠和倉儲,分組名就是聚合根的名字私股;
  • 分組是聚合摹察,聚合下只有聚合根一個實體,其他同上倡鲸;
  • 分組是值對象供嚎,很少見但也存在,分組名就是值對象的名字峭状。分組下除過值對象克滴,還有倉儲,倉儲的作用不是持久化优床,而是從另一個微服務(wù)獲取到該值對象劝赔,并且還可以在倉儲中封裝緩存該值對象的算法。

因為在六邊形架構(gòu)中胆敞,內(nèi)層不能依賴外層着帽,所以倉儲在領(lǐng)域?qū)有枰x為抽象,然后在適配器層實現(xiàn)這個抽象移层。

領(lǐng)域?qū)映^模型仍翰,剩下的主要就是領(lǐng)域服務(wù)和領(lǐng)域事件了,它們?nèi)齻€是并列的目錄观话。

DDD 強調(diào)模型和代碼的一致性予借,因此我們需要基于面向模型的實現(xiàn)模式來準(zhǔn)確的表達領(lǐng)域模型,讓模型和代碼一一映射×槠龋考慮到這一點秦叛,我們在領(lǐng)域?qū)釉黾恿?base 目錄束倍,主要定義戰(zhàn)術(shù)設(shè)計元素的原語慰枕,比如聚合根,實體亥宿,值對象等利凑。

以 C++ 語言為例浆劲,我們看幾個原語的定義:

struct AggregateRoot : Entity
{
    AggregateRoot(int id) : Entity(id) {}
};

struct Entity
{
    Entity(int id);

    bool operator==(const Entity* rhs);
    bool operator!=(const Entity* rhs);
    int getId() const;

private:
    int id;
};

struct ValueObject
{
    virtual ~ValueObject() = default;

    virtual bool operator==(ValueObject* rhs) = 0;
    virtual bool operator!=(ValueObject* rhs) = 0;
};

綜上,我們整體看一下領(lǐng)域?qū)拥奈锢碓O(shè)計:


domain-physical-design.png

補充:在 model 目錄下還可能有 common 包哀澈,但比較少見牌借,所以在領(lǐng)域?qū)游锢碓O(shè)計視圖中沒有體現(xiàn)

  • 有的值對象是獨立存在的,不依附于任何實體割按,比如時間段對象 Period膨报,可以用來描述任何實體的屬性,應(yīng)該放在 common 包适荣;
  • 當(dāng)使用 DCI (Data现柠、Context 和 Interactive)架構(gòu)模式時,Data 放在領(lǐng)域?qū)ο蟀诿珻ontext 放在領(lǐng)域服務(wù)包够吩,Role 一般放在領(lǐng)域?qū)ο蟀?dāng)某個 Role 被多個領(lǐng)域?qū)ο髲?fù)用時丈氓,比如 Worker 作為一個 Role周循,被工人和機器人兩個領(lǐng)域?qū)ο髲?fù)用,應(yīng)該放在 common 包万俗。

公共層物理設(shè)計

從物理設(shè)計上看湾笛,適配器層、應(yīng)用層闰歪、領(lǐng)域?qū)雍凸矊佣嘉挥谕患壓垦校m配器層、應(yīng)用層和領(lǐng)域?qū)訉儆谕粋€平面库倘,而公共層屬于另一個平面临扮,且對前一個平面進行支撐。

公共層我們一般放日志包(log)和工具包(util)教翩。

綜上公条,我們整體看一下公共層的物理設(shè)計:


common-physical-design.png

擴展的物理設(shè)計

當(dāng)模型變得比較復(fù)雜時,我們就需要擴展一下之前的物理設(shè)計了迂曲,一般有兩種方式:多模塊和多限界上下文。

多模塊

隨著業(yè)務(wù)的長期發(fā)展寥袭,領(lǐng)域模型中的概念越來越多路捧,一些領(lǐng)域?qū)ο蠹捌潢P(guān)系混雜在了一起关霸,超過了一般人的認知負載,這時就需要將這些業(yè)務(wù)概念分組杰扫,每一組是一個高內(nèi)聚的模塊队寇,而模塊間盡量低耦合。

微服務(wù)有了多個模塊后章姓,其物理設(shè)計的各層中會增加模塊的目錄佳遣,如下所示:


module-physical-design.png

說明:上圖中適配層沒有展開,同樣可以添加模塊目錄進行分組凡伊。

多限界上下文

限界上下文的拆分因素零渐,既考慮到了業(yè)務(wù)概念的完整性和一致性,又考慮到了團隊的認知負載和可復(fù)用性(共享內(nèi)核)系忙。我們可以說诵盼,微服務(wù)拆分的基礎(chǔ)是限界上下文,接著再根據(jù)性能银还、安全和可用性等非功能性需求繼續(xù)拆分风宁。

但有時候,可以考慮將幾個限界上下文放到一個微服務(wù)里蛹疯。極端情況下戒财,可以將所有限界上下文都放在一個服務(wù)里,這就變成了單體架構(gòu)捺弦。

在一些場景下饮寞,采用單體架構(gòu)比較適合,盡管微服務(wù)架構(gòu)現(xiàn)在比較流行羹呵。在虛機或裸機骂际,以容器化方式部署的服務(wù),如果已經(jīng)可以滿足用戶需求了冈欢,那么上云只會帶來更大的復(fù)雜度和成本歉铝,不劃算。筆者比較信奉單體優(yōu)先的架構(gòu)策略凑耻,除非收益大于成本太示,才會考慮逐步演進到微服務(wù)架構(gòu)。

對于單體架構(gòu)香浩,當(dāng)發(fā)展到一定程度类缤,業(yè)務(wù)概念變得非常多,概念之間的關(guān)系很復(fù)雜邻吭,需求仍處于高位餐弱,系統(tǒng)越來越難理解了,溝通成本越來越高了,缺陷修復(fù)波及面越來越廣了膏蚓,是時候用 DDD 來拆分限界上下文和管理核心復(fù)雜度了瓢谢。

對,是時候了驮瞧。當(dāng)限界上下文拆分完成后氓扛,不管是將所有限界上下文都放在一個服務(wù)里,還是僅將一部分限界上下文放在一個微服務(wù)里论笔,都會導(dǎo)致多限界上下文的物理設(shè)計采郎,如下所示:


bc-physical-design.png

說明:多個限界上下文復(fù)用公共層,限界上下文之間優(yōu)先采用函數(shù)調(diào)用狂魔,但都封裝在了適配層蒜埋。這就是說,當(dāng)按限界上下文劃分微服務(wù)后毅臊,僅需修改適配層的代碼理茎。

小結(jié)

本文遵從六邊形架構(gòu),闡述了微服務(wù)物理設(shè)計的方方面面管嬉。一般情況下皂林,基礎(chǔ)的物理設(shè)計就夠用了,但在復(fù)雜場景下蚯撩,需要考慮擴展的物理設(shè)計础倍。

如果你曾糾結(jié)過如何為微服務(wù)分層分包,或者如何為微服務(wù)設(shè)計目錄和文件胎挎,那么你就是目標(biāo)讀者沟启,希望能給你一定的啟發(fā)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末犹菇,一起剝皮案震驚了整個濱河市德迹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌揭芍,老刑警劉巖胳搞,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異称杨,居然都是意外死亡肌毅,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門姑原,熙熙樓的掌柜王于貴愁眉苦臉地迎上來悬而,“玉大人,你說我怎么就攤上這事锭汛”康欤” “怎么了袭蝗?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長艰躺。 經(jīng)常有香客問我呻袭,道長,這世上最難降的妖魔是什么腺兴? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮廉侧,結(jié)果婚禮上页响,老公的妹妹穿的比我還像新娘。我一直安慰自己段誊,他們只是感情好闰蚕,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著连舍,像睡著了一般没陡。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上索赏,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天盼玄,我揣著相機與錄音,去河邊找鬼潜腻。 笑死埃儿,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的融涣。 我是一名探鬼主播童番,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼威鹿!你這毒婦竟也來了剃斧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤忽你,失蹤者是張志新(化名)和其女友劉穎幼东,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體檀夹,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡筋粗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了炸渡。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片娜亿。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蚌堵,靈堂內(nèi)的尸體忽然破棺而出买决,到底是詐尸還是另有隱情沛婴,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布督赤,位于F島的核電站嘁灯,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏躲舌。R本人自食惡果不足惜丑婿,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望没卸。 院中可真熱鬧羹奉,春花似錦、人聲如沸约计。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽煤蚌。三九已至耕挨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間尉桩,已是汗流浹背筒占。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留魄健,地道東北人赋铝。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像沽瘦,于是被迫代替她去往敵國和親革骨。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容