6.4 Docker三剑客之Swarm
Docker Swarm是Docker官方三剑客项目之一,提供Docker容器集群服务,是Docker官方对容器云生态进行支持的核心方案。使用它,用户可以将多个Docker主机抽象为大规模的虚拟Docker服务,快速打造一套容器云平台。
6.4.1几个概念
Swarm集群(Cluster):Swarm集群(Cluster)为一组被统一管理起来的Docker主机。
节点:
- 管理节点(manager node):负责响应外部对集群的操作请求,并维持集群中资源,分发任务给工作节点。同时,多个管理节点之间通过Raft协议构成共识。一般推荐每个集群设置5个或7个管理节点;
- 工作节点(worker node):负责执行管理节点安排的具体任务。默认情况下,管理节点自身也同时是工作节点。每个工作节点上运行代理(agent)来汇报任务完成情况。
服务:一个服务可以由若干个任务组成,每个任务为某个具体的应用。服务还包括对应的存储、网络、端口映射、副本个数、访问配置、升级配置等附加参数。
- 复制服务(replicated services)模式:默认模式,每个任务在集群中会存在若干副本,这些副本会被管理节点按照调度策略分发到集群中的工作节点上。此模式下可以使用-replicas参数设置副本数量;
- 全局服务(global services)模式:调度器将在每个可用节点都执行一个相同的任务。该模式适合运行节点的检查,如监控应用等。
任务:任务是Swarm集群中最小的调度单位,即一个指定的应用容器。
6.4.2环境准备
准备3台linux主机(swarm191,swarm192,swarm193);
分别配置好ip地址,使它全可以互通;
分配一下/etc/hosts文件,使3台主机可以使用主机名通讯(比较方便一点);
在swarm191这台主机上安装好docker
参照本文2.2
在swarm191上安装machine(其它节点使用machine来部署)
具体参照本文6.2
6.4.3创建集群
docker swarm init [OPTIONS]
Options:
--advertise-addr:指定监听的IP地址和端口 (format: <ip|interface>[:port]),默认为所有网口的2377端口。
--autolock:自动锁定管理服务的启停操作,对服务进行启动或停止都需要通过口令来解锁;
--availability string:节点的可用性,包括active、pause、drain三种,默认为active;
--cert-expiry duration:根证书的过期时长,默认为90天;
--data-path-addr:指定数据流量使用的网络接口或地址;
--dispatcher-heartbeat duration:分配组件的心跳时长,默认为5秒;
--external-ca external-ca:指定使用外部的证书签名服务地址;
--force-new-cluster:强制创建新集群;
--max-snapshots uint:Raft协议快照保留的个数;
--snapshot-interval uint:Raft协议进行快照的间隔(单位为事务个数),默认为10 000个事物;
--task-history-limit int:任务历史的保留个数,默认为5。
群集创建成功,上图中红框内的一串字符需要保存下来,用于其它节点加入集群时使用。
使用netstat –tupln命令看一下端口:
红框内的三个端口,就是swarm开放的三个端口,我们需要在防火墙中把它们放通
firewall-cmd --add-port=2377/tcp --permanent
firewall-cmd --add-port=2377/tcp
firewall-cmd --add-port=7946/tcp --permanent
firewall-cmd --add-port=7946/tcp
firewall-cmd --add-port=4789/udp --permanent
firewall-cmd --add-port=4789/udp
查看一下集群的信息:
docker info
这里会把集群的基本信息列出来。
查看节点信息:
docker node ls
目前只有一个节点,也就是创建群集的本机。
6.4.4其它节点加入集群
docker swarm join [OPTIONS] HOST:PORT
options:
--advertise-addr:指定管理节点的IP地址和端口 (format: <ip|interface>[:port]),默认为2377端口。
--availability string:节点的可用性,包括active、pause、drain三种,默认为active;
--data-path-addr:指定数据流量使用的网络接口或地址;
--listen-addr node-addr 监听端口 (format: <ip|interface>[:port]) (default 0.0.0.0:2377)
--token:标识字符串(就是创建集群时产生的那串字符)
我们到另一台主机上执行:
docker swarm join –token xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ip:port
这里显示已经加入了集群。
到管理节点上看一下节点信息:
发现已经有三个节点了。swarm191那台是leader,也就是管理节点。
看下工作节点上的端口信息:
工作节点会开放两个端口,我们同样也需要在防火墙中把这两个端口打开。
firewall-cmd --add-port=7946/udp--permanent
firewall-cmd --add-port=7946/udp
firewall-cmd --add-port=4789/udp --permanent
firewall-cmd --add-port=4789/udp
6.4.5服务管理
docker service COMMAND
COMMAND:
create:创建一个服务
inspect:查看服务的详细信息
logs:获取服务的日志
ls:列出服务
ps:列出服务内的任务(容器)
rm:删除服务
rollback:恢复服务的配置变更
scale:扩展一个或多个被复制的服务
update:更新服务
6.4.5.1创建建服务
docker service create [OPTIONS] IMAGE [COMMAND] [ARG...]
options:
--config config:指定暴露给服务的配置;
--constraint list:应用实例在集群中被放置时的位置限制;
-d,detach:不等待创建后对应用进行状态探测即返回;
--dns list:自定义使用的DNS服务器地址;
--endpoint-mode string:指定外部访问的模式,包括vip(虚地址自动负载均衡)或dnsrr(DNS轮询);
--e,-env list:环境变量列表;
--health-cmd string:进行健康检查的指令;
--l,-label list:执行服务的标签;
--mode string:服务模式,包括replicated(默认)或global;
--replicas uint:指定实例的复制份数;
--secret secret:向服务暴露的秘密数据;
--u,-user string:指定用户信息,UID:[GID];
--w,-workdir string:指定容器中的工作目录位置。
-p, --publish:指定宿主机到容器的端口映射
docker service create --name nginx -p 8080:80 --replicas 2 nginx
这里我们创建一个nginx服务,服务名为nginx,端口映射为宿主机的8080端口映射到服务的80端口,
实例复制份数为2份。
6.4.5.2列出服务
docker service ls
这里服务已经起来了。
6.4.5.3查看服务详细信息
docker service inspect nginx
6.4.5.4列出任务
docker service ps nginx
有两个任务(容器),分别运行在swarm192和swarm191这两个节点上。
我们看下,nginx有没有正常启动。
这里我们发现,使用三外节点的IP都可以访问nginx.
即使任务并没有跑在swarm193上,用swarm193的地址,也可以正常访问,这里有点神奇,下面一节我们再来研究这个。
如果我们让一个节点上的任务(容器)停止一下,发现还是可以正常访问,因为我们这个任务(容器)有两个,可以帮到冗余的效果,且swarm还会做负载均衡,将负载分别分配到各个任务中。
6.4.5.5扩展服务
将服务的复制份数增加或减少。
如上例中我,我们创建nginx服务时,选择的复制份数是2份,但我们有三个节点,就可以将复制份数扩展到3份。
docker service scale nginx=3
已经扩展为3份了
6.4.5.6更新服务
docker service update [OPTIONS] SERVICE
Options:
--args command:服务的命令参数;
--config-add config:增加或更新一个服务的配置信息;
--config-rm list:删除一个配置文件;
--constraint-add list:增加或更新放置的限制条件;
--constraint-rm list:删除一个限制条件;
--d,-detach:执行后返回,不等待服务状态校验完整;
--dns-add list:增加或更新DNS服务信息;
--dns-rm list:删除DNS服务信息;
--endpoint-mode string:指定外部访问的模式,包括vip(虚地址自动负载均衡)或dnsrr(DNS轮询);
--entrypoint command:指定默认的入口命令;
--env-add list:添加或更新一组环境变量;
--env-rm list:删除环境变量;
--health-cmd string:进行健康检查的指令;
--label-add list:添加或更新一组标签信息;
--label-rm list:删除一组标签信息;
--no-healthcheck:不进行健康检查;
--publish-add port:添加或更新外部端口信息;
--publish-rm port:删除端口信息;
--q,-quiet:不显示进度信息;
--read-only:指定容器的文件系统为只读;
--replicas uint:指定服务实例的复制份数;
--rollback:回滚到上次配置;
--secret-add secret:添加或更新服务上的秘密数据;
--secret-rm list:删除服务上的秘密数据;
--update-parallelism uint:更新执行的并发数;
我们给nginx这个服务增加一个端口映射:
docker service update --publish-add 8081:80 nginx
这样,我们通过8081端口也可以访问nginx了。
6.4.6探索swarm的工作原理
上面我们只是从表面上看到了swarm的基本功能,接下来,我们深入探索一下swarm的工作原理。
6.4.6.1 观测外部访问swarm时的效果
先做一个小实验:
还是有三个节点,swarm191,swarm192,swarm193
创建一个nginx服务,复制份数为2。
docker service create --name nginx -p 8080:80 --replicas 2 nginx:1.0
可以看到,这个nginx服务运行在swarm191和swarm193上。
在三个节点上分别用docker ps这个命令查看一下容器:
发现在swarm191和swarm192上各生成了一个容器,而swarm193上没有。
现在我们先用docker exec –it <container name> bash这个命令进入 swarm191和swarm192上的容器,修改一下nginx的index.html文件。
swarm191上的容器里的/usr/share/nginx/html/index.html文件内容改为swarm191
swarm193上的容器里的/usr/share/nginx/html/index.html文件内容改为swarm193
改完以后,访问swarm191节点,显示的内容是swarm191,说明访问的是在节点swarm191上的那个容器。
访问swarm193节点,显示的内容是swarm193,说明访问的是在节点swarm193上的那个容器。
那么访问swarm192节点,会显示什么内容呢?它会访问哪个节点上的容器呢?
最开始,它会访问到swarm191,你尝试不断刷新网页后,它又会访问到swarm193。
现在,我们把191上的容器暂停掉,再来观测:
这里我们发现,它会跳转到swarm193去
从这个实验我可以得出以下结论:
- 当任务(容器)在本节点上,有外部访问时,会优先转发到本节点的容器上;
- 当任务(容器)不在本节点上,有外部访问时,会转发到其它节点上,并且会访问其它不同的节点,也就是可以实现负载均衡的功能。
- 当本节点上的容器故障,有外部访问时,会转发到其它节点上,也就是可以实现冗余的功能。
6.4.6.2理解网络命名空间
要想深入探索swarm的工作原理,我们首先需要理解网络命名空间。
在 Linux 中,网络名字空间可以被认为是隔离的拥有单独网络栈(网卡、路由转发表、iptables)的环境。网络名字空间经常用来隔离网络设备和服务,只有拥有同样网络名字空间的设备,才能看到彼此。
我们重新开一下虚拟来做网络命名空间的实验。
- 添加两个网络命名空间
ip netns add test0
ip netns add test1
- 查看网络命名空间
ip netns ls
这里我们可以看到刚刚创建的网络命名空间了。
- 创建一对veth,两个网络空间之间要通信的话,需要veth。把网络空间比喻成两个水池的话,veth就是连接水池的管道有了管道,两个水池的水才能相互流通
ip link add 第一个veth名称 type veth peer name 第二个veth名称
ip link add veth0 type veth peer name veth1
使用ip link show查看一下
已经生成了veth对
- 将两个虚拟网卡分别放到不同的命名空间中
ip link set veth0 netns test0
ip link set veth1 netns test0
- 为veth添加ip地址
ip netns exec test0 ip addr add 192.168.10.10/24 dev veth0
ip netns exec test1 ip addr add 192.168.11.11/24 dev veth1
- 启动两块网卡
ip netns exec test0 ip link set up dev veth0
ip netns exec test1 ip link set up dev veth1
- 查看两个命名空间的IP地址
ip netns exec test0 ip a
ip netns exec test1 ip a
注意红框的数字,他们交叉相等,说明这两个命空间已经成功建立了veth对,应该是可以互通的。
ip netns exec test0 ping 192.168.10.11
我们再建一个命名空间test3,并重新创建一块虚拟网卡,看会不会通
ip netns add test2
ip link add veth2 type veth
ip link set veth2 netns test2
ip netns exec test2 ip addr add 192.168.10.12/24 dev veth2
ip netns exec test2 ip link set up dev veth2
ip netns exec test2 ip a
发现,是ping不通的。
6.4.6.2swarm集群通讯过程探索
上面,我们只是看到了swarm集群工作的表面,接下来,我们来尝试探索一下swarm集群整个通讯的过程。
我们应用上一小节的知识,就可以很容易地搞清楚swarm的网络结构了。
首先,docker swarm会生成三个网络命名空间,具体位置在/run/docker/netns这个目录下面。
但是我们无法使用ip netns来管理这几个网络命名空间,但可以使用nsenter这个工具来管理。
nsenter --net=<filename> [command]
分别看一下三个网络命名空间、宿主机容器的IP。
6.4.7 创建服务栈
看一下容器的IP
发现容器的IP和e6058a169c2f是一样的,可以认定,容器实际上使用的是e6058a169c2f这个网络命名空间。
再看一下docker系统中的网络,docker network ls
这里docker_gwbridge和ingress这两个网络是swarm创建的。
我们先看一下ingress这个网络的详细信息
注意看这个ID,和这个/run/docker/netns/1-u48p3plmon网络命名空间太像了,我认定,这个ingress和/run/docker/netns/1-u48p3plmon,就是同一个命名空间。
至于docker里的docker_gwbridge这个网络,就是宿主机上的那个docker_gwbridge网络。
而且这个docker_gwbridge又桥接了两块虚拟网卡。
就是这两块,veth51和50.
根据我们上面的连线,51和50其实是连接到ingress-box这个命名空间的。
再看一下iptables
这里有一条指向172.18.0.2:8080的源地址NAT,也就是指向Ingress-box.
根据以上分析,我可以得出以下网络拓扑图:
结论:
经过以上的探索,我们可以猜想,swarm集群的拓扑应该如下图:
当外部访问到宿主机IP时,通过iptables DNAT,将数据包丢到ingress-box,再由ingress-box通过ingress网络选择丢到哪个容。
至于ingress-box如何选择哪个容器,这个不在我们本次的讨论范围之内。
6.4.7服务栈管理
6.3小节,我们讲过compose可用于服务栈的编排,但compose无法在swarm环境中部署,在swarm环境中部署栈,就需要使用docker stack。
docker stack [OPTIONS] COMMAND
Options:
--orchestrator指定集群类型,可选项:swarm|kubernetes|all
Command:
deploy:部署或更新服务栈
ls:列出已部署的服务栈
ps:列出服务栈中的任务(容器)
rm:删除服务栈
services:列出服务栈中的服务
6.4.7.1部署服务栈
docker stack deploy [OPTIONS] STACK
Options:
-c, --compose-file:指定一个compose文件
--orchestrator:指定编排格式,可选项:swarm|kubernetes|all
--prune:删除没有被引用的服务,默认为false
--resolve-image:查询注册表以解决图像摘要和支持的平台(“always”|“changed”|“never”),默认为 alwys.
--with-registry-auth:向Swarm代理发送注册表认证详细信息,默认为false
要部署服务栈,首先要创建一个compose.yml文件,这个文件和6.3小节中docker-compose中的yml文件差不多,但是不支持以下命令。
build #docker stack不支持创建镜像,所以必须先在节点上部署好镜像文件。
cgroup_parent
container_name
devices
tmpfs
external_links
links
network_mode
restart
security_opt
userns_mode
我们以6.3.5小节中,部署lnmp服务栈为例。
step 1:在集群的各个节点上部署好容器镜像
这两个镜像的创建过程,详见6.3.5小节
step 2:创建一个nfs共享存储,并挂载到群集的各个节点中,挂载的目录名和路径需要一致,用于存储数据(nginx数据和mysql数据)
- 新开一台linux主机,作为 nfs服务器,主机名设为 nfs195,ip地址设为192.168.0.195
- 在nff195上安装nfs服务
yum install nfs-utils
- 创建一个共享目录,将将共享目录的拥有者设为nfsnobody
mkdir lnmp_data
mkdir lnmp_data/html
mkdir lnmp_data/data
chown -R nfsnobody:nfsnobody lnmp_data/
- 配置并启动NFS
- vim /etc/exports
红框中的all_squash表示,所有连到nfs的用户,都被映射到nfs服务器的nfsnobody用户
- 重启rpcbind服务,并将rpcbind服务设置为开机自启动
systemctl restart rpcbind
systemctl enable rpcbind
- 启动NFS服务,并将NFS服务设置为开机自启动
systemctl start nfs-server
systemctl enable nfs-server
- 放通防火墙相关服务
firewall-cmd --add-service=rpc-bind
firewall-cmd --add-service=rpc-bind --permanent
firewall-cmd --add-service=nfs
firewall-cmd --add-service=nfs --permanent
firewall-cmd --add-service=mountd
firewall-cmd --add-service=mountd --permanent
- 到三个节点上查看NFS共享信息
看到这个信息,说明nfs服务已经启动成功。
- 在三个节点上创建用来挂载NFS的目录
mkdir /root/lnmp/lnmp_data
- 在三个节点上挂载NFS共享目
mount -t nfs 192.168.0.125:/root/lnmp_data /root/lnmp/lnmp_data
- 让内核重新生成挂载信息:
partprobe
- 查看挂载情况:
df -h
已经挂载成功
- 在三个节点上将NFS共享挂载写入分区配置文件:
vim /etc/fstab
确保三个节点都可以访问到nfs的共享目录
step 3:创建一个compose文件
version: '3.4'
services:
nginx: #第一个服务nginx
image: lnmp_nginx#定义创建服务所使用的镜像文件
labels:#打一个标签
- nginx
env_file:#使用一个环境变量文件
- ./myenv.env
depends_on:#该服务依赖于mysql这个服务
- mysql
ports:#将宿主机的8080端口映射到服务的80端口
- 8080:80
deploy:#定义容器复本数为2
replicas: 2
volumes:#将nginx数据目录挂载到宿主机
- /root/lnmp/lnmp_data/html:/usr/share/nginx/html
mysql:#第二个服务mysql
image: lnmp_mysql#定义创建服务所使用的镜像文件
labels:#打一个标签
- mysql
env_file:#使用一个环境变量文件
- ./myenv.env
ports:#将宿主机的3306端口映射到服务的3306端口
- 3306:3306
deploy:#定义容器复本数为1
replicas: 1
volumes:#将mysql数据目录映射到宿主机
- /root/lnmp/lnmp_data/mysql:/var/lib/mysql
step 4 创建一个环境变量文件
DATABASE_NAME=wordpress #定义wordpress使用的数据库名称
DATABASE_USER=wordpress#定义wordpress使用的数据库用户名
DATABASE_PASSWORD_ROOT=123qweasd#定义mysql的root密码
DATABASE_PASSWORD_USER=123qweasd#定义给wordpress使用的mysql用户名密码
DATABASE_HOST=mysql#定义给wordpress使用的数据库服务器地址或主机名
step 5 创建服务
docker stack deploy -c docker-stack.yml lnmp
这里的docker-stack.yml就是我们在step3中创建的那个compose文件。
这里我们看到,创建服务时做了三件事件上:
- 创建了一个名为lnmp_default的网络
- 创建了一个名为lnmp_nginx的服务
- 创建了一个名为lnmp_mysql的服务
然后我们看一下,我们的lnmp网站有没有起来:
网站已经起来了。
6.4.7.2列出服务栈
docker stack ls [OPTIONS]
options:
--format:指定输出的显示格式
--orchestrator:--orchestrator:指定编排格式,可选项:swarm|kubernetes|all
docker stack ls
这里已经列出了服务栈,里面有两个服务,编排格式为swarm.
6.4.7.3列出服务
docker stack services [OPTIONS] STACK
options:
-f, --filter: 根据提供的条件筛选输出
--format: 指定输出的显示格式
--orchestrator:--orchestrator:指定编排格式,可选项:swarm|kubernetes|all
-q, --quiet:安静模式,只显示id
这里列出了,lnmp这个服务栈有两人个服务,分别是lnmp_mysql和lnmp_nginx
6.4.7.4列出任务
docker stack ps [OPTIONS] STACK
options:
-f, --filter: 根据提供的条件筛选输出
--format: 指定输出的显示格式
--no-resolve: 不将id映射到名称
--no-trunc:不截断输出
--orchestrator:--orchestrator:指定编排格式,可选项:swarm| kubernetes|all
-q, --quiet:安静模式,只显示id
这里看到,服务栈中有三个任务(容器),一个mysql,在swarm191上,两个nginx分别在node swarm192和swarm193上。
6.4.7.5删除服务栈
docker stack rm [OPTIONS] STACK [STACK...]
options:
--orchestrator:--orchestrator:指定编排格式,可选项:swarm|kubernetes|all