- 容器即服務:從零構建企業級容器集群
- 林帆
- 9279字
- 2019-09-09 16:31:50
1.1 虛擬化與容器
1.1.1 計算資源虛擬化
虛擬化在現代計算機領域的使用相當廣泛,它通過將真實的硬件抽象為軟件可控的邏輯單元,使得昂貴的硬件資源能夠按需、按量分配,以達到減少浪費、實現硬件利用率最大化的目的。計算資源的虛擬化是虛擬化領域里比較重要的一個分支,這里的計算資源主要指的是CPU、內存、硬盤等與計算機運算直接相關的硬件資源。在很長的一段時間里,計算資源的虛擬化始終是各類虛擬機技術爭奪的熱土。從最初IBM和Sun等公司主導的早期CPU虛擬化實現,到VMware、Xen、KVM、QEMU等虛擬化或半虛擬化技術的成熟,就經歷了40余年的發展過程。
與此同時,另一種虛擬化技術的分支也在緩慢發展。這類虛擬化技術不依賴與硬件相關的特性,而是在系統內核的層次之上,將進程運行的上下文環境加以限制和隔離。最初它們并不被視為虛擬化方法,比如在Unix系統中引入的chroot工具僅僅是將特定進程的文件上下文鎖定在特定目錄中,制造出在同個系統里模擬多個隔離的系統目錄的效果。隨后,在FreeBSD 4.0中出現的Jails和前SWsoft公司(現已更名為Parallels)開發的基于Windows系統的Virtuozzo(睿拓)等技術在chroot的基礎上增加了進程空間和網絡空間的隔離,更好地實現了進程之間互不干擾地共享硬件資源的目的。這種虛擬化方式就是最早的容器技術雛形。
隨后不久,IBM、Sun和惠普等老牌虛擬機和操作系統公司也紛紛進入不依賴特定CPU和硬件支持的虛擬化技術陣營,分別在自家的操作系統里推出相應的產品,例如運行在Solaris系統的Zones、運行在IBM AIX小型機系統的WPARs以及運行在惠普服務器系統HP-UX的SRP Containers等。在這段時期里,特別值得一提的是在開源GNU/Linux系統上實現的操作系統級虛擬化服務:Linux-VServer。
開源社區的介入使得這類虛擬化技術迅速發展,并被應用到更多的領域中。然而作為社區產品,由于參與人員眾多、早期目標定位不明確,Linux-VServer的配置細節很復雜,加上文檔十分混亂,因此當時只有對Linux內核有一定了解的用戶才能駕馭它。這種狀態一直持續到2005年,這一年,曾經設計了Virtuozzo的SWsoft公司開始在Linux系統上開發一款全新的開源虛擬化產品:OpenVZ。這款采用了GNU/GPL協議開源的軟件很好地改善了Linux系統上進行虛擬化隔離的使用體驗,并為SWsoft公司帶來了可觀的收入。同一時期還誕生了一個對虛擬化技術影響頗遠的概念:VPS(Virtual Private Server,虛擬專用服務器)。VPS技術是指將一臺服務器分割成多個邏輯上的虛擬專享服務器。每個VPS都可分配獨立公網IP地址、獨立操作系統、獨立硬盤空間、獨立內存和CPU資源。這種虛擬主機間的隔離服務為用戶和應用程序模擬出“獨占使用計算資源”的體驗。OpenVZ理所當然地成為了當時虛擬化技術的代表之一,與Xen、KVM并列成為VPS提供商首選的虛擬化實施方案。
此時的Linux系統級虛擬化技術已經逐漸成熟,然而對代碼質量頗為嚴苛的Linux內核團隊并沒有采納Linux-Vserver或OpenVZ提交的內核補丁,而是在Linux 2.6的內核中重新設計了Namespace和CGroup等功能,實現了更加靈活的可組合式虛擬化能力。隨后在2008年,Linux社區就出現了基于內核隔離能力設計的LXC(Linux Containers)虛擬化項目,它充分利用Linux內核的Namespace隔離能力和CGroup(Control Groups,控制組)控制能力實現了操作系統內核層面上的虛擬化,不再需要修改內核代碼,大大降低了使用該項技術的門檻。此后的幾年里,又相繼出現了linux-utils和systemd-nspawn等Linux內核虛擬化工具。Linux系統開始在內核虛擬化技術的演進過程中發揮越來越重要的作用,如圖1-1所示。

圖1-1 內核虛擬化技術的演進
2013年3月,DotCloud公司(現已更名為Docker公司)基于LXC項目封裝了一套工具,將其命名為Docker。在Docker的宣傳下,這種基于操作系統的虛擬化技術被廣泛地稱為Container(容器),并為許多開發者所知。2014年,Docker重新設計自己的虛擬化層并發布了Libcontainer項目,從此脫離了對LXC的依賴,成為一個獨立發展的平臺。2015年,Docker與微軟、IBM等幾十家公司共同成立規范化容器技術標準的OCI組織(Open Container Initiative,開放容器計劃),并設立新的符合OCI標準的容器引擎RunC
。在此期間,由CoreOS公司主導的Rkt容器項目也在迅速地成長,作為比Docker更加輕量的容器工具成為了許多容器集群技術的備選搭配方案。
1.1.2 容器技術的本質
虛擬機和容器技術的目的都是抽象硬件并對系統資源提供隔離和配額,然而兩者在本質上有著很大的差異。
虛擬機的原理是通過額外的虛擬化層,將虛擬機中運行的操作系統指令翻譯成宿主機系統能夠執行的系統調用,然后操作具體的硬件。這樣做能夠比較好地實現虛擬機和宿主機操作系統的異構,例如在Linux系統上運行Windows的虛擬機,或是在Mac系統上運行Linux的虛擬機。其缺點是通常需要依賴硬件支持,特別是CPU虛擬化的支持。
容器技術則完全建立在操作系統內核特性之上,是一種與運行硬件無關的虛擬化技術。由于這種方式實現的虛擬化沒有轉換異構指令的虛擬化層,因此在運行效率上較虛擬機方式更高,但只能實現與宿主操作系統相同系統的虛擬化。在實際使用中,有時會將容器技術和虛擬機結合,以實現“跨不同操作系統”運行容器的目的,Windows和Mac版本的Docker就是這樣的例子。
虛擬機和容器技術的結構差異如圖1-2所示。注意容器結構中的“容器工具”只是一個邏輯層,它并沒有與容器中的應用程序存在實際的層級關系,而是同樣運行在宿主操作系統上,兩者是兩個不同隔離空間中的平級服務而已。

圖1-2 虛擬機與容器的對比
前文在介紹容器技術演進史時已經提到,現代Linux系統中的容器技術主要是利用內核的Namespace特性和CGroup特性實現了服務進程組的資源隔離和配額。
1.Namespace
Linux內核實現Namespace的主要目的就是為了實現內核級虛擬化(容器)服務,讓同一個Namespace下的進程可以感知彼此的變化,同時又能確保對外界的進程一無所知。這樣就可以讓容器中的進程產生錯覺,仿佛置身于一個獨立的系統環境中,以達到獨立和隔離的目的。
Mount Namespace在2002年進入Linux 2.4.19內核,它是內核中最早出現的、用于運行時隔離的Namespace。較新的Linux 4.7.1版本內核已經實現了7種不同的Namespace類型,除了比較特殊的CGroup Namespace用于隔離進程組對不同CGroup的可視性外,其他的6種都和某些傳統意義上的系統資源直接相關。
通過查看“/proc”目錄下以進程PID作為名稱的子目錄中的信息,能夠了解該進程的一組Namespace ID,如下所示。
# ls -l /proc/1240/ns total 0 lrwxrwxrwx 1 root root 0 Aug 18 15:46 cgroup -> cgroup:[4026531835] lrwxrwxrwx 1 root root 0 Aug 18 15:46 ipc -> ipc:[4026531839] lrwxrwxrwx 1 root root 0 Aug 18 15:46 mnt -> mnt:[4026531840] lrwxrwxrwx 1 root root 0 Aug 18 15:46 net -> net:[4026531993] lrwxrwxrwx 1 root root 0 Aug 18 15:46 pid -> pid:[4026531836] lrwxrwxrwx 1 root root 0 Aug 18 15:46 user -> user:[4026531837] lrwxrwxrwx 1 root root 0 Aug 18 15:46 uts -> uts:[4026531838]
顯然,每個進程都具有這樣的7個屬性,因此每個進程都會分別在這些Namespace控制的系統資源上與另一些進程共享空間。在沒有使用容器的情況下,系統中所有進程都具有相同的Namespace ID組合。然而如果有一個進程運行在Docker或其他的容器里,它就很有可能具有完全不同的一組Namespace ID。這些Namespace是可以任意組合的,容器中的進程也可以只做部分的隔離,例如在使用--network=host參數啟動的Docker容器中運行的進程就會與主機上的其他進程擁有相同的Network Namespace ID,此時它可以直接通過127.0.0.1的地址訪問主機上運行的服務。
下面對每種Namespace的作用進行簡單地解釋。
·CGroup Namespace:提供基于CGroup(控制組)的隔離能力。CGroup是Linux在內核級對進程可用資源進行限制的一組規則,CGroup的隔離能夠讓不同進程組看到的CGroup規則各不相同,為不同進程組采用各自的配額標準提供便利。
·IPC Namespace:提供基于System V進程信道的隔離能力。IPC全稱為Inter-Process Communication,是Linux中一種標準的進程間通信方式,包括共享內存、信號量、消息隊列等具體方法。IPC隔離使得只有在同一個命名空間下的進程才能相互通信,這一特性對于消除不同容器空間中進程的相互影響具有十分重要的作用。
·Mount Namespace:提供基于磁盤掛載點和文件系統的隔離能力。這種隔離的效果與chroot系統調用十分相似,但從實際原理來看,MNT Namespace會為隔離空間創建獨立的mount節點樹,而chroot只改變了當前上下文的根mount節點位置,從而影響文件系統查找文件和目錄的結果。在文件系統隔離的作用下,容器中的進程將無法訪問到容器以外的任何文件。在必要情況下,可以通過掛載額外目錄的方式和主機共享文件系統。
·Network Namespace:提供基于網絡棧的隔離能力。網絡棧的隔離允許使用者將特定的網卡與特定容器中的進程運行上下文關聯起來,使得同一個網卡在主機和容器中分別呈現不同的名稱。Network Namespace的重要作用之一就是讓每個容器通過命名空間來隔離和管理自己的網卡配置。因此可以創建一個普通的虛擬網卡,并將它作為特定容器運行環境的默認網卡eth0使用。這些虛擬網絡網卡最終可以通過某些方式(NAT、VxLan、SDN等)連接到實際的物理網卡上,從而實現像普通主機一樣的網絡通信。
·PID Namespace:提供基于進程的隔離能力。進程隔離使得在容器中的首個進程成為所在命名空間中PID值為1的進程。在Linux系統中,PID為1的進程地位非常特殊,它作為所有進程的根父進程,有很多特權,比如屏蔽信號、接管孤兒進程等。一個比較直觀的現象是,當系統中的某個子進程脫離了父進程(例如父進程意外結束),那么它的父進程就會自動成為系統的根父進程。此外,當系統中的根父進程退出時,所有同屬一個命名空間的進程都會被殺死。
·User Namespace:提供基于系統用戶的隔離能力。系統用戶隔離是指同一個系統用戶在不同的命名空間中可以擁有不同的UID(用戶標識)和GID(組標識),它們之間存在一定的映射關系。因此,在特定命名空間中,UID為0的用戶并不一定是整個系統的root管理員用戶。這一特性限制了容器的用戶權限,有利于保護主機系統的安全。
·UTS Namespace:提供基于主機名的隔離能力。主機名隔離是指每個獨立容器空間中的程序可以有不同的主機名稱信息。值得一提的是,主機名只是一個用于標示虛擬主機或容器空間的代號,并不一定是全網唯一的,允許重復。因此,它雖然可以在網絡中用于通信或定位服務,但并不是可靠的方法。
2.CGroup
CGroup是Linux內核提供的一種可以限制、記錄、隔離進程組所使用的物理資源(包括CPU、內存、磁盤I/O速度等)的機制,它的v1版本由Google的Paul Menage設計,并在2007年進入Linux 2.6.24內核(這個內核版本實際上在2008年1月才正式發布),之后發布的v2版本在2016年3月也已經成為了Linux 4.5內核的一部分。CGroup也是LXC等現代容器管理虛擬化系統資源的手段。
CGroup最初設計出來是為了統一Linux下混亂的資源管理工具,例如過去限制CPU使用時可以用renice和cpulimit命令,限制內存要用ulimit或者PAM(Pluggable Authentication Modules),而限制磁盤I/O和網絡又需要其他的專用工具。CGroup作為一種內核級的資源限制手段,在功能和效率方面都是早期工具不能比擬的。值得一說的是,在現代的Linux進程管理中,CGroup的使用已經比較普遍,并不是非得使用容器才與CGroup有關。例如在采用了Systemd管理服務的Linux發行版中,其service文件定義中使用的MemoryLimit、BlockIOWeight等配置其實就是在間接地為進程配置CGroup。又如在專門為大規模服務器部署而設計的CoreOS操作系統里,為了避免在系統升級時因占用數據帶寬而影響正常業務服務,同樣使用CGroup的管控實現了“只在帶寬空閑時下載”的效果,這些應用都是與容器沒有直接關系的。
在Linux“一切皆文件”的思想中,CGroup同樣直觀地表現為一些特殊的目錄和文件。查看任意一個進程在“/proc”目錄下的內容,可以看到下面這樣一個名為“cgroup”的文件。
# cat /proc/1240/cgroup 11:perf_event:/ 10:blkio:/ 9:freezer:/ 8:cpu, cpuacct:/ 7:net_cls, net_prio:/ 6:devices:/ 5:cpuset:/ 4:pids:/ 3:memory:/ 2:hugetlb:/ 1:name=systemd:/
這個文件中除了最后一行,都對應了一個CGroup的子系統。每個子系統是CGroup中用于定義某類資源控制規則的結構。與Namespace類似,如果觀察運行在主機上每個進程的cgroup文件內容,會發現它們中的絕大多數都一樣,也就是說共用了相同的CGroup。但如果觀察一個運行在容器中的進程,會發現它具有與其他進程完全不同的一組CGroup路徑。
那么這個路徑到底指的是哪里呢?來看一下當前系統中的所有掛載點,會看到其中有許多是CGroup類型的項目,如下所示。
# mount | grep 'type cgroup' cgroup on /sys/fs/cgroup/systemd type cgroup (…) cgroup on /sys/fs/cgroup/cpuset type cgroup (…) cgroup on /sys/fs/cgroup/blkio type cgroup (…) cgroup on /sys/fs/cgroup/perf_event type cgroup (…) cgroup on /sys/fs/cgroup/pids type cgroup (…) cgroup on /sys/fs/cgroup/devices type cgroup (…) cgroup on /sys/fs/cgroup/net_cls, net_prio type cgroup (…) cgroup on /sys/fs/cgroup/memory type cgroup (…) cgroup on /sys/fs/cgroup/freezer type cgroup (…) … …
這當中的每個掛載點都是一個CGroup子系統的根目錄,例如上一例中那個進程所屬的cpuset子系統路徑為“/”,實際上就是指“/sys/fs/cgroup/cpuset”這個目錄,其余子系統的位置也可以此類推。
在Linux 4.7.1內核中,已經支持了10類不同的子系統,分別如下所示。
·hugetlb子系統用于限制進程對大頁內存(Hugepage)的使用。
·memory子系統用于限制進程對內存和Swap的使用,并生成每個進程使用的內存資源報告。
·pids子系統用于限制每個CGroup中能夠創建的進程總數。
·cpuset子系統在多核系統中為進程分配獨立CPU和內存。
·devices子系統可允許或者拒絕進程訪問特定設備。
·net_cls和net_prio子系統用于標記每個網絡包,并控制網卡優先級。
·cpu和cpuacct子系統用于限制進程對CPU的用量,并生成每個進程所使用的CPU報告。
·freezer子系統可以掛起或者恢復特定的進程。
·blkio子系統用于為進程對塊設備(比如磁盤、USB等)限制輸入/輸出。
·perf_event子系統可以監測屬于特定的CGroup的所有線程以及運行在特定CPU上的線程。
為了比較方便地與系統CGroup進行交互,可以安裝CGroupTools工具包。對于Debian或Ubuntu系統可使用apt-get或apt命令安裝,如下所示。
# apt install cgroup-tools
Redhat或CentOS系統則可用yum或dnf命令來安裝,如下所示。
# dnf install libcgroup-tools.x86_64
這個工具包中包含了一組用于創建和修改CGroup信息的命令。以通過CGroup實現CPU的配額限制為例,安裝完cgroup-tools工具包后,通過cgcreate命令來創建兩個CPU CGroup的子系統分組(需要root權限來執行),如下所示。
# cgcreate -g cpu:/cpu50 # cgcreate -g cpu:/cpu30
其中-g cpu表示設定的是CPU子系統的配額。除CPU子系統外,cgcreate同樣可以創建其他各種配額的子系統。通過lscgroup命令就能看到創建出來的這兩個子系統,如下所示。
# lscgroup … … cpu:/ cpu:/cpu30 cpu:/cpu50 … …
接下來為兩個CPU分組分別設置一條限制規則,CPU子系統的cfs_quota_us可以設定進程在每個“時間片周期”內可占用的最大CPU時間,單位是μs。CPU時間片周期由子系統的cfs_period_us屬性指定,默認為100000,單位同樣是 μs。因此例如cfs_quota_us的數值50000和30000表示在該組中的進程最多分別能夠使用50%和30%的CPU時間。使用cgset命令將規則設定進去,如下所示。
# cgset -r cpu.cfs_quota_us=50000 cpu50 # cgset -r cpu.cfs_quota_us=30000 cpu30
構造一個耗CPU的程序,例如下面這個不斷累加并輸出數據的腳本,將其命名為“app.sh”。
#! /bin/bash N=0 while true; do N=$((N+1)) echo $N done
首先,嘗試在后臺直接運行它,如下所示
# ./app.sh > /dev/null &
接著在一個單核的機器上進行測試。觀察系統CPU的使用情況,用不了多久,這個進程就會將所有CPU資源統統耗盡,如下所示。
# ps aux USER PID %CPU %MEM …… COMMAND root 10366 99.6 0.0 …… /bin/bash ./app.sh
然而如果使用cgexec命令讓app.sh進程在cpu50這個CGroup中重新運行,就會發現這次進程的CPU使用被穩定地限制在了50%附近,如下所示。
# cgexec -g cpu:cpu50 ./app.sh > /dev/null & # ps aux USER PID %CPU %MEM …… COMMAND root 10366 51.8 0.0 …… /bin/bash ./app.sh
再啟動一個相同的進程,同樣放到cpu50子系統里。此時兩個進程的CPU使用率都有所降低,其總和依然大約為50%,如下所示。
# cgexec -g cpu:cpu50 ./app.sh > /dev/null & # ps aux USER PID %CPU %MEM …… COMMAND root 10380 32.2 0.0 …… /bin/bash ./app.sh root 10384 24.6 0.0 …… /bin/bash ./app.sh
現在不妨在這個子系統里再啟動一個相同的進程實例,然后觀察CPU使用率的變化,可以發現CGroup的效果的確是作用于同屬于該子系統中所有進程的,如下所示。
# ps aux USER PID %CPU %MEM …… COMMAND root 10380 18.6 0.0 …… /bin/bash ./app.sh root 10384 17.5 0.0 …… /bin/bash ./app.sh root 10390 16.5 0.0 …… /bin/bash ./app.sh
如果在剛才創建的cpu30子系統中重復這個測試,則所有進程的總CPU用量將穩定在30%左右,如下所示。
# cgexec -g cpu:cpu30 ./app.sh > /dev/null & # cgexec -g cpu:cpu30 ./app.sh > /dev/null & # cgexec -g cpu:cpu30 ./app.sh > /dev/null & # ps aux USER PID %CPU %MEM …… COMMAND root 10410 14.3 0.0 …… /bin/bash ./app.sh root 10433 10.5 0.0 …… /bin/bash ./app.sh root 10450 9.9 0.0 …… /bin/bash ./app.sh
本質來說,對CGroup的所有操作都是在對系統掛載的CGroup目錄進行修改。上述操作實際是在“/sys/fs/cgroup/cpu/”目錄下創建了cpu50和cpu30這兩個子目錄,查看其中任意一個目錄的內容,就會看到類似下面這樣的文件結構。
# ls /sys/fs/cgroup/cpu/cpu30/ cgroup.clone_children cgroup.event_control cgroup.procs cpu.cfs_period_us cpu.cfs_quota_us cpu.shares cpu.stat notify_on_release tasks
觀察里面的幾個文件內容,如下所示,基本上就真相大白了。
# cat /sys/fs/cgroup/cpu/cpu30/cpu.cfs_quota_us 30000 # cat /sys/fs/cgroup/cpu/cpu30/tasks 10410 10433 10450
實際完全可以不通過cgroup-tools工具來完成以上的所有操作,只要用普通的Linux命令創建出這個目錄結構就可以達到相同的目的。例如下面的命令創建了一個限制CPU使用率為20%的子系統。
# mkdir /sys/fs/cgroup/cpu/cpu20/ # echo 20000 > /sys/fs/cgroup/cpu/cpu20/cpu.cfs_quota_us # ./app.sh > /dev/null & # echo $! >> /sys/fs/cgroup/cpu/cpu20/tasks
稍微解釋一下上面的命令。首先,由于“/sys/fs/cgroup/cpu”目錄是被掛載為CGroup類型的文件系統,當用戶在該目錄創建子目錄時,系統會自動在該目錄創建作為CGroup子系統所需的文件結構,因此在后續的操作中就可以直接讀寫這些文件了。其次,在最后一條命令中的$!是Bash中用于獲取前一個命令PID的方式,因此這個命令的意思是將前一條命令執行的app.sh進程PID寫到子系統的tasks文件里面。
不難看出,CGroup的文件結構至少包含三層目錄樹,它的三個核心概念及相互間的邏輯關系如下所示。
·Hierarchy(控制樹):每個控制樹表示系統中掛載的一套CGroup配置樹,其中可以包含多個子系統。
·Subsystem(子系統):每個子系統對應一種控制資源,比如CPU子系統就是控制CPU時間分配的一個控制器。
·Control Group(控制組):在每個子系統下都可以創建不同的控制組,而每個控制組可以被賦予一組進程,通過指定控制組的參數來限制該控制組的進程對相應系統資源的使用。
這樣的體系實際上形成了一種層級狀的CGroup控制鏈,如圖1-3所示。

圖1-3 CGroup的層級控制樹
當然,以上只是一個很簡略的例子,在實際情況中,容器使用的CGroup約束方式會更加復雜,關于Namespace和CGroup的細節這里就不再展開了。
1.1.3 基于容器的軟件交付
容器技術在內核中出現的時間要遠遠早于Docker和Rkt這種容器管理軟件。然而不可否認的是,Docker項目是迄今為止最成功的容器技術產品之一。Docker的成功并非因為它在技術領域有什么重大突破,它的最初版本只是LXC工具的一層封裝,Docker的獨特之處在于它引入了鏡像、版本、倉庫等一系列的思想,以及Build、Ship、Run這樣的軟件交付理念,從而徹底地改變了軟件發布的過程。
在軟件開發實踐中,CI(Continuous Integration,持續集成)和CD(Continuous Delivery,持續交付)是經常被用來提高模塊間集成測試頻率和優化發布流程的手段,通過可視化交付流程、監控代碼變化、自動化部署和運行測試,以此更加頻繁地獲得軟件質量反饋。此外,流水線還能很好地反映實際產品交付過程中所經歷的各個環節,尤其是只要將這些環節涉及的實踐自動化,就能看到從代碼提交到每個階段的測試、部署過程中潛在的流程問題和交付瓶頸。
容器技術會直接改變軟件打包、發布和運維的許多方面,這些實踐通過持續集成/交付的流水線就能十分直觀地體現出來。圖1-4展示了一個典型的使用容器交付的項目的持續集成流水線,末尾省略號的部分表示流程包含的其他步驟,直到最終部署上線。在這個環環相扣的鏈條里,容器的運用場景貫穿全程。

圖1-4 基于容器的持續集成流水線
具體來說,這些影響體現在以下幾個方面。
1.在容器中進行構建、代碼檢查和單元測試
在容器中構建和測試代碼最直接的好處是,對于任何技術棧的項目,總是能夠恰到好處地提供一個適合運行相應開發工具鏈的干凈環境。在多個不同產品項目需要采用不同版本、種類的構建工具或SDK的時候,這個優勢特別有用,干凈的執行環境能夠減少構建或測試結果與預期不一致的情況。此外,由于有些代碼檢測(如安全性掃描)、全量的集成測試和回歸測試需要運行較長時間,通常放在夜間執行,而白天進行的構建任務則相對較多,容器除了能夠分別提供所需的環境外,還具有更好地利用容器動態擴縮的特性,很適合混合調度這些周期性的任務,將基礎設施資源池化。
根據所用的編程語言和技術棧,可將與構建和單元測試相關的鏡像分為很多種類,常用的Docker鏡像包括以下這些。
·C/C++:https://hub.docker.com/r/_/gcc/。
·Java:https://hub.docker.com/r/_/openjdk/。
·Golang:https://hub.docker.com/r/_/golang/。
·Nodejs:https://hub.docker.com/r/_/node/。
·其他語言的基礎鏡像。
在實際使用時往往需要基于這些鏡像添加相應的框架和SDK庫,以滿足使用的要求。
2.通過鏡像打包和分發服務
容器的封裝意味著,不論運行的服務是用Java、Python、PHP還是Golang設計的,平臺都可以用幾乎相同的方式去完成部署,而不用考慮安裝服務所需的環境,容器能夠做到這一點的原因就在于它采用了鏡像打包和分發方式。
以Docker容器為例,所有的運行時依賴都會在服務設計的時候以聲明或描述的方式體現在一個Dockerfile文件中。當服務打包時,將依次執行其中的描述性代碼,使得服務以及所需的完整執行環境,包括文件目錄、環境變量、啟動參數等全部固化在鏡像里。這種工作方式將最終運行環境的定義提前到了編碼設計的階段,緩解了開發和運維之間的信息不對稱問題,對促進團隊DevOps文化起到了積極的作用。
鏡像倉庫是當下使用最多的容器鏡像分發方式,通過一個集中式的存儲點,將構建出的鏡像按照一定的組織結構進行管理,并按需發布到運行服務的節點上。本書的第7章還會討論容器倉庫的技術方案和實施細節。值得指出的是,鏡像倉庫并不是唯一的分發容器打包產物的方法,一些研究項目也在嘗試通過例如共享存儲、P2P傳輸等途徑解決鏡像倉庫中心節點流量壓力較大的問題,然而由于成熟度和實施難度方面的原因,這些研究并沒有被作為主流的鏡像分發手段。
3.使用容器優化測試過程
在測試的過程中,需要依賴的外部系統或組件往往比較多,然而這些系統或組件未必總是處于空閑或可用狀態。這時就需要有大量的臨時資源或是模擬資源(稱為“樁”或“Mock”)。幾種典型的測試依賴資源包括瀏覽器、數據庫和第三方服務接口。對于這些場景,容器都有用武之地。
臨時的瀏覽器資源主要用于界面測試。由于每個瀏覽器在執行測試時一般都由某個用例獨占,為了并發地完成界面測試,測試人員需要準備許多不同種類的瀏覽器資源。特別在自動化并行測試的時候,對瀏覽器種類和數量的需求變化是十分迅速的,必須按照實際用量的最大值來準備這些資源,因而造成不少浪費。此外,目前進行與瀏覽器相關的測試通常都是使用Selenium/WebDriver和相關的SDK來驅動的,提前部署、長期維護所需的服務和相應驅動同樣都很費時。
容器提供了很好的臨時瀏覽器提供方法。實際上,Selenium提供了一組基于Chrome和Firefox并預裝了相應驅動的瀏覽器鏡像。該項目的GitHub地址為:https://github.com/SeleniumHQ/docker-selenium。
對于測試時期的數據庫依賴,比如與數據相關的集成測試,由于測試過程需要反復運行,如果前一次測試運行沒有正常地結束,或是沒有正確地清理留下的數據,就特別容易影響后續測試的運行結果。容器恰恰是提供這種即用即棄基礎設施的最佳方式,完全可以在測試腳本中先啟動一個全新的MySQL服務,測試完就銷毀,保證了每次測試的獨立性。
在Docker Hub上有許多主流數據庫的鏡像可供使用。例如MySQL和MongoDB的鏡像地址分別是https://hub.docker.com/r//mysql和https://hub.docker.com/r/_/mongo。
這些鏡像都提供了在容器啟動時修改數據庫配置和注入初始化數據的方法。例如MySQL的鏡像啟動后會自動執行放在“/docker-entrypoint-initdb.d”目錄下的所有SQL或Shell腳本,而MongoDB的鏡像則建議用戶將初始化過的數據內容直接通過Volume掛載到容器的“/data/db”目錄下。
更加常見的場景是測試時需要通過“服務樁”來模擬某些第三方服務的行為,開源社區里已經有許多很不錯的解決方法。其中獲得過Oracle頒發的Duke選擇獎的Moco就是一款值得采用的工具。
雖然Moco的使用本來就十分簡單,在某些集群測試的場景下,將它容器化仍然能獲得快速分發和運行環境無關性的好處。有些社區鏡像已經提供了這方面的功能,例如Docker Hub上的https://hub.docker.com/r/rezzza/docker-moco這個鏡像。
使用者僅僅需要將一個被模擬接口的配置描述文件掛載到該容器的特定位置,就實現了具備特定功能的“服務樁”。
4.通過容器進行集群自動化部署和管理
部署意味著產品的最終發布上線。在傳統流程中,軟件的部署有時是十分耗時、費力的事情,特別是線上環境的部署,一點小小的差錯就會造成嚴重的后果。在用傳統的虛擬機方式部署服務時,相應的操作通常都會使用一個經過反復優化的自動化腳本來完成,但這些復雜的腳本依然可能在一些環節上出現問題,容器化方式的部署通過從根本上簡化部署的過程,可以達到提升部署可靠性的目的。
對于單個服務而言,通過容器進行部署只需要兩個動作,先是停止舊鏡像運行的服務,然后使用新的鏡像啟動服務。在集群中進行服務的部署,雖然本質上與簡單地部署單個容器沒多少差別,但加上在實際運用時需要涉及的因素,就會復雜很多。
像Docker這樣的容器,現在已經具有了許多成熟、開源的集群調度和管理工具,例如SwarmKit、Kubernetes、Mesos或Rancher,它們的價值在于自動地完成了節點選擇、網絡配置、路由切換等一系列的額外工作。同時由于容器使服務不再依賴于主機的配置和環境,從整體來看,采用容器在集群中部署服務依然比采用虛擬機的方式更容易管控。此外,容器化的集群工具通常還具有任務編排和擴展API等能力,能夠簡化日常的運維和比較方便地進行二次開發。