1. 程式人生 > >阿里技術專家:持續交付與微服務背後的實踐邏輯

阿里技術專家:持續交付與微服務背後的實踐邏輯

崔力強

崔力強

阿里巴巴技術專家

  • 《微服務設計》中文譯者之一;曾在ThoughtWorks任職軟體交付和敏捷顧問;
  • 對持續整合、自動化測試有豐富經驗;目前專注於持續交付SaaS產品的開發,提供精益需求管理、軟體設計、敏捷轉型相關諮詢服務。

前言

大家好,我是崔力強。目前在阿里巴巴任職。負責一款持續交付領域的SaaS產品的開發。非常高興能夠和大家分享持續交付和微服務的話題。

本次分享的重點是持續交付。也會提到一些微服務的概念,以及持續交付和微服務之間的關係。今天會涉及的一些實踐可能大家或多或少有所耳聞。我會著重講述這些實踐背後的邏輯,及它們之間的關係。

先看一看提綱:

  • 持續交付的概念
  • 持續交付會遇到的問題及解決方案
  • 微服務與持續交付的相互作用

持續交付的概念

關於持續交付的概念。從《持續交付》這本書的副標題可見一斑:“釋出可靠軟體的系統方法”。可以看到這本書中講的“持續交付”主要是技術相關的實踐,雖然近年有些朋友把持續交付的概念進行了延伸,把精益需求管理和精益創業也包含了進來,不過今天我還是會按照它最初的內涵,只講技術相關的實踐。

這裡其實有兩個概念,第一個是,怎麼樣才算持續交付,也就是目標;第二個是,如何做到持續交付,也就是一些技術實踐。

為了瞭解“怎麼樣才算持續交付”,讓我們先解剖一下軟體開發的過程。

針對單個需求來說,我們會先進行需求分析、細化。然後開發和測試。部署時,需要拷貝一些檔案到一些機器,執行一些指令碼,有時候還需要改一些配置檔案或者做一些資料遷移等。

持續交付

然而在實際的專案中,肯定不會只有一個需求。需求會源源不斷地輸入到開發團隊。下面是一個開發團隊隨著時間進行需求開發的示意圖。藍色的條是開發某個功能的開始時間和結束時間點。有的需求佔用時間長,有的需求佔用時間短。

微服務

在每個功能的完成時刻,對於業務方來說,都可能是一個可以釋出的狀態(也有可能不是,比如業務方認為相關的幾個需求必須都做完了才能一起上,或者需要等到某個特定的時間點才能上)。那麼如果立馬就能夠進行一次釋出,並且能夠快速並且安全的完成這次釋出,則能夠對業務的發展具有非常積極的作用。也就是在下圖的這些時刻。

持續交付

這些時刻,不一定每個都需要釋出。但作為開發團隊,要給業務方足夠的靈活度。所以持續交付的目標,並不是每次提交都進行釋出,而是每次提交都是可釋出的狀態。這就回答了“怎麼樣才算持續交付”這個問題。再換一種方式來說,持續交付是對業務方友好的一種,開發團隊的開發節奏。

接下來就要討論使用什麼樣的技術實踐來達到這種開發節奏。為了討論具體的技術實踐,首先來看看在軟體開發中有什麼因素會阻礙我們達到這種節奏。

持續交付會遇到的問題及解決方案

測試

第一個問題是:上線前總還是要做測試的吧,至少做一遍重要功能的迴歸測試(手工迴歸本身其實也是有章可循的,推薦我同事的一篇文章:https://yq.aliyun.com/articles/6898)。隨著一個系統上的功能越來越多,每次上線前需要測試的東西就越來越多。慢慢的,釋出就慢下來了。我曾經工作過的一個系統,上面承載了三塊業務,有將近40多個人在上面工作。每次釋出前都要先進行幾天的迴歸測試,順利的話,從確定要釋出的版本到驗證完該版本,確定可以釋出,也要差不多一週的時間。

另一個我工作過的專案規模要小一些,做一次釋出也需要一天的時間來回歸,通常都會發現一些問題。順利的話,可以在當天把所有的問題修復掉,進行釋出。不過因為bug沒修完而不能當天釋出,拖到第二天的這種情況也時常發生。

甚至有一次,到了晚上九點多的時候還沒有修完bug,但當時大家都憋著一口氣,硬是要發,結果發到了晚上一點多。

因為成本高,所以不敢做的太頻繁;做的不頻繁的話,就會在釋出中積累更多的功能,從而進一步增加出問題的可能性,從而形成一個惡性迴圈。

作為一個妥協,人們不得不使用瀑布或者迭代內小瀑布的開發模式。

迭代內小瀑布

把開發的週期分解為一個一個的迭代,比如兩週到四周的時間。在迭代開始前,保證該迭代內計劃的需求分析完畢。然後在迭代內部開發。順利的話,會在接近迭代末尾時完成迭代內計劃的所有任務,然後拉一個釋出分支出來,開始測試,然後釋出。

做出上述妥協的直接原因就是“測試和部署”花費的時間過長。如果只花費一個人一個小時就能夠完成迴歸和釋出,那顯然團隊就更願意去頻繁的釋出。

一個行之有效的方法就是進行自動化測試

測試金字塔

自動化測試大致可以分為幾種:單元測試、API測試、驗收測試/功能測試/端到端測試。在不同的技術棧下,分類可能會略有不同的,但本質上來講是類似的。不同層次的測試有自己的側重點,組要組合使用來達到一個比較好的效果。如上圖所示:

這裡以Java Spring專案為例來列舉不同層次的測試工具:單元測試使用Junit;整合測試使用Spring Test + Junit;功能測試使用cucumber+ capybara+selinium或者robotframework+ selinium。

如果使用了前端框架,比如Angular、ReactJS等,它們本身也提供了相應的測試框架。

底層的單元測試,測試的範圍較小,一般只涉及一個或者幾個類,不會呼叫網路或者資料庫。所以編寫和執行起來都比較快。在這個級別應該覆蓋儘量多的分支和邏輯。這個級別的測試能夠達到比較高的覆蓋率,所以在它的保護下,可以放心大膽的做重構或者新增新程式碼,只需要花上幾秒鐘的時間執行一遍單元測試,就能夠知道這次修改是否引入了問題。前陣子做了很長時間的nodejs開發,單元測試對這種弱型別的語言尤其重要。因為像變數未定義,傳引數的個數錯誤等很低階的問題IDE都無法給出有效的提示。

因為單元測試需要隔離被測類和系統的其它程式碼,所以需要有一些測試替身來代替真實的類。有很多工具可以做這樣的事情,比如Java中的mockito。

單元測試與測試替身

如上圖所示,它會創造一個假的ClassB的例項出來,並傳給ClassA的例項。然後對ClassB的行為做出一些假設,在此假設的基礎上對ClassA的行為進行測試。

但是一個類或者幾個類的正確,並不能讓你對系統的正確性有足夠的信心。因為單元測試中充滿了對其它類行為的假設。所以一旦這個假設錯誤,就會出現測試依然能通過,但整個系統的行為已經錯了的尷尬情況。所以我們還需要覆蓋面更大的整合測試。這種測試在服務內部不使用任何的測試替身。但對外部的服務進行打樁。對基於HTTP的服務進行打樁的工具包括moco(https://github.com/dreamhead/moco)和pact(https://github.com/realestate-com-au/pact,其實pact能做的事情不止打樁,更多的是做“契約測試”)。這種測試更真實,但執行起來會慢,所以這個層面的測試主要保證的是連通性。不需要100%的覆蓋率。

整合測試

整合測試覆蓋面很大,但它仍然是白盒測試,因為它直接呼叫了函式(比如上頁的controller)。如果這個服務只提供API,那麼這種測試就夠了。但如果這個服務是提供頁面的,也就是一個web應用,那麼就還需要一層直接操作網頁來進行基於使用者行為的測試,我們一般稱之為驗收測試,或者功能測試。上面列舉的cucumber和robotframework是非常流行的兩款功能測試工具。他們是通用的測試框架,與具體的被測系統是無關的。我也另一位前同事都對這兩種工具比較熟悉,並且寫了文章作總結:http://www.infoq.com/cn/articles/cucumber-robotframework-comparison

如果要測試web系統的話,就需要能夠驅動網頁的驅動程式。現在非常主流的驅動是selinium(https://github.com/SeleniumHQ/selenium

),當然我更喜歡的是在其上包了一層的capybara(https://github.com/jnicklas/capybara),它是用ruby編寫的,封裝的API更好用。

建議至少對核心的流程編寫功能測試,以保證上線不要出現嚴重的故障。前段時間我們的功能測試發現了一個bug。這個bug對於老使用者都不會有問題,但是使用者首次登入就會500。而使用者首次登入的場景恰恰是平時自測的時候很容易忽略的,因為準備資料還是有點麻煩的。當時發現這個問題的時候大家並沒有什麼感覺,因為已經習慣有測試保護的軟體了。但如果跳出來想想,這個問題要是在一週後的“迭代末尾”才發現,會多麼的打擊氣勢。如果上線才發現,那麼產品的新使用者增長量一定會直線下降。

端到端測試其實也就是使用功能測試的工具在更大的範圍進行測試,也就是包含所有的服務。

下面總結一下各個層次測試的特點。

測試金字塔

迴歸測試是迭代開發中必不可少的一個步驟。我們能做的就是通過自動化測試去儘量減小這個時間。

很多人對於測試有一些顧慮,覺得會花費很多時間。而且當代碼結構調整時,測試也要跟著改。

我的看法是,測試程式碼也是程式碼,維護測試程式碼的代價跟測試程式碼本身的質量是直接相關的。所以對測試程式碼也需要及時重構,提高可維護和可重用性。具體一點對於測試來說,提高可維護性和看重用性,無非就是要在資料準備、斷言工具方面去抽取一些庫。比如Ruby的factory girl就是一個極好的基於ActiveRecord的資料準備庫。有了它,寫測試的代價大大降低。那如果你不用Ruby怎麼辦,那隻好自己實現一個其它語言版本的factory girl嘍。我上一個專案用的是nodejs,就寫了一個nodejs版本的factory girl。

而單元測試能夠給你帶來的好處不僅僅是迴歸這麼簡單。有了完備的單元測試,你才有信心,有動力去做一些重構。只要測試通過,我就知道我的重構是正確的,你才敢不斷的去重構,優化程式碼,才能使得程式碼更易維護。所以可以說寫測試是保證你程式碼可維護性的必由之路。不要考慮寫不寫測試,而是考慮,如何低成本的寫測試。

當我開發新功能時候,編寫好測試執行一下,就知道功能正確與否,這樣就不用把伺服器啟起來,減小反饋的週期。在這個場景下,它會直接節省你的時間,雖然你寫了更多的程式碼。

關於程式碼改動,測試也要跟著改的問題,我想說兩點:

第一,專案的初期,一般程式碼的架構會不太穩定,那麼在不影響執行和編寫速度的前提下,儘量測試的範圍大一些,也就是包含的類多一些。這樣改動一個類,引起測試變化的可能性就會降低,並且可以在測試的保護下放心大膽的做架構調整。

第二,要善用IDE幫你做改動。測試程式碼也是程式碼,當你修改一個函式簽名時,IDE會幫你把所有的呼叫處都改掉,包括測試程式碼。所以IDE用得好,修改程式碼也不是那麼痛苦的事情。

那麼有了這些測試之後,我應該什麼時候執行它們呢。是迭代結束時嗎?不!我們應該在每次提交時都完整的執行一遍這些測試。這樣一旦出了問題我就可以第一時間知道。這就是持續整合的基本概念。

持續整合

每次提交程式碼觸發編譯、測試、靜態檢查、打包歸檔、然後再執行驗收測試(AT),然後再部署到類生產環境進行效能測試,再部署到端到端測試環境執行端到端測試。並且把每一步的結果反饋給開發團隊。

我們把上圖稱為持續整合流水線。可以使用很多工具來實現,比如最常見的開源工具Jenkins。或者我目前所負責產品:crp.aliyun.com。

關於更多的持續整合的實踐和流水線設計,因為內容很多,這裡只討論幾個要點。

  1. 程式碼倉庫: 每個開發要及時的提交程式碼,不要把程式碼長期留在本地,一天至少提交一次。不要開長時間的分支。越頻繁的提交程式碼,就能得到越及時的反饋。
  2. 構建指令碼: 構建指令碼必須要放在程式碼庫,切忌把它們放在只要少數人能夠訪問的神祕的地方。
  3. 軟體包伺服器: 每次構建的產出物,必須要按照一定的版本規則存放起來,以供後續的步驟使用,比如做測試和部署。軟體包的形式是多種多樣的,比如Java的jar、war,Ruby的gem,作業系統的rpm等等。甚至是最通用的tar,也可以成為你的軟體包形態。
  4. 分stage: 大家可以看到上面這個流水線是分stage的,每個stage是順序執行的。越往前的 stage檢測越快,並越簡單。越往後的stage檢測越耗時。任何一個stage執行失敗,後續的stage都不會再繼續進行,本次流水線的進行就失敗了。所以流水線執行到越往後的階段,我們對於本次構建是可以上線的信心就越強。

我們使用自動化測試加持續整合解決了第一個釋出前回歸測試耗時的問題。

持續整合

第二個問題就是:“別人的功能還沒做完”。假設現在團隊正在進行單分支開發(也就是說所有的功能都提交在一個分支上,不會為了一個功能單獨 開出一個長期存在分支)。就拿圖中紅色線這個時間點來講,第三個需求完成了,業務人員也認為可以做一次釋出。但是同時還有另外三個需求正在開發。如果做釋出的話,就會把做了一半的東西也給發上去。這是不可以接受的。

解決這個問題有兩個思路:那就是功能分支和功能開關。

微服務

先看看功能分支:

每開一個新功能,就開一個分支。這個分支存活的時間通常是“周”這個數量級的。哪個功能開發完成了就合入到主幹,進行一次釋出。這樣,其它未完成的功能還沒有合入到主幹,就不會造成影響。但功能分支有很多的問題,最嚴重的一個問題是:它和持續整合的理念是衝突的。持續整合是希望你每次提交都能夠放在一起進行驗證。但使用了分支的話,就只能在合併的時刻,才能真正把所有的東西放在一起進行驗證。而這時發現的問題可能一週前已經發生了。

另一個方法,功能開關,會給任何一個新開發的功能在程式碼級別加上一個開關,使得可以簡單的修改一個配置就把一個功能完全隱藏掉。預設所有的開關都是關閉的,如果一個功能做完了,想上,則修改配置,開啟開關,進行一次釋出即可。聽起來很理想,但事實上也需要花費不少的程式碼來把這件事情真正做好。

關於功能分支和功能開關今天不展開細講了。有興趣的朋友可以參看我之前寫的一篇文章:http://www.infoq.com/cn/articles/function-switch-realize-better-continuous-implementations

接下來我們聊一聊釋出。因為我們希望釋出也是快速並安全可靠的。

釋出

釋出是一件麻煩事。一次釋出可能會需要部署多個應用,每個應用都要部署多臺機器,有時候除了改程式碼之外,還需要修改配置,比如nginx配置等。大多運維團隊都會有一些指令碼來做這些變更。但這些指令碼通常都藏在某些只有運維團隊才知道(並有許可權)的機器上,開發和業務團隊都已經就緒之後,還需要等待運維團隊抽出時間來做些變更,這就無形中增加的時間成本。還是在前面提到的那個專案中,作部署就是有專門的運維團隊, 排期來對該應用進行部署。通常又會再多等一兩天。

Devops

DevOps是一種團隊合作的模式,即開發人員自己可以按需進行部署,不需要等待一個專門的釋出團隊的時間。DevOps其實現在還是沒有一個標準的翻譯,我的一個前同事將它翻譯為“開發自運維”,我覺得還是挺貼切的。

在這種模式下,原先的運維團隊應該轉換自己的職責,從負責具體業務的變更,變成基礎資源的提供者。比如當開發團隊需要一臺虛擬機器,或者一個Docker叢集時,能夠通過簡單的呼叫API,在很快的時間得到它,而不需要繁雜冗長的審批流程。運維團隊還可以提供有效的監控、告警工具等,同樣把他們以基礎服務的形式提供給開發團隊。就像現在AWS和阿里雲做的事情。

其實很多小團隊,包括我自己所在的團隊,都採用了DevOps的合作模式。但是做歸做,如何能做好呢?如何能夠保證每個開發(甚至是入職不久的開發)能夠安全快速的完成一次釋出呢?

答案是自動化加視覺化。自動化就是部署一個應用時,應該有指令碼能夠一鍵從構建物倉庫拉取出正確版本的構建物,然後部署到相應的機器(或者多臺機器)上。更重要的是這個自動化指令碼不應該藏在一個祕密機器的角落,因為這樣的話, 就很難告訴團隊成員如何去使用它們。所以應該把它視覺化,而視覺化的最好的平臺就是上面提到的那個持續整合流水線,在流水線的後面再加上一個部署線上的環節。

這樣,開發人員就能夠在一個統一的入口去了解從驗證、打包,再到生產環境部署的的全流程。當然我們的持續整合流水線也就順理成章的變成了持續交付流水線。

持續交付

在釋出方面,還有一個重要的課題就是環境管理。

環境管理

現在大多的線上部署模式是:申請幾臺虛擬機器,標明每一臺的用途,然後開始在各個虛擬機器上安裝各自需要的基礎軟體,比如nginx、tomcat等。然後寫一個指令碼進行各個應用程式的部署(這個指令碼最終會整合到持續交付流水線中),注意這裡的指令碼僅僅負責應用程式的部署,而不包含前面提到的基礎軟體。如果基礎軟體需要升級,或者安裝新的基礎軟體,或者需要調整系統引數,這些過程都需要手動進行。這種模式在大多數情況下是沒有問題的,但一旦機器出了問題,或者需要擴容時,就需要花費大量時間來重新安裝一臺和之前那臺一模一樣的機器,再修改部署指令碼把軟體部署上去。這個過程不但耗時,而且非常不可靠,因為你沒法保證你裝出來的這臺機器就和之前的那臺一模一樣,很有可能就給未來埋下了一顆定時炸彈。

解決這個問題的思路就是所謂的“基礎設施即程式碼”。也就是把環境的建立過程使用程式碼的形式描述出來,並且提交到程式碼庫中。任何的環境變更都必須通過修改程式碼、提交,然後總是使用程式碼庫中的最新版本重新構建環境。禁止直接在機器上進行任何環境變更,比如裝軟體,升級軟體,該軟體配置等。這樣所有機器的狀態就是可預測的,並且是一致的。

基礎設施及程式碼的相關工具有很多,比如最早流行的chef和puppet,到後來的Ansible。這裡我們就拿Ansible為例子講一講環境管理和部署自動化。

Ansible是一個Agent Less的通用部署及環境管理工具。也就是說不需要在目標機器上預先安裝任何客戶端軟體,這點與chef有所差別。它依賴的就是簡單的ssh命令。你說這跟我直接寫shell或者ruby指令碼ssh到目標機器,然後執行一些指令碼有什麼差別呢?

差別當然是有的,它會在以下幾個方面給你提供便利:

第一:Inventory管理。Inventory,即你要管理的那些機器。使用Ansible,你可以在一個集中的檔案中,以結構化的形式列出所有你需要管理的機器,及如何登陸它們,也就是ssh的使用者名稱和密碼 等資訊。不同機器的用途不同,比如這三臺是web伺服器,那兩臺是搜尋引擎等。那麼Ansible也提供了對機器進行分組的能力。有了這些分組後,就可以很容易的在命令列中指明我這次要對那些機器做變更。並且Ansible會自動地對所有這些機器做變更,省去了自己做迴圈的工作。

下面看幾個Ansible官方的Inventory例子:

Inventory

第二個重要的點叫做變更操作的冪等性。舉個例子,某一次對機器的變更是在~/.bash_profile中新增一行對JAVA_HOME的配置:“export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_71.jdk/Contents/Home/”。那麼我可以寫一個shell指令碼完成這件事情:“echo export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_71.jdk/Contents/Home/ >> ~/.bash_profile”。但是如果下次我的shell腳本里面多了安裝apache web server的程式碼。我就需要把這整個指令碼再對目標機器執行一次。那麼就會出現~/.bash_profile中出現兩行JAVA_HOME的配置的問題。雖然不至於引入錯誤,但也是很沒有必要的操作。

所謂冪等性,就是同一個指令碼對同一臺機器執行多次後,機器的狀態應該都是一致的。Ansible中模組(module)的概念就覆蓋了“冪等性”這個概念。所謂模組是預先寫好的一些庫,然後可以在Ansible的指令碼中進行呼叫。上面的在一個檔案中新增一行的操作就可以使用“lineinfile”這個module來做。在Ansible指令碼中的寫法是:“lineinfile: dest=~/.bash_profile line=/Library/Java/JavaVirtualMachines/jdk1.8.0_71.jdk/Contents/Home/”。再比如還有一個module,叫做service,Ansible指令碼中對於service的一個呼叫示例是這樣的: “service: name=httpd state=started”。這個描述的含義就是:“保證名為httpd的service是started狀態”。所以你可以想象到它的具體實現就是先檢查下httpd這個service的狀態,如果已經是started的就什麼都不做,否則就啟動它。

這種冪等性在配置管理方面是非常有用的,這樣我就可以放心的執行這些指令碼,知道最終一定可以得到某個一致的狀態。而且可以節省執行這些指令碼的時間,比如發現JDK已經裝好了,就不需要再裝一遍。

上面提到的Ansible編寫的指令碼被稱作Playbook,下面是幾個playbook的例子:

這個playbook是一個完整的例子,其中包括了我要部署那些機器(hosts)。是用什麼賬戶登入(root),執行哪些任務(tasks)等等。task中的name只是描述資訊。

Playbook

但是遺憾的是這種冪等性是不能完全保證的,有的module可以保證,比如上面提到的service和lineinfile。但有些是不行的,比如command module,它做的事情就是執行一條命令。Ansible無法判斷這條命令是否執行過。

所以在使用Ansible的過程中需要儘量使用能夠保證冪等性的module。這樣才能保證所有的機器在執行一段時間之後配置是相同的,避免“配置漂移”。當然還有一個避免配置漂移的方法就是每次都重新申請一臺新的機器,然後對著它執行一遍這些指令碼。這也是可行的,我們後面對此進行討論。

Ansible作為一個完備的工具,在錯誤處理,回滾,除錯等方面也都提供了便利的支援。詳情大家可以參看Ansible的官網。上面有關於Ansible本身的介紹,和一系列的擴充套件module。

最後再看一看Ansible整體的結構:

Ansible部署管理

前面我們提到了一種模式,即每次都新做出來一臺機器,然後把這些Ansible(或者其它什麼工具)指令碼對著這些乾淨的機器執行一遍。最後再把特定版本的軟體部署上去。也就是說每次部署都會把原來的虛擬機器例項幹掉,再重新生成一臺。在實際場景中,同一個應用會存在多個例項,我們沒有必要對每臺機器都這麼做,只需要把一臺機器使用Ansible裝好之後,再打個映象,然後通過這個映象啟動多臺例項。

這種模式能夠帶來的好處是顯而易見的,不但保證了環境的一致性,且擴容非常容易,只需要把同一個映象再多啟動幾個例項,然後掛接到相應的負載均衡中即可。而且永遠不需要害怕線上機器crash掉。按照映象再啟動一個就可以了。但這種模式帶來的問題也是顯而易見的,首先打虛擬機器映象的時間是很長的。其次這種做法就要求伺服器是沒有狀態的,也就是不能在硬碟上存檔案,寫log。還好現在的雲服務提供商(AWS,阿里雲等)都有相應的產品來解決這些問題。

對於上傳的附件和圖片等檔案來說,有兩種方式:

  1. 使用者上傳的檔案直接轉存第三方雲端儲存。
  2. 把NAS等第三方儲存掛載到例項的本地目錄。

這兩種方法能夠在一定程度上解決問題,但終究不是本地磁碟,在讀寫速度和併發寫的處理等方面都會多多少少存在一定問題。所以只能適用於對這些指標要求不高的場景。

對於log來說,也有兩種方式:

  1. 使用者產生的log不要寫到本地,而是直接推送到一箇中央log處理服務。
  2. log還是寫在本地,但是本地會有一個收集log的agent,不斷的讀取log內容,併發送到集中的log服務。比如阿里雲的SLS就是這麼工作的。

使用上述的方式時,所有的程式碼,軟體和配置的變更都需要走這麼一個流程:各種各樣的自動化測試、映象構建,例項化映象啟動服務。所有的變更,不管再小,都會走這樣的流程,而不會直接更改在例項機器上。所以例項機器就是不可變的了。這就是所謂“不可變伺服器”的概念。

但這個過程是比較漫長的。所以對於緊急釋出之類的場景,是很讓人捉急的。而Docker技術的出現就很好的解決了這個問題。Docker基於Linux Container(LXC)技術,能夠做到輕量級的虛擬化。

Docker採用了分層的檔案系統,所以如果我在包含Java的映象的基礎上打一個包含Tomcat的映象,只需要創建出一層只包含Tomcat的映象,然後和原先的包含Java的映象疊加在一起,就可以形成一個完整的可執行的映象。

在這種分層的映象機制下,如果每次只修改最上面一層映象,則構建的速度是很快的。而最上面一層通常就是新增應用程式。以Java Web程式為例,包含Java和Tomcat的映象可以作為一個 基礎映象。然後每次生成的WAR包通過Dockerfile(用於構建Docker映象的描述檔案)中的ADD指令新增到新的映象層中即可。

Docker映象as交付物

舉個例子:這裡有一個Java的web應用,通過執行“./gradlew war”的命令會在本地目錄下生成’build/libs/bookstore.war’。然後編寫如上圖的Dockerfile,它會把本地生成的war包ADD到Docker映象中。執行’docker build . -t bookstore: <版本號>’就可以生成一個映象。

分層映象

通過Docker的history命令可以看到1.5和1.3兩個版本的最上面一層是不同的,但它們的基礎映象層都是“25e98610c7d0”。最上面一層的大小是6.106M,也就是比一個war包稍微大了一點點。

綜上所述,可以看到相比使用虛擬機器映象作為不可變伺服器,使用Docker映象有如下優勢:

  1. 構建時間短。
  2. 使用空間小。

而前面提到的那些使用虛擬機器作為不可變伺服器時,需要解決的問題(本地檔案,log等),使用Docker同樣會面對。而解決方法也是類似的。

既然Docker這麼方便,那麼使用虛擬機器作為不可變伺服器是否還有價值呢?這個其實主要還是看相關工具,及其成熟度。比如AWS和阿里雲都提供了使用配置檔案來編排虛擬機器資源的能力,而且可以設定一些觸發器來自動以虛擬機器為單位對應用程式進行擴充套件(scale)。這種模式已經非常成熟了。

而對於容器而言,這些雲提供商也開始逐漸推出容器服務,把上述的那些對虛擬機器的操作也引入到了容器的領域。今年五月份阿里雲的容器服務就已經商用化了。它提供了叢集管理的能力,也可以設定觸發器對某一個應用進行擴容和縮容。關於阿里雲容器服務提供的更多能力,因為時間關係,就不再贅述,有興趣的朋友可以在這裡做詳細瞭解:https://yq.aliyun.com/teams/11。

Ansbile、虛擬機器不可變伺服器、Docker Image都是很有用的技術,但針對每個具體的技術,還是需要仔細評估你的應用是否能夠克服或者容忍前文提出的相應的限制和問題。並且需要看看這些技術能給你的業務帶來多大的好處。

最重要的一點就是無論你在部署階段使用的是何種技術,使用一條完整的從程式碼提交到最終部署上線的持續交付流水線都是必須的。在流水線上看到的都只是一個一個的stage,並且某些stage(比如部署)應該需要手動批准觸發。至於點選之後到底是呼叫了Ansible指令碼,還是運行了docker pull都是實現細節了。下面是一個使用 http://crp.aliyun.com 配置出來的示例持續交付流水線,及其不同的狀態。

微服務

上圖是一個持續整合流水線的不同狀態的樣子。可以看到剛開始的兩個stage,程式碼檢出和整合測試,是由程式碼提交自動觸發的。到了第三個stage,也就是部署測試環境,就需要手工批准了,所以出現了一個按鈕給你按。後續的預發和生產環境也都類似。

持續交付部分就講到這裡,下面是個小結:

持續交付與微服務

接下來我們再聊聊微服務。

微服務與持續交付的相互作用

關於微服務的概念,《微服務設計》一書給出的定義是:一些協同工作的小而自治的服務。微服務能夠帶來很多的好處,幫助我們更好的進行持續交付。當然微服務本身也需要很多實踐的支撐,比如Martin Fowler就在他的bliki(http://martinfowler.com/bliki/MicroservicePrerequisites.html)中提到了“You must be this tall to use microservices”。而這個’tall’中的很多內容都已經涵蓋前面討論的那些持續交付的技術實踐中。所以可以說微服務和持續交付也是相輔相成的關係。

使用微服務之後,顯然你需要部署的服務就會增多。如果一個服務的自動化部署和相應的流水線都沒有做好,那麼服務多了之後部署的複雜性就可想而知了。所以只有把持續交付的實踐先做好,才有可能順利地使用微服務。

反過來看,微服務架構下,每個服務都很小。因此如果我的某次修改只涉及了一個微服務的程式碼,我只需要釋出這一個服務即可。那麼相應的測試工作也就簡單的多。

其實按理說,雖然服務拆開了,但是還是需要這些服務在一起才能完成整個系統的功能。所以只修改一個服務,還是有可能影響整個系統的功能的。但是因為它們是不同的服務,所以一定會有非常清晰的API介面。這種API介面其實跟一個單塊系統中的模組化的概念很類似。只不過API容易做的清晰,而單塊系統中的模組化的邊界很難維持。所以從這個角度看,微服務帶來的其實是“強制的模組化”,從而帶來更好的設計。

好的,話說回來。既然每次釋出只涉及到需要修改的那些微服務,那麼影響的面就相應的較小,所以就可以更加放心大膽的去做釋出,也就進一步促進了持續交付。

微服務所涉及的話題非常多,大家可以移步《微服務設計》這本書檢視所有的話題。這裡只分享一點,那就是使用漸進式的方式進行微服務化。當然其實“漸進式”是我做大部分變動時的一個通用原則,比如重構,架構變化等。

漸進式微服務化的一個場景就是當你要新做一塊相對來說比較大,而且比較獨立的功能時,就可以考慮,是否可以單獨寫在一個服務中。舉個例子,若干年前我在一個比較大的Java Spring專案上工作。然後客戶有了一塊新的業務,最終希望以主站上的一個tab頁的形式存在。但我們都不想在這個陳舊的系統上繼續開發。最終的方式就是新啟一個應用。使用當時開發效率最高的技術。

那麼怎麼做到存在主站上的一個tab呢?答案是使用nginx整合。為了不暴漏客戶資訊,下面我們會用一些加的資訊。這個應用的所有url都在/new_app/下。在主站的nginx配置中加上一條轉發的規則,把/new_app/*這樣的url,都轉發到新部署的應用上。

nginx

這是一個行之有效的粘合新服務的方法。後來我們使用類似的方法把其它的“tab頁”(也就是其它不同的業務)也都一一用新的技術重寫了一遍,掛載到了主站上。

當然,這只是一種微服務的形態而已。關於更多的形態,大家可以瞭解一下淘寶的前後端分離技術:http://blog.jobbole.com/65513/

好的,稍微總結一下今天的內容:

今天主要講了什麼是持續交付的目標,為了達到這個目標需要使用哪些技術。然後還聊了聊微服務的方法論會給持續交付這件事情帶來怎樣的機遇和挑戰。最後舉了一個例子來說明如何逐步進行微服務化。

感謝大家的聆聽。

Q&A

Q1:使用Docker部署微服務持續交付時,應該注意什麼?你們的Docker使用情況是怎樣的?

A1:我現在做的是一款持續交付產品,本身會有一個構架叢集,執行任務使用的是Docker,但叢集軟體本身並沒有使用Docker來不熟。不然就是在Docker中執行Docker了,效能會有些影響。

前端的portal正在做Docker化,還沒有應用到生產環境中。一個可以分享的就是,要把自己的應用的相關配置都環境變數化,這樣對於Docker化比較友好。

我們還有一個產品是code.aliyun.com。這個產品也沒有Docker化,或者說『不可變伺服器化』,原因就是因為要在本地磁碟寫程式碼庫。

所以上面提到對本地讀寫要求比較高的應用做『不可變伺服器』還是有些困難的。

Q2:你們目前關注的持續整合的量化指標有哪些?比如專案單元測試/介面測試/靜態檢查等方面的內容。整個持續整合有效運轉的效率如何?失敗了怎麼辦?怎麼保證交付系統的穩定性?

A2:指標主要是測試覆蓋率、程式碼複雜度,及checkstyle檢查出來的一些問題。持續整合會有一個大螢幕把資訊輻射出來,所以如果出錯了,所有人都能看到會要求把CI break的同學立即修復,並且在修復之前不允許新功能的提交。

Q3:問個小問題,崔力強老師描述的package能包括什麼內容?

A3:可以是純粹的應用,比如war包。也可以是一個壓縮包,裡面包含了war包和安裝指令碼,這樣這個軟體包就是可以自安裝的。

Q4:Windows的架構會支援嗎?

A4:剛才提到的Ansible是支援Windows的。Docker的話,現在出了原生的Docker,可以做開發測試之用。但生產環境的效能如何,還需要測試一下。

Windows上有原生的Docker,我是mac使用者,win版的Docker其實也沒有用過,只是看到Docker官網的訊息:https://www.docker.com/products/docker :)原生的mac版Docker的volume掛載的效能也很差,猜測可能win版本的也不會太好。

文章出處:DBAplus社群