1. 程式人生 > >微服務浪潮中,程序猿如何讓自己 Be Cloud Native

微服務浪潮中,程序猿如何讓自己 Be Cloud Native

vat 上線 完全 做的 一個人 意圖 寫作 後來 指定

前言
CNCF 與 Cloud Native 這兩個技術詞匯最近頻頻走進了程序員的視野,一切和他能搭上邊的軟件意味著標準、開放、時尚,也更能俘獲技術哥哥們的心;這篇文章不想去帶大家重溫這個詞匯後面的軟件體系,筆者覺得單憑用到了這些開源軟件,不等於我們自己的軟件就已經是 Cloud Native,在使用啞鈴和成為肌肉男之間還隔著科學使用和自律鍛煉兩道工序;在此,筆者想根跟大家聊聊讓我們的應用真正變得 Cloud Native 時的理論依據:微服務的十二要素。這篇文章也是先從作者自身項目的角度(一個基於 EDAS 的微服務架構),來闡述對這十二條要素的前兩條 —— 倉庫(Code Base)與依賴(Dependency)的理解

Code Base 的原文釋義是:"一份基準代碼,多份部署,基準代碼和應用之間總是保持一一對應的關系;不同環境中的相同應用,應該來源於同一份代碼"。我的理解有兩個:

一個應用,產生自同一個倉庫。
一個倉庫,只產生一個應用。
為什麽推演出這麽兩個結論呢?讓我們先看一個實際的項目。

為什麽是一個應用?
? 給大家舉一個一個倉庫包含多個應用的反例,筆者自己的一個項目是一個的微服務的架構,和大部分的微服務架構一樣,一開始是由一個單體的應用拆解而來,拆解之後,大致簡化成四個服務:微服務網關(Gateway),兩個後臺服務(UserService, OrderService),後臺管理控制臺服務(Admin),簡單的架構示意圖如下:

技術分享圖片

? 在拆分的過程一開始為了項目上線的減少風險,將拆分之後的應用都放在了一個 GIT 倉庫中進行管理,同時也共用了同一個庫。重構之後倉庫的目錄如下:

~/workspace/java/app/ $ tree -L 2
.
├── README.md
├── service-api # 通用的 API 接口定義
│ ├── userservice-api # 服務 UserService 的聲明
│ ├── orderservice-api # 服務 OrderService 的聲明
│ ├── rpc-api # 遠程服務調用相關的接口聲明
│ ├── common-api # UserService 與 OrderService 都依賴的聲明

| .....
├── service-impl # 對應 API 的相關具體業務實現
│ ├── userservice-impl
│ ├── orderservice-impl
│ ├── common-impl
| .....
├── web-app # Web 應用工程
│ ├── admin
│ ├── userservice
│ ├── orderservice
│ ├── gateway
? 一開始這些服務之間的發布和改動彼此都不受影響,這一過程持續了大約兩個叠代,隨著叠代的不斷進行和新人的加入,後來我們線上發現一個很奇怪的現象,每次用戶進入刷新訂單的地址列表的時候,會伴隨這一次用戶 Token 的刷新而導致用戶被踢出,線上的排查過程在 EDAS 的分布式鏈路跟蹤系統 EagleEye 的幫助下,馬上就定位到了出問題的代碼:

// User Service 中
public class User {
public void refresh() {
// 刷新登錄 token
}
}

// Order Service 中
public class OrderUser extends User {
// 函數少了一個字母,導致 refresh 調用了父類的 refresh
public void refesh() {
// 刷新地址列表
}
}
?這個故障,我先邀請大家一起思考一下幾個問題:

從編碼角度,如何避免上述重寫的方法因為名字誤寫造成故障?
從設計角度,OrderUser 和 User,是否是繼承關系?
這個問題的根因是什麽?
以上的幾個問題中,第一個問題的答案,很多同學都知道,就是使用 Java 自帶的 Annotation @Override,他會自動強制去檢查所修飾的方法簽名在父子類中是否一致。第二個問題,需要從領域邊界來說,這是一個典型的邊界劃分的問題,即:訂單中的用戶,和會員登錄中的用戶,是不是相同的“用戶”?會員中的用戶,其實只需要關心用戶名密碼,其他都是這個用戶的屬性;而訂單中的用戶,最重要的肯定是聯系方式,即一個聯系方式,確定一個人。雖然他們都叫做用戶,但是在彼此的上下文中,肯定是不一樣的概念。所以這裏的 OrderUser 和 User 是不能用繼承關系的,因為他們就不是一個 "IS A" 的關系。
倉庫共享,加上沒有多加思考的模型,導致依賴混亂;如果兩個 User 對象之間代碼上能做到隔離,不是那麽輕易的產生“關系”,這一切或許可以避免。

為什麽是一個倉庫?
嚴格意義上說,一個應用的所有代碼都肯定來源於不同的倉庫?我們所依賴的三方庫如(fastjson, edas-sdk 等)肯定是來源於其他的倉庫;這些類庫是有確切的名稱和版本號,且已經構建好的"制品",這裏所說的一個倉庫,是指源碼級別的“在制品”。可能在很多的項目中不會存在這樣的情況,以 GIT 為例,他一般發生在 submodule 為組織結構的工程中,場景一般是啥呢?在我們這個工程中確實是有一個這樣的例子:

為了解掉第一個問題,我們決定拆倉庫,倉庫的粒度按照應用粒度分,同時把 common 相關的都拆到一個叫做 common 倉庫中去;業務服務都好說,這裏特殊處理的是 admin 應用,admin 是一個後臺管理應用,變化頻度特別大,需要依賴 UserService 和 OrderService 一大堆的接口。關於和其他倉庫接口依賴的處理,這裏除了常見的 Maven 依賴方式之外,還有另外一個解決方案就是 git submodule,關於兩個方案的對比,我簡單羅列在了下表之中:

優點 缺點
Maven 依賴 可指定已固化的版本進行依賴 必須發布成二方包
Submodule 依賴 靈活、可直接共享代碼庫 變更不可控
我覺得如果這個項目組只有一兩個人的時候,不會帶來協作的問題;上面的方案隨便哪一個都是不需要花太多時間做特殊討論,挑自己最熟悉最拿手的方案肯定不會有錯,所謂小團隊靠技術嗎,說的就是這麽個道理;我們當時是一個小團隊,同時團隊中也有同學對 submodule 處理過類似的情況,所以方案的選擇上就很自然了。

後來隨著時間的推移,團隊慢慢變大,就發現需要制定一些流程和和規範來約束一些行為,以此保障團隊的協作關系的時候;這時候發現之前靠一己之力打拼下來的地盤在多人寫作下變得脆弱不堪,尤其是另外一個 submodule 變成一個團隊進行維護的時候,submodule 的版本管理幾乎不可預期,而且他的接口變動和改動是完全不會理會被依賴方的感受的,因為他也不知道是否被依賴;久而久之,你就會明白什麽叫做你的項目被腐化了。簡單理解腐化這個詞就是,你已經開始害怕你所做的一切改動,因為你不知道你的改動是否會引來額外的麻煩。從這個角度也可以去理解為什麽一門語言設計出來為什麽要有 privatepublic這些表示範圍的修飾詞。正因為有這些詞的存在,才讓你的業務代碼的高內聚成為的有可能,小到設計一個方法一個類、再引申到一個接口一個服務、再到一個系統一個倉庫,這個原則始終不變。

上述問題帶來的解法很簡單,就是變成顯示依賴的關系,所謂顯示依賴是指的兩個依賴之間是確定的。什麽是確定的?確定 == No Supprise !對,不管什麽時候,線上還是線下,我依賴你測試環境的接口返回是一個整數,到了線上,返回的也必須是一個整數、不能變成浮點數。而讓確定性變得可行的,不是君子協定;只能是一個版本依賴工具。比如說 Java 中的 Maven 正式的版本依賴。

結語
職責內聚、依賴確定,是我們的應用變得真正 Cloud Native 的前提。沒有了這些基本的內功,懂的開源軟件再多、對微服務棧再熟悉,也會有各種意想不到的事情出來,試想一下,如果應用的職責到處分散,那到時候擴容到底擴誰呢?如果依賴方變得及其不確定,誰又來為每次發版的不確定的成本買單?Be Cloud Native,請從應用代碼托管的住所開始。

微服務浪潮中,程序猿如何讓自己 Be Cloud Native