微服務化的基石——持續集成

 

本文由  網易雲 發佈。java

 

做者:劉超,網易雲解決方案架構師mysql

 

1、持續集成對於微服務的意義:拆以前要先解決合的問題

 

在不少微服務化的文章中,不多會把持續集成放在第一篇,由於大多數的文章都會將如何拆的問題,例如拆的粒度,拆的時機,拆的方式。程序員

爲何須要拆呢?由於這是人類處理問題的本質方式:將一個大的複雜問題,變成不少個小問題解決。redis

因此當一個系統複雜到必定程度,當維護一個系統的人數多到必定程度,解決問題的難度和溝通成本大大提升,於是須要拆成不少個工程,拆成不少個團隊,分而治之。spring

然而當每一個子團隊將子問題解決了,整個系統的問題就解決了麼?你能夠想象你將一輛整車拆成零件,而後再組裝起來的過程,你就能夠想象拆雖然不容易,合則更難,須要各類標準,各類流水線,才能將零件組裝稱爲車。sql

 

咱們先來回顧一下拆的過程。數據庫

 

最初的應用大多數是一個單體應用:編程

單體應用json

一個Java後端,後面跟一個數據庫,基本上就搞定了。後端

隨着系統複雜度的增長,首先Java程序須要作的是縱向的拆分。

縱向拆分

  1. 首先最外面是一個負載均衡,接着是接入的Nginx,作不一樣服務的路由。
  2. 不一樣的服務拆成獨立的進程,獨立部署,每一個服務使用本身的數據庫和緩存,解決數據庫和緩存的單點瓶頸。
  3. 數據庫使用一主多從的模式,進行讀寫分離,主要針對讀多寫少的場景。
  4. 爲了承載更多的請求,設置緩存層,將數據緩存到Memcached或者Redis中,增長命中率。
  5. 固然還有些跨服務的查詢,或者非結構化數據的查詢,引入搜索引擎,比關係型數據庫的查詢速度快不少。

                                                                                                                                服務化架構

 

在高併發狀況下,僅僅縱向拆分還不夠,於是須要作真正的服務化。

 

一個服務化的架構如圖所示。

  1. 首先是接入層,這一層主要實現API網關和動態資源和靜態資源的分離及緩存,而且能夠在這一層作整個系統的限流。
  2. 接下來是Web層,也就是controller,提供最外層的API,是對外提供服務的一層。
  3. 下面是組合服務層,有時候被稱爲編排層,compose層,是實現複雜邏輯的一層。
  4. 下面是基礎服務層,是提供原子性的基本的邏輯的一層,他下面是緩存,數據庫。
  5. 服務之間須要治理,須要相互發現,因此通常會有dubbo或者springcloud同樣的框架。
  6. 對全部的服務,都應該有監控告警,及時發現異常,並自動修復或者告警運維手動修復。
  7. 對於全部的服務的日誌,應該有相同的格式,收集到一塊兒,稱爲日誌中心,方便發現錯誤的時候,在統一的一個地方能夠debug。
  8. 對於全部的服務的配置,有統一的管理的地方,稱爲配置中心,能夠經過修改配置中心,下發配置,對於整個集羣進行配置的修改,例如打開熔斷或者降級開關等。

 

經過簡單的描述,你們能夠發現,從一個簡單的單體應用,變成如此複雜的微服務架構,除了關心怎麼拆的問題,還必須關注:

  • 如何控制拆的風險
  • 如何保證代碼質量
  • 如何保證功能不變,不引入新的Bug

 

答案固然就是集成,從一開始就集成,而且不斷的集成,反覆的將拆分的模塊從新組合,看看是否可以順利組合起來,而且保證功能的不變。

要是不沒事兒就組合一下,天知道幾個月之後還能不能合的起來。

別忘了程序是人寫的,你和你媳婦長時間不溝通都對不上默契,別說兩個程序員了。

 

2、持續集成就是不斷的嘗試在一塊兒

 

集成就是在一塊兒。

集成的邏輯

 

爲何須要一個統一的代碼倉庫Git來作代碼管理呢?是爲了代碼集成在一塊兒。

爲何須要進行構建build呢?就是代碼邏輯須要集成在一塊兒,編譯不出錯。

爲何要單元測試呢?一個模塊的功能集成在一塊兒可以正確工做。

爲何須要聯調測試Staging環境呢?須要將不一樣模塊之間集成在一塊兒,在一個類生產的環境中進行測試。最終纔是部署到生產環境中,將全部人分開作的工做纔算真正的合在了一塊兒。

持續集成解決的問題

 

持續集成就是制定一系列流程,或者一個系列規則,將須要在一塊兒的各個層次規範起來,方便你們在一塊兒,強迫你們在一塊兒。

 

3、持續集成,持續交付,持續部署,敏捷開發,DevOps都啥關係?

 

這些概念都容易混淆,他們之間是什麼關係呢?

持續

集成,持續交付,持續部署,敏捷開發,DevOps的關係

 

敏捷開發Agile是一種開發流程,是一種快速迭代的開發流程,每一個開發流程很是短,長到一個月,短到兩個星期,就會是一個週期,在這個週期中,天天都要開會同步,天天都要集成。正是由於週期短,才須要持續的作這件事情,若是一個開發週期長達幾個月,則不須要持續的集成,最後留幾個星期的集成時間一塊兒作也是能夠的,可是這樣就不能達到互聯網公司的快速迭代,也是咱們經常看到傳統公司的作法。

 

持續集成每每指對代碼的提交,構建,測試的過程,也就是上述的在一塊兒的過程。

 

持續交付是指將集成好的交付物,例如war,jar,或者容器鏡像,部署在聯調環境,或者預發環境的過程。

 

持續部署是指將交付物持續部署在生產環境的過程。

 

咱們常說CICD,CD有時候指的是Delivery交付,有的是指Deployment部署,對於非生產環境,自動部署是沒有問題的,對於生產環境,每每仍是須要有專人來進行更爲嚴肅的部署過程,不會徹底的自動化。

 

接下來就是DevOps,DevOps不僅是CICD,除了技術和流程,還包含文化。例如容器化帶來的一個巨大的轉變是,原來只有運維關心環境的部署,不管是測試環境,仍是生產環境,都是運維搞定的,而容器化以後,須要開發本身寫Dockerfile,本身關心環境的部署。由於微服務以後,模塊太多了,讓少數的運維可以很好的管理全部的服務,壓力大,易出錯,然而開發每每分紅不少的團隊,每一個模塊本身關心本身的部署,則不易出錯,這就須要運維一部分的工做讓研發來作,須要研發和運維的打通,若是公司沒有這個文化,研發的老大說咱們不寫Dockerfile,則DevOps是搞不定的。

 

4、從一個持續集成的平常,看上述的幾個概念如何實踐

                                                                                                                  持續集成的流程

 

這是一個持續集成的流程,可是運行起來更加的複雜。

 

首先,項目開發的流程使用的是Agile,用常見的Scrum爲例子。

Scrum

 

天天早上第一件事情,就是開站會standup meeting,爲何要站着呢?由於時間不能太長,微服務的一個模塊,大概須要5-9人的團隊規模,若是團隊規模太大了,說明服務應該進行拆分了,這個團隊規模,是可以保證比較短的時間以內過完昨天的狀態的。

 

必定要你們一塊兒開,而不要線下去更新Jira,雖然看起來同樣,可是執行起來徹底不同。只有你們一塊兒開,一塊兒看燃盡圖,一塊兒說我昨天作了什麼,今天打算作什麼,有什麼阻礙,纔可以讓你們都瞭解狀況,不要指望你們會去看別人的Jira,經驗告訴你,不會的。

 

並且這個站會對於開發是比較大的壓力,例如你的一個功能block了依賴方的開發,在會議上會暴露出來,你們都知道這件事情了,一天block,兩天block,第三天你都很差意思去說了,這會強迫你將大任務,好比原來寫1周幹一件什麼事情,寫成小時級別,這樣天天你都有的說,昨天完成了一個task,而不是周只在那裏說幹一樣一件事情,並且一旦有了block,team lead會知道這件事情,會幫你趕忙解決這個事情,推動整個項目的進展。讓一個技術人員在團隊面前認可這件事情我嘗試了幾天,的確搞不定了,也是一種壓力。

站會中的內容其實在前一天晚上就要開始準備了。

 

持續集成要求天天都提交代碼,這樣才能下降代碼集成的風險,不能埋頭寫一週一塊兒提交,這樣每每集成不成功。怎麼樣才能鼓勵天天都提交代碼呢?一個就是次日的站會,你這個功能代碼提交了,單元測試經過了,次日才能說作完了,不然不算,這就逼得你,將大任務拆成小任務,天天都屢次提交。

並且Git的提交方式,是後提交者有責任去merge,保證代碼的編譯經過和測試經過,你會發現,若是你不及時提交,等你改了一大片代碼,別人都提交完了,這一大片的衝突都是你來merge,測試用例不經過的你來fix,因此逼的你有一個小的功能的改動,就儘早提交,pull一下發現沒有人提交,趕忙提交。

提交不是立刻進入主庫,而是須要代碼審覈,這是把控代碼質量的重要的環節。

代碼質量的控制每每每一個公司都有文檔,甚至你能夠從網上下載一篇很長很長的Java代碼規範。可是咱們經常看到的例子是,規範是有,可是蝨子多了不咬人,規範太多的,誰也記不住,等於沒有規範。

 

因此建議將複雜的規範經過項目組內部的討論,簡化爲簡單的10幾條軍規,深刻人心,你們都容易記住,而且容器執行。

 

代碼審覈每每須要注意下面的幾方面:

  • 代碼結構:整個項目組應該規定統一的代碼組織結構,使得每一個開發拿到另外一我的的代碼,都能看的熟悉的面孔。這也是scrum中提倡的每一個開發之間是可替代的,當一個模塊有了阻礙,其餘人是能夠幫上忙的。至於核心的邏輯,估計審覈人員也來不及細看,這沒關係,核心邏輯是否經過,不能靠眼睛,要靠測試。
  • 有沒有註釋,尤爲是對外的接口,應該有完善的註釋,方便自動生成接口文檔。
  • 異常的處理,是否拋出太過寬泛的異常,是否吞掉異常,是否吞掉異常的日誌等。
  • 對於pom是否有修改,引入了新的jar。
  • 對於配置文件是否有修改,對外訪問是否設置超時
  • 對於數據庫是否有修改,是否通過DBA審覈
  • 接口實現是否冪等,由於Dubbo和springcloud都會重試接口。接口是否會升級,是否帶版本號
  • 是否有單元測試

 

固然還有一些不容易一眼看出來的,能夠經過一段時間經過統一的代碼review,來修改這些問題:

  • 某個類代碼長度過長
  • 設計是否合理,高內聚低耦合
  • 數據庫設計是否合理
  • 數據庫事務是否使用合理
  • 代碼是否有明顯的阻塞

 

代碼審覈完畢以後提交上去以後,一個是要經過靜態代碼審查,能夠發現一些可能帶來代碼風險的問題,例如異常過於寬泛等。

在就是要經過單元測試。咱們應該要求每一個類都要有單元測試,而且單元測試覆蓋率要達到必定的指標。單元測試要有帶Mock的模塊內的集成測試。

在編譯過程當中會觸發單元測試,單元測試不經過,已經代碼覆蓋率,都會統計後發郵件,抄送全部的人,這對於研發來說又是一個壓力。

當有一天你的提交break掉了測試,或者代碼覆蓋率很低,則就像通報批評同樣,你須要趕忙去修改。

 

單元測試完畢以後,就會上傳成果物,或者是war或者是jar,通常會用nexus,由於有版本號,有md5,能夠保證安裝在環境中的就是某個版本的某個包,咱們還遇到過有使用FTP的,這樣一個是很難保證版本號的維護,升級和回滾比較難弄,另外一個是沒有md5,極可能包不完整都有可能的,並且一旦發生,很難發現。

若是使用了容器,則還須要編譯Dockerfile,使用Docker鏡像做爲交付,可以實現更好的環境一致性,保證原子的升級和回滾。

天天下班前,當天的代碼須要提交到庫中去,晚上會作一次統一的環境部署和集成測試。

 

天天晚上凌晨,會有自動化的腳本將Docker鏡像經過編排部署一個完整的環境,而後跑集成測試用例,集成測試用例應該是基於API的,不少的公司是基於UI的,這樣因爲UI變化太快,還有UI不能覆蓋全部的場景,因此仍是建議UI和API分離,經過API進行集成測試,有了天天的測試,才能保證天天晚上的版本都是能夠交付的版本,也保證咱們微服務拆分的時候,儘管改了不少,不會由於新的修改,破壞掉原來可以經過的測試用例,保證不會有了新的,壞了舊的。

這個集成測試或者叫回歸測試天天晚上都作,都是在一個全新的環境中,這就是持續部署和持續交付。

 

若是某一天測試不經過,則會發出郵件來,是由於當天誰的哪一個提交,致使測試不經過,抄送全部人,這是另外一個壓力。

因此次日的站會上,昨天你完成了哪些功能,是否提交了,是否完成了單元測試,是否經過了集成測試,就都知道了,你須要給你們一個解釋,而後進入到新一天的開發。

到了兩週,一個週期完畢,能夠上線到生產環境了,能夠通知有權限的運維進行操做,可是也是經過自動化的腳本進行部署的。

這就是整個過程,層層保證質量,從中能夠看到,敏捷開發,持續集成,持續交付,持續部署,DevOps是互相聯繫的,少了任何一個,整個流程都玩不轉。

 

5、有關代碼結構

 

代碼結構每每包括:

  • API接口包
  • 訪問外部服務包
  • 數據庫DTO
  • 訪問數據庫包
  • 服務與商務邏輯
  • 外部服務

 

若是使用Dubbo RPC,則API接口每每在一個單獨的jar裏面,被服務端和客戶端共同依賴,可是使用了springcloud的restful方式就不用了,只要在各自的代碼裏面定義就能夠了,會變成json的方式傳遞,這樣的好處是當jar有多個版本依賴,須要升級的時候,關係很是複雜,難以維護,而json的方式比較好的解決了這個問題。

 

這個模塊提供了哪些接口,只要到API接口這個package下面找就能夠了。由於不管是Dubbo仍是springcloud,接口的調用都會重試,於是接口須要實現冪等。

 

訪問外部服務的包,這將全部對外的訪問獨立出來,好處一是能夠抽象出來,在服務拆分的時候,可能會用到,例如原來支付的邏輯在下單的模塊中,要講支付獨立出來,則會有一個抽象層,涉及到老的支付方式,仍是調用本模塊中的邏輯,涉及到新接入的支付方式使用遠程調用,有了這一層方便的多。好處二是能夠實現熔斷,當被調用的服務不正常的時候,在這裏能夠返回託底數據。好處三是能夠實現Mock,這樣對於單元測試來說很是好,不用依賴於其餘服務,就能夠本身進行測試。

 

DTO和訪問數據庫的包,看到了這些數據結構,會幫助程序員快速掌握代碼邏輯,不知道你們有沒有這個體驗,你去看一個開源軟件的代碼,首先要看的是他的數據結構,數據結構和關係看懂了,代碼邏輯就比較容易懂了,若是數據結構沒看懂,則光看邏輯,就容易雲裏霧裏的。

 

還有就是核心的代碼邏輯和對接口的實現。在這裏面是軟件代碼設計的內功所在,可是卻不是流程可以控制的。

 

6、有關接口設計規範

 

上面也說過了,Dubbo和Springcloud會對接口進行重試,於是接口須要保持冪等。也即屢次調用,應該產生一致的結果,例如轉帳1元,由於調用失敗或者超時重試的時候,最終結果還應該是轉帳1元,而非調用兩次變成轉帳2元。

  • 冪等判斷儘可能提早,可使用ID做爲判斷條件。
  • 接口的實現應該儘可能避免阻塞,可使用異步方式提高性能。
  • 接口應該包括可以區分不一樣狀況的異常,而非拋出寬泛的Exception,不能吞掉異常。
  • 接口的實現要有足夠的容錯性,以及對不一樣版本的兼容性。當要引入新接口的時候,使用先添加,後刪除的方式。
  • 接口應該有良好的註釋。

 

7、有關代碼設計

 

對於代碼的設計,這裏常說的就是SOLID原則

  • S是單一責任原則,若是你的代碼中有一個類行數太長,可能你須要從新審視一下,是否是這個類承擔了過多的責任。
  • O是開放關閉原則,比較拗口,對擴展開放,對修改關閉。思想是對於代碼的直接修改是很是危險的事情,由於你不知道這段代碼原來被誰用了,並且當時候用的時候,面臨的狀況都是怎樣的。於是不要貿然修改一段代碼,而是選擇用接口進行調用,用實現進行擴展的方式進行。當你要實現一段新的功能的時候,不要改原來的代碼,也不要if-else,而是應該擴展一種實現,讓原來的調用的代碼邏輯仍是原來的,在新的狀況下使用新實現的代碼邏輯。
  • L是里氏替換原則,若是基於接口進行編程,則子類必定要可以擴展父類的功能,若是不能,說明不該該繼承與這個接口。例如你的實現的時候,發現接口中有一個方法在你這裏實在對應不到實現,不是接口設計的問題,就是你不該該繼承這個接口,毫不能出現not implemented相似之類的實現方法。
  • I是接口隔離原則,接口不該該設計的大而全,一個接口暴露出全部的功能,從而使得客戶端依賴了本身不須要的接口或者接口的方法。而是應該講接口進行細分和提取,而不該該將太過靈活的參數和變量混雜在一個接口中。
  • D是依賴倒置原則,A模塊依賴於B模塊,B模塊有了修改,反而要改A,就是依賴的過於緊密的問題。這就是常說的,你變了,我沒變,爲啥我要改。若是基於抽象的接口編程,將修改隱藏在後面,則可以實現依賴的解耦。

 

以上是模塊內部常見的設計原則,對於模塊之間,則是對於雲原生應用常說的十二要素原則。

十二要素原則

詳情可雲原生時代下的12-factor應用與實踐

 

8、有關配置文件

 

在代碼倉庫中,還須要管理的是配置文件,每每在src/main/resource下面。

配置的管理原來多使用profile進行管理,對於dev, test, production使用不一樣的配置文件。

然而當配置很是多的時候,比較的痛苦,並且配置不斷的修改,每次上線各類配置須要仔細的核對,眼睛都花了,纔敢上線。

咱們能夠將配置分爲下面的三類:

  • 內部配置項(啓動後不變,改變須要重啓)
  • 集中配置項(配置中心,可動態下發)
  • 外部配置項(外部依賴,和環境相關)

在梳理配置的時候,能夠按着三類歸類,分門別類管理。

在使用了容器以後,不少的內部配置項可固化在配置文件中,放在容器鏡像中,須要啓動的時候修改的,則經過環境變量,在啓動容器的時候,在編排文件中進行修改。

依賴的內部服務的地址,在容器平臺kubernetes裏面,能夠經過配置服務名進行服務發現,僅僅在配置文件中配置名稱就能夠了,不用配置真實的地址,kubernetes能夠根據不一樣的環境,不一樣的namespace自動關聯好,大大簡化了配置。固然也能夠用服務中心Dubbo和Springcloud作內部服務的相互發現。

依賴的外部服務的地址,例如mysql,redis等,每每不一樣的環境不一樣,也能夠經過配置kubernetes外部服務名的方式進行,而不用一一覈對,擔憂測試環境連上了生產環境的IP地址。

還有一些集中配置項,須要動態修改的,例如限流,降級的開關等,須要經過統一的配置中心進行管理。

 

9、有關數據庫版本

 

代碼能夠很好的版本化,應用也能夠用鏡像進行原子化的升級和回滾。

惟一比較難作到的就是數據庫如何版本化管理。

有一個開源工具 flyway 能夠比較好的作這件事情。

在代碼中,flyway須要有如下的結構:

  • 在src/db/migration中有sql文件,命名規則,如:V1__2017_4_13.sql ,V開頭+版本號+雙下劃線+描述,後綴爲sql
  • 增長flyway的java類,實現migration方法

在數據庫中,flyway會自動增長SCHEME_VERSION表。

當服務啓動的時候,java類的migration方法會被調用,它會按照指定路徑中sql語句的版本號進行排序而且按照這個排序去執行,當每個sql文件被執行後,元數據的表就會按照格式進行更新。

當服務重啓的時候,Flyway 再次掃描sql的時候,它就會檢查元數據表中遷移版本,若是要執行的遷移腳本的版本小於或者等於當前版本,Flyway將會忽略,再也不重複執行。

 

可是flyway歷來不解決數據庫升級和回滾的代碼兼容性問題。

 

太多的人問這個問題了,代碼能夠灰度發佈,數據庫咋灰度?代碼升級了,發現不對能夠回滾,數據庫咋回滾。

 

若是能夠停服的話,天然是使用數據庫快照備份的方式進行回滾了。

 

若是不能夠停服,沒辦法,只有在代碼層面作兼容性。每次涉及數據庫升級的都是大事情,代碼固然應該有個開關,保證隨時能夠切回原來的邏輯。

 

 瞭解網易雲:

網易雲官網:https://www.163yun.com/

新用戶大禮包:https://www.163yun.com/gift

網易雲社區:https://sq.163yun.com/

相關文章
相關標籤/搜索