天天看點

雲上應用docker化持續傳遞實踐 — 【包含Qcon講稿】

因為我非常啰嗦,是以寫的分享也太長,全部内容被内部同學review後的回報是: 像看小說一樣……

是以為了防止大家看了開頭就去逛别的小網站了,開篇我先點個題, 這篇檔案最終的目的是講清楚下面這張圖:

雲上應用docker化持續傳遞實踐 — 【包含Qcon講稿】

就是一個完整的,應用docker化持續傳遞需要做的事情。

并且,這篇文章不是硬廣, 圖中涉及到的服務也是基礎服務,提供便捷的配置方式,最佳實踐的推薦。

我們并不去定義标準和規範,會相容業内所有的規範和标準的玩法。

下面開始正文:

阿裡雲容器服務

其他會舉例引用等都來自docker.com , github 和 travis-ci 等業内标準。

傳統cd過程中遇到的問題


變革軟體傳遞方式的技術: docker

應用docker化傳遞的過程實踐

在幹貨之前,先要引導一下為什麼要做持續傳遞,任何一家網際網路或者軟體公司,随着産品規模的擴大,市場需求的變化,都會逐漸的發現産品版本管理混亂,運維人員總是在兜底, 不知道開發/測試/內建/預釋出/生産等等環境到底經曆過幾代運維人員之手,是以環境壓根沒人敢動。

因為市場永遠在變化, 需求一定在變化,人員也在變化,導緻了研發過程中遇到的這樣那樣的問題。 是以,大多數企業都用ci/cd 這個解決方案來應對 , 如下圖:

雲上應用docker化持續傳遞實踐 — 【包含Qcon講稿】

額外說一下,我們認為的持續傳遞概念總結如下:

<code>在一起</code>就是內建,每次內建都應該有<code>回報</code>。

隻有不停的內建才是持續內建。越少持續,每次回報<code>代價越大</code>。

<code>多次內建</code>産生一次傳遞。

ci/cd 是無法提升你的代碼品質的,是無法解決你代碼中的bug的,但能夠提升效率和品質的原因是: 他能把問題發現在前面, 讓小問題提前暴露出來.

我們說做持續內建最重要的是<code>有效回報</code>和<code>持續</code> ,因為ci就像體檢服務一樣,好比我這個胖子要減肥,體檢服務不能讓我吃的更少動的更多,但我如果每天都稱一下體重,就能随時知道自己身體的狀态,随時知道我每天該幹什麼, 這就是持續的重要性。

如果我不做這個事兒,很可能等到我年度體檢的時候才發現,tmd脂肪肝又加重了。。 同理如果每次代碼送出都能自動和其他代碼內建,和測試環境內建, 就不會出現最終釋出的時候出現各種各樣的問題, 也就是剛才說的運維總在兜底的問題。

ci過程的<code>有效回報</code>也很重要,每次內建都應該給出準确的問題定位和建議, 誰的代碼merge出現沖突,誰送出的commit導緻ut失敗, 誰應該立刻去解決什麼樣的問題, 這都是有效的回報。 就好比我中午沒吃飯,去稱一下體重,體重秤告訴我:還湊合。 那這個回報讓我晚飯是吃。。還是不吃呢? 。。 這就是無效回報。

簡單來說,持續傳遞的pipeline就像下面的管道圖一樣:

雲上應用docker化持續傳遞實踐 — 【包含Qcon講稿】
當然這個圖裡的每個節點(stage)的定義并不适用于所有應用,每個stage 是不同角色,運作需要耗費不同的成本,那麼隻要保證每個stage 是一個獨立有效的回報就是正确的持續傳遞pipeline 。

那麼,建構出能夠運作這樣pipeline的一個環境,都需要什麼東西:

雲上應用docker化持續傳遞實踐 — 【包含Qcon講稿】

如上圖, 你需要有代碼托管服務(存儲),運作ci中的單元測試,編譯打包服務(環境), 如果你的應用已經托管在公共雲上,還要涉及到網絡問題。 也就是你核心要解決的除了需要服務本身,關鍵是解決“存儲,環境和網絡”這三個問題。

現在,當你辛辛苦苦做好了這些過程之後,仍然會遇到一些問題:

每次build,是需要不同的build環境的

編譯環境維護困難

每次內建 test,是需要依賴其他環境,被依賴的環境不受送出者的控制

依賴環境維護困難

每個package, 在不同的環境, run的結果是不一樣的

切換環境調試困難

每個package ,是無法回溯的

運作包的版本維護困難

每個環境,是不同的維護者(開發環境,測試環境,生産/産品環境)

統一環境标準困難

每個環境,除了維護者,是無法清楚知道環境的搭建過程的

環境回溯,更是難上加難

why ? 為什麼會遇到這樣那樣的問題? 為什麼開發人員經常抱怨: “明明我的程式在測試環境已經調試好了,為什麼一上生産環境就運作不了 ? ”

歸根結底的原因是:

開發人員傳遞的隻是軟體代碼本身, 而運維人員需要維護的是一整套運作環境,以及運作環境之間的依賴關系

雲上應用docker化持續傳遞實踐 — 【包含Qcon講稿】

有人說:“傳遞方式的變革,改變了全球的經濟格局”

雲上應用docker化持續傳遞實踐 — 【包含Qcon講稿】

那麼,在軟體開發領域,docker ( an open platform for distributed applications for developers and sysadmins) , 就是<code>變革軟體傳遞方式的技術。</code>

雲上應用docker化持續傳遞實踐 — 【包含Qcon講稿】

回到第一章節的問題, 我們找到了開發和運維之間問題的關鍵,找到了寫代碼和維護生産環境之間的核心差别, 那麼我們yy一下。

如果我們能像描述代碼依賴關系一樣,描述代碼運作所需的環境依賴呢? 如果又能像描述應用之間的依賴關系一樣,描述環境之間的依賴呢?

假定,我們的代碼中有一個檔案,定義了運作需要的環境依賴棧(就像pom.xml檔案中定義了java應用的jar包依賴一樣)

建構時,我們能根據整個檔案,将所有軟體依賴棧安裝到一個鏡像中,鏡像是隻讀的。任何變更都會新産生一個新的鏡像而不會更改原先的鏡像。

并且隻要這個鏡像不變,鏡像起來的容器之内的環境也不變。

那我們是不是可以像把代碼,依賴,測試腳本,環境依賴,環境描述等等這些東西裝到集裝箱中一樣, 集裝箱作為一個整體來傳遞, 作為一個整體在不同的平台上運作, 集裝箱不變,任何平台上運作的結果都不變。 yy思路如下圖:

雲上應用docker化持續傳遞實踐 — 【包含Qcon講稿】

如果我們能輕松的傳遞整個軟體依賴棧, 是不是剛才說到的在不同環境調試的問題就能大大減少或者不複存在了?

這個yy過程正好被docker 技術所覆寫, 我們看一下docker 提供什麼樣的能力,能滿足剛才的yy:

描述環境的能力

提供了描述運作棧,并且自定義build 過程的能力。code 中的描述檔案就是dockerfile

分層檔案系統

image可以像git一樣進行管理,并且每一層都是隻讀的,對環境的每個操作都會被記錄,并且可回溯

docker registry

提供了管理image 存儲系統,可以存儲,傳遞,并且對image進行版本管理

屏蔽host os 差異

解決了環境差異,保證在任何環境下的運作都是一緻的(隻要滿足運作docker的linux 核心)

這幾種能力天然的幫助我們解決環境描述和傳遞的問題, 是以docker 能夠做到<code>build once, run everywhere !</code>

是以,軟體的傳遞方式,變成了最簡單的 build -- ship -- run, 如下圖:

雲上應用docker化持續傳遞實踐 — 【包含Qcon講稿】

這是本文最核心的一章, 首先先看個例子,用docker做持續傳遞能帶來的好處, 避免廣告嫌疑,我用docker官方網站上的案例: bbc news

簡單來說,一個全球新聞中心,内容的變化是最快的, bbc 公司内部的第一個問題是涉及10幾種ci環境,26000 jobs,500dev人員

第二個核心問題是,ci任務需要等待,無法并行

經過docker化改造之後:

雲上應用docker化持續傳遞實踐 — 【包含Qcon講稿】
最明顯的改變,開發可以自己定義自己的開發語言,自己所需的build,內建測試環境,以及應用運作所需的依賴環境。

基本思路如下:

安裝好docker環境

docker 化你的應用運作環境

docker 化你的應用編譯,ut環境

docker 化你的應用運作的依賴環境

這個現在已經非常非常的簡便了:

配置安裝

安裝雲驅動

<a href="https://help.aliyun.com/document_detail/containerservice/developer-tool/summary.html">ecs driver for docker machine</a>

aws, gce, etc.

建立docker運作環境

這句話可以翻譯為: 如何将我的應用環境通過dockerfile描述出來?

假如我的應用是一個java web 應用,需要java運作環境和tomcat 容器 ,那麼大概我的環境所需下面這些東西:

某linux發行版作業系統

基礎軟體(起碼有個能解壓縮包的吧)

openjdk 7 &amp;&amp; 配置 java home 等環境變量

tomcat 7 &amp;&amp; 配置 環境變量

應用包 target.war

應用包 啟動參數 jvm

web server 指定端口 8080

啟動tomcat

轉化為成dockerfile 的語言大緻如下:

可以看出 ,dockerfile 第一步永遠是from 某個鏡像, 開始安裝了一些基礎包(這裡是jre7), 又設定了java的環境變量, 之後安裝tomat(這裡是7.0),再聲明啟動8080端口,最後運作tomcat的啟動腳本結束,在最後結束之前将我的web 應用.war包copy或者add進去即可。

我們再看一個nodejs的環境:

關于這個環境,copy了本地的sources.list和.npmrc 到容器中,是更換了安裝源為mirrors.aliyun.com 和 npm源為npm.taobao.org , 國内源更快。 其他就是安裝了基本的nodejs 運作環境

那麼通過這兩個例子,我們發現dockerfile 還是寫起來很麻煩的(其實也不麻煩,就是剛剛說的裝要裝的東西,配置,運作這三步)。 那麼,剛剛說到每一個dockerfile的第一行都是from另一個鏡像, 那麼思考一下:

如果有一個安裝好java的環境 ?

如果有一個安裝好java和tomcat的環境 ?

如果是微服務,對環境隻依賴java/node基礎環境,是不是所有應用都可以共用1個環境?

通過這些思考,得到如下尋找docker鏡像的過程:

尋找java鏡像 ,選擇鏡像版本, 檢查 dockerfile

尋找tomcat鏡像,選擇 tomcat &amp; java 版本, 檢查 dockerfile

測試運作 : <code>docker run -ti —rm -v /home/app.war:/canhin/webapp/ tomcat:7-jre7</code>

說句題外話,這個思路同樣适用于公司内部,因為dockerfile 明确劃分出了開發和運維的邊界, 如果公司有統一的運維标準,比如某個作業系統的某個版本, 某種确定的web server, 這樣開發隻需要from 運維提供的鏡像來描述自己的應用環境特殊的部分就好了。 如果大家的環境都一樣,調試和測試的過程中,隻需要把應用代碼通過-v 的參數挂載進去運作就好了, 這樣世界就變的很簡單和清楚了。
雲上應用docker化持續傳遞實踐 — 【包含Qcon講稿】

跟我所需的環境基本一緻, 那就可以基于這個鏡像再寫自己的dockerfile ,這樣就會簡單很多了。

編譯/ci環境往往在公司規模越來越大的時候, 變得越來越麻煩, 因為不同語言,不同類型的應用對編譯環境的要求都不一樣。 就像剛才說到的bbc news的例子,一個大公司幾十種編譯環境的存在是很正常的。

那麼,編譯環境docker化最大的好處是: 自定義,可擴充,可複制

試想一下, 假如你的應用編譯隻需要依賴标準的jdk 1.7 和 maven 2, 或者你是python應用編譯過程其實隻是需要安裝依賴, 那麼你可以跟很多人共用編譯鏡像。

但假如你的應用是nodejs ,編譯依賴特定的c庫, 或者是c++之類的編譯環境一定要和運作環境一緻等等,那就需要定制自己的編譯環境了。

這裡我做一個最簡單的用于編譯java的鏡像示例:

編譯鏡像的docker file 示例:

上述dockerfile的build.sh示例:

運作方式示例:

解釋一下這個過程:

我的編譯環境需要centos7 系統, 安裝jdk1.7 , 然後把maven 的setting (這裡主要配置指向其他私有nexus) 和 編譯腳本拷貝進去。

編譯腳本也很簡單,就是maven編譯打包指令,并且把最終生成的war拷貝到一個定義好的docker目錄下, 這個目錄随便定義。

最後是運作方式, 即把源代碼挂載到容器裡進行編譯, 同時可以選擇把本地的.m2 緩存到鏡像内加快編譯速度

這裡提兩個小提示,都是經驗之談:

建議: build app 和 build docker image 建議分開進行, 即先進行應用本身的編譯,再将輸出物拷貝到鏡像内(但腳本語言可以例外) 因為:

鏡像分層概念導緻源碼可能洩露:因為dockerimage 每一層都會儲存一個版本, 即便是add代碼進去,編譯後再rm掉,也可以通過擷取add這一層鏡像拿到源碼,因為鏡像是運作在各個環境中,是不應該包含源代碼資訊的。

鏡像最小化原則:編譯環境可能需要和運作環境不一樣的東西,比如maven的配置,nodejs的一些c庫的依賴, 都不需要在運作環境中展現,是以本着鏡像應該最小化原則,不需要的東西最好都不要放進去,也應該分開進行這個步驟。

是以,整個過程還是分為build app和build docker image 兩個過程,類似下面這個簡單流程 :

雲上應用docker化持續傳遞實踐 — 【包含Qcon講稿】
建議: docker file 不要放到代碼根目錄下

避免大量檔案傳給docker deamon : docker build會先加載dockerfile同級目錄下所有檔案進去,如果有不需要add/copy到鏡像裡的檔案不應該放到dockerfile目錄下, 可以試一下把dockerfile放到系統/根目錄下,這時build 十有八九就會讓docker deamon挂掉。

簡單思路: 運作docker 鏡像環境,安裝測試所需依賴 , 運作docker容器, 運作測試指令/腳本

用一個travis-ci 官方的例子來說明容器測試這件事,先看下面一個ruby的鏡像:

簡單來說就是标準的一個ruby 鏡像, 啟動4567端口 。 那麼通過這個鏡像進行的測試過程如下:

這個其實就是大家可以在本地進行的一個過程,在before install部分内可以看到過程是:

先build出運作環境的鏡像

運作這個鏡像,看看服務能否正常啟動

檢視容器是否存活(保證容器不是運作一下就挂了退出)

運作測試

再來看一個python的例子,也很好了解:

簡單來說就是運作容器, 安裝依賴, 運作測試腳本 。 或者 直接通過下面一行指令進行

<code>docker run -v mycode:/ws mytestimage:master /bin/sh -c "python3 djanus/manage.py test djanus mobilerpc "</code>

tips: 這裡不是說推薦大家用travis-ci ,但travis-ci 制定了一種文法标準, 非常清楚的能夠看到整個過程。 同時: <code>阿裡雲持續傳遞平台未來将會完全支援相容travis-ci定義的yml文法結構</code>

剛剛說了單獨一個容器運作測試的情況, 但實際情況可能是即便是運作測試,也需要依賴proxy,依賴db,依賴redis等。 簡單來說一般web應用會需要下面的結構:

雲上應用docker化持續傳遞實踐 — 【包含Qcon講稿】

這個結構很簡單也很常見, 那在傳統思想裡,要運作ut或者內建測試,需要依賴的元件,都是去搭建。 搭一個mysql,配置mysql ,運作mysql 這種思路。

但是在docker的思想裡,是聲明的概念,就是說我需要一個mysql 去存一些資料進行測試, 這個mysql運作在哪裡我根本不care 。 同樣的思路告訴docker:

i need 負載均衡(haproxy,nginx)

i need 資料庫(mysql)

i need 檔案存儲(通過-v , ossfs)

i need 緩存服務(redis,kv-store)

i need ...

這時,用于編排多個docker image 的服務,docker-compose 就出現了,官方文檔裡用三張最簡單的圖表明了compose是怎麼用的:

雲上應用docker化持續傳遞實踐 — 【包含Qcon講稿】

就是說,我運作一次測試, 需要mysql, 那我就啟動一個mysql容器就行,通過link 的方式将我的app連結上,配置一個密碼即可,至于其他的資訊,我根本不需要,或者說不關心。

再舉一個例子,假設一個php的wordpress 應用, 除了應用本身還需要一個db ,他的編排檔案(docker-compose.yml)如下:

除了自身的配置,檔案挂載之外,聲明的mysql 就是官方5.7的版本,隻需要設定一個密碼即可, 這樣直接運作起來無論是提供服務, 還是運作測試, 都非常的友善。

tips1: compose的好處還在于将配置從dockerfile中提取出來,比如在測試/生産環境所需要的配置差别, 就可以放到compose裡,在不同環境運作的時候換不同的compose檔案即可,不用重複的編出不同環境用的docker image
tips2: 上面的wordpress示例裡,啟動多個應用容器之上,并沒有用nginx做代理,因為阿裡雲容器服務提供了routing,省去了這部分, 如果是在企業内部,當啟動三個應用,還是需要在compose裡再聲明一個nginx 或者 haproxy 在前面做應用代理和負載均衡的。

現在回到文章一開始那張圖 :

雲上應用docker化持續傳遞實踐 — 【包含Qcon講稿】

對一個公司/企業來說,将自身應用docker化,編譯服務,測試叢集docker化之後, 要跑通整個的過程,達到bbc news 這樣的效果, 整個流程就如圖中所示:

代碼,測試腳本,配置,dockerfile/compose 等從開發本地push到代碼倉庫中

代碼倉庫能夠hook 這個資訊,通過事件trigger build 服務,通過容器進行app build,運作test, 通過後對應用進行docker image 的build

build 好的docker image push到遠端docker registry 用于存儲和傳遞

當build test 都pass之後, 通過deploy service 告訴應用叢集進行更新,從docker registry 上pull 下來新的image進行應用更新,或更新叢集配置

docker技術是 devops 的最好诠釋, devops不是開發去做運維的事情, 而是:

将程式設計的思想應用到運維領域

舉例來說: immutable,copy on write 這些思想在研發領域是耳熟能詳的,好處大家秒懂。而在運維領域的immutable,傳統是怎麼做的? 靠組織架構,權限管理。各種人為訂制的機制,規範。 而docker 是用技術來解決了這個問題, 官方文檔的介紹docker是 an open platform for distributed applications for developers and sysadmins, 很明顯看到了devops有木有?

由于應用的軟體依賴棧完全由應用自己在dockerfile中定義和維護 ,是以開發人員能夠更清楚,更靈活的掌控自己的軟體運作環境。 運維人員也不用為應用軟體依賴棧的變更碎片化自己的時間。

最最重要的一點,dockerfile的存在,非常清晰地将研發和pe的責任和界限劃厘清楚了。 開發人員可以from 運維人員提供的基礎鏡像,配置自己應用的依賴棧; 運維人員可以from 更底層的系統工程師的基礎鏡像, 配置環境依賴棧; 系統工程師則定義了一個公司的基礎linux系統所需的版本和配置。

另外,從資源的角度上講, docker化能夠大大減少開發/測試環境的成本,測試或者調試的場景是當發起測試的時候才需要, 其他時候測試環境并不承擔業務, 如果用虛拟機則白白的在那裡空跑。 docker 化之後可以在需要的時候随時拉起來整個環境,很快,并且不會出錯, 是以阿裡雲持續傳遞平台crp在會在将來提供內建測試環境,作為一項基礎服務, 如果沒有容器化,那提供整個服務的成本和可行性都是無法想象的

片尾:希望大家都能運用docker技術做到被說了很久但無法落地的:devops

雲上應用docker化持續傳遞實踐 — 【包含Qcon講稿】

不是都片尾了怎麼還有花絮? 因為我感覺剛才通篇說的好像把docker神話了, 為了防止大家出現過度崇拜和追捧的情況,還是要回過頭來考慮一下, 到底什麼樣的應用适合docker化,換句話說到底什麼樣的應用适合容器化?

雲上應用docker化持續傳遞實踐 — 【包含Qcon講稿】

如上圖: 我們認為web應用,微服務,這種即無狀态(是指好比一個web應用,通過10台伺服器提供服務,當挂掉1台的時候流量自動被其他九台分攤,不會影響到使用者,這樣就叫無狀态應用),又生命周期很短的業務适合docker化, 反過來,每個應用都是有狀态,有存儲的這種情況,不太容易docker化,或者說docker化的好處不明顯 。

但我們認為的也不一定是對的,今天docker技術,容器技術發展的速度太快, 是以花絮裡這個問題 let’s think out together ……

本文基本上表達了我在qcon2016北京站上的分享,和阿雲其他同僚在各種運維,容器大會上分享裡關于容器化持續傳遞的思想, 表達方式不同,但思想基本上是一緻的。

本來想寫成step by step的教程, 但考慮到docker 技術還是要了解它的特性和思想, 才能熟練運用,否則會出現把容器當vm來用等形式, 并且既然說了是完整版,是以分别用了java,ruby, nodejs, pyhton 和php 舉例,當然都涉及的很淺,都是很簡單的示例。

還是希望大家能對docker的特性有更深的了解, 講其長處運用在自己的公司,自己的項目和自己的應用中。