Docker Swarm 零基础入门

4,433 阅读8分钟

Docker Swarm 是 Docker 官方项目之一,提供 Docker 容器集群服务,是 Docker 官方对容器云生态进行支持的核心方案。使用它,用户可以将多个 Docker 主机封装为单个大型的虚拟 Docker 主机,快速打造一套容器云平台。

Docker 1.12 Swarm mode 已经内嵌入 Docker 引擎,成为了 docker 子命令 docker swarm

Swarm mode 内置 kv 存储功能,提供了众多的新特性,比如:具有容错能力的去中心化设计、内置服务发现、负载均衡、路由网格、动态伸缩、滚动更新、安全传输等。

概念

docker 中和 swarm 相关的命令有:

  • docker swarm
  • dokcer node
  • docker service
  • docker stack
  • docker secret

Swarm 中一台主机就是一个节点(node),节点分为管理( manager)和工作(worker)节点。

管理节点用于 Swarm 集群的管理。一个 Swarm 集群可以有多个管理节点,但只有一个管理节点可以成为 leaderleader 通过 raft 协议实现。

管理节点有内置 Raft 数据库,它们用来存放配置等数据。而且它们之间的通信都是加密的。

工作节点是任务执行节点,管理节点将服务 (service) 下发至工作节点执行。管理节点默认也作为工作节点。也可以通过让服务只运行在管理节点上,管理节点和工作节点只是它们的权限不同,工作节点就没有管理节点那么多权限,比如在工作节点上不能查看集群中的容器。

有了 swarm 我们就不用自己一个个的创建容器了,比如我们有 3 个主机,我们告诉 swarm 我们要创建 5 个 nginx 容器,swarm 会自己帮我们部署到不同主机上,比如那个主机部署一个那个部署两个。

启用 Swarm

$ docker swarm init
# 我们执行这个命令,来启用 swarm,我们当前主机作为管理节点
# 它还创建了 swarm 根证书,还创建了 join token,用来让其他节点加入这个 Swarm
# 还创建了 raft 数据库。
Swarm initialized: current node (86pi89uue3q0eqiqpomna3ejy) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-5aco0o1iqxbgjludw6m6rhl9qug60ylm8lj938hnyfy2cyt8d7-7jq47t76cohvtah61sab7rwcf 192.168.65.3:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

我们可以直接复制上面那条输出的命令,让其他节点作为 worker 加入这个 swarm

如我我们想让加入的节点作为 manager 加入这个 swarm 可以执行:

$ docker swarm join-token manager
To add a manager to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-5aco0o1iqxbgjludw6m6rhl9qug60ylm8lj938hnyfy2cyt8d7-ecmkdz7x0eswu3uyn0vij72dy 192.168.65.3:2377
$ docker node ls
# docker node 命令可以用来管理我们的节点,ls 表示列出集群所有节点
$ docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE               PORTS

# docker service 命令用来取代 docker run 命令
# 因为在 swarm 中我们不关心容器的个个配置信息,而且也不会去其他节点自己手动创建容器
# 我们只需要抛出一个 任务(task) 由 Swarm 来协调

任务 (Task)是 Swarm 中的最小的调度单位,目前来说就是一个单一的容器。

服务 (Services) 是指一组任务的集合,服务定义了任务的属性。服务有两种模式:

  • replicated services 按照一定规则在各个工作节点上运行指定个数的任务。
  • global services 强制在每个 node 上都运行一个且最多一个容器。
$ docker service create alpine ping baidu.com
# 创建一个容器,让它来 ping 百度
$ docker service ls
ID                  NAME                  MODE                REPLICAS            IMAGE               PORTS
yjoejehnu4yx        nostalgic_zhukovsky   replicated          1/1                 alpine:latest
# 我们发现 MODE 是 replicated(默认)
# REPLICAS 是 1/1 右边是要运行,左边是实际运行的数量
$ docker service ps nostalgic_zhukovsky
ID                  NAME                    IMAGE               NODE                    DESIRED STATE       CURRENT STATE           ERROR               PORTS
snpjphe14ztv        nostalgic_zhukovsky.1   alpine:latest       linuxkit-00155d01020f   Running             Running 4 minutes ago
# 查看这个 service 的任务(容器)
$ docker service update --replicas 3 nostalgic_zhukovsky
# docker service update 用来更新一个 service
# 这里让它运行 3 个任务
$ docker service ls
ID                  NAME                  MODE                REPLICAS            IMAGE               PORTS
yjoejehnu4yx        nostalgic_zhukovsky   replicated          3/3                 alpine:latest
$ docker service scale -d nostalgic_zhukovsky=10
# 我们还可以通过 scale 来扩大缩小我们的服务
$ docker service ls
ID                  NAME                  MODE                REPLICAS            IMAGE               PORTS
yjoejehnu4yx        nostalgic_zhukovsky   replicated          7/10                alpine:latest
$ docker service scale nostalgic_zhukovsky=2
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS
584829ddf89f        alpine:latest       "ping baidu.com"    56 seconds ago      Up 49 seconds
8678f634f14b        alpine:latest       "ping baidu.com"    18 minutes ago      Up 18 minutes
$ docker rm -f 8678f634f14b
$ docker service ps yjoejehnu4yx
ID                  NAME                        IMAGE               NODE                    DESIRED STATE       CURRENT STATE            ERROR
snpjphe14ztv        nostalgic_zhukovsky.1       alpine:latest       linuxkit-00155d01020f   Running             Running 17 minutes ago
vk0ih9ds1kdm        nostalgic_zhukovsky.2       alpine:latest       linuxkit-00155d01020f   Running             Running 9 seconds ago
m4ylibww9l87         \_ nostalgic_zhukovsky.2   alpine:latest       linuxkit-00155d01020f   Shutdown            Failed 15 seconds ago    "task: non-zero exit (137)"
# 我们发现我们强制关闭一个容器,swarm 自动帮我们新建了一个。

不像我们自己启动容器,使用 docker swarm 我们只要说出自己任务就可以了,swarm 会自己有没有完成任务,比上面发现要运行两个,却发现一个被我们删了,它就会再创建一个容器。

多节点

创建 Swarm 集群,我们要创建多个 node,就需要多台主机或虚拟机。

我们可以去 play with docker 网站来创建多个节点,这个网站是免费的,但是 4 个小时后就会销毁你创建的所有虚拟机。

我们还可以使用 docker machine 帮我们快速的在本地创建多台 docker 虚拟机。

再或者去云服务商那里买几台云主机,使用 这个脚本 快速安装 docker。

docker machine

Docker Machine 是 Docker 官方编排(Orchestration)项目之一,负责在多种平台上快速安装 Docker 环境。它使用 Go 语言实现。

对于 windos 和 mac 安装 docker 的时候就自带了 docker machine。

对于 linux 可以使用如下命令安装。

$ sudo curl -L https://github.com/docker/machine/releases/download/v0.13.0/docker-machine-`uname -s`-`uname -m` > /usr/local/bin/docker-machine
$ sudo chmod +x /usr/local/bin/docker-machine
$ docker-machine create \
    --engine-registry-mirror https://dockerhub.azk8s.cn \
    -d virtualbox \
    node1
# 创建一个驱动是 virtualbox 类型的 docker 主机
# --engine-registry-mirror 用来指定镜像加速器
# 驱动除了是 virtualbox 还可以是 macOS 的 xhyve 驱动
# 它比 virtualbox 运行效率要高

如果是在 Windows 10 上执行上面那个命令就会报错,因为 Windows 10 安装 Docker for Windows 之后不能再安装 VirtualBox,也就不能使用 virtualbox 驱动来创建 Docker Machine。

我们可以使用 hyperv 驱动,而且必须事先在 Hyper-V 管理器中新建一个 外部虚拟交换机 执行下面的命令时,使用 --hyperv-virtual-switch=MY_SWITCH 指定虚拟交换机名称。

$ docker-machine create \
    --engine-registry-mirror https://dockerhub.azk8s.cn \
    --hyperv-virtual-switch PVS \
    -d hyperv \
    node1
# 然后我们以管理员身份打开终端执行这条命令
# --hyperv-virtual-switch 后面是刚刚取的虚拟交换机名称
# 我们重复上面那个命令,分别创建 node1 node2 node3 3个节点

创建好 3 个 docker 主机后,我们登录上去创建 swarm 集群。

$ docker-machine ssh node1
# 登录到 node1
$ docker swarm init
# docker swarm join --token SWMTKN-1-1ve56x9y7784s0enqvhghnls4rbjazwxovz5me1cmqub9jlgwa-83msyrvzwpgrk46fsifmht8nj 192.168.1.168:2377

但后去 node2 和 node3 执行上面输出的那条命令,然后再登录到 node1。

$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
2y3ih8gtvit225155x5u33690 *   node1               Ready               Active              Leader              18.09.7
q0ag3cuma8sqiszjlou2ttc3t     node2               Ready               Active                                  18.09.7
cx4yap7apqnii58xmmxs8mdo7     node3               Ready               Active                                  18.09.7

可以看到这个 Swarm 中已经有 3 个节点了,其中 node1 是管理节点,剩下两个是工作节点。

worker 节点没有管理集群,为了方便我们可以复制 manager 的 token 让另外两个节点也成为 manager。

或者在 node1 上执行:

$ docker node update --role manager node2
$ docker node update --role manager node3

网络

在使用多节点之前,我们先在本地执行一下,如下命令:

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
rkfr0w8kpqvi        ingress             overlay             swarm

我们可以看到一个驱动类型是 overlay 类型的网络,那是刚才 swarm 帮我们创建的。

要使用服务发现,需要相互通信的 service 必须属于同一个 overlay 网络。自动创建的 ingress 没有提供服务发现,必须创建自己的 overlay 网络。

它是 Swarm 范围桥接网络,容器可以跨主机互相访问,就像它们在一台主机上一样。通过 overlay 网络,主机与容器、容器与容器之间可以相互访问。

它只在一个 Swarm 内部,这样就不会搞乱主机网络配置。

使用

现在我们使用 Swarm 来创建一个 drupal 网站(Drupal 是自由开源内容管理系统,用PHP语言写成)。

登录到 node1 首先创建一个自己 overlay 网络

$ docker network create -d overlay drupal

然后创建一个 postgres service,用来存储数据。

$ docker service create --name psql --network drupal -e POSTGRES_PASSWORD=mypass postgres
# --network 加入到我们刚刚创建的那个网络
# -e 添加环境变量,用来指定 postgres 密码

然后就是 drupal service。

$ docker service create --name drupal --network drupal -p 80:80 drupal
$ docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
4c1rgorpz5hp        drupal              replicated          1/1                 drupal:latest       *:80->80/tcp
wyd2y3roqwh3        psql                replicated          1/1                 postgres:latest
$ docker service ps psql
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
v05c9pi98i4a        psql.1              postgres:latest     node2               Running             Running 10 minutes ago
$ docker service ps drupal
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE           ERROR               PORTS
qz4443imlexv        drupal.1            drupal:latest       node3               Running             Running 6 minutes ago
# 可以看到 postgres 运行在 node2,drupal 运行在 node3

我们打开浏览器,输入上面 node1 的 ip 地址。可以使用 docker-machine lsdocker-machine ip node1 查看。

打开过后就可以看到 drupal 页面,快速填写一下配置。

我们可以输入一下 node2 和 node3 的 ip 到浏览器。

我们发现同样可以访问到 drupal 网站,虽然它只运行在 node2 上。

routing mesh

swarm 向外暴露端口,所有节点都参与进入 routing mesh 中。每个节点都能接受暴露端口连接,即使 node 中没有运行这个 service。routing mesh 会将请求路由到运行这个 service 上活跃的容器中,它在所有节点上做负载均衡。

当内部容器对容器通信时,它们使用虚拟 IP (VIP)通信,它是 Swarm 虚拟网络中的私有 IP,它会让请求分布到所有服务任务中,比如我们有 10 个 worker 容器,我们无需做负载均衡,swarm 已经帮你做了。

当外部流量访问监听的端口时,所有 node 都会监听该端口流量,然后它会将该流量负载均衡的路由到合适的容器。如果是不同 node 那么就会通过虚拟网络路由到容器,如果是同一个 node 上,那么就会直接路由到容器的端口。

可以看到每个节点中都会有一个 load balancer。swarm 的负载均衡会把你的请求路由到一个任意节点的可用的容器上。routing mesh 在 swarm 节点的所有 IP 上监听 published 端口。

如果在一个服务器端口上运行一个网站,我们就会失去 swarm 的负载均衡,因为它是 TCP 层面的负载均衡而不是 DNS 层面的。这时候我们就需要一个外部负载均衡,可以使用 nginx 或 HAProxy。

滚动升级

滚动升级是一次只升级一部分副本,不一次性全部升级,它降低了应用更新的风险,如果某个副本更新失败,整个更新将暂停,其他副本则可以继续提供服务。在更新的过程中,总是有副本在运行的,也保证了业务的连续性。

现在我们把 nginx:1.16 版本升级到 nginx:1.17

$ docker service create --name web --replicas=3 nginx:1.16
# --replicas=3 启用 3 个副本
$ docker service update --image nginx:1.17 web
# 升级到 1.17
# swarm 会停止一个容器,更新它,如果失败就会暂停整个更新过程
$ docker service ps web
ID                  NAME                IMAGE               NODE                    DESIRED STATE       CURRENT STATE                 ERROR
           PORTS
nqch5ivpmmcd        web.1               nginx:1.17          linuxkit-00155d010229   Running             Running 57 seconds ago

uclfd0iev0sk         \_ web.1           nginx:1.16          linuxkit-00155d010229   Shutdown            Shutdown 58 seconds ago

ku2aomr1br3d        web.2               nginx:1.17          linuxkit-00155d010229   Running             Running about a minute ago

fpqp9mdfez9r         \_ web.2           nginx:1.16          linuxkit-00155d010229   Shutdown            Shutdown about a minute ago

ww6hxdsutlog        web.3               nginx:1.17          linuxkit-00155d010229   Running             Running about a minute ago

hvy2g68j9ah4         \_ web.3           nginx:1.16          linuxkit-00155d010229   Shutdown            Shutdown about a minute ago

Swarm 还有回滚功能,可以通过 --rollback 快速恢复到更新之前的状态。

$ docker service update --rollback web
# 也可以写成 docker serivice rollback web

--rollback 只能回滚到上一次执行 docker service update 之前的状态,并不能无限制地回滚。

控制 Service 运行的节点

Swarm 会自动帮我们把 Service 分配到合适的 node 上,性能高的 node 分配的 Service 就越多。除了 Swarm 自动分配,我们也可以手动分配。

我们可以给 node 设置 label,然后让 Service 运行在指定 label 的 node 上。

$ docker node update --label-add key=value node1
$ docker node inspect node1 --pretty
...
Labels:
    - key = value
...

然后创建 Service

$ docker service create \
      --constraint node.labels.key==value \
      --replicas 3 \
      --name web \
      --p 80:80 \
      nginx
# --constraint 限制将 service 部署到指定的 node
$ docker service inspect web --pretty
...
Placement:Contraints: [node.labels.key==value]
...

我们还可以更新 Service constraint,让它更换 node。

$ docker service update --constraint-rm node.labels.env==test web 
$ docker service update --constraint-add node.labels.env==prod web

如果我们想 swarm 强制更新可以使用 --force

$ docker service update --force web

Secret

我们经常要向容器传递敏感信息,比如上面传递的 POSTGRES_PASSWORD=mypass 环境变量。密码是以明文的形式写在命令中,潜在的巨大的安全风险。

我们可以通过 Secret 安全地管理 Swarm 集群中密码、密钥证书等敏感数据,并允许在多个 Docker 容器实例之间共享访问指定的敏感数据。它最大支持 500KB 的字符串或二进制内容。

Secret 会被加密的保存在管理节点的硬盘上,被加密传输。只有被允许的容器才能查看 Secret,在容器中它只会被存在内存中,可以在 /run/secrets/<secret_name | secret_alias> 访问到。

Secret 可以通过两种方式创建,一种是文件另一种是 stdin 创建。

$ docker secret create psql_user psql_user.txt
$ echo 'mypass' | docker secret create psql_pass -
$ docker service create --name psql --secret psql_user --secret psql_pass \
    -e POSTGRES_PASSWORD_FILE=/run/secrets/psql_pass \
    -e POSTGRES_USER_FILE=/run/secrets/psql_user postgres

--secret 用来指定 Service 能使用那个 secret

如果我们现在删除 Service 的 secret,可以使用 --secret-rm

$ docker service update --secret-rm psql_pass psql
# 删除 Service 的 secret。
# 容器会自动重建,因为 Service 是容器的一部分。

管理配置文件

Swarm 除了可以帮我们管理敏感数据,还可以帮我们管理配置文件。configsecret 命令的使用方法完全一致。

$ docker config create redis.conf redis.conf
$ docker service create \
     --name redis \
     # --config source=redis.conf,target=/etc/redis.conf \
     --config redis.conf \
     -p 6379:6380 \
     redis:latest \
     redis-server /redis.conf

Stack

我们每次启动一个 service 的时候都要写一堆命令行命令非常的笨拙冗长。

我们可以用 Stack 来简化这个操作。Stack 是 Swarm 调用的抽象,和 docker-compose 一样它也接收 docker compose 文件,用来定义 Services, Networks 和 Volumes 等。

我们使用 docker stack deploy 而不是 docker service create,Stack 会帮我们管理这些对象。

我们可以直接用 docker-compose.yml 文件,但是不能 build 镜像,Swarm 只接收构建好的镜像,新加了一个 deploy 字段。当使用 docker-compose 执行这个文件时,会忽略 deploy 字段。docker stack 中会忽略 build 字段,所以我们可以开发和发布都使用一个 docker-compose.yml 文件。

version: '3.7'

services:
  db:
    image: mysql:5.7
    networks:
      - wordpress
    volumes:
      - db-data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD_FILE: /run/secrets/db_password
    secrets: # 指定 secret
      - db_root_password
      - db_password
    deploy: # 部署
      update_config: # 更新规则
        parallelism: 2 # 一次两个
        delay: 10s # 更新延迟 10s,给 app 一个启动时间
      restart_policy: # 重启规则
        condition: on-failure
      placement:
        constraints: [node.role == manager] # 只部署在 manager 节点

  wordpress:
    depends_on:
      - db
    image: wordpress
    networks:
      - wordpress
    ports:
      - '80:80'
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    deploy:
      replicas: 2
      labels: [APP=WORDPRESS] # 添加自定义 label
      restart_policy:
        condition: on-failure
        delay: 10s
        max_attempts: 3

  visualizer: # 可视化页面,打开浏览器 8080 端口可以看见效果
    image: dockersamples/visualizer:stable
    networks: 
      - wordpress
    ports:
      - "8080:8080"
    stop_grace_period: 1m30s
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
    deploy:
      placement:
        constraints: [node.role == manager]
volumes:
  db-data:

networks:
  wordpress:

secrets: 
  db_password:
    file: db_password.txt
  db_root_password:
    file: db_root_password.txt

然后我们就可以部署这个 stack

$ docker stack deploy -c docker_compose.yml wordpress
# -c 指定配置文件
# deploy 也可以换成 up
$ docker stack ls
NAME                SERVICES            ORCHESTRATOR
wordpress           2                   Swarm
$ docker stack ps wordpress
ID                  NAME                    IMAGE               NODE                    DESIRED STATE       CURRENT STATE           
qk67so5btzln        wordpress_wordpress.1   wordpress:latest    linuxkit-00155d01022b   Running             Running 2 minutes ago
vmnk1gb7zl05        wordpress_db.1          mysql:latest        linuxkit-00155d01022b   Running             Running 2 minutes ago
s5x09e5q8fr9        wordpress_wordpress.2   wordpress:latest    linuxkit-00155d01022b   Running             Running 2 minutes ago
$ docker stack services wordpress
ID                  NAME                  MODE                REPLICAS            IMAGE               PORTS
kuro9uavayjq        wordpress_wordpress   replicated          2/2                 wordpress:latest    *:80->80/tcp
l667za415j6s        wordpress_db          replicated          1/1                 mysql:latest
$ docker network ls
NETWORK ID          NAME                  DRIVER              SCOPE
xnunueoy9t5m        wordpress_wordpress   overlay             swarm
$ docker secret ls
ID                          NAME                         DRIVER              CREATED             UPDATED
6wxq7esrrikmiq9rnhnl1uuzk   wordpress_db_password                            5 minutes ago       6 minutes ago
scvzglyfk4cjxfco1nytoajjz   wordpress_db_root_password                       5 minutes ago       6 minutes ago

通过上面可以看到 stack 自动帮我们创建 secret, network 等,我们只需要一个命令就可以了,如果我们更新这个配置文件,我们可以再执行下面这个命令更新。

$ docker stack deploy -c docker-compose.yml wordpress

如果要停止这个 stack,可以执行 rm 命令

$ docker stack rm wordpress

多配置文件

我们可以只是用一个 yaml 文件完成本地和生产环境开发,但是当我们的应用变得复杂的时候,一个配置文件可能没那么好用,这时候我们就可以使用多配置文件。

比如我们可以新建如下 yaml 文件

docker-compose.yml
docker-compose.override.yml
docker-compose.test.yml
docker-compose.prod.yml

docker-compose.yml 作为其他配置文件的基础,它会合并到其他配置文件上。

docker-compose.override.yml 当执行 docker-compose up 的时候 docker-compose 会自动将 docker-compose.yml 和名为 docker-compose.override.yml 合并成为一个文件执行。

docker-compose.test.yml 用于 CI 环境,我们可以执行

$ docker-compose -f docker-compose.yml -f docker-compose.test.yml up -d
# 基本文件在前面

docker-compose.prod.yml 生产环境文件,我们可以使用 docker-compose 将它和 docker-compose.yml 合并成一个文件再交给 docker stack

$ docker-compose -f docker-compose.yml -f docker-compose.prod.yml config > out.yml

Docker 零基础入门

Docker Compose 零基础入门

Kubernetes 零基础入门