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 \ --restartalways \ 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] }改完以后重启Dockersystemctl restart docker。注意Swarm集群的所有节点都要改不只是管理节点。我见过有人只改了manager节点结果服务调度到worker上拉取镜像时直接失败。2.2 镜像标记规范决定回滚体验搞定了仓库下一个问题就是镜像的tag怎么打。这句话听起来像废话但我在实际集群里见过太多因为tag混乱导致发布事故的情况。最典型的错误是只用latest。开发阶段用latest没问题但生产环境一旦用了latest你就会发现自己根本不知道当前跑的是哪个版本更糟糕的是某次发布后想回滚却找不到上一个镜像到底长什么样。我自己的规范非常简单每次构建必须指定版本号格式是项目名-主版本.次版本.修订号比如order-service-1.2.3。只有需要给测试环境用的临时构建才用dev-时间戳这种tag。任何一次生产构建同时打好两个tagorder-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个人replicas5要求会某技能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 部署命令与发布流程实际部署业务时我的标准流程大概这样构建镜像并打好版本号推送到私有仓库。在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用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-service5这个命令会在现有基础上把副本数扩到5多出来的两个task由调度器决定跑在哪台节点上。同理缩容就是把数字调小多余的task会被优雅关闭。我经常遇到的问题是扩容后流量并没有均匀分布。这可能有两种原因一是Swarm内置的负载均衡是基于VIP的它会将流量均衡分发到该服务的所有后端副本上但你在客户端看到的连接数可能不均衡特别是长连接场景二是你的应用本身有状态比如Session新扩的实例并不能立刻接收流量因为客户端还是连到旧实例上。所以扩缩容之前先想清楚业务是长连接还是短连接、是否有状态这比命令本身重要得多。4. 镜像更新链路从push到滚动发布的完整拆解4.1 更新流程中的两个隐含阶段一个新镜像从推送到私有仓库到服务里的容器真正用上它中间要经过两个阶段用户执行pushSwarm执行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 until168h第一条清理悬空镜像没有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分配了一个稳定的虚拟IPVIP。DNS会把服务名解析到这个VIPVIP再负责把流量转发给后面的task容器。这意味着你在容器里调用另一个服务时直接使用服务名curl http://order-service:8080/api/orders这比维护一堆IP列表靠谱得多。但有个问题Swarm默认的VIP模式下服务间的负载均衡粒度是连接级别的。如果你的应用发出的是HTTP请求那没问题但如果是长连接比如gRPC、WebSocket建议把服务创建时的endpoint模式改为dnsrrdocker service create --endpoint-mode dnsrr --name my-service imagednsrr模式下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 storagessd 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 typevolume 节点本地的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应用DockerfileFROM nginx:alpine RUN echo h1version 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嘛但实际生产里的稳定性往往就靠这些细节堆出来的。
返回列表