今天的話題是,持續集成和“云”,主要部分是我之前兩年的工作和我的一些個人思考。
這個話題我之前在中國的ruby大會上講過,slides在這里 ,供參考,不過但是現在講的內容根據最近多半年的工作進展又有所變化。
先自我介紹一下,我是軟件工程師,從業13年,主要從事的領域都是應用系統開發,涉及OA、電信網管/增值業務、互聯網等領域。
2009年底加入阿里,2015年4月離職,主要做的事情:廣告應用系統 -> 運維自動化平臺 -> 持續集成服務平臺。
最后的持續集成服務平臺是來自于實踐需要,我最先在做廣告業務系統的研發工作,廣告系統雖然復雜,但是其中的應用系統從軟件架構上看并沒有什么特別的地方,所以希望將精力投在可以改進團隊工作水平的地方。
一開始是一個運維自動化平臺,由于團隊人手有限,我基本是一個人做的,發現開發效率很好,軟件質量也不錯,所以在工作中總結了一些質量改進的實踐,在團隊中推廣,這是我從研發進入QA的起點。
經過一段時間摸索,我們發現測試自動化搞不起來的原因之一是成本太高。
我之前習慣用ruby或者rails,所有的測試都可以單機完成,用cucumber這樣的工具可以做BDD,用vagrant可以避免環境污染,所以自動化沒問題。而java就沒有這些條件了,數據庫掌握在DBA手里,測試的linux是大家公用的,很容易引起沖突。
于是我和主管商量,決定搞一個平臺,通過它降低研發成本,在本團隊開展一段時間以后,又帶著系統轉到技術質量部,把CISE做大,測試部門成立了專門的團隊,離職前已經開始在各bu和研發團隊廣泛運用
對CI的理解
持續集成平臺究竟解決什么問題呢?
簡單說,就是兩個自動化:
構建自動化
測試自動化
測試自動化好理解,構建自動化比想象中要復雜一些,我一般用下面這張圖來解釋——
我們手里的軟件,總是可以進行不斷地分解,從系統到模塊,最后到類和方法。由于整體和部分的功能不能完全劃等號,所以測試需要在各個層面上進行,但是這些測試的成本和收益有所不同。
如圖,我之前感到java開發的痛苦,就源于工程師手中沒有更上層的“武器”,所以只能在單測上用力。
所以,如果我們能做好更大尺度系統的自動化的構建,那么研發人員也就有機會使用“高層”的自動化測試,而避免在細節上寫太多的用例。
但這個并不容易,我們的努力也只是起到了一部分作用。
平臺介紹
下面說一下這個平臺本身,由于涉及到阿里巴巴內部系統,有些是不能說的,不過還好,核心部分并沒啥技術含量 :-)
很多人對CI的了解是基于jenkins,當然也有些人接觸過travis CI或者circle CI,我們的系統更像后者,當然功能上要更強些。
平臺的起點是公司的gitlab和svn服務,通過自動監控或者hook觸發,每一次代碼提交都會觸發一個自動化過程,在這個過程中,平臺負責分配虛擬機、數據庫等必要資源,然后將代碼編譯打包構建運行。
編譯打包構建運行看起來是一個自動化過程,但每個環節實際上都可以有驗證——這其實就是各種測試。
編譯前可以做代碼掃描(有的語言是編譯后做代碼掃描)
編譯之后可以做單測
打包運行后可以做集成測試
......
和travis不同的是,如果目標涉及多個應用,之間存在服務調用,那么我們還會自動的將相關應用也部署好,然后在一個機器群里面做更接近真實場景的功能測試。
做過類似工作的同學一定知道這一點的代價有多大,但這樣會有很大好處——我們可以在一次commit后自動進行所有層面的測試——從單測到系統交付測試。
當然這是理論上的,實際中可以根據研發團隊需要進行選擇。
總結一下,我們認為,CI平臺應該能進行所有粒度的測試,最小針對函數,最大可以針對分布式系統。作為前提,CI平臺需要支持整個系統的自動化構建。
這個大概是和travis CI之間最大的不同,下面再列幾個不是很重要的區別。
1.使用云平臺解決虛機問題,身在阿里巴巴,所以我們使用阿里云的ECS,需要資源是隨時申請,用過以后立即釋放重置。
這樣可以解決環境污染的問題(即使是java應用,有些團隊也會留下本地文件操作,這些東西會導致測試不可重復)。
2.數據庫自動分配,我們構建了mysql集群,不過不是一般意義上的那種協作集群,而是一個數據庫池,讓數據庫和虛機一樣隨用隨取,用后重置。
這樣做是為了讓java程序員也可以像rails的db migration一樣可以用到干凈的數據庫。
對這個平臺的介紹就這些,下面討論一些經驗教訓。
經驗教訓
UI應該盡量輕
這個話題要和jenkins/hudson做對比,這兩個系統我其實不是很熟,不過也知道它們都是很強大的自動化系統。但是,jenkins/hudson的最大問題是,它們的UI做的太多了,用戶可以在UI上做很多事——很多和CI沒關系的事情。
舉個例子:
jenkins的任務是可以排隊調度的,而對于CI來說,排隊是什么意思呢?研發工程師養成持續小步提交代碼這個好習慣以后,又硬生生由于資源不足而被迫等待,最終可能會放棄這個好習慣,實在是不劃算。
我們的辦法是——敞開供應,只要有代碼提交就分配機器,當然有人會質疑,因為這會導致需要一個很大的資源后備池,不過這可能正是“云”時代的不同思考方式——在“云”的時代,資源的使用毛刺應該通過大規模后備池來抹平。
當然,濫用資源還是要避免的,只是我們認為需要“后置懲罰”,比如通過審計,找出資源消耗大戶,打他的板子 :-P
當我們把所有的額外功能都剝掉以后,發現UI其實就一個作用——展現,因為CI需要的是完全的自動化,人工本來就不需要介入,只要最后被notify一下,或者偶爾過來在web上看看報表趨勢什么的就夠了。
CI是服務加最佳實踐
我們理解的第二個經驗就是,搞CI,是服務加最佳實踐,所以一定要指導研發團隊,而不能完全任由研發團隊提要求,很多團隊的工程師良莠不齊,對各種編程api比較熟悉,但是可能缺乏做事的好習慣,這時需要指導他們,當然前提是你也要對開發很了解才行,否則會被鄙視的 :-P
對CI來說,最大的常見麻煩是不寫測試,這個一般是通過管理教育,比較簡單。
另外一個隱藏的比較深——開發人員對系統的運行并不了解,比如開發的系統運行在linux上,但是基本的命令都不會。
這個問題我們遇到了挑戰,有人認為這涉及到分工,研發工程師不應該了解線上,但是這個觀點是有問題的。
因為很多bug和環境相關,如果開發人員不能在”真“的環境中嘗試,一旦系統報錯,很難不發生扯皮——這個扯皮可能發生在開發和測試之間,更可怕的是發生在線上,光定位就需要N多人參與,成本極大。
插件
最后是插件的問題,這個說來簡單——插件能解決一些問題,不過插件設計之初實際上就限制了其使用,所以我總結的教訓就是:先別急著做插件,想好了設計再動手。
[page]更進一步
先進一步談談對CI平臺職責的理解,然后結合這些理解最后說說“云”和docker的作用
CI平臺的職責
關于CI平臺的職責,我上次已經提過,基本上就是這些:
構建自動化:提供環境
測試自動化:提供平臺
先說構建
構建工作主要產出兩個東西:最終輸出的軟件包和待測的應用系統(有時后者會包含前者),這兩個產出的核心要點有所不同。
最終輸出的軟件包大多數公司都會做的,重點是構建過程環境無關而且可重復,因此需要提供配管服務,比如最好有yum、npm、gem等軟件包服務。
但是這里有個問題——間接依賴的軟件包如何鎖定的問題,ruby的bundle機制很不錯,通過Gemfile.lock讓依賴包都有明確的版本(而不是“最新版本”這種含糊的說法),但是maven就沒有這樣的支持,目前沒有很好地辦法,只能讓構建號和軟件包號建立關聯,便于回溯。
而待測的應用系統是比較難的地方,它也需要配管系統支持,同時還需要資源的就緒能力,一個關鍵要點是——資源必須是隔離的,否則很難避免測試時互相干擾。
這個隔離應該做到什么程度呢?舉個例子,我們為了進行系統聯調,自動構建了一套涉及N個系統的集群,這個集群是為了進行自動的聯調測試。而理論上我們如果需要,CI平臺應該可以再構建另一個集群,各種架構細節和前一個集群完全相同,但這兩個集群進行工作時應該互不影響。
這要求應用需要符合一些最佳實踐的要求,比如12 factor里面說的不要硬編碼ip地址等等
為什么要這樣?因為持續集成是不斷前進的工作節奏,一個人的工作有了階段性結果后,需要進行驗證,這種驗證應該是獨立的,如果和其他人共享測試環境,要么測試結果不穩定,要么變成我上次說到的排隊,那就降低了效率。
這兩張圖說的就是這個意思,同一團隊的兩個程序員公用數據庫進行測試,測試結果就會不穩定,同樣,如果他們的應用依賴了另一個公共服務,那么測試依然不能穩定。
不知道第二種情況會不會難以理解,簡單說就是那個公共服務也是有存儲的,所以那個數據庫里面的數據會造成干擾。
構建自動化比較復雜,而測試自動化相對來說簡單一些,不過這里的重點是對各種測試的抽象和區分
根據上次分享的反饋,發現有相當多的人不太理解這張圖。
我們把測試分為UT/FT/IT/ST等等,但其實它們可以抽象成一個東西——都是對某個軟件單元的驗證,區別在于單元的粒度
補充說明一下上面說的一些名詞:
UT:單元測試
FT:功能測試
IT:集成測試
ST:系統測試
所謂的“上層用例變化慢“,是相對的,因為應用系統常常是需求一變,整個推翻,所以上層需求用例的變化常常帶來下層大量用例的變化,而反之則未必.
考慮到需求和測試用例直接聯系很緊密,我們可以認為需求用例的變動頻率和測試用例的變動正相關。
當然,測試自動化還有一個要做的工作是對結果的分析匯總,主要是各種測試手段的輸出千差萬別,需要進行數據匯總,這個和普通的數據處理沒啥區別,我也不是這方面的專家,就不獻丑了。
但是,作為CI平臺,對結果數據的分析匯總要建立在測試階段的界定上,簡單說就是要明確區分UT、IT等環節階段,這是后續報表很重要的信息,不能小看。
CI職責講完了,我下面想說一下自己對“云”在CI方面價值的理解——簡單說就是標準化。
CI的自動化測試和普通測試一樣,有天生就要面對的問題:
bug確認(可重現)
代碼覆蓋
測試的真實性
先說bug確認的困難,在測試團隊待過的人一般都能理解問題確認有多麻煩,很多的bug是辛辛苦苦發現的,結果被開發同學一句“環境不一樣”就打發了。
所以測試非常需要標準的環境,而這正是“云”可以提供的,要是進一步考慮自動化測試,那是比普通測試更需要標準環境的場合,因為它是無人值守的,對意外的適應能力更弱。
代碼覆蓋也是測試的一個重要指標,幾乎所有的開發語言和框架都有不止一個代碼測試覆蓋率統計工具,而代碼覆蓋其實是涉及到測試層次的,在上層測試一個系統,往往能夠覆蓋不少下層用例,如果能從多個層次測試系統,可以讓工作事半功倍。
最后再說一下測試的真實性,這里是指對mock技術的使用。我們很多時候使用mock技術只有一個原因——對方系統太難打交道了,所以做個mock先繞開(有時需要模擬對端錯誤,這種情況還是需要mock的)。
但是真實情況下我們訪問的不是白板方法,這么做的有風險,最后還是要聯調,所以這種情況是把測試推后了,是轉移矛盾而不是解決矛盾。
我之前做的CI平臺,正是想通過云技術,可以相對低成本的構建“全部系統”,因為(借助我們的平臺)有時候這個做法比mock要簡單,更重要的是,這種做法肯定比mock要真實。
以上都是云的價值,也是docker的價值,不過docker有個獨特的價值,就是有可能將測試甚至運維工作變成服務。
這里說的服務不是那種在公司里某個部門為其它團隊提供的服務平臺,那很容易模糊邊界(比如開發要求測試幫忙等等)。
這里的服務是指成立公司,把這些工作變成business的東西
當然,這塊其實不稀奇,我之前說過的Traivs CI、shippable、coding應該都在做,docker創業團隊大多都在做這個,不過我想說的是——為什么這事變得可行了?
這是由于標準化,一個應用應該是個什么樣子?在docker的語境中是比較一致的,這為用戶和服務平臺提供了相對簡單的協作邊界。
舉個例子,這是我們平臺自己的一個模塊在進行自動化構建時寫到描述文件(相當于 .tavis.yml)中的內容:
prepare:
exec:
- yum remove taobao-jdk -q -y
- yum install -b test ali-jdk -y -q
- echo 'export PATH=/xxxxxx/bin:$PATH' >> /etc/bashrc
- echo 'export JAVA_HOME=/xxxxxx/java' >> /etc/bashrc
- yum install jemalloc -b current -y -q
- yum remove mysql* ruby19 -y -q
- yum install mysql-devel -y -q
- yum install ruby21 -b test -y -q
- echo /usr/local/lib >> /etc/ld.so.conf.d/ruby21.conf
- ldconfig -v
- gem sources -r https://rubygems.org/
這么一大坨,我不相信會有人能受得了,但是如果在docker中呢?
prepare:
exec:
- docker build -t xxxxx .
這樣還符合我之前說的原則——開發人員自己管理環境
在這種變化下,我覺得我們做的東西可以大幅度簡化,以至于變為一個對外服務的business,根據這個想法,我自己做了一點簡單的嘗試,不過是用青云做的,錄了兩段很短的視頻,在這里:
http://v.youku.com/v_show/id_XODAzNzgyMjUy.html
http://v.youku.com/v_show/id_XODAzNzgxOTky.html
我要講的基本上就這些,今天應該沒超時,最后關于docker上的CI再多說兩句,提個想法——docker應該依靠但不依賴IAAS。
借助SDN,避免通過Docker來劃分安全域:我看了一點最近docker大會的介紹,老實說,有一種觀點是把vm廢掉,我是不以為然的,所謂vm所帶來的成本其實正是我們獲得安全隔離的原因,這兩者是一體的,我們直接拿來用vm做安全隔離最好,讓docker解決安全問題應該是一條歧路。
深度依賴compose機制,建立聯調/系統測試的構建標準:應該會有很多人和我想的一樣——將compose.yml作為重要信息來源,外部的系統通過它理解應用間的聯系,即使不使用 docker compose 這個軟件,也應該遵循 compose.yml 的規范,這樣我們不但能讓單兵(容器)的外部邊界清晰,還能讓戰陣(分布式系統)也能被管控系統理解和支撐。
深度依賴compose需要讓compose.yml目的變得純粹些,我知道compose描述的內容很容易變成“不同環境”,其實這個想法也許不正確,我認為一個git分支就對應一種場景——比如某些分支是用來開發局部功能的,它不需要系統測試和聯調,而master必然要聯調——所以不用在代碼中留下多份 compose_xxx.yml ,而應該在不同分支上編寫不同的 compose.yml ,其內容由分支維護者負責。
分享人李建業,前阿里巴巴員工(花名:李福),2002年本科畢業,之后一直從事軟件開發,涉及辦公自動化、電信網管/增值業務系統以及互聯網;2009年12月加入淘寶的廣告應用開發團隊;從2011年底開始,關注軟件研發本身,主要工作包括運維自動化系統和持續集成服務平臺。