拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Docker Swarm服务部署与镜像管理实战:从私有仓库到滚动更新

Docker Swarm服务部署与镜像管理实战:从私有仓库到滚动更新

1. 为什么还要聊Docker Swarm的服务部署和镜像管理

你在搜“kafka集群安装”“redis哨兵模式”“hadoop集群搭建”这些关键词的时候,大概率已经被各种分布式系统的资料淹没了。但如果你用的是Docker生态,或者团队规模还不够上Kubernetes,那Docker Swarm其实是性价比最高的集群方案。我从很早开始就用Swarm管理生产环境,它没有Kubernetes那么复杂的控制面,但核心的**服务部署(service deployment)和镜像管理(image management)**都做得非常扎实。

聊集群绕不开一个基本事实:集群的日常工作中,80%以上的时间都在处理两件事——部署服务、更新镜像。这就像组织一个家庭,你以为大家讨论的是米其林餐厅指南,实际每天操心的是今天晚饭吃什么、冰箱里还有没有菜。Docker Swarm的service deployment解决的是“服务怎么跑、跑几个副本、挂了怎么办”,而image management解决的是“新版本怎么过去、旧版本怎么清理、镜像存哪里”。这两个环节理顺了,集群运维就成功了一大半。

这篇文章我按照自己在真实集群里的操作顺序来写:先把镜像仓库搭好,再讲服务部署的核心机制,然后重点拆解镜像更新的链路和坑,最后补上服务间的通信、资源约束和故障转移。你学完以后,再回头看热搜词里那些“集群故障转移”“集群调度”“gateway集群”,会发现很多概念在Swarm里都有对应物,一通百通。

适合谁看?刚搭好Swarm集群、不知道下一步怎么部署业务的人;已经在用Swarm但每次更新镜像都提心吊胆的人;以及学了Kubernetes但想先弄懂集群通识的人。目标只有一个:看完能直接在自己的环境里操作,不用再一边百度一边试错。

2. 私有镜像仓库是Swarm镜像管理的第一步

2.1 一个能用的registry需要几步

很多初学者把集群搭起来以后,第一个问题就是:镜像怎么分发给所有节点?生产环境绝对不能靠每个节点手动docker pull,因为Swarm的调度器会把容器随便调度到任意节点上,万一调度到一台没镜像的机器,就得现场拉取,网络一抖服务就起不来。

所以第一步永远是搭私有仓库。我自己常用的是轻量级的registry:2,复杂需求才上Harbor。一个最简可用的私有仓库,只需要一台机器加一条命令:

docker run -d -p 5000:5000 \ -v /opt/registry-data:/var/lib/registry \ --name registry \ --restart=always \ registry:2

但这一步有个极其隐蔽的坑:默认的registry只支持HTTP协议,而Docker守护进程默认只信任HTTPS仓库。如果你直接docker push 192.168.1.10:5000/myapp:1.0,大概率会收到一个类似http: server gave HTTP response to HTTPS client的报错。

解决办法是在每一台Swarm节点的/etc/docker/daemon.json里声明这个仓库是可信的HTTP仓库:

{ "insecure-registries": ["192.168.1.10:5000"] }

改完以后重启Docker:systemctl restart docker。注意,Swarm集群的所有节点都要改,不只是管理节点。我见过有人只改了manager节点,结果服务调度到worker上拉取镜像时直接失败。

2.2 镜像标记规范决定回滚体验

搞定了仓库,下一个问题就是镜像的tag怎么打。这句话听起来像废话,但我在实际集群里见过太多因为tag混乱导致发布事故的情况。

最典型的错误是只用latest。开发阶段用latest没问题,但生产环境一旦用了latest,你就会发现自己根本不知道当前跑的是哪个版本,更糟糕的是,某次发布后想回滚,却找不到上一个镜像到底长什么样。

我自己的规范非常简单:

  • 每次构建必须指定版本号,格式是项目名-主版本.次版本.修订号,比如order-service-1.2.3。
  • 只有需要给测试环境用的临时构建,才用dev-时间戳这种tag。
  • 任何一次生产构建,同时打好两个tag:order-service-1.2.3和order-service-latest。前者用于精确回溯,后者用于方便测试环境自动拉取最新版。

这套规范的好处在于,回滚时你只需要知道“上一版本是1.2.2”,然后一条docker service update命令就能切回去,不需要去仓库翻历史标签。

2.3 多集群场景下的镜像仓库选择

如果你只有一个Swarm集群,单机registry完全够用。但如果你和很多团队一样,同时管理着开发、测试、生产三套环境,甚至还有多个机房的集群,那建议直接用Harbor。

为什么?因为多集群意味着镜像派发的路径比较复杂,Security和配额管理必须跟上。Harbor自带镜像复制功能,可以配置从主仓库自动同步到其他机房的仓库,这样每个集群都从本地仓库拉镜像,跨机房带宽被吃满的问题就解决了。

我踩过的一个真实教训是:在双机房场景下,最初只部署了一个主仓库,结果另一机房的集群每次拉新镜像都要走公网,一个1GB的镜像能推半小时,发布窗口被拉得非常长。后来在每台带存储的manager节点上部署了Harbor的复制规则,让两个机房各自有完整镜像副本,发布时间从半小时降到了两分钟。

3. 服务部署的核心机制:看懂service、task、container三者的关系

3.1 service到底是个什么东西

很多人一上来就用docker service create,命令能跑通,但遇到问题就抓瞎,因为搞不清楚Swarm内部的组织方式。这里必须用一句话点破:service是一个声明,task是调度单元,container是最终运行的东西。

你可以把service想象成一张招聘需求:需要5个人(replicas=5),要求会某技能(image),工作时长24小时不间断(restart-policy)。Swarm看到这张需求后,会生成5个task——每个task就是一个“招聘名额”。然后调度器把这些task安排到各个节点上,每个task最终会启动一个container。

所以执行docker service create以后,你看到的不是“5个容器”,而是“定义了一个service,它正在努力把副本数维持在5”。这个区别用docker service ps命令能直观看到:

docker service ps my-service

输出里的每一行就代表一个task,状态可能依次是prepare、running、complete、failed、shutdown。我就是靠这个命令排查大多数集群问题的,比如某个task反复启动失败、某个task被调度到哪台节点、发布时哪个task还没有更新等。

3.2 常用但容易被忽略的参数

docker service create的常用参数,很多教程只会列出来,但不会告诉你每个参数在什么场景下必须加。我挑几个实战中最关键的:

参数作用我的使用建议
--replicas副本数至少2,否则谈不上高可用
--publish端口映射格式是宿主机端口:容器端口,支持TCP/UDP
--network接入网络必须让服务加入同一个overlay网络才能互通
--update-parallelism滚动更新并行数生产环境建议1,一个一个来
--update-delay每批更新的间隔比如30s,给服务留出启动检查时间
--restart-condition重启策略进程崩溃时自动拉起,默认就是any,别改成none
--env环境变量容器配置化的主要手段
--constraint调度约束结合节点label,控制服务跑在哪些机器上

还有一个很多老手都容易漏掉的细节:--update-delay不是等容器启动完再更新下一批,而是等上一批更新完、状态稳定后等这么久。理解这一点后,你就明白为什么不能只设--update-parallelism 5而不设delay,因为那等于同时把副本全换掉,一个更新脚本写错,整个服务就全毁了。

3.3 部署命令与发布流程

实际部署业务时,我的标准流程大概这样:

  1. 构建镜像并打好版本号,推送到私有仓库。
  2. 在manager节点上执行docker service create,先建服务:
    docker service create \ --name order-service \ --replicas 3 \ --network demo-network \ --publish 8080:8080 \ --update-parallelism 1 \ --update-delay 30s \ --restart-condition any \ registry.example.com/order-service-1.2.3
  3. 用docker service ls确认服务状态,用docker service ps order-service确认所有副本都跑起来了。

第一次部署还有一个容易忽视的操作:在调度周期内,Swarm不一定立即把容器铺到所有节点上。如果节点资源不足,新task会一直处于pending状态。这个时候docker service ls显示可能还是1/3,很多新手会以为命令没生效,反复重试。实际上只要等调度器腾出资源,或者把不必要的容器清理掉,task就会自动创建。

3.4 用service scale实现动态扩缩容

服务上线以后,扩容和缩容是每天的常规操作。Swarm不需要修改配置文件,直接一行命令:

docker service scale order-service=5

这个命令会在现有基础上把副本数扩到5,多出来的两个task由调度器决定跑在哪台节点上。同理,缩容就是把数字调小,多余的task会被优雅关闭。

我经常遇到的问题是:扩容后流量并没有均匀分布。这可能有两种原因:一是Swarm内置的负载均衡是基于VIP的,它会将流量均衡分发到该服务的所有后端副本上,但你在客户端看到的连接数可能不均衡,特别是长连接场景;二是你的应用本身有状态(比如Session),新扩的实例并不能立刻接收流量,因为客户端还是连到旧实例上。所以扩缩容之前,先想清楚业务是长连接还是短连接、是否有状态,这比命令本身重要得多。

4. 镜像更新链路:从push到滚动发布的完整拆解

4.1 更新流程中的两个隐含阶段

一个新镜像从推送到私有仓库,到服务里的容器真正用上它,中间要经过两个阶段:用户执行push,Swarm执行pull和recreate。这两个阶段经常被混为一谈,导致很多人认为“只要push完镜像,服务就会自动更新”——这是天大的误解。

事实是:你可以把镜像推到仓库,Swarm也不会主动关心这件事。要让服务更新,必须显式执行:

docker service update --image registry.example.com/order-service-1.3.0 order-service

这条命令背后的逻辑是:Swarm比较“当前定义”和“新定义”的镜像地址,发现不同后,开始按--update-parallelism指定的数量逐个创建新task、销毁旧task,直到所有副本都运行在新镜像上。

那有没有办法让push完镜像后自动更新?有,但不建议做得很激进。我见过两种做法:一种是用脚本定时执行docker service update,另一种是配合CI/CD流水线,在构建完成推送镜像后,自动调用manager节点的API触发更新。第二种是生产环境最推荐的方式,但要注意给--update-parallelism 1 --update-delay 20s留出足够的观察时间,否则自动化工具会把“发布到一半”误判为“发布失败”。

4.2 滚动更新的几个核心参数如何配合

滚动更新最怕的是“全部一起换”,所以--update-parallelism和--update-delay的组合是重中之重。

我的典型配置是:

docker service update \ --image registry.example.com/order-service-1.3.0 \ --update-parallelism 1 \ --update-delay 20s \ --update-order start-first \ order-service

这里有一个值得说道的参数:--update-order。默认值是stop-first,也就是先把旧容器停掉,再启动新容器。这会导致更新期间的短暂不可用。如果业务不能接受秒级抖动,可以改成start-first,先启动新容器、等它正常后再停旧容器。不过这会带来一个问题:新旧容器会在同一节点上短暂共存,端口不能冲突。解决方式通常是让容器直接使用Swarm内置的VIP访问方式,不映射到宿主机端口。

另外,--update-monitor参数决定了Swarm等待task健康确认的时间。如果你设置了--health-cmd(在service create时定义健康检查命令),Swarm会等新容器通过健康检查后才认为更新成功,然后才继续下一批。如果没有健康检查,那Swarm默认只要容器进程没退出就算成功。这也是为什么很多服务“看着更新完了,实际业务没起来”。

4.3 更新失败怎么办:回滚策略

发布之后出问题,最快的恢复方式是回滚,而不是重新构建一个修复版本。Swarm提供了非常人性的回滚命令:

docker service rollback order-service

这条命令会让服务回到更新前的镜像版本和参数配置。但注意一个前提:你过去的镜像tag必须还能拉取到。这就是为什么我在前面强调tag要带版本号,而不是随手覆盖latest。

我自己在更新脚本里通常会加一条保险逻辑:推送新镜像之前,先确认旧的镜像还在仓库里。比如用脚本读取当前服务正在使用的镜像tag,然后手动执行一次pull验证,避免仓库清空或标签被覆盖后,回滚时找不到镜像。

4.4 更新后的镜像清理艺术

更新多了以后,每个节点上都会残留一堆旧镜像的悬空层。磁盘空间告急是Swarm集群非常常见的问题。我一般用两条命令处理:

docker image prune -f docker image prune -a --filter "until=168h"

第一条清理悬空镜像(没有tag、没有容器引用的镜像层),第二条清理7天前创建的未使用镜像。但直接全量清理有个风险:如果回滚到几天前的版本,那些镜像可能已经被清掉了。所以我的做法是保留最近N个版本:

docker images | grep order-service | head -n 20

然后手动把需要保留的tag做个标记,再执行清理。自动化程度高一点的环境,可以写个cron脚本,把仓库中最近5个tag的镜像在节点上做docker tag加上keep标签,防止被prune -a误删。

5. 服务通信、资源约束与故障转移的实战细节

5.1 服务发现与VIP之间的那些事

在Swarm里,服务与服务之间通信不需要知道对方的IP,因为Swarm为每个service分配了一个稳定的虚拟IP(VIP)。DNS会把服务名解析到这个VIP,VIP再负责把流量转发给后面的task容器。

这意味着你在容器里调用另一个服务时,直接使用服务名:

curl http://order-service:8080/api/orders

这比维护一堆IP列表靠谱得多。但有个问题:Swarm默认的VIP模式下,服务间的负载均衡粒度是连接级别的。如果你的应用发出的是HTTP请求,那没问题;但如果是长连接(比如gRPC、WebSocket),建议把服务创建时的endpoint模式改为dnsrr:

docker service create --endpoint-mode dnsrr --name my-service image

dnsrr模式下,DNS会返回所有task的独立IP列表,客户端自行选择连接哪个实例。这是很多gRPC服务在Swarm上踩坑后必须改的配置。

5.2 资源约束:给Swarm的调度器一个边界

集群里最怕的情况是某个服务把节点内存吃满,导致其他服务陆续OOM。Swarm允许你在创建服务时指定CPU和内存限制:

docker service create \ --name heavy-service \ --reserve-cpu 0.5 \ --limit-cpu 1.0 \ --reserve-memory 512M \ --limit-memory 1G \ --replicas 2 \ image

--limit是硬上限,超了就会被杀死或重启;--reserve是调度器做资源规划时的预留值。生产环境建议永远设置--limit-memory,这是最有效的防雪崩手段。

除了资源限制,调度约束也很有用。比如你希望数据库类服务只跑在带SSD的节点上,先给节点打标签:

docker node update --label-add storage=ssd node-name

创建服务时加:

docker service create \ --constraint 'node.labels.storage == ssd' \ --name db-service \ image

注意--constraint的语法是严格匹配,空格不能乱加。

5.3 故障转移:节点宕机后会发生什么

Swarm的故障转移核心要点是:管理器会周期性检查节点心跳,如果某个节点超过一定时间没有上报心跳,就会把它标记为不可用,并将其上的task重新调度到其他节点。

这里面有几个容易被忽略的细节:

  • 默认的”一定时间“其实是由--heartbeat和--election相关参数间接决定的,对于大多数场景,默认值够用。
  • 如果服务使用了--constraint,而满足约束的节点数量不足以承载所有副本,那么多出来的task会一直处于pending状态,不会自动消失。
  • task在新节点上重新启动时,会重新从registry拉取镜像。如果节点之前没缓存该镜而仓库又在线,启动会比较慢。

我做高可用演练时,最喜欢干的一件事是直接docker node stop worker-1来模拟节点宕机,然后观察docker service ps的输出变化:

docker service ps order-service

如果一切正常,你会看到原本跑在worker-1上的task状态从running变为shutdown,然后新task在其他节点上从prepare变为running。这个过程是自愈的,不需要任何手动干预。

5.4 数据持久化的思考

容器可以被调度到任意节点,那数据怎么办?Swarm的标准方案是volume,但这里的细节值得细说。

有两种常见选择:

方案适用场景限制
--mount type=volume+ 节点本地的named volume单副本且对数据可靠性要求不是极高重新调度到其他节点时数据不在新节点上
NFS/共享存储多副本或容器可能在节点间漂移网络延迟和性能取决于存储方案

如果你的服务是有状态数据库,我强烈建议不要直接跑在Swarm的默认调度逻辑下,而是用--constraint把它固定在特定节点,并使用本地volume。这样做牺牲了一定的调度灵活性,但数据安全性大幅提升。对于无状态业务,就无所谓了,数据丢了直接从镜像重建即可。

6. 一个完整的实操示例:部署并更新一个两副本服务

前面讲了太多理论,最后我放一个从零到一的完整示例,照着敲一遍基本就能掌握Swarm服务部署和镜像管理的核心流程。

6.1 环境准备和初始部署

假设你有三个节点,一个manager,两个worker。先初始化集群:

docker swarm init --advertise-addr 192.168.1.10 docker node ls

接着创建一个overlay网络,让服务之间能够互相发现:

docker network create -d overlay demo-network

写一个最简单的Web应用Dockerfile:

FROM nginx:alpine RUN echo "<h1>version 1.0.0</h1>" > /usr/share/nginx/html/index.html

构建并推送:

docker build -t 192.168.1.10:5000/demo-app-1.0.0 . docker push 192.168.1.10:5000/demo-app-1.0.0

创建服务:

docker service create \ --name demo-app \ --replicas 2 \ --network demo-network \ --publish 80:80 \ --update-parallelism 1 \ --update-delay 10s \ 192.168.1.10:5000/demo-app-1.0.0

验证:

docker service ls curl http://192.168.1.10

正常会看到页面上显示version 1.0.0,同时两个节点都在运行task。

6.2 发布新版本

修改Dockerfile里的内容,改成version 1.1.0,重新构建推送:

docker build -t 192.168.1.10:5000/demo-app-1.1.0 . docker push 192.168.1.10:5000/demo-app-1.1.0

执行滚动更新:

docker service update \ --image 192.168.1.10:5000/demo-app-1.1.0 \ --update-parallelism 1 \ --update-delay 10s \ demo-app

在更新过程中,你可以不断执行curl验证服务始终可用,然后再观察docker service ps demo-app,会看到两个task逐个被替换。

如果你把某个task的镜像写错,比如push了一个不存在的tag,更新会卡住。此时docker service ps demo-app中会有task一直start-failed,但你不用担心,Swarm不会继续更新下一批,因为默认的失败动作是pause。用docker service inspect demo-app --format '{{.UpdateStatus.State}}'可以看到更新状态是paused。

这时候你可以回滚:

docker service rollback demo-app

服务会回到1.0.0,页面再次显示version 1.0.0。

6.3 实际体验中的操作顺序总结

最后总结一下我这套流程的核心顺序,也是多年摸索后觉得最稳妥的:

  • 打tag一定带版本号,永不用latest顶生产。
  • 所有节点配置insecure-registries。
  • 每次更新显示指定--update-parallelism 1 --update-delay 10s。
  • 发布完以后清理旧镜像,但保留最近5个版本供回滚。
  • 所有无状态服务加--limit-memory,有状态服务加调度约束。

按这个节奏走,Swarm集群不会太折腾。尤其是镜像管理这块,很多人觉得不就是push和pull嘛,但实际生产里的稳定性往往就靠这些细节堆出来的。

返回列表