尧图精选

Docker Swarm混合调度策略实践:标签、约束与资源预留的协同编排

🕒 发布时间:2026/9/19 2:30:53 📁 来源:尧图网络
Docker Swarm 混合调度策略综合演练报告【20260108-002篇】先说说这次演练的来由。我负责维护的一套基于 Docker 的内部业务系统从单机 Docker Compose 迁移到 Docker Swarm 集群后稳定性确实上来了但随着服务和节点增多新的问题冒了出来容器在集群里的分布完全不受控。数据库可能会被调度到磁盘性能一般的节点CPU 密集型任务和内存敏感型任务挤在同一台机器某个定时任务每次重启都可能换一台机器跑。Swarm 默认调度器只关心节点是否健康、资源是否够用它不知道这台机器是 SSD 还是 HDD也不知道哪个服务应该和哪个服务分开。于是我做了一轮比较完整的混合调度策略演练把节点标签、部署约束、资源预留、全局模式这些手段组合起来用最终把容器的分布从“看似随机”变成了“可预期、可解释、可排障”。这篇文章就是这次演练的过程记录和复盘适合正在用 Swarm 做生产集群、但还没有系统设计过调度策略的运维和开发同学参考。1. 为什么需要混合调度默认调度器的短板1.1 Swarm默认调度模型的运作方式Docker Swarm 内置的调度器在 Swarm 模式下其实是分布式的管理节点Manager Node通过 Raft 协议维护整个集群的期望状态然后由调度器把服务的副本任务分配到各个工作节点。它的核心逻辑并不复杂主要看两点节点是否满足健康状态和资源可用性。默认情况下Swarm 对副本类服务replicated services的调度倾向于打散也就是尽量把多个副本分散到不同节点上这样能避免单点压力。调度器依据的是每个节点上报告的可用 CPU 和内存来决定把新的任务放到哪里。它不关心任务本身是什么类型也不关心节点有什么硬件属性、部署在哪个机房。这里要特别说明Swarm 模式下的调度策略和我们以前在 Docker 1.x 时代用的--strategy spread/--strategy binpack不是一回事。Swarm 模式内置的调度器不再暴露这种策略参数而是以一种“多维度评分”的方式工作节点当前资源余量、副本是否已经分布在某个节点上、节点是否处于 drain 状态、节点的角色等都会影响最终选择。你没法直接指定 binpack 或 random只能通过约束和偏好来引导调度结果。这个设计的好处是简单、安全坏处也很明显——当业务模型复杂时默认调度完全不理解你的业务意图。1.2 我在演练前遇到的真实调度问题这套系统大概有 6 个服务跑在 3 台工作节点上。其中有一个服务要定期做批量数据计算CPU 占用很高有一个缓存服务对内存延迟非常敏感还有一个内部数据库服务磁盘 IO 压力大。一开始大家图省事所有服务都用docker service create直接拉起参数里只有镜像和副本数没有任何调度约束。结果运行一段时间后我发现数据库任务被调度到了一块机械硬盘上磁盘 IO 经常被打满而另一台挂着 SSD 的节点上只跑了一个静态页面服务。CPU 密集的数据计算服务和内存密集的缓存服务被分配到了同一台机器导致这台机器内存频繁告警CPU 也持续高位。一个每天凌晨执行的批处理任务每次迁移重启后都可能落到不同的节点一旦它所在节点临时负载高任务的执行时间就会明显拉长。那时候我才意识到Swarm 集群不是搭起来就能自动变得“智能”调度策略必须由人来设计并且在设计之前要先弄清楚服务的分类和节点的属性。这也是我发起这次混合调度策略综合演练的直接原因。1.3 本轮演练要验证的混合调度组合混合调度不是说某一种策略多厉害而是把多种调度手段组合成一个整体形成一套可解释的规则。我在这轮演练中重点验证的是四类手段的组合效果节点标签Node Labels给节点打上业务分组、存储类型、网络区域等标签作为调度的基础信息。部署约束Constraints利用--constraint把服务限定到满足标签条件的节点上。资源预留与限制Reservations Limits通过--reserve-cpu、--reserve-memory、--limit-cpu、--limit-memory控制任务对节点资源的“报价”让调度器在分配前就知道该留出多少余量。服务模式混用Replicated Global用 replicated 模式管理普通业务服务用 global 模式部署每台节点都必须存在的监控、日志类组件。演练的最终目标是让任何一个人看到服务列表时都能说出这个服务应该运行在哪类节点上而不是全靠猜。2. 演练环境搭建三节点Swarm集群的初始化与准备2.1 节点规划与基础环境检查我准备了三台虚拟机来做这次演练全部使用 Ubuntu 22.04 LTSDocker Engine 版本统一为 24.0.x。节点规划如下节点角色配置磁盘用途swarm-managermanager4C 8GSSD 60G管理节点、监控组件swarm-worker1worker2C 4GSSD 80G后端业务、数据库类服务swarm-worker2worker2C 4GHDD 80G前端展示、低 IO 服务这里建议所有节点的 Docker 版本尽量保持一致。版本差异过大会导致集群内 API 行为不一致尤其是旧版本节点可能不支持新版本集群下发的任务规格。基础环境检查我列了一个清单按顺序执行hostnamectl set-hostname swarm-manager分别设置三台主机名并在/etc/hosts中写入互相解析的记录。关闭防火墙或放行 Swarm 所需的 2377/TCP、7946/TCP/UDP、4789/UDP 端口。测试环境我直接ufw disable生产环境一定按最小权限放行。配置时间同步。Swarm 集群对节点时间偏差很敏感时间不同步可能引发 Raft 选举异常和任务状态错乱。确认 Docker 服务正常运行systemctl status docker。如果 Docker 启动失败先看/var/log/docker或journalctl -u docker权限问题就检查/var/run/docker.sock属组。如果是从零开始安装 Docker直接走官方仓库安装即可。离线内网环境则需要提前准备好 deb 包或二进制包三台节点版本保持一致镜像也要提前导出导入。2.2 Swarm集群初始化与节点加入在 swarm-manager 上执行初始化命令docker swarm init --advertise-addr 192.168.122.11这里的--advertise-addr必须指定为集群内其他节点能访问到的地址不要用默认的 eth0 自动探测否则多网卡环境下很容易出现节点可以 join 但不停 rejoin 的诡异问题。初始化完成后会输出一段docker swarm join --token ...命令在两个 worker 节点上分别执行docker swarm join --token SWMTKN-1-xxxx 192.168.122.11:2377回到 manager 节点查看集群状态docker node ls确认三个节点都是 Ready 状态。这里有个小习惯我会顺手给每个节点补上角色之外的标识信息方便后续辨识当前节点在哪个机房、什么用途。这些标识就是后面调度要用的标签。2.3 用Compose文件组织演练服务栈演练中我要部署多个服务一直敲docker service create太容易出错也不利于重复执行。所以我用 Compose 文件来定义整个服务栈然后通过docker stack deploy部署到 Swarm。需要注意的是docker stack deploy使用的 Compose 文件版本和docker compose有差异很多字段在 stack 模式下不可用。我们需要关注的核心字段是deploy块下的placement、resources、replicas和mode。下面是我准备的stack-schedule.yml骨架具体参数在下一节逐步展开version: 3.8 services: frontend: image: nginx:alpine ports: - 8080:80 deploy: replicas: 3 placement: constraints: - node.labels.app frontend resources: limits: cpus: 0.5 memory: 256M reservations: cpus: 0.25 memory: 128M这个文件同时定义了三种服务的调度规则。用docker stack deploy -c stack-schedule.yml demo一条命令就能完成全量部署并且每次修改后重新执行即可增量更新非常适合反复试验。3. 混合调度策略的落地实施标签、约束与资源控制的组合拳3.1 节点标签体系设计让Swarm认识机器的“分工”节点标签是 Swarm 调度设计的地基。没有标签约束条件就无从谈起。我的标签体系分三个维度设计业务分组appfrontend、appbackend、appmonitor存储类型storagessd、storagehdd节点区域regioncn-east给节点打标签的命令格式是docker node update --label-add appbackend --label-add storagessd swarm-worker1 docker node update --label-add appfrontend --label-add storagehdd swarm-worker2查看标签是否生效docker node inspect swarm-worker1 --format {{json .Spec.Labels}}管理节点 swarm-manager 不参与业务调度所以我给它打上rolemanager的标签后续只在特殊场景下让部分管理类任务落在它上面。这里有一个经验标签命名要从一开始就明确规范否则一旦标签打错后面所有约束都会跟着错。比如我当时差点把appfrontend打到 worker1 上一旦生效前端容器就全跑到了后端节点上整个调度规则会变得毫无意义。因此在正式部署前一定要用docker node inspect逐个核对标签。3.2 部署约束与调度偏好把容器放到该放的位置标签打完之后就可以用约束来控制服务位置了。对于前端服务我要求它只能调度到appfrontend节点docker service create \ --name frontend \ --constraint node.labels.appfrontend \ --replicas 3 \ --publish 8080:80 \ nginx:alpine对于数据库类服务要求调度到storagessd节点docker service create \ --name backend-db \ --constraint node.labels.storagessd \ --replicas 2 \ --mount typevolume,srcdbdata,dst/var/lib/postgresql/data \ postgres:16-alpine约束是硬性条件节点标签不匹配时任务会一直处于 Pending 状态绝不会自动降低要求找别的节点。这个特性既是保护也是陷阱保护在于不会把数据库放到 HDD 上陷阱在于如果候选节点数量少于副本数多余副本会永久卡在 Pending。如果只想表达一种“偏好”而不是“必须”Swarm 没有原生 soft constraint 参数但可以通过 placement preferences 实现。例如我可以用--placement-pref spreadnode.labels.region把服务尽量分散到不同区域的节点上但又不强制约束。后面实战复盘里会再提到这个点。3.3 资源预留与限制CPU、内存与调度的联动只控制“放在哪”还不够还得告诉调度器“这个服务会用多少资源”否则节点一样会被超额塞满。资源参数分两类很多人一开始分不清--limit-cpu、--limit-memory容器运行时的资源上限超了会被限制或杀掉。--reserve-cpu、--reserve-memory调度器计算节点剩余容量时认为这个任务“占掉”的资源量。关键点在于Swarm 调度器做节点选择时依据的是 reserve 值而不是 limit 值。如果你只设置了 limit 而不设置 reserve调度器会认为这个任务不占资源照样往节点上塞。等到运行时容器真的冲击 limit 上限同节点的其他服务就会遭殃。这次演练的资源分配方案如下服务reserve CPUreserve 内存limit CPUlimit 内存期望节点frontend0.25128M0.5256Mworker2backend-api0.5256M1.0512Mworker1backend-db0.5512M2.01Gworker1SSDmonitor-agent0.164M0.25128M每节点给已有服务补充资源限制用docker service updatedocker service update \ --reserve-cpu 0.5 \ --reserve-memory 256M \ --limit-cpu 1.0 \ --limit-memory 512M \ backend-api在 Compose 文件里对应的就是deploy.resources.reservations和deploy.resources.limits。这里我的设计思路是只给有明确资源特征的服务做预留。比如前端服务是纯静态流量入口预留 0.25 核足够数据库服务必须预留 512M 内存避免和其他任务抢内存。而那些跑批、低优先级任务不设置任何预留让它们在资源缝隙里运行这样能最大化集群利用率。3.4 全局模式与副本模式混用常驻组件与扩展组件的不同玩法很多人在 Swarm 里只知道--replicas忽略了--mode global。全局模式的含义是在每一个满足约束条件的节点上都运行且仅运行一个副本。这次演练中我在每个节点上部署了一个日志采集组件和一个基础监控探针使用的就是全局模式docker service create \ --name node-agent \ --mode global \ --constraint node.roleworker \ --mount typebind,src/var/run/docker.sock,dst/var/run/docker.sock \ busybox tail -f /dev/null这里用 busybox 快速模拟一个常驻进程。加上node.roleworker约束后manager 节点不会部署 agent只让两个 worker 节点各跑一个。这是全局模式和约束条件组合的典型用法。而像前端服务、后端 API 这类需要横向扩展的应用则使用--mode replicated通过--replicas控制副本数。三种模式在 Compose 里长这样services: frontend: image: nginx:alpine deploy: mode: replicated replicas: 3 node-agent: image: busybox command: [tail, -f, /dev/null] deploy: mode: global placement: constraints: - node.role worker到这里混合调度的主要手段都已经落地标签告诉 Swarm 节点是什么样的约束告诉 Swarm 服务应该去哪些节点资源预留告诉 Swarm 每个服务占多大位置全局模式保证基础设施组件无处不在。接下来就该验证这套组合拳到底有没有用了。4. 演练结果验证服务分布、故障转移与资源水位实测4.1 用docker service ps验证调度分布是否符合预期部署完成后首先看服务任务的实际分布情况。以 frontend 服务为例docker service ps frontend --format table {{.Name}}\t{{.Node}}\t{{.CurrentState}}\t{{.Error}}我期望的输出是三个副本全部落在 swarm-worker2 上因为它们约束了node.labels.appfrontend。实际输出确实如此NAME NODE CURRENT STATE demo_frontend.1 swarm-worker2 Running 3 minutes ago demo_frontend.2 swarm-worker2 Running 3 minutes ago demo_frontend.3 swarm-worker2 Running 3 minutes ago再验证 backend 服务docker service ps backend-api可以看到两个副本都在 swarm-worker1 上没有跑到 worker2。这里我还会用docker node ps从节点视角反向检查一遍docker node ps swarm-worker1 docker node ps swarm-worker2这一步的目的是发现有没有“漏网之鱼”——比如某个服务没有约束条件悄悄落在了不该在的节点上。实际演练中我就发现监控服务因为忘记加约束被调度到了 worker2而 worker2 本来负载就比较重这就是检查的价值。4.2 节点故障后的任务重调度实测混合调度不仅要保证正常状态下分布合理更要保证节点故障后服务能迁移到“仍然满足所有约束条件”的节点上。我模拟了一次 worker2 故障。为了不直接拔电源把 Raft 搞出问题我先用docker node update --availability drain swarm-worker2让节点进入排空状态Swarm 会自动把 worker2 上的任务迁移到其他节点docker node update --availability drain swarm-worker2等几十秒后再查docker service ps frontend --format table {{.Name}}\t{{.Node}}\t{{.CurrentState}}关键发现来了frontend 原本有 3 个副本约束条件是node.labels.appfrontend而集群中只有 worker2 满足这个标签。当 worker2 被 drain 之后这 3 个副本没有任何一个能迁移到其他节点全部保持 Pending 状态。这正是硬约束的双面性正常情况下定位精准异常情况下也会因为没有候选节点而拒绝妥协。docker service ps的输出会明确显示demo_frontend.1 swarm-worker2 Running ... demo_frontend.1 swarm-worker2 Shutdown ... demo_frontend.2 swarm-worker2 Running ... demo_frontend.2 swarm-worker2 Shutdown ... demo_frontend.3 swarm-worker2 Running ... demo_frontend.3 swarm-worker2 Shutdown ...而 backend-api 只有 worker1 一个候选节点同时也满足 SSD标签所以它依然能正常运行只是两个副本都压缩到了一台机器上。这个结果让我对约束条件的风险有了更直观的认识在设计约束时必须同步考虑“如果该标签对应的节点全部不可用服务怎么办”这个问题。恢复节点后执行docker node update --availability active swarm-worker2让节点重新参与调度。4.3 混合策略下的资源利用率对比调度分布验证完之后我用docker stats观察了一段时间的资源水位把混合调度前后的情况做了对比。混合调度前节点资源分布是很乱的节点CPU 使用率内存使用率主要容器swarm-manager25%45%控制面组件误调度的业务容器swarm-worker170%82%CPU密集内存密集混部swarm-worker220%30%少量静态服务混合调度后各节点的资源水位和容器分布变成节点CPU 使用率内存使用率主要容器swarm-manager12%28%控制面组件、monitor-agentswarm-worker145%55%backend-api、backend-dbswarm-worker232%47%frontend、node-agent从数据上看worker1 的高负载降下来了worker2 的资源被有效利用起来manager 节点不再承担不必要的业务压力。更重要的是现在看到任何一个容器的负载异常第一反应能直接对应到它所在节点的属性和同节点其他容器排障范围大幅缩小。当然资源利用率并不是越高越好。混合调度牺牲了一部分极限密度换来了可控性和稳定性。对于生产集群稳定可控通常比“把每一兆内存都用掉”更有价值。5. 实战复盘混合调度设计中的坑与优化建议5.1 标签命名不规范引发的调度混乱第一次做标签设计的时候我犯了一个典型的错误给 worker1 打上了appfrontend标签因为当时心里想的是“这个节点放前端”但实际操作时手一抖打反了导致两个 worker 节点的标签和计划完全互换。问题爆发是在部署前端服务时Swarm 严格按标签约束把任务分配到了错误的节点而表面上一切正常只有深入检查docker node inspect才能发现。这个坑的教训有三条标签命名要带前缀或者分层比如node.labels.app、node.labels.storage不要用含义模糊的frontendyes这种。打标签前先列清单一张表写清楚节点、标签键、标签值粘贴命令之前逐条核对。每次部署前加一个检查步骤输出所有节点的标签和期望清单做比对。docker node ls --format {{.Hostname}} | xargs -I {} docker node inspect {} --format {{.Description.Hostname}} {{json .Spec.Labels}}5.2 约束条件与副本数冲突导致的服务Pending演练中我故意设计了一个“错误”的案例来验证 Swarm 的行为设置--replicas 5但约束条件是node.labels.storagessd而整个集群只有 worker1 一个节点是 SSD。结果 5 个任务里有 4 个停留在 Pending 状态。排查方法很直接docker service ps backend-dbPending 状态的任务会显示原因。Swarm 对 Pending 任务不会反复尝试调度到不满足约束的节点它会一直等直到有新的 SSD 节点加入或者你手动调整约束。针对这个问题我的建议是设计约束前先数清楚候选节点数量副本数必须小于等于候选节点数乘单节点可容纳副本数。如果副本数注定多余候选节点数考虑把约束改成偏好使用--placement-pref spreadnode.labels.storage之类的参数。对关键服务设计额外的告警监控docker service ps中长期处于 Pending 状态的任务避免业务已经停服半天才发现。5.3 资源预留过高拖垮整个集群的教训还有一次是资源预留过大导致的集体 Pending。当时我给每个服务都设置了比较保守的 reserve 参数想着“预留多一点更安全”结果所有服务的 reserve 内存加起来超过了整个集群的内存总量。新部署的服务全部无法调度而旧服务的实际内存使用量远没有 reserve 值那么高。这个问题的根源是我混淆了“预留”和“使用”。Swarm 调度器只认 reserve它不会去看容器已经用了多少内存。你把 reserve 设成 1G即使实际只用了 100M调度器也会认为这台节点少了 1G 可用容量。所以资源预留的设计原则应该是只对关键服务设置 reserve普通服务只设置 limit。reserve 值要参考服务实际运行时的峰值留出 1.2 到 1.5 倍余量即可不要拍脑袋。在 CI/CD 流水线里加一道检查把所有服务的 reserve 总和与集群总容量对比一旦超过阈值就阻止发布。可以用脚本定期执行docker node inspect swarm-worker1 --format {{.Description.Resources.MemoryBytes}}去核对集群剩余可分配资源防患于未然。5.4 混合调度设计的经验沉淀与推广演练结束后我把这套调度规则沉淀成了团队的部署规范核心思路可以用三句话概括节点先分类所有节点必须打上业务、存储、区域三类标签没有标签的节点不允许参与生产调度。服务再分型常驻组件用 global无状态应用用 replicas constraints有资源特征的服务必须写 reserve 和 limit。发布前验证每一次发布前先跑一遍调度预检脚本确认约束、副本数、资源预留三者不冲突。Swarm 的混合调度并不是什么黑科技它就是把节点的物理属性和业务属性用标签表达出来再用约束和资源参数引导调度器做决策。这个过程不需要写代码不需要额外组件完全基于 Docker 内置能力但却能让整个集群的调度行为从“不可控的随机”变成“有依据的决策”。我个人在实际操作中的体会是混合调度策略的维护成本大头不在配置参数本身而在持续维护节点标签和服务分类信息的准确性。只要把这两件事管好了Swarm 集群的调度可靠性会远超你的预期。以后如果再遇到“容器被安排在不该去的地方”这类问题不要急着换编排系统先回头看看自己的标签和约束是否真正表达了业务的诉求。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →