先说清楚:什么时候单机不够用了
前面十几篇里,我们的服务都跑在一台机器上:一份 Compose 文件、一台服务器、一个 docker compose up -d 搞定。这套打法简单、省心,但它有两个天花板,撞上任何一个,单机就真的不够用了。
第一个是可用性。单机意味着单点:服务器宕机、Docker 守护进程崩溃、机房断网,服务就整体消失。Watchtower 更新容器时那几秒的重启窗口可以忍,但机器本身挂掉几小时这种事,如果服务是对外营业的,就忍不了。更现实的场景是:你想给服务器打补丁、换内核、重启 Docker,单机上无路可退,只能挑深夜停机维护。
第二个是横向扩容。单机的 CPU 和内存就是上限。某个服务火了、流量翻了十倍,你在单机上能做的只有把 replicas 写大——可副本再多也挤在同一台机器上抢资源,机器本身就是瓶颈。真正需要的是把副本分散到多台机器上,机器不够了再加一台。
撞上这两堵墙,就需要”集群编排”:让多台机器组成一个整体,你只声明”我要这个服务跑 3 个副本”,系统自己决定每个副本落在哪台机器上,某台机器挂了,副本自动在别的机器上补回来。
做这件事的工具,名气最大的是 Kubernetes,但它概念多、组件重,学习曲线陡峭。其实 Docker 自己就内置了一个编排器——Swarm。它就藏在你已经装好的 Docker 里,不用额外安装任何软件,一条命令就能把单机 Docker 变成集群节点。Swarm 的定位就是:用你已经会的 Compose 知识,平滑地跨到多台机器上。本篇我们用三台机器把集群组起来,跑服务、玩自愈、做滚动更新,最后把 Swarm、单机 Compose、Kubernetes 三者的选型边界讲清楚。
WARNINGSwarm 是 Docker 的内置功能,
docker swarm系列命令在装好 Docker Engine 的机器上直接可用。本文的三台机器请提前各自按系列第一篇的方法装好 Docker,并确保它们之间网络互通(同机房、同内网最好)。
核心概念:先把词对上号
Swarm 的概念不多,一张表对完号,后面实战就不会晕。
| 概念 | 人话解释 |
|---|---|
| 节点(Node) | 集群里的一台机器,装着 Docker 并加入了集群 |
| 经理节点(Manager) | 集群的”大脑”,保存集群状态、调度副本、接受你的命令;它自己也能干活跑容器 |
| 工作节点(Worker) | 纯”干活的”,只负责运行分给它的容器,不参与决策 |
| 服务(Service) | 你声明的期望状态:“我要 nginx 镜像跑 3 个副本,暴露 80 端口” |
| 任务(Task) | 服务拆出来的单个副本,落到某个节点上变成一个实际容器 |
| 副本(Replicas) | 同一个服务要维持的任务数量,少了 Swarm 自动补,多了自动删 |
| 栈(Stack) | 一组相关服务的集合,用一份 Compose 文件整体部署、整体管理 |
| overlay 网络 | 跨机器的虚拟网络,不同机器上的容器像在同一个局域网里互相访问 |
| 路由网格(Routing Mesh) | 在任意节点访问服务的端口,都能被转到某个健康副本上,不用关心副本具体在哪 |
这里面最重要的是理解**“服务”和”容器”的区别**。以前你操作的是容器:docker run 出来一个,死了就是死了。现在你操作的是服务:你只负责声明”应该是什么样”,Swarm 负责让现实不断向声明靠拢——副本死了它补、节点挂了它迁移。这种”声明式”的思维方式,是所有编排系统的共同点,先在这里适应,后面看 Kubernetes 也不会陌生。
组集群实战:三台机器变一台”大机器”
假设三台机器的内网 IP 分别是:
| 机器 | 内网 IP | 计划角色 |
|---|---|---|
| node1 | 192.168.1.11 | Manager |
| node2 | 192.168.1.12 | Worker |
| node3 | 192.168.1.13 | Worker |
第一步:在 node1 上初始化集群
在 node1 上执行:
docker swarm init --advertise-addr 192.168.1.11--advertise-addr 是告诉集群其他节点”用这个地址来找我”,多网卡机器上必须写对,写错了后面加入会连不上。执行成功后输出大致如下:
Swarm initialized: current node (xxxx) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-xxxxxx 192.168.1.11:2377这一刻,node1 就成了集群的第一个 Manager,集群诞生了。输出里那条现成的 join 命令带着令牌(token),就是给其他机器入伙用的”邀请码”。
第二步:其余机器加入
在 node2、node3 上分别执行输出里给的那条命令:
docker swarm join --token SWMTKN-1-xxxxxx 192.168.1.11:2377成功后会看到 This node joined a swarm as a worker.。如果令牌忘了,也不用慌,在 Manager 上随时能重新查看:
# 查看 worker 加入令牌docker swarm join-token worker
# 查看 manager 加入令牌docker swarm join-token manager两种令牌权限不同:拿 worker 令牌加入的只能当 Worker,拿 manager 令牌加入的会成为新的 Manager。join 令牌等于集群的入伙凭证,泄露了任何人都能把机器加进你的集群,务必当密码保管。怀疑泄露时用 docker swarm join-token --rotate worker 一键轮换,旧令牌立即作废。
第三步:验证集群状态
回到 Manager(node1)上:
docker node lsID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSIONa1b2c3 * node1 Ready Active Leader 27.xd4e5f6 node2 Ready Active 27.xg7h8i9 node3 Ready Active 27.x三台都是 Ready,node1 是 Leader,集群就组好了。注意:所有 docker service、docker stack、docker node 管理命令只能在 Manager 上执行,在 Worker 上敲会直接报错,这是故意的——Worker 不持有集群状态,无权发号施令。
经理节点为什么要奇数个
Manager 之间靠 Raft 协议同步集群状态,任何决策都需要”多数派”同意。这就决定了 Manager 数量的选择不是越多越好,而是要看容错:
| Manager 数量 | 决策需要的多数 | 能容忍挂掉的 Manager 数 | 评价 |
|---|---|---|---|
| 1 | 1 | 0 | 单点,仅适合尝鲜和测试 |
| 2 | 2 | 0 | 和 1 台没区别,还更脆弱,不要用 |
| 3 | 2 | 1 | 小集群的甜点配置 |
| 5 | 3 | 2 | 中等规模生产可用 |
| 7 | 4 | 3 | 上限附近,再多同步成本大于收益 |
关键结论:2 台 Manager 是最差配置——挂任意一台就凑不齐多数派,整个集群无法调度,还不如 1 台。所以生产环境要么 3 台、要么 5 台,永远是奇数。还有一个常见误区要避开:Manager 也能跑容器,但节点多了以后,建议用下面的命令把 Manager 设成只调度不干活,避免管理进程和业务容器抢资源:
docker node update --availability drain node1drain(排空)会把该节点上的副本迁走并标记为不再接收新副本,这个命令在后面维护单台机器时也非常好用。
WARNING本文为了演示方便只设了 1 个 Manager,真实生产请至少 3 个。单 Manager 时它一旦宕机,正在运行的容器不会停(Worker 会继续跑已有副本),但集群无法再调度、无法补副本、无法更新服务——所以只是”暂时死不了”,不是高可用。
第一个服务:亲眼看一次自愈
在 Manager 上创建第一个服务:
docker service create --name web --replicas 3 --publish published=8080,target=80 nginx:alpine参数逐个看:--name web 服务名;--replicas 3 要维持 3 个副本;--publish published=8080,target=80 把容器内的 80 端口通过路由网格发布到集群所有节点的 8080 端口。注意新式写法推荐 published=...,target=... 的长格式,比老式的 -p 8080:80 更明确(短格式当然也能用)。
查看服务与副本分布:
docker service lsdocker service ps webNAME MODE REPLICAS IMAGE PORTSweb replicated 3/3 nginx:alpine *:8080->80/tcp
ID NAME IMAGE NODE CURRENT STATExxxx web.1 nginx:alpine node1 Runningyyyy web.2 nginx:alpine node2 Runningzzzz web.3 nginx:alpine node3 RunningREPLICAS 3/3 表示期望 3 个、实际 3 个,状态健康。Swarm 自动把三个副本均匀摊到了三台机器上。现在,在浏览器或用 curl 访问任意一台机器的 8080 端口:
curl http://192.168.1.13:8080都能打开 nginx 的欢迎页——哪怕你访问的这台机器上根本没有空闲副本在监听,路由网格也会把请求转到有副本的节点。这就是集群的感觉:三台机器对外像一台。
接下来见证自愈。去 node2 上,把 web.2 对应的容器强行删掉,或者干脆直接在 node2 上执行 docker stop 停掉那个容器。然后回 Manager 上再看:
docker service ps web你会看到 web.2 的旧任务变成 Shutdown/Failed,紧接着一个新的任务被创建并调度起来,REPLICAS 很快回到 3/3。整个过程你什么都没做。你声明了”3 个副本”,Swarm 就永远让现实等于 3 个,这就是编排的全部魔法。极端一点,你把 node2 整台机器关机,它的副本会被重新调度到 node1、node3 上补齐——单机时代那种”机器挂了服务就没了”的日子结束了。
玩够了删掉服务:
docker service rm webstack 部署:把 Compose 文件搬上集群
单个服务用命令创建还行,真实项目是一组服务加网络加卷,还是得靠文件。这就是 stack:它吃的正是你已经写熟的 Compose 文件,部署命令是 docker stack deploy。
先看一份能直接用于 stack 的 Compose 文件(保存为 stack.yml):
services: web: image: nginx:alpine ports: - "8080:80" deploy: replicas: 3 update_config: parallelism: 1 delay: 10s restart_policy: condition: on-failure max_attempts: 3和普通 Compose 文件九成相同,区别集中在新增的 deploy 段:replicas 是副本数,update_config 控制滚动更新的节奏,restart_policy 控制副本失败后的重启策略。这些字段的思路在系列的 Compose 进阶篇里已经铺垫过,细节可以回去对照看:Docker Compose 进阶:环境变量、健康检查与多环境。
部署:
docker stack deploy -c stack.yml myappmyapp 是栈名,栈内所有服务、网络、卷的名字都会自动加上 myapp_ 前缀,避免多栈之间撞名。查看与管理:
docker stack ls # 所有栈docker stack services myapp # 栈里的服务docker stack ps myapp # 栈里的全部任务docker stack rm myapp # 整个栈一键拆除和 docker compose up 的区别,心里要有数
stack 用的是同一份文件格式,但执行者换成了 Swarm,两边行为有几处关键不同:
| 对比项 | docker compose up | docker stack deploy |
|---|---|---|
| 运行范围 | 单台机器 | 整个集群多台机器 |
| 副本与调度 | 容器就在本机 | 按 replicas 分散到各节点 |
build: 字段 | 现场构建镜像 | 被忽略,镜像必须提前构建好推到仓库 |
depends_on | 控制启动顺序 | 被忽略,服务各自独立启动 |
deploy 段 | 单机下基本被忽略(Swarm 模式专用) | 完整生效:副本、更新、重启、资源、放置约束 |
| 容器名 | 可用 container_name 指定 | 由 Swarm 自动生成,不可指定 |
| 更新方式 | up 时重建变化的容器 | 声明式收敛 + 滚动更新 |
最需要记住的是 build: 被忽略这一条:Swarm 节点是多台,每台现场构建既慢又不一致,所以stack 部署的镜像必须先构建好并推送到所有节点都能拉取的镜像仓库(Docker Hub、私有 Registry 或 Harbor 都行),文件里只写镜像名和标签。本地没推仓库就 deploy,副本会在拉不到镜像的节点上反复失败。
滚动更新:换版本不停服
服务跑起来只是开始,真正体现编排价值的是更新。假设 nginx 出了新版本,要把镜像从 nginx:1.27-alpine 升到 nginx:1.28-alpine。
先看用命令怎么滚动更新一个已有服务:
docker service update \ --image nginx:1.28-alpine \ --update-parallelism 1 \ --update-delay 10s \ web--update-parallelism 1 表示一次只更新 1 个副本,--update-delay 10s 表示每批更新之间停 10 秒观察。更新过程是:停掉一个旧副本 → 在原地(或别的节点)拉起新版副本 → 等它 Running → 隔 10 秒 → 再动下一个。3 个副本分三批换完,全程服务始终有副本在对外响应,零停机。如果用的是 stack 管理,更新更简单:改掉 stack.yml 里的镜像标签和 update_config,重新执行一遍 docker stack deploy -c stack.yml myapp,Swarm 会自己算出差异并滚动收敛。
观察更新进度:
docker service ps webdocker service inspect --pretty web更新到一半发现新版有 bug?一条命令回滚到上一个版本:
docker service rollback webSwarm 保存了服务的上一份规格,回滚同样是滚动执行的——旧版本副本逐个替换回来。注意 rollback 只能回退一步,它不是版本历史系统;真正的版本管理靠你的镜像标签和 Git 里的 Compose 文件,这也是为什么 stack 文件要进版本库。
WARNING滚动更新零停机的前提是服务本身无状态、且新旧版本能短暂共存(比如数据库表结构变更要兼容新旧两个版本)。有状态的强一致服务别指望靠滚动更新蒙混过关,下一节细说。
数据与状态:Swarm 的现实短板
必须把话说在前面:Swarm 天生适合无状态服务,有状态服务在它上面很难受。
原因在于卷(volume)的本质。Docker 的本地卷是绑在某台具体机器上的:一个数据库服务的副本今天落在 node2,它的卷就在 node2 的硬盘里;明天 node2 宕机,Swarm 在 node3 上把这个副本补起来——可 node3 上没有那份数据,新副本面对的是一个空卷,等于数据”丢”了。对 nginx 这类无状态服务这完全不是问题,副本之间一模一样、随起随灭;但对 MySQL、PostgreSQL 这类服务,这是灾难。
业界的通行处理方式有三条,按推荐程度排序:
| 方案 | 做法 | 适用情况 |
|---|---|---|
| 数据库不进 Swarm | 数据库单独部署(单机、主从或云数据库),Swarm 只跑无状态的应用层 | 绝大多数中小项目,首选 |
| 放置约束钉住节点 | 用 deploy.placement.constraints 把有状态服务固定在某台机器,卷不漂移 | 接受”这台机器挂了该服务就停”的妥协时 |
| 分布式存储 | 上 Ceph、GlusterFS、NFS 或云盘做共享卷 | 有专门运维能力的大型场景 |
放置约束的写法长这样,把服务钉在 node2 上:
deploy: placement: constraints: - node.hostname == node2钉住之后它就不再漂移,本质上是”借 Swarm 的壳、行单机之实”,可用性回到了单机水平,但至少更新和管理统一了。我的建议很明确:应用层放心上 Swarm,数据库这类有状态核心先留在集群之外,等规模真的逼到那一步,再考虑 Kubernetes 生态里的 StatefulSet 和专业存储方案。
secrets 与 configs:敏感信息别再写进环境变量
在单机 Compose 时代,密码经常直接写在 .env 或环境变量里,多机集群下这更危险——环境变量在 docker service inspect 里一览无余。Swarm 内置了 secrets(机密)和 configs(普通配置)两个机制:它们由 Manager 加密保存(静态加密于 Raft 日志中),只在运行时以文件形式挂进需要它的容器,路径是 /run/secrets/<名字>,且只存在于内存文件系统里,不落盘。
secrets 最小例子
# 从标准输入创建 secret,不在命令行和 shell 历史里留明文printf 'myS3cretPass' | docker secret create db_pass -
# 创建服务时挂载docker service create --name db \ --secret db_pass \ -e MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db_pass \ mysql:8注意这里给应用的是 MYSQL_ROOT_PASSWORD_FILE 这个文件路径而不是密码本身——主流数据库镜像都支持用 _FILE 后缀的环境变量从文件读密码。应用读到的仍是密码,但密码从未出现在环境变量、镜像和 Compose 文件里。
configs 最小例子
configs 是同一套机制的不加密版,用来分发 nginx 配置这类普通文件,改配置不用重做镜像:
docker config create nginx_conf ./nginx.conf
docker service create --name web \ --config source=nginx_conf,target=/etc/nginx/nginx.conf \ nginx:alpine配置更新时创建新版本 config 再 docker service update --config-rm / --config-add 滚动替换即可。两者的小结:密码、令牌、证书私钥用 secret;普通配置文件用 config;都不要再塞环境变量和镜像层。
选型:Swarm、Compose、Kubernetes 怎么选
最后把三者的边界一次说清:
| 维度 | 单机 Compose | Docker Swarm | Kubernetes |
|---|---|---|---|
| 机器数量 | 1 台 | 2 到几十台 | 几台到成千上万台 |
| 学习成本 | 最低,你已经会了 | 低,复用 Compose 知识 | 高,概念与组件成体系 |
| 高可用 | 无 | 副本自愈 + 多 Manager | 副本自愈 + 更细的控制面冗余 |
| 功能丰富度 | 基础 | 够用:滚动更新、secrets、overlay 网络 | 极强:自动伸缩、丰富的调度与生态 |
| 有状态支持 | 卷在本机,直观 | 弱,需绕开或钉节点 | StatefulSet + 存储生态,完整但复杂 |
| 运维负担 | 几乎为零 | 很轻 | 需要专人或托管服务 |
| 生态现状 | 活跃 | 仍在维护,功能稳定但生态在收缩 | 事实标准,生态庞大 |
我的建议分三档:
- 一台机器能装下、停机几分钟能接受:继续单机 Compose,别折腾。这仍是个人项目和小型服务的最佳解。
- 有了两三台机器、要高可用和不停机更新、团队不想再学一套新系统:选 Swarm,它的每一分复杂度都花在了刀刃上。你前面十几篇积累的镜像、网络、Compose 知识全部直接复用,一个下午就能上线。
- 规模奔着几十台以上、需要自动伸缩、复杂调度、接第三方生态(监控、日志、服务网格全家桶):直接 Kubernetes,不要在 Swarm 上硬撑。需要说明的是,Swarm 本身仍由 Docker 公司维护、没有停止更新,“Swarm 已死”的说法并不准确;但新功能与社区生态的重心确实在 Kubernetes 一边,选型时要把这点算进长期账。
判断标准就一条:先看你的痛点是”单机不够用”还是”需要平台级能力”,前者 Swarm 绰绰有余,后者一步到位上 Kubernetes,中间反复横跳才是最贵的。
排障:最常见的四类问题
节点状态 Down 或 Disconnected
在 Manager 上 docker node ls 看到某节点不是 Ready:
- 先查那台机器本身:Docker 服务是否在跑(
systemctl status docker)、机器是否宕机、磁盘是否写满(磁盘满会导致 Docker 异常)。 - 查节点间连通性:Manager 与 Worker 之间要互通 2377/TCP(集群管理)、7946/TCP+UDP(节点间发现)、4789/UDP(overlay 网络数据面),云服务器记得看安全组,很多人栽在这一步。
- 临时失联的节点恢复网络后会自动归队;确认机器已报废的,用
docker node rm -f <节点>在 Manager 上强制清除,再决定是否补新机器。
swarm join 失败
最常见三类报错与对策:
| 现象 | 常见原因 | 处理 |
|---|---|---|
| connection refused / timeout | 2377 端口被防火墙或安全组拦了 | 放行 2377/TCP 后重试 |
| token 相关的 unauthorized | 令牌抄错或已轮换 | 在 Manager 上 docker swarm join-token worker 重新取 |
| node already part of a swarm | 这台机器以前加过别的集群 | 先 docker swarm leave 退出旧集群再加入 |
另外 --advertise-addr 写错(写成了机器的另一张网卡地址)也会导致 join 时连不上 Manager init 时广播的地址,初始化时务必用其他节点真正能到达的那个 IP。
副本一直 Pending 或 Preparing 起不来
服务层面查起:
docker service ps --no-trunc webdocker service inspect webservice ps 最后一列的错误信息最关键,逐个对:
- Pending 且无错误信息:没有节点满足调度条件——副本数超过可用节点、放置约束写错、或者节点都被 drain 了,
docker node ls看 AVAILABILITY 列。 - Preparing 反复重试:多半在拉镜像,镜像名或标签写错、仓库要登录、私有仓库没在每台节点上
docker login(Swarm 部署需要加--with-registry-auth把登录凭证分发给各节点)。 - Rejected:节点资源不足或端口冲突,看错误信息里的具体描述。
- 进一步深挖单个任务:
docker service ps找到任务所在节点,去那台机器上docker logs看对应容器的日志,排障方法与单机完全一致。
服务发布了端口却访问不通
先确认路由网格工作正常:docker service ls 看 PORTS 列是否真的发布了;再查云安全组和系统防火墙是否放行了发布端口到所有节点(路由网格是任意节点都能进,所以每台都要放行);最后检查 overlay 数据面端口 4789/UDP 是否被拦,它不通时跨节点的转发会直接失效。
小结
这一篇我们跨过了单机到集群的门槛:
- Swarm 是 Docker 内置编排器,不用另装,先想清楚”可用性”和”横向扩容”两个单机天花板再上;
- 核心概念就一组:你声明服务的期望状态,Swarm 用任务副本去逼近它,副本死了自动补,这就是自愈;
- 经理节点用 Raft 多数派,数量只取奇数,3 台是小集群甜点,2 台是最差配置;
docker stack deploy复用 Compose 文件,但build:会被忽略、镜像必须先推仓库,deploy段在集群里才真正生效;- 滚动更新靠
--update-parallelism与--update-delay控制节奏,出事一条rollback回退; - 有状态服务是 Swarm 的短板,数据库留在集群外、或用放置约束钉住节点,别让卷漂移;
- 密码走 secrets、配置走 configs,别再进环境变量;
- 选型记住一句话:单机痛点用 Swarm,平台级需求上 Kubernetes。
至此,从单机部署、自动更新、监控、安全,一路走到多机集群,这套 Docker 系列的骨架就完整了。