- 容器即服務:從零構建企業級容器集群
- 林帆
- 11330字
- 2019-09-09 16:31:52
2.3 Docker Swarm Mode
2.3.1 Swarm Mode綜述
Swarm Mode指的是Docker整合了SwarmKit項目后增加的那部分功能,包括對容器集群的管理、節點的管理、服務的管理和編排,以及其他的一些輔助功能。Docker代碼有許多地方直接import了“github.com/docker/swarmkit/”項目中的內容,也就是說Docker中與集群相關的功能實際上直接代理給了SwarmKit來實現。
從使用的角度上,Swarm Mode在許多方面也都有著明顯的SwarmKit影子。例如將集群節點分成Manager和Worker,使用Token方式為集群添加節點;其中的許多概念,如Service、Task、Secret等,都直接沿用了SwarmKit中的相應稱呼。同時,二者依然存在著一些差異,比如Swarm Mode弱化了Task的概念而增強了對服務編排的支持。
2.3.2 集群的創建與銷毀
通過Docker的docker swarm命令集可以創建和管理集群,它的參數比SwarmKit中的swarmd命令還要簡單。對于大多數情況,使用不帶任何參數的docker swarm init命令就可以創建出一個新的集群,如下所示。
$ docker swarm init Swarm initialized: current node (1tgyppr5dnz31ua18hxwft3up) is now a manager. To add a worker to this swarm, run the following command: Dockerswarm join -tokenSWMTKN-1-42zcv0......10ruju 172.31.31.164:2377 ... ...
執行了創建集群的節點會自動成為該集群的第一個Manager節點,同時它打印了一個用來添加更多Worker節點的命令。這個命令的模式是docker swarm join-token <Token><Manager-IP>,其中的<Token>是一個用來區分請求加入者角色的序列碼。要是忘記了,可以通過docker swarm join-token命令找回來(加上manage或worker表示要找回的Token種類),如下所示。
$ docker swarm join-token manager To add a manager to this swarm, run the following command: docker swarm join --token SWMTKN-1-42zcv0......48b01p 192.168.65.2:2377 $ docker swarm join-token worker To add a worker to this swarm, run the following command: docker swarm join --token SWMTKN-1-42zcv0......10ruju 192.168.65.2:2377
值得注意的是,添加Worker和Manager節點的Token值是不一樣的。Token不僅僅是用來保護集群不會隨意混入無關節點的憑證,也是在節點加入時區分不同角色的依據。因此,作為集群的管理者,應該妥善保管Token序列的內容。特別是Manager角色的Token,一旦泄露,則有可能使得入侵者能夠隨意向集群里加入新的管理節點,從而控制整個集群。
如果Token泄露了該怎么辦呢?Docker提供了一種Token輪換的機制,即在查看Token的命令中加上--rotate參數,如下。
$ docker swarm join-token --rotate manager Successfully rotated manager join token. To add a manager to this swarm, run the following command: docker swarm join --token SWMTKN-1-42zcv0......qdz76e 192.168.65.2:2377
每次執行這個命令都會改變相應角色加入集群的Token內容,并使得過去的Token失效,這只會影響加入的節點,已經在集群中的節點不受Token輪換的影響。將其他的節點加入集群的命令已經在顯示join-token時完整地顯示了,例如新增一個Worker節點只需SSH登錄到目標節點,然后執行以下命令(注意其中的Token值和IP地址是變化的,請根據實際情況落實)。
$ docker swarm join --token SWMTKN-1-42zcv0......10ruju 192.168.65.2:2377 This node joined a swarm as a worker.
僅需一條命令就完成子節點的加入,這對于使用者而言是十分友好的。
集群的安全隱患除了Manager節點的Token泄露,還出現在Manager節點本身——任何連接到Manager節點的用戶都能直接操縱集群。為此Docker提供了一個給集群上鎖的機制,它是由集群的一個autolock屬性控制的,可以通過docker swarm update來修改,如下所示。
$ docker swarm update --autolock=true Swarm updated. To unlock a swarm manager after it restarts, run the 'docker swarm unlock' command and provide the following key: SWMKEY-1-vhdf9LBAZ16qx8XoLH6TyPfqSaZjlaXWuyrG8aI3pH4 Please remember to store this key in a password manager, since without it you will not be able to restart the manager.
這個命令會輸出一個用于解鎖集群的密鑰值,請將其妥善保存。當autolock屬性開啟后,每當Manager節點的Docker服務被重啟,就會進入鎖定狀態(本質上是鎖了Raft數據文件),此時用戶的所有與集群相關的操作都會被拒絕,如下所示。
$ sudo systemctl restart docker $ docker node ls Error response from daemon: Swarm is encrypted and needs to be unlocked before it can be used. Please use "docker swarm unlock" to unlock it.
此時,如果需要通過這個節點對集群進行操作,需要在該節點執行一次docker swarm unlock命令,然后根據提示輸入解鎖的密鑰,如下所示。
$ docker swarm unlock Please enter unlock key: ********************************
如果忘記了密鑰,只要當前集群中還有一個沒有被鎖定的節點,都可以在該節點上執行docker swarm unlock-key命令重新顯示當前的解鎖密鑰。如果解鎖的密鑰意外泄露了,則可以通過docker swarm unlock-key -rotate命令更新解鎖密鑰的值。
節點解鎖后,每次重啟都會再次自動上鎖。如果希望消除這個功能,只需取消集群的autolock屬性,如下所示。
$ docker swarm update --autolock=false Swarm updated.
如何讓一個節點退出當前已經加入的集群呢?操作同樣十分簡單,只需SSH登錄到節點上執行docker swarm leave命令,如下所示。
$ docker swarm leave Node left the swarm.
此時會發生以下這幾種情況。
·如果這個節點是Worker節點,那么它將直接退出集群。Docker集群會自動將該節點上的所有服務遷移到其他節點上繼續運行。
·如果這個節點是Manager節點,并且恰好是當前Manager中Raft集群的Leader,則退出集群失敗。如果確實要這么做,可以使用--force參數使其強制退出。
·如果這個節點是Manager,且當前集群中共有三個或以上Manager,且該節點是Follower角色,那么它首先將自動降級為Worker,使得Raft集群自動調整,然后再正常退出集群。
·如果這個節點是Manager,且當前集群中有且只有兩個Manager節點,那么即使此節點是Follower角色,由于它若退出會使得Raft集群無法保持“半數以上成員”可用,因此退出集群失敗。如果確實需要退出,同樣可以使用--force參數強制執行。當集群中的所有節點都退出后,集群就被銷毀了。
2.3.3 節點管理
在Docker Swarm Mode中管理節點的命令是docker node。
當集群創建創建完成后,使用docker node ls命令可以看到各個節點的基本狀態信息,如下所示。
$ docker node ls ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS 1h3s0f7tl... * manager-1 Ready Active Leader 645i7ayla... worker-1 Ready Active custji2pm... worker-2 Ready Active
以上顯示的信息包括節點的ID、主機名稱以及狀態。STATUS和AVAILABILITY分別表示節點的健康性和可用性,正常情況下它們的值應該分別為Ready和Active。MANAGER STATUS用于區分節點的Swarm Mode角色(Manager或Worker)、Manager的Raft角色(Leader或Follower)以及Manager節點的健康性。其中節點健康性狀態的顯示關系較復雜,如表2-1所示。
表2-1 Swarm Mode中的狀態屬性

也就是說,對于Worker節點,MANAGER STATUS區域的值始終為空,而當節點有故障時,其STATUS屬性會變為Down。對于Manager節點來說,如果節點是Leader則顯示為Leader, Leader節點一定不會有故障,否則會觸發一輪Raft選舉以重新產生新Leader將其取代。如果Manager節點是Follower,則通過MANAGER STATUS的值區分其狀態,而STATUS屬性始終顯示為Active。
有兩個命令可以用來獲得與特定節點相關的信息,它們都接收一個節點ID作為參數。docker node inspect命令獲得的是節點的完整狀態和屬性,如下所示。
$ docker node inspect 1h3s0f7tl31jn0hqfsaeb5axr [ { "ID": "1h3s0f7tl31jn0hqfsaeb5axr", "Version": { "Index": 30 }, "Spec": { "Role": "manager", "Availability": "drain" }, . ...
docker node ps命令獲得的是指定節點上運行的Task狀態,如下所示。關于“Task”的概念會在下一個小節中介紹。
$ docker node ps 1h3s0f7tl31jn0hqfsaeb5axr ID NAME IMAGE NODE DESIRED CURRENT ... vz45... nginx.1 nginx:latest manager-1 Running Running ...
與SwarmKit一樣,Swarm Mode中節點的角色是可以轉換的。進行角色轉換操作的命令是docker node promote和docker node demote,它們的參數是要被轉換的節點ID。
轉換Worker成Manager:
$ docker node promote 645i7aylaciu680a0skaelzgy Node 645i7aylaciu680a0skaelzgy promoted to a manager in the swarm.
轉換Manager成Worker:
$ docker node demote 1h3s0f7tl31jn0hqfsaeb5axr Manager 1h3s0f7tl31jn0hqfsaeb5axr demoted in the swarm.
需要指出的是,所有與集群節點、服務以及密文的管理相關的操作都只能在Manager節點上進行。因此Worker節點是不能自己將自己提拔成Manager的。
對節點角色的更改實際上是更改節點屬性的一種特例,對于更通用的情況,可以使用docker node update命令。比如docker node promote <node-id>和docker node demote <node-id>實際上等效于docker node update <node-id> --role manager和docker node update <node-id> --role worker。
另一個比較常用的節點屬性是節點可用性狀態,它的值可以是Active、Pause或Drain。其中Active狀態表示節點可以正常使用。Pause狀態的節點不會再參與新的Task調度,但已經調度到該節點的Task可以繼續運行。Drain狀態通常用于對節點進行維護或升級,處于此狀態的節點將清理掉調度到當前的所有Task容器(Swarm Mode會自動將這些容器在其他節點啟動),同時也不再參與新Task調度。
可以使用如下命令更改節點可用性狀態。
$ docker node update <node-id> --availability <active|pause|drain>
在前一小節中介紹到,為了讓節點正常退出集群,需要登錄到目標節點上,然后執行docker swarm leave命令。然而有時集群中的節點會發生故障,此時用戶無法登錄到節點上將該節點退出,因此該節點的可用性狀態始終顯示為Down,如下所示。
$ docker node ls ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS 1h3s0f7tl... * manager-1 Ready Active Leader 645i7ayla... worker-1 Ready Active custji2pm... worker-2 Down Active
針對這種情況,Swarm Mode提供了一個docker node rm命令,這個命令只能作用在已經處于Down狀態的節點,如下所示。
$ docker node rm custji2pmx9u6z7q8tigr0fx3 custji2pmx9u6z7q8tigr0fx3 $ docker node ls ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS 1h3s0f7tl... * manager-1 Ready Active Leader 645i7ayla... worker-1 Ready Active
當被指定的節點當前處于Active狀態時,執行docker node rm命令會失敗,此時如果要移除該節點,需根據節點角色區分情況,如下所示。
·指定節點是Worker角色:在這種情況下,如果需要移除指定節點,應該登錄到節點上執行docker swarm leave操作。
·指定節點是Manager角色:在這種情況下,首先需要將指定節點轉換成Worker節點,然后根據轉換后的節點狀態決定重新執行docker node rm或docker swarm leave操作。
2.3.4 服務管理
在開始深入Swarm Mode的服務相關操作之前,有必要了解一下它的服務管理模型,如圖2-4所示。

圖2-4 Swarm Mode中的服務管理模型
在介紹SwarmKit時,已經簡單地提到了Task和Service的概念。Task是對底層運行單元的抽象,在當前的Swarm Mode中,一個Task實際上就是一個容器,它是Swarm Mode的最小調度單元,用于運行一個Service的容器副本。在Swarm Mode中沒有專門管理Task的命令,不過通常直接使用docker container命令組來操作容器。
Service是管理部署的單元,每個Service中可以包含一個或多個相同的容器副本,分布在集群的不同節點上,根據負載需要進行擴縮。此外,Service還封裝了負載均衡的功能,用于為屬于同一個Service的分散容器提供統一的訪問入口。本書中為了使行文通順,在許多地方使用中文的“服務”替代“Service”一詞,可以根據上下文判斷指的是Swarm Mode模型的“Service”,還是泛指普通的服務。
Stack是Swarm Mode與服務編排相關的概念,它將多個不同的固定Service關聯在一起,進行整體的部署和升級,對外體現為由多種容器組合而成的復雜系統。
在Swarm Mode集群中,創建服務與在單個節點上創建容器頗有幾分相似,只不過使用的不是docker container run,而是docker service create,如下所示。
$ docker service create --detach \ --name web \ --publish 8080:80 \ --replicas=3 \ nginx:1.11.1-alpine
這個命令創建了一個使用nginx:1.11.1-alpine鏡像部署的服務,將其命名為“web”。它包含三個容器,并將容器中的80端口映射到外部的8080端口。集群里有那么多節點,這個服務映射的是哪個節點IP的8080端口呢?答案是所有的節點。現在訪問集群中的任意一個節點IP地址的8080端口,都將看到一個Nginx的默認首頁頁面。
此外,Docker Swarm Mode還悄悄地做了一些額外的配置工作。首先是將服務注冊到了集群內的DNS,這樣與該容器處于同一網絡的其他容器就能直接通過服務名稱來訪問此服務,這里的網絡指的是由docker network命令管理的容器網絡。然后為服務添加了一個由Linux IPVS管理的內核四層負載均衡器,任何訪問該服務的請求都會由各個后端容器輪詢處理。下面通過一個例子來驗證。
首先創建一個獨立的容器Overlay網絡,如下所示。
$ docker network create demo --driver overlay
在這個網絡中使用flin/whoami鏡像創建由三個副本組成的服務,如下所示,這個鏡像會運行一個監聽8000端口進程,當有請求訪問它時,會通過HTTP返回當前運行容器的ID,據此可以判斷出請求是哪個容器響應的。
$ docker service create --detach \ --name whoami --replicas 3 \ --network demo --publish 8000:8000 flin/whoami
創建一個用來訪問被測試服務的容器,它需要與前者在同一個網絡中,如下所示。
$ docker service create --detach \ --name curl --network demo flin/curl
接下來需要進入curl服務的容器,它只有一個容器副本,使用docker service ps命令可以看到這個副本當前運行在哪個節點上,如下所示。
$ docker service ps curl ID NAME IMAGE NODE DESIRED CURRENT ... 6czgya1... curl.1 flin/curl worker-1 Running Running ...
SSH進入這個節點,使用docker container ps命令找到容器的實際ID,然后從容器內訪問whoami服務,如下所示。
$ docker ps | grep curl | awk '{print $1}' ebed7646cf85 $ docker exec ebed7646cf85 curl -s whoami:8000 I'm d819ec8ff4cd $ docker exec ebed7646cf85 curl -s whoami:8000 I'm c2bf30b3fb17 $ docker exec ebed7646cf85 curl -s whoami:8000 I'm a2ccf532b47d
可以看到,在容器中通過“whoami”這個名稱可以直接訪問相應的服務,并且多次訪問此服務時,后端的各個Task容器是輪流處理請求的。
Swarm Mode創建服務時有許多可用的參數,比較常用的有--port、--mount、--mode、--restart-condition和--constraint。
·--port是暴露服務端口的通用寫法,例如前面Nginx服務例子的--publish 8080:80等同于--port mode=ingress, target=80, published=8080, protocol=tcp。
·--mount用于掛載存儲卷或本地目錄,例如--mount type=volume, source=data_vol_01, target=/var/data, volume-driver=flocker。在集群中掛載存儲目錄時需要特別注意,因為當服務被自動遷移到另一個節點時,掛載目錄存儲的內容可能會丟失,因此集群服務的容器通常只是掛載如“/var/run/docker.sock”“/sys”這種有特定目的系統目錄,或是采用NFS、Ceph存儲的網絡磁盤分區(配合Flocker或Convoy等工具更佳)。
·--mode參數的值可以是replicated或global,默認值是replicated,此時可以用--replicas參數設置副本的個數。若將服務的mode設置為global,則該服務會自動在每個節點上都運行一個副本。這個特性特別適用于部署監控和基礎設施類的服務,每個新節點進入集群后都將自動創建這些global類型服務的Task容器。
·--restart-condition參數服務的容器停止后,Swarm Mode需要判斷是否將其自動重啟,默認值是any,表示總是重啟。可以將它設置為on-failure,只在容器中進程退出且返回值不為0的時候才重啟。或設置為none,表示不自動重啟該服務。
·--constraint參數用于限制服務的容器可以被調度的節點,它可以根據節點的ID、主機名、角色或是標簽進行選擇性調度,其中標簽是最靈活的一種方式。例如下面的操作給worker-1節點添加了一個zone=cn標簽,然后在創建容器時指定服務只調度到具有這個標簽的節點。
$ docker node update worker-1--label-add zone=cn $ docker service create --detach \ --constraint node.labels.zone==cn ...
使用docker service create --help命令可以查看創建服務時其他的可用參數列表。
Swarm Mode管理服務的命令與管理節點命令十分相似,如下所示。例如docker service ls命令用于查看服務列表,docker service inspect命令用于查看指定服務的詳細信息,docker service rm命令用于刪除一個服務。docker service update用于更新服務的屬性。
$ docker service ls ID NAME REPLICAS IMAGE COMMAND 7zvnvn0ymy5x web 3/3 nginx:1.11.1-alpine a80zs876w8ft curl 1/1 flin/curl e7rn44ba6let whoami 3/3 flin/whoami $ docker service inspect web [ { "ID": "7zvnvn0ymy5xvk859nmfvl6zz", "Version": { "Index": 225 }, "Spec": { "Name": "web", "TaskTemplate": { "ContainerSpec": { "Image": "nginx:1.11.1-alpine" }, ... . $ docker service rm web web $ docker service update web --publish-add 8000:80 web
此外還有幾個服務管理特有的命令。前面已經使用過的docker service ps命令用于顯示指定服務的Task信息。在Swarm Mode中沒有提供專門管理Task的命令,因此想列出集群中所有的Task需要遍歷每個服務。這里有個小技巧可以將docker service ls和docker service ps命令組合起來,一次性查看所有Task的狀態信息,如下所示。
$ docker service ls -q | xargs -n1 docker service ps ID NAME IMAGE NODE DESIRED ... 4gs065b94... curl.4 flin/curl manager-1 Running ... ID NAME IMAGE NODE DESIRED ... 60l95vxus... whoami.1 flin/whoami manager-1 Running ... 5cng10elv... whoami.2 flin/whoami worker-1 Running ... esipzyazs... whoami.3 flin/whoami worker-2 Running ... ID NAME IMAGE NODE DESIRED ... a7tdpqmr5... web.1 nginx:... manager-1 Running ... 1dsofk5fs... web.2 nginx:... worker-1 Running ... e3xra5oru... web.3 nginx:... worker-2 Running ...
docker service logs命令可以使用服務命令直接查看服務各個容器的日志,這個命令可以避免用戶在集群中反復查找和登錄各個節點去查看容器日志,對于排查服務故障十分有用。docker service scale命令用于修改服務的容器副本數目,它其實是docker service update命令其中一個功能的快捷表示方式。以下這兩個命令是等效的。
$ docker service scale web=5 $ docker service update web --replicas 5
在服務管理中,還有一個必須考慮的事情——服務的版本升級。在容器化的體系中,升級一個服務實際上就是替換服務鏡像。然而替換鏡像意味著需要重啟容器,但在實際生產環境的應用場景中,一個服務中的所有容器并不是隨時想停就能停的。不過由于Swarm Mode為每個集群中創建的服務都提供了與容器副本相關聯的負載均衡器,因此如果服務本身是無狀態的,就可以使用一種被稱為“滾動升級”的方式完成不離線的版本變更。
服務的“滾動升級”指的是將集群中處于同一個負載均衡器背后的副本實例逐步地替換成新的版本,并動態更新負載的流量,以確保在整個升級過程中對外的服務功能不停止。服務的鏡像也是服務屬性之一,因此可使用docker service update命令進行更改,而逐步替換功能在Swarm Mode中是通過在升級服務鏡像的同時限制更新并發數量實現的。下面以升級Web服務的鏡像版本為例。
$ docker service update web --image nginx:1.11.5-alpine \ --update-parallelism 1--update-delay 3s
在升級過程中,可以從另一個控制臺窗口觀察服務的Task容器變化過程,每隔三秒替換一個容器,直到所有的版本升級完成,如下所示。
$ watch 'docker service ps web | grep Running'
嚴格來說Swarm Mode中的“滾動升級”支持并不是非常完整,因為它只能做正向的滾動,如果升級出現錯誤,只能通過以低版本鏡像為目標的再一次“滾動升級”來完成降級。但這對于通常的自動化運維而言已經足夠了。
最后順帶介紹一個比較有用的技巧。在Docker中有許多ID,例如節點ID、服務ID、網絡ID,等等,許多命令都需要和這些長長的ID串打交道,看起來頗為啰唆。實際上,在不引起歧義的情況下,Docker允許使用ID的開頭任意一個字符來替代這個長ID串,如下所示。
$ docker service create --detach --name nginx nginx:latest
此時如果在所有的服務里,只有這一個服務的ID是“m”開頭的,那么可以直接這樣引用它,如下所示。
$ docker service logs m
此時的“m”就指代前面的那串長ID:mfhocfzr8uk6uxyranoqo1yg2。如果有兩個服務都是字母“m”開頭,則Docker會提示錯誤,如下所示。
$ docker service logs m Error response from daemon: service m is ambiguous (2 matches found)
那么用戶至少需要指定開頭兩個字母,比如“mf”,以唯一確定ID,以此類推。熟練掌握這個技巧可以避免許多拷貝和復制ID串的麻煩。
2.3.5 服務編排
服務編排指的是將一組服務按照它們之間的關聯進行統一管理,以快速構建基于容器的復雜應用的一種方式。Compose一直以來都是Docker的服務編排工具,它通過一個YAML配置文件來描述各個容器的關聯,然后提供一組命令來以整體視角操作所有容器。
在介紹集群級別的服務編排之前,先看一下在單主機上進行服務編排的過程。新建一個目錄,創建名稱為“docker-compose.yml”的文件,內容如下所示。
version: "3" services: redis: image: redis:3.2.5-alpine networks: - demo-net app: image: flin/page-hit-counter:v1 ports: -5000:5000 depends_on: - redis networks: - demo-net networks: demo-net: external: false
“docker-compose.yml”是Compose默認的編排規則描述文件名,上述YAML文件描述了一個由兩個服務和一個網絡組成的系統,其中Redis服務提供了數據的外部緩存功能,而App服務是一個訪問計數器,每次收到用戶請求時,它就會從外部緩存中獲取當前的訪問總計數,將計數值加1,然后寫回外部緩存。通過使用外部緩存保存計數結果,App服務就實現了無狀態化,因此可以使用多個負載均衡的副本來分擔用戶的訪問請求。
使用Compose將這個文件描述的服務快速創建出來,如下所示。
$ docker-compose up -d
連續訪問當前節點的5000端口,會看到不斷遞增的訪問計數,如下所示。
$ curl localhost:5000 You have hit this page 1 times. - Edition v1 $ curl localhost:5000 You have hit this page 2 times. - Edition v1 ... ...
使用docker-compose命令可以批量地管理這些服務,例如將服務的所有容器停止,如下所示。
$ docker-compose stop
快速刪除整個服務,包括服務涉及的所有容器、網絡和存儲等資源,如下所示。
$ docker-compose down
docker-compose命令行的其他子命令及其說明如表2-2所示,其中的許多子命令與Docker工具本身十分相似,只是將作用范圍擴大到了編排文件所描述的整個服務組。
表2-2 Docker Compose的子命令及其說明

值得注意的是,使用不帶參數的docker-compose up命令啟動服務后,所啟動的所有容器會保持在前臺,并實時打印運行日志到控制臺,這個行為與Docker命令行工具是一致的。但更多的時候用戶希望docker-compose命令在啟動完服務之后就把所有容器放到后臺運行,此時需要加上-d參數,如之前的例子所示。
從這個命令列表不難發現,Docker Compose的所有操作都是基于服務編排描述文件的內容執行的。下面簡單介紹一下這個編排文件的語法結構。
docker-compose文件的頂級屬性只能是version、services、volumes、networks、configs、secrets等固定的幾個(其中configs和secrets不能用于單機的服務編排)。每個頂級屬性下面會配置相應的資源描述。
1.版本
version屬性僅僅描述當前編排文件所用的語法版本,Compose的編排語法從誕生以來進行了幾次大的版本升級,在本書截稿時(2017年10月)的最新穩定版本是3.3。讀者可根據實際情況指定。
2.服務
services屬性是整個編排描述中最核心的部分,包含了所需要運行的鏡像信息、容器信息、副本數量以及對網絡、磁盤、密文等資源的引用。
第二層級的屬性是服務的名稱,如下所示。
services: redis: ... app: ...
這表示這個服務編排組中一共包含兩個服務,分別是redis和app,每個名稱后的部分則是對該服務的詳細描述。
對于服務所用鏡像的描述,可以使用build或image兩種語法,前者表示構建需要從Dockerfile開始構建,此時進一步指定構建源所使用的工作目錄和Dockerfile文件名稱,如下所示。
services: webapp: build: context: ./dir dockerfile: Dockerfile-demo
后者表示從直接倉庫獲得指定鏡像,如下所示。
services: webapp: image: redis
描述容器副本數量的屬性是deploy.replicas,同樣在deploy屬性下面的子屬性還有滾動升級參數、重啟動參數、資源配額限制、部署位置約束等。注意,deploy屬性對單機部署的服務編排無效,只能用于集群模式(即“應用棧”,見下一小節)。具體示例如下。
services: webapp: image: webapp deploy: replicas: 4 #啟動4個副本 update_config: #升級時每次替換2個容器,間隔10s parallelism: 2 delay: 10s restart_policy: #出錯退出時自動重啟,最多3次,間隔3s condition: on-failure delay: 5s max_attempts: 3 resources: #最大和最小的資源使用量 limits: cpus: '0.1' memory: 300M reservations: cpus: '0.01' memory: 100M
服務的容器可以使用端口、網絡、存儲、外置配置和密文等資源,并設置相關參數,但不包括對這些資源本身的描述,如下所示。
services: webapp: image: webapp volumes: #掛載webapp_volumn存儲卷 - webapp_volumn:/opt/app/static ports: #指定端口映像 - "3000/udp" - "8000:8000" networks: #加入webapp_network網絡 - webapp_network configs: #掛載webapp_config外置配置 - webapp_config secrets: #掛載webapp_secret密文 - webapp_secret
對資源的描述是寫在各自單獨的屬性段里的。此外常用的屬性還有指定容器啟動順序關系的depends_on、覆寫容器入口命令的command、配置環境變量的environment、進行容器健康檢查的healthcheck等,這里不再逐個展開介紹。
3.存儲卷
volumes屬性是對服務中所使用到的存儲卷進行詳細描述的地方,主要包含存儲卷類型和參數。
下面就是一個使用了存儲卷的服務示例。
services: db: image: postgres volumes: #掛載data存儲卷 - data:/var/lib/postgresql/data volumes: data: #名稱是data的存儲卷描述 driver: local external: false
其中的external屬性為false表示這個存儲卷是由Compose管理的,會自動創建和刪除,若用戶希望自己預先創建存儲卷,并自行管理存儲卷生命周期,可將該屬性設置為true。
4.網絡
networks屬性是對服務中使用到的網絡進行詳細描述的地方,主要包含網絡類型和參數。
下面就是一個指定了網絡的服務示例。
services: app: build: ./app networks: - backend #加入backend網絡 networks: backend: #名稱是backend的網絡描述 driver: bridge external: false
網絡的類型可以是bridge或overlay,但overlay類型的網絡只能用于集群模式。
在網絡配置中同樣有一個external屬性,若為false表示這個網絡是由Compose管理的,會自動創建和刪除,若用戶希望自己預先創建網絡,并自行管理網絡生命周期,可將該屬性設置為true。
5.外置配置
configs屬性是對服務中使用到的外置配置進行詳細描述的地方,只對集群模式下的編排(即“應用棧”)有效,不能用于單機的服務編排。
下面就是一個指定了外置配置的服務示例。
services: app: build: ./app configs: - init_cf #掛載init_cf外置配置 configs: init_cf: #名稱是init_cf的外置配置描述 file: ./init.cf external: false
同樣有external屬性,表示外置配置的生命周期是否由Compose統一管理。
6.密文
secrets屬性是對服務中使用到的密文進行詳細描述的地方,只對集群模式下的編排(即“應用棧”)有效,不能用于單機的服務編排。
下面就是一個指定了密文的服務示例。
services: app: build: ./app secrets: - app_passwd #掛載app_passwd密文 secrets: app_passwd: #名稱是app_passwd的密文描述 file: ./secret_passwd external: false
完整的compose服務編排語法可參考官方文檔。
Docker Compose的編排文件名若由于特殊原因沒有出現在當前目錄,或沒有命名為“docker- compose.yml”時,可以在執行相應命令之前使用“-f <編排文件名>”的方式指定所用編排文件的位置,如下所示。
$ docker-compose -f path/to/my-compose-file.yml up
對于更復雜的場景,還可以將編排文件進行組合。例如將一個完整服務的非必須組件剝離到單獨的編排描述文件,或是將在不同環境運行時需要配置的不同部分寫到單獨的編排描述文件,然后在創建服務時通過多個-f參數依次指定這幾個編排文件的位置,Docker Compose會將它們中的內容合并為一個編排文件來執行,如下所示。
$ docker-compose -f docker-compose.yml -f docker-compose-dev.yml up
若compose-file.yml文件的內容發生了變化,例如替換了服務的鏡像、修改了服務參數或是增加了新的服務,只需要在“compose-file.yml”文件所在的目錄中再次執行docker-compose up-d命令,Docker Compose會自動調整和重啟相關的容器,使得運行的服務與描述保持一致,而無須手動重啟或重建整個服務組。
2.3.6 應用棧的管理
描述服務編排的Docker Compose同樣可以被用于在集群中部署服務和管理服務之間的關聯,不過集群編排部署服務和單機的情況會稍有一些差異,主要體現在兩者支持功能的差異上。譬如,在集群部署的服務通常會使用Overlay類型的網絡,不支持--net=host模式的容器,不支持掛載本地設備(各節點設備可能不一致,存在隱患),但Swarm集群模式下的Docker增加了對外置配置和密文的支持。因此,雖然同樣可以使用類似的YAML語法編排,可用的元素仍存在一些差異。
在集群中,每個服務在描述具體的編排細節時,有時會引用一些外部的文件,如外置配置或者密文的文件,在跨節點分發這些信息時,單獨管理它們之間的關系會是一件麻煩事。Docker曾經提出過一種實驗性的集群部署專用打包文件來承載集群的服務編排配置,稱為“分布式應用包”(Distributed Application Bundles,簡稱DAB),保存為后綴名“*.dab”的文件。使用Docker Compose的docker-compose bundle命令可以將標準的“docker-compose. yml”文件轉換成這種包。不過目前看來,這種打包格式實際很少有人使用,因此本書不打算詳細介紹它。若讀者對這部分內容有興趣,請移步Docker文檔。
在Swarm集群中,將通過編排方式部署到集群中的一組服務稱為“應用棧(Stack)”。通過Docker命令行工具的docker stack下的子命令可以對集群中的應用棧進行管理。
首先將上個小節中的服務編排文件稍加修改,使它更適于在集群部署,如下所示。
version: "3.3" services: redis: image: redis:3.2.5-alpine networks: - demo-net app: image: flin/page-hit-counter:v1 ports: -5000:5000 depends_on: - redis networks: - demo-net deploy: #增加多個副本 replicas: 2 networks: demo-net: driver: overlay #更改網絡驅動為overlay external: false
使用docker swarm命令創建集群并添加幾個節點。然后使用docker stack deploy命令創建部署到集群的應用棧,如下所示。注意docker stack命令只能用于已經開啟Swarm Mode的節點。
$ docker stack deploy -c docker-compose.yml app Creating network app_demo-net Creating service app_app Creating service app_redis
執行docker stack ls命令可以列出當前集群中的所有應用棧列表以及每個應用棧中包含的服務種類,如下所示。
$ docker stack ls NAME SERVICES app 2
指定一個應用棧,用docker stack services命令列出該應用棧中的所有服務,如下所示。
$ docker stack services app ID NAME MODE REPLICAS IMAGE PORTS yrrdl... app_redis replicated 1/1 redis:... v19nq... app_app replicated 2/2 flin/p... *:5000->5000/tcp
使用docker stack ps命令可以直接看到應用棧里的所有容器,如下所示。
$ docker stack ps app ID NAME IMAGE NODE DESIRED CURRENT svr6s... app_redis.1 redis:... node1 Running Running 8pa51... app_app.1 flin/page... node2 Running Running pej7j... app_app.2 flin/page... node1 Running Running
基于Docker Service的特性,一旦集群模式的容器指定了監聽端口,它會占用整個集群中所有節點的指定端口。雖然只運行了兩個容器的副本,但在集群的任意一個節點上訪問5000端口,都能夠訪問到這個服務,如下所示。
$ curl localhost:5000 You have hit this page 1 times. - Edition v1
換一個節點訪問,如下所示,由于服務的狀態是外置在Redis中的,請求的數量會被累計。
$ curl localhost:5000 You have hit this page 2 times. - Edition v1 ... ...
服務升級時首先要修改“docker-compose.yml”文件,例如將flin/page-hit-counter: v1鏡像替換為flin/page-hit-counter:v2,然后還要直接執行docker stack deploy命令,如下所示。
$ docker stack deploy -c docker-compose.yml app Updating service app_redis (id: yrrdl...) Updating service app_app (id: v19nq...)
再次訪問5000端口,如下所示,會發現服務返回內容有了變化(末尾的Edition v2,這是在新版本鏡像中修改的),同時保存在Redis內存中的訪問計數并沒有被清零。這說明在升級過程中,只有發生了更改的容器(本例中的App)被重新創建了,而未發生變化的容器(本例中的Redis)并不會被連帶重啟。
$ curl localhost:5000 You have hit this page 3 times. - Edition v2
最后,執行docker stack rm命令刪除指定的應用棧及所有相關的資源,如下所示。
$ docker stack rm app Removing service app_redis Removing service app_app Removing network app_demo-net
2.3.7 外置配置和密文管理
在許多企業中,一些與環境相關的信息往往是由專門的運維團隊管理的,開發團隊并不關心在線上使用的數據庫地址、密碼以及其他與程序邏輯無關的事情。此外,這些數據可能需要定期地變動,將它們放在服務的鏡像中并不太合適。Swarm Mode的外置配置和密文就是為了支持這種職責分離的場景而引入的。
這兩個功能是Swarm Mode在Docker 1.13以后新增的特性,它允許用戶將一些運行時的配置信息提前保存到集群中,在啟動服務時再動態地加載到容器里。實際上,在Kubernetes中早就有ConfigMap和Secret這兩類對象了,同樣是為了將軟件的開發與運行解耦。具體來說,用戶預先保存到集群里的信息,有一些是不帶有保密性質的,比如一段普通文本信息或是某個測試用的緩存地址,另一些可能包含重要的敏感信息,比如生產運行環境數據庫的密碼。可以使用外置配置來管理那些不需要加密存儲的普通數據,而包含敏感信息的數據則應該采用密文來管理。
與外置配置相關的操作定義在docker config命令下,它遵循Swarm Mode命令的一貫模式,由幾個主要的子命令組成,如下所示。
·docker config create命令能夠創建需要存儲在集群的配置文件。
·docker config ls命令用于查看當前集群中所有保存過的外置配置列表。
·docker config inspect命令可以查看指定配置文件的詳細屬性。
·docker config rm命令用于刪除指定外置配置。
下面就以存儲一個配置文件內容到集群,并在服務啟動后讀取為例,介紹外置配置的運用場景。首先創建一個外置配置對象,如下所示。
$ cat <<EOF | docker config create demo-config - { "name": "demo" } EOF
創建一個服務,使用--config參數將預先創建到集群中的外置配置掛載到容器,如下所示。
$ docker service create \ --detach \ --replicas 1 \ --name nginx \ --config demo-config \ nginx:alpine
外置配置默認掛載的位置是容器的根目錄,如下所示。
$ docker service ps nginx # 找到運行的節點 $ docker ps # 找到容器的ID $ docker exec -it <容器ID> cat /demo-config { "name": "demo" }
使用docker service update可以動態地修改掛載的外置配置,例如將它移除,如下所示。
$ docker service update --detach --config-rm demo-config nginx $ docker exec -it <容器ID> cat /demo-config cat: can't open '/demo-config': No such file or directory
注意,這個操作實際上重新創建了新的容器,因此容器的ID會發生變化,同時隱含一個服務重啟的操作。
在掛載外置配置時,也可以指定掛載的目標文件,讓掛載后的文件名和被掛載的外置配置名稱不一樣。還可以在外掛配置對象的名稱上增加版本標識,實現配置版本化管理,如下所示。
$ cat <<EOF | docker config create demo-config-v1- { "name": "demo", "version": "v1" } EOF $ docker service create \ --detach \ --replicas 1 \ --name nginx \ --config src=demo-config-v1, target="/etc/demo-config" \ nginx:alpine
更新服務配置的時候,可以同時指定移除和添加的外置配置,從而實現配置內容替換,如下所示。
$ cat <<EOF | docker config create demo-config-v2- { "name": "demo", "version": "v2" } EOF $ docker service update --detach \ --config-rm demo-config-v1 \ --config-add source=demo-config-v2, target=/etc/demo-config \ nginx
與密文相關的操作由docker secret命令定義,包含的子命令與外置配置基本相同,如下所示。
·docker secret create命令能夠創建需要存儲在集群的密文數據。
·docker secret ls命令用于查看當前集群中所有保存過的密文列表。
·docker secret inspect命令可以查看指定密文數據的詳細屬性。
·docker secret rm命令用于刪除指定密文。
與外置配置相比,密文的主要區別在于,它的內容在Swarm中存儲和在容器網絡中傳輸時都是以加密的形式存在的,只在目標節點中被解密到內存中,然后從內存映射到容器里。而外置配置是以普通磁盤文件的形式掛載進容器的。由于存在著加解密的額外開銷和運行時的額外內存占用,單個密文內容被限制在500KB內。
密文的管理方式與外置配置幾乎相同,下面這個例子把存儲的內容從配置文件換成一串密碼字符。首先把密碼內容使用管道發送給Docker,創建一個名稱為“demo-password”的密文對象。發送給Docker的內容在集群內部會被自動加密存儲,如下所示。
$ echo "這是密碼的內容" | docker secret create demo-password -
接下來創建一個服務,在創建時用--secret參數給服務添加一個密文對象,如下所示。
$ docker service create \ --detach \ --replicas 1 \ --name nginx \ --secret demo-password \ nginx:alpine
當這個服務的所有容器啟動時,會自動在“/run/secrets”目錄下掛載一個與密文對象同名的文件,例如“/run/secrets/demo-password”,其中的內容是解密后的原始密文,如下所示。
$ docker service ps nginx # 找到運行的節點 $ docker ps # 找到容器的ID $ docker exec -it <容器ID> cat /run/secrets/demo-password 這是密碼的內容
通過這種方式,服務中的進程就可以在實際運行的時刻,從特定的目錄獲取所需的密文。而這些密文的內容可以比較方便地由管理集群和運行環境人員提供和更換。
在Docker 17.06版本開始,密文的位置也是可以隨意指定的,格式與外置配置相似,如下所示。
$ docker service create \ --detach \ --replicas 1 \ --name nginx \ --secret source=demo-password, target=/opt/passwd \ nginx:alpine
同樣可以對服務掛載的密文進行動態替換,如下所示。
$ echo "這是更新過的密鑰" | docker secret create demo-password-v2- $ docker service update \ --detach \ --secret-rm demo-password \ --secret-add source=demo-password-v2, target=/opt/passwd \ nginx $ docker exec <容器ID> cat /opt/passwd 這是更新過的密鑰