1. 从一次“改了 Job 却不生效”的故障说起
有次我在维护一个 Nomad 集群,执行 nomad job run 更新了一个服务的 CPU 配额,从 500MHz 改成 1000MHz,nomad job status 也显示 job 版本号确实从 7 变成了 8。可我等了十来分钟,该 group 下的所有 allocation 还是旧参数,新版本压根没部署上去。
排查到最后发现,问题不在执行流程,出在我自己对 Job 和 Allocation 这对关系的理解上。我潜意识里把它们当成了“父子关系”,认为 Job 变了 Allocation 就会跟着变。但在 Nomad 的世界里,Job 是“你想要的”,Allocation 是“实际跑起来的”,这两者之间隔着一整套调度、评估、部署、健康检查的链路,而“Job 变了 Allocation 必须立即同步”这个假设,本身就是错的。
这恰恰是理解 Nomad 最关键的一个坎。很多人刚接触 Nomad 时会看官方文档里那句“Job 定义了你想要运行的任务,Allocation 是调度器在节点上为任务分配的资源”,但这句话太简略了,完全没有讲清楚这两者之间那种“既绑定又独立、既同步又异步”的微妙关系。这篇文章我想花一整章的篇幅,把 Job 与 Allocation 的辩证关系彻底掰开揉碎,聊聊任务定义背后的设计哲学,以及我在实际运维中被教育过的那些时刻。
这篇文章适合谁?适合已经能用 nomad job run 跑起来一个简单任务、但还没真正理解 Job 版本、部署状态、Allocation GC、reschedule 策略这些概念的工程师。如果你已经在生产环境摸爬滚打过一段时间,那这篇文章里的“反直觉点”部分,大概率能让你会心一笑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Job 与 Allocation:其实是“两个世界”的东西
2.1 Job 是声明式契约,Allocation 是运行时实体
我见过不少人在学习 Nomad 时,会把 Job 和 Allocation 下意识地理解成 Docker 镜像和容器、Kubernetes 里的 Deployment 和 Pod、或者进程和线程的关系。这个类比方向是对的,但如果只停留在“Job 是模板,Allocation 是实例”这一层,还是太粗糙了。
更准确地说,Job 是一个“声明式的期望状态”。你提交一份 Job 文件给 Nomad,本质上是在告诉调度器:我需要 n 个副本,每个副本需要多少 CPU、多少内存,跑什么命令,挂载什么卷,要不要健康检查,升级策略是什么。Nomad 拿到这份声明之后,会把它解析成一个 versioned job,然后基于当前集群的空闲资源做一次评估(evaluation),决定在哪些节点上启动多少个 allocation 来满足这个声明。
Allocation 则是调度器评估完之后的产物。它是一个具体的运行时实体,绑定了一个 job、一个 group、一个节点、一份网络端口分配、一段资源预留。你可以把 Allocation 理解为“这一次调度决策的结果快照”,它记录的是“在什么时间、在哪个节点、以怎样的资源约束、为哪个任务组创建了一份实例”。
这就引出一个关键点:Job 是你期望的世界,Allocation 是物理世界真实发生的事。这两者通常是一致的,因为调度器会努力让真实世界向期望状态收敛。但“努力收敛”不等于“瞬间一致”,更不等于“每次修改 Job 都会触发一次干净利落的替换”。中间隔着评估、调度、下载驱动、启动容器、健康检查、部署策略等一系列环节,任何一环发生变化,Job 和 Allocation 之间的关系都会表现出不同的行为。
2.2 一个 Job,多个 Allocation;一个 Allocation,必然只属于一个 Job
从数量关系上看,Job 和 Allocation 是一对多的关系。一个 Job 下面可以有多个 group,每个 group 可以设置 count,调度器最终创建出来的 allocation 总数,等于所有 group 的 count 之和。
举个实际的例子:
hcl复制job "web" {
group "frontend" {
count = 3
task "nginx" {
driver = "docker"
config {
image = "nginx:1.24"
}
}
}
group "api" {
count = 2
task "app" {
driver = "java"
config {
jar = "app.jar"
}
}
}
}
这个 job 提交之后,Nomad 最终会创建 5 个 allocation:3 个属于 web/frontend,2 个属于 web/api。每一个 allocation 都有一个全局唯一的 ID(通常是一串随机字符),它的命名空间里会记录自己属于哪个 job、哪个 group、在哪个节点上运行。
反过来说,一个 allocation 只可能属于一个 job、一个 group。不存在同一个 allocation 被两个 job 共享的情况。这也意味着,Allocation 是你在排查问题时最主要的“抓手”——当你发现某个任务行为异常,你几乎总是从 allocation 开始查起,通过 allocation 的 ID 反查 job 定义、节点状态、资源预留情况。
2.3 概念辨析:Job、Task Group、Task、Allocation、Evaluation
我见过不少讨论帖里,热搜词里那些奇怪的错误信息其实反映了大家真正关心的场景,比如 “job for mariadb.service failed”、“quartz job 被注册了两个 trigger”,虽然这些和 Nomad 没关系,但说明“Job”这个词在不同系统里有完全不同的语义。在 Nomad 里,必须先分清几个容易混的概念:
- Job:用户提交的任务声明,描述期望状态。包含一个或多个 Task Group。
- Task Group:一组必须调度在同一节点上的任务集合,是调度的最小单位。注意,调度不是以 task 为单位的,而是以 group 为单位的。
- Task:group 里的具体任务,比如跑一个 nginx 容器、执行一个 shell 脚本。
- Allocation:调度器按照 group 的 count,在某个节点上创建出来的运行时实例。一个 allocation 对应一个 task group 的一份拷贝,但它内部包含了该 group 里所有 task 的启动信息。
- Evaluation:调度器处理 Job 变更时创建的评估记录,用来决定需要做什么操作。每次 job 提交、节点故障、allocation 失败,都可能触发一次或多次 evaluation。
这 5 个概念之间的关系,可以类比成“菜谱、一桌菜、每道菜、服务员端上来的一桌菜、后厨评估该做多少桌”。Job 是菜谱,Task Group 是一桌菜的搭配,Task 是菜谱里的每一道具体菜,Allocation 是最终端到你面前的那一桌菜,Evaluation 是后厨接单后的统筹判断。
我在给团队做分享时有个习惯:先让大家看 nomad job status <job> 和 nomad alloc status <alloc-id> 这两个命令的输出,对着输出讲概念。因为命令输出的字段名和官方文档里术语是一一对应的,看多了自然就能建立“Job 是要什么,Allocation 是有什么”的直觉。
3. 从 Job 到 Allocation:一次调度决策的完整旅程
3.1 提交 Job 后,Nomad 内部发生了什么
理解 Job 和 Allocation 的辩证关系,光看静态定义不够,还得看动态流转过程。一次 nomad job run 之后,Nomad 内部大致会走这么几步:
- API 接收与持久化:
nomad job run把 Job 文件提交到 server 的 API,server 校验格式、权限后,将 Job 持久化到 Raft 日志中,并赋予一个新的版本号。这一步之后,Job 的“期望世界”就正式落地了。 - 创建 Evaluation:Job 版本变更会触发一条 evaluation,像一条待办事项一样进入队列。
- 调度器执行 Plan:调度器从队列里捞到 evaluation 后,查看当前集群节点资源、已有 allocation、各种约束和优先级,产出一份调度计划(plan)。计划里会包含:哪些节点上需要新建 allocation、哪些 allocation 需要迁移、哪些 allocation 需要保留。
- Node 上的 Client 执行:server 把计划推送给相关节点的 Nomad client,client 开始实际干活——下载驱动、启动任务、上报状态。
- Deployment 跟踪:如果 Job 定义了滚动更新策略,Nomad 还会维护一个 deployment 对象,跟踪这批新 allocation 的健康状态,逐步放量替换旧 allocation。
这里有个特别容易误解的点:Job 被提交后,不是 Job 自己“变成了”Allocation,而是调度器按照 Job 的期望,在物理世界里“再造”了一批 Allocation。Job 本身始终是那份声明,它的版本可以不断更新;Allocation 是调度结果的物化,状态会随生命周期不断变化。
3.2 用一个最小 Job 走一遍全流程
假设我提交了下面这个 Job:
hcl复制job "demo" {
datacenters = ["dc1"]
type = "service"
group "cache" {
count = 1
task "redis" {
driver = "docker"
config {
image = "redis:7.0"
}
resources {
cpu = 500
memory = 256
}
}
}
}
提交后,我通常会立刻开三个终端分别跑 nomad job status demo、nomad eval status、nomad alloc status,观察状态变化。第一次看到的状态大概是这样的:
nomad job status demo显示 job 是running,version 为 1,但底下 allocation 列表可能还是空的或显示pending。- 紧接着,调度器创建了 evaluation,并完成 plan 和 submit。
- 几十毫秒或几秒后,
nomad alloc status里出现一个 allocation,状态从pending变成running。
如果你盯着命令看的频率够高,会发现“job 是 running”和“allocation 是 running”这两者不是同时发生的。Job 的 running 意味着“期望状态已经被接受并积极推进”,Allocation 的 running 才意味着“真实世界里这个任务已经在跑了”。
这个时间差,在集群负载低、资源充足时几乎察觉不到;但在资源紧张、需要排队等资源的场景里,Job 可能已经 running 很久了,allocation 一直 pending 甚至卡在 queued 状态。这时候如果误以为“Job 的状态就是服务实际状态”,就会得出完全错误的结论。
3.3 Allocation 的状态机:不只是 running/stopped
很多新手看 nomad alloc status 时,只关心 running 和 stopped 两个状态,但 Allocation 的状态其实丰富得多,理解这些状态才能准确判断“Job 的期望状态和 Allocation 的实际状态到底差在哪”。
| Allocation 状态 | 含义 | 常见触发原因 |
|---|---|---|
| pending | 调度决策已生成,但客户端尚未启动完成 | 镜像拉取、资源等待、客户端正在装载卷 |
| running | 任务在节点上正常执行 | 健康检查通过、进程处于运行中 |
| complete | 任务正常退出(batch 任务) | 批处理任务执行完毕 |
| failed | 任务异常退出或健康检查失败 | 进程崩溃、端口冲突、资源不足被 OOM kill |
| lost | 客户端失联,服务端判定 allocation 丢失 | 节点宕机、网络分区 |
| garbage collected | 状态记录已从集群中清理 | 达到 GC 时间阈值 |
这里要特别提一下 lost 状态。当节点失联,server 会等待一个 node_gc_threshold 和 heartbeat 相关的时间窗口,如果节点一直不恢复,allocation 会被标记为 lost,并根据 job 的 reschedule 策略在另一个节点上重建。这个过程中,Job 的期望状态始终没有变,Allocation 却经历了一次“从有到无再到有”的轮回。这就是 Job 和 Allocation 辩证关系最生动的体现:Job 是那个不变的锚点,Allocation 则是围绕锚点不断漂移的浮标。
4. Job 修改后,Allocation 的表现是你必须预判的
4.1 哪些改动会触发替换,哪些不会
这是我在生产环境里被问得最多的问题:“我改了 Job,为什么不生效?”答案在于,Job 里的配置项分两类:一类改动会影响调度器对 allocation 的决策,另一类只影响 Job 的元数据。
先说不会触发 allocation 重建的改动。比如修改 Job 的 meta 字段、修改 job 级别的 namespace 描述、修改 datacenters 列表但实际可用 DC 没变,这些改动只会让 Job 的版本号增加,但调度器可能认为“期望状态没实质变化”,于是不产生新的 allocation,也不会重建旧 allocation。
再说一定会触发 allocation 重建的改动。包括修改 task 的 driver 配置、修改 resources(比如 cpu、memory 配额)、修改 group 的 count、修改网络端口设置。这些改动动了“调度决策的依据”,所以调度器会规划新 allocation 替换旧 allocation。具体是“先删旧的再建新的”还是“先建新的再删旧的”,取决于你配置的更新策略。
| 修改内容 | 是否重建 Allocation | 说明 |
|---|---|---|
| 修改 task 的镜像 tag | 是 | 镜像 tag 变化属于 task config 变更,会触发替换 |
| 修改 resources.cpu | 是 | 资源预留变化需要重新放置 allocation |
| 修改 group 的 count | 是(新建或收缩) | count 增加会新建,减少会停止多余的 |
| 修改 job 的 meta 字段 | 否 | meta 只影响展示和模板渲染,不影响调度 |
| 修改 job 的 datacenters | 可能 | 如果可用 DC 变化导致原节点不再满足约束,才会重建 |
| 修改 constraint 表达式 | 是 | 约束是放置依据,变化会影响 placement |
| 修改 update 策略本身 | 否 | 只影响未来变更的滚动方式 |
这一点极其重要,因为很多人把 Job 简单理解成一个“配置模板”,以为改模板就会自动更新所有实例、像让所有 Pod 重新拉镜像一样。但 Nomad 的模型更朴素、更贴近“资源调度器”的本质——只有真正改变“资源占用逻辑”的改动,才会让调度器产生新的 allocation。如果只是改个 meta,期待它像环境变量一样注入进去,那肯定落空。
我踩过一次坑:给某个 Java 服务的 Job 加了几个 meta 字段,打算让服务启动时从 NOMAD_META_* 环境变量里读取配置。提交之后发现 allocation 没有任何变化,服务也没拿到新配置。后来才意识到,meta 变化不会触发重建,只能配合 job run -force-reschedule 或者手动 nomad job stop 再 run 才能让新 meta 字段真正生效。所以如果你的架构里有“动态配置注入”这类场景,更合理的做法是使用 Consul KV 或配置中心,而不是指望通过改 Job 的 meta 来热更新。
4.2 滚动更新机制:Deployment 是连接两边的那座桥
Job 改动需要重建 allocation 时,Nomad 默认会按 update 块里定义的策略做滚动更新。默认策略是:一次只替换一个 instance,等新 instance 健康后再替换下一个。如果想要更快速的并行替换,可以设置 max_parallel;想要更安全的发布,可以设置 canary 先行发布一个金丝雀。
我在生产环境常用的一段配置长这样:
hcl复制update {
max_parallel = 1
health_check = "checks"
min_healthy_time = "10s"
healthy_deadline = "5m"
auto_revert = true
auto_promote = true
canary = 1
}
注意,这里有个关键点:滚动更新的触发条件是 Job 的期望状态发生变化,但这只是触发条件,真正驱动过程的是 Deployment。只有 job 级别或 group 级别配置了 update 块,Nomad 才会为这次变更创建 deployment 对象,然后由 deployment 控制器逐步推进。如果你在 Job 里没有写 update 块,Nomad 会采用保守的默认策略,但 Deployment 依然会被创建,只是进度逻辑用的默认参数。
看滚动更新状态时,我最常用的命令是:
bash复制nomad job status demo
nomad deployment status <deployment-id>
nomad deployment list
其中 nomad job status 输出的 Deployment 一行会显示 running、successful、failed、cancelled 等状态。如果更新失败,allocation 会停在 failed,deployment 会报错,auto_revert 在配置了的情况下会把 Job 回滚到上一个版本。
这里就体现了 Job 与 Allocation 的第二层辩证关系:Job 是“目标”,Deployment 是“从当前 Allocation 状态走向目标 Allocation 状态的路径”。路径上每一步,都会产生新的 allocation、销毁旧的 allocation,直到最终所有 allocation 都对应到新版本 Job 的期望状态。
我在生产实操中特别推荐在发布时开着三个窗口:一个跑 nomad job status,一个跑 nomad deployment status,一个跑 nomad alloc status -verbose。因为滚动更新过程中,Job 的 version 会先变,部分 allocation 还是旧版,部分已经是新版,只看某一个命令很容易得出“更新失败了”或“更新成功了”的错误结论。只有三个视图配合看,才能对实际进度有准确的感知。
4.3 Canary、Auto Revert 与 Allocation 的“新旧共存”
我特别想展开说说 canary 这个机制,因为它是 Job 与 Allocation 关系中最能体现“辩证”二字的场景。当你配置 canary = 1 并提交带新定义的 Job,Nomad 会只创建一个新的 allocation,但暂时保留所有旧的 allocation 不动。此时集群里新旧版本的 allocation 会同时存在,Job 的版本号已经是最新,而绝大多数 allocation 还对应旧版本。
这个“新旧共存”的阶段,本质上是 Job 的期望状态(新定义)和物理世界状态(新旧混合)之间出现了一个鸿沟。你不能说 Nomad 出错了,因为这是 deployment 策略主动制造出来的中间状态,目的是让人类或自动化系统有机会对新版本做验证。验证通过后执行 nomad job promote <job>,剩下的旧 allocation 才开始被逐个替换;验证不通过则执行 nomad job revert <job> <version> 或依赖 auto_revert,整个系统退回旧版本。
这里有个经验:很多人第一次用 canary 时,都在 promote 之前就跑去检查新 allocation 的日志,确认没问题后却忘了执行 promote,结果新旧 allocation 一直共存,流量还是正常打到旧的上面,新版本只是“白跑了一遍”。所以如果你用了 canary,一定要把 promote 动作写进发布流程的检查清单里,或者配合 CI/CD 流水线自动化执行。
5. 任务定义的艺术:写出让调度器“理解”的 Job
5.1 从 Allocation 的视角反推 Job 定义
既然 Allocation 是调度的结果,那反过来思考就很重要:你在写 Job 的时候,应该站在调度器创建 allocation 的视角来审视每一行配置。这是我带团队几年下来给新人的一条核心建议。
什么意思?比如 resources 块里的 cpu 和 memory,表面上是“给任务分配多少资源”,实际上是在告诉调度器“想创建成功的 allocation,必须找到能同时满足这些资源剩余量的节点”。如果你的资源数值定义得过高,调度器在 plan 阶段找不到合适的节点,allocation 会一直停在 pending 或 queued,Job 状态却是 running。
再比如 constraint 块,它是在调度阶段就过滤节点集。如果你写了一个过于严格的 constraint(比如 attribute = "${attr.os.name}", value = "linux" + 某个特定标签),而集群里刚好没有节点满足,那么 Job 可以被接受,但永远不会有 allocation 被创建出来。这种“Job 存在但 Allocation 永远归零”的状态,是运维时最容易出现、也最容易被误判的场景。
我还经常建议团队在定义 Job 时用 nomad job plan 先跑一遍,而不是直接 nomad job run。plan 命令会在不影响实际集群的情况下,模拟一次调度,输出“这个 Job 如果提交,会创建多少 allocation、在哪些节点上放置、是否可能成功”。这个命令能提前暴露出约束不满足、资源不足、镜像无法下载等问题,避免你把一个注定无法落地的 Job 推上线。
5.2 用 Group 的设计影响 Allocation 的“命运”
Task Group 是调度的最小单位,这个事实其实给 Job 设计带来了一个非常核心的原则:需要运行在同一节点上的任务,必须放在同一个 group 里;不需要绑定在一起的任务,尽量拆成多个 group。
例如,一个 web 服务和一个 log shipper(日志采集 agent)如果放在同一个 group 里,调度器会保证它们永远在同一个节点上、同生共死。这很合理,因为 log shipper 需要读取同机 web 服务的日志文件。但如果你把 MySQL 和 Redis 放进同一个 group,调度器虽然会保证它们同节点,却也意味着它们会同时被调度、同时被替换,资源竞争也更激烈,这大概率不是你想要的效果。
我见过一个比较极端的案例:有团队把一个 batch 任务的多个 task 放在了同一个 group 里,count 设为 1,结果所有 task 都被调度到了同一个节点上执行,完全没法发挥分布式并行计算的优势,任务运行时间比拆分前还长了 3 倍。原因是他们理解错了“Task Group 是调度最小单位”的含义——同一个 group 的 task 是打包调度的,不会分散到不同节点。如果想让任务分散到多节点并行,正确的做法是配置 count > 1,让调度器在同一 group 下创建多个 allocation,而不是在一个 allocation 里塞多个 task。
5.3 Alloc 的命名、标签与可观测性
还有一个容易被忽略的点:Allocation 是调度器分配的随机 ID,很难让人一眼看出它对应哪个服务。所以我在团队里很早就要求,在 Job 定义里必须给 task 和 group 写上清晰的 meta 标签,并且尽量用 task 名和实际业务模块一一对应。
hcl复制group "api-server" {
count = 3
task "api-server-task" {
meta {
service_name = "api-server"
owner = "backend-team"
}
}
}
这样在 nomad alloc status、监控告警、日志聚合平台里,看到 allocation 时能快速通过 meta 标签定位到业务归属。如果不用 meta 标签,一个 allocation 5f3a2b 报错时,你根本不可能从 ID 里猜出它是哪条业务链路上的实例,只能去翻 job 映射,排查效率会低很多。
另外一个实战技巧:在 Consul Connect 或服务发现场景里,Allocation 的端口随机分配是常态,服务注册名和地址往往由 Nomad 自动写入 Consul,所以盯着 allocation ID 排查不如直接看 Consul 里 service 实例的健康检查结果来得快。但无论如何,Allocation 的状态仍然是真相的一个维度,特别是当服务注册成功但流量异常时,你需要回到 allocation 层面检查它实际被分配的资源限额和本地网络配置。
6. 常见误解与排查实录:搞懂 Job 与 Allocation 少走三年弯路
6.1 误解一:Job 状态 = 服务实际状态
我在前面反复提过这个点,因为它是几乎所有新手都会踩的坑。nomad job status 显示的 running 是“期望状态已被接受并持续推进”,不是“所有实例都已健康运行”。更可靠的检查方式是 nomad job allocs <job> 查看每个 allocation 的 Client Status 和 Desired Status。
如果 Desired Status 是 run,Client Status 是 running,才是真正的健康。如果 Desired Status 是 run,Client Status 是 failed,说明调度器想让它跑,但实际没跑起来,需要进一步看日志。如果 Desired Status 是 stop,Client Status 还是 running,说明正在停止过程中,或者停止卡住了。
我还把这一项写进了团队的告警规则里:监控只盯 Job 状态是不够的,必须盯 Allocation 的状态分布,尤其要盯 failed 和 lost 的数量变化。
6.2 误解二:重复执行 job run 会产生重复 allocation
这是另一个高频误解。nomad job run 是声明式地同步期望状态,不是重复创建任务。同一个 job 文件跑一百次,只要 Job 内容没变化,Nomad 只会在第一次创建 evaluation,后续重复执行时 Job 版本号不变,调度器会认为没有任何变更,不会产生新的 allocation。
但如果 Job 文件里有哪怕一个空格的变化、或者一个字段的顺序变了,够形成一个新的版本吗?严格意义上说,Nomad 会对 job 定义做哈希比较。为避免不必要的新版本,我建议团队在 CI/CD 里直接用 nomad job plan 输出判断是否有变更,再决定是否继续执行 job run,这样能避免频繁地把 Job 版本号刷上去,减少不必要的 evaluation 和 deployment 追踪。
6.3 误解三:Allocation 失败后应该手动去节点上重启
我在搜索热词里看到不少人会碰到任务异常退出后卡在 failed 状态。很多人的第一反应是 SSH 到节点上手动把容器拉起来。这个大可不必,而且手动拉起往往治标不治本。
Nomad 本身有重启策略(restart 块)和重调度策略(reschedule 块)。任务进程异常退出时,restart 策略会先在原节点上尝试重启几次;restart 次数耗尽后,reschedule 策略才决定是否将 allocation 迁移到别的节点重建。这是两层递进的关系,理解清楚才能配置出你想要的行为:
hcl复制restart {
attempts = 3
delay = "15s"
mode = "fail"
}
reschedule {
attempts = 2
delay = "30s"
unlimited = false
}
这个配置表示:进程异常退出后先在同一节点重启最多 3 次,每次间隔 15 秒;3 次都失败后,调度器会在其他节点最多重调度 2 次,每次间隔 30 秒。注意 restart.mode = "fail" 是指“本节点上已经重启失败”时的行为模式,而 reschedule 是跨节点层面的兜底。
6.4 排查思路:Allocation 不一致时的检查顺序
最后整理一下我的排查顺序,遇到 Job 和 Allocation 状态不一致时,按这个顺序走基本都能找到根因:
nomad job status <job>:看 Job 整体状态、版本号、Deployment 状态。nomad job allocs <job>:列出所有 allocation,关注 Desired/Client 状态是否匹配。nomad alloc status <alloc-id>:查看单个 allocation 的详细信息,包括节点、资源预留、任务状态。nomad alloc logs <alloc-id>:看任务日志,排查进程异常原因。nomad eval status <eval-id>:如果 Job 提交后一直没有 allocation,回看 evaluation 的输出,看是资源不足、约束不满足还是调度器报了错。nomad deployment status <deployment-id>:如果是发布场景,看 deployment 的进度和失败点。
这套顺序基本覆盖了 90% 的问题。真正难处理的是节点级故障导致 allocation lost,这时候需要回到节点层面排查,但思路依然是“先确认期望状态,再对比实际状态,然后逐层深挖”。
在文章开头我提到过我在生产环境里踩过的那个坑,改 CPU 限制却不生效。现在回头看,原因就是我当时没有意识到,在 service 类型的默认更新策略下,resources 的变更虽然会触发 deployment,但如果 deployment 被之前的操作卡住了,或者上次更新进入了一个失败的停滞状态,新的 deployment 会排队等待,而不是抢占执行。这个“deployment 排他性”是另一个隐藏很深的坑——同一个 job 同一时间只允许一个活跃的 deployment,如果旧的 deployment 没有成功或取消,新的变更可能不会像预期那样立刻推进。此时正确的处理方式是先检查旧的 deployment 状态,必要时用 nomad deployment cancel <id> 取消卡住的 deployment,再重新提交 Job。
写在最后
Job 与 Allocation 的关系,本质上是一种“期望与现实”的关系。Job 是声明,是契约,是一份理想;Allocation 是调度器基于现实约束,在物理世界上做出的折中与落地的结果。理解它们不是从属关系,而是实时双向校验的关系,你才算真正掌握 Nomad 的任务定义艺术。
我个人在实际操作中最大的体会是:写 Job 的时候永远要追问一句“如果调度器现在执行我的定义,会创建出什么样的 allocation”;排查问题的时候永远要反问一句“当前 allocation 的状态,真的对应了我期望的 Job 状态吗”。这两个问题想透了,Nomad 的很多反直觉行为都会变得顺理成章。最后再分享一个小技巧:多在生产环境用 nomad job plan 和 nomad job allocs 这两个命令,一个帮你校验期望状态,一个帮你观测实际状态,它们的组合价值远远被大多数人低估了。
