Docker Swarm 搞定高可用集群,生产环境再也不怕服务挂掉了
Docker Swarm是什么?
Docker Swarm 是 Docker 官方推出的容器集群管理工具,基于 Go 语言实现。代码开源在:https://github.com/docker/swarm 使用它可以将多个 Docker 主机封装为单个大型的虚拟 Docker 主机,快速打造一套容器云平台。
Docker Swarm 是生产环境中运行 Docker 应用程序最简单的方法。作为容器集群管理器,Swarm 最大的优势之一就是 100% 支持标准的 Docker API。各种基于标准 API 的工具比如 Compose、docker-py、各种管理软件,甚至 Docker 本身等都可以很容易的与 Swarm 进行集成。大大方便了用户将原先基于单节点的系统移植到 Swarm 上,同时 Swarm 内置了对 Docker 网络插件的支持,用户可以很容易地部署跨主机的容器集群服务。
Docker Swarm 和 Docker Compose 一样,都是 Docker 官方容器编排工具,但不同的是,Docker Compose 是一个在单个服务器或主机上创建多个容器的工具,而 Docker Swarm 则可以在多个服务器或主机上创建容器集群服务,对于微服务的部署,显然 Docker Swarm 会更加适合。
为什么选 Docker Swarm 而不是 K8s
说实话,Kubernetes 确实强大,但对我们这种中小团队来说,K8s 的学习成本和维护成本都太高了。光是搭建一套高可用的 K8s 集群就得折腾好几天,更别说日常维护了。
Docker Swarm 就不一样了,它是 Docker 原生自带的编排工具,学过 Docker 的基本上一个小时就能上手。而且功能足够强大,服务发现、负载均衡、滚动更新、健康检查这些该有的都有,完全能满足大部分生产场景的需求。
客户现在的业务规模大概有 30 多个微服务,每天访问量在百万级别,用 Docker Swarm 完全够用。如果你的规模跟我差不多,真心建议别一上来就搞 K8s,Swarm 真的够用了。
环境准备
5 台服务器,配置如下:
- 3 台 Manager 节点:16G 内存,4 核 CPU
- 2 台 Worker 节点:8G 内存,2 核 CPU
为什么 Manager 要 3 台?这个后面会详细说。系统用的是 CentOS 7.9,Docker 版本是 20.10.14。
在开始之前,有几件事得先做好:
1. 主机名和 hosts 配置
每台机器都要设置不同的主机名,不然等会儿集群建起来都叫 localhost,看着就头疼。
# 在 manager1 上执行
hostnamectl set-hostname manager1
# 其他机器类似,改成 manager2、manager3、worker1、worker2
然后修改 /etc/hosts 文件,把所有节点的 IP 和主机名都写进去:
192.168.1.101 manager1
192.168.1.102 manager2
192.168.1.103 manager3
192.168.1.104 worker1
192.168.1.105 worker2
2. 关闭防火墙或者开放端口
Docker Swarm 需要几个端口:
- 2377/tcp:集群管理通信
- 7946/tcp+udp:节点间通信
- 4789/udp:覆盖网络流量
生产环境建议还是开防火墙,只开放这些端口。测试环境图省事直接关了也行:
systemctl stop firewalld
systemctl disable firewalld
3. 时间同步
这个特别重要!如果节点时间不同步,集群选举会出问题,服务调度也会乱套。我们用 chrony 做时间同步:
yum install -y chrony
systemctl start chronyd
systemctl enable chronyd
初始化 Swarm 集群
准备工作做完了,现在开始正式搭建集群。
在 manager1 上执行初始化命令:
docker swarm init --advertise-addr 192.168.1.101
执行完会输出一串命令,大概是这样的:
Swarm initialized: current node (dxn1zf6l61qsb1josjja83ngz) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-49nj1cmql0jkz5s954yi3oex3nedyz0fb0xx14ie39trti4wxv-8vxv8rssm7h7g6w7br5d7vwlh 192.168.1.101:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
这个 token 特别重要,一定要保存好,后面添加节点都要用它。
现在集群已经有一个 Manager 节点了,查看一下集群状态:
docker node ls
输出类似这样:
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
dxn1zf6l61qsb1josjja83ngz * manager1 Ready Active Leader 20.10.14
能看到 manager1 是 Leader 状态。
添加其他 Manager 节点
前面说了要用 3 个 Manager 节点,为什么要奇数个?这个跟 Raft 共识算法有关。
Docker Swarm 用 Raft 算法来管理集群状态,需要半数以上的 Manager 节点在线才能正常工作。如果你有 2 个 Manager,挂掉 1 个,剩下 1 个不满足"半数以上"的条件,集群就不可用了。但如果是 3 个 Manager,挂掉 1 个,剩下 2 个还能正常工作。
所以奇数个 Manager 是最佳实践,一般 3 个就够用了,能容忍 1 个节点故障。如果想要更高的容错能力,可以用 5 个,能容忍 2 个节点故障。[2]
在 manager1 上获取 Manager 加入令牌:
docker swarm join-token manager
会输出一个命令,复制到 manager2 和 manager3 上执行:
docker swarm join --token SWMTKN-1-49nj1cmql0jkz5s954yi3oex3nedyz0fb0xx14ie39trti4wxv-8vxv8rssm7h7g6w7br5d7vwlh 192.168.1.101:2377
执行完后再看集群状态:
docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
dxn1zf6l61qsb1josjja83ngz * manager1 Ready Active Leader 20.10.14
rfm5vi4vpxpmhjw6vz0t4wqdv manager2 Ready Active Reachable 20.10.14
y6jcg5z4m7x0tz1f0qz3n5k8s manager3 Ready Active Reachable 20.10.14
看到 3 个 Manager 都在线了,manager1 是 Leader,其他两个是 Reachable 状态。
添加 Worker 节点
Worker 节点只负责运行容器,不参与集群管理。在 manager1 上获取 Worker 加入令牌:
docker swarm join-token worker
复制输出的命令到 worker1 和 worker2 上执行:
docker swarm join --token SWMTKN-1-49nj1cmql0jkz5s954yi3oex3nedyz0fb0xx14ie39trti4wxv-8vxv8rssm7h7g6w7br5d7vwlh 192.168.1.101:2377
再看集群状态:
docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
dxn1zf6l61qsb1josjja83ngz * manager1 Ready Active Leader 20.10.14
rfm5vi4vpxpmhjw6vz0t4wqdv manager2 Ready Active Reachable 20.10.14
y6jcg5z4m7x0tz1f0qz3n5k8s manager3 Ready Active Reachable 20.10.14
n8xkp2z3m5q1rt7v9w2j4h6c0 worker1 Ready Active 20.10.14
t3h7m9k2p8q5wv1n4x6j0r2b8 worker2 Ready Active 20.10.14
5 个节点的集群就搭建完成了。
创建覆盖网络
集群搭好了,但服务之间怎么通信呢?这就需要创建覆盖网络(Overlay Network)。
覆盖网络允许不同节点上的容器互相通信,就像在同一个局域网里一样。
docker network create --driver overlay --attachable my-overlay
--attachable 参数很重要,它允许独立容器连接到这个网络。如果不加这个参数,只有 Swarm 服务才能用这个网络。
查看网络:
docker network ls
NETWORK ID NAME DRIVER SCOPE
n33wq0p5m6q7 ingress overlay swarm
r2t5v8x1p4m9 my-overlay overlay swarm
部署一个测试服务
光搭集群没意思,咱们部署一个实际的服务试试。
我这里用 Nginx 做演示,部署 3 个副本:
docker service create \
--name nginx-test \
--replicas 3 \
--network my-overlay \
--publish 80:80 \
nginx:latest
参数解释一下:
--name:服务名称--replicas:副本数量,这里启动 3 个容器--network:使用的网络--publish:端口映射,把容器的 80 端口映射到宿主机的 80 端口
查看服务状态:
docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
k8z2m9p4n7q1 nginx-test replicated 3/3 nginx:latest *:80->80/tcp
REPLICAS 显示 3/3,说明 3 个副本都正常运行。
查看服务详情:
docker service ps nginx-test
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
t3h7m9k2p8q5 nginx-test.1 nginx:latest worker1 Running Running 2 minutes ago
r2t5v8x1p4m9 nginx-test.2 nginx:latest manager2 Running Running 2 minutes ago
n8xkp2z3m5q1 nginx-test.3 nginx:latest worker2 Running Running 2 minutes ago
能看到 3 个容器分别运行在 worker1、manager2 和 worker2 上。Swarm 会自动把容器调度到不同的节点,保证负载均衡。
现在随便访问哪个节点的 80 端口,都能看到 Nginx 的欢迎页面。这就是 Swarm 的负载均衡功能,外部请求会自动分发到各个容器。
服务高可用测试
说了半天高可用,到底高可用在哪里?
我们来模拟一下节点故障。直接把 worker1 关机:
# 在 worker1 上执行
shutdown -h now
然后等个几十秒,再查看服务状态:
docker service ps nginx-test
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
t3h7m9k2p8q5 nginx-test.1 nginx:latest worker1 Shutdown Shutdown 30 seconds ago
k7v2m9p4n7q1 nginx-test.1 nginx:latest manager3 Running Running 10 seconds ago
r2t5v8x1p4m9 nginx-test.2 nginx:latest manager2 Running Running 5 minutes ago
n8xkp2z3m5q1 nginx-test.3 nginx:latest worker2 Running Running 5 minutes ago
看到了吗?原来在 worker1 上运行的容器自动关闭了,Swarm 在 manager3 上重新启动了一个新的容器。整个过程完全自动,不需要人工干预。
这就是高可用的核心:当某个节点挂掉,上面的服务会自动迁移到其他健康节点上运行。
再测试一下 Manager 节点故障。我们把 manager1(Leader)关机:
# 在 manager1 上执行
shutdown -h now
等一会儿,在 manager2 或 manager3 上查看集群状态:
docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
dxn1zf6l61qsb1josjja83ngz manager1 Down Active Unreachable 20.10.14
rfm5vi4vpxpmhjw6vz0t4wqdv * manager2 Ready Active Leader 20.10.14
y6jcg5z4m7x0tz1f0qz3n5k8s manager3 Ready Active Reachable 20.10.14
n8xkp2z3m5q1rt7v9w2j4h6c0 worker1 Ready Active 20.10.14
t3h7m9k2p8q5wv1n4x6j0r2b8 worker2 Ready Active 20.10.14
manager1 显示 Down 和 Unreachable,而 manager2 自动变成了 Leader。这就是 Raft 协议的功劳,剩下的 2 个 Manager 节点重新选举,manager2 成了新的 Leader。
集群依然正常工作,服务不受影响。
部署真实业务服务
测试服务跑通了,现在部署一个真实的业务。我们有个用户中心服务,用 Spring Boot 写的,依赖 MySQL 和 Redis。
1. 创建 MySQL 服务
docker service create \
--name mysql \
--replicas 1 \
--network my-overlay \
--mount type=volume,source=mysql-data,target=/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=your_password \
-e MYSQL_DATABASE=user_center \
--constraint 'node.role==worker' \
mysql:5.7
这里用了 --constraint 'node.role==worker',强制 MySQL 只运行在 Worker 节点上。因为 Manager 节点已经够忙了,数据库这种重服务还是放 Worker 上比较好。
--mount 参数挂载了数据卷,保证数据持久化。如果不挂载卷,容器重建后数据就丢了。
2. 创建 Redis 服务
docker service create \
--name redis \
--replicas 1 \
--network my-overlay \
--mount type=volume,source=redis-data,target=/data \
--constraint 'node.role==worker' \
redis:6.2
3. 创建用户中心服务
docker service create \
--name user-center \
--replicas 2 \
--network my-overlay \
-e SPRING_PROFILES_ACTIVE=prod \
-e MYSQL_HOST=mysql \
-e REDIS_HOST=redis \
--publish 8080:8080 \
your-registry.com/user-center:latest
注意这里的环境变量 MYSQL_HOST=mysql 和 REDIS_HOST=redis,直接用服务名就能访问,这就是服务发现的好处。Swarm 内置了 DNS 服务器,会自动把服务名解析到对应的容器 IP。
服务部署完了,看看整体状态:
docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
k7v2m9p4n7q1 mysql replicated 1/1 mysql:5.7
r2t5v8x1p4m9 redis replicated 1/1 redis:6.2
t3h7m9k2p8q5 user-center replicated 2/2 your-registry.com/user-center:latest *:8080->8080/tcp
所有服务都正常运行。
使用 Docker Compose 文件部署
上面的命令行方式适合单个服务,如果服务多了,管理起来很麻烦。Docker Swarm 支持用 Docker Compose 文件部署,方便多了。
创建一个 docker-compose.yml 文件:
version: '3.8'
services:
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: your_password
MYSQL_DATABASE: user_center
volumes:
- mysql-data:/var/lib/mysql
networks:
- my-overlay
deploy:
replicas: 1
placement:
constraints:
- node.role == worker
redis:
image: redis:6.2
volumes:
- redis-data:/data
networks:
- my-overlay
deploy:
replicas: 1
placement:
constraints:
- node.role == worker
user-center:
image: your-registry.com/user-center:latest
environment:
SPRING_PROFILES_ACTIVE: prod
MYSQL_HOST: mysql
REDIS_HOST: redis
ports:
- "8080:8080"
networks:
- my-overlay
deploy:
replicas: 2
update_config:
parallelism: 1
delay: 10s
failure_action: rollback
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
networks:
my-overlay:
external: true
volumes:
mysql-data:
redis-data:
这个文件里加了几个重要的配置:
deploy.update_config:滚动更新配置,一次更新 1 个容器,间隔 10 秒,失败自动回滚deploy.restart_policy:重启策略,失败后重启,最多重试 3 次
部署服务:
docker stack deploy -c docker-compose.yml myapp
查看 stack:
docker stack ls
NAME SERVICES
myapp 3
查看 stack 里的服务:
docker stack services myapp
用 stack 的好处是一次性部署多个服务,而且更新的时候也很方便,改完配置文件重新执行 deploy 命令就行。
滚动更新实战
业务迭代是常事,更新服务是家常便饭。传统方式更新服务,得先停掉旧版本,再启动新版本,中间会有服务中断。
Docker Swarm 的滚动更新就优雅多了,它会逐步用新版本替换旧版本,保证服务始终可用。
假设我们要把 user-center 服务从 v1.0 更新到 v2.0:
docker service update \
--image your-registry.com/user-center:v2.0 \
--update-parallelism 1 \
--update-delay 10s \
--update-failure-action rollback \
myapp_user-center
参数说明:
--update-parallelism 1:一次更新 1 个容器--update-delay 10s:更新间隔 10 秒--update-failure-action rollback:更新失败自动回滚
更新过程中查看状态:
docker service ps myapp_user-center
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
k7v2m9p4n7q1 myapp_user-center.1 your-registry.com/user-center:v2.0 worker1 Running Running 5 seconds ago
t3h7m9k2p8q5 \_ myapp_user-center.1 your-registry.com/user-center:v1.0 worker1 Shutdown Shutdown 6 seconds ago
r2t5v8x1p4m9 myapp_user-center.2 your-registry.com/user-center:v1.0 worker2 Running Running 2 minutes ago
能看到 worker1 上的容器已经更新到 v2.0,worker2 上的还是 v1.0。等 10 秒后,worker2 上的也会更新。
如果新版本有问题,比如容器启动失败,Swarm 会自动停止更新并回滚到旧版本。这个功能太实用了,再也不用担心更新把服务搞挂了。
服务扩缩容
业务高峰期,服务扛不住了,需要快速扩容。Docker Swarm 扩容只需要一条命令:
docker service scale myapp_user-center=5
瞬间就把 user-center 服务扩展到 5 个副本。Swarm 会自动把新容器调度到空闲的节点上。
查看扩容结果:
docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
t3h7m9k2p8q5 myapp_user-center replicated 5/5 your-registry.com/user-center:v2.0 *:8080->8080/tcp
缩容也一样简单:
docker service scale myapp_user-center=2
Swarm 会自动选择要停止的容器,保证负载均衡。
私有镜像仓库配置
生产环境的镜像一般都存在私有仓库里,Docker Swarm 拉取私有镜像需要认证。
假设我们用 Harbor 搭建了私有仓库,地址是 registry.example.com。
1. 创建 Docker 登录认证
在任意 Manager 节点登录私有仓库:
docker login registry.example.com
输入用户名和密码,登录成功后,认证信息会保存在 ~/.docker/config.json 文件里。
2. 创建 Docker Secret
把认证信息创建成 Swarm Secret:
docker secret create registry-auth ~/.docker/config.json
3. 创建服务时使用 Secret
docker service create \
--name user-center \
--replicas 2 \
--network my-overlay \
--secret source=registry-auth,target=/root/.docker/config.json \
--publish 8080:8080 \
registry.example.com/user-center:latest
这样服务就能从私有仓库拉取镜像了。
监控和日志
集群跑起来了,监控不能少。我们用 Portainer 来管理 Swarm 集群,用 ELK 收集日志。
部署 Portainer
Portainer 是个轻量级的容器管理界面,支持 Docker Swarm。
docker service create \
--name portainer \
--replicas 1 \
--publish 9000:9000 \
--mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
--mount type=volume,src=portainer-data,dst=/data \
portainer/portainer-ce:latest
访问任意节点的 9000 端口,就能看到 Portainer 的管理界面。可以查看所有服务、容器、网络、卷,还能在 Web 界面直接操作,非常方便。[5]
日志收集
容器的日志默认存在本地,不方便查看和分析。我们用 ELK(Elasticsearch + Logstash + Kibana)来收集日志。
不过 ELK 部署比较复杂,这里就不展开了。简单点的方案是用 Filebeat 收集日志,发送到 Elasticsearch,然后用 Kibana 查看。
踩过的坑
搭建过程中遇到不少坑,记录下来给大家避雷。
坑 1:Manager 节点不要跑太多服务
刚开始我把所有服务都往 Manager 节点上放,结果 Manager 负载太高,集群管理都受影响。后来加了 --constraint 'node.role==worker',把重服务都限制在 Worker 节点上,问题解决。
坑 2:数据卷挂载要小心
MySQL 这种有状态的服务,一定要挂载数据卷,不然容器重建后数据就丢了。而且挂载的时候要用 type=volume,不要用 type=bind,因为 bind 挂载依赖宿主机的目录结构,迁移起来很麻烦。
坑 3:端口冲突
多个服务不能用同一个宿主机端口。我有次部署两个服务都用 8080 端口,结果第二个服务启动失败。解决办法是每个服务用不同的端口,或者用 Ingress 路由。[4]
坑 4:节点时间不同步
前面说过,节点时间不同步会导致集群选举出问题。我有次重启服务器后,时间同步服务没起来,结果集群状态一直不正常。后来加了监控,定期检查时间同步状态。
坑 5:网络问题
Docker Swarm 用覆盖网络实现跨节点通信,如果网络配置有问题,容器之间会无法通信。有次我忘了开放 4789/UDP 端口,结果容器之间 ping 不通。排查了半天才发现是防火墙的问题。
生产环境的一些建议
在测试环境跑通了,上生产环境还得注意几点:
1. 资源限制
给每个服务设置资源限制,防止某个服务占用太多资源影响其他服务:
docker service update \
--limit-cpu 2 \
--limit-memory 4G \
--reserve-cpu 1 \
--reserve-memory 2G \
myapp_user-center
2. 健康检查
给服务加上健康检查,Swarm 会定期检查容器状态,不健康就自动重启:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
3. 备份策略
定期备份 Manager 节点的 /var/lib/docker/swarm 目录,里面存着集群的状态数据。如果所有 Manager 都挂了,可以用备份恢复集群。
4. 监控告警
部署监控(Prometheus + Grafana),设置告警规则。集群状态、服务状态、资源使用情况都要监控。
5. 灰度发布
更新服务时,不要一次性全部更新。可以先更新 1 个副本,观察没问题后再继续更新。用 --update-parallelism 参数控制。
写在最后
Docker Swarm 虽然不如 Kubernetes 强大,但对中小团队来说,绝对够用了。搭建简单,维护成本低,功能也齐全。我们公司用了快一年,除了刚开始踩了几个坑,后面一直很稳定。
最重要的是,它真的解决了我们单点故障的问题。现在随便挂一台服务器,服务都能自动迁移到其他节点,再也不用半夜起来重启服务了。
如果你们团队也在考虑容器编排方案,不妨先试试 Docker Swarm。等业务规模大了,再考虑迁移到 Kubernetes 也不迟。
这篇文章写了好久,把搭建过程和踩过的坑都详细记录下来了。如果你也在用 Docker Swarm,或者准备搭建 Swarm 集群,希望这篇文章能帮到你。
有问题欢迎在评论区留言,我会尽量回复。如果觉得文章有用,点个赞再走呗,也欢迎转发给需要的朋友。
公众号:运维躬行录
个人博客:躬行笔记
关注公众号,获取更多运维干货文章,一起成长,一起进步!
更多推荐



所有评论(0)