你的解耦戰(zhàn)術(shù),決定了架構(gòu)高度!
架構(gòu)設(shè)計(jì)中,大家都不喜歡耦合,但有哪些典型的耦合是我們系統(tǒng)架構(gòu)設(shè)計(jì)中經(jīng)常出現(xiàn)的,又該如何優(yōu)化?
這里列舉了 6 個(gè)點(diǎn):IP、jar 包、數(shù)據(jù)庫(kù)、服務(wù)、消息、擴(kuò)容。
這些點(diǎn),如果設(shè)計(jì)不慎,都會(huì)導(dǎo)致系統(tǒng)出現(xiàn)一些耦合問題,基本都是大家實(shí)際遇到的痛點(diǎn)。本文將與大家分享如何用常見的方案去解除這些耦合。
如何找到系統(tǒng)中的耦合
什么是耦合?就是每每我們作為技術(shù)人,在心中罵上下游、罵兄弟部門,說這個(gè)東西跟我有什么關(guān)系?為什么我要配合來做這個(gè)事情?這里面就非常有可能是系統(tǒng)中存在耦合的地方。
明明我們不應(yīng)該聯(lián)動(dòng),但兄弟部門要做一個(gè)事情,上下游要做一個(gè)事情,我卻要被動(dòng)地配合來做這個(gè)事情。還有可能這個(gè)配合的范圍特別特別的大,那就說明耦合非常非常的重。
下面來看具體的六個(gè)案例。
典型耦合與對(duì)應(yīng)解耦實(shí)踐
IP 耦合
第一個(gè)案例,特別常見。原來線上有服務(wù)或者有條數(shù)據(jù)庫(kù),因?yàn)楦鞣N原因,例如磁盤硬件有故障,要換一臺(tái)機(jī)器,然后運(yùn)維給了我們一臺(tái)機(jī)器,我們把數(shù)據(jù)庫(kù)或者把服務(wù)給部署好了。
部署好了 IP 要換,原來有個(gè)舊 IP,現(xiàn)在有個(gè)新 IP。那就有很多上游依賴我,我 IP 換了怎么辦?就找到上游說我的 IP 換了,麻煩上游部門改配置重啟一下,連到我新的 IP 上去。
不知道大家工作中會(huì)不會(huì)遇到這樣的場(chǎng)景,這時(shí)如果你作為上游的調(diào)用方,不管你調(diào)數(shù)據(jù)庫(kù)還是調(diào)服務(wù),你心里可能就在罵他了,明明是你 IP 變了,為什么配合重啟、配合改配置的人是我?
特別是如果一個(gè)基礎(chǔ)服務(wù)或者一個(gè)基礎(chǔ)數(shù)據(jù)庫(kù),依賴它的人很多,那么你可能要找到這些依賴它的人,可能有 A 部門、B 部門、C 部門,所有業(yè)務(wù)都依賴你,你要全部找一遍,全部重啟。
所以這個(gè)因?yàn)?IP 配置使得上下游耦合在一起的案例,它的耦合范圍其實(shí)是非常廣的,我們都覺得很討厭。
我們的希望是:你改一個(gè) IP,能不能我不動(dòng),你自己升級(jí)了,我流量就默默遷移過去,這是一個(gè)非常直觀的理解上下游的耦合。
內(nèi)網(wǎng) IP 修改為內(nèi)網(wǎng)域名,這是我們的實(shí)踐,強(qiáng)烈的建議大家回去馬上干這個(gè)事情。為什么我們 IP 要修改、要重啟?
很有可能是我們將 IP 寫在了自己的配置文件中。如果我們把這個(gè)內(nèi)網(wǎng) IP 變?yōu)閮?nèi)網(wǎng)域名,那么我們是不是就可以不讓上游配合去改配置重啟呢?
假設(shè)我們現(xiàn)在不用 IP 了,用域名了?,F(xiàn)在換了一臺(tái)機(jī)器域名沒變,IP 指向變了。我們可以讓運(yùn)維統(tǒng)一將內(nèi)網(wǎng) DNS 切到新的機(jī)器上面去,并將舊機(jī)器的連接切斷,重連后就會(huì)自動(dòng)連到新機(jī)器上去了。
這樣的話只要運(yùn)維配合就可以完成遷移,對(duì)于所有上游的調(diào)用方、服務(wù)的調(diào)用方、數(shù)據(jù)的調(diào)用方都不需要?jiǎng)?,這是第一個(gè)案例。
我們的最佳實(shí)踐是強(qiáng)烈建議使用內(nèi)網(wǎng)域名來替換內(nèi)網(wǎng)的 IP,連服務(wù)、連數(shù)據(jù)庫(kù)統(tǒng)統(tǒng)取走。
公共庫(kù)耦合
第二個(gè)案例是公共庫(kù),這個(gè)公共庫(kù)可能是一個(gè)跟業(yè)務(wù)相關(guān)的通用業(yè)務(wù)庫(kù),比如用戶的業(yè)務(wù)、支付的業(yè)務(wù),這些業(yè)務(wù)寫在了一個(gè) jar 包里,各個(gè)業(yè)務(wù)線通過這個(gè) jar 包來實(shí)現(xiàn)相關(guān)的一些業(yè)務(wù)邏輯。
所有的業(yè)務(wù)方因?yàn)檫@個(gè)公共庫(kù)耦合了,不管你是 so、dll,還是 jar 包代碼,不同語言的公共庫(kù)方式不一樣,本質(zhì)是上游通過這個(gè)公共庫(kù)耦合在一起。
我們?cè)?jīng)碰到什么樣的情況呢?58 有招聘、房產(chǎn)、二手很多業(yè)務(wù)線,用戶的一些操作,登錄、查詢信息、修改信息可能都是相通的,所以我們有一個(gè) user.jar,對(duì)所有用戶的操作可能通過這個(gè) jar 包去做。
然后有個(gè)業(yè)務(wù)線,比如說招聘,他可能修改了用戶的操作的一些代碼,修改了這個(gè) jar 包。
修改之后,上線之前會(huì)進(jìn)行測(cè)試,但招聘只會(huì)測(cè)試自己的業(yè)務(wù),不會(huì)測(cè)試兄弟業(yè)務(wù)線的業(yè)務(wù),導(dǎo)致上線的結(jié)果是,上線后兄弟業(yè)務(wù)線全掛了。
于是就出現(xiàn)了一個(gè)很有意思的場(chǎng)景, A 和 B 的業(yè)務(wù)老大在群里面說怎么業(yè)務(wù)都掛了,然后有研發(fā)兄跳出來解釋說 C 部門上線了,所以我們都掛了,這個(gè)解釋是很難說通的。
為什么兄弟部門好好的,他上線了他沒問題,而我們掛了,就是因?yàn)?jar 包耦合在一起,可能我們也在心里會(huì)默默地罵他們,修改代碼的是你,沒問題的也是你,有問題的是我,我其實(shí)什么都沒動(dòng),我很委屈。
多個(gè)上游因?yàn)?jar 包耦合在了一起,那有什么樣的優(yōu)化方法?
如果代碼庫(kù)個(gè)性很強(qiáng)
如果這個(gè) jar 包、這個(gè)公共庫(kù)的個(gè)性比較強(qiáng),如果是偏招聘的、房產(chǎn)的、二手的,我們的建議是把這些個(gè)性的代碼拆分到各個(gè)業(yè)務(wù)線自己的 jar 包里面去。
這樣的話,你修改的那一塊只影響你自己,至少不會(huì)擴(kuò)大影響范圍,這個(gè)需要對(duì)業(yè)務(wù)進(jìn)行剖析,把個(gè)性的地方拿出來。
如果長(zhǎng)時(shí)間解決不了,我剛剛說的那種耦合頻發(fā),出現(xiàn)的次數(shù)特別多,最差的情況下我們可以 copy 代碼,比如說拷三份,但這個(gè)不推薦。
我們的建議:還是抽取其中的個(gè)性部分,把原來的一個(gè) business 的 jar 包變成三個(gè)加包,每一塊只跟一塊業(yè)務(wù)相關(guān)。
如果公共庫(kù)通用性很強(qiáng)
那如果這個(gè)庫(kù)的共性比較強(qiáng),我們建議通用的部分下沉獨(dú)立一個(gè) service,這個(gè) service 對(duì)上游提供接口,我每次測(cè)試你也要測(cè)試接口的兼容性。
如果是新的業(yè)務(wù),我們建議新增接口,這樣至少不會(huì)對(duì)舊有的代碼產(chǎn)生影響,通過 service 或 RPC 調(diào)用的方式來解除耦合。
數(shù)據(jù)庫(kù)耦合
第三個(gè)案例應(yīng)該也是大家會(huì)遇到比較多的情況,數(shù)據(jù)庫(kù)的耦合。
我先說一下業(yè)務(wù)場(chǎng)景:業(yè)務(wù) A、業(yè)務(wù) B、業(yè)務(wù) C,這里還是拿用戶的業(yè)務(wù)舉例,有些用戶的數(shù)據(jù)是通用的,存在 table-user 里,而個(gè)性的數(shù)據(jù)我們存在個(gè)性的數(shù)據(jù)庫(kù)里。
比如業(yè)務(wù) A 我們可能有個(gè) table-A、業(yè)務(wù) B 有 table-B、業(yè)務(wù) C 有 table-C。
假設(shè)我的業(yè)務(wù)線既要取個(gè)性的數(shù)據(jù),又要取共性的數(shù)據(jù),我們的代碼往往這么寫,個(gè)性表 join 個(gè)性表,UID 相同,UID 等于我的用戶 1、2、3,個(gè)性的數(shù)據(jù)和共性的數(shù)據(jù)一起抽取出來,沒有任何問題。
業(yè)務(wù)線 B、業(yè)務(wù)線 C 也是這么做的。所以你會(huì)發(fā)現(xiàn) join 語句其實(shí)導(dǎo)致了 user 的 table 和業(yè)務(wù)線 A、B、C 的 table 耦合到一個(gè)數(shù)據(jù)庫(kù)實(shí)例里。
這樣會(huì)導(dǎo)致什么問題呢?比如 A 業(yè)務(wù)線要上線一個(gè)功能,這個(gè)功能沒有索引,對(duì)全表都要掃描,數(shù)據(jù)庫(kù) CPU 100%,數(shù)據(jù)庫(kù)實(shí)例 IO 性能下降,影響業(yè)務(wù)。
對(duì)B 和 C 都有影響,即某個(gè)業(yè)務(wù)線的數(shù)據(jù)庫(kù)性能急劇下降導(dǎo)致所有業(yè)務(wù)都受影響。
這時(shí) DBA 兄弟、運(yùn)維兄弟殺過來說性能不行了,我再給你兩臺(tái)機(jī)器,給我兩個(gè)實(shí)例,你會(huì)發(fā)現(xiàn)沒用,所有表都耦合在一個(gè)實(shí)例里,給機(jī)器也拆不開,擴(kuò)不了容。
2015 年我調(diào)去 58 到家時(shí),當(dāng)時(shí)整個(gè) 58 到家有一個(gè)庫(kù)叫做 58 到家?guī)欤锩嬗袔装賯€(gè)表,性能越來越低,但因?yàn)楦鞣N join 又必須耦合在一個(gè)實(shí)例里,很悲慘。
我們?cè)趺醋瞿?垂直切分與服務(wù)化。你會(huì)發(fā)現(xiàn)跟 jar 包解耦非常相似,垂直拆分。
比如說用戶的基礎(chǔ)數(shù)據(jù),我抽向一個(gè)用戶的服務(wù),user 最基礎(chǔ)的數(shù)據(jù)庫(kù)只能夠被這個(gè)服務(wù)鎖訪問,數(shù)據(jù)庫(kù)私有是服務(wù)化的一個(gè)特點(diǎn)。
此時(shí)業(yè)務(wù)線原來的業(yè)務(wù)怎么樣滿足?原來是業(yè)務(wù)方直接一個(gè) join 既取了共有的數(shù)據(jù)又取了私有的數(shù)據(jù),此時(shí)原來的一次數(shù)據(jù)庫(kù)訪問變成了兩次數(shù)據(jù)庫(kù)訪問,第一次取個(gè)性數(shù)據(jù),第二次取共性數(shù)據(jù),然后業(yè)務(wù)層拼裝。
之前的方式和之后的方式相比,之前的方式業(yè)務(wù)代碼可能會(huì)更簡(jiǎn)單一些,因?yàn)樗菍⑦@個(gè)業(yè)務(wù)邏輯放在了 SQL 語句中,但是導(dǎo)致數(shù)據(jù)庫(kù)耦合在了一起。
后面這種方式就是業(yè)務(wù)的代碼會(huì)更復(fù)雜,會(huì)變成多次訪問,將原來在 SQL 中進(jìn)行的邏輯計(jì)算變成我們自己的代碼的邏輯計(jì)算。
此時(shí)業(yè)務(wù)有自己的庫(kù),公共有公共的庫(kù),你會(huì)發(fā)現(xiàn)很有可能這些庫(kù)早期也在一個(gè)實(shí)例,但是性能下降時(shí)可以很容易地新增實(shí)例,把其中一個(gè)公共的庫(kù)從一個(gè)實(shí)例里放到另外一個(gè)實(shí)例,甚至新增一臺(tái)機(jī)器做到硬件的擴(kuò)容。
所以垂直切分是指業(yè)務(wù)側(cè)自己的數(shù)據(jù)庫(kù)放到自己的上去,公共的放到公共的上去,不要耦合在一個(gè)實(shí)例當(dāng)中,這是一個(gè)比較典型的業(yè)務(wù)場(chǎng)景。
服務(wù)化耦合
第四個(gè)案例是服務(wù)化耦合的例子。服務(wù)化之后,如果業(yè)務(wù)代碼拆分得不干凈,即使你做了服務(wù)化也不能夠解除耦合。這里舉一個(gè)服務(wù)化解耦不徹底的案例。
上面是 ABC 三個(gè)業(yè)務(wù)方,底下是一個(gè)通用的服務(wù)。假如你解耦不徹底,你這個(gè)通用的服務(wù)里有業(yè)務(wù)側(cè)的代碼,最典型的業(yè)務(wù)側(cè)的代碼是什么樣的?
即服務(wù)層 switch case,根據(jù)調(diào)用方的類型走不通的業(yè)務(wù)邏輯代碼。我們做服務(wù)化其實(shí)是想把共性的部分抽象下沉,是共性的部分會(huì)做的服務(wù)。但如果解耦不徹底,就會(huì)有傳入不同 biz-type 執(zhí)行不同邏輯這樣的代碼。
這會(huì)出現(xiàn)什么問題呢?如果新增業(yè)務(wù)需求,你會(huì)發(fā)現(xiàn)很有可能要改代碼的是底層的服務(wù),比如說業(yè)務(wù) 1 來了一個(gè)需求,他過來找到你,說我這個(gè)需求有個(gè)擴(kuò)展,麻煩你這邊升級(jí)一下。
業(yè)務(wù) 2 和業(yè)務(wù) 3 相同,明明有需求的是業(yè)務(wù)方,為什么修改代碼的是我底層呢,業(yè)務(wù)需求方很多,所有業(yè)務(wù)需求側(cè)都是你來實(shí)現(xiàn),你是忙不過來的。這時(shí)你可能在心中罵他。
這個(gè)的耦合范圍相對(duì)較小,因?yàn)橹挥幸粋€(gè)基礎(chǔ)服務(wù)維護(hù)的痛點(diǎn)。解決方案也很容易想到,當(dāng)然是把業(yè)務(wù)個(gè)性化的 case 分支搬到上游去,底層只做通用的功能。
業(yè)務(wù)代碼上浮,這樣的話上游的業(yè)務(wù)迭代速度、迭代效率會(huì)提升,每塊業(yè)務(wù)有功能就會(huì)自己實(shí)現(xiàn)了,不需要兄弟部門去實(shí)現(xiàn),沒有一個(gè)溝通的過程。這是服務(wù)化不徹底的一個(gè)常見的耦合的案例。
消息通知耦合
第五個(gè)案例是消息通知的耦合。我猜應(yīng)該也有很多公司遇到過,有一些事件,這個(gè)事件可能要讓很多下游知曉,這里舉一個(gè)我們?cè)?jīng)出現(xiàn)過的案例。
58 同城發(fā)布帖子,發(fā)布帖子的這個(gè)事件可能要周知很多方,例如有一個(gè)用戶分級(jí)的服務(wù),他發(fā)了帖之后,這個(gè)用戶發(fā)帖的一些統(tǒng)計(jì)數(shù)據(jù),一些信息數(shù)據(jù)可能要進(jìn)行更新。
還要通知離線消息反作弊的部門在發(fā)布這個(gè)帖子之后,可能做一些離線的分析和處理,看有沒有反作弊的嫌疑。
甚至我們這個(gè)消息可能要通知業(yè)務(wù)線,比如說招聘業(yè)務(wù)線,最近做了一些營(yíng)銷活動(dòng),只要發(fā)招聘的帖子就給你獎(jiǎng)積分。
帖子發(fā)布服務(wù),這本來應(yīng)該是一個(gè)非?;A(chǔ)的服務(wù),它是否要承擔(dān)將帖子消息同步給通知關(guān)注方的職責(zé)呢?
最早我們是怎么實(shí)現(xiàn)的?58 同城都是服務(wù)化的架構(gòu),通過 RPC 告訴你發(fā)布一個(gè)帖子。
所以我們的上游是帖子發(fā)布的基礎(chǔ)服務(wù),他會(huì)通知反作弊的部門說發(fā)了一個(gè)帖子,會(huì)通知數(shù)據(jù)統(tǒng)計(jì)的部門發(fā)個(gè)帖子,會(huì)通知業(yè)務(wù)線說發(fā)個(gè)帖子,這樣的架構(gòu)其實(shí)是因?yàn)檫@個(gè)通知上下游耦合在了一起。
然后我們?cè)谑裁磿r(shí)候會(huì)偷偷地去罵這個(gè)下游呢?假設(shè)現(xiàn)在又新增了一個(gè)業(yè)務(wù)線,房產(chǎn)業(yè)務(wù)線也做營(yíng)銷活動(dòng),也要關(guān)注帖子發(fā)布,麻煩發(fā)布的兄弟能不能調(diào)用一下我。
發(fā)布的兄弟會(huì)發(fā)現(xiàn)改的是發(fā)布服務(wù)的代碼,他原來要調(diào) 123,他現(xiàn)在還要調(diào) 4,有人有新增的需求還要調(diào) 5。發(fā)布服務(wù)的工程師很痛苦,明明有需求的是業(yè)務(wù)方,但修改代碼的卻是我。
原因就是消息的上下游耦合在一起。非常常見的解耦方案是通過 MQ,這個(gè)案例里的 MQ 以及下一個(gè)案例里的配置中心是互聯(lián)網(wǎng)架構(gòu)中兩個(gè)非常常見的解耦工具。
MQ 能夠做到上下游物理上和邏輯上都解耦,增加 MQ 之后,首先上游互不知道彼此的存在,它當(dāng)然不會(huì)建立物理連接了,大家都與 MQ 建立物理連接,就是物理連接上解耦了。
邏輯上也解耦了,消息發(fā)布方甚至不用知道哪些下游訂閱了這個(gè)消息。新增消息的訂閱方只需要找 MQ 就行了,上游不需要關(guān)注。
所以 MQ 是一個(gè)非常常見的物理上解耦、邏輯上也解耦的利器。
下游擴(kuò)容耦合
第六個(gè)案例,我相信也幾乎是所有的公司都會(huì)遇到的一個(gè)案例,它和第一個(gè)案例很像,但又不一樣。
我們的第一個(gè)案例是說 IP 變化,上游調(diào)下游 IP 發(fā)生了變化,我們的建議是使用內(nèi)網(wǎng)域名,而不是 IP 來做配置,來做上下游的連接解耦。
擴(kuò)容換 IP 是一個(gè)場(chǎng)景,擴(kuò)容又是第二個(gè)場(chǎng)景。
現(xiàn)在有 service1、service2、Web1,底層的 service 是個(gè)集群,隨著業(yè)務(wù)、數(shù)據(jù)量、并發(fā)的增長(zhǎng),service 要擴(kuò)容了,我要新增兩個(gè)節(jié)點(diǎn)。
假設(shè)我要新增 IP4、IP5,你會(huì)發(fā)現(xiàn)案例一的場(chǎng)景又出現(xiàn)了,你得通知所有的上游麻煩幫忙增加兩 個(gè)IP,增加兩個(gè)內(nèi)網(wǎng)域名,因?yàn)槲覕U(kuò)容了。擴(kuò)容的明明是下游,但需要修改配置、需要重啟的是上游。
我們?cè)缙诘慕鉀Q方案是怎么樣的?我們對(duì)配置采用的是配置私藏的方式。
一般對(duì)于每個(gè)上游來說,都有個(gè)自己的配置文件,依賴于下游,這個(gè)配置文件會(huì)放在上游的配置文件里。
service2 一般有一個(gè)配置 conf,這個(gè)里面寫了依賴于內(nèi)網(wǎng)配置,內(nèi)網(wǎng)域名是 123,然后這個(gè)服務(wù)在啟動(dòng)時(shí)可能通過配置把這個(gè)連接建立上。
Web 也是一樣,它有一個(gè) Web1.conf,大家想想自己所服務(wù)的公司是不是這樣的。
它是一個(gè)數(shù)據(jù)的擴(kuò)散,本來數(shù)據(jù)在這一份,但是你會(huì)發(fā)現(xiàn)這個(gè)數(shù)據(jù)擴(kuò)散到不同的上游,每個(gè)人都存儲(chǔ)一份這樣的數(shù)據(jù),我這個(gè)數(shù)據(jù)要變動(dòng)時(shí)每個(gè)上游都需要變動(dòng)。
如果數(shù)據(jù)只存在一個(gè)地方,這一個(gè)地方變了就都變了,不用擔(dān)心數(shù)據(jù)的一致性。
如果你能夠知道上游是誰,通知你的上游去為用戶改善配置重啟還好,我們碰到的痛點(diǎn)是什么?
58 同城幾千號(hào)人,業(yè)務(wù)幾百個(gè),那么多,我不知道誰依賴了我,如果我能知道 123 依賴了我,那我就告訴你就行了。
現(xiàn)在我不知道誰依賴了我,因?yàn)槟氵B接我,你不需要經(jīng)過我的允許,你在手冊(cè)上看調(diào)用方式是什么就看懂了。我們會(huì)增加 IP,我怎么通知你?
剛剛說根本的原因其實(shí)是一份配置數(shù)據(jù)擴(kuò)散到了多個(gè)上游,那我們能不能將這個(gè)配置數(shù)據(jù)放在一個(gè)地方不擴(kuò)散,我改了這一個(gè)地方就都改了?
解決方案是配置中心,配置中心的細(xì)節(jié)我在這不展開講,網(wǎng)上可能也有一些公司的實(shí)踐,配置后臺(tái)、DB 存儲(chǔ)等。
配置中心是一個(gè)典型的邏輯上解耦、但物理上不解耦的一個(gè)架構(gòu)工具。我們的所有上游依賴于下游,還要建立物理的連接。
你引入配置中心之后,它不是通過私有的配置,也不是通過全局的配置文件去讀取下游的 IP,而是配置中心說我要訪問 user service。
配置中心告訴他 user service 的內(nèi)網(wǎng)域名是 123,service 的 1、2、3 還是按照內(nèi)網(wǎng)的 1、2、3,物理上還是連接 user service,所有的上游都按照這種方式讀取下游的配置。
在配置中心側(cè),他就能夠知道有哪些人連接了 user service,他在配置中心的后臺(tái)就可以配哪些人我設(shè)置多少的限流,然后將這個(gè)限流可以同步到調(diào)用方的客戶端,當(dāng)然也可以同步到服務(wù)端進(jìn)行雙向的保護(hù)。
如果 user service 進(jìn)行擴(kuò)容,比如我要增加幾個(gè)節(jié)點(diǎn),我增加了 4 和 5,那么我在配置后臺(tái)說增加了 4 和 5,后臺(tái)能夠知道哪些上游依賴了它,反向給后臺(tái)通知,就完全不需要上游去做了。
總結(jié)
解耦之后系統(tǒng)能夠更美好一點(diǎn),程序員心中能夠少一點(diǎn)怨氣,希望今天分享的主題及案例能夠幫助大家解決一些工作中的實(shí)際問題,謝謝大家。
沈劍,架構(gòu)師之路公眾號(hào)作者。曾任百度高級(jí)工程師、58 同城高級(jí)架構(gòu)師、58 同城技術(shù)委員會(huì)主席、58 同城 C2C 技術(shù)部負(fù)責(zé)人?,F(xiàn)任 58 到家技術(shù)委員會(huì)主席,高級(jí)技術(shù)總監(jiān),負(fù)責(zé) 58 速運(yùn)研發(fā)與管理工作。本質(zhì),技術(shù)人一枚。