我對(duì)軟件分層設(shè)計(jì)的思考
不點(diǎn)藍(lán)字,我們哪來(lái)故事?

每天 11 點(diǎn)更新文章,餓了點(diǎn)外賣,點(diǎn)擊 ??《無(wú)門檻外賣優(yōu)惠券,每天免費(fèi)領(lǐng)!》

1. 什么是分層設(shè)計(jì)?它有何好處? 2. 計(jì)算機(jī)語(yǔ)言的發(fā)展 3. Linux 內(nèi)核 4. TCP/IP 網(wǎng)絡(luò)協(xié)議堆棧 5. Netty 6. 微服務(wù)分層 7. Rails On Rack 8. 總結(jié)
在日常開發(fā)中,經(jīng)常聽到大家說一句話“任何需求都可以通過一個(gè)間接的的中間層來(lái)解決”。今天,通過幾個(gè) case 就“分層”話題梳理下自己的思考,其中,有些 case 比較直觀,而有些不那么直觀,甚至有些微妙,需要我們自己多品味。這意味著學(xué)習(xí)過程需要我們不斷將新知識(shí)與舊知識(shí)進(jìn)行關(guān)聯(lián),形成自己的知識(shí)體系,而非一個(gè)個(gè)知識(shí)孤島。
1. 什么是分層設(shè)計(jì)?它有何好處?

分層設(shè)計(jì)將軟件劃分成若干層,每一層只解決一部分問題,通過所有層的協(xié)作來(lái)完成整體目標(biāo)。一個(gè)復(fù)雜問題通過分解成一個(gè)個(gè)系統(tǒng)子問題,這樣就有效的降低了每個(gè)子問題的規(guī)模與復(fù)雜度。
分層設(shè)計(jì)帶來(lái)的好處:
降低了系統(tǒng)軟件的復(fù)雜度,將一個(gè)復(fù)雜問題通過分解,分而治之 功能的復(fù)用和封裝
2. 計(jì)算機(jī)語(yǔ)言的發(fā)展

機(jī)器語(yǔ)言
早期,軟件開發(fā)是機(jī)器語(yǔ)言,直接用二進(jìn)制 0 和 1 表示機(jī)器可以識(shí)別的指令和數(shù)據(jù),看起來(lái)像這樣:
0010000100100011
這就是計(jì)算機(jī) CPU 唯一可以理解的語(yǔ)言。對(duì)人類為說,二進(jìn)制的程序是不可讀的。
匯編語(yǔ)言
為了解決語(yǔ)言可讀性的問題,匯編程序誕生了。匯編程序是人類可讀的機(jī)器代碼。它又被稱為“符號(hào)語(yǔ)言”,使用助記符來(lái)代替機(jī)器的操作碼。
匯編語(yǔ)言是二進(jìn)制的文本形式,與 CPU 的指令是一一對(duì)應(yīng)的關(guān)系。而我們不同的 CPU 體系結(jié)構(gòu)(比如 PC 的 X86、嵌入式的 ARM) 是不同的,面向機(jī)器的語(yǔ)言帶來(lái)的問題就是:對(duì)于不同的 CPU 體系架構(gòu),就需要不同的匯編語(yǔ)言。
高級(jí)語(yǔ)言
為了解決語(yǔ)言對(duì)機(jī)器的無(wú)關(guān)性,高級(jí)語(yǔ)言誕生了。一條高級(jí)語(yǔ)言通常由若干條機(jī)器語(yǔ)言實(shí)現(xiàn)的,并且不具有對(duì)應(yīng)性。
高級(jí)語(yǔ)言讓開發(fā)者不需要關(guān)注底層 CPU 體系結(jié)構(gòu)與指令,只關(guān)注業(yè)務(wù)即可。
計(jì)算機(jī)語(yǔ)言的發(fā)展就是不斷的抽象,只有通過抽象,將一個(gè)復(fù)雜的的系統(tǒng)變成一層層的接口集合,讓我們每次只需要考慮關(guān)注當(dāng)前層集合內(nèi)的邏輯,而不用去考慮當(dāng)前層次以上或者以下的復(fù)雜度,才有可能讓我們從復(fù)雜系統(tǒng)中解放出來(lái),逐步理解以及構(gòu)造一個(gè)復(fù)雜系統(tǒng)。
3. Linux 內(nèi)核
內(nèi)核功能層與內(nèi)核硬件層

操作系統(tǒng)內(nèi)核,可以簡(jiǎn)化理解成三大層:
內(nèi)核接口層 :向上對(duì)用戶態(tài)應(yīng)用程序提供一套接口子集,開發(fā)者使用的系統(tǒng)調(diào)用 APIs。 內(nèi)核功能層 :這一層完成各種實(shí)際的功能,我們知道 OS 主要負(fù)責(zé)資源管理、內(nèi)存、進(jìn)程這些資源,物理內(nèi)存如何申請(qǐng)、釋放,進(jìn)程如何調(diào)度。具體來(lái)說進(jìn)程管理、內(nèi)存管理、中斷管理、設(shè)備管理。 內(nèi)核硬件層 :分離硬件的相關(guān)性,我們知道一個(gè) OS 可以運(yùn)行不同的指令集,也就是運(yùn)行在不同的硬件平臺(tái)。
不管是 ARM 體系結(jié)構(gòu),還是 X86,選擇一個(gè)進(jìn)程調(diào)度的算法是可以相同的,需要改變的進(jìn)程切換相關(guān)代碼,因?yàn)椴煌挠布脚_(tái)的上下文是不同的,CPU 的寄存器也不同。這時(shí)候最好的設(shè)計(jì)是分層,當(dāng)操作系統(tǒng)運(yùn)行在不同的硬件平臺(tái)時(shí),就只需要修改硬件平臺(tái)相關(guān)層代碼,實(shí)現(xiàn)操作系統(tǒng)的高可移植性。
操作系統(tǒng)有兩個(gè)關(guān)鍵設(shè)計(jì):
內(nèi)核接口層區(qū)分用戶態(tài)與內(nèi)核態(tài),來(lái)保護(hù)硬件資源受限訪問。 內(nèi)核硬件層分離多種硬件平臺(tái)相關(guān)性。這種分層的架構(gòu),極大提升了系統(tǒng)的穩(wěn)定性和擴(kuò)展性。
MMU 抽象層
操作系統(tǒng)負(fù)責(zé)管理物理內(nèi)存,而用戶進(jìn)程使用虛擬內(nèi)存。操作系統(tǒng)呈現(xiàn)給用戶進(jìn)程的是連續(xù)的虛擬空間,但不一定是連續(xù)的物理空間。因?yàn)槲锢韮?nèi)存被整個(gè) OS 共享。
什么是 MMU 呢?它是硬件,即內(nèi)存管理單元,它對(duì) CPU 發(fā)出的訪存地址進(jìn)行映射與檢查,可以讓處理器發(fā)出的訪存地址訪問不同的物理內(nèi)存單元。
如果將計(jì)算機(jī)上有限的物理內(nèi)存分配給多個(gè)應(yīng)用程序使用,如果讓應(yīng)用程序直接訪問物理內(nèi)存,如果沒有 MMU 這層抽象呢?帶來(lái)的問題是每個(gè)應(yīng)用程序地址空間不隔離,內(nèi)存使用率低,程序運(yùn)行地址也無(wú)法固定。

解決的問題:虛擬內(nèi)存 VA 與物理內(nèi)存 PA 的映射——通過在 CPU 與內(nèi)存之間加入 MMU 抽象層,讓 CPU 在運(yùn)行指令時(shí)發(fā)出的 VA 虛擬地址通過 MMU 轉(zhuǎn)換后變成 PA 物理地址,然后再去訪問物理內(nèi)存。

MMU 引入帶來(lái)的好處:
權(quán)限控制。可以對(duì)一些虛擬地址進(jìn)行訪問控制,比較代碼段為只讀,用戶程序代可寫。 提升內(nèi)存使用率:物理內(nèi)存按需申請(qǐng)。fork 子進(jìn)程的對(duì)應(yīng)的物理空間是能過寫時(shí)復(fù)制才進(jìn)行真正的物理內(nèi)存分配。 不同進(jìn)程之間可以使用相同的虛擬內(nèi)存地址空間,而進(jìn)程的物理內(nèi)存又可以隔離。 系統(tǒng)運(yùn)行多個(gè)進(jìn)程,所分配的內(nèi)存之和可以大于實(shí)際物理內(nèi)存大小。
這是我認(rèn)為最經(jīng)典、最本質(zhì)、最受啟發(fā)的中間抽象層的設(shè)計(jì)。
CPU 與外設(shè)的通信
CPU 訪問外設(shè)有兩種方法;
IO 與內(nèi)存統(tǒng)一編址 IO 與內(nèi)存的獨(dú)立編址

外設(shè)接口中的 IO 寄存器(即 IO 端口)與主存單元一樣看待,每個(gè)端口占用一個(gè)存儲(chǔ)單元的地址,將主存的一部分劃分出來(lái)用作 IO 的地址空間。
把外設(shè)的寄存器當(dāng)做是一個(gè)內(nèi)存地址,從而 CPU 以類似訪問內(nèi)存相同的方式來(lái)操作外設(shè)。
對(duì) IO 外設(shè)的端口映射到一個(gè)物理內(nèi)存單元地址,在 CPU 與外設(shè)之間的“內(nèi)存”抽象層,帶來(lái)好處是訪問內(nèi)存一樣去訪問外設(shè)。
小結(jié)
Linux 中的內(nèi)核硬件層設(shè)計(jì)、MMU、CPU 與 IO 外設(shè)通信設(shè)計(jì)處處體現(xiàn)了分層 / 中間層的設(shè)計(jì)思想。
4. TCP/IP 網(wǎng)絡(luò)協(xié)議堆棧
從最底層的物理鏈路層層層向上封裝抽象,解決了復(fù)雜的網(wǎng)絡(luò)通信的問題。同樣的,任何復(fù)雜的問題,通過分層最終總能夠回歸最本質(zhì)、最簡(jiǎn)單。這個(gè)分層架構(gòu),對(duì)所有開發(fā)者而言,再熟悉不過,它的引入是想與后續(xù)介紹的 Netty 形成對(duì)比。這里先賣個(gè)關(guān)子,后面解開謎底。

舉例說明::
來(lái)自杭州西湖區(qū)某個(gè)小區(qū)的商務(wù)人士來(lái)京出差后,被確診新冠肺炎,實(shí)施在京隔離措施,同時(shí)北京將此報(bào)告先發(fā)給浙江省,接著浙江省發(fā)給杭州市政府,然后市政府再向西湖區(qū)發(fā)送,最后到達(dá)某小區(qū)。這個(gè)發(fā)送報(bào)告過程也是分層報(bào)告思想。
DNS 中間層

DNS (domain name system) 是域名系統(tǒng),是用來(lái)將主機(jī)轉(zhuǎn)換為 IP 地址的服務(wù)。我們有至少三種方式在互聯(lián)網(wǎng)上標(biāo)識(shí)一臺(tái)主機(jī)、主機(jī)名、IP 地址以及 MAC 地址。為什么有引入 DNS 中間抽象層呢? 主要是主機(jī)名便于記憶,而 IP 地址方便于在計(jì)算機(jī)網(wǎng)絡(luò)設(shè)備的處理,因此需要設(shè)計(jì)出一個(gè) DNS 協(xié)議 (中間層) 來(lái)做主機(jī)名到 IP 地址的轉(zhuǎn)換。
ARP 中間層

ARP(address resolution protocol) 是地址解析協(xié)議,它根據(jù) IP 地址來(lái)獲取物理地址。上面也談到,MAC 與 IP 都可以用來(lái)標(biāo)識(shí)一臺(tái)主機(jī)。那二者區(qū)別是什么?
同一個(gè)局域網(wǎng)中的一臺(tái)主機(jī)和另一臺(tái)主機(jī)通信的時(shí)候,需要通過 MAC 地址進(jìn)行定位,之后才能進(jìn)行數(shù)據(jù)包的傳送。
而在網(wǎng)絡(luò)層和傳輸層中,主機(jī)之間是通過 IP 地址來(lái)定位的,對(duì)應(yīng)的數(shù)據(jù)包中必須攜帶目標(biāo)主機(jī)的 IP 地址, 而沒有 MAC 地址。
因此,ARP 協(xié)議 (中間層) 用來(lái)實(shí)現(xiàn)從 IP 到 MAC 地址的轉(zhuǎn)換。
5. Netty
Netty 提供了異步的,基于事件驅(qū)動(dòng)的網(wǎng)絡(luò)應(yīng)用程序框架。目前分布式搜索引擎,Spark 框架底層是擴(kuò)展使用 Netty 框架。Netty 本身的架構(gòu)理解有些曲線,為了講清楚,我還是希望循序漸進(jìn)方式,通過它的發(fā)展歷史來(lái)一步步介紹。先鋪墊再介紹,大家需要一些耐心。
傳統(tǒng)阻塞 IO 服務(wù)模型

思路:
采用阻塞 IO 模式獲取輸入數(shù)據(jù) 每個(gè)連接都需要獨(dú)立的線程完成數(shù)據(jù)的輸入,業(yè)務(wù)的處理和數(shù)據(jù)返回
問題:
當(dāng)并發(fā)數(shù)很大時(shí),就會(huì)創(chuàng)建大量的線程,占用了很大的系統(tǒng)資源。 連接創(chuàng)建后,如果當(dāng)前線程沒有數(shù)據(jù)可讀,這個(gè)線程會(huì)阻塞在 read 方法上,造成資源浪費(fèi)。
單 Reactor 單線程

思路:
通過引入 selector 事件選擇器來(lái)監(jiān)聽多路連接的請(qǐng)求。 Reactor 對(duì)象通過 selector 監(jiān)控客戶端請(qǐng)求事件后,通過 Dispatch 進(jìn)行分發(fā)。 如果建立連接請(qǐng)求事件,則由 Acceptor 負(fù)責(zé)建立一個(gè)連接,然后創(chuàng)建一個(gè) Handler 對(duì)象處理連接完成后的業(yè)務(wù)處理。
問題:
模型簡(jiǎn)單,沒有多線程,資源競(jìng)爭(zhēng)的問題。所以工作在一個(gè)線程完成。 性能問題,一個(gè)線程,無(wú)法發(fā)揮多核 CPU 的性能。 可靠性問題,線程 crash,會(huì)導(dǎo)致整個(gè)系統(tǒng)不可用。
主從 Reactor 多線程
主 React 處理所有 socket 連接事件的監(jiān)聽和響應(yīng),而從 React 處理所有 socket 的讀寫事件的監(jiān)聽與響應(yīng)。主從 React 都在多線程中運(yùn)行。

Netty 模型
Netty 主要基于主從 Reactor 多線程模型發(fā)展出來(lái)的。

Netty 邏輯架構(gòu)
前面 Netty 的發(fā)展階段都是鋪墊,Nettty 邏輯架構(gòu)為典型網(wǎng)絡(luò)分層架構(gòu)設(shè)計(jì),從下到上分別為網(wǎng)絡(luò)通信層、事件調(diào)度層、服務(wù)編排層。

網(wǎng)絡(luò)通信層 :它執(zhí)行網(wǎng)絡(luò) I/O 操作,核心組件包含 BootStrap、ServerBootStrap、Channel。——Channel 通道,提供了基礎(chǔ)的 API 用于操作網(wǎng)絡(luò) IO,比如 bind、connect、read、write、flush 等等。它以 JDK NIO Channel 為基礎(chǔ),提供了更高層次的抽象,同時(shí)屏蔽了底層 Socket 的復(fù)雜性。Channel 有多種狀態(tài),比如連接建立、數(shù)據(jù)讀寫、連接斷開。隨著狀態(tài)的變化,Channel 處于不同的生命周期,背后綁定相應(yīng)的事件回調(diào)函數(shù)。
事件調(diào)度層 :它的核心組件包含 EventLoopGroup、EventLoop。——EventLoop 本質(zhì)是一個(gè)線程池,主要負(fù)責(zé)接收 Socket I/O 請(qǐng)求,并分配事件循環(huán)器來(lái)處理連接生命周期中所發(fā)生的各種事件。
服務(wù)編排層 :它的職責(zé)實(shí)現(xiàn)網(wǎng)絡(luò)事件的動(dòng)態(tài)編排和有序傳播——ChannelPipeline 基于責(zé)任鏈模式,方便業(yè)務(wù)邏輯的攔截和擴(kuò)展;本質(zhì)上它是一個(gè)雙向鏈表將不同的 ChannelHandler 鏈接在一塊,當(dāng) I/O 讀寫事件發(fā)生時(shí), 會(huì)依次調(diào)用 ChannelHandler 對(duì) Channel(Socket) 讀取的數(shù)據(jù)進(jìn)行處理。
ChannelPipeline 私有協(xié)議棧 vs. TCP/IP 協(xié)議棧

前面鋪墊這么久,就是為了自然過渡到上面的圖,請(qǐng)務(wù)必與 TCP/IP 協(xié)議棧進(jìn)行對(duì)比。
socket。read 經(jīng)過 TCP/IP 協(xié)議棧后,進(jìn)入 netty 的網(wǎng)絡(luò)通信層,事件調(diào)度層,最后來(lái)到服務(wù)編排層。而服務(wù)編排層的 channelPipeline 的設(shè)計(jì)也是一個(gè) upstream/downstream 的 stack,一進(jìn)一出的二個(gè) pipeline。負(fù)責(zé)處理流入 / 流出的數(shù)據(jù)包。
上面的 stack 就非常類似 TCP/IP 協(xié)議棧。根據(jù)公司組織的需要可以定制分層的私有協(xié)議棧,比如從 authentication-handler、message-validation-handler、message-encode-handler、message-decoder-handler。
6. 微服務(wù)分層

grpc-gateway ——它是一個(gè)開源框架, 讀取 protobuf 接口定義并生成一個(gè)反向代理服務(wù)器, 此服務(wù)器時(shí)一步將 restful http API 轉(zhuǎn)換成 grpc 服務(wù).
middleware ——實(shí)現(xiàn)鑒權(quán)功能, 比如哪些 URL 需要權(quán)限檢驗(yàn)
handler 通用處理層 ——參數(shù)檢驗(yàn): handler 層負(fù)責(zé)執(zhí)行與客戶端約定參數(shù)的檢驗(yàn), 檢驗(yàn)通過后再組裝成后端服務(wù)需要的數(shù)據(jù)結(jié)構(gòu)發(fā)往后端;接口聚合 / 組合服務(wù): handler 層可以根據(jù)業(yè)務(wù)需要, 調(diào)用多個(gè)后端服務(wù)的 endpoint 來(lái)組合實(shí)現(xiàn)一個(gè)新的接口, 同時(shí)將下層返回的數(shù)據(jù)進(jìn)行聚合處理.
service/model 業(yè)務(wù)邏輯層 ——對(duì)業(yè)務(wù)邏輯的封裝, 負(fù)責(zé)將多個(gè) DAO 數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換和封裝成一個(gè)有邏輯意義的模型;可以引入緩存策略, 優(yōu)化數(shù)據(jù)存取效率.
DAO 層 ——數(shù)據(jù)訪問層, 主要負(fù)責(zé)操作 DB 中某張表并映射到內(nèi)存中某個(gè) DAO 模型;與數(shù)據(jù)表結(jié)構(gòu)一一對(duì)應(yīng), 通過 DAO 內(nèi)存模型向上層傳遞數(shù)據(jù)源的對(duì)象.
數(shù)據(jù)訪問層 DAL ——對(duì)底層的數(shù)據(jù)源做統(tǒng)一的抽象, 屏蔽數(shù)據(jù)庫(kù). 如果沒有 DAL 的存在, 那么向乎所有的業(yè)務(wù)邏輯層都會(huì)去與具體的數(shù)據(jù)庫(kù)存儲(chǔ)強(qiáng)挷定. 耦合性就很高.
還有一個(gè)補(bǔ)充點(diǎn):
業(yè)務(wù)邏輯層中的服務(wù)在實(shí)際場(chǎng)景中不可避免的會(huì)出現(xiàn)互相調(diào)用的場(chǎng)景,這種情況往往需要將耦合 / 公共的功能進(jìn)行下沉,比如數(shù)據(jù)請(qǐng)求下沉為數(shù)據(jù)訪問層服務(wù),而業(yè)務(wù)下沉為穩(wěn)定的通用業(yè)務(wù)服務(wù),被其它服務(wù)穩(wěn)定依賴。
7. Rails On Rack
熟悉 Ruby On Rails Web 應(yīng)用框架的開發(fā)者,肯定知道 Rack 是如何成為應(yīng)用容器 (webserver) 和應(yīng)用框架之間的橋梁的。

Rack 在 webserver 和應(yīng)用框架之間提供了一套最小的 API 接口,如果 webserver 都遵循 Rack 提供的這套規(guī)則,那么所有的框架都能通過協(xié)議任意地改變底層使用 webserver。

Rack 分層設(shè)計(jì)非常類似 Decorate Pattern 或者 Chain of Responsibility Pattern。
8. 總結(jié)
本文作者結(jié)合自身工作經(jīng)驗(yàn), 總結(jié)一些典型分層設(shè)計(jì)案例
計(jì)算機(jī)語(yǔ)言的發(fā)展 Linux 內(nèi)核設(shè)計(jì) (內(nèi)核功能層與內(nèi)核硬件層,MMU 抽象層,CPU 與外設(shè)的通信) TCP/IP 網(wǎng)絡(luò)協(xié)議堆棧 (DNS 和 ARP 協(xié)議) Netty 框架發(fā)展以及分層私有協(xié)議棧分析 微服務(wù)分層 應(yīng)用框架 Rails On Rack
這些案例充分說明了計(jì)算機(jī)系統(tǒng)本身就是通過一層一層抽象構(gòu)造出來(lái)的。
硬件方面是從一個(gè)個(gè)小的晶體管,抽象成一個(gè)個(gè)門電路,再到 CPU 器件,最后抽象組成計(jì)算機(jī)。 軟件設(shè)計(jì)也是一個(gè)層次一個(gè)層次功能完善疊加的,無(wú)論是自頂向下還是自底向上。
楊敏,F(xiàn)reewheel 首席工程師,負(fù)責(zé) SFX 團(tuán)隊(duì)的整體工作。目前從事服務(wù)化框架、容器化平臺(tái)相關(guān)。關(guān)注與感興趣的技術(shù)主要有 Python/Java 虛擬機(jī)、Golang、K8s、分布式數(shù)據(jù)庫(kù)、分布式搜索引擎 ElasticSearch。
- END -
往期推薦
下方二維碼關(guān)注我

技術(shù)草根,堅(jiān)持分享 編程,算法,架構(gòu)

看完文章,餓了點(diǎn)外賣,點(diǎn)擊 ??《無(wú)門檻外賣優(yōu)惠券,每天免費(fèi)領(lǐng)!》

