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 worker22. 关闭防火墙或者开放端口
Docker Swarm 需要几个端口:
- 2377/tcp:集群管理通信
- 7946/tcp+udp:节点间通信
- 4789/udp:覆盖网络流量
生产环境建议还是开防火墙,只开放这些端口。测试环境图省事直接关了也行:
systemctl stop firewalld
systemctl disable firewalld3. 时间同步
这个特别重要!如果节点时间不同步,集群选举会出问题,服务调度也会乱套。我们用 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 lsID 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 lsID 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.145 个节点的集群就搭建完成了。
创建覆盖网络
集群搭好了,但服务之间怎么通信呢?这就需要创建覆盖网络(Overlay Network)。
覆盖网络允许不同节点上的容器互相通信,就像在同一个局域网里一样。
docker network create --driver overlay --attachable my-overlay--attachable 参数很重要,它允许独立容器连接到这个网络。如果不加这个参数,只有 Swarm 服务才能用这个网络。
查看网络:
docker network lsNETWORK 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 lsID NAME MODE REPLICAS IMAGE PORTS
k8z2m9p4n7q1 nginx-test replicated 3/3 nginx:latest *:80->80/tcpREPLICAS 显示 3/3,说明 3 个副本都正常运行。
查看服务详情:
docker service ps nginx-testID 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-testID 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 lsID 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.14manager1 显示 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.23. 创建用户中心服务
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 lsID 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 lsNAME 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-centerID 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 lsID 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=2Swarm 会自动选择要停止的容器,保证负载均衡。
私有镜像仓库配置
生产环境的镜像一般都存在私有仓库里,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.json3. 创建服务时使用 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-center2. 健康检查
给服务加上健康检查,Swarm 会定期检查容器状态,不健康就自动重启:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s3. 备份策略
定期备份 Manager 节点的 /var/lib/docker/swarm 目录,里面存着集群的状态数据。如果所有 Manager 都挂了,可以用备份恢复集群。
4. 监控告警
部署监控(Prometheus + Grafana),设置告警规则。集群状态、服务状态、资源使用情况都要监控。
5. 灰度发布
更新服务时,不要一次性全部更新。可以先更新 1 个副本,观察没问题后再继续更新。用 --update-parallelism 参数控制。
写在最后
Docker Swarm 虽然不如 Kubernetes 强大,但对中小团队来说,绝对够用了。搭建简单,维护成本低,功能也齐全。我们公司用了快一年,除了刚开始踩了几个坑,后面一直很稳定。
最重要的是,它真的解决了我们单点故障的问题。现在随便挂一台服务器,服务都能自动迁移到其他节点,再也不用半夜起来重启服务了。
如果你们团队也在考虑容器编排方案,不妨先试试 Docker Swarm。等业务规模大了,再考虑迁移到 Kubernetes 也不迟。
这篇文章写了好久,把搭建过程和踩过的坑都详细记录下来了。如果你也在用 Docker Swarm,或者准备搭建 Swarm 集群,希望这篇文章能帮到你。
有问题欢迎在评论区留言,我会尽量回复。如果觉得文章有用,点个赞再走呗,也欢迎转发给需要的朋友。
公众号:运维躬行录
个人博客:躬行笔记
关注公众号,获取更多运维干货文章,一起成长,一起进步!