前言
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,關於兩個方案的對比,我簡單羅列在了下表之中:
我覺得如果這個專案組只有一兩個人的時候,不會帶來協作的問題;上面的方案隨便哪一個都是不需要花太多時間做特殊討論,挑自己最熟悉最拿手的方案肯定不會有錯,所謂小團隊靠技術嗎,說的就是這麼個道理;我們當時是一個小團隊,同時團隊中也有同學對 submodule 處理過類似的情況,所以方案的選擇上就很自然了。
後來隨著時間的推移,團隊慢慢變大,就發現需要制定一些流程和和規範來約束一些行為,以此保障團隊的協作關係的時候;這時候發現之前靠一己之力打拼下來的地盤在多人寫作下變得脆弱不堪,尤其是另外一個 submodule 變成一個團隊進行維護的時候,submodule 的版本管理幾乎不可預期,而且他的介面變動和改動是完全不會理會被依賴方的感受的,因為他也不知道是否被依賴;久而久之,你就會明白什麼叫做你的專案被__腐化__了。簡單理解__腐化__這個詞就是,你已經開始害怕你所做的一切改動,因為你不知道你的改動是否會引來額外的麻煩。從這個角度也可以去理解為什麼一門語言設計出來為什麼要有 private、__public__這些表示範圍的修飾詞。正因為有這些詞的存在,才讓你的業務程式碼的高內聚成為的有可能,小到設計一個方法一個類、再引申到一個介面一個服務、再到一個系統一個倉庫,這個原則始終不變。
上述問題帶來的解法很簡單,就是變成顯示依賴的關係,所謂顯示依賴是指的兩個依賴之間是確定的。什麼是確定的?確定 == No Supprise !對,不管什麼時候,線上還是線下,我依賴你測試環境的介面返回是一個整數,到了線上,返回的也必須是一個整數、不能變成浮點數。而讓確定性變得可行的,不是君子協定;只能是一個版本依賴工具。比如說 Java 中的 Maven 正式的版本 依賴。
結語
職責內聚、依賴確定,是我們的應用變得真正 Cloud Native 的前提。沒有了這些基本的內功,懂的開源軟體再多、對微服務棧再熟悉,也會有各種意想不到的事情出來,試想一下,如果應用的職責到處分散,那到時候擴容到底擴誰呢?如果依賴方變得及其不確定,誰又來為每次發版的不確定的成本買單?Be Cloud Native,請從應用程式碼託管的住所開始。
歡迎關注“阿里巴巴中介軟體官方微博” ※一個集乾貨與前衛的技術號
歡迎關注“阿里巴巴中介軟體”官方公眾號,與技術同行