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 这是更新过的密钥