Docker:分佈式系統的軟件工程革命(上)

轉自:http://cxwangyi.github.io/story/docker_revolution_1.md.htmlhtml


Docker:分佈式系統的軟件工程革命(上)

做者:王益node

最後更新:2014年7月25日linux

歡迎轉載。請註明出處:http://cxwangyi.github.iogit

Docker最近很火。Docker實現了「集裝箱」——一種介於「軟件包」和「虛擬機」之間的概念——並被寄予厚望,以期革新Internet服務以及其餘大數據處理系統的開發、測試、和部署流程。github

爲了使用Docker,須要瞭解很多工具及其設計思路;而這些工具的文檔分佈在不一樣的網站。爲了方便你們學習,本文以開發一個極簡的搜索引擎爲例,展現Docker帶來的革新。golang

說是革新,實際上是Google已經用了不少年的方式,只是最近才由於Docker開源項目而廣爲人知。最近這將近十年的時間裏,各互聯網公司和高校都在奮力模仿Google的計算技術。瞭解這一模仿的過程,能夠幫助咱們深刻理解分佈式系統(包括如今常說的「大數據系統」)中若干重要問題。爲此,本文以技術教程爲主線,穿插了一些關於Hadoop和Mesos等「模仿」項目的介紹,簡要追溯它們勇敢而艱難的「邯鄲學步」的歷程。最後,本文會介紹Google最近公佈的「正確答案」——Kubernetes——Google核心技術Borg的開源版本。docker

Docker

Docker是一個軟件系統,實現了一種稱爲「集裝箱」的概念。集裝箱相似Google機羣管理系統Borg中的(package)。shell

一般咱們說的「包」是軟件包——好比Ubuntu/Debian Linux裏常見的.deb文件——安裝的時候,安裝程序會把被依賴的包也裝上。但是執行的時候呢?得根據具體狀況配置,而後依次啓動互相依賴的多個程序。好比,啓動一個Web服務以前,要啓動Apache和MySQL;並且他們仨都得有合理的配置,確保它們能一塊兒工做,來實現這個Web服務。apache

可是Docker集裝箱以及Borg中的包更像虛擬機。虛擬機裏包括程序和配置,因此能夠被執行——也就是執行其中的程序。由於程序是配置好的,因此虛擬機能夠被扔到各類環境上去執行——包括開發機、作演示用的筆記本電腦、用VirtualBox虛擬的機羣、測試機羣、預發佈環境和產品環境。近幾年隨着「雲計算」概念的普及,虛擬機被普遍使用,做爲分佈式計算的基礎調度單元。windows

Docker做爲一個軟件系統,能夠用來建立「集裝箱鏡像」(container image)和執行這些鏡像。就像VirtualBox是一個軟件系統,能夠用來建立和執行虛擬機。可是集裝箱比虛擬機「輕」——一個虛擬機包括一組虛擬硬件、操做系統,用來執行用戶程序;而集裝箱裏沒有虛擬的硬件,也沒有操做系統,它用主機(host)的硬件和操做系統來執行程序。

那麼在集裝箱裏跑程序和直接在主機上跑有什麼區別呢?一個區別是,集裝箱有一套網絡端口空間(port space)。一個集裝箱裏的進程能夠各自開端口,也能夠鏈接對方的端口進行通訊。可是這些端口是集裝箱以外的進程看不到的。咱們也可讓集裝箱把某些內部端口號展現給外部,好比把集裝箱內的端口5000映射到外部的8080。這樣,當咱們用主機上的程序(好比瀏覽器)訪問本機(主機)的8080端口時,實際上訪問的是集裝箱裏的5000端口。這項對外公開集裝箱內部端口的技術,稱爲端口轉發(port forwarding),和虛擬機的端口轉發概念同樣。另外一個區別在於,集裝箱裏有虛擬的文件系統。這樣咱們能夠把要執行的程序拷貝進集裝箱。也能夠把主機上的某些目錄映射成集裝箱虛擬文件系統的某些目錄。

集裝箱這個想法已經在深入地改變傳統分佈式系統的開發、測試和部署的流程了。傳統的作法是,開發者寫一個Makefile(或者其餘描述,好比CMakeList、POM等)來講明如何把源碼編譯成二進制文件。隨後,開發人員會在開發機上配置而且執行二進制文件,來做測試。測試人員會在測試機羣上配置和執行,來做驗證。而運維人員會在數據中內心的預發佈環境和產品環境上配置和執行,這就是部署。由於開發機、測試機羣、和產品環境裏機器的數量和質量都不一樣,因此配置每每很不一樣。加上每一個新版本的軟件系統,配置方式不免有所差別,因此常常形成意外錯誤。以致於絕大部分團隊都選擇趁夜深人靜、用戶不活躍的時候,上線新版本,苦不堪言。

而利用集裝箱概念的開發流程裏,開發者除了寫Makefile,還要寫一個Dockerfile,來描述如何把二進制文件安裝進一個集裝箱鏡像(container image),而且作好配置。而一個鏡像就像一臺配置好的虛擬機,能夠在機羣上啓動多個實例(instance),而每一個實例一般稱爲一個集裝箱(container)。在自測的時候,開發者在開發機上執行一個或者多個集裝箱;在驗證時,測試人員在測試機羣上執行集裝箱;在部署時,運維人員在產品環境執行集裝箱。由於執行的都是一樣地集裝箱,因此不容易出錯。

這種流程更合理的劃分了開發者和其餘角色的工做邊界,也大大簡化了測試和部署工做。

boot2docker

上節提到,Docker虛擬了網絡地址空間和文件系統。實際上,它還虛擬了進程ID空間(pid space)等系統數據結構。這些功能是一個叫dockerd的daemon程序藉助Linux內核中的control groups(又叫cgroups)功能實現的。

dockerd負責執行集裝箱;就像VirtualBox負責執行虛擬機同樣。而cgroup是Google的兩個工程師Paul Menage和Rohit Seth貢獻給Linux社區的。從他們的工做記錄看,主要工做集中在2008和2009年。聽說,Google開發它就是爲了方便在本身的機羣上部署各類Internet應用和離線處理系統。具體一點兒的故事,請看這篇Information Week上的帖子。。

由於cgroups功能只有Linux內核有,因此Docker目前只能運行在Linux上。但是,如今不少開發者都在用Mac。爲了能讓這些開發者方便的測試本身創做的集裝箱鏡像,Docker的開發者寫了boot2docker——利用VirtualBox虛擬一個Linux主機,而且在上面安裝dockerd。而命令行控制程序docker執行在Mac主機上,被配置成和虛擬Linux主機上的dockerd協做。

boot2docker的安裝方式很簡單:照着這個流程,下載並執行一個安裝包便可。由於boot2docker利用了VirtualBox,因此安裝它以前須要先裝VirtualBox。Homebrew也提供了安裝boot2docker的選項,可是可能由於bug致使dockerd和docker版本不一樣,無法協同工做。

在利用boot2docker在Mac上開始工做以前,還有幾個注意事項。當咱們在Linux主機上啓動一個集裝箱的時候,咱們可讓Docker把主機的某些目錄映射成集裝箱內的目錄。這樣集裝箱裏的程序和主機上的程序共享數據,是一種方便的調試方式。可是在用boot2docker的時候,「主機」不是Mac,而是虛擬Linux主機。此時若是想把Mac上的目錄映射到集裝箱,先得將其經過VirtualBox映射到Linux主機。

另外一個注意事項和端口轉發有關。當咱們把集裝箱內的某個端口映射爲主機的某個端口時,只是映射到了虛擬Linux主機;若是想讓Mac上的程序能訪問,還得把虛擬機端口經過VirtualBox映射成Mac上的端口。這些注意事項,在下文中會有詳細解釋。

CoreOS

實際開發中的測試機羣和產品環境一般都是用的Linux服務器。要在上面執行集裝箱,也須要安裝Docker。由於Docker的開發者提供各類Linux軟件包,因此一般輸入一個命令,便可安裝Docker。好比在Ubuntu/Debian Linux裏,這個命令是:

sudo apt-get install docker.io

可是目前最經常使用的用來執行Docker集裝箱的Linux發行版本既不是Ubuntu、Debian也不是RedHat、Fedora,而是CoreOS。這個發行版本根本沒有軟件包管理程序,因此也不能經過輸入某個命令來安裝軟件。可是CoreOS預裝了Docker,因此能夠製做集裝箱鏡像,或者下載別人發佈的集裝箱鏡像來執行。目前,Amazon AWS和Google Compute Engine這兩大雲計算平臺都提供預裝了CoreOS的虛擬機。

實際上,Google數據中內心運行的Linux系統和CoreOS有不少類似之處。我記得2010年我剛離開Google加入騰訊的時候,一位騰訊的同事好奇地問:「Google的機羣裏用的Linux用什麼軟件包管理程序?是apt-get嗎?仍是yum?」我回答:「其實服務器上運行的Linux是不須要包管理的,只有桌面Linux系統才須要」。這位同事很難相信。其實,要不是由於「見了一回豬跑」,我也想不到會是這樣。

CoreOS和其餘Linux發行版本相比,執行效率高、內存耗費省;此外,利用雙磁盤分區技術,即使是更新Linux內核也不須要重啓。CoreOS還有不少獨特之處,使得它在問世後很短的時間裏就被Amazon和Google採用。若是想進一步瞭解這些特性,請看這個對Docker做者的訪談

Go語言

接下來,咱們看看如何在Mac上用Go語言寫一個極簡化的搜索引擎,而且封裝成集裝箱鏡像。

咱們選擇Go語言爲例,而不是更常見的Java、Python、Perl、Ruby、Scala等,有很現實的緣由——後面這些語言寫的程序,在執行時都須要某些運行環境的支持。好比,Java程序依賴Java虛擬機,Python程序須要Python解釋器,這些加上預裝的程序庫須要佔用幾百MB的集裝箱空間。而用Go寫的程序默認是全靜態編譯的,執行時不須要任何環境支持,不須要預裝庫,甚至連Linux系統動態庫都不依賴。鑑於一家公司的系統每每由成千上萬的集裝箱構成;每一個集裝箱少幾百MB,能爲公司省出很大一筆開銷。那些每個月要向Amazon或者Goolgle付帳的公司,對此必然印象深入。這是Go語言在不少創業公司拓展迅猛的一個緣由。

若是咱們用C或者C++開發,也能夠生成全靜態連接的二進制程序文件。可是在Web時代,C/C++的開發效率不如Go。Google裏卻是廣泛使用C++,可是Google裏有一套精心設計、積攢多年的C++庫,這是外界沒有的。外界廣泛得使用第三方庫,並每每所以撓頭。好比,不一樣的第三方庫(Thrift和boost)各有各的線程池機制,很難統一管理多線程。C++11卻是有了標準線程管理,可是把不少庫統一到C++11是一項開銷極大的工做。Go語言是專門爲分佈式系統開發設計的,根本就沒有線程的概念,在語法上用goroutine代替了,線程池實如今Go runtime裏,被編譯進每一個二進制程序。

交叉編譯

由於集裝箱用主機的操做系統和硬件來執行程序,而Docker只支持Linux,因此Go程序必須被編譯成Linux二進制文件,才能經過Docker運行。而咱們在Mac上開發,須要利用交叉編譯技術來生成Linux二進制文件。

爲了獲得一個支持交叉編譯的Go語言編譯器,咱們須要從源碼安裝Go,而且須要作一些額外的安裝工做。具體過程以下:

  1. 安裝Xcode,從而得到C編譯器。
  2. 下載Go編譯器的源碼包。好比Go 1.3在這裏
  3. 解壓和編譯

    tar xzvf go1.3.src.tar.gz
     cd go/src
     ./all.bash
  4. 編譯各類平臺下的Go標準庫

    git clone git://github.com/davecheney/golang-crosscompile.git
     source golang-crosscompile/crosscompile.bash
     go-crosscompile-build-all

這裏,咱們用到了Dave Cheney寫的一個Bash腳本程序。這個程序支持生成如下平臺上的Go語言標準庫:

  1. darwin/386
  2. darwin/amd64
  3. freebsd/386
  4. freebsd/amd64
  5. freebsd/arm
  6. linux/386
  7. linux/amd64
  8. linux/arm
  9. windows/386
  10. windows/amd64
  11. nacl/amd64
  12. nacl/386

並行計算最經常使用的目標平臺是linux/amd64——64bit的Linux系統,也是CoreOS的平臺格式。下文中咱們會演示如何在Mac下用這個編譯器生成Linux平臺的二進制代碼文件。

極簡版搜索引擎

這篇帖子裏,做者Adriaan de Jonge用一個最簡單的http server做爲例子,說明如何在Mac下用Docker運行一個程序。

這篇帖子對我頗有幫助。只是這個例子程序太過簡單了——一般一個互聯網產品包含不僅一個程序——現代互聯網產品幾乎都採用micro service架構,一個http server和多個RPC server協同工做。以外,還會有一些daemon程序,不時向RPC server提供不斷更新的數據。好比在搜索引擎裏,一個indexer程序會不斷將cralwer程序爬下來的網頁內容加以整理,而且發送給搜索引擎服務。

本節裏咱們介紹的極簡版的搜索引擎就包括兩個程序——search engine server和向它提供索引內容的indexer daemon。search engine server首先是一個http server,能夠經過瀏覽器訪問——對每一個輸入的query,返回相應的結果。同時,它仍是一個RPC server,接受從indexer daemon發來的更新後的索引內容。這兩個程序的源碼在這裏

爲了下載和構建這個例子程序,請輸入以下命令:

mkdir -p /tmp/learn-docker
cd /tmp/learn-docker
export GOPATH=`pwd`
go get github.com/wangkuiyi/helloworld/indexer
go get github.com/wangkuiyi/helloworld/searchengine

此時,在 /tmp/learn-docker/bin 目錄裏應該有兩個二進制程序文件 indexersearchengine。這兩個文件都是Darwin/AMD64格式的。咱們能夠在Mac主機上運行它倆:

./bin/searchengine -addr=":10000" &
./bin/indexer -searchengine="localhost:10000"

這樣首先啓動了searchengine,而且讓它的http和rpc服務都監聽本機(Mac主機)的10000端口;隨後啓動了indexer,它每秒鐘經過RPC調用告訴searchengine更新索引內容。

啓動成功以後,咱們能夠在瀏覽器裏訪問以下網址:http://localhost:10000/?q=news,從而看到searchengine返回的搜索結果(以下圖):

固然,咱們也能夠用命令行程序,好比wget和curl,來訪問searchengine服務。這樣咱們能夠很方便的寫一個集成測試(regression test)程序。好比這個

建立集裝箱

接下來,咱們看看如何把這兩個程序打包進Docker集裝箱鏡像,而後在Mac主機(其實是boot2docker建立的Linux虛擬機)上運行集裝箱。接下來咱們會看到:這些集裝箱不用修改,也就能在Amazon AWS和Google Compute Engine上運行,從而完成發佈。

首先,咱們須要從源碼生成Linux/AMD64二進制程序文件。用上文介紹的方法,獲得一個支持交叉編譯的Go編譯器以後,編譯示範程序很簡單:

GOOS=linux GOARCH=amd64 go install \
github.com/wangkuiyi/helloworld/indexer \
github.com/wangkuiyi/helloworld/searchengine

能夠看到,咱們只是經過環境變量設置了一下目標操做系統和架構。

隨後,咱們要建立一個Docker集裝箱鏡像,把編譯好的兩個程序放進去。由於如上文介紹的,Go程序執行時不須要特殊的運行環境,因此這個集裝箱鏡像裏,除了一些metadata和咱們的程序以外,什麼都不須要。以致於咱們能夠從Docker Hub網站上下載一個空的鏡像,在裏面安裝咱們的程序便可。爲此,咱們須要寫一個Dockerfile:

FROM scratch
ADD bin/linux_amd64/searchengine /searchengine
ADD bin/linux_amd64/indexer /indexer

這裏的第一行是讓Docker自動從Docker Hub上下載名爲scratch的鏡像;第二行說把本地文件bin/linux_amd64/searchengine裝進這個鏡像的根目錄,成爲/searchengine;第三行拷貝indexer

有了Dockerfile咱們就能用docker命令建立一個鏡像了。下面命令建立一個鏡像,並命名爲wangkuiyi/helloworld

cp $GOPATH/src/github.com/wangkuiyi/helloworld/Dockerfile $GOPATH/
docker build -t wangkuiyi/helloworld $GOPATH

此時,咱們能夠用docker images命令看到咱們建立的鏡像:

yiwang@yiwang-mn1-> docker images
REPOSITORY             TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
wangkuiyi/helloworld   latest              255460c3d095        3 hours ago         13.86 MB

分佈式系統的部署

最簡單的使用Docker的部署方案是:啓動一個集裝箱,在其中運行一個searchengine進程和一個indexer進程。這和上文中介紹的在Mac主機上運行的方式是同樣的,但這不符合分佈式系統的通常部署原則。

一般,爲了提升處理速度、提高吞吐量和系統容錯能力,每一個程序都會啓動爲多個進程,運行在不一樣的機器上。好比,indexer程序的每一個進程處理一部分數據(好比一個cralwer進程的輸出)。這樣的並行處理提高創建索引的效率。這種狀況下,每一個進程及其處理的數據被稱爲一個shard。(shard應該怎麼翻譯?我不知道)。

相似地,searchengine進程也會啓動爲多個進程,每一個進程的內存空間裏都裝着一樣地索引結構,因此都能提供一樣地服務,從而提高吞吐量。若是這些進程運行在不一樣的機器上,那麼哪怕某些機器掛了,還有活着的進程能不間斷地提供搜索服務。這樣的每一個進程被稱爲一個replication

其實每一個indexer shard也能夠是一組多個進程,其中每一個進程是隸屬本shard的一個replication。從而同時提高indexer的處理速度和容錯能力。

這麼多進程應該啓動在哪些機器上呢?要靠人來決定,可就忙不過來咯;得靠機羣管理系統。Google Borg就是這樣一套系統。

但是在不少年的時間裏,外界都不知道Borg。有一些項目試圖模仿Google的計算架構,好比Hadoop意圖模仿MapReduce。Google MapReduce是一個構建在Borg之上的並行計算框架。可是Hadoop的開發者沒有開發相似Borg的系統,而是讓Hadoop(計算框架)兼任資源管理和調度的功能,致使系統複雜,代碼亂做一團。

實際上,在Hadoop開始的若干年裏,甚至沒有像Google MapReduce那樣讓每一個job有一個master進程來管理;而是讓機羣上全部job裏的全部進程都向一個叫Job Tracker的進程彙報心跳(heartbeat),以致於一個Hadoop機羣不能太大,不然Job Tracker會處理不過來。並且Job Tracker做爲性能和穩定性的雙重瓶頸,一旦累壞了,整個機羣上全部job就都掛了。Hadoop的開發者直到2011年左右才意識到這一點,併發布了一篇文章,開始計劃開發「下一代Hadoop」,如今被稱爲YARN的系統。

YARN的功能和Google Borg有相似之處,可是真正引起外界對Google Borg關注的,是加州大學伯克利分校和Twitter的合做項目Mesos。這是一個試圖複製Borg的嘗試。當Mesos在Twitter運行起來的時候,不少從Google加入Twitter的工程師都很興奮——終於從新能「高效工做」了!這裏的故事,能夠參見這篇Wired文章。Mesos系統設計思路描述在這篇論文裏。其第一做者Ben Hindman曾經在Google實習,後來在Twitter任職。

實際上,即使Mesos也沒有能很類似地模仿Google Borg。至少在程序的發佈和部署上。Mesos沒有和Google Borg等效的打包和執行包的功能。而這個功能能爲外界所訪問,正是靠了本文着重介紹的Docker。Docker和Google Borg同樣,使用Google工程師爲Linux內核貢獻的cgroups功能來實現集裝箱機制。

藉助Docker,Google終於於本月(2014年7月)開源了Borg——可是是用Go語言重寫的Borg,稱爲Kubernetes——Google Borg是用C++開發的。感謝開源社區不懈的推進!

集成測試

基於上一節的介紹,咱們能想象,若是每一個集裝箱只執行一個進程,那麼機羣管理系統在部署和調度應用時受到的限制最少。反過來想,若是咱們在一個集裝箱裏同時運行一個indexer進程和一個searchengine進程,那麼咱們實際上引入了一個沒必要要的約束——indexer進程和searchengine進程一一對應。並且若是機羣中有一臺機器,能夠承擔運行一個進程的負載,可是不能承擔同時運行兩個進程,那麼這臺機器上就無法部署上述「大」集裝箱了。

因此,在Google Borg和Google Kubernetes裏,都建議每一個集裝箱裏只執行一個進程。

基於「打包一次,兼顧測試和發佈」的原則,咱們能夠想象,對於一個應用(或者叫作產品,好比上述的極簡搜索引擎),最多見的打包方式是產生一個集裝箱鏡像,可是每一個集裝箱裏只執行一個程序的一個進程。

上文中,咱們已經用一個Dockerfile把兩個程序:indexersearchengine都裝進一個鏡像wangkuiyi/hellworld了。接下來,咱們嘗試在Mac主機上啓動兩個集裝箱,分別執行一個indexer和一個searchengine進程:

docker run -d -p 8080:8080 --name searchengine wangkuiyi/helloworld /searchengine
VBoxManage modifyvm "boot2docker-vm" --natpf1 "tcp-port8080,tcp,,8080,,8080"
docker run -d --name indexer --link searchengine:se wangkuiyi/helloworld /indexer -searchengine=se:8080

這裏,第一行啓動了一個集裝箱,而且起名叫searchengine,執行的鏡像是wangkuiyi/helloworld-d的意思是在後臺執行,相似一個shell命令後面跟上一個&符號的效果。-p 8080:8080的意思是:「這個集裝箱裏有個程序會監聽8080端口(若是看看searchengine的源碼,會發現8080是其默認端口),把這個端口映射到主機(boot2docker建立的Linux虛擬機)的8080端口」。

第二個命令讓VirtualBox把Linux虛擬機的8080端口映射爲Mac主機的8080端口。這樣就能夠在Mac主機上啓動一個瀏覽器,經過訪問本機的8080端口,來訪問集裝箱裏的searchengine服務。(若是你在Linux主機上開發,就不須要boot2docker虛擬一個Linux主機了,也就不須要這個命令了。)

上述第三個命令啓動了一個名爲indexer的集裝箱,執行的也是wangkuiyi/helloworld鏡像。在這個集裝箱裏啓動了一個indexer進程;這個進程會去鏈接se:8080這個網絡地址,並經過RPC調用,向這個目標地址發送更新的索引數據。se這個IP地址是怎麼來的呢?這是--link seachengine:se參數的效果——這個參數使得Docker在啓動indexer集裝箱以前,修改了其中/etc/hosts文件,在其中增長了一行:

xxx.xxx.xxx.xxx se

這裏 xxx.xxx.xxx.xxx 指代集裝箱searchengine--link searchengine:se中冒號左邊的部分)的虛擬IP地址,se--link searchengine:se中冒號右邊的部分)也就是其域名了。Docker就是經過--link這個參數,讓不一樣集裝箱內的多個進程能夠互相通訊的。

此時,在本機打開一個瀏覽器窗口並訪問http://localhost:8080/?q=news,能夠看到和上圖徹底同樣的結果。

自動部署

到目前爲止,咱們都是手動調用docker命令來操做docker的。而獲得的效果——在Mac主機上啓動極簡搜索引擎——和不用Docker是同樣的。你們不由會問,爲何要引入Docker呢?

其實,實際使用Docker時,咱們不會手動敲docker命令,而是會利用fleet或者Kubernetes來部署和啓動集裝箱。這樣只須要寫一個很是簡明的部署配置文件,就能夠在開發機、集成測試機羣、預發佈機羣、和產品環境中完成部署了。這篇文章爲了說明Docker的設計思路和使用方法已經很長了,因此關於fleet和Kubernetes的介紹,我準備放在《Docker:分佈式系統的軟件工程革命(下)》中。

謝謝你們看到這裏!

相關文章
相關標籤/搜索