《阿里技術(shù)專家:持續(xù)交付與微服務(wù)背后的實(shí)踐邏輯》要點(diǎn):
本文介紹了阿里技術(shù)專家:持續(xù)交付與微服務(wù)背后的實(shí)踐邏輯,希望對您有用。如果有疑問,可以聯(lián)系我們。
崔力強(qiáng)
阿里巴巴技術(shù)專家
大家好,我是崔力強(qiáng).目前在阿里巴巴任職.負(fù)責(zé)一款持續(xù)交付領(lǐng)域的SaaS產(chǎn)品的開發(fā).非常高興能夠和大家分享持續(xù)交付和微服務(wù)的話題.
本次分享的重點(diǎn)是持續(xù)交付.也會提到一些微服務(wù)的概念,以及持續(xù)交付和微服務(wù)之間的關(guān)系.今天會涉及的一些實(shí)踐可能大家或多或少有所耳聞.我會著重講述這些實(shí)踐背后的邏輯,及它們之間的關(guān)系.
先看一看提綱:
關(guān)于持續(xù)交付的概念.從《持續(xù)交付》這本書的副標(biāo)題可見一斑:“發(fā)布可靠軟件的系統(tǒng)方法”.可以看到這本書中講的“持續(xù)交付”主要是技術(shù)相關(guān)的實(shí)踐,雖然近年有些朋友把持續(xù)交付的概念進(jìn)行了延伸,把精益需求管理和精益創(chuàng)業(yè)也包含了進(jìn)來,不過今天我還是會按照它最初的內(nèi)涵,只講技術(shù)相關(guān)的實(shí)踐.
這里其實(shí)有兩個概念,第一個是,怎么樣才算持續(xù)交付,也就是目標(biāo);第二個是,如何做到持續(xù)交付,也就是一些技術(shù)實(shí)踐.
為了了解“怎么樣才算持續(xù)交付”,讓我們先解剖一下軟件開發(fā)的過程.
針對單個需求來說,我們會先進(jìn)行需求分析、細(xì)化.然后開發(fā)和測試.部署時,需要拷貝一些文件到一些機(jī)器,運(yùn)行一些腳本,有時候還需要改一些配置文件或者做一些數(shù)據(jù)遷移等.
然而在實(shí)際的項(xiàng)目中,肯定不會只有一個需求.需求會源源不斷地輸入到開發(fā)團(tuán)隊(duì).下面是一個開發(fā)團(tuán)隊(duì)隨著時間進(jìn)行需求開發(fā)的示意圖.藍(lán)色的條是開發(fā)某個功能的開始時間和結(jié)束時間點(diǎn).有的需求占用時間長,有的需求占用時間短.
在每個功能的完成時刻,對于業(yè)務(wù)方來說,都可能是一個可以發(fā)布的狀態(tài)(也有可能不是,比如業(yè)務(wù)方認(rèn)為相關(guān)的幾個需求必須都做完了才能一起上,或者需要等到某個特定的時間點(diǎn)才能上).那么如果立馬就能夠進(jìn)行一次發(fā)布,并且能夠快速并且安全的完成這次發(fā)布,則能夠?qū)I(yè)務(wù)的發(fā)展具有非常積極的作用.也就是在下圖的這些時刻.
這些時刻,不一定每個都需要發(fā)布.但作為開發(fā)團(tuán)隊(duì),要給業(yè)務(wù)方足夠的靈活度.所以持續(xù)交付的目標(biāo),并不是每次提交都進(jìn)行發(fā)布,而是每次提交都是可發(fā)布的狀態(tài).這就回答了“怎么樣才算持續(xù)交付”這個問題.再換一種方式來說,持續(xù)交付是對業(yè)務(wù)方友好的一種,開發(fā)團(tuán)隊(duì)的開發(fā)節(jié)奏.
接下來就要討論使用什么樣的技術(shù)實(shí)踐來達(dá)到這種開發(fā)節(jié)奏.為了討論具體的技術(shù)實(shí)踐,首先來看看在軟件開發(fā)中有什么因素會阻礙我們達(dá)到這種節(jié)奏.
第一個問題是:上線前總還是要做測試的吧,至少做一遍重要功能的回歸測試(手工回歸本身其實(shí)也是有章可循的,推薦我同事的一篇文章:https://yq.aliyun.com/articles/6898).隨著一個系統(tǒng)上的功能越來越多,每次上線前需要測試的東西就越來越多.慢慢的,發(fā)布就慢下來了.我曾經(jīng)工作過的一個系統(tǒng),上面承載了三塊業(yè)務(wù),有將近40多個人在上面工作.每次發(fā)布前都要先進(jìn)行幾天的回歸測試,順利的話,從確定要發(fā)布的版本到驗(yàn)證完該版本,確定可以發(fā)布,也要差不多一周的時間.
另一個我工作過的項(xiàng)目規(guī)模要小一些,做一次發(fā)布也需要一天的時間來回歸,通常都會發(fā)現(xiàn)一些問題.順利的話,可以在當(dāng)天把所有的問題修復(fù)掉,進(jìn)行發(fā)布.不過因?yàn)閎ug沒修完而不能當(dāng)天發(fā)布,拖到第二天的這種情況也時常發(fā)生.
甚至有一次,到了晚上九點(diǎn)多的時候還沒有修完bug,但當(dāng)時大家都憋著一口氣,硬是要發(fā),結(jié)果發(fā)到了晚上一點(diǎn)多.
因?yàn)槌杀靖?所以不敢做的太頻繁;做的不頻繁的話,就會在發(fā)布中積累更多的功能,從而進(jìn)一步增加出問題的可能性,從而形成一個惡性循環(huán).
作為一個妥協(xié),人們不得不使用瀑布或者迭代內(nèi)小瀑布的開發(fā)模式.
把開發(fā)的周期分解為一個一個的迭代,比如兩周到四周的時間.在迭代開始前,保證該迭代內(nèi)計劃的需求分析完畢.然后在迭代內(nèi)部開發(fā).順利的話,會在接近迭代末尾時完成迭代內(nèi)計劃的所有任務(wù),然后拉一個發(fā)布分支出來,開始測試,然后發(fā)布.
做出上述妥協(xié)的直接原因就是“測試和部署”花費(fèi)的時間過長.如果只花費(fèi)一個人一個小時就能夠完成回歸和發(fā)布,那顯然團(tuán)隊(duì)就更愿意去頻繁的發(fā)布.
一個行之有效的方法就是進(jìn)行自動化測試.
自動化測試大致可以分為幾種:單元測試、API測試、驗(yàn)收測試/功能測試/端到端測試.在不同的技術(shù)棧下,分類可能會略有不同的,但本質(zhì)上來講是類似的.不同層次的測試有自己的側(cè)重點(diǎn),組要組合使用來達(dá)到一個比較好的效果.如上圖所示:
這里以Java Spring項(xiàng)目為例來列舉不同層次的測試工具:單元測試使用Junit;集成測試使用Spring Test + Junit;功能測試使用cucumber+ capybara+selinium或者robotframework+ selinium.
如果使用了前端框架,比如Angular、ReactJS等,它們本身也提供了相應(yīng)的測試框架.
底層的單元測試,測試的范圍較小,一般只涉及一個或者幾個類,不會調(diào)用網(wǎng)絡(luò)或者數(shù)據(jù)庫.所以編寫和運(yùn)行起來都比較快.在這個級別應(yīng)該覆蓋盡量多的分支和邏輯.這個級別的測試能夠達(dá)到比較高的覆蓋率,所以在它的保護(hù)下,可以放心大膽的做重構(gòu)或者添加新代碼,只需要花上幾秒鐘的時間運(yùn)行一遍單元測試,就能夠知道這次修改是否引入了問題.前陣子做了很長時間的nodejs開發(fā),單元測試對這種弱類型的語言尤其重要.因?yàn)橄褡兞课炊x,傳參數(shù)的個數(shù)錯誤等很低級的問題IDE都無法給出有效的提示.
因?yàn)閱卧獪y試需要隔離被測類和系統(tǒng)的其它代碼,所以需要有一些測試替身來代替真實(shí)的類.有很多工具可以做這樣的事情,比如Java中的mockito.
如上圖所示,它會創(chuàng)造一個假的ClassB的實(shí)例出來,并傳給ClassA的實(shí)例.然后對ClassB的行為做出一些假設(shè),在此假設(shè)的基礎(chǔ)上對ClassA的行為進(jìn)行測試.
但是一個類或者幾個類的正確,并不能讓你對系統(tǒng)的正確性有足夠的信心.因?yàn)閱卧獪y試中充滿了對其它類行為的假設(shè).所以一旦這個假設(shè)錯誤,就會出現(xiàn)測試依然能通過,但整個系統(tǒng)的行為已經(jīng)錯了的尷尬情況.所以我們還需要覆蓋面更大的集成測試.這種測試在服務(wù)內(nèi)部不使用任何的測試替身.但對外部的服務(wù)進(jìn)行打樁.對基于HTTP的服務(wù)進(jìn)行打樁的工具包括moco(https://github.com/dreamhead/moco)和pact(https://github.com/realestate-com-au/pact,其實(shí)pact能做的事情不止打樁,更多的是做“契約測試”).這種測試更真實(shí),但運(yùn)行起來會慢,所以這個層面的測試主要保證的是連通性.不需要100%的覆蓋率.
集成測試覆蓋面很大,但它仍然是白盒測試,因?yàn)樗苯诱{(diào)用了函數(shù)(比如上頁的controller).如果這個服務(wù)只提供API,那么這種測試就夠了.但如果這個服務(wù)是提供頁面的,也就是一個web應(yīng)用,那么就還需要一層直接操作網(wǎng)頁來進(jìn)行基于用戶行為的測試,我們一般稱之為驗(yàn)收測試,或者功能測試.上面列舉的cucumber和robotframework是非常流行的兩款功能測試工具.他們是通用的測試框架,與具體的被測系統(tǒng)是無關(guān)的.我也另一位前同事都對這兩種工具比較熟悉,并且寫了文章作總結(jié):http://www.infoq.com/cn/articles/cucumber-robotframework-comparison
如果要測試web系統(tǒng)的話,就需要能夠驅(qū)動網(wǎng)頁的驅(qū)動程序.現(xiàn)在非常主流的驅(qū)動是selinium(https://github.com/SeleniumHQ/selenium
),當(dāng)然我更喜歡的是在其上包了一層的capybara(https://github.com/jnicklas/capybara),它是用ruby編寫的,封裝的API更好用.
建議至少對核心的流程編寫功能測試,以保證上線不要出現(xiàn)嚴(yán)重的故障.前段時間我們的功能測試發(fā)現(xiàn)了一個bug.這個bug對于老用戶都不會有問題,但是用戶首次登錄就會500.而用戶首次登錄的場景恰恰是平時自測的時候很容易忽略的,因?yàn)闇?zhǔn)備數(shù)據(jù)還是有點(diǎn)麻煩的.當(dāng)時發(fā)現(xiàn)這個問題的時候大家并沒有什么感覺,因?yàn)橐呀?jīng)習(xí)慣有測試保護(hù)的軟件了.但如果跳出來想想,這個問題要是在一周后的“迭代末尾”才發(fā)現(xiàn),會多么的打擊氣勢.如果上線才發(fā)現(xiàn),那么產(chǎn)品的新用戶增長量一定會直線下降.
端到端測試其實(shí)也就是使用功能測試的工具在更大的范圍進(jìn)行測試,也就是包含所有的服務(wù).
下面總結(jié)一下各個層次測試的特點(diǎn).
回歸測試是迭代開發(fā)中必不可少的一個步驟.我們能做的就是通過自動化測試去盡量減小這個時間.
很多人對于測試有一些顧慮,覺得會花費(fèi)很多時間.而且當(dāng)代碼結(jié)構(gòu)調(diào)整時,測試也要跟著改.
我的看法是,測試代碼也是代碼,維護(hù)測試代碼的代價跟測試代碼本身的質(zhì)量是直接相關(guān)的.所以對測試代碼也需要及時重構(gòu),提高可維護(hù)和可重用性.具體一點(diǎn)對于測試來說,提高可維護(hù)性和看重用性,無非就是要在數(shù)據(jù)準(zhǔn)備、斷言工具方面去抽取一些庫.比如Ruby的factory girl就是一個極好的基于ActiveRecord的數(shù)據(jù)準(zhǔn)備庫.有了它,寫測試的代價大大降低.那如果你不用Ruby怎么辦,那只好自己實(shí)現(xiàn)一個其它語言版本的factory girl嘍.我上一個項(xiàng)目用的是nodejs,就寫了一個nodejs版本的factory girl.
而單元測試能夠給你帶來的好處不僅僅是回歸這么簡單.有了完備的單元測試,你才有信心,有動力去做一些重構(gòu).只要測試通過,我就知道我的重構(gòu)是正確的,你才敢不斷的去重構(gòu),優(yōu)化代碼,才能使得代碼更易維護(hù).所以可以說寫測試是保證你代碼可維護(hù)性的必由之路.不要考慮寫不寫測試,而是考慮,如何低成本的寫測試.
當(dāng)我開發(fā)新功能時候,編寫好測試運(yùn)行一下,就知道功能正確與否,這樣就不用把服務(wù)器啟起來,減小反饋的周期.在這個場景下,它會直接節(jié)省你的時間,雖然你寫了更多的代碼.
關(guān)于代碼改動,測試也要跟著改的問題,我想說兩點(diǎn):
第二,要善用IDE幫你做改動.測試代碼也是代碼,當(dāng)你修改一個函數(shù)簽名時,IDE會幫你把所有的調(diào)用處都改掉,包括測試代碼.所以IDE用得好,修改代碼也不是那么痛苦的事情.
那么有了這些測試之后,我應(yīng)該什么時候運(yùn)行它們呢.是迭代結(jié)束時嗎?不!我們應(yīng)該在每次提交時都完整的運(yùn)行一遍這些測試.這樣一旦出了問題我就可以第一時間知道.這就是持續(xù)集成的基本概念.
每次提交代碼觸發(fā)編譯、測試、靜態(tài)檢查、打包歸檔、然后再運(yùn)行驗(yàn)收測試(AT),然后再部署到類生產(chǎn)環(huán)境進(jìn)行性能測試,再部署到端到端測試環(huán)境運(yùn)行端到端測試.并且把每一步的結(jié)果反饋給開發(fā)團(tuán)隊(duì).
我們把上圖稱為持續(xù)集成流水線.可以使用很多工具來實(shí)現(xiàn),比如最常見的開源工具Jenkins.或者我目前所負(fù)責(zé)產(chǎn)品:crp.aliyun.com.
關(guān)于更多的持續(xù)集成的實(shí)踐和流水線設(shè)計,因?yàn)閮?nèi)容很多,這里只討論幾個要點(diǎn).
我們使用自動化測試加持續(xù)集成解決了第一個發(fā)布前回歸測試耗時的問題.
第二個問題就是:“別人的功能還沒做完”.假設(shè)現(xiàn)在團(tuán)隊(duì)正在進(jìn)行單分支開發(fā)(也就是說所有的功能都提交在一個分支上,不會為了一個功能單獨(dú) 開出一個長期存在分支).就拿圖中紅色線這個時間點(diǎn)來講,第三個需求完成了,業(yè)務(wù)人員也認(rèn)為可以做一次發(fā)布.但是同時還有另外三個需求正在開發(fā).如果做發(fā)布的話,就會把做了一半的東西也給發(fā)上去.這是不可以接受的.
解決這個問題有兩個思路:那就是功能分支和功能開關(guān).
先看看功能分支:
每開一個新功能,就開一個分支.這個分支存活的時間通常是“周”這個數(shù)量級的.哪個功能開發(fā)完成了就合入到主干,進(jìn)行一次發(fā)布.這樣,其它未完成的功能還沒有合入到主干,就不會造成影響.但功能分支有很多的問題,最嚴(yán)重的一個問題是:它和持續(xù)集成的理念是沖突的.持續(xù)集成是希望你每次提交都能夠放在一起進(jìn)行驗(yàn)證.但使用了分支的話,就只能在合并的時刻,才能真正把所有的東西放在一起進(jìn)行驗(yàn)證.而這時發(fā)現(xiàn)的問題可能一周前已經(jīng)發(fā)生了.
另一個方法,功能開關(guān),會給任何一個新開發(fā)的功能在代碼級別加上一個開關(guān),使得可以簡單的修改一個配置就把一個功能完全隱藏掉.默認(rèn)所有的開關(guān)都是關(guān)閉的,如果一個功能做完了,想上,則修改配置,打開開關(guān),進(jìn)行一次發(fā)布即可.聽起來很理想,但事實(shí)上也需要花費(fèi)不少的代碼來把這件事情真正做好.
關(guān)于功能分支和功能開關(guān)今天不展開細(xì)講了.有興趣的朋友可以參看我之前寫的一篇文章:http://www.infoq.com/cn/articles/function-switch-realize-better-continuous-implementations
接下來我們聊一聊發(fā)布.因?yàn)槲覀兿Ml(fā)布也是快速并安全可靠的.
發(fā)布是一件麻煩事.一次發(fā)布可能會需要部署多個應(yīng)用,每個應(yīng)用都要部署多臺機(jī)器,有時候除了改代碼之外,還需要修改配置,比如nginx配置等.大多運(yùn)維團(tuán)隊(duì)都會有一些腳本來做這些變更.但這些腳本通常都藏在某些只有運(yùn)維團(tuán)隊(duì)才知道(并有權(quán)限)的機(jī)器上,開發(fā)和業(yè)務(wù)團(tuán)隊(duì)都已經(jīng)就緒之后,還需要等待運(yùn)維團(tuán)隊(duì)抽出時間來做些變更,這就無形中增加的時間成本.還是在前面提到的那個項(xiàng)目中,作部署就是有專門的運(yùn)維團(tuán)隊(duì), 排期來對該應(yīng)用進(jìn)行部署.通常又會再多等一兩天.
DevOps是一種團(tuán)隊(duì)合作的模式,即開發(fā)人員自己可以按需進(jìn)行部署,不需要等待一個專門的發(fā)布團(tuán)隊(duì)的時間.DevOps其實(shí)現(xiàn)在還是沒有一個標(biāo)準(zhǔn)的翻譯,我的一個前同事將它翻譯為“開發(fā)自運(yùn)維”,我覺得還是挺貼切的.
在這種模式下,原先的運(yùn)維團(tuán)隊(duì)?wèi)?yīng)該轉(zhuǎn)換自己的職責(zé),從負(fù)責(zé)具體業(yè)務(wù)的變更,變成基礎(chǔ)資源的提供者.比如當(dāng)開發(fā)團(tuán)隊(duì)需要一臺虛擬機(jī),或者一個Docker集群時,能夠通過簡單的調(diào)用API,在很快的時間得到它,而不需要繁雜冗長的審批流程.運(yùn)維團(tuán)隊(duì)還可以提供有效的監(jiān)控、告警工具等,同樣把他們以基礎(chǔ)服務(wù)的形式提供給開發(fā)團(tuán)隊(duì).就像現(xiàn)在AWS和阿里云做的事情.
其實(shí)很多小團(tuán)隊(duì),包括我自己所在的團(tuán)隊(duì),都采用了DevOps的合作模式.但是做歸做,如何能做好呢?如何能夠保證每個開發(fā)(甚至是入職不久的開發(fā))能夠安全快速的完成一次發(fā)布呢?
答案是自動化加可視化.自動化就是部署一個應(yīng)用時,應(yīng)該有腳本能夠一鍵從構(gòu)建物倉庫拉取出正確版本的構(gòu)建物,然后部署到相應(yīng)的機(jī)器(或者多臺機(jī)器)上.更重要的是這個自動化腳本不應(yīng)該藏在一個秘密機(jī)器的角落,因?yàn)檫@樣的話, 就很難告訴團(tuán)隊(duì)成員如何去使用它們.所以應(yīng)該把它可視化,而可視化的最好的平臺就是上面提到的那個持續(xù)集成流水線,在流水線的后面再加上一個部署線上的環(huán)節(jié).
這樣,開發(fā)人員就能夠在一個統(tǒng)一的入口去了解從驗(yàn)證、打包,再到生產(chǎn)環(huán)境部署的的全流程.當(dāng)然我們的持續(xù)集成流水線也就順理成章的變成了持續(xù)交付流水線.
在發(fā)布方面,還有一個重要的課題就是環(huán)境管理.
現(xiàn)在大多的線上部署模式是:申請幾臺虛擬機(jī),標(biāo)明每一臺的用途,然后開始在各個虛擬機(jī)上安裝各自需要的基礎(chǔ)軟件,比如nginx、tomcat等.然后寫一個腳本進(jìn)行各個應(yīng)用程序的部署(這個腳本最終會集成到持續(xù)交付流水線中),注意這里的腳本僅僅負(fù)責(zé)應(yīng)用程序的部署,而不包含前面提到的基礎(chǔ)軟件.如果基礎(chǔ)軟件需要升級,或者安裝新的基礎(chǔ)軟件,或者需要調(diào)整系統(tǒng)參數(shù),這些過程都需要手動進(jìn)行.這種模式在大多數(shù)情況下是沒有問題的,但一旦機(jī)器出了問題,或者需要擴(kuò)容時,就需要花費(fèi)大量時間來重新安裝一臺和之前那臺一模一樣的機(jī)器,再修改部署腳本把軟件部署上去.這個過程不但耗時,而且非常不可靠,因?yàn)槟銢]法保證你裝出來的這臺機(jī)器就和之前的那臺一模一樣,很有可能就給未來埋下了一顆定時炸彈.
解決這個問題的思路就是所謂的“基礎(chǔ)設(shè)施即代碼”.也就是把環(huán)境的創(chuàng)建過程使用代碼的形式描述出來,并且提交到代碼庫中.任何的環(huán)境變更都必須通過修改代碼、提交,然后總是使用代碼庫中的最新版本重新構(gòu)建環(huán)境.禁止直接在機(jī)器上進(jìn)行任何環(huán)境變更,比如裝軟件,升級軟件,該軟件配置等.這樣所有機(jī)器的狀態(tài)就是可預(yù)測的,并且是一致的.
基礎(chǔ)設(shè)施及代碼的相關(guān)工具有很多,比如最早流行的chef和puppet,到后來的Ansible.這里我們就拿Ansible為例子講一講環(huán)境管理和部署自動化.
Ansible是一個Agent Less的通用部署及環(huán)境管理工具.也就是說不需要在目標(biāo)機(jī)器上預(yù)先安裝任何客戶端軟件,這點(diǎn)與chef有所差別.它依賴的就是簡單的ssh命令.你說這跟我直接寫shell或者ruby腳本ssh到目標(biāo)機(jī)器,然后執(zhí)行一些腳本有什么差別呢?
差別當(dāng)然是有的,它會在以下幾個方面給你提供便利:
第一:Inventory管理.Inventory,即你要管理的那些機(jī)器.使用Ansible,你可以在一個集中的文件中,以結(jié)構(gòu)化的形式列出所有你需要管理的機(jī)器,及如何登陸它們,也就是ssh的用戶名和密碼 等信息.不同機(jī)器的用途不同,比如這三臺是web服務(wù)器,那兩臺是搜索引擎等.那么Ansible也提供了對機(jī)器進(jìn)行分組的能力.有了這些分組后,就可以很容易的在命令行中指明我這次要對那些機(jī)器做變更.并且Ansible會自動地對所有這些機(jī)器做變更,省去了自己做循環(huán)的工作.
下面看幾個Ansible官方的Inventory例子:
第二個重要的點(diǎn)叫做變更操作的冪等性.舉個例子,某一次對機(jī)器的變更是在~/.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的代碼.我就需要把這整個腳本再對目標(biāo)機(jī)器運(yùn)行一次.那么就會出現(xiàn)~/.bash_profile中出現(xiàn)兩行JAVA_HOME的配置的問題.雖然不至于引入錯誤,但也是很沒有必要的操作.
所謂冪等性,就是同一個腳本對同一臺機(jī)器運(yùn)行多次后,機(jī)器的狀態(tài)應(yīng)該都是一致的.Ansible中模塊(module)的概念就覆蓋了“冪等性”這個概念.所謂模塊是預(yù)先寫好的一些庫,然后可以在Ansible的腳本中進(jìn)行調(diào)用.上面的在一個文件中添加一行的操作就可以使用“l(fā)ineinfile”這個module來做.在Ansible腳本中的寫法是:“l(fā)ineinfile: dest=~/.bash_profile line=/Library/Java/JavaVirtualMachines/jdk1.8.0_71.jdk/Contents/Home/”.再比如還有一個module,叫做service,Ansible腳本中對于service的一個調(diào)用示例是這樣的: “service: name=httpd state=started”.這個描述的含義就是:“保證名為httpd的service是started狀態(tài)”.所以你可以想象到它的具體實(shí)現(xiàn)就是先檢查下httpd這個service的狀態(tài),如果已經(jīng)是started的就什么都不做,否則就啟動它.
這種冪等性在配置管理方面是非常有用的,這樣我就可以放心的運(yùn)行這些腳本,知道最終一定可以得到某個一致的狀態(tài).而且可以節(jié)省運(yùn)行這些腳本的時間,比如發(fā)現(xiàn)JDK已經(jīng)裝好了,就不需要再裝一遍.
上面提到的Ansible編寫的腳本被稱作Playbook,下面是幾個playbook的例子:
這個playbook是一個完整的例子,其中包括了我要部署那些機(jī)器(hosts).是用什么賬戶登錄(root),運(yùn)行哪些任務(wù)(tasks)等等.task中的name只是描述信息.
但是遺憾的是這種冪等性是不能完全保證的,有的module可以保證,比如上面提到的service和lineinfile.但有些是不行的,比如command module,它做的事情就是運(yùn)行一條命令.Ansible無法判斷這條命令是否執(zhí)行過.
所以在使用Ansible的過程中需要盡量使用能夠保證冪等性的module.這樣才能保證所有的機(jī)器在運(yùn)行一段時間之后配置是相同的,避免“配置漂移”.當(dāng)然還有一個避免配置漂移的方法就是每次都重新申請一臺新的機(jī)器,然后對著它運(yùn)行一遍這些腳本.這也是可行的,我們后面對此進(jìn)行討論.
Ansible作為一個完備的工具,在錯誤處理,回滾,調(diào)試等方面也都提供了便利的支持.詳情大家可以參看Ansible的官網(wǎng).上面有關(guān)于Ansible本身的介紹,和一系列的擴(kuò)展module.
最后再看一看Ansible整體的結(jié)構(gòu):
前面我們提到了一種模式,即每次都新做出來一臺機(jī)器,然后把這些Ansible(或者其它什么工具)腳本對著這些干凈的機(jī)器運(yùn)行一遍.最后再把特定版本的軟件部署上去.也就是說每次部署都會把原來的虛擬機(jī)實(shí)例干掉,再重新生成一臺.在實(shí)際場景中,同一個應(yīng)用會存在多個實(shí)例,我們沒有必要對每臺機(jī)器都這么做,只需要把一臺機(jī)器使用Ansible裝好之后,再打個鏡像,然后通過這個鏡像啟動多臺實(shí)例.
這種模式能夠帶來的好處是顯而易見的,不但保證了環(huán)境的一致性,且擴(kuò)容非常容易,只需要把同一個鏡像再多啟動幾個實(shí)例,然后掛接到相應(yīng)的負(fù)載均衡中即可.而且永遠(yuǎn)不需要害怕線上機(jī)器crash掉.按照鏡像再啟動一個就可以了.但這種模式帶來的問題也是顯而易見的,首先打虛擬機(jī)鏡像的時間是很長的.其次這種做法就要求服務(wù)器是沒有狀態(tài)的,也就是不能在硬盤上存文件,寫log.還好現(xiàn)在的云服務(wù)提供商(AWS,阿里云等)都有相應(yīng)的產(chǎn)品來解決這些問題.
對于上傳的附件和圖片等文件來說,有兩種方式:
這兩種方法能夠在一定程度上解決問題,但終究不是本地磁盤,在讀寫速度和并發(fā)寫的處理等方面都會多多少少存在一定問題.所以只能適用于對這些指標(biāo)要求不高的場景.
對于log來說,也有兩種方式:
使用上述的方式時,所有的代碼,軟件和配置的變更都需要走這么一個流程:各種各樣的自動化測試、鏡像構(gòu)建,實(shí)例化鏡像啟動服務(wù).所有的變更,不管再小,都會走這樣的流程,而不會直接更改在實(shí)例機(jī)器上.所以實(shí)例機(jī)器就是不可變的了.這就是所謂“不可變服務(wù)器”的概念.
但這個過程是比較漫長的.所以對于緊急發(fā)布之類的場景,是很讓人捉急的.而Docker技術(shù)的出現(xiàn)就很好的解決了這個問題.Docker基于Linux Container(LXC)技術(shù),能夠做到輕量級的虛擬化.
Docker采用了分層的文件系統(tǒng),所以如果我在包含Java的鏡像的基礎(chǔ)上打一個包含Tomcat的鏡像,只需要創(chuàng)建出一層只包含Tomcat的鏡像,然后和原先的包含Java的鏡像疊加在一起,就可以形成一個完整的可運(yùn)行的鏡像.
在這種分層的鏡像機(jī)制下,如果每次只修改最上面一層鏡像,則構(gòu)建的速度是很快的.而最上面一層通常就是添加應(yīng)用程序.以Java Web程序?yàn)槔?包含Java和Tomcat的鏡像可以作為一個 基礎(chǔ)鏡像.然后每次生成的WAR包通過Dockerfile(用于構(gòu)建Docker鏡像的描述文件)中的ADD指令添加到新的鏡像層中即可.
舉個例子:這里有一個Java的web應(yīng)用,通過運(yùn)行“./gradlew war”的命令會在本地目錄下生成’build/libs/bookstore.war’.然后編寫如上圖的Dockerfile,它會把本地生成的war包ADD到Docker鏡像中.運(yùn)行’docker build . -t bookstore: <版本號>’就可以生成一個鏡像.
通過Docker的history命令可以看到1.5和1.3兩個版本的最上面一層是不同的,但它們的基礎(chǔ)鏡像層都是“25e98610c7d0”.最上面一層的大小是6.106M,也就是比一個war包稍微大了一點(diǎn)點(diǎn).
綜上所述,可以看到相比使用虛擬機(jī)鏡像作為不可變服務(wù)器,使用Docker鏡像有如下優(yōu)勢:
而前面提到的那些使用虛擬機(jī)作為不可變服務(wù)器時,需要解決的問題(本地文件,log等),使用Docker同樣會面對.而解決方法也是類似的.
既然Docker這么方便,那么使用虛擬機(jī)作為不可變服務(wù)器是否還有價值呢?這個其實(shí)主要還是看相關(guān)工具,及其成熟度.比如AWS和阿里云都提供了使用配置文件來編排虛擬機(jī)資源的能力,而且可以設(shè)置一些觸發(fā)器來自動以虛擬機(jī)為單位對應(yīng)用程序進(jìn)行擴(kuò)展(scale).這種模式已經(jīng)非常成熟了.
而對于容器而言,這些云提供商也開始逐漸推出容器服務(wù),把上述的那些對虛擬機(jī)的操作也引入到了容器的領(lǐng)域.今年五月份阿里云的容器服務(wù)就已經(jīng)商用化了.它提供了集群管理的能力,也可以設(shè)置觸發(fā)器對某一個應(yīng)用進(jìn)行擴(kuò)容和縮容.關(guān)于阿里云容器服務(wù)提供的更多能力,因?yàn)闀r間關(guān)系,就不再贅述,有興趣的朋友可以在這里做詳細(xì)了解:https://yq.aliyun.com/teams/11.
Ansbile、虛擬機(jī)不可變服務(wù)器、Docker Image都是很有用的技術(shù),但針對每個具體的技術(shù),還是需要仔細(xì)評估你的應(yīng)用是否能夠克服或者容忍前文提出的相應(yīng)的限制和問題.并且需要看看這些技術(shù)能給你的業(yè)務(wù)帶來多大的好處.
最重要的一點(diǎn)就是無論你在部署階段使用的是何種技術(shù),使用一條完整的從代碼提交到最終部署上線的持續(xù)交付流水線都是必須的.在流水線上看到的都只是一個一個的stage,并且某些stage(比如部署)應(yīng)該需要手動批準(zhǔn)觸發(fā).至于點(diǎn)擊之后到底是調(diào)用了Ansible腳本,還是運(yùn)行了docker pull都是實(shí)現(xiàn)細(xì)節(jié)了.下面是一個使用 http://crp.aliyun.com 配置出來的示例持續(xù)交付流水線,及其不同的狀態(tài).
上圖是一個持續(xù)集成流水線的不同狀態(tài)的樣子.可以看到剛開始的兩個stage,代碼檢出和集成測試,是由代碼提交自動觸發(fā)的.到了第三個stage,也就是部署測試環(huán)境,就需要手工批準(zhǔn)了,所以出現(xiàn)了一個按鈕給你按.后續(xù)的預(yù)發(fā)和生產(chǎn)環(huán)境也都類似.
持續(xù)交付部分就講到這里,下面是個小結(jié):
接下來我們再聊聊微服務(wù).
關(guān)于微服務(wù)的概念,《微服務(wù)設(shè)計》一書給出的定義是:一些協(xié)同工作的小而自治的服務(wù).微服務(wù)能夠帶來很多的好處,幫助我們更好的進(jìn)行持續(xù)交付.當(dāng)然微服務(wù)本身也需要很多實(shí)踐的支撐,比如Martin Fowler就在他的bliki(http://martinfowler.com/bliki/MicroservicePrerequisites.html)中提到了“You must be this tall to use microservices”.而這個’tall’中的很多內(nèi)容都已經(jīng)涵蓋前面討論的那些持續(xù)交付的技術(shù)實(shí)踐中.所以可以說微服務(wù)和持續(xù)交付也是相輔相成的關(guān)系.
使用微服務(wù)之后,顯然你需要部署的服務(wù)就會增多.如果一個服務(wù)的自動化部署和相應(yīng)的流水線都沒有做好,那么服務(wù)多了之后部署的復(fù)雜性就可想而知了.所以只有把持續(xù)交付的實(shí)踐先做好,才有可能順利地使用微服務(wù).
反過來看,微服務(wù)架構(gòu)下,每個服務(wù)都很小.因此如果我的某次修改只涉及了一個微服務(wù)的代碼,我只需要發(fā)布這一個服務(wù)即可.那么相應(yīng)的測試工作也就簡單的多.
其實(shí)按理說,雖然服務(wù)拆開了,但是還是需要這些服務(wù)在一起才能完成整個系統(tǒng)的功能.所以只修改一個服務(wù),還是有可能影響整個系統(tǒng)的功能的.但是因?yàn)樗鼈兪遣煌姆?wù),所以一定會有非常清晰的API接口.這種API接口其實(shí)跟一個單塊系統(tǒng)中的模塊化的概念很類似.只不過API容易做的清晰,而單塊系統(tǒng)中的模塊化的邊界很難維持.所以從這個角度看,微服務(wù)帶來的其實(shí)是“強(qiáng)制的模塊化”,從而帶來更好的設(shè)計.
好的,話說回來.既然每次發(fā)布只涉及到需要修改的那些微服務(wù),那么影響的面就相應(yīng)的較小,所以就可以更加放心大膽的去做發(fā)布,也就進(jìn)一步促進(jìn)了持續(xù)交付.
微服務(wù)所涉及的話題非常多,大家可以移步《微服務(wù)設(shè)計》這本書查看所有的話題.這里只分享一點(diǎn),那就是使用漸進(jìn)式的方式進(jìn)行微服務(wù)化.當(dāng)然其實(shí)“漸進(jìn)式”是我做大部分變動時的一個通用原則,比如重構(gòu),架構(gòu)變化等.
漸進(jìn)式微服務(wù)化的一個場景就是當(dāng)你要新做一塊相對來說比較大,而且比較獨(dú)立的功能時,就可以考慮,是否可以單獨(dú)寫在一個服務(wù)中.舉個例子,若干年前我在一個比較大的Java Spring項(xiàng)目上工作.然后客戶有了一塊新的業(yè)務(wù),最終希望以主站上的一個tab頁的形式存在.但我們都不想在這個陳舊的系統(tǒng)上繼續(xù)開發(fā).最終的方式就是新啟一個應(yīng)用.使用當(dāng)時開發(fā)效率最高的技術(shù).
那么怎么做到存在主站上的一個tab呢?答案是使用nginx集成.為了不暴漏客戶信息,下面我們會用一些加的信息.這個應(yīng)用的所有url都在/new_app/下.在主站的nginx配置中加上一條轉(zhuǎn)發(fā)的規(guī)則,把/new_app/*這樣的url,都轉(zhuǎn)發(fā)到新部署的應(yīng)用上.
當(dāng)然,這只是一種微服務(wù)的形態(tài)而已.關(guān)于更多的形態(tài),大家可以了解一下淘寶的前后端分離技術(shù):http://blog.jobbole.com/65513/.
好的,稍微總結(jié)一下今天的內(nèi)容:
今天主要講了什么是持續(xù)交付的目標(biāo),為了達(dá)到這個目標(biāo)需要使用哪些技術(shù).然后還聊了聊微服務(wù)的方法論會給持續(xù)交付這件事情帶來怎樣的機(jī)遇和挑戰(zhàn).最后舉了一個例子來說明如何逐步進(jìn)行微服務(wù)化.
感謝大家的聆聽.
Q1:使用Docker部署微服務(wù)持續(xù)交付時,應(yīng)該注意什么?你們的Docker使用情況是怎樣的?
A1:我現(xiàn)在做的是一款持續(xù)交付產(chǎn)品,本身會有一個構(gòu)架集群,執(zhí)行任務(wù)使用的是Docker,但集群軟件本身并沒有使用Docker來不熟.不然就是在Docker中運(yùn)行Docker了,性能會有些影響.
前端的portal正在做Docker化,還沒有應(yīng)用到生產(chǎn)環(huán)境中.一個可以分享的就是,要把自己的應(yīng)用的相關(guān)配置都環(huán)境變量化,這樣對于Docker化比較友好.
我們還有一個產(chǎn)品是code.aliyun.com.這個產(chǎn)品也沒有Docker化,或者說『不可變服務(wù)器化』,原因就是因?yàn)橐诒镜卮疟P寫代碼庫.
所以上面提到對本地讀寫要求比較高的應(yīng)用做『不可變服務(wù)器』還是有些困難的.
Q2:你們目前關(guān)注的持續(xù)集成的量化指標(biāo)有哪些?比如項(xiàng)目單元測試/接口測試/靜態(tài)檢查等方面的內(nèi)容.整個持續(xù)集成有效運(yùn)轉(zhuǎn)的效率如何?失敗了怎么辦?怎么保證交付系統(tǒng)的穩(wěn)定性?
A2:指標(biāo)主要是測試覆蓋率、代碼復(fù)雜度,及checkstyle檢查出來的一些問題.持續(xù)集成會有一個大屏幕把信息輻射出來,所以如果出錯了,所有人都能看到會要求把CI break的同學(xué)立即修復(fù),并且在修復(fù)之前不允許新功能的提交.
Q3:問個小問題,崔力強(qiáng)老師描述的package能包括什么內(nèi)容?
A3:可以是純粹的應(yīng)用,比如war包.也可以是一個壓縮包,里面包含了war包和安裝腳本,這樣這個軟件包就是可以自安裝的.
Q4:Windows的架構(gòu)會支持嗎?
A4:剛才提到的Ansible是支持Windows的.Docker的話,現(xiàn)在出了原生的Docker,可以做開發(fā)測試之用.但生產(chǎn)環(huán)境的性能如何,還需要測試一下.
Windows上有原生的Docker,我是mac用戶,win版的Docker其實(shí)也沒有用過,只是看到Docker官網(wǎng)的消息:https://www.docker.com/products/docker :)原生的mac版Docker的volume掛載的性能也很差,猜測可能win版本的也不會太好.
文章出處:DBAplus社群
轉(zhuǎn)載請注明本頁網(wǎng)址:
http://www.snjht.com/jiaocheng/4469.html