Docker Swarm 集群

本文合并自:03-Docker-Swarm集群.md、04-Docker-Swarm集群.md

本章介绍 Docker Swarm mode 集群的原理、搭建与运维:容器编排的必要性与方案对比、Swarm 核心概念(Node / Service / Task / Overlay / Routing Mesh)、集群搭建与节点管理、服务编排与滚动更新、Stack 与 secrets、生产实践与排错清单。文中同时保留两份源文档中的完整实验:3 节点 Swarm 集群 + Consul 高可用 + Registrator 服务发现,以及 Consul + Registrator + Nginx(nginx-upsync-module)实现 Tomcat 微服务动态负载均衡。

一、容器编排与 Swarm 架构

1.1 单机 Docker 的局限与编排的必要性

单机 Docker 解决的是「一个应用如何打包、分发、隔离运行」的问题,docker run / docker-compose up 在单台主机上已经够用。但一旦进入生产环境,单机方案立刻暴露出下面这些无法回避的问题:

单机 Docker 的局限 说明
无法横向扩展 单台主机的 CPU、内存、网络带宽有上限,副本数受限于一台机器
单点故障 主机宕机则其上所有容器全部不可用,没有任何自动迁移能力
容器无自愈 --restart=always 只能在本机重启容器,主机挂了就没救
跨主机网络缺失 默认 bridge 网络只在本机有效,不同主机的容器无法通过容器名互访
无统一调度 哪台机器还有资源、把新容器放到哪里,全靠人工判断
无声明式管理 期望状态(我要 3 个副本)与实际状态(现在只有 1 个)之间没有自动收敛机制
发布风险高 更新镜像需要手工停旧起新,无分批灰度、无自动回滚
服务发现困难 容器 IP 漂移,上游负载均衡器需要手工维护后端列表

容器编排(Orchestration) 就是把「多台主机 + 大量容器」抽象成一个资源池,由编排系统负责调度、网络、伸缩、健康检查、滚动更新与故障自愈。使用者只需要声明「我要什么」,编排系统负责「怎么做到」。

详见 13-01-Docker基础与进阶.md

1.2 主流编排方案对比

维度 Docker Swarm(Swarm mode) Kubernetes(K8s) Apache Mesos + Marathon
出品方 Docker 官方,内置于 Docker Engine CNCF(源于 Google Borg) Apache 基金会 / Mesosphere
定位 轻量级容器编排,Docker 原生体验 通用容器编排平台,事实标准 数据中心级资源调度框架(不止容器)
安装复杂度 极低,docker swarm init 一条命令 高,需要 etcd、多组件、CNI 插件、证书 高,需要 ZooKeeper、Mesos Master/Agent、Framework
学习曲线 平缓,会 Docker + Compose 即可上手 陡峭,概念多(Pod/Deployment/Service/Ingress/CRD) 陡峭,偏向大数据与混合负载
配置文件 Compose 文件 v3(docker stack deploy) YAML 清单(apiVersion/kind/spec) JSON 应用定义
服务发现 内置 DNS + VIP,零配置 内置 CoreDNS + Service 需借助 Mesos-DNS 等组件
负载均衡 内置 Routing Mesh(L4) Service(L4)+ Ingress(L7) 需外接 Marathon-LB / HAProxy
滚动更新与回滚 支持(--update-* / rollback) 支持且更精细(maxSurge/maxUnavailable) 支持
自动扩缩容(HPA) 不支持,只能手工 scale 支持(基于 CPU/内存/自定义指标) 需第三方组件
有状态服务支持 弱(无 StatefulSet,卷不跨节点) 强(StatefulSet + PV/PVC + CSI) 中等
存储编排 依赖 volume 插件,能力有限 CSI 生态完善 中等
生态与社区 活跃度明显下降,功能基本冻结 极其活跃,云厂商全面支持 已明显衰退
适用规模 数节点 ~ 数十节点,几十个服务 数十 ~ 数千节点,大规模微服务 数百 ~ 数千节点,批处理 + 长服务混部
典型场景 中小企业内部系统、教学、边缘小集群 生产主流、公有云容器服务 大数据混合调度(Spark/Hadoop 与容器混部)

结论性判断:

  • Swarm 简单易上手:概念少(node、service、task、stack 四个词就能讲完),与 Docker CLI 无缝衔接,Compose 文件几乎可以直接复用,非常适合作为容器编排的入门教学以及中小规模生产环境。
  • Kubernetes 是事实标准:功能完备、生态繁荣、云厂商全部提供托管服务。学会 Swarm 之后再学 K8s,会发现「服务」「副本」「滚动更新」「服务发现」这些概念是一一对应的,Swarm 是理解 K8s 的很好跳板。
  • Mesos 更偏资源调度框架,容器编排只是它的一种 Framework,如今在容器领域已经很少作为首选。

详见 13-04-Kubernetes集群.md

1.3 Swarm 与 Swarm Mode 的历史区别

很多资料里的「Swarm」指的其实是两个不同的东西,务必区分清楚:

项目 独立 Swarm(Docker Swarm standalone,即 swarm-classic) Swarm Mode(Docker 1.12+ 内置)
形态 一个独立的容器/二进制程序(swarm 镜像),充当代理 直接内建在 Docker Engine 中,无需额外安装
出现时间 2014 年(Docker 1.6 前后) 2016 年 Docker 1.12
外部依赖 需要外部 KV 存储(Consul / etcd / ZooKeeper)保存集群状态 内置 Raft,无需任何外部 KV 存储
使用方式 客户端连到 Swarm Manager 的 API,像操作单机 Docker 一样 docker run 使用全新的 docker swarm / docker service / docker stack 命令族
编排能力 只做容器调度,无服务概念、无滚动更新、无内置负载均衡 有 service/task 抽象、声明式副本、滚动更新、Routing Mesh、secrets
网络 需自行配置 overlay(依赖 KV 存储) 内置 overlay 与 ingress 网络,开箱可用
现状 已废弃 现行版本,本文全部内容基于此

本章所讲的全部内容均为 Swarm Mode。 判断当前 Docker 是否处于 Swarm Mode:

docker info | grep -i swarm
Swarm: inactive

初始化或加入集群后会变为:

Swarm: active

1.4 核心概念

1.4.1 节点(Node)

节点就是加入 Swarm 集群的一个 Docker Engine 实例(通常一台物理机或虚拟机)。节点分两种角色:

角色 职责
Manager(管理节点) 维护集群状态、参与 Raft 共识、接收用户 API 请求、执行编排与调度决策。Manager 默认同时也承担 Worker 的职责(可以跑任务)
Worker(工作节点) 只负责执行 Manager 分配下来的任务(运行容器),不参与集群决策,无法执行 docker service / docker node 等集群管理命令

在 Worker 节点上执行管理命令会报错:

Error response from daemon: This node is not a swarm manager. Worker nodes can't be used to view or modify cluster state. Please run this command on a manager node or promote the current node to a manager.

1.4.2 Manager 的 Raft 一致性与数量选择

所有 Manager 节点通过 Raft 一致性算法 复制集群状态(服务定义、节点列表、secrets、configs 等),保证任意 Manager 上看到的集群状态一致,并在 Leader 故障时自动重新选举。

Raft 要求「多数派(quorum)」达成一致才能写入集群状态。N 个 Manager 的 quorum 为:

quorum = floor(N / 2) + 1
最多可容忍故障 Manager 数 = N - quorum = ceil(N / 2) - 1
Manager 数量 N quorum 最多容忍故障数 评价
1 1 0 无容错,Manager 挂掉集群不可管理(仅测试用)
2 2 0 与 1 个的容错能力相同,却多了一份故障概率,最差选择
3 2 1 推荐,中小集群标准配置
4 3 1 与 3 个容错能力相同,浪费一台,且写延迟更高
5 3 2 推荐,较大集群配置
6 4 2 与 5 个容错能力相同,不划算
7 4 3 官方上限建议,Raft 写开销已经明显

为什么 Manager 数量要是奇数:偶数个 Manager 不会提升容错能力(2 和 1 一样、4 和 3 一样、6 和 5 一样),却增加了 Raft 日志复制开销与出故障的节点数量,同时在网络分区时更容易出现「两边都不是多数派」的僵局。所以生产环境推荐 3 个或 5 个 Manager,官方不建议超过 7 个。

Leader 选举:Manager 中有且只有一个 Leader(docker node ls 的 MANAGER STATUS 列显示 Leader),所有写操作由 Leader 串行处理并复制给其他 Manager;其余 Manager 状态为 Reachable,随时可以被选举为新 Leader。当 Leader 宕机且剩余 Manager 仍满足 quorum 时,几秒内会自动选出新 Leader,业务容器不受影响。

失去 quorum 的后果:集群仍然会继续运行已有容器(Worker 上的任务不会被停掉),但无法做任何变更:不能创建/更新/删除服务,不能增删节点,docker node ls 会报错。恢复方式见第四章的灾难恢复。

1.4.3 服务(Service)、任务(Task)与容器的三层关系

这是 Swarm 最核心的抽象,务必理解清楚:

Service(服务,声明式的期望状态:镜像 + 副本数 + 网络 + 策略)
   │  Manager 按 replicas 拆分
   ├── Task 1(任务,调度的最小单位,被绑定到某个具体节点,不可迁移)
   │        └── Container(容器,Task 的实际运行体,1 : 1)
   ├── Task 2 ──> Container
   └── Task 3 ──> Container
层次 说明
Service 用户声明的期望状态,例如「用 nginx:1.8 跑 3 个副本,发布 80 端口,接入 microservice 网络」。用户只和 Service 打交道
Task Manager 根据 Service 定义生成的调度单元,一个 Task 对应一个容器。Task 一旦被分配到某节点就终身绑定在该节点上,状态机是单向的(NEW → PENDING → ASSIGNED → PREPARING → STARTING → RUNNING → COMPLETE/FAILED/SHUTDOWN)。Task 失败后不会「迁移」,而是由 Manager 新建一个 Task 来替代它
Container Task 的具体载体,即 docker ps 能看到的容器。容器名形如 <服务名>.<副本序号>.<任务ID>

正因为 Task 不可复用,docker service ps 输出中才会同时出现「Shutdown/Failed 的历史 Task」和「新建的 Running Task」,这是正常现象,也是排错时查看失败原因的地方。

1.4.4 两种服务模式:replicated 与 global

模式 命令写法 行为 适用场景
replicated(默认) --replicas N 全集群总共运行 N 个副本,由调度器决定放在哪些节点,可以多个副本落在同一节点 无状态业务服务:Web、API、Tomcat 应用
global --mode global 每个符合条件的节点上运行且仅运行 1 个副本,新节点加入时自动补一个,节点移除时自动减一个;不能指定 --replicas 每机一份的基础设施:日志采集 Agent(Filebeat)、监控 Agent(node_exporter、Zabbix Agent)、网络/存储插件

docker service ls 中 global 服务的 REPLICAS 显示为「当前节点数/期望节点数」,例如 3/3。global 服务同样可以配合 --constraint 限定只在部分节点上运行(例如只在 worker 上跑)。

1.4.5 Stack 与 Compose 文件的关系

概念 说明
Compose 文件 描述多服务应用的 YAML 文件。单机场景由 docker-compose 使用;Swarm 场景要求 version 3.x,其中 deploy 段专门用于 Swarm
Stack 一组相关服务的集合,由一个 Compose v3 文件通过 docker stack deploy -c docker-compose.yml <stack名> 部署到 Swarm。Stack 会自动为服务名加上 <stack名>_ 前缀,并创建带前缀的网络与卷

一句话概括:Compose 文件是「写什么」,Stack 是「怎么把它部署到 Swarm 集群」。同一份 v3 文件可以用 docker-compose up 在开发机跑(此时忽略 deploy 段),也可以用 docker stack deploy 部署到集群(此时忽略 build、depends_on、links 等单机选项),这是 Swarm 相对 K8s 最大的易用性优势。

1.4.6 Overlay 网络、Ingress 网络与 Routing Mesh

网络 驱动 作用域 说明
bridge bridge local 单机默认网桥,跨主机不通
docker_gwbridge bridge local 初始化 Swarm 时自动创建,负责把容器/overlay 网络连到宿主机外网(南北向出口)
ingress overlay swarm 初始化 Swarm 时自动创建的特殊 overlay 网络,承载 Routing Mesh 的入口流量。不支持自定义服务发现,普通容器不能 attach
自建 overlay overlay swarm 用户创建,用于服务间东西向通信与服务名 DNS 解析;加 --attachable 后普通 docker run 的容器也能接入

Overlay 网络原理:基于 VXLAN 隧道(UDP 4789 端口)在多台主机之间构建二层虚拟网络。每个接入 overlay 的容器获得该网络内的一个独立 IP,跨主机容器之间可以直接用这个 IP 或服务名互访,报文在宿主机之间被封装进 VXLAN 帧传输。控制平面(网络元数据、节点成员、服务端点)通过 gossip 协议在 TCP/UDP 7946 端口同步。

Routing Mesh(路由网格)原理:当服务以默认的 ingress 模式发布端口(-p 8080:80)时,Swarm 会在集群每一个节点上都监听该端口。访问任意节点的该端口,即使这个节点上没有运行该服务的任何副本,请求也会通过 ingress overlay 网络被转发到某个真正运行副本的节点上。这带来两个好处:

  1. 外部负载均衡器(HAProxy/Nginx/F5)可以把集群里任意节点当作后端,不必关心副本实际在哪;
  2. 副本扩缩容、节点上下线,对外部入口配置完全透明。

请求流转过程(客户端访问 http://node3:8080,而副本只在 node1、node2 上):

客户端
  │ ① TCP 连到 node3:8080
  ▼
node3 的 iptables/IPVS 规则(Routing Mesh 在所有节点监听已发布端口)
  │ ② DNAT 到 ingress 网络中该服务的 VIP
  ▼
ingress overlay 网络(VXLAN,UDP 4789)
  │ ③ IPVS 按轮询在服务的所有健康 Task 端点间做负载均衡
  ▼
node1 或 node2 上的容器:80
  │ ④ 响应沿原路返回
  ▼
客户端

1.4.7 内置负载均衡:VIP 与 dnsrr

Swarm 为每个服务在每个所接入的 overlay 网络中分配一个 VIP(Virtual IP),并把「服务名 → VIP」写入内置 DNS。

端点模式 参数 行为 适用场景
VIP(默认) --endpoint-mode vip 服务名解析到一个稳定的虚拟 IP,内核 IPVS 在该 VIP 后面对所有健康 Task 做 L4 负载均衡。客户端只看到一个 IP 绝大多数场景,客户端无需感知副本变化
dnsrr --endpoint-mode dnsrr 不分配 VIP,服务名的 DNS 查询直接返回所有 Task 的 IP 列表(DNS 轮询),由客户端自己选择 需要客户端侧负载均衡、需要拿到每个副本地址的场景(如某些 Java 客户端、外接 Nginx/HAProxy 做 L7);注意 dnsrr 不能与 ingress 模式的 -p 端口发布同时使用,需配合 mode=host

服务发现:同一 overlay 网络内,容器可以直接用 服务名 或 任务名 互相访问,由 Swarm 内置的 DNS 服务器(每个容器 /etc/resolv.conf 指向 127.0.0.11)负责解析,无需任何外部注册中心。特别地,tasks.<服务名> 这个特殊域名在 VIP 模式下也会返回所有 Task 的 IP 列表。

1.5 Swarm 架构图与整体流转

                     ┌──────────────── 外部客户端 / 外部 LB ────────────────┐
                     │            访问任意节点的已发布端口                  │
                     └───────────┬──────────────┬──────────────┬───────────┘
                                 ▼              ▼              ▼
   ┌──────────────────────┐  ┌──────────────────────┐  ┌──────────────────────┐
   │ node1  Manager/Leader│  │ node2  Manager       │  │ node3  Worker        │
   │ ┌──────────────────┐ │  │ ┌──────────────────┐ │  │                      │
   │ │ Raft 状态存储     │◄┼──┼►│ Raft 状态存储    │ │  │  (不参与 Raft)        │
   │ │ /var/lib/docker/ │ │  │ │  Reachable       │ │  │                      │
   │ │ swarm            │ │  │ └──────────────────┘ │  │                      │
   │ ├──────────────────┤ │  │ ┌──────────────────┐ │  │ ┌──────────────────┐ │
   │ │ Orchestrator     │ │  │ │ Dispatcher/Agent │ │  │ │ Agent            │ │
   │ │ Scheduler        │ │  │ └──────────────────┘ │  │ └──────────────────┘ │
   │ │ Dispatcher       │ │  │        Task          │  │        Task          │
   │ └──────────────────┘ │  │      Container       │  │      Container       │
   │        Task          │  │                      │  │                      │
   │      Container       │  │                      │  │                      │
   └──────────┬───────────┘  └──────────┬───────────┘  └──────────┬───────────┘
              │  2377/tcp 集群管理 · 7946/tcp+udp 控制面 gossip · 4789/udp VXLAN 数据面
              └──────────────────────────┴──────────────────────────┘
                       ingress overlay + 自建 overlay 网络(跨节点二层)

一次 docker service create --replicas 3 的完整流转:

  1. 用户在任意 Manager 上执行命令,请求经 Docker API 到达该 Manager;
  2. 若该 Manager 不是 Leader,请求被转发给 Leader;
  3. Leader 的 Orchestrator 对比期望状态与实际状态,生成 3 个 Task;
  4. Allocator 为每个 Task 分配 overlay 网络 IP 与服务 VIP;
  5. Scheduler 根据资源预留、--constraint 约束、--placement-pref 分散策略和节点当前负载,为每个 Task 挑选节点;
  6. Leader 通过 Raft 把新状态复制给其他 Manager 并持久化到 /var/lib/docker/swarm;
  7. Dispatcher 把 Task 下发给目标节点的 Agent;
  8. 各节点 Agent 拉取镜像、创建并启动容器,把 Task 状态回报给 Manager;
  9. Manager 持续对账(reconcile):一旦某个 Task 变为 Failed 或其所在节点 Down,立即新建 Task 补足副本数,这就是故障自愈。

二、集群搭建与节点管理

2.1 环境准备

2.1.1 实验环境规划

本章实验以 3 节点集群 为主线(2 个 Manager + 1 个 Worker):

主机名 IP 角色 说明
node1 1.1.1.3 Manager(Leader) 执行 docker swarm init,后续所有集群管理命令都在此执行
node2 1.1.1.4 Manager(Reachable) 以 manager 身份加入,提供管理面冗余
node3 1.1.1.5 Worker 只运行任务

旁注(另一份源文档的环境差异):源文档 04 使用的是 2 节点 规划,只有 node1 = 1.1.1.3(Manager)和 node2 = 1.1.1.4(Worker),Consul 也只跑 1 个 server(-bootstrap-expect=1)。本章统一以上面的 3 节点 规划为主线叙述;凡涉及 04 的 2 节点差异,都会在对应位置以旁注形式标出,请不要把两套 IP 规划混用,否则会跟不下去。

配置静态 IP 与主机名,设置主机名:

hostnamectl set-hostname <主机名>

hostnamectl 为 CentOS 7 命令。CentOS 6 请改为 hostname node1 临时生效,并编辑 /etc/sysconfig/network 中的 HOSTNAME=node1 永久生效。

三台机器都写入 /etc/hosts:

cat >> /etc/hosts <<'EOF'
1.1.1.3  node1
1.1.1.4  node2
1.1.1.5  node3
EOF
1.1.1.3  node1
1.1.1.4  node2
1.1.1.5  node3

2.1.2 时间同步

Raft 协议与 TLS 证书校验都对时间敏感,节点之间时间不同步会导致 Raft 心跳异常、证书验证失败、节点频繁 Down。所有节点必须先做时间同步:

# CentOS 7(chrony,推荐)
yum install -y chrony
systemctl enable chronyd && systemctl start chronyd
chronyc sources -v

# CentOS 6(ntp)
yum install -y ntp
service ntpd start && chkconfig ntpd on
ntpdate -u ntp1.aliyun.com

详见 08-03-时间同步与Xinetd.md

2.1.3 必须放行的端口清单

这是 Swarm 搭建中最常见的坑,端口不通会直接导致 join 失败、节点 Down、overlay 网络不通:

端口 协议 用途 需要放行的节点
2377 TCP 集群管理通信(docker swarm join 连接的就是这个端口) 仅 Manager 需要监听,所有节点需能访问 Manager 的 2377
7946 TCP 和 UDP 节点之间的控制面通信(gossip:节点成员发现、网络元数据同步) 所有节点(Manager + Worker)双向互通
4789 UDP overlay 网络的数据面(VXLAN 封装的业务流量) 所有节点双向互通
50 (ESP) IP 协议号 仅当启用 overlay 网络加密(--opt encrypted)时需要 所有节点

放行方式(CentOS 7 firewalld):

firewall-cmd --permanent --add-port=2377/tcp
firewall-cmd --permanent --add-port=7946/tcp
firewall-cmd --permanent --add-port=7946/udp
firewall-cmd --permanent --add-port=4789/udp
firewall-cmd --permanent --add-protocol=esp
firewall-cmd --reload
firewall-cmd --list-all

CentOS 6(iptables):

iptables -I INPUT -p tcp --dport 2377 -j ACCEPT
iptables -I INPUT -p tcp --dport 7946 -j ACCEPT
iptables -I INPUT -p udp --dport 7946 -j ACCEPT
iptables -I INPUT -p udp --dport 4789 -j ACCEPT
service iptables save
service iptables restart

2.1.4 防火墙与 SELinux 处理

教学与测试环境为省事可以直接关闭(源文档做法);生产环境请按上一节精确放行端口,不要粗暴关闭。

修改 SELinux 配置:

vim /etc/sysconfig/selinux
SELINUX=disabled
# 立即生效(无需重启)
setenforce 0
getenforce

清空 iptables 规则:

iptables -F

关闭并禁用 firewalld(CentOS 7):

systemctl disable firewalld
systemctl stop firewalld

CentOS 6 对应操作:

service iptables stop
chkconfig iptables off

注意:iptables -F 会连带清掉 Docker 自己创建的 DOCKER/DOCKER-USER 链规则,导致容器网络异常。清空后应 systemctl restart docker 让 Docker 重建规则。

详见 07-03-防火墙与SELinux.md

2.1.5 内核参数:转发与桥接过滤

初始化或运行容器时如果出现以下警告,说明内核转发/桥接过滤未开启,overlay 与 Routing Mesh 都会不正常:

WARNING: IPv4 forwarding is disabled
WARNING: bridge-nf-call-iptables is disabled
WARNING: bridge-nf-call-ip6tables is disabled
Swarm: active

修改 /etc/sysctl.conf 配置文件:

vim /etc/sysctl.conf
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-arptables = 1

使配置生效:

sysctl -p

2.2 安装 Docker(所有节点版本必须一致)

集群中所有节点应安装相同版本的 Docker,版本不一致可能导致 API 兼容性问题与调度异常。

2.2.1 方式一:使用 CentOS 自带源安装(版本较老,1.13.1)

yum install -y docker

如需安装最新版 Docker,先卸载旧版本再通过官方脚本安装:

yum remove -y docker*
curl -fsSL https://get.docker.com | sh

2.2.2 方式二:安装 docker-ce 官方源(推荐)

yum install -y yum-utils device-mapper-persistent-data lvm2 && \
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo && \
yum install -y docker-ce && \
systemctl daemon-reload && systemctl start docker

2.2.3 依赖报错处理

安装过程中可能出现以下提示:

错误:软件包:3:docker-ce-18.09.6-3.el7.x86_64 (docker-ce-stable)
          需要:container-selinux >= 2.9

解决方法:先下载并安装 container-selinux:

wget http://mirror.centos.org/centos/7/extras/x86_64/Packages/container-selinux-2.95-2.el7_6.noarch.rpm
rpm -ivh container-selinux-2.95-2.el7_6.noarch.rpm

然后重新执行安装:

yum install -y docker-ce

2.2.4 配置国内镜像加速

写法一(编辑文件):

vim /etc/docker/daemon.json
{
  "registry-mirrors": ["https://registry.docker-cn.com"]
}

写法二(tee + here-document,适合批量脚本化):

tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": ["https://br10hqrl.mirror.aliyuncs.com"]
}
EOF

可用的加速地址还有网易 http://hub-mirror.c.163.com、中科大 https://docker.mirrors.ustc.edu.cn 等,按实际可用性选择。

2.2.5 启动 Docker 并检查

systemctl daemon-reload
systemctl start docker

或重启:

systemctl daemon-reload && systemctl restart docker

CentOS 6 使用 service docker start 与 chkconfig docker on。

查看版本:

docker version
Client:
 Version:         1.13.1
 API version:     1.26
 Package version: docker-1.13.1-94.gitb2f74b2.el7.centos.x86_64
 Go version:      go1.10.3
 Git commit:      b2f74b2/1.13.1
 Built:           Tue Mar 12 10:27:24 2019
 OS/Arch:         linux/amd64

Server:
 Version:         1.13.1
 API version:     1.26 (minimum version 1.12)
 Package version: docker-1.13.1-94.gitb2f74b2.el7.centos.x86_64
 Go version:      go1.10.3
 Git commit:      b2f74b2/1.13.1
 Built:           Tue Mar 12 10:27:24 2019
 OS/Arch:         linux/amd64
 Experimental:    false

查看详细信息:

docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 1.13.1
Storage Driver: overlay2
 Backing Filesystem: xfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: journald
Cgroup Driver: systemd
Plugins:
 Volume: local
 Network: bridge host macvlan null overlay
Swarm: inactive
...
Kernel Version: 3.10.0-957.el7.x86_64
Operating System: CentOS Linux 7 (Core)
OSType: linux
Architecture: x86_64
Number of Docker Hooks: 3
CPUs: 1
Total Memory: 1.777 GiB
Name: node1
...
Registry Mirrors:
 http://hub-mirror.c.163.com
...

2.3 初始化集群

在哪台服务器上执行初始化命令,哪台服务器就成为集群的管理节点(manager),并自动成为 Leader。 在 node1 上执行:

docker swarm init --advertise-addr 1.1.1.3
Swarm initialized: current node (l9mveonpubd7dugdpbrraq1q3) is now a manager.

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

    docker swarm join \
    --token SWMTKN-1-6co0gt73819kggxcsnqfr7j32ner9tdl7wi3mf0dnvu0o2bknz-ablwzg4iahibpq6r9dquwppvr \
    1.1.1.3:2377

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

新版本(18.09)输出格式为单行,含义相同:

Swarm initialized: current node (um6ju2i70cfa8mchdgb02mz47) is now a manager.

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

    docker swarm join --token SWMTKN-1-6905vu9zrd6jge8h0oom6dzjp1gef3fwmdr6f6o6zpllfci6cu-2mqgv8s46doyzlwgz8yysm2mz 1.1.1.3:2377

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

--advertise-addr 的必要性:它指定「本节点向其他节点公告的地址」,其他节点将用这个地址来连接本节点。

  • 单网卡机器可以省略,Docker 会自动探测;
  • 多网卡(或有 NAT、公私双网、多 IP)的机器必须显式指定,否则 Docker 无法判断该用哪个 IP 公告,会报错 could not choose an IP address to advertise since this system has multiple addresses on interface ...,或者公告了一个其他节点访问不到的地址,导致节点看起来 Ready 但 overlay 不通。
  • 相关的还有 --listen-addr(实际监听地址,默认 0.0.0.0:2377)和 --data-path-addr(指定 VXLAN 数据面走哪块网卡,用于管理面与数据面分离的场景)。

docker swarm init 常用选项:

选项 说明
--advertise-addr <IP[:port]> 公告给其他节点的地址,多网卡必填
--listen-addr <IP:port> 管理面监听地址,默认 0.0.0.0:2377
--data-path-addr <IP> overlay 数据面(VXLAN)使用的地址
--force-new-cluster 从当前节点的 Raft 数据强制新建单节点集群,用于灾难恢复
--autolock 开启自动锁定,Docker 重启后需用 unlock key 解锁才能恢复管理
--task-history-limit <N> 保留的历史 Task 数量,默认 5
--dispatcher-heartbeat <duration> Agent 心跳周期,默认 5s

join token 的含义:token 格式为 SWMTKN-1-<集群CA证书哈希>-<角色密钥>。

  • 前半段是集群根 CA 证书的摘要,用于新节点校验「我连的是不是正确的集群」,防中间人;
  • 后半段是与角色绑定的密钥,worker token 和 manager token 后半段不同,决定新节点以什么角色加入;
  • token 一旦泄露,任何人都能把机器加入你的集群,因此需要妥善保管并定期轮换。

查看以 worker 身份加入集群的命令:

docker swarm join-token worker

查看以 manager 身份加入集群的命令:

docker swarm join-token manager
docker swarm join \
    --token SWMTKN-1-6co0gt73819kggxcsnqfr7j32ner9tdl7wi3mf0dnvu0o2bknz-cy4uh1dfa4l4s3cng0yg5lujn \
    1.1.1.3:2377

只输出 token 本身(便于脚本引用):

docker swarm join-token -q worker

轮换 token(旧 token 立即失效,已加入的节点不受影响):

docker swarm join-token --rotate worker
docker swarm join-token --rotate manager

查看 swarm 激活状态:

docker info | grep -i swarm
Swarm: active

2.4 加入节点

本例规划 2 个 manager、1 个 worker。

在 node2 上以 manager 身份加入:

docker swarm join \
    --token SWMTKN-1-6co0gt73819kggxcsnqfr7j32ner9tdl7wi3mf0dnvu0o2bknz-cy4uh1dfa4l4s3cng0yg5lujn \
    1.1.1.3:2377

在 node3 上以 worker 身份加入:

docker swarm join \
    --token SWMTKN-1-6co0gt73819kggxcsnqfr7j32ner9tdl7wi3mf0dnvu0o2bknz-ablwzg4iahibpq6r9dquwppvr \
    1.1.1.3:2377

加入成功会输出:

This node joined a swarm as a worker.

或

This node joined a swarm as a manager.

旁注(源文档 04 的 2 节点做法):只把 node2 以 worker 身份加入,直接执行 init 输出里的那条 join 命令:

docker swarm join --token SWMTKN-1-6905vu9zrd6jge8h0oom6dzjp1gef3fwmdr6f6o6zpllfci6cu-2mqgv8s46doyzlwgz8yysm2mz 1.1.1.3:2377

加入失败的常见原因:

现象 / 报错 原因与处理
Timeout was reached before node joined Manager 的 2377 端口不通:防火墙未放行、Manager 未启动、--advertise-addr 写错
rpc error: code = Unavailable 网络不通或 Manager 正在选举中,检查连通性 telnet 1.1.1.3 2377
invalid join token token 抄错(换行/空格)或已被 --rotate 轮换失效,重新 docker swarm join-token worker 获取
This node is already part of a swarm 节点已在其他集群中,先 docker swarm leave --force 再加入
certificate has expired or is not yet valid 节点之间时间不同步,先做 NTP 时间同步
加入后马上变 Down 7946 TCP/UDP 未放行,控制面 gossip 不通
节点 Ready 但 overlay 不通 4789 UDP 被拦,或 --advertise-addr 公告了不可达地址

2.5 节点管理命令

2.5.1 查看集群成员 docker node ls

在任意 manager 节点上执行:

docker node ls
ID                           HOSTNAME  STATUS  AVAILABILITY  MANAGER STATUS
5bq2l28mb6kdl88t4lzjdz6xy    node2     Ready   Active        Reachable
l0j2psjlfs3hoywwjzlk785c2    node3     Ready   Active
l9mveonpubd7dugdpbrraq1q3 *  node1     Ready   Active        Leader

新版本还会多一列 ENGINE VERSION:

ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS      ENGINE VERSION
um6ju2i70cfa8mchdgb02mz47 *   node1               Ready               Active              Leader              18.09.6
u3p5431dqq6a62vk5x9t0c9b7     node2               Ready               Active                                  18.09.6

各列含义:

列 含义
ID 节点 ID,带 * 表示当前执行命令的节点
HOSTNAME 节点主机名(所以主机名必须唯一且有意义)
STATUS 节点与 Manager 的通信状态:Ready(正常)/ Down(失联)/ Unknown / Disconnected
AVAILABILITY 节点是否接受新任务:Active / Pause / Drain
MANAGER STATUS 管理面状态,见下表;为空表示该节点是 worker
ENGINE VERSION 该节点的 Docker Engine 版本

AVAILABILITY 取值:

取值 含义
Active 正常,调度器可以向该节点分配新任务
Pause 保留已有任务继续运行,但不再接受新任务
Drain 不接受新任务,且把该节点上已有任务全部驱逐到其他节点,用于维护下线

MANAGER STATUS 列说明:

状态 含义
Leader 主管理节点,负责集群决策,所有写操作由它串行处理
Reachable 管理节点,参与 Raft 共识,可在 Leader 故障时被选举为新 Leader
Unavailable 管理节点已失联,不再参与共识(若失联数量导致失去 quorum,集群将无法变更)
空 该节点为 worker,不参与管理

本例中 node1 是 Leader,node2 是 Reachable 的 manager,node3 是 worker。

常用过滤与格式化:

docker node ls -q                                  # 只输出节点 ID
docker node ls --filter role=manager               # 只看 manager
docker node ls --filter node.label=zone=bj         # 按节点标签过滤
docker node ls --format "{{.Hostname}}\t{{.Status}}\t{{.Availability}}"

2.5.2 查看节点详情 docker node inspect

docker node inspect node1
docker node inspect self --pretty
docker node inspect node3 --pretty

--pretty 以人类可读格式输出,可以看到节点角色、可用性、标签、CPU/内存资源、Docker 版本、插件列表、TLS 信息等。

2.5.3 查看某节点上运行的任务 docker node ps

docker node ps            # 默认看当前节点
docker node ps node3      # 看指定节点
docker node ps --no-trunc node3
docker node ps -f "desired-state=running" node3

2.5.4 修改节点属性 docker node update

调整可用性:

docker node update --availability active node3
docker node update --availability pause  node3
docker node update --availability drain  node3

drain 用于维护下线的完整流程:

# ① 把节点标记为 drain,Swarm 会把该节点上的任务在其他节点上重建
docker node update --availability drain node3

# ② 确认该节点上已无正在运行的任务(应只剩 Shutdown 状态的历史任务)
docker node ps node3

# ③ 确认服务副本数已在其他节点补齐
docker service ls
docker service ps <服务名>

# ④ 此时可以安全地对 node3 做维护:升级内核、升级 Docker、重启主机
systemctl stop docker
reboot

# ⑤ 维护完成、Docker 启动并 STATUS 恢复 Ready 后,重新置为 active
docker node update --availability active node3

# ⑥ 注意:置为 active 后已有服务不会自动重新平衡到该节点,
#    如需重新分布,对服务执行强制重调度
docker service update --force <服务名>

给节点打标签(用于 --constraint 调度约束):

docker node update --label-add zone=bj  --label-add disk=ssd node2
docker node update --label-add role=web node3
docker node update --label-rm  disk node2

查看标签:

docker node inspect node2 --pretty | grep -A5 Labels
docker node inspect -f '{{ .Spec.Labels }}' node2

2.5.5 角色升降级 promote / demote

docker node promote node3        # worker → manager
docker node demote  node2        # manager → worker

等价写法:

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

降级最后一个 Manager 会导致集群不可管理,Docker 会拒绝该操作。做 Manager 数量调整时,务必保证操作过程中始终满足 quorum。

2.5.6 移除节点:drain → leave → rm

正确顺序非常重要,直接 docker node rm 一个仍在运行任务的节点会留下孤儿容器:

# ① 在 Manager 上先驱逐任务
docker node update --availability drain node3

# ② 若是 Manager,先降级为 worker(Manager 不能直接 leave,否则影响 quorum)
docker node demote node3

# ③ 在被移除的节点(node3)上执行离开集群
docker swarm leave

# ④ 回到 Manager,此时 node3 状态为 Down,将其从节点列表中删除
docker node ls
docker node rm node3

docker swarm leave 与 --force:

命令 说明
docker swarm leave 节点主动离开集群。在 Manager 上执行会被拒绝(提示会破坏 quorum)
docker swarm leave --force 强制离开,Manager 也可执行。最后一个 Manager 执行它等于解散集群,Raft 数据被清除,务必谨慎
docker node rm <节点> 在 Manager 上把已 Down 的节点从列表移除
docker node rm --force <节点> 强制移除仍处于 Ready 状态的节点(不推荐,会留下未清理的本地状态)

节点被 rm 之后如需重新加入,必须在该节点先 docker swarm leave --force 清理本地状态,再用新的 join token 加入。

2.6 集群备份与恢复

2.6.1 备份 Raft 数据

Swarm 的全部集群状态(服务定义、网络、secrets、configs、节点信息、TLS CA)都保存在 Manager 节点的 /var/lib/docker/swarm 目录中。备份步骤:

# ① 在一个非 Leader 的 Manager 上停止 Docker,保证数据一致
systemctl stop docker

# ② 打包备份整个 swarm 目录(含 raft 日志、证书、加密密钥)
tar -czvf /root/swarm-backup-$(date +%F).tar.gz -C /var/lib/docker swarm

# ③ 启动 Docker,节点重新加入 Raft 集群
systemctl start docker
docker node ls

注意事项:

  • 备份必须在 Manager 上做,Worker 上没有集群状态;
  • 备份期间该 Manager 不可用,因此要保证剩余 Manager 仍满足 quorum(3 Manager 停 1 个是安全的);
  • 如果集群开启了 --autolock,备份文件中的数据是加密的,恢复时必须有 unlock key,请与备份分开保存;
  • 备份不包含容器内的业务数据与数据卷,业务数据需另行备份。

2.6.2 从备份恢复

# ① 在目标 Manager 上停止 Docker
systemctl stop docker

# ② 移走当前的 swarm 目录
rm -rf /var/lib/docker/swarm

# ③ 解压备份
tar -zxvf /root/swarm-backup-2019-06-01.tar.gz -C /var/lib/docker/

# ④ 启动 Docker
systemctl start docker

# ⑤ 以备份数据强制新建单节点集群
docker swarm init --force-new-cluster --advertise-addr 1.1.1.3

# ⑥ 检查恢复结果:服务定义应全部回来,其他节点显示 Down
docker node ls
docker service ls

# ⑦ 把旧的 Down 节点删掉,让它们重新加入
docker node rm node2
docker node rm node3
docker swarm join-token manager

2.6.3 灾难恢复:--force-new-cluster

当多数 Manager 同时损毁、集群失去 quorum、无法通过正常手段恢复时,在某个仍存活的 Manager 上执行:

docker swarm init --force-new-cluster --advertise-addr 1.1.1.3

它的作用是:以本节点现有的 Raft 数据为基准,重建一个只有本节点作为 Manager 的新集群,同时保留所有服务、网络、secrets 定义与集群 ID。Worker 上原本运行的容器不会被杀掉,但它们会显示为 Down,需要重新加入。恢复后应尽快把 Manager 数量补回 3 或 5。

2.7 集群解散的完整步骤

# ① 在 Manager 上删除所有 stack 与 service
docker stack ls
docker stack rm $(docker stack ls --format '{{.Name}}')
docker service ls
docker service rm $(docker service ls -q)

# ② 删除自建的 overlay 网络、secrets 与 configs
docker network rm microservice
docker secret rm $(docker secret ls -q)
docker config rm $(docker config ls -q)

# ③ 让所有 Worker 依次离开集群(在各 Worker 上执行)
docker swarm leave

# ④ 在 Manager 上把已 Down 的节点移除
docker node ls
docker node rm node3

# ⑤ 把多余的 Manager 降级为 worker,再让它们 leave
docker node demote node2
# 在 node2 上:
docker swarm leave
# 回到 node1:
docker node rm node2

# ⑥ 最后一个 Manager 强制离开,集群解散
docker swarm leave --force

# ⑦ 确认状态
docker info | grep -i swarm
Swarm: inactive

三、服务编排与运维

3.1 docker service create 选项逐项详解

基本形式:

docker service create [选项] <镜像> [命令] [参数]

最小示例(源文档实验,3 副本 nginx 接入自建 overlay 网络):

docker service create --replicas 3 --name nginx --network microservice --publish 80:80 nginx:1.8
ix7b42n0ek1ko8f0b4bdouxtd

源文档给出的基础参数说明:

参数 说明
--replicas 指定服务副本个数,Swarm 会自动将副本调度到各节点
--name 服务名称
--network 服务所使用的网络
--publish 端口映射,格式 <宿主机端口>:<容器端口>

后续可通过 docker service scale <服务名>=<副本数> 增加或减少副本数量。

3.1.1 名称与副本

选项 说明
--name <名称> 服务名。同时也是集群内的 DNS 名称,容器之间用它互访,必须唯一
--replicas <N> replicated 模式下的副本数,默认 1
--mode global 全局模式,每个符合条件的节点各跑 1 个副本,不能与 --replicas 同时使用
--replicas-max-per-node <N> 限制单节点上最多运行的副本数,避免副本挤在同一台机器(较新版本支持)
# replicated:全集群共 3 个副本
docker service create --name web --replicas 3 nginx:1.8

# global:每个节点 1 个副本,适合日志/监控 agent
docker service create --name node-exporter --mode global \
  --mount type=bind,src=/proc,dst=/host/proc,readonly \
  prom/node-exporter

3.1.2 端口发布:ingress 与 host 两种模式

写法 模式 行为
-p 8080:80 或 -p published=8080,target=80 ingress(默认) 启用 Routing Mesh,集群所有节点都监听 8080,请求被负载均衡到任意副本
-p published=8080,target=80,mode=host host 只在实际运行副本的节点上监听 8080,不经 Routing Mesh,请求直达本机副本
-p published=8080,target=80,protocol=udp — 指定协议(tcp/udp/sctp),默认 tcp

两种模式的差异对比:

维度 ingress 模式 host 模式
监听范围 所有节点 仅运行副本的节点
负载均衡 Swarm 内置 IPVS 负载均衡 无,需外部 LB 自行寻址
客户端源 IP 被 SNAT,容器看到的是网关 IP(会丢失真实源 IP) 保留真实源 IP
单节点副本数限制 无 同一端口每节点只能有 1 个副本(端口冲突)
性能 多一跳 VXLAN 转发 直连,延迟更低
适用 通用 Web 服务 需要真实源 IP、极致性能,或配合 global 模式做每机入口(如边缘 Nginx)
# ingress 模式:从任意节点 curl 都能访问
docker service create --name web -p 8080:80 --replicas 2 nginx:1.8

# host 模式 + global:每台机器一个入口,保留源 IP
docker service create --name edge --mode global \
  -p published=80,target=80,mode=host nginx:1.8

3.1.3 网络

选项 说明
--network <网络名> 把服务接入指定 overlay 网络,可重复指定接入多个网络
--network name=<网络>,alias=<别名> 同时设置网络别名
--endpoint-mode vip\|dnsrr 端点模式,见 1.4.7

部署服务时的两条注意事项(源文档强调):

  1. 暴露的端口与容器内部端口必须一致;
  2. 服务必须指定相同的网络,例如前面创建的 --network microservice,否则服务之间无法通过服务名互相调用。

3.1.4 环境变量与配置

选项 说明
-e KEY=VALUE / --env KEY=VALUE 设置环境变量,可重复
--env-file <文件> 从文件批量读取环境变量
--hostname <主机名> 设置容器主机名(支持 {{.Node.Hostname}}、{{.Task.Slot}} 等模板变量)
--user <uid:gid> / -u 以指定用户运行容器进程,生产环境避免 root
--workdir <目录> / -w 容器工作目录
--stop-grace-period <时长> 停止容器时的优雅退出等待时间,默认 10s
--stop-signal <信号> 停止容器发送的信号,默认 SIGTERM
--init 使用 init 进程作为 PID 1,回收僵尸进程
--read-only 容器根文件系统只读

模板变量示例(让每个副本的 hostname 带上节点名和副本序号,便于排错):

docker service create --name web --replicas 3 \
  --hostname "{{.Node.Hostname}}-{{.Service.Name}}-{{.Task.Slot}}" nginx:1.8

3.1.5 挂载 --mount:volume 与 bind 两种 type

# type=volume:使用 Docker 卷(本地卷仅在所在节点有效,见排错清单的概念陷阱)
--mount type=volume,src=<卷名>,dst=<容器路径>
--mount type=volume,src=mydata,dst=/data,volume-driver=local
--mount type=volume,src=nfsdata,dst=/data,volume-driver=local,\
volume-opt=type=nfs,volume-opt=device=:/export/data,"volume-opt=o=addr=1.1.1.10,rw"

# type=bind:把宿主机目录/文件挂进容器(该路径必须在每个可能运行副本的节点上都存在)
--mount type=bind,src=/data/app,dst=/usr/local/tomcat/webapps
--mount type=bind,src=/etc/localtime,dst=/etc/localtime,readonly

# type=tmpfs:内存文件系统
--mount type=tmpfs,dst=/tmp,tmpfs-size=64m
type 特点 注意
volume 由 Docker 管理,卷不存在时自动创建 本地卷不跨节点共享,副本换节点后数据「消失」;跨节点需要 NFS/分布式存储卷插件
bind 直接映射宿主机路径 路径必须在所有可能调度到的节点上都存在且内容一致,否则容器起不来或读到空目录
tmpfs 内存盘,容器停止即丢失 适合缓存、临时文件

docker service create 只支持 --mount,不支持 docker run 的 -v 简写(较新版本才兼容部分写法),书写时注意区别。

3.1.6 调度约束 --constraint

基于节点标签、角色、主机名等把服务限制到特定节点:

# 只调度到 worker 节点(生产强烈推荐,避免业务挤占 manager)
docker service create --name web --replicas 3 \
  --constraint 'node.role == worker' nginx:1.8

# 只调度到 manager
--constraint 'node.role == manager'

# 按自定义节点标签(先 docker node update --label-add disk=ssd node2)
--constraint 'node.labels.disk == ssd'
--constraint 'node.labels.zone != sh'

# 按主机名 / 节点 ID
--constraint 'node.hostname == node3'
--constraint 'node.id == l0j2psjlfs3hoywwjzlk785c2'

# 按引擎标签(daemon.json 中 labels 配置的)
--constraint 'engine.labels.os == centos7'

# 多个 --constraint 之间是 AND 关系
docker service create --name db --replicas 1 \
  --constraint 'node.role == worker' \
  --constraint 'node.labels.disk == ssd' mysql:5.7

可用的内置约束键:node.id、node.hostname、node.role、node.platform.os、node.platform.arch、node.labels.<key>、engine.labels.<key>。操作符只有 == 和 !=。

约束无法满足时,服务会一直停留在 0/N,Task 状态为 Pending,docker service ps --no-trunc 会提示 no suitable node (scheduling constraints not satisfied on N nodes)。

3.1.7 分散策略 --placement-pref

让副本按某个标签维度均匀分散,提升可用性(例如跨机架、跨可用区):

docker service create --name web --replicas 6 \
  --placement-pref 'spread=node.labels.zone' nginx:1.8

# 可叠加多级分散:先按机房分散,机房内再按机架分散
docker service create --name web --replicas 12 \
  --placement-pref 'spread=node.labels.datacenter' \
  --placement-pref 'spread=node.labels.rack' nginx:1.8

--constraint 是硬约束(不满足就不调度),--placement-pref 是软偏好(尽量均匀,节点不足时仍会调度)。

3.1.8 资源限制与预留

选项 含义 作用时机
--limit-cpu <核数> 运行时上限:容器最多能用多少 CPU(cgroup 限制),如 0.5 表示半核 容器运行期间生效
--limit-memory <大小> 运行时上限:容器内存上限,超出触发 OOM Kill,如 512M 容器运行期间生效
--reserve-cpu <核数> 调度预留:调度器只会把 Task 放到「剩余可预留 CPU ≥ 该值」的节点上 调度决策阶段生效
--reserve-memory <大小> 调度预留:同上,针对内存 调度决策阶段生效

关键区别:

  • limit 管「能用多少」,是对容器的强制约束,不影响调度;
  • reserve 管「能放哪里」,是对调度器的资源承诺,节点上所有 Task 的 reserve 之和不能超过节点总资源;
  • 只写 limit 不写 reserve,可能把大量容器挤到同一节点导致资源争抢;只写 reserve 不写 limit,容器可能吃掉整机资源;
  • 生产建议两者都写,且 limit ≥ reserve。
docker service create --name web --replicas 3 \
  --limit-cpu 1 --limit-memory 1G \
  --reserve-cpu 0.5 --reserve-memory 512M nginx:1.8

常见坑:--reserve-memory 4G 但节点只有 1.7G 内存,服务会永远 0/3,docker service ps --no-trunc 提示 no suitable node (insufficient resources on N nodes)。

3.1.9 重启策略

选项 说明 默认值
--restart-condition <none\|on-failure\|any> 何时重启:none 不重启 / on-failure 仅非零退出时 / any 任何退出都重启 any
--restart-delay <时长> 两次重启之间的等待时间,如 5s 5s
--restart-max-attempts <N> 最大重启尝试次数,0 表示不限制 0(无限)
--restart-window <时长> 用于统计重启次数的时间窗口 无限
docker service create --name web --replicas 3 \
  --restart-condition on-failure \
  --restart-delay 10s \
  --restart-max-attempts 3 \
  --restart-window 120s nginx:1.8

一次性任务(跑完就该退出的批处理)应使用 --restart-condition none,否则 Swarm 会认为它「异常退出」而不断重启。

3.1.10 滚动更新与回滚策略

这些选项在 create 时设定服务的默认更新行为,之后 docker service update 会按此策略执行:

选项 说明 默认值
--update-parallelism <N> 每批同时更新的 Task 数量,0 表示一次全部更新 1
--update-delay <时长> 两批之间的等待时间,如 10s、1m30s 0s
--update-failure-action <pause\|continue\|rollback> 更新失败后的动作:暂停 / 继续 / 自动回滚 pause
--update-max-failure-ratio <比例> 容忍的失败比例(0~1),低于此比例不算更新失败 0
--update-monitor <时长> 每个 Task 更新后的观察期,期间失败即算失败 5s
--update-order <stop-first\|start-first> 更新顺序:先停旧再起新 / 先起新再停旧(蓝绿式,需要端口不冲突) stop-first
--rollback-parallelism <N> 回滚时每批处理的 Task 数量,0 表示一次全部回滚 1
--rollback-delay <时长> 回滚批次间隔 0s
--rollback-failure-action <pause\|continue> 回滚失败后的动作 pause
--rollback-monitor <时长> 回滚后的观察期 5s
--rollback-max-failure-ratio <比例> 回滚容忍的失败比例 0
docker service create --name web --replicas 6 -p 8080:80 \
  --update-parallelism 2 \
  --update-delay 10s \
  --update-monitor 15s \
  --update-failure-action rollback \
  --update-max-failure-ratio 0.2 \
  --update-order start-first \
  --rollback-parallelism 3 \
  nginx:1.8

--update-order 两种取值的差异:

取值 行为 适用
stop-first(默认) 先停掉旧 Task,再启动新 Task 端口/资源冲突场景(host 模式端口、独占卷)
start-first 先启动新 Task 并等它健康,再停旧 Task 要求零中断的服务,但更新期间资源占用翻倍

3.1.11 健康检查

docker service create --name web --replicas 3 \
  --health-cmd "curl -fs http://localhost/ || exit 1" \
  --health-interval 10s \
  --health-timeout 3s \
  --health-retries 3 \
  --health-start-period 30s \
  nginx:1.8
选项 说明
--health-cmd <命令> 健康检查命令,退出码 0 表示健康
--health-interval <时长> 检查间隔
--health-timeout <时长> 单次检查超时
--health-retries <N> 连续失败多少次判定为 unhealthy
--health-start-period <时长> 容器启动后的宽限期,期间失败不计数(Java 应用启动慢,必须设置)
--no-healthcheck 禁用镜像中定义的健康检查

健康检查对滚动更新至关重要:start-first 模式下 Swarm 用健康状态判断新 Task 是否可用;unhealthy 的 Task 会被 Swarm 杀掉重建,也会被从服务 VIP 的负载均衡后端中摘除。

3.1.12 私有仓库认证 --with-registry-auth

# ① 先在 Manager 上登录私有仓库
docker login registry.example.com

# ② 创建服务时带上 --with-registry-auth
docker service create --name web --replicas 3 \
  --with-registry-auth registry.example.com/team/web:v1.0.0

为什么必须加 --with-registry-auth:docker login 的凭据只保存在执行命令的那台 Manager 的 ~/.docker/config.json 中。Task 是在各个 Worker 节点上拉镜像的,Worker 并没有这份凭据。加上该选项后,Manager 会把仓库认证信息随 Task 一起加密下发给目标节点,节点才能成功 docker pull。

不加的典型后果:服务卡在 0/3,docker service ps --no-trunc 显示:

No such image: registry.example.com/team/web:v1.0.0
或
pull access denied for registry.example.com/team/web, repository does not exist or may require 'docker login'

docker service update 与 docker stack deploy 同样支持 --with-registry-auth,每次更新镜像都要带上。

3.2 查看与排错命令

3.2.1 docker service ls 与 REPLICAS 列判读

列出所有服务:

docker service ls
ID            NAME   MODE    REPLICAS  IMAGE
2vykjzjq29ly  nginx  global  3/3       nginx:latest
ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
uak7xc4laygv        ge-service          replicated          1/1                 ge-service:v1.0.0   *:8080->8080/tcp, *:8838->8838/tcp

REPLICAS 列读作 实际运行数/期望数:

显示 含义与处理
3/3 健康,全部副本就绪
2/3 有 1 个副本未就绪:可能正在拉镜像/启动,也可能反复失败。等几秒再看,仍不变则用 docker service ps 排查
0/3 一个副本都起不来:镜像拉不到、约束无法满足、资源不足、端口冲突。必看 docker service ps --no-trunc
3/3(global) global 模式下表示 3 个符合条件的节点上都跑起来了
数字长时间跳动 Task 反复启动失败被重建,检查容器启动命令与日志

3.2.2 docker service ps 与 --no-trunc(最关键的排错命令)

查看服务运行在哪些节点上:

docker service ps nginx
ID            NAME                             IMAGE         NODE   DESIRED STATE  CURRENT STATE               ERROR  PORTS
1fgg4nr7pm9c  nginx.l0j2psjlfs3hoywwjzlk785c2  nginx:latest  node3  Running        Running about a minute ago
7hroiw0p5etj  nginx.5bq2l28mb6kdl88t4lzjdz6xy  nginx:latest  node2  Running        Running 59 seconds ago
i9t8rsif4aqz  nginx.l9mveonpubd7dugdpbrraq1q3  nginx:latest  node1  Running        Running about a minute ago
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE                ERROR               PORTS
vdy2v9s1tev0        ge-service.1        ge-service:v1.0.0   node1               Running             Running 4 minutes ago
dhzfd3jses4t        ge-service.2        ge-service:v1.0.0   node2               Running             Running about a minute ago

--no-trunc 是排错的关键:默认输出会把 ERROR 列截断,加上它才能看到完整的失败原因。

docker service ps --no-trunc nginx
docker service ps --no-trunc -f "desired-state=running" nginx
docker service ps --no-trunc -f "desired-state=shutdown" nginx

各列含义:

列 含义
ID Task ID
NAME Task 名,格式 <服务名>.<副本序号> 或 <服务名>.<节点ID>(global 模式);带 \_ 前缀的是被替换的历史 Task
IMAGE Task 使用的镜像(滚动更新期间可看到新旧两种镜像并存)
NODE Task 被调度到哪个节点
DESIRED STATE 期望状态:Running(应在运行)/ Shutdown(应被关闭)/ Accepted / Ready
CURRENT STATE 实际状态:Running / Preparing(拉镜像中)/ Starting / Pending(无法调度)/ Complete / Failed / Rejected(节点拒绝执行)/ Shutdown
ERROR 失败原因,配合 --no-trunc 查看完整信息
PORTS host 模式发布的端口

常见 ERROR 与含义速查:

ERROR 片段 原因
No such image / manifest unknown 镜像名或 tag 写错,或私有仓库未认证(缺 --with-registry-auth)
pull access denied 私有仓库认证失败
no suitable node (scheduling constraints not satisfied on N nodes) --constraint 无法满足
no suitable node (insufficient resources on N nodes) --reserve-cpu/--reserve-memory 超过节点可用资源
no suitable node (max replicas per node limit exceed) 触发 --replicas-max-per-node 限制
task: non-zero exit (137) 容器被 SIGKILL(手工 kill 或 OOM)
task: non-zero exit (1) 容器内进程启动失败,用 docker service logs 看应用日志
starting container failed: port is already allocated 端口冲突(host 模式或宿主机已有进程占用)
invalid mount config / bind source path does not exist bind 挂载的宿主机路径在该节点不存在
Rejected + mount source path 节点拒绝执行,通常是挂载路径或权限问题

3.2.3 docker service inspect

查看服务详细信息,添加 --pretty 以可读格式输出:

docker service inspect nginx --pretty
docker service inspect nginx                              # 完整 JSON
docker service inspect -f '{{.Spec.Mode.Replicated.Replicas}}' nginx
docker service inspect -f '{{.Endpoint.VirtualIPs}}' nginx
docker service inspect -f '{{.Spec.TaskTemplate.ContainerSpec.Image}}' nginx
docker service inspect -f '{{json .PreviousSpec}}' nginx   # 上一版本定义(回滚依据)

3.2.4 docker service logs

聚合查看服务所有副本的日志(无需登录各节点):

docker service logs nginx
docker service logs -f nginx                # 实时跟踪
docker service logs --tail 100 nginx        # 只看最后 100 行
docker service logs -f --timestamps nginx   # 带时间戳
docker service logs --since 10m nginx       # 最近 10 分钟
docker service logs nginx.1                 # 只看某个副本
docker service logs <TaskID>                # 只看某个 Task

注意:docker service logs 要求节点使用支持读取的日志驱动(json-file、journald、local)。若 daemon.json 中把日志驱动改成了 gelf、fluentd 等,该命令会报 configured logging driver does not support reading,此时需到日志平台查看。

详见 08-04-ELK日志分析.md

进入正在运行的容器(需在容器所在节点上执行):

docker ps
docker exec -it <containerID> /bin/bash

3.3 扩缩容

两种等价写法:

# 写法一:scale 子命令(可同时操作多个服务,最直观)
docker service scale nginx=6
docker service scale nginx=2 ge-service=3

# 写法二:update --replicas
docker service update --replicas 2 nginx

源文档中的示例:

docker service update --replicas 2 nginx
docker service scale nginx=6

扩容后可用 docker service ps 观察新副本被调度到哪些节点:

docker service scale ge-service=2
docker service ps ge-service
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE                ERROR               PORTS
vdy2v9s1tev0        ge-service.1        ge-service:v1.0.0   node1               Running             Running 4 minutes ago
dhzfd3jses4t        ge-service.2        ge-service:v1.0.0   node2               Running             Running about a minute ago

global 模式的服务不能 scale(副本数由节点数决定),执行会报 scale can only be used with replicated or replicated-job mode。

3.4 验证 Swarm 故障自愈

Swarm 集群的一个重要特性是:当运行服务的容器停止时,集群会自动重启一个新的容器(准确说是新建一个 Task)。

在 node1 上查看容器:

docker ps
CONTAINER ID        IMAGE                                                                           COMMAND                  CREATED             STATUS              PORTS                                                                                                                                        NAMES
834feedec0b1        nginx@sha256:e71b1bf4281f25533cf15e6e5f9be4dac74d2328152edf7ecde23abc54e16c1c   "nginx -g 'daemon ..."   11 minutes ago      Up 11 minutes       80/tcp

手动杀掉该容器:

docker kill 834feedec0b1
834feedec0b1

再次查看服务任务:

docker service ps nginx
ID            NAME                             IMAGE         NODE   DESIRED STATE  CURRENT STATE           ERROR                        PORTS
j9fa09h7kegk  nginx.l9mveonpubd7dugdpbrraq1q3  nginx:latest  node1  Running        Running 2 minutes ago
1fgg4nr7pm9c  nginx.l0j2psjlfs3hoywwjzlk785c2  nginx:latest  node3  Running        Running 14 minutes ago
7hroiw0p5etj  nginx.5bq2l28mb6kdl88t4lzjdz6xy  nginx:latest  node2  Running        Running 14 minutes ago
i9t8rsif4aqz  nginx.l9mveonpubd7dugdpbrraq1q3  nginx:latest  node1  Shutdown       Failed 2 minutes ago    "task: non-zero exit (137)"

可以看到 node1 节点上的 nginx 容器被杀掉后,Swarm 集群又在 node1 上重新运行了一个新的容器。注意输出中同时存在:

  • 新建的 Task(j9fa09h7kegk,Running);
  • 失败的旧 Task(i9t8rsif4aqz,Shutdown / Failed,错误为 task: non-zero exit (137),137 = 128 + 9,即被 SIGKILL)。

进阶验证(节点级故障自愈):直接关闭 node3 的 Docker 或关机

# 在 node3 上
systemctl stop docker

# 在 Manager 上观察
docker node ls              # node3 变为 Down
docker service ps nginx     # node3 上的 Task 变为 Shutdown,并在其他节点新建 Task 补足副本

3.5 docker service update:滚动更新

update 可以修改服务的几乎任意属性,Swarm 会按更新策略分批替换 Task。

不改变服务运行参数,仅滚动更新镜像版本:

docker service update --image nginx:1.9 nginx

常用更新操作:

# 更新镜像(最常见)
docker service update --image nginx:1.9 nginx

# 更新副本数
docker service update --replicas 5 nginx

# 增删环境变量
docker service update --env-add LOG_LEVEL=debug nginx
docker service update --env-rm  LOG_LEVEL nginx

# 增删端口
docker service update --publish-add 8443:443 nginx
docker service update --publish-rm  8443 nginx

# 增删网络
docker service update --network-add mynet nginx
docker service update --network-rm  mynet nginx

# 增删挂载
docker service update --mount-add type=bind,src=/data/html,dst=/usr/share/nginx/html nginx
docker service update --mount-rm  /usr/share/nginx/html nginx

# 增删调度约束与节点标签约束
docker service update --constraint-add 'node.role == worker' nginx
docker service update --constraint-rm  'node.role == worker' nginx

# 调整资源与更新策略
docker service update --limit-memory 512M --reserve-memory 256M nginx
docker service update --update-parallelism 2 --update-delay 15s nginx

# 私有仓库镜像更新务必带认证
docker service update --with-registry-auth --image registry.example.com/team/web:v2 web

--force 强制重调度:在服务定义没有任何变化时强制重建所有 Task。典型用途:

docker service update --force nginx
  • 节点从 drain 恢复 active 后,让副本重新在集群中分布(rebalance);
  • 镜像 tag 没变但仓库中的镜像内容已更新(如 latest),强制重新拉取并重建;
  • 挂载的宿主机配置文件已变化,需要重启容器加载。

更新过程的观察:

# 另开一个终端持续观察 Task 的新旧镜像交替
watch -n 1 'docker service ps web --filter "desired-state=running"'

# 观察更新状态机
docker service inspect web --pretty | grep -A10 UpdateStatus
docker service inspect -f '{{json .UpdateStatus}}' web

UpdateStatus.State 的取值:updating(正在更新)、paused(因失败暂停)、completed(更新完成)、rollback_started、rollback_completed。

3.6 docker service rollback:回滚

docker service rollback nginx

回滚会把服务恢复到 PreviousSpec(上一次更新前的定义),包括镜像、环境变量、资源限制等。要点:

  • 只能回滚一步:Swarm 只保存一个历史版本,连续两次 rollback 等于在两个版本间来回切换;
  • 回滚同样是滚动进行的,遵循 --rollback-parallelism / --rollback-delay;
  • --update-failure-action rollback 会在更新失败时自动触发回滚,无需人工介入;
  • 回滚前可以先确认上一版本是什么:
docker service inspect -f '{{.PreviousSpec.TaskTemplate.ContainerSpec.Image}}' nginx

3.7 删除服务

docker service rm nginx
docker service rm $(docker service ls -q)     # 删除全部服务

rm 会立即停止并删除所有 Task 与容器,不做优雅滚动,且无法撤销。删除服务不会删除其使用的网络与卷。

3.8 滚动更新与回滚完整实验

3.8.1 准备两个版本的镜像

用最简单的方式制造两个可区分的版本(每个节点都要构建,或推到私有仓库):

mkdir -p /data/rollingv1 /data/rollingv2

# v1
cat > /data/rollingv1/Dockerfile <<'EOF'
FROM nginx:1.8
RUN echo "<h1>version 1</h1>" > /usr/share/nginx/html/index.html
EOF

# v2(正常版本)
cat > /data/rollingv2/Dockerfile <<'EOF'
FROM nginx:1.8
RUN echo "<h1>version 2</h1>" > /usr/share/nginx/html/index.html
EOF

# 在 node1、node2、node3 上分别构建(无私有仓库时必须每台都构建)
cd /data/rollingv1 && docker build -t myweb:v1 .
cd /data/rollingv2 && docker build -t myweb:v2 .
docker images | grep myweb

3.8.2 创建带更新策略的服务

docker service create --name myweb \
  --replicas 6 \
  --network microservice \
  --publish 8080:80 \
  --update-parallelism 2 \
  --update-delay 15s \
  --update-monitor 10s \
  --update-failure-action rollback \
  --update-order start-first \
  --rollback-parallelism 3 \
  myweb:v1

docker service ls
docker service ps myweb
curl http://1.1.1.3:8080/
<h1>version 1</h1>

3.8.3 执行更新并观察分批过程

# 终端 A:持续观察
watch -n 1 'docker service ps myweb --filter "desired-state=running"'

# 终端 B:执行更新
docker service update --image myweb:v2 myweb

更新过程中会看到:

overall progress: 2 out of 6 tasks
1/6: running   [==================================================>]
2/6: running   [==================================================>]
3/6: preparing [=================================>                 ]
...
verify: Service converged

可观察到的现象:

  • 每次只有 2 个 Task 被替换(--update-parallelism 2);
  • 每批之间间隔 15 秒(--update-delay 15s);
  • docker service ps 中新旧镜像并存,旧 Task 逐步变为 Shutdown;
  • 更新期间 curl 会交替返回 version 1 与 version 2,直到全部替换完成;
  • 更新完成后:
docker service ls
ID            NAME    MODE        REPLICAS  IMAGE
xxxxxxxxxxxx  myweb   replicated  6/6       myweb:v2
curl http://1.1.1.3:8080/
<h1>version 2</h1>

3.8.4 模拟更新失败触发自动回滚

制造一个「启动即失败」的坏版本:

mkdir -p /data/rollingbad
cat > /data/rollingbad/Dockerfile <<'EOF'
FROM nginx:1.8
RUN echo "<h1>bad version</h1>" > /usr/share/nginx/html/index.html
# 故意写一个错误的启动命令,让容器立刻退出
CMD ["nginx-not-exist"]
EOF

cd /data/rollingbad && docker build -t myweb:bad .   # 三个节点都构建

执行更新到坏版本:

docker service update --image myweb:bad myweb

观察输出,Swarm 会在失败后自动回滚:

overall progress: rolling back update: 1 out of 6 tasks
...
service rolled back: rollback completed
docker service ps --no-trunc myweb

可以看到坏版本的 Task 状态为 Failed,ERROR 列显示类似:

task: non-zero exit (127)   或   starting container failed: exec: "nginx-not-exist": executable file not found in $PATH

而服务镜像已自动回到 myweb:v2:

docker service ls
docker service inspect -f '{{json .UpdateStatus}}' myweb
{"State":"rollback_completed","Message":"rollback completed"}

如果把 --update-failure-action 设为默认的 pause,更新会停在失败处不再继续,服务处于新旧混杂状态,UpdateStatus.State 为 paused,此时必须人工介入(手工回滚或修好镜像后再 update)。

3.8.5 手工回滚

# 先看看上一个版本是什么
docker service inspect -f '{{.PreviousSpec.TaskTemplate.ContainerSpec.Image}}' myweb

# 手工回滚
docker service rollback myweb

# 确认
docker service ls
curl http://1.1.1.3:8080/
<h1>version 1</h1>

也可以直接用 update 指定旧镜像达到同样效果(更可控,推荐生产使用):

docker service update --image myweb:v1 myweb

3.9 Overlay 网络实验

3.9.1 创建 attachable 的 overlay 网络

Swarm 默认的 ingress 网络不支持服务发现,因此需要重新创建一个 overlay 网络:

docker network create --attachable --driver overlay microservice
rjxxb5gz6gbx2bc50077k9uby

踩坑说明:Swarm 默认的 ingress 网络不支持服务发现,会出现服务无法被调用的情况,因此需要自行创建一个 --attachable 的 overlay 网络。--attachable 的作用是允许普通 docker run 启动的独立容器也接入该 overlay 网络(否则只有 service 的 Task 能接入),这对后面用普通容器方式运行 Consul、做联通性测试都是必需的。

其他常用选项:

# 指定子网与网关
docker network create -d overlay --attachable \
  --subnet 10.10.0.0/24 --gateway 10.10.0.1 microservice

# 启用数据面加密(IPSec,需放行 IP 协议号 50/ESP,有性能损耗)
docker network create -d overlay --attachable --opt encrypted secure-net

查看集群网络:

docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
0aa88eeb09b2        bridge              bridge              local
38c8f825c2a8        docker_gwbridge     bridge              local
2df53c0f2885        host                host                local
sce8abmnyz7j        ingress             overlay             swarm
rjxxb5gz6gbx        microservice        overlay             swarm
2e8597069347        none                null                local

另一次实验的输出(网络 ID 不同,结构一致):

NETWORK ID          NAME                DRIVER              SCOPE
600ee39432e7        bridge              bridge              local
a7134bf47409        docker_gwbridge     bridge              local
b52001389024        host                host                local
jr8szahfn7un        ingress             overlay             swarm
zk5m8j0p55gc        microservice        overlay             swarm
315c13982c22        none                null                local

Swarm 集群搭建完成。

注意 SCOPE 列:swarm 作用域的网络是集群级的,local 是节点本地的。overlay 网络在首个使用它的容器被调度到某节点时才会在该节点上真正创建,所以在 worker 上执行 docker network ls 一开始可能看不到自建的 overlay 网络,这是正常现象。

查看网络详情,确认哪些容器接入了该网络:

docker network inspect microservice
docker network inspect -f '{{json .Containers}}' microservice

3.9.2 跨节点容器互访验证

在 node1 和 node3 上各起一个接入 microservice 网络的普通容器(依赖 --attachable):

# 在 node1
docker run -dit --name box1 --network microservice busybox sh

# 在 node3
docker run -dit --name box3 --network microservice busybox sh

查看各自在 overlay 网络中的 IP:

docker exec box1 ip addr | grep 'inet 10.'
docker exec box3 ip addr | grep 'inet 10.'

跨节点 ping(node1 上的 box1 直接 ping node3 上的 box3,用容器名):

docker exec box1 ping -c 3 box3
PING box3 (10.0.1.5): 56 data bytes
64 bytes from 10.0.1.5: seq=0 ttl=64 time=0.652 ms
64 bytes from 10.0.1.5: seq=1 ttl=64 time=0.531 ms
64 bytes from 10.0.1.5: seq=2 ttl=64 time=0.498 ms

ping 不通时的排查方向:4789/UDP 是否被防火墙拦截、7946 TCP+UDP 是否互通、两个容器是否在同一个 overlay 网络。

3.9.3 服务名 DNS 解析验证

创建一个服务,然后从同网络的容器中用服务名访问:

docker service create --name web --replicas 3 --network microservice nginx:1.8

# 在 node1 的 box1 中解析并访问
docker exec box1 nslookup web
docker exec box1 wget -qO- http://web/

nslookup 输出的是服务的 VIP(一个稳定的虚拟 IP,不是某个容器的 IP):

Server:    127.0.0.11
Address 1: 127.0.0.11

Name:      web
Address 1: 10.0.1.2

查看服务 VIP:

docker service inspect -f '{{json .Endpoint.VirtualIPs}}' web

解析所有 Task 的真实 IP(tasks.<服务名> 特殊域名,返回列表):

docker exec box1 nslookup tasks.web
Name:      tasks.web
Address 1: 10.0.1.3 web.1.xxxxxxxx.microservice
Address 2: 10.0.1.4 web.2.yyyyyyyy.microservice
Address 3: 10.0.1.5 web.3.zzzzzzzz.microservice

对比 dnsrr 模式(服务名直接返回所有 Task IP,无 VIP):

docker service create --name web-dnsrr --replicas 3 \
  --network microservice --endpoint-mode dnsrr nginx:1.8
docker exec box1 nslookup web-dnsrr

3.9.4 Routing Mesh 验证

创建一个 ingress 模式发布端口、但副本数少于节点数的服务:

docker service create --name mesh-test --replicas 1 -p 8081:80 nginx:1.8
docker service ps mesh-test        # 记下副本实际在哪个节点,假设是 node2

从三个节点分别访问,全部都能返回页面(包括没有副本的 node1 和 node3):

curl -I http://1.1.1.3:8081/
curl -I http://1.1.1.4:8081/
curl -I http://1.1.1.5:8081/
HTTP/1.1 200 OK
Server: nginx/1.8.1
...

这就是 Routing Mesh:所有节点都监听已发布端口,请求被转发到真正运行副本的节点。可以在任意节点上确认端口确实被监听:

netstat -lntp | grep 8081
ss -lntp | grep 8081

再验证负载均衡效果(扩容到 3 副本,反复请求可看到不同副本的 hostname):

docker service update --replicas 3 mesh-test
for i in $(seq 1 10); do curl -s http://1.1.1.3:8081/ | grep -o 'version [0-9]'; done

对比 host 模式(只有运行副本的节点才监听端口):

docker service create --name host-test --replicas 1 \
  -p published=8082,target=80,mode=host nginx:1.8
docker service ps host-test           # 假设落在 node2
curl -I http://1.1.1.4:8082/          # 200 OK
curl -I http://1.1.1.3:8082/          # Connection refused

Routing Mesh 不生效的常见原因:4789/UDP 不通、ingress 网络异常(可删除并重建)、使用了 mode=host、--endpoint-mode dnsrr。重建 ingress 网络的方法:

docker network rm ingress            # 需先删除所有使用 ingress 的服务
docker network create --driver overlay --ingress \
  --subnet 10.255.0.0/16 --gateway 10.255.0.1 ingress

3.10 Stack 部署

3.10.1 部署与管理命令

# 部署(或更新)一个 stack
docker stack deploy -c docker-compose.yml mystack

# 私有仓库镜像必须带认证
docker stack deploy -c docker-compose.yml --with-registry-auth mystack

# 部署时先解析 Compose 文件中的变量(如 ${TAG})
docker stack deploy --resolve-image always -c docker-compose.yml mystack

# 删除 stack 中已从文件移除的服务(保持文件与集群完全一致)
docker stack deploy -c docker-compose.yml --prune mystack

# 查看
docker stack ls                       # 列出所有 stack 及服务数量
docker stack services mystack         # 列出该 stack 的服务(等价于带过滤的 service ls)
docker stack ps mystack               # 列出该 stack 所有 Task 及所在节点
docker stack ps --no-trunc mystack    # 排错时看完整错误
docker stack rm mystack               # 删除整个 stack(服务、网络一并删除,卷保留)

docker stack deploy 会自动:

  • 给服务名加前缀:mystack_web、mystack_db;
  • 创建带前缀的 overlay 网络:mystack_backend;
  • 创建带前缀的卷:mystack_dbdata;
  • 打上标签 com.docker.stack.namespace=mystack,便于按 stack 过滤。

3.10.2 Compose v3 的 deploy 段完整配置

deploy 段只对 docker stack deploy 生效,docker-compose up 会忽略它。

version: "3.8"

services:
  web:
    image: nginx:1.8
    deploy:
      # 服务模式:replicated(默认)或 global
      mode: replicated
      replicas: 6

      # 端点模式:vip(默认)或 dnsrr
      endpoint_mode: vip

      # 服务与容器标签
      labels:
        com.example.description: "web frontend"

      # 调度约束与分散偏好
      placement:
        constraints:
          - node.role == worker
          - node.labels.disk == ssd
          - node.platform.os == linux
        preferences:
          - spread: node.labels.zone
          - spread: node.labels.rack
        max_replicas_per_node: 2

      # 资源限制(limits)与资源预留(reservations)
      resources:
        limits:
          cpus: "1.0"
          memory: 1G
        reservations:
          cpus: "0.5"
          memory: 512M

      # 重启策略
      restart_policy:
        condition: on-failure      # none | on-failure | any
        delay: 10s
        max_attempts: 3
        window: 120s

      # 滚动更新策略
      update_config:
        parallelism: 2
        delay: 15s
        failure_action: rollback   # continue | rollback | pause
        monitor: 10s
        max_failure_ratio: 0.2
        order: start-first         # stop-first | start-first

      # 回滚策略
      rollback_config:
        parallelism: 3
        delay: 5s
        failure_action: pause      # continue | pause
        monitor: 10s
        max_failure_ratio: 0
        order: stop-first

3.10.3 v3 中被 Swarm 忽略的 Compose 选项

用 docker stack deploy 部署时,以下单机 Compose 选项会被直接忽略(部署时可能给出 Ignoring unsupported options 警告):

被忽略的选项 原因与替代方案
build(及 dockerfile、args) Swarm 不在集群里构建镜像。必须先本地 docker build 并 push 到镜像仓库,Compose 文件里只写 image
depends_on Swarm 无启动顺序编排;即使写了 condition: service_healthy 也无效。应用需自己实现重试等待(如启动脚本轮询数据库端口)
links 已废弃,overlay 网络内直接用服务名通信
container_name Task 容器名由 Swarm 生成,不能自定义
restart 被 deploy.restart_policy 取代
external_links 不支持
network_mode 不支持(用 networks 与 overlay 网络)
cgroup_parent、devices、tmpfs(部分版本)、security_opt、userns_mode Swarm 模式不支持或支持受限
cpu_shares、cpu_quota、cpuset、mem_limit、memswap_limit 被 deploy.resources 取代
extends v3 不再支持

这也是 Swarm 与单机 Compose 的关键区别:Swarm 里没有「等数据库起来再起应用」这回事,必须让应用自身具备重连能力。K8s 的 initContainer / readinessProbe 才提供了更完整的编排能力。

3.10.4 完整的多服务 Stack 示例

/data/stack/docker-compose.yml,包含 Web、数据库、网络与卷:

version: "3.8"

services:
  web:
    image: registry.example.com/team/ge-service:v1.0.0
    ports:
      - target: 8080
        published: 8080
        protocol: tcp
        mode: ingress
      - target: 8838
        published: 8838
        protocol: tcp
        mode: ingress
    environment:
      - SERVICE_8838_NAME=ge-service
      - SERVICE_PORT=8838
      - TZ=Asia/Shanghai
      - DB_HOST=db
      - DB_PORT=3306
      - DB_NAME=ge_data
      - DB_USER=ge_data
    secrets:
      - source: db_password
        target: db_password
        mode: 0400
    configs:
      - source: app_conf
        target: /usr/local/tomcat/conf/app.properties
    networks:
      - microservice
    healthcheck:
      test: ["CMD-SHELL", "curl -fs http://localhost:8080/ || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 60s
    deploy:
      mode: replicated
      replicas: 3
      placement:
        constraints:
          - node.role == worker
        preferences:
          - spread: node.labels.zone
      resources:
        limits:
          cpus: "1.0"
          memory: 1G
        reservations:
          cpus: "0.5"
          memory: 512M
      restart_policy:
        condition: on-failure
        delay: 10s
        max_attempts: 3
      update_config:
        parallelism: 1
        delay: 20s
        failure_action: rollback
        monitor: 30s
        order: start-first
      rollback_config:
        parallelism: 2
        delay: 10s

  db:
    image: mysql:5.7
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
    environment:
      - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db_root_password
      - MYSQL_DATABASE=ge_data
      - MYSQL_USER=ge_data
      - MYSQL_PASSWORD_FILE=/run/secrets/db_password
      - TZ=Asia/Shanghai
    secrets:
      - db_root_password
      - db_password
    volumes:
      - dbdata:/var/lib/mysql
    networks:
      - microservice
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints:
          # 有状态服务必须钉在固定节点上,否则换节点后本地卷数据就「不见了」
          - node.labels.role == db
      resources:
        limits:
          memory: 2G
        reservations:
          memory: 1G
      restart_policy:
        condition: any
        delay: 10s

networks:
  microservice:
    driver: overlay
    attachable: true
    # 若要复用已经手工创建的网络,改为:
    # external: true

volumes:
  dbdata:
    driver: local
    # 生产环境建议使用共享存储,例如 NFS:
    # driver_opts:
    #   type: "nfs"
    #   o: "addr=1.1.1.10,rw,nfsvers=4"
    #   device: ":/export/mysql"

secrets:
  db_password:
    external: true
  db_root_password:
    external: true

configs:
  app_conf:
    file: ./app.properties

部署与验证:

cd /data/stack
docker stack deploy -c docker-compose.yml --with-registry-auth ge
docker stack ls
docker stack services ge
docker stack ps --no-trunc ge
docker service logs -f ge_web
docker stack rm ge

3.10.5 Stack 与直接 service create 的取舍

维度 docker stack deploy docker service create
配置管理 YAML 文件,可入 Git 做版本管理、Code Review 一长串命令行,难以复现与审计
多服务应用 一次部署一整套(含网络、卷、secrets、configs) 需要逐个手工创建,顺序与命名靠人保证
声明式 / 幂等 是,重复 deploy 收敛到文件描述的状态;--prune 可清理多余服务 命令式,create 已存在会报错,需改用 update
微调单个参数 需改文件再整体 deploy docker service update 一条命令,立即生效
临时试验 略繁琐 更快
CI/CD 集成 天然适合(改 tag → deploy) 需要拼装命令
与开发环境一致 同一份 v3 文件可用于 docker-compose up 无法复用

建议:生产环境一律用 Stack + Compose 文件管理,service create/update 只用于临时验证与紧急处置。

3.11 configs 与 secrets

3.11.1 secrets:敏感数据

secret 用于分发密码、私钥、证书等敏感数据。数据在 Raft 日志中加密存储,只下发给真正需要它的 Task 所在节点,并以 tmpfs(内存文件系统) 挂载到容器的 /run/secrets/<name>,容器停止即从节点上消失。

# 从文件创建
docker secret create db_password /root/db_password.txt

# 从标准输入创建(避免密码落盘,推荐)
printf 'yHfaEurUbT6DPWwG' | docker secret create db_password -
echo -n "MySuperSecret" | docker secret create db_root_password -

# 查看(只能看到元数据,看不到内容)
docker secret ls
docker secret inspect db_password
docker secret inspect --pretty db_password

# 删除(被服务使用中的 secret 无法删除)
docker secret rm db_password
ID                          NAME               CREATED             UPDATED
u2b5xkq9vfd7c1nqz3lhu1m0k   db_password        2 minutes ago       2 minutes ago

在服务中使用 secret:

# 简写:挂载到 /run/secrets/db_password
docker service create --name db --secret db_password \
  -e MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db_password mysql:5.7

# 完整写法:指定目标文件名、属主与权限
docker service create --name db \
  --secret source=db_password,target=mysql_pwd,uid=999,gid=999,mode=0400 \
  -e MYSQL_ROOT_PASSWORD_FILE=/run/secrets/mysql_pwd mysql:5.7

# 给已有服务增删 secret(会触发滚动更新)
docker service update --secret-add db_password web
docker service update --secret-rm  db_password web

在容器内验证:

docker exec -it <containerID> ls -l /run/secrets/
docker exec -it <containerID> cat /run/secrets/db_password
docker exec -it <containerID> mount | grep secrets
tmpfs on /run/secrets type tmpfs (ro,relatime)

secret 不可修改,轮换密码的标准做法是「新建 → 更新服务 → 删除旧的」:

printf 'NewPassword123' | docker secret create db_password_v2 -
docker service update --secret-rm db_password --secret-add db_password_v2 db
docker secret rm db_password

3.11.2 configs:非敏感配置文件

config 与 secret 用法几乎一致,区别是不加密存储,且可以挂载到容器内任意路径,适合分发 nginx.conf、application.yml 这类配置文件,免去做镜像或 bind mount 的麻烦。

docker config create nginx_conf /data/nginx.conf
docker config ls
docker config inspect --pretty nginx_conf
docker config rm nginx_conf

# 在服务中挂载到指定路径
docker service create --name web \
  --config source=nginx_conf,target=/etc/nginx/nginx.conf,mode=0444 \
  -p 8080:80 nginx:1.8

# 给已有服务增删 config
docker service update --config-add source=nginx_conf,target=/etc/nginx/nginx.conf web
docker service update --config-rm nginx_conf web

3.11.3 相比环境变量传密码的优势

对比项 -e MYSQL_ROOT_PASSWORD=xxx 环境变量 --secret
是否可见 docker inspect、docker service inspect、/proc/<pid>/environ 都能看到明文;子进程继承 只在容器内的 tmpfs 文件中,不进环境变量
集群存储 明文写入 Raft 日志与服务定义 加密存储于 Raft
落盘风险 会出现在 shell history、CI 日志、Compose 文件、镜像层中 通过标准输入创建可完全不落盘
分发范围 服务定义全集群可见 只下发给运行该 Task 的节点
容器停止后 — tmpfs 内容随即消失
权限控制 无 可设置 uid/gid/mode
轮换 需改服务定义,密码历史留存 新建 secret + update,旧的可删除

源文档中 -e MYSQL_ROOT_PASSWORD=yHfaEurUbT6DPWwG、grant all on *.* to 'ge_data'@'%' identified by '123' 这类写法在教学演示中可以接受,生产环境应改用 secret(MySQL、Postgres 等官方镜像都支持 *_FILE 形式的环境变量来读取 secret 文件)。

四、综合实验:Swarm + Consul + Registrator 服务发现

本章实验来自两份源文档,用于说明「Swarm 内置服务发现之外,如何对接外部注册中心,让集群外的 Nginx/网关也能感知容器的上下线」。

该实验涉及服务注册中心与微服务治理,属于微服务体系内容。 详见 阶段14(微服务与服务治理)

方案构成:

  • 服务自动发现:Consul
  • 服务注册:Registrator
  • 动态负载均衡:微博开源的 Nginx 模块 nginx-upsync-module

涉及技术栈:

Docker + Consul + Registrator + Nginx

相关官网:

4.1 部署 Consul 集群与 Server 高可用

官方建议在运行 Docker 容器时使用 host 网络模式,因为一致性协议和 gossip 对延迟比较敏感。本例使用前面创建的 microservice overlay 网络(这也是必须加 --attachable 的原因)。

4.1.1 在 node1 上启动 Consul Server

docker run -d -p 8300:8300 -p 8301:8301/tcp -p 8302:8302/tcp -p 8301:8301/udp \
-p 8302:8302/udp -p 8500:8500 -p 8600:8600/udp -p 8600:8600/tcp \
--restart=always \
-h node1 \
--name consul \
--network microservice \
-v /data/consul:/consul/data \
-v /etc/localtime:/etc/localtime:ro \
consul agent \
-server \
-bootstrap-expect=2 \
-node=node1 \
-rejoin \
-client 0.0.0.0 \
-advertise 1.1.1.3 \
-ui

旁注(源文档 04 的 2 节点差异):04 中 node1 的 Consul 只作为单个 server 启动,因此 -bootstrap-expect=1(其余参数完全相同)。3 节点主线用 -bootstrap-expect=2,表示等待 2 个 server 就绪后再选举 Leader。

4.1.2 在 node2 上启动 Consul Server

与 node1 的命令相比,node2 不启用 -ui,并通过 -join 1.1.1.3 加入集群:

docker run -d -p 8300:8300 -p 8301:8301/tcp -p 8302:8302/tcp -p 8301:8301/udp \
-p 8302:8302/udp -p 8500:8500 -p 8600:8600/udp -p 8600:8600/tcp \
--restart=always \
-h node2 \
--name consul \
--network microservice \
-v /etc/localtime:/etc/localtime:ro \
consul agent \
-server \
-node=node2 \
-rejoin \
-client 0.0.0.0 \
-join 1.1.1.3 \
-advertise 1.1.1.4

旁注(源文档 04):在 2 节点方案中,node2 上的 Consul 以 agent(client) 身份运行,即去掉 -server 参数,其余参数与上面完全一致:

docker run -d -p 8300:8300 -p 8301:8301/tcp -p 8302:8302/tcp -p 8301:8301/udp \
-p 8302:8302/udp -p 8500:8500 -p 8600:8600/udp -p 8600:8600/tcp \
--restart=always \
-h node2 \
--name consul \
--network microservice \
-v /etc/localtime:/etc/localtime:ro \
consul agent \
-node=node2 \
-rejoin \
-client 0.0.0.0 \
-join 1.1.1.3 \
-advertise 1.1.1.4

4.1.3 在 node3 上启动 Consul Client

node3 上不加 -server,以 client 身份加入:

docker run -d -p 8300:8300 -p 8301:8301/tcp -p 8302:8302/tcp -p 8301:8301/udp \
-p 8302:8302/udp -p 8500:8500 -p 8600:8600/udp -p 8600:8600/tcp \
--restart=always \
-h node3 \
--name consul \
--network microservice \
-v /etc/localtime:/etc/localtime:ro \
consul agent \
-node=node3 \
-rejoin \
-client 0.0.0.0 \
-join 1.1.1.3 \
-advertise 1.1.1.5

Consul 常用参数含义:

参数 说明
agent 启动 Consul agent 进程
-server 以 server 身份运行(参与 Raft 共识);不加则为 client
-bootstrap-expect=<N> 期望的 server 数量,达到该数量后开始选举 Leader
-node=<名称> 该节点在 Consul 集群中的名称
-rejoin 重启后自动重新加入之前的集群
-client 0.0.0.0 客户端 API/DNS 监听地址,0.0.0.0 表示允许外部访问
-advertise <IP> 向集群其他成员公告的地址
-join <IP> 启动时加入指定节点所在的集群
-ui 启用内置 Web UI(8500 端口)

4.1.4 查看 Consul 集群节点

在 node1 上执行:

docker exec consul consul members

3 节点主线的输出(2 server + 1 client):

Node   Address       Status  Type    Build  Protocol  DC   Segment
node1  1.1.1.3:8301  alive   server  1.4.4  2         dc1  <all>
node2  1.1.1.4:8301  alive   server  1.4.4  2         dc1  <all>
node3  1.1.1.5:8301  alive   client  1.4.4  2         dc1  <default>

旁注(源文档 04 的 2 节点输出,1 server + 1 client):

Node   Address       Status  Type    Build  Protocol  DC   Segment
node1  1.1.1.3:8301  alive   server  1.5.1  2         dc1  <all>
node2  1.1.1.4:8301  alive   client  1.5.1  2         dc1  <default>

4.2 基于 Registrator 实现服务注册

Registrator 负责监听本机 Docker 事件,自动把容器注册到 Consul。需要在每个节点上各运行一个(node1、node2、node3 都要执行)。

写法一(--volume 挂载 docker.sock,源文档 03):

docker run -d \
    --restart=always \
    --name=registrator \
    --net=host \
    --volume=/var/run/docker.sock:/tmp/docker.sock \
    -v /etc/localtime:/etc/localtime:ro \
    doubleshit/registrator:v7 \
    -resync 60 \
    -cleanup \
    -internal \
    -ip 1.1.1.3 \
    -useIpFromNetworkName=microservice \
    consul://1.1.1.3:8500

在 node2、node3 上执行时,将 -ip 1.1.1.3 分别替换为对应节点的 IP(1.1.1.4、1.1.1.5)。

写法二(--mount type=bind 挂载 docker.sock,源文档 04;两种写法等价,推荐 --mount 显式写法):

node1:

docker run -d \
--restart=always \
--name=registrator \
--net=host \
--mount type=bind,src=/var/run/docker.sock,dst=/tmp/docker.sock \
-v /etc/localtime:/etc/localtime:ro \
doubleshit/registrator:v7 \
-resync 60 \
-cleanup \
-internal \
-ip 1.1.1.3 \
-useIpFromNetworkName=microservice \
consul://1.1.1.3:8500

node2:

docker run -d \
--restart=always \
--name=registrator \
--net=host \
--mount type=bind,src=/var/run/docker.sock,dst=/tmp/docker.sock \
-v /etc/localtime:/etc/localtime:ro \
doubleshit/registrator:v7 \
-resync 60 \
-cleanup \
-internal \
-ip 1.1.1.4 \
-useIpFromNetworkName=microservice \
consul://1.1.1.4:8500

注意末尾 Consul 地址的差异:源文档 03 中三个节点统一注册到 consul://1.1.1.3:8500(都指向 node1 的 Consul);源文档 04 中 node2 注册到本机 Consul consul://1.1.1.4:8500。两种都可行——后者依赖本机 Consul agent 转发到 server,是官方更推荐的做法(本地 agent 模式)。

4.2.1 Registrator 参数说明

参数 作用
-cleanup 清理悬空服务
-resync <seconds> 频率,所有服务重新同步;默认值 0,表示从不
-internal 使用公开端口而不是已发布端口(注册 Docker 内部 IP 和端口,而非主机映射的端口)
-ip <ip address> 强制用于注册服务的 IP 地址
-useIpFromLabel <label> 使用存储在给定标签中的 IP 地址(分配给容器)向 Consul 注册
-useIpFromNetworkName <网络名> 使用容器在指定网络(这里是 microservice)中的 IP 注册

默认情况下注册服务时,Registrator 会通过尝试解析当前主机名来分配服务地址;如果要强制使用特定地址,可指定 -ip 参数。

4.3 验证集群与服务

整个集群搭建完成。可以通过浏览器访问 Consul UI 观察运行的服务:

http://1.1.1.3:8500

也可以通过 API 在终端查看服务列表:

curl http://1.1.1.3:8500/v1/catalog/services
{"consul":[],"consul-8300":[],"consul-8301":["udp"],"consul-8302":["udp"],"consul-8500":[],"consul-8600":["udp"]}

查看某个服务的详细信息:

curl http://1.1.1.3:8500/v1/catalog/service/<服务名>

查看健康的服务实例(Nginx upsync 用的就是这个接口):

curl http://1.1.1.3:8500/v1/health/service/ge-service

五、综合实验:在 Swarm 上部署 ge-service 微服务

ge-service 是一个基于 tomcat-8.5 构建的 Java 项目,依赖 mysql-5.7,并由 Nginx 为其做负载均衡。

5.1 部署微服务注意事项

部署业务微服务时需注意:

  1. 暴露端口与内部端口必须一致;
  2. 网络必须指定为同一个 overlay 网络,如上文创建的 --network microservice。

5.2 在 node1 运行 mysql-5.7

docker run -d -p 3306:3306 \
--name micro-mysql \
--restart=always \
-v /data/docker/mysql/micro-mysql:/var/lib/mysql \
-v /data/docker/mysql_conf_d:/etc/mysql/conf.d \
-v /etc/localtime:/etc/localtime:ro \
-e MYSQL_ROOT_PASSWORD=yHfaEurUbT6DPWwG \
mysql:5.7 \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_unicode_ci

在宿主机安装 mysql 客户端,以便连接容器中的数据库:

yum install -y mysql

这里的数据库是以普通容器(而非 service)方式运行并固定在 node1 上的,正是因为数据库是有状态服务,本地卷不跨节点共享。生产环境的正确做法见第六章「有状态服务在 Swarm 上的困难」。

详见 阶段05(数据库与MySQL)

5.3 准备业务文件

将 Java 项目包、Dockerfile、Tomcat 配置文件、SQL 脚本上传到 node1 和 node2 的 /data 目录下:

ls /data
Dockerfile  ge_data.sql  platform.war  server.xml

5.4 创建数据库、授权并导入数据

mysql -uroot -p123 -h1.1.1.3
create database ge_data;
grant all on *.* to 'ge_data'@'%' identified by '123';
flush privileges;
use ge_data;
source /data/ge_data.sql;

5.5 编写 Dockerfile

/data/Dockerfile 内容如下:

FROM tomcat:8.5
MAINTAINER TX tx971429121@163.com
ADD platform.war /usr/local/tomcat/webapps/platform.war
ADD server.xml /usr/local/tomcat/conf/server.xml
ENV LC_ALL=en_US.UTF-8
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US.UTF-8
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
ENTRYPOINT ["catalina.sh","run"]
EXPOSE 8838

5.6 在各节点上构建镜像

docker build -f Dockerfile -t ge-service:v1.0.0 .
Sending build context to Docker daemon  73.76MB
Step 1/11 : FROM tomcat:8.5
...
Successfully built d860a4d41ff0
Successfully tagged ge-service:v1.0.0

重要:Swarm 不会在集群内构建或分发镜像。这里必须在每个可能运行副本的节点上都执行一遍 docker build(源文档在 node1、node2 上都构建了),否则 Task 调度到没有该镜像的节点时会因 No such image 而失败。生产环境的正确做法是构建后 docker push 到私有仓库,并在创建/更新服务时加 --with-registry-auth。

详见 13-02-Docker私有仓库与Compose.md

5.7 在管理节点创建 service

在 node1(管理节点)上运行:

docker service create \
--name ge-service \
--replicas 1 \
--publish 8080:8080 \
--publish 8838:8838 \
--network microservice \
--mount type=bind,src=/etc/localtime,dst=/etc/localtime,readonly \
--env SERVICE_8838_NAME=ge-service \
--env SERVICE_PORT=8838 \
--env SERVICE_3306_IGNORE=true \
--env SERVICE_80_IGNORE=true \
--env SERVICE_443_IGNORE=true \
--env SERVICE_9000_IGNORE=true \
--env SERVICE_9001_IGNORE=true \
ge-service:v1.0.0

其中:

  • SERVICE_8838_NAME=ge-service:注册到 Consul 的服务名为 ge-service;
  • SERVICE_PORT=8838:注册端口为 8838;
  • 多条 SERVICE_xxx_IGNORE=true:忽略 Tomcat 镜像暴露的其他端口,避免它们被注册。

这些 SERVICE_* 环境变量是 Registrator 约定的元数据,Registrator 读取容器环境变量来决定注册的服务名、端口与忽略项,与 Swarm 本身无关。

查看所有服务:

docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
uak7xc4laygv        ge-service          replicated          1/1                 ge-service:v1.0.0   *:8080->8080/tcp, *:8838->8838/tcp

5.8 扩容服务到 2 个副本

docker service scale ge-service=2

查看服务任务分布:

docker service ps ge-service
ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE                ERROR               PORTS
vdy2v9s1tev0        ge-service.1        ge-service:v1.0.0   node1               Running             Running 4 minutes ago
dhzfd3jses4t        ge-service.2        ge-service:v1.0.0   node2               Running             Running about a minute ago

此时到 Consul 中应能看到 ge-service 有 2 个健康实例。

六、Nginx + upsync 实现动态负载均衡

利用微博开源的 nginx-upsync-module,在扩容或缩容时可以自动把容器的 IP 和端口注册到 Nginx 的 upstream 中,无需 reload Nginx。

本节属于负载均衡与反向代理内容,涉及 Nginx 编译安装与第三方模块。 详见 12-01-Nginx与LVS负载均衡.md

6.1 下载 Nginx 与 upsync 模块

wget http://nginx.org/download/nginx-1.16.0.tar.gz
tar xf nginx-1.16.0.tar.gz
git clone https://github.com/onecer/nginx-upsync-module.git

6.2 安装编译环境与依赖

yum install -y pcre pcre-devel zlib zlib-devel openssl openssl-devel gcc gcc-c++
cd nginx-1.16.0/

6.3 配置、编译并安装

./configure --with-http_stub_status_module --with-http_ssl_module --with-http_v2_module \
--with-http_gzip_static_module --with-http_gunzip_module --with-stream --with-stream_ssl_module \
--with-http_sub_module --with-http_flv_module --with-http_addition_module --with-http_realip_module \
--with-http_mp4_module --with-stream --with-stream_ssl_module \
--add-module=../nginx-upsync-module

make && make install

6.4 创建软链接与运行用户

ln -s /usr/local/nginx/sbin/nginx /usr/sbin/nginx
groupadd nginx && useradd -r -s /sbin/nologin -g nginx nginx

6.5 配置 systemd 管理 Nginx

新增 /usr/lib/systemd/system/nginx.service:

[Unit]
Description=The NGINX HTTP and reverse proxy server
After=syslog.target network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/var/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/usr/sbin/nginx -s reload
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target

注意:这里的 PID 文件路径要与 nginx.conf 中的 pid 指令保持一致。

systemd 为 CentOS 7 机制。CentOS 6 请改用 SysV 风格的 /etc/init.d/nginx 脚本,配合 chkconfig nginx on 与 service nginx start 管理。

详见 阶段06(服务管理与启动流程)

6.6 修改 Nginx 配置文件

编辑 /usr/local/nginx/conf/nginx.conf,通过 upsync 指令从 Consul 拉取 ge-service 的健康节点列表:

user  nginx;
pid        /var/run/nginx.pid;
http {
  upstream ge-service {
          server 127.0.0.1:11111;
          upsync 127.0.0.1:8500/v1/health/service/ge-service upsync_timeout=6m upsync_interval=500ms upsync_type=consul_health strong_dependency=off;
          upsync_dump_path /usr/local/nginx/conf/servers/servers_ge-service.conf;
  }
  server {
    listen       80;
    server_name  1.1.1.3;

    location / {
      proxy_pass http://ge-service/;
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_set_header X-NginX-Proxy true;
      proxy_redirect off;
    }
  }
}

upstream 中的占位 server 127.0.0.1:11111 用于占位启动,真正的后端列表由 upsync 从 Consul 动态拉取并 dump 到 upsync_dump_path 指定的文件中。

upsync 指令参数含义:

参数 说明
upsync <consul地址>/v1/health/service/<服务名> 从 Consul 健康检查接口拉取实例列表
upsync_timeout=6m 长轮询超时时间
upsync_interval=500ms 拉取间隔
upsync_type=consul_health 后端类型:consul / consul_services / consul_health / etcd
strong_dependency=off 启动时是否强依赖注册中心;off 表示拉不到也允许 Nginx 启动
upsync_dump_path <路径> 把动态 upstream 持久化到文件,Nginx 重启后可直接加载

6.7 创建所需目录并启动 Nginx

mkdir /usr/local/nginx/conf/servers
systemctl daemon-reload && systemctl start nginx

6.8 验证容器信息是否注册到 Nginx

cat /usr/local/nginx/conf/servers/servers_ge-service.conf

扩容到 2 个副本后可以看到两个节点:

server 1.1.1.4:8838 weight=1 max_fails=2 fail_timeout=10s;
server 1.1.1.3:8838 weight=1 max_fails=2 fail_timeout=10s;

6.9 缩容验证动态负载均衡

将服务缩减到 1 个副本,检验 Nginx 是否能动态更新 upstream:

docker service scale ge-service=1

再次查看 dump 出的 upstream 信息:

cat /usr/local/nginx/conf/servers/servers_ge-service.conf
server 1.1.1.3:8838 weight=1 max_fails=2 fail_timeout=10s;

可以看到,集群能够自动把当前运行的容器信息注册/反注册到 Nginx 的 upstream 中,扩容与缩容都无需手动 reload Nginx。

补充说明:如果只需要「从集群外部访问服务并自动负载均衡」,其实 Swarm 内置的 Routing Mesh 就够了(把外部 LB 指向任意几个节点的固定端口即可)。引入 Consul + Registrator + upsync 的价值在于:能拿到每个容器实例的真实地址做更精细的 L7 负载均衡(会话保持、权重、灰度),并把服务实例信息共享给其他微服务组件使用。

七、生产实践

7.1 Manager 规划与不在 Manager 上跑业务

  • Manager 数量取 3 或 5(奇数),不要用 2 或 4;小于 10 个节点的集群用 3 个 Manager 足够。
  • Manager 不承担业务负载。Manager 默认也是可调度的 Worker,一旦业务容器把 Manager 的 CPU/内存/磁盘 IO 吃满,Raft 心跳与选举就会超时,整个集群管理面瘫痪。两种做法:
# 做法一(推荐):把 Manager 的可用性设为 drain,彻底不接受业务任务
docker node update --availability drain node1
docker node update --availability drain node2

# 做法二:为业务服务统一加约束,只调度到 worker
docker service create --name web --replicas 3 \
  --constraint 'node.role == worker' nginx:1.8

注意:drain 只影响新任务的调度,Manager 上原有的业务任务会被驱逐;而 Swarm 自身的系统任务(如 ingress 网络组件)不受影响。

与之相对,全局的基础设施服务(日志/监控 agent)通常需要覆盖 Manager,可以用 --mode global 并显式允许在 drain 节点运行(global 服务同样受 drain 影响,若必须覆盖 Manager 则该 Manager 不能 drain,此时应改用做法二)。

  • Manager 之间应有低延迟稳定网络(同机房),Raft 对网络抖动敏感;不要把 Manager 跨广域网部署。
  • Manager 的 /var/lib/docker/swarm 建议放在 SSD 上,Raft 是同步写日志的。

7.2 跨可用区/机架分布

给节点打上位置标签,用 --placement-pref 让副本均匀分散,用 --constraint 排除不该去的节点:

docker node update --label-add zone=az1 node1
docker node update --label-add zone=az2 node2
docker node update --label-add zone=az3 node3

docker service create --name web --replicas 6 \
  --constraint 'node.role == worker' \
  --placement-pref 'spread=node.labels.zone' \
  nginx:1.8

Manager 也应尽量跨 3 个可用区各放 1 个,这样任何单个可用区故障都不会让集群失去 quorum。但注意跨可用区的网络延迟必须足够低(同城,RTT < 10ms 级别)。

7.3 镜像仓库的准备

Swarm 不会在集群内构建也不会分发镜像,这是新手最容易踩的坑(本文第五章的实验就必须在每个节点手工 build)。生产环境标准流程:

# ① 在构建机上构建并打上带版本号的 tag(不要用 latest)
docker build -t registry.example.com/team/web:v1.0.3 .

# ② 推送到私有仓库
docker login registry.example.com
docker push registry.example.com/team/web:v1.0.3

# ③ 在 Manager 上登录并部署,务必带 --with-registry-auth
docker login registry.example.com
docker service create --name web --replicas 3 \
  --with-registry-auth registry.example.com/team/web:v1.0.3

# ④ 后续发版
docker service update --with-registry-auth \
  --image registry.example.com/team/web:v1.0.4 web

要点:

  • 禁用 latest tag:Swarm 会把 tag 解析成镜像 digest 固定下来,用 latest 会导致「明明推了新镜像但服务没更新」或不同副本运行不同代码;
  • 私有仓库需要 HTTPS,自签证书要在每个节点的 daemon.json 配 insecure-registries 或安装 CA;
  • 大镜像首次拉取慢,可考虑仓库就近部署或预热(在各节点提前 docker pull);
  • --with-registry-auth 下发的凭据有时效,凭据过期后新调度的 Task 会拉取失败,需重新 docker login 并 docker service update --with-registry-auth --force。

详见 13-02-Docker私有仓库与Compose.md

7.4 有状态服务在 Swarm 上的困难

核心问题:Swarm 的本地卷不跨节点共享,且没有 K8s 的 StatefulSet / PV / PVC 机制。

困难 表现
卷不跟随 Task 迁移 副本从 node1 迁到 node2 后,node2 上是一个空的本地卷,数据「消失」(数据其实还在 node1,只是容器看不到)
无稳定网络标识 没有 StatefulSet 那样的 pod-0、pod-1 固定序号与稳定 DNS,主从集群难以固定拓扑
无有序启停 副本并发启动,无法保证「先起主库再起从库」
缺少存储编排 没有 CSI 标准,卷插件生态薄弱,扩容/快照等能力缺失

实践建议(按优先级):

  1. 数据库不要轻易上 Swarm。MySQL、Redis、Kafka、Elasticsearch 等有状态服务建议部署在物理机/虚拟机上,或使用云厂商托管服务,由 Swarm 里的应用通过网络访问。
  2. 如果一定要跑,使用外部共享存储:NFS、GlusterFS、Ceph RBD、云盘,通过卷插件挂载,保证副本换节点后仍能读到同一份数据。
# NFS 卷示例
docker volume create --driver local \
  --opt type=nfs \
  --opt o=addr=1.1.1.10,rw,nfsvers=4 \
  --opt device=:/export/mysql mysqldata

docker service create --name db --replicas 1 \
  --mount type=volume,src=mysqldata,dst=/var/lib/mysql mysql:5.7
  1. 用约束把有状态服务钉死在固定节点(配合该节点的本地盘),副本数只能是 1:
docker node update --label-add role=db node3
docker service create --name db --replicas 1 \
  --constraint 'node.labels.role == db' \
  --mount type=volume,src=dbdata,dst=/var/lib/mysql mysql:5.7
  1. 需要真正的有状态编排能力时,迁移到 Kubernetes。

详见 13-04-Kubernetes集群.md

7.5 日志集中收集

容器日志分散在各节点,必须集中收集:

# 方式一:Docker 日志驱动直接送到日志平台(daemon.json 全局配置)
cat > /etc/docker/daemon.json <<'EOF'
{
  "registry-mirrors": ["https://registry.docker-cn.com"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "3"
  }
}
EOF
systemctl restart docker

# 方式二:为单个服务指定日志驱动
docker service create --name web \
  --log-driver gelf --log-opt gelf-address=udp://1.1.1.10:12201 nginx:1.8

# 方式三(推荐):保留 json-file,用 global 模式部署 Filebeat 采集
docker service create --name filebeat --mode global \
  --mount type=bind,src=/var/lib/docker/containers,dst=/var/lib/docker/containers,readonly \
  --mount type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock,readonly \
  --mount type=bind,src=/data/filebeat.yml,dst=/usr/share/filebeat/filebeat.yml,readonly \
  docker.elastic.co/beats/filebeat:7.6.0

务必配置 max-size / max-file 做日志轮转,否则 json-file 日志会把节点磁盘写满,导致所有容器异常。注意:改用 gelf、fluentd 等驱动后 docker service logs 将不可用。

详见 08-04-ELK日志分析.md

7.6 监控方案

需要覆盖三个层次:

层次 关注指标 常用手段
节点 CPU、内存、磁盘、网络、Docker 进程存活 Zabbix Agent 或 node_exporter(--mode global 部署)
容器 每容器 CPU/内存/网络/重启次数 cAdvisor(--mode global)+ Prometheus
集群与服务 节点 Ready/Down、Manager 是否有 Leader、服务 REPLICAS 是否 N/N、Task 失败次数 Prometheus + Docker daemon metrics、自定义脚本巡检

关键告警项:

# 巡检脚本片段:任何节点非 Ready、任何服务副本不满,即告警
docker node ls --format '{{.Hostname}} {{.Status}}' | grep -v Ready
docker service ls --format '{{.Name}} {{.Replicas}}' | awk -F'[ /]' '$2!=$3'
docker info --format '{{.Swarm.ControlAvailable}}'

Docker Engine 暴露 Prometheus 指标(daemon.json):

{
  "metrics-addr": "0.0.0.0:9323",
  "experimental": true
}

详见 08-05-Zabbix监控.md

7.7 外部负载均衡器配合 Routing Mesh 的部署拓扑

推荐拓扑(两层负载均衡):

                        用户
                          │
                    公网 VIP(Keepalived)
                          │
            ┌─────────────┴─────────────┐
            ▼                           ▼
      HAProxy / Nginx (LB1)      HAProxy / Nginx (LB2)     ← 集群外,做 L7、SSL 卸载、限流
            │                           │
            └─────────────┬─────────────┘
                          │  后端指向所有 Swarm 节点的已发布端口
        ┌─────────────────┼─────────────────┐
        ▼                 ▼                 ▼
   node1:8080        node2:8080        node3:8080          ← Routing Mesh 在每个节点监听
        └─────────────────┴─────────────────┘
                          │  Swarm 内置 IPVS L4 负载均衡
                          ▼
                 服务的各个副本容器:80

HAProxy 配置示例:

frontend http-in
    bind *:80
    default_backend swarm_nodes

backend swarm_nodes
    balance roundrobin
    option httpchk GET /healthz
    server node1 1.1.1.3:8080 check inter 3000 rise 2 fall 3
    server node2 1.1.1.4:8080 check inter 3000 rise 2 fall 3
    server node3 1.1.1.5:8080 check inter 3000 rise 2 fall 3

要点:

  • 外部 LB 的后端只写节点 IP + 固定发布端口,与副本数量、副本位置完全解耦,扩缩容无需改 LB 配置;
  • 必须配健康检查,节点宕机时 LB 才能摘除(Routing Mesh 不会让宕机节点自动从 LB 消失);
  • 使用 ingress 模式会丢失真实源 IP,需要真实源 IP 时改用 mode=host + --mode global,或在 LB 层通过 X-Forwarded-For 传递;
  • LB 自身要做高可用(Keepalived VIP),否则 LB 成为新的单点。

详见 12-02-Haproxy负载均衡.md

7.8 TLS 证书轮换 docker swarm ca

Swarm 节点之间的所有通信都用 mTLS 加密,证书由集群内置 CA 签发,默认有效期 90 天并自动轮换。

# 查看当前 CA 与证书配置
docker swarm ca
docker info --format '{{json .Swarm.Cluster.Spec.CAConfig}}'

# 调整证书有效期(如 720 小时 = 30 天)
docker swarm update --cert-expiry 720h

# 轮换 CA(生成新的根 CA 并让所有节点换发证书,需要 --rotate)
docker swarm ca --rotate

# 使用自定义外部 CA 的根证书与私钥
docker swarm ca --rotate --ca-cert ca.pem --ca-key ca-key.pem

# 使用外部 CA 服务(CFSSL)
docker swarm ca --rotate --external-ca protocol=cfssl,url=https://ca.example.com

docker swarm ca --rotate 期间集群仍可用,Docker 会逐步给所有节点换发新证书,完成后旧 CA 失效(这也会使旧的 join token 失效,因为 token 里包含 CA 摘要)。CA 私钥泄露或有安全审计要求时应执行轮换。

7.9 自动锁定 --autolock 与解锁 key 管理

默认情况下,Manager 节点的 Raft 日志与 TLS 私钥在磁盘上是加密的,但解密密钥也存在磁盘上,因此偷走硬盘就能拿到集群全部 secrets。开启 autolock 后,解密密钥不再明文落盘,Docker 重启后必须人工提供 unlock key。

# 初始化时就开启
docker swarm init --advertise-addr 1.1.1.3 --autolock

# 对已有集群开启
docker swarm update --autolock=true
Swarm updated.
To unlock a swarm manager after it restarts, run the `docker swarm unlock`
command and provide the following key:

    SWMKEY-1-Xzu5vJmZ8sYqR3Fk2pT7wLnQ9dHbG4cA6eNjM0iVyU8

Please remember to store this key in a password manager, since without it you
will not be able to restart the manager.

Docker 重启后的解锁:

systemctl restart docker
docker node ls
Error response from daemon: Swarm is encrypted and needs to be unlocked before it can be used. Please use "docker swarm unlock" to unlock it.
docker swarm unlock
# 粘贴 unlock key 后回车

密钥管理命令:

docker swarm unlock-key              # 查看当前 unlock key(需集群已解锁)
docker swarm unlock-key -q           # 只输出 key
docker swarm unlock-key --rotate     # 轮换 unlock key
docker swarm update --autolock=false # 关闭 autolock

管理要点:

  • unlock key 必须存放在密码管理器/保险库中,绝不能只存在集群节点上,丢失后所有 Manager 重启即无法恢复(只能从备份重建);
  • 每个 Manager 重启后都要单独解锁,无人值守重启(如批量打补丁)前要有预案;
  • 轮换 unlock key 后旧 key 立即失效,注意同步更新文档与保险库;
  • 开启 autolock 的集群做 Raft 备份时,备份文件与 unlock key 要分开保存,否则加密就失去意义。

八、Swarm 的现状与向 Kubernetes 的迁移考虑

8.1 Swarm 的现状

  • 社区活跃度明显下降:自 2017 年前后 Kubernetes 成为事实标准后,Docker 公司自身也在产品中集成了 K8s,Swarm mode 的新特性开发基本停滞,功能处于「维护但不再演进」的状态。
  • 仍然可用且稳定:作为 Docker Engine 的内置能力,Swarm mode 没有被移除,Mirantis 收购 Docker 企业业务后也承诺继续支持。对小规模稳定业务,它足够可靠。
  • 仍适合小规模与教学:
    • 3~10 台机器、几十个无状态服务的内部系统;
    • 边缘/门店场景,运维人力有限,不适合维护 K8s;
    • 教学:用最少的概念讲清编排的本质(声明式副本、调度、服务发现、滚动更新)。
  • 最大优势是 Compose 文件可平滑用于 Swarm:同一份 v3 YAML,开发机 docker-compose up,集群 docker stack deploy,几乎零改造成本。K8s 则需要把整套配置翻译成 Deployment/Service/Ingress/ConfigMap/Secret 等清单(或引入 Helm、Kompose)。

8.2 Swarm 与 K8s 的概念对应关系(迁移对照表)

Swarm 概念 Kubernetes 对应 差异要点
Service(replicated) Deployment + ReplicaSet K8s 支持 HPA 自动扩缩容
Service(global) DaemonSet 语义几乎一致
Task Pod Pod 可含多个容器(sidecar),Task 只有一个
Container Container(Pod 内) —
--replicas spec.replicas —
--constraint nodeSelector / nodeAffinity / taints & tolerations K8s 表达能力强得多
--placement-pref spread podAntiAffinity / topologySpreadConstraints —
--limit-* / --reserve-* resources.limits / resources.requests 概念完全对应
overlay 网络 CNI 网络插件(Calico/Flannel) K8s 网络需自行选型
服务名 DNS + VIP Service(ClusterIP)+ CoreDNS 概念对应
Routing Mesh + -p Service(NodePort / LoadBalancer) NodePort 行为与 Routing Mesh 类似
— Ingress / Gateway API Swarm 无 L7 入口,需外接 Nginx/Traefik
docker secret Secret K8s Secret 默认仅 base64,需额外开启静态加密
docker config ConfigMap 概念对应
Stack + Compose v3 Namespace + 多份清单 / Helm Chart 迁移工作量主要在这里
无 StatefulSet / PV / PVC / CSI 有状态服务能力的关键差距
无 Job / CronJob Swarm 无原生定时任务
无 HPA / VPA Swarm 无自动扩缩容
无 RBAC / ServiceAccount Swarm 权限模型非常薄(谁能连 Docker socket 谁就是管理员)

8.3 迁移建议

  • 触发迁移的信号:需要自动扩缩容、需要有状态编排、需要精细的多租户与 RBAC、需要 L7 Ingress 与灰度发布、节点规模超过几十台、团队需要与云厂商生态对接。
  • 迁移路径:先把应用改造为「无状态 + 配置外置 + 日志输出到 stdout + 具备启动重试」,这些工作在 Swarm 阶段就该做好,做好之后迁到 K8s 的成本主要只是清单转换。
  • 辅助工具:kompose 可以把 Compose 文件转换成 K8s 清单作为起点,但生成结果需要人工调整(尤其是卷、探针、资源)。
  • 不要为了迁移而迁移:如果现有 Swarm 集群稳定、规模不大、团队 K8s 经验不足,继续用 Swarm 是合理选择。

详见 13-04-Kubernetes集群.md

九、排错清单

9.1 节点 join 失败

检查项 命令 / 处理
Manager 的 2377 端口是否可达 telnet 1.1.1.3 2377 或 nc -vz 1.1.1.3 2377;不通则检查 firewalld/iptables/安全组
7946 TCP+UDP、4789 UDP 是否放行 firewall-cmd --list-all;只放行 2377 时能 join 但随后节点会 Down
token 是否正确/过期 重新在 Manager 执行 docker swarm join-token worker;被 --rotate 或 CA 轮换后旧 token 立即失效
节点是否已在其他集群 报 This node is already part of a swarm,先 docker swarm leave --force
时间是否同步 报证书 not yet valid / has expired,先做 NTP 同步
Manager 是否处于 locked 状态 报需要 unlock,先 docker swarm unlock
Manager 是否失去 quorum Manager 自身 docker node ls 就报错,先恢复 quorum 再加节点
Docker 版本差异过大 统一各节点 Docker 版本
# 完整排查动作
systemctl status docker
journalctl -u docker -n 100 --no-pager
nc -vz 1.1.1.3 2377
docker swarm join-token worker

9.2 节点状态 Down 的排查

docker node ls                              # 确认哪个节点 Down
docker node inspect node3 --pretty          # 看最后状态与消息
# 到该节点上:
systemctl status docker
journalctl -u docker -n 200 --no-pager
可能原因 处理
该节点 Docker 已停止或主机宕机 启动 Docker / 修复主机
7946 TCP/UDP 被拦,控制面 gossip 中断 放行端口
节点负载过高(CPU 100%、磁盘写满),心跳超时 降负载、清理磁盘(docker system prune)
时间严重不同步 NTP 同步
--advertise-addr 公告了错误 IP(多网卡/换 IP) docker swarm leave --force 后带正确地址重新加入
节点已被销毁 Manager 上 docker node rm node3 清理

9.3 服务 REPLICAS 一直 0/3

第一动作永远是:

docker service ps --no-trunc <服务名>

按 ERROR 内容对症处理:

原因 ERROR 特征 处理
镜像拉不到 No such image、manifest unknown、pull access denied、Get https://... timeout 检查镜像名/tag;私有仓库加 --with-registry-auth;配置镜像加速;在节点手工 docker pull 验证
资源不足 no suitable node (insufficient resources on N nodes) 调低 --reserve-cpu/--reserve-memory,或扩容节点
约束无法满足 no suitable node (scheduling constraints not satisfied on N nodes) 检查 --constraint 拼写与节点标签:docker node inspect -f '{{.Spec.Labels}}' node2;确认目标节点不是 drain
端口冲突 port is already allocated、Bind for 0.0.0.0:8080 failed 换发布端口;host 模式下同一节点不能有两个副本占同一端口;ss -lntp \| grep 8080 查占用
所有节点都 drain no suitable node (2 nodes not available for new tasks) docker node update --availability active <节点>
挂载路径不存在 invalid mount config、bind source path does not exist 在所有候选节点创建该路径,或改用 volume
容器启动即退出 task: non-zero exit (1) docker service logs <服务名> 看应用日志

辅助命令:

docker service ps --no-trunc -f "desired-state=running" web
docker service ps --no-trunc -f "desired-state=shutdown" web
docker service inspect web --pretty
docker node ls
docker node ps <节点>

9.4 Task 反复 Rejected 或 Failed

状态 含义 常见原因
Rejected 节点拒绝执行 Task 挂载源路径不存在、SELinux 拒绝、磁盘满、卷插件不可用、镜像不存在于该节点
Failed 容器启动了但异常退出 启动命令错误、配置/依赖缺失、OOM(exit 137)、健康检查持续失败
Pending 一直无法调度 约束/资源/端口问题,见 9.3
反复 Starting → Failed 循环 应用崩溃循环 看应用日志;设 --restart-max-attempts 避免无限重启掩盖问题;设 --health-start-period 给慢启动应用宽限期
docker service ps --no-trunc web
docker service logs --tail 200 web
# 到出问题的节点上:
journalctl -u docker -n 200 --no-pager
df -h                       # 磁盘满是高频原因
free -m
docker system df
docker system prune -f      # 清理无用镜像/容器/网络,谨慎使用

9.5 Overlay 网络不通

检查项 命令 / 处理
4789/UDP 是否被拦(最高频原因) firewall-cmd --list-ports;用 tcpdump -i any -n udp port 4789 在两端抓包确认 VXLAN 报文
7946 TCP+UDP 是否互通 控制面不通会导致网络元数据不同步,容器 IP 分配异常
两个容器是否在同一 overlay 网络 docker network inspect microservice 查看 Containers 列表
网络是否 attachable 普通容器接入自建 overlay 需 --attachable,否则报 network is not manually attachable
--advertise-addr / --data-path-addr 是否正确 多网卡时数据面可能走了不可达网卡
内核参数 sysctl net.ipv4.ip_forward 应为 1;br_netfilter 模块是否加载 lsmod \| grep br_netfilter
MTU 问题 VXLAN 封装占 50 字节,底层 MTU 1500 时应用发大包可能不通(表现为小包正常、大包卡住)。创建网络时指定 --opt com.docker.network.driver.mtu=1450
云环境安全组 云主机需在安全组中放行 4789/UDP、7946/TCP+UDP、2377/TCP
# 验证脚本
docker exec box1 ping -c 3 box3
docker exec box1 ping -c 3 <box3的overlay IP>
docker network inspect microservice
ip -d link show | grep -A2 vxlan

9.6 Routing Mesh 不生效

现象 原因与处理
只有运行副本的节点能访问端口 用了 mode=host 发布端口,改为默认 ingress 模式
所有节点都访问不通 4789/UDP 不通;或 ingress 网络异常,删除并重建 ingress 网络
端口未监听 ss -lntp \| grep <端口> 确认;用 -p 而非 --expose;确认服务已 Running
使用 dnsrr 端点模式 dnsrr 不支持 ingress 发布端口,需改回 vip 或改用 mode=host
iptables 规则被清空 iptables -F 后 Docker 规则丢失,systemctl restart docker 重建
# 重建 ingress 网络(需先删除所有使用它的服务)
docker network inspect ingress
docker network rm ingress
docker network create --driver overlay --ingress \
  --subnet 10.255.0.0/16 --gateway 10.255.0.1 \
  --opt com.docker.network.driver.mtu=1200 ingress

9.7 服务名解析不到

检查项 说明
两个服务是否在同一个自建 overlay 网络 只共享 ingress 是不行的,ingress 不提供自定义服务发现(源文档踩坑点)
服务名是否写对 Stack 部署的服务真实名是 <stack名>_<服务名>;但同一 stack 内可以直接用短名
是否在容器内解析 宿主机不能解析服务名,必须 docker exec 进容器测试
DNS 是否正常 容器内 cat /etc/resolv.conf 应为 nameserver 127.0.0.11
服务是否有健康副本 0 个 Running 副本时 VIP 无后端,解析到 VIP 但连不上
endpoint-mode dnsrr 模式下解析返回多个 A 记录而非 VIP,客户端需支持
docker exec -it <容器> nslookup web
docker exec -it <容器> nslookup tasks.web
docker exec -it <容器> cat /etc/resolv.conf
docker service inspect -f '{{json .Endpoint.VirtualIPs}}' web

9.8 私有仓库镜像各节点拉取失败

# ① 在 Manager 上登录
docker login registry.example.com

# ② 创建/更新服务时必须带 --with-registry-auth
docker service create --with-registry-auth --name web registry.example.com/team/web:v1 --replicas 3
docker service update --with-registry-auth --image registry.example.com/team/web:v2 web

# ③ 若是自签证书仓库,每个节点都要配置并重启 Docker
cat /etc/docker/daemon.json
{
  "insecure-registries": ["registry.example.com:5000"]
}
systemctl restart docker

# ④ 在某个 worker 上手工验证能否拉取
docker pull registry.example.com/team/web:v1

9.9 Manager 失去 quorum 无法操作集群

现象:

docker node ls
Error response from daemon: rpc error: code = Unknown desc = The swarm does not have a leader. It's possible that too few managers are online. Make sure more than half of the managers are online.

恢复步骤(按优先级从轻到重):

# ① 首选:把故障的 Manager 修复上线,自然恢复 quorum
#    到故障 Manager 上:
systemctl start docker
journalctl -u docker -n 200 --no-pager
docker swarm unlock          # 若开启了 autolock

# ② 若 Manager 主机永久损毁且无法恢复,在仍存活的某个 Manager 上强制新建集群
docker swarm init --force-new-cluster --advertise-addr 1.1.1.3

# ③ 检查恢复结果:服务定义应完整保留,其他节点显示 Down
docker node ls
docker service ls

# ④ 清理损毁节点,重新补充 Manager 到 3 个
docker node rm node2
docker swarm join-token manager
# 在新机器上执行输出的 join 命令

# ⑤ 确认 Leader 已产生
docker node ls --filter role=manager
docker info --format '{{.Swarm.ControlAvailable}} {{.Swarm.Managers}} {{.Swarm.Nodes}}'

重点提醒:

  • 失去 quorum 期间,Worker 上已运行的容器不会被停掉,业务通常仍可提供服务,不要慌张地重启节点;
  • 不要在多个存活 Manager 上分别执行 --force-new-cluster,那会产生多个互不相识的「脑裂」集群;
  • 这就是为什么 Manager 必须是 3 或 5 个奇数、且要有 Raft 目录备份。

9.10 概念性陷阱:数据卷在不同节点上不共享

这是初学者最容易「丢数据」的地方,务必理解:

# 在 node1 上运行的 db 副本,写入本地卷
docker service create --name db --replicas 1 \
  --mount type=volume,src=dbdata,dst=/var/lib/mysql mysql:5.7

# node1 宕机 → Swarm 在 node2 新建 Task → node2 上自动创建一个**空的** dbdata 卷
# 结果:应用看到的是全新空数据库,历史数据「不见了」

真相:数据并没有丢,它还在 node1 的 /var/lib/docker/volumes/dbdata/_data 里,只是 node2 上的容器读不到。同理 --mount type=bind,src=/data/html 在没有该目录的节点上会挂载出空目录(或直接 Rejected)。

更新后容器在新节点起来但旧数据没了,也是同一个原因:docker service update(尤其带 --force)会重建 Task,重建时调度器可能选择另一个节点。

规避方式:

方式 做法
钉死节点 --constraint 'node.labels.role == db',配合该节点本地盘
共享存储 NFS / GlusterFS / Ceph / 云盘 卷插件,任意节点都能读到同一份数据
数据外置 数据库跑在 Swarm 之外,应用只通过网络访问(本文第五章的做法)
迁移到 K8s 使用 StatefulSet + PVC + CSI

验证陷阱的实验:

docker service create --name voltest --replicas 1 \
  --mount type=volume,src=testvol,dst=/data busybox sh -c "date >> /data/log; sleep 3600"
docker service ps voltest                      # 记下所在节点,假设 node2
docker node update --availability drain node2  # 逼它换节点
docker service ps voltest                      # 现在在 node3
# 在 node3 上:
docker exec <容器> cat /data/log               # 只有新写入的一行,node2 的历史记录不见了
# 在 node2 上:
ls /var/lib/docker/volumes/testvol/_data       # 数据其实还在这里
docker node update --availability active node2

9.11 防火墙与 Docker iptables 规则冲突

现象 原因与处理
重启 firewalld 后容器网络全部不通 firewalld 重载会清掉 Docker 的 DOCKER/DOCKER-USER 链,执行 systemctl restart docker 让 Docker 重建规则
iptables -F 后 Routing Mesh 失效 同上,重启 Docker
自定义 iptables 规则被 Docker 绕过 Docker 的 DNAT 规则在 nat 表 PREROUTING,先于 filter INPUT 生效。自定义规则要写到 DOCKER-USER 链,不要写在 INPUT
daemon.json 中 "iptables": false Docker 不再维护规则,overlay 与端口发布都会异常,除非你完全自行管理网络,否则不要设置
云安全组与本机防火墙双层限制 两层都要放行 2377/tcp、7946/tcp+udp、4789/udp
# 正确的自定义规则位置
iptables -I DOCKER-USER -s 10.0.0.0/8 -j ACCEPT
iptables -I DOCKER-USER -j DROP

# 查看 Docker 生成的规则
iptables -t nat -L -n --line-numbers | head -40
iptables -L DOCKER-USER -n

9.12 时间不同步导致 Raft 异常

现象 说明
节点 join 报 x509: certificate has expired or is not yet valid 节点时间比 Manager 快/慢太多,签发的证书在对方看来无效
Manager 之间频繁重新选举、MANAGER STATUS 在 Reachable/Unavailable 间跳动 Raft 选举超时判断依赖时间,时钟漂移会导致误判
节点无规律地变 Down 又恢复 心跳时间戳错乱
日志时间错乱,无法排错 各节点时区/时间不一致
# 检查所有节点时间是否一致
date; timedatectl                     # CentOS 7
chronyc sources -v; chronyc tracking  # CentOS 7
ntpq -p                               # CentOS 6

# 强制立即同步
chronyc -a makestep                   # CentOS 7
ntpdate -u ntp1.aliyun.com            # CentOS 6

# 统一时区
timedatectl set-timezone Asia/Shanghai    # CentOS 7
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime   # 通用

容器内时间同步:容器共享宿主机内核时钟,只需保证宿主机时间正确;容器内时区需要单独处理(挂载 /etc/localtime 或设置 TZ 环境变量,两份源文档的所有容器都挂载了 -v /etc/localtime:/etc/localtime:ro,正是为此)。

详见 08-03-时间同步与Xinetd.md

练习

两份源文档均以实验步骤形式呈现,未附独立习题。以下练习中,「实操题」部分对应源文档的完整实验流程,「问答题」为配套补充。

一、问答题

  1. 单机 Docker 有哪些无法克服的局限,容器编排解决了什么问题?

    答:无法横向扩展(受单机资源上限)、主机宕机即全部服务不可用(单点故障)、容器无跨主机自愈、跨主机容器网络不通、无统一调度、无声明式期望状态管理、发布无灰度与自动回滚、服务发现困难。编排系统把多台主机抽象成资源池,由它负责调度、跨主机网络、伸缩、健康检查、滚动更新与故障自愈,用户只声明期望状态。

  2. 独立 Swarm 与 Swarm Mode 的区别是什么?

    答:独立 Swarm(swarm-classic)是一个额外部署的代理程序,依赖外部 KV 存储(Consul/etcd/ZooKeeper)保存状态,只做容器调度,无服务/滚动更新/内置负载均衡,现已废弃。Swarm Mode 是 Docker 1.12+ 内置能力,自带 Raft 无需外部 KV,提供 service/task 抽象、声明式副本、滚动更新、Routing Mesh、secrets/configs,命令为 docker swarm / docker service / docker stack。

  3. 为什么 Manager 数量要是奇数,推荐 3 或 5?7 个 Manager 最多能容忍几个故障?

    答:Raft 需要多数派(quorum = floor(N/2)+1)才能写入状态,偶数个 Manager 的容错能力与少一个的奇数相同(2 与 1 都是 0,4 与 3 都是 1),却增加了故障面与 Raft 复制开销,并在网络分区时更易两边都无多数派。3 个可容忍 1 个故障,5 个可容忍 2 个,兼顾容错与性能,故推荐 3 或 5。7 个 Manager:quorum = 4,最多容忍 3 个故障。

  4. 描述 Service、Task、Container 的三层关系,为什么 docker service ps 会看到 Shutdown/Failed 的历史记录?

    答:Service 是用户声明的期望状态;Manager 按副本数把它拆成多个 Task,每个 Task 绑定到一个具体节点且终身不可迁移,状态机单向推进;每个 Task 对应一个 Container。Task 失败后不会被「重启迁移」,而是标记为 Failed/Shutdown 并由 Manager 新建一个 Task 来替代,因此历史 Task 会保留在输出中(数量由 --task-history-limit 控制),这也是查看失败原因的地方。

  5. replicated 与 global 两种模式有什么区别,各自适用什么场景?

    答:replicated 由 --replicas N 指定全集群总副本数,调度器决定分布,可多个副本落在同一节点,适用无状态业务服务;global 由 --mode global 指定,每个符合条件的节点各运行且仅运行 1 个副本,节点增减时自动增减,不能指定 --replicas、不能 scale,适用日志采集、监控 agent、网络/存储插件等「每机一份」的基础设施。

  6. Swarm 集群必须放行哪些端口?分别用于什么?

    答:2377/TCP 集群管理通信(join 连接的端口,Manager 监听);7946/TCP 和 UDP 节点间控制面 gossip(成员发现、网络元数据同步);4789/UDP overlay 数据面 VXLAN 业务流量;若启用 overlay 加密(--opt encrypted)还需放行 IP 协议号 50(ESP)。只放 2377 时能 join 成功但节点随后会 Down,只漏 4789 时节点 Ready 但 overlay 不通。

  7. --advertise-addr 在什么情况下必须指定?不指定会出现什么问题?

    答:多网卡、多 IP、有 NAT 或公私双网的机器必须显式指定,因为 Docker 无法判断该向其他节点公告哪个地址。不指定会直接报 could not choose an IP address to advertise since this system has multiple addresses,或公告了其他节点不可达的地址,导致节点显示 Ready 但 overlay 网络与 Routing Mesh 都不通。

  8. Routing Mesh 的原理是什么?ingress 与 host 两种端口发布模式有何差异?

    答:ingress 模式下 Swarm 在集群每个节点上都监听已发布端口,通过 iptables DNAT 把请求送入 ingress overlay 网络的服务 VIP,再由内核 IPVS 负载均衡到任意健康 Task,即使本节点没有副本也能处理请求。host 模式只在实际运行副本的节点上监听端口,请求直达本机副本,无内置负载均衡但保留真实源 IP、延迟更低,且同一端口每节点只能有 1 个副本。

  9. VIP 与 dnsrr 两种端点模式有什么区别?

    答:VIP(默认)为服务分配一个稳定虚拟 IP,服务名解析到该 VIP,内核 IPVS 在其后做 L4 负载均衡,客户端只见一个 IP;dnsrr 不分配 VIP,服务名的 DNS 查询直接返回所有 Task 的 IP 列表由客户端自行选择,适合客户端侧负载均衡或外接 L7 代理,但不能与 ingress 模式的端口发布同时使用(需配合 mode=host)。

  10. --limit-memory 与 --reserve-memory 有什么区别?为什么建议两者都写?

    答:--limit-* 是运行时上限(cgroup 强制约束容器最多能用多少,超内存触发 OOM Kill),不影响调度;--reserve-* 是调度预留,调度器只把 Task 放到剩余可预留资源足够的节点上,节点上所有 Task 的 reserve 之和不能超过节点总量。只写 limit 可能把大量容器挤到同一节点导致争抢;只写 reserve 容器可能吃满整机。建议两者都写且 limit ≥ reserve。

  11. --constraint 与 --placement-pref 的区别?

    答:--constraint 是硬约束,不满足就不调度(服务会停在 0/N,Task 为 Pending),基于 node.role / node.hostname / node.labels.* / engine.labels.*,操作符只有 == 和 !=,多个之间是 AND;--placement-pref spread=<标签> 是软偏好,让副本按标签维度尽量均匀分散,节点不足时仍会调度。

  12. --update-order 的 stop-first 与 start-first 分别适用什么场景?

    答:stop-first(默认)先停旧 Task 再起新 Task,适合端口或资源独占的场景(host 模式端口、独占卷);start-first 先起新 Task 并等其健康再停旧 Task,可做到零中断,但更新期间资源占用翻倍,且要求新旧副本能并存(端口不冲突)。

  13. 为什么使用私有仓库镜像时必须加 --with-registry-auth?

    答:docker login 的凭据只保存在执行命令的那台 Manager 的 ~/.docker/config.json 中,而镜像是由各个 Worker 节点自己拉取的,Worker 没有凭据。加该选项后 Manager 会把仓库认证信息随 Task 加密下发给目标节点。不加会导致服务卡在 0/N,docker service ps --no-trunc 显示 No such image 或 pull access denied。

  14. 用 docker stack deploy 部署时,Compose 文件中哪些选项会被忽略?为什么 depends_on 无效?

    答:被忽略的主要有 build(含 dockerfile/args)、depends_on、links、external_links、container_name、restart、network_mode、extends,以及被 deploy.resources 取代的 cpu_shares/mem_limit 等。Swarm 不在集群内构建镜像,必须先 build 并 push 到仓库;Swarm 也没有启动顺序编排能力,因此 depends_on 无效,应用必须自己实现连接重试(例如启动脚本轮询数据库端口)。

  15. 用 secret 传递密码相比环境变量有哪些优势?

    答:环境变量在 docker inspect、docker service inspect、/proc/<pid>/environ 中都是明文,会被子进程继承,且明文写入 Raft 与 shell history、CI 日志;secret 在 Raft 中加密存储,只下发给运行该 Task 的节点,以 tmpfs 挂载到 /run/secrets/<name>,容器停止即消失,可设置 uid/gid/mode,还能通过标准输入创建做到完全不落盘。轮换时新建 secret + update 即可。

  16. 为什么源文档要创建 --attachable 的 overlay 网络,而不用默认的 ingress?

    答:ingress 是 Swarm 保留的特殊 overlay 网络,只承载 Routing Mesh 的入口流量,不提供自定义服务发现,服务之间无法通过服务名互相调用;而且普通 docker run 的独立容器不能 attach 到它。创建 --attachable 的自建 overlay 网络后,service 与普通容器(如实验中的 Consul)都能接入同一网络,服务名 DNS 解析与跨节点互访才正常。

  17. Manager 失去 quorum 后集群会怎样?如何恢复?

    答:Worker 上已运行的容器不会被停掉,业务通常仍可访问,但集群无法做任何变更(不能创建/更新/删除服务、增删节点,docker node ls 报 The swarm does not have a leader)。恢复:优先修复故障 Manager 使其上线自然恢复 quorum(必要时 docker swarm unlock);若主机永久损毁,在仍存活的某一个 Manager 上执行 docker swarm init --force-new-cluster --advertise-addr <IP>,再 docker node rm 清理损毁节点并把 Manager 补回 3 个。切勿在多个 Manager 上分别执行该命令,会造成脑裂。

  18. 一个副本在节点间迁移后为什么可能「数据丢失」?如何避免?

    答:--mount type=volume 使用的 local 卷只存在于所在节点的 /var/lib/docker/volumes/ 下,副本换节点后会在新节点创建一个空卷;type=bind 在缺少该路径的节点上会挂到空目录或被 Rejected。数据并未真丢,只是新节点读不到。避免方式:用 --constraint 把有状态服务钉在固定节点、改用 NFS/GlusterFS/Ceph 等共享存储卷、把数据库放到 Swarm 之外,或迁移到 K8s 使用 StatefulSet + PVC。

  19. 服务 REPLICAS 一直显示 0/3,你的排查步骤是什么?

    答:第一步永远执行 docker service ps --no-trunc <服务名> 看 CURRENT STATE 与完整 ERROR。按错误分类:No such image / pull access denied → 镜像名错或缺 --with-registry-auth;insufficient resources → reserve 值过大;scheduling constraints not satisfied → 约束或节点标签问题、节点被 drain;port is already allocated → 端口冲突;invalid mount config → 挂载路径不存在;non-zero exit (1) → 用 docker service logs 看应用日志。

  20. Swarm 目前的现状如何?相比 Kubernetes 它最大的优势与最大的短板分别是什么?

    答:社区活跃度明显下降、功能基本冻结,但仍随 Docker Engine 提供且稳定可用,适合小规模(数节点到数十节点)与教学场景。最大优势是极低的学习与运维成本,以及同一份 Compose v3 文件可在开发机 docker-compose up、在集群 docker stack deploy,几乎零改造。最大短板是缺少有状态编排(无 StatefulSet/PV/PVC/CSI)、无自动扩缩容(HPA)、无 L7 Ingress、无 RBAC 权限模型,且生态与云厂商支持远不如 K8s。

二、实操题

  1. 按主线规划搭建 3 节点 Swarm 集群(node1/node2 为 Manager,node3 为 Worker),要求:统一 Docker 版本、配置 hosts 与时间同步、按端口清单精确放行防火墙(不允许直接关闭 firewalld),最后用 docker node ls 验证出现 1 个 Leader、1 个 Reachable、1 个 worker。

    参考答案:见 2.1(环境准备、端口清单)、2.2(安装 Docker)、2.3(docker swarm init --advertise-addr 1.1.1.3)、2.4(分别用 manager/worker token join)、2.5.1。

  2. 创建 --attachable 的 overlay 网络 microservice,在 node1 与 node3 各起一个 busybox 容器接入该网络,验证跨节点用容器名 ping 通。

    参考答案:见 3.9.1、3.9.2。

  3. 创建 3 副本的 nginx 服务并接入 microservice 网络、发布 80 端口,然后手工 docker kill 掉其中一个容器,用 docker service ps 观察故障自愈过程,解释输出中为什么同时存在 Running 与 Failed 的记录,以及 task: non-zero exit (137) 的含义。

    参考答案:见 3.1 与 3.4。137 = 128 + 9,表示容器被 SIGKILL 终止;Failed 的是旧 Task,Running 的是 Manager 新建的替代 Task(Task 不可迁移只能新建)。

  4. 构建 myweb:v1、myweb:v2、myweb:bad 三个镜像,创建带更新策略(--update-parallelism 2 --update-delay 15s --update-failure-action rollback --update-order start-first)的 6 副本服务,依次完成:滚动更新到 v2 并观察分批过程、更新到 bad 版本触发自动回滚、再手工回滚一次。

    参考答案:见 3.8 全节。

  5. 验证 Routing Mesh:创建只有 1 个副本、ingress 模式发布 8081 端口的服务,从三个节点分别 curl 均能成功;再创建 mode=host 的服务,验证只有运行副本的节点能访问。

    参考答案:见 3.9.4。

  6. 编写一个包含 Web 与 MySQL 的 Stack(v3 文件),要求:Web 3 副本只调度到 worker 并按 zone 分散、设置资源 limits/reservations、滚动更新失败自动回滚、健康检查带 start_period;MySQL 用 secret 传密码并用约束钉在带 role=db 标签的节点。部署后用 docker stack ps --no-trunc 验证。

    参考答案:见 3.10.4 完整示例、3.11.1 secret 用法。

  7. 节点维护演练:把 node3 置为 drain,确认其上任务被驱逐到其他节点并且服务副本数补齐,重启 node3 后置回 active,再用 docker service update --force 让副本重新分布。

    参考答案:见 2.5.4 的 drain 完整流程。

  8. 备份 Manager 的 Raft 数据,然后模拟灾难:停掉 node2、node3 的 Docker,在 node1 上用 --force-new-cluster 恢复集群,验证服务定义完整保留,再把两个节点重新加回来。

    参考答案:见 2.6 全节、9.9。

  9. 复现源文档实验:在 3 节点上部署 Consul 集群(node1/node2 为 server,node3 为 client)与 Registrator,创建 ge-service 服务并扩容到 2 副本,通过 curl http://1.1.1.3:8500/v1/catalog/services 与 Consul UI 确认服务已自动注册。

    参考答案:见第四章、第五章。

  10. 复现源文档的动态负载均衡实验:编译带 nginx-upsync-module 的 Nginx,配置从 Consul 健康接口拉取 ge-service 实例,先扩容到 2 副本再缩容到 1 副本,观察 servers_ge-service.conf 内容随之变化且无需 reload Nginx。

    参考答案:见第六章。

  11. 用 secret 替换 MySQL 的明文密码:创建 db_root_password secret,用 MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db_root_password 启动服务,在容器内验证 /run/secrets 是 tmpfs 挂载且 docker service inspect 中看不到明文密码;然后完成一次密码轮换。

    参考答案:见 3.11.1、3.11.3。

  12. 排错演练:分别人为制造下列故障并用 docker service ps --no-trunc 定位——① 写一个不存在的镜像 tag;② --reserve-memory 8G 超过节点内存;③ --constraint 'node.labels.disk == ssd' 但没有任何节点打此标签;④ 两个服务发布同一个宿主机端口。记录每种情况的 ERROR 文本。

    参考答案:见 9.3 的对照表。

  13. 阻断 4789/UDP(iptables -I INPUT -p udp --dport 4789 -j DROP),观察跨节点容器 ping 与 Routing Mesh 的表现,用 tcpdump -i any -n udp port 4789 确认 VXLAN 报文被丢弃,然后恢复规则。

    参考答案:见 9.5、9.6。

  14. 复现「卷不跨节点」陷阱:按 9.10 的实验脚本创建 voltest 服务写入数据,用 drain 逼它换节点,验证新节点上数据为空、旧节点上数据仍存在,并给出三种规避方案。

    参考答案:见 9.10。

  15. 开启 --autolock,重启 Manager 的 Docker,验证 docker node ls 报错需解锁,用 docker swarm unlock 恢复,随后轮换 unlock key 并最终关闭 autolock。

    参考答案:见 7.9。

  16. 按 2.7 的步骤完整解散集群,最后确认所有节点 docker info | grep -i swarm 均输出 Swarm: inactive。

    参考答案:见 2.7。

本章小结

常用命令速查表

集群管理

命令 作用
docker swarm init --advertise-addr <IP> 初始化集群,本节点成为 Manager/Leader
docker swarm join --token <token> <IP>:2377 加入集群
docker swarm join-token worker \| manager 查看加入命令
docker swarm join-token -q worker 只输出 token
docker swarm join-token --rotate worker 轮换 token
docker swarm leave [--force] 离开集群(Manager 需 --force)
docker swarm update --autolock=true 开启自动锁定
docker swarm unlock / unlock-key [--rotate] 解锁 / 查看或轮换 unlock key
docker swarm ca [--rotate] 查看 / 轮换集群 CA
docker swarm update --cert-expiry 720h 调整证书有效期
docker swarm init --force-new-cluster 灾难恢复:以现有 Raft 数据重建单节点集群
docker info \| grep -i swarm 查看 Swarm 状态

节点管理

命令 作用
docker node ls 列出节点(看 STATUS / AVAILABILITY / MANAGER STATUS)
docker node inspect <节点> --pretty 节点详情
docker node ps [节点] 查看某节点上的任务
docker node update --availability active\|pause\|drain <节点> 调整可用性
docker node update --label-add k=v <节点> 打标签
docker node promote / demote 角色升 / 降级
docker node rm [--force] <节点> 移除节点

服务管理

命令 作用
docker service create ... 创建服务
docker service ls 列出服务(看 REPLICAS 是否 N/N)
docker service ps --no-trunc <服务> 排错第一命令,看 CURRENT STATE 与完整 ERROR
docker service inspect <服务> --pretty 服务定义详情
docker service logs -f --tail 100 <服务> 聚合日志
docker service scale <服务>=<N> 扩缩容
docker service update --replicas <N> <服务> 扩缩容(等价写法)
docker service update --image <镜像> <服务> 滚动更新镜像
docker service update --force <服务> 强制重调度 / 重新平衡
docker service rollback <服务> 回滚一步
docker service rm <服务> 删除服务

Stack / 网络 / 密钥

命令 作用
docker stack deploy -c docker-compose.yml [--with-registry-auth] [--prune] <名> 部署/更新 Stack
docker stack ls / services / ps --no-trunc / rm Stack 查看与删除
docker network create --attachable -d overlay <名> 创建可附加的 overlay 网络
docker network ls / inspect 查看网络
docker secret create <名> - / ls / rm 管理 secret(挂载到 /run/secrets/<名>)
docker config create <名> <文件> / ls / rm 管理 config(可挂到任意路径)

关键选项速记

--name --replicas --mode global
-p 8080:80                        (ingress,Routing Mesh)
-p published=8080,target=80,mode=host   (host,保留源 IP)
--network / --endpoint-mode vip|dnsrr
-e / --env-file / --hostname / --user
--mount type=volume|bind|tmpfs,src=,dst=
--constraint 'node.role == worker'       (硬约束)
--placement-pref 'spread=node.labels.zone'(软偏好)
--limit-cpu/--limit-memory               (运行时上限)
--reserve-cpu/--reserve-memory           (调度预留)
--restart-condition/--restart-delay/--restart-max-attempts
--update-parallelism/--update-delay/--update-failure-action/--update-order
--rollback-parallelism
--health-cmd/--health-interval/--health-start-period
--with-registry-auth                     (私有仓库必带)

端口清单

端口 协议 用途 漏放的后果
2377 TCP 集群管理(join 目标端口) join 直接超时失败
7946 TCP + UDP 节点间控制面 gossip 能 join 但节点随后 Down,网络元数据不同步
4789 UDP overlay 数据面 VXLAN 节点 Ready 但跨节点不通、Routing Mesh 失效
50(ESP) IP 协议 overlay 加密(--opt encrypted)时需要 加密网络不通
firewall-cmd --permanent --add-port=2377/tcp
firewall-cmd --permanent --add-port=7946/tcp
firewall-cmd --permanent --add-port=7946/udp
firewall-cmd --permanent --add-port=4789/udp
firewall-cmd --permanent --add-protocol=esp
firewall-cmd --reload

排错口诀

一看 node ls        节点 Ready 否、有没有 Leader、AVAILABILITY 是不是 drain
二看 service ls     REPLICAS 是不是 N/N,0/N 立刻进下一步
三看 service ps     必带 --no-trunc,ERROR 里写着答案
四看 service logs   容器起来了但业务不对,就看应用日志
五看 端口防火墙      2377 / 7946 双协议 / 4789,云上还要看安全组
六看 时间同步        证书 not yet valid、节点乱 Down,先对时
七想 镜像在哪        每个节点都得有镜像,私有仓库别忘 --with-registry-auth
八想 数据在哪        本地卷不跨节点,换节点就是空目录
九查 iptables       iptables -F 之后重启 Docker 重建规则
十保 quorum         Manager 3 或 5 个,备份 /var/lib/docker/swarm

三条最容易踩的坑,重复强调:

  1. 端口没放全:只放 2377 不够,7946 要 TCP+UDP 两条,4789 是 UDP。
  2. 镜像不会自动分发:Swarm 不构建也不分发镜像,要么每个节点都 build,要么 push 到仓库并用 --with-registry-auth。
  3. 本地卷不跨节点:有状态服务要么钉死节点,要么用共享存储,要么放到 Swarm 之外。

与后续内容的衔接

  • 单机 Docker 基础与镜像构建 → 详见 13-01-Docker基础与进阶.md
  • 私有仓库与 Compose 单机编排 → 详见 13-02-Docker私有仓库与Compose.md
  • 更强大的编排平台 Kubernetes → 详见 13-04-Kubernetes集群.md
最后修改:2026 年 09 月 20 日
如果觉得我的文章对你有用,请随意赞赏