Nomad 只有一个二进制,server 和 client 都是它,装完就能跑 job,这套简洁感很容易让人以为"配置这件事它也替我省了"。真上了规模你会发现,坑恰恰在这份简洁里:job 文件里不写 resources,调度器不会拦你,它会按默认值把任务安排到任意一台节点上,集群状态一片绿,直到某台机器被几个没声明资源的任务撑到失联。这篇不聊 Nomad 和 Kubernetes 谁功能多,只盯两件事——资源声明的粒度,以及调度失败时它把问题暴露给你看的方式。把这两件事搞清楚,你才知道该租几台机器、每台该给多少核多少内存。
第一次跑通 Nomad 的体验确实很爽:下载一个二进制,写份二十行的 job 文件,nomad job run 一敲,alloc 就起来了。没有 etcd 要调,没有 CNI 插件要配,没有一堆 CRD 要先装。这种"零前置"的代价是:Nomad 默认对你的业务一无所知,它只相信 job 文件里写出来的数字。你写多少,它就按多少算;你不写,它就按默认值算,然后高高兴兴给你一个绿色的部署结果。
问题在于,绿色的部署结果和健康的系统之间隔着很远。调度成功只意味着"有一个节点的剩余资源满足你声明的数字",跟"这个任务跑起来真的只用这么点"完全是两码事。我见过最典型的现场是:三台 32 核 128G 的机器,跑了四十多个 alloc,nomad node status 一看 CPU 分配了 85%,实际 CPU 利用率常年 15%,同时每周总有一两次某个节点内存被打爆、一堆 alloc 同时进入 pending——因为有一半的 job 压根没写 resources,另外一半写的是从别人那儿抄来的数字。
task 级别的 resources 不写,Nomad 会按内置默认值落位:CPU 100 MHz、内存 300 MB、磁盘 300 MB。100 MHz 是什么概念?现在随便一台物理机 fingerprint 出来的 CPU 总量都是几万 MHz,比如双路 16 核 2.4GHz 的机器大概是 76800 MHz。也就是说,一个不写 resources 的 task,在调度器眼里只占这台机器的千分之一。
后果很直接:你可以在一台机器上塞进几百个这样的 alloc,调度器全程给你放行,因为它觉得这台机器还空着大半。等真正跑起来,这些任务各自吃掉 1–2 GB 内存、偶尔吃满一个核,机器就开始 swap、开始 GC 抖动、开始丢心跳,最后 client 节点被 server 标记为 down,上面所有 alloc 一起被 reschedule 到别的节点——而别的节点同样"看起来很空",于是雪崩就发生了。这才是 Nomad 上手快、翻车也快的真正含义:它不阻止你犯错,它只是把错误的账单推迟到凌晨三点寄给你。
group 或 task 里的 resources 块是给调度器看的。调度器拿它做两件事:一是可行性判断(这个节点剩余的资源够不够放下这个 alloc),二是打分排序(放哪个节点更合适)。它是纯粹的账本数据,调度器不会去核对你的进程实际用了多少,也不会主动去限制你的进程。你声明 512 MB,实际用 8 GB,调度器全程无感,直到 cgroup 或者内核出来收场。
这个块里能写的字段就那么几个,但粒度差别很大:cpu 的单位是 MHz,memory 的单位是 MB,disk 是临时盘 MB,cores 是预留的物理核(给按核授权的商业软件用,Nomad 会做 CPU 亲和性绑定),网络部分单独在 network 块里写 mbits 和 port。字段少是好事,少就意味着没有太多"差不多就行"的余地。
Linux client 上,Nomad 把声明翻译成 cgroup 参数。CPU 这一路,Nomad 把 MHz 映射成 CPU shares(cgroup v1 的 cpu.shares / v2 的 cpu.weight)—注意是"份额"不是"配额"。份额是个相对权重:节点空闲时,你的任务可以随便超用 CPU,跑到请求值的三倍五倍都没人管;节点上有争抢时,大家按份额比例分时间片,超用的部分被 CFS 调度器节流。
内存这一路的性质完全不同。内存是不可压缩资源,没法"暂时少给一点",进程申请了就得给它物理页。Nomad 会给 alloc 建一个 memory cgroup 并设上限,超过上限就是 OOM。这也是为什么调 Nomad 的资源声明时,CPU 和内存要分开想:CPU 写错了顶多是慢,内存写错了是直接死。
memory_max 是资源声明里最容易被跳过的字段,也是最值得写的一个。它的语义是"这个 task 最多允许用到多少 MB",实现上就是 cgroup 硬上限。设了它之后,内存使用会在 memory(调度请求值)和 memory_max(硬上限)之间有一个可突发的区间:平时按请求值算装箱,业务高峰允许往上顶一顶,顶穿了 memory_max 才被杀。
被杀的方式很关键。硬上限是加在 alloc 自己的 cgroup 上的,所以 OOM 发生在这一个 cgroup 内部,内核只会在你这个 alloc 的进程里挑牺牲品,同节点的其他 alloc、系统进程、Docker daemon 都不会被牵连,Nomad 会记录一条 OOM 事件,然后按 restart 策略决定是原地重启还是标记失败。而不设 memory_max 时,硬限制通常回落到 memory 的声明值(不同版本与 driver 的细节有出入,上线前用 nomad alloc status 和实际压测确认一次最稳),任务刚越过请求线就被杀;更糟的情况是整节点内存耗尽触发内核全局 OOM killer,这时候谁被杀全看内核的 oom_score 打分,很可能是你的数据库而不是那个真正吃内存的任务。
所以"只声明请求、不设上限"有两种结局:设了上限但没写 memory_max 的,任务完全没有突发空间,抖动一下就死;节点层面没有任何隔离的,全机一起陪葬。两个都不体面。
写 Nomad job 的人,十个里有八个第一次会把 cpu = 4 当成"我要 4 个核"。这是错的,这里填的是 4 MHz,也就是 0.004 核——一个连 Hello World 都跑不利索的量。反过来,也有人知道是 MHz,于是很豪爽地写下 cpu = 2000,以为是 2 GHz 的核,实际只是 2 核机的十分之一。
正确算法是先知道节点总量:nomad node status
理解这一条,比背下所有字段都有用。CPU 时间是可以欠着再补的,进程拿不到 CPU 只是变慢,不会崩溃(超时是另一回事,那是业务层的判断)。内存是刚性需求,缺一页就是缺一页,没有"慢一点也能过"的选项。
这个区别决定了两者的声明策略完全相反。CPU 声明可以写得偏紧:按常态水位写,把突发留给节点的空闲时间,反正份额机制会保证争抢时你拿到的比例大致对得上声明值。内存声明必须写得偏松:RSS 峰值再乘个安全系数,否则一次大请求、一次缓存预热就直接把任务送走。我自己的习惯是 CPU 按 P90 写、内存按 P99 乘 1.2 写,配合 memory_max 再松一档做突发区。这样调度器看到的账本不会虚胖,节点上又能留下真实的缓冲。
Nomad 的默认调度算法就是 binpack(装箱)。逻辑很朴素:所有通过可行性检查的节点里,哪个节点装得越满,得分越高。它的设计意图是让任务尽量集中,剩下的节点可以空出来、被 drain 掉、被自动伸缩器回收,从而省钱。在云上按量付费的环境里,这个默认策略非常对;在你自己租物理机的场景里,它同样对——前提是那些空节点你真的敢关。
binpack 的打分依据可以在 server 的 scheduler 配置里指定,可以按 CPU 权重、按内存权重,或者按自定义指标(比如把 GPU 数量、某个 meta 标签作为维度)。这一步经常被忽略:如果你的业务是内存密集型的,而打分只看 CPU,那装箱结果会往 CPU 余量大的机器上堆,最后内存先见底。默认配置未必适配你的负载形状,改一个 binpack { weight } 就能把利用率拉上去几个点。
spread stanza 让 Nomad 按某个属性(datacenter、机架、node meta、甚至 node id)把 alloc 摊开,可以配 target 给每个维度设百分比权重,还支持跨多种目标同时摊。constraint 是硬过滤(不满足直接判不可调度),affinity 是软打分(加分不强制)。三者的边界要分清:能接受"宁可调度失败也不放这儿"的用 constraint,能接受"尽量但不行就算了"的用 affinity,想控制分布比例的用 spread。
从租机器的角度看,这三者直接换算成机器数量。要求三副本必须跨三台物理机,那你至少得有 3 台 client,多一台做故障接管就是 4 台;要求跨机架,那你租的时候就得明确要不同机架的机器,而不是同一柜里挨着的三台。很多团队的"高可用"最后是假高可用——因为他们的 spread 目标是 node 属性,而三台机器其实在同一个电源回路下。
声明太保守(数字往大了写)的后果是"节点看着满了,CPU 才用了 20%"。这时候 binpack 认为没地方放新任务,你被迫加机器,而加进来的机器同样只用 20%。这是最容易被当成"Nomad 性能不行"的假象——其实是账本记错了。
声明太激进(数字往小了写)的后果更隐蔽。表面利用率漂亮得能拿去汇报,实际是单机塞了太多任务,大家都在争抢同一批核、同一块磁盘、同一份 page cache。CPU 争抢会表现为 P99 延迟拉长(因为是份额节流,不是硬限),内存争抢会表现为偶发 OOM,IO 争抢会表现为"没有任何指标异常但就是慢"。这类问题排查起来最费时间,因为所有监控都指向"资源充足"。
校准的办法就在 Nomad 自己身上,不需要额外上监控系统:nomad node status 的输出里同时有 Allocated Resources(账本:声明了多少)和 Allocation Resource Utilization(实测:alloc 实际用了多少),还有 Host Resource Utilization(整机实际用量)。三个数字摆一起看,偏差超过 30% 就该回去改 job 文件了。
下面这张表把 Nomad 里真正参与调度的几个维度摊开。看的时候请记住一条主线:调度器只认识写出来的数字,不认识进程的实际行为,所以"不写"这一栏的后果往往比"写错"更严重。
| 资源/配置维度 | 调度器怎么用它算装箱 | 不写或写错的后果 | 规划建议 | 常见误判 |
|---|---|---|---|---|
| resources.cpu(MHz) | 折算为 cgroup CPU shares 权重,参与可行性判断与 binpack 打分,不是硬性上限 | 不写按默认 100 MHz 落位,单机可塞进数百个 alloc,跑起来集体被 CFS 节流 | 按常态水位写,留 20%–30% 余量;按节点总量的比例换算而非按 GHz 猜 | 把 MHz 当成核数填(cpu = 4 当成 4 核) |
| resources.memory(MB) | 作为硬约束参与可行性判断,节点剩余内存不足直接判该节点不可调度 | 不写按默认 300 MB 落位,实际用几 GB 也不会被拒绝,节点被撑到失联后全量重调度 | 按 RSS 峰值 ×1.2 写;与实测利用率对比,偏差超 30% 就回改 job | 以为内存和 CPU 一样能超用、只会被节流 |
| memory_max(MB) | 作为 alloc 级 cgroup 硬上限,不参与装箱计算,只在运行时兜底 | 不写时硬限制回落到 memory 声明值,任务越线即死;节点层面无兜底时触发内核全局 OOM 乱杀进程 | 请求值之上再给一档突发空间,重启策略与告警同步配好 | 以为设了 memory_max 就等于可以无限超用 |
| network 端口(静态/动态) | 静态端口构成节点上的硬约束,动态端口从 client 的 port_range 池里分配 | 静态端口写死导致同节点冲突,第二个 alloc 起不来并反复耗尽重启次数;动态端口没接服务发现则调用方找不到实例 | 一律用动态端口,配合 Consul 或 Nomad 原生服务发现;核对 port_range 与 ulimit -n | 以为单机能起的副本数只受内存限制 |
| ephemeral_disk 与 IO | disk 只计入临时盘 MB 容量,Nomad 不感知 IOPS、吞吐与磁盘队列 | 多个 IO 密集 alloc 被塞到同一台机器,调度器完全不避让;alloc 换节点后临时盘数据丢失 | IO 密集任务用 spread 摊开或独占节点类;有状态数据走 host volume、CSI 卷或外部存储 | 拿 ephemeral_disk 当有状态存储还指望能迁移 |
| Server 节点磁盘与内存 | 不参与装箱计算,但 Raft 日志 fsync 延迟决定选举心跳是否超时 | 用高延迟盘或与重负载 Client 混布,会出现 leader 抖动、评估积压、任务下发停滞 | 3 或 5 台 Server,企业级低延迟 SSD,独占机器不跑业务任务 | 觉得 Server 只跑一个进程,随便给台机器就行 |
| client.reserved 预留 | 预留的 CPU/内存/磁盘/端口从节点可分配量里先扣掉,调度器不再触碰 | 不预留时系统进程、日志、容器运行时与业务 alloc 抢同一份资源,节点被压垮失联 | 给操作系统与守护进程留 1–2 核、若干 GB 内存与独立日志盘,并预留监控端口 | 认为节点标称资源全部可以分配给任务 |
这张表还有个用法:新集群上线前,拿它当 checklist 逐项过一遍 job 文件和 client 配置。七个维度里有任何一个"没写",都不算配完。
网络端口是 Nomad 里最容易被当成"配置细节"正经资源。在 network 块里写 port "http" { static = 8080 },等于向调度器声明"我必须占住这个节点上的 8080"。新版本的 Nomad 会跟踪节点上已分配的端口并在打分阶段给出反馈,但实践里更常见的现象是两个 job 都想要 8080、一台机器上先到的占了、后到的要么起不来、要么在容器里绑定失败,把 restart 次数耗光后进入 failed。
静态端口还有个隐性代价:它把"这台机器能跑几个实例"从内存约束变成了端口约束。你有 256 GB 内存,理论上能跑三十个实例,但只要它们都要 8080,实际只能跑一个。剩下的 250 GB 内存就那么空着,而 binpack 因为别的原因还可能继续往这台机器上塞别的任务。这种浪费非常难从监控上看出来,因为 CPU 和内存都显示"有余量"。
不指定 static 的端口就是动态端口,Nomad 从 client 配置的 port_range 里挑一个没被占用的分配给你,默认范围通常是 20000–60000 这一段。分配结果通过环境变量 NOMAD_PORT_
那么一台机器上到底能跑多少个用动态端口的实例?第一道坎是 port_range 的大小——四万个端口听起来很多,但每个实例通常不止占一个(HTTP、gRPC、metrics、健康检查各占一个的话,四万除以四就剩一万);第二道坎是文件描述符上限 ulimit -n,每个监听 socket 加每条连接都吃 fd,高并发服务在这上面先撞墙;第三道坎是 TIME_WAIT 状态下的连接堆积,短连接密集的服务会把可用端口消耗得比想象中快。内存的排名在这三条后面。想提高单机密度,先把这三道坎量清楚,而不是一味加内存。
动态端口解决了冲突,但同时制造了一个新问题:调用方不知道你这次被分到哪个端口。所以动态端口必须配套服务发现,否则它带来的麻烦比静态端口还多。两条路:一是接 Consul,Nomad 的 service 块会自动把 alloc 的 IP 和动态端口注册上去,健康检查通过才对外可见;二是用 Nomad 原生服务发现,直接在集群 DNS 里以 service.nomad 域提供解析,不必额外维护一套 Consul。
选哪条取决于你还用不用 Consul 做别的。只做服务发现和健康检查的话,Nomad 原生那套更轻,少一个二进制少一份运维;已经上了 Consul 做 KV、做 connect 网格的,继续用 Consul 更一致。无论哪条,service 块里的 check 一定要写,而且是检查真实业务端口而不是随便 ping 一下——否则流量会被导到还没 ready 的实例上。
resources 里的 disk 字段,单位是 MB,管的是临时盘的空间占用,仅此而已。Nomad 的 fingerprint 不会去测这块盘是 SATA 还是 NVMe、能跑多少 IOPS、队列深度多少,调度器在打分时对 IO 完全无感。它的世界观里,磁盘就是一个容量计数器。
这意味着:三个 IO 密集的任务(比如一个在做大批量的日志落盘、一个在跑本地索引构建、一个在做数据库 compaction)完全可能被 binpack 塞到同一台机器上,因为它们的 CPU 和内存声明都不大、节点账本上还很空。调度器放行,然后这三兄弟开始抢同一块盘的 IO 带宽,所有任务的延迟一起上天。你去看监控,CPU 不忙、内存不忙、磁盘容量才用了 40%,只有 IO await 高得离谱。
ephemeral_disk 块里的 size 声明的是这个 alloc 能用的临时空间,它的生命周期和 alloc 完全一致:alloc 被停掉、被驱逐、被 reschedule 到别的节点,这块盘上的东西就没了,一点不留。
这里最容易踩的是 sticky = true。很多人看到"sticky"就理解成"数据会粘在机器上、能保住",实际它的语义只是 Nomad 在重调度时尽量把这个 alloc 放回原来的节点上,它既不保证节点故障后还能放回去,也不会把磁盘内容搬到新机器上。节点真的挂了、真的被 drain 了,alloc 落到别的机器上就是全新的一块空盘。真正意义上的"能迁移的有状态存储",走的是 host volume(手工绑定到具体节点的目录)、CSI 卷(Nomad 0.11 之后支持,对接外部存储系统),或者干脆把状态放到外部的分布式存储里。把有状态任务放在 ephemeral_disk 上还指望它能迁移,是这批坑里后果最严重的一个——因为它是"平时一切正常,出事那天才发现什么都没了"。
既然调度器不管,就得自己在 job 层面绕。几个能立刻用上的办法:给 IO 密集的任务打上专属的 node_class 或 meta 标签,用 constraint 把它们限定在专门的一批机器上,这些机器配企业级 NVMe,别的任务不许进;同一批 IO 密集任务之间用 spread 按 node 摊开,避免集中;把落盘时间错开(cron 类任务把触发时间打散,别都卡在整点);如果是 docker driver,可以用 cgroup 的 blkio 限制(Nomad 本身没把这个暴露成 job 字段,需要在 driver 或系统层面处理)。
说白了,Nomad 把 IO 隔离这件事留给了架构设计而不是配置文件。这一点跟 Kubernetes 的差别不在"谁更先进",而在 K8s 至少把 requests/limits 的语义统一到了可扩展的资源模型里,Nomad 则是明确告诉你"这块我不管,你自己看着办"。接受这个前提,配置才做得对。
Nomad 的 server 跑 Raft 共识,数量必须是奇数,因为要能形成多数派。3 台的集群容忍 1 台故障,5 台容忍 2 台。选哪个不是"越大越好",而是看你的机房环境和故障预期:
这是整篇里最容易被忽略、但一旦踩到就最要命的一条。Raft 协议要求每条日志在提交前必须持久化(fsync)到磁盘。server 的磁盘 fsync 延迟高,直接后果是心跳和日志复制超时,follower 认为 leader 失联,发起新选举;新 leader 上来又卡在同一个慢盘上,于是选举反复发生,集群进入"选举震荡"——这时候所有调度决策全部停滞,job 提交没反应,client 心跳得不到响应,alloc 状态不更新。
触发这种震荡的往往不是"磁盘坏了",而是"磁盘被别的负载挤占了"。一台 server 上如果还跑了日志采集、监控 agent 高频写盘、或者干脆混跑了业务 client,fsync 延迟能从亚毫秒跳到几十毫秒。所以 server 的磁盘选型只有一条硬标准:低延迟的企业级 SSD,并且这块盘别给别的东西用。容量反而不重要,Raft 日志加状态存储的量通常不超过几十 GB,随便一块 480GB 的盘都绰绰有余,钱要花在延迟上而不是容量上。
内存方面,server 的压力来自集群规模:节点数、job 数、alloc 数越多,状态存储占的内存越大,评估(evaluation)批量处理时的瞬时内存也不低。中小集群给到 16 GB 起步比较从容,上百节点、数千 alloc 的集群建议 32 GB 往上,并且监控 server 进程的 RSS 走势——它涨起来通常意味着该清理旧 job 版本或者该扩容了。
把 server 和 client 跑在同一台机器上,是 Nomad 部署里性价比最差的一个决定。理由不是"不安全",而是资源抢占模型的冲突:client 上的任务是突发性的、无度的(尤其是没写 resources 的那些),server 需要的却是稳定的、低抖动的 CPU 时间片和磁盘响应。当一台 client 上跑满 alloc、CPU 被吃光、page cache 被刷爆的时候,同机 server 的 fsync 立刻变慢,Raft 心跳随之超时,整个集群跟着一起抖。
更麻烦的是故障排查的方向感。混布环境下,一旦出现选举震荡,你很难第一时间判断是 server 自己的问题还是被同机的某个 alloc 拖累的,因为所有指标都指向"这台机器忙",而"忙"的原因藏在十几个 alloc 里。分开部署之后,server 的负载曲线几乎是平的——三条稳稳的直线,任何偏离都值得立刻查。
唯一可以接受混布的场景是极小规模的测试环境:单台机器跑一个 server 加一个 client,用来验证 job 文件能不能跑通。这种环境掉几次选举无所谓,但别把它当成生产架构的原型。
client 节点的标称资源不等于可分配资源。操作系统本身、Nomad agent、容器运行时、日志采集、监控 agent、SSH 这些东西都要吃 CPU 和内存,而 Nomad 默认会把 fingerprint 到的全部资源都当作可分配的。解决办法是 client 配置里的 reserved 块:把要给系统留的 CPU(MHz)、内存(MB)、磁盘(MB)和端口先扣掉,调度器就只会用剩下的部分算装箱。
这一刀切多大,取决于 driver。用 docker driver 的机器上还得给 daemon 和镜像层留余量;跑 qemu 的机器要给虚拟机管理程序预留。经验值是从 1–2 个核和几 GB 内存起步,然后看节点的 Host Resource Utilization 和 Allocated Resources 的差值慢慢调。预留写小了,节点会被压到心跳超时;写大了,纯粹浪费。别忘了 reserved 里也能写 reserved_ports,把监控、SSH 这些端口锁住,免得被动态端口分配撞上。
driver 决定任务以什么形式跑在机器上,也就决定了有多少资源是被"运行时"吃掉而不是被业务用掉。同样的 job 声明,换一个 driver,单机密度可能差一倍。这部分开销在 Nomad 的调度账本里是不可见的——调度器只知道你声明了 512 MB,不知道 docker 为了拉起这个容器另外花了多少。
四个 driver 的资源画像可以这么记:
选 driver 的实用原则是这样的:能用 exec 就别上 docker,省掉一整层守护进程;需要打包发布和依赖隔离就老实上 docker,别为了省那点内存把发布流程搞复杂;不信任的、需要强边界的负载上 qemu;raw_exec 只留给临时救急。集群里的 client 也可以按 driver 分角色——一批机器专门跑 exec 任务、一批专门跑 docker,各自做各自的 reserved 和监控基线,比全都混在一起的"通用节点"好管得多,排查问题时也不用在一堆进程里猜是谁吃的内存。
资源声明不是写完就定终身的东西,它需要一轮一轮校准。最省事的校准数据源就在 Nomad 自己身上:nomad node status <节点ID> 会同时给出三组数字。Allocated Resources 是账本,也就是所有 alloc 声明量加起来的总和;Allocation Resource Utilization 是实测,是这些 alloc 真正用了多少;Host Resource Utilization 是整机视角的实际用量,比前一组多出来的部分就是系统、守护进程和容器运行时的开销。
把这三组数字记上两周,规律基本就出来了。账本远高于实测的,说明声明虚胖,把 cpu 和 memory 往下调,腾出来的余量能少租一台机器;实测贴近甚至顶到硬上限的,说明声明偏紧,赶紧往上加并配 memory_max;整机用量比 alloc 用量高出一大截的,说明 reserved 写小了,该给系统多切一点。集群规模大以后这套人工比对会累,可以把这些数据导进时序库画趋势图,但逻辑不变——永远看"声明值"和"实测值"的差值,而不是只看某一个绝对值。
Nomad 在调度失败这件事上做得比很多调度器诚实:它不满足于告诉你"放不下",而是会告诉你"有多少台机器被筛掉了、分别是因为什么"。nomad job status
要更细的颗粒度,就去看 Nomad 自带的 Web UI 里的调度可视化:它会把每个候选节点的打分明细展开,binpack 加了多少分、spread 加了多少、affinity 加了多少,一目了然。很多人对着"为什么调度到这台机器"百思不得其解,其实那个页面三秒钟就能给答案。真遇到集群级别的问题(比如评估积压、节点反复失联),nomad operator debug 能把一段时间内的集群状态、日志和 API 输出打包成一个压缩包,交给别人排查或者自己留档都好用。
为什么坑:调度器按默认的一百 MHz 和三百 MB 落位,一台机器能塞进几百个这样的 alloc,账本上还显示"很空"。真正的压力全部堆到运行时,节点被撑爆后上面所有任务一起重调度,容易引发连锁反应。
怎么避:把 resources 当成 job 文件的必填项,用 CI 检查或者 job 模板强制约束——没有 resources 块的 job 直接不许提交。哪怕先填一个粗糙的估算值,也比默认值强,后面再用实测数据校准。
为什么坑:业务任务的突发 CPU 和刷盘会抬高同机 server 的 fsync 延迟,Raft 心跳随之超时,选举反复发生,整个集群的调度全部停滞。而且混布之后故障归因极难,所有指标都指向"这台机器忙"。
怎么避:Server 三台或五台独立机器,配低延迟企业级 SSD,不跑任何业务 alloc(client agent 都不装)。测试环境可以混布,但不要把测试环境的结构照搬到生产。
为什么坑:端口变成节点上的硬约束,同节点第二个实例要么调度失败、要么运行时绑定失败并耗尽重启次数;同时把"单机能跑几个"从内存约束变成端口约束,剩下几百 GB 内存空转。
怎么避:一律用动态端口,读到 NOMAD_PORT_
为什么坑:临时盘的生命周期跟 alloc 绑死,换节点就是换空盘。sticky = true 只是"尽量放回原节点"的偏好,不承担数据不丢的承诺。这类问题平时完全看不出来,只在节点故障那天一次性爆发。
怎么避:状态数据走 host volume、CSI 卷或者外部存储服务;真要用本地盘,就在 job 里明确标注不可迁移,并把备份和恢复流程做成日常演练的一部分,而不是出事才想起来。
为什么坑:cpu = 4 在 Nomad 里是 4 MHz,不是 4 核。按核数填出来的任务会拿到极低的 CPU 份额,一旦节点上有争抢就被严重节流,表现为"资源充足但就是慢"。
怎么避:先查节点的 CPU Total Compute,再按比例换算;在团队内部的 job 模板里写死注释说明单位;对确实需要整核的业务用 cores 字段做预留,而不是拿 MHz 去凑。
为什么坑:节点标称资源被全部当成可分配,操作系统、容器运行时、日志与监控 agent 只能和业务任务抢同一份资源,节点被压到心跳超时失联,上面的 alloc 全部重调度。
怎么避:在 client 配置里按 driver 类型预留一到两个核、若干 GB 内存和独立日志盘,用 reserved_ports 锁住监控与运维端口,再按实测差值逐步微调。
为什么坑:Nomad 不感知 IOPS 和吞吐,只算容量。几个 IO 密集任务被塞到同一台机器上时调度器毫无察觉,结果是 CPU、内存、容量三项指标全部正常,只有延迟全线劣化,排查起来最费时间。
怎么避:给 IO 密集任务打 meta 标签并用 constraint 限定到专门的节点池,池内机器配企业级 NVMe;同类任务之间用 spread 按节点摊开;批量任务的触发时间打散,别都卡在整点。
把前面的分析落到机器上,出机清单其实就两类,而且这两类的配置方向完全相反,绝不该用同一档机器去凑。
Server 节点(3 台或 5 台):要的是低延迟而不是大资源。企业级 SSD 是硬指标,容量给个几百 GB 就足够(Raft 日志加状态存储撑死几十 GB),钱要花在 fsync 延迟上;内存中小集群 16 GB 起步、规模上去再加到 32 GB;CPU 四到八核足够。这批机器不装 client,不跑业务,负载曲线应该平得像条直线。
Client 节点(按任务画像扩):要的是核数与内存的总量。跑 docker driver 的机器内存给足(常规业务 128 GB 起,密度高的场景往 256 GB 走),核数按 CPU 总量 MHz 折算成你要的并发份数;跑 exec driver 的可以更激进地堆密度;有 IO 密集任务的机器单独配 NVMe 并单独成池;需要强隔离的负载预留支持硬件虚拟化的机型跑 qemu。每台 client 记得在系统层面留出 reserved 的那一刀。
出机方式上,一万网络深耕 IDC 19 年(成立于 2007 年),可以按 Server 节点与 Client 节点分别出机——Server 用低延迟 SSD、Client 用大内存多核,不用被迫拿同一档配置去将就两种完全不同的负载曲线;深圳自营机柜、最快 1 分钟上架,扩 client 的时候不用等上一周;7×24 中文工单、平均 5 分钟响应,半夜遇到选举震荡能立刻找到人。价格需实时询价,价格主要受 CPU、内存、存储、带宽和 IP 影响。
以下为典型部署思路,并非特指某一真实客户:一个二十来个微服务、三四十个实例的团队,通常 3 台 Server 加 4 到 6 台 Client 起步,其中 1 到 2 台 Client 打成 IO 专属池。规模再往上,加 client 比加 server 划算——server 只在节点数和 alloc 数真正压到它的时候才需要扩。
Q1:Server 到底要 3 台还是 5 台?
A1:看你一次能承受丢几台。3 台容忍 1 台故障,5 台容忍 2 台。单机房、换机能在小时级内完成的团队,3 台足够,也是多数人的起点;需要跨机架、或者要在一台故障的同时还能计划内维护另一台的,上 5 台。代价是每次写入要多等一票确认,机器成本多六成左右。别为了"看起来更稳"硬拉三个相距很远的数据中心——Raft 的提交延迟取决于最慢那一票,跨城带来的几十毫秒会把选举超时逼出来,反而更不稳。
Q2:Server 能不能和 Client 混布在一台机器上?
A2:测试环境随便混,生产环境别干。原因不是安全,是资源抢占模型冲突:client 上的任务突发、无度(尤其是没写 resources 的那些),server 要的是稳定的 CPU 时间片和低抖动的 fsync。一台 client 跑满 alloc 的时候,同机 server 的磁盘延迟立刻被抬高,心跳超时、选举震荡、全集群调度停滞。混布还让故障归因变得极难,你分不清是 server 自己的问题还是被某个 alloc 拖累的。分开部署之后 server 的负载曲线几乎是平的,任何偏离都值得立刻查。
Q3:CPU 那个 MHz 到底怎么换算?
A3:先查节点总量再算比例,别拿 GHz 硬猜。nomad node status 里的 CPU Total Compute 就是 fingerprint 出来的 MHz 总数,比如 76800 MHz 大致对应 32 核 2.4GHz(含睿频折算)。想要这台机器八分之一的 CPU 份额,就写 9600 左右。不同代际 CPU、开不开超线程、睿频策略不同,换算比例都不一样,所以最稳的做法是"按节点总量比例写",而不是"按主频算"。真需要独占核的业务(比如按核授权的商业软件)用 cores 字段做预留,不要拿 MHz 去凑。
Q4:内存超了是 OOM 还是被驱逐?
A4:取决于你有没有设 memory_max。设了的话,超线发生在这个 alloc 自己的 cgroup 内部,内核只在你这批进程里挑牺牲品,同节点的其他 alloc 和系统进程不受牵连,Nomad 记一条 OOM 事件,然后按 restart 策略决定是原地重启还是标记失败——这是最理想的情况。没设的话,硬限制通常回落到 memory 的声明值,任务刚过请求线就被杀,业务上表现为"莫名其妙重启"。最糟的是节点层面也没有任何兜底,整机内存耗尽触发内核全局 OOM killer,这时候谁被杀全看 oom_score,倒下的很可能是你最不想失去的那个进程。Nomad 本身不会主动"驱逐"内存超用的任务,只会在它死掉之后按策略处理。
Q5:有状态任务到底该怎么放?
A5:唯一正确的答案是别放 ephemeral_disk。临时盘的生命周期跟 alloc 绑死,alloc 被停、被驱逐、被 reschedule 到别的节点,数据就清零,换节点就是换一块全新空盘。sticky = true 只是"尽量放回原节点"的偏好,不保证节点故障后还能放回去,也不会搬数据。真要在 Nomad 里跑有状态负载,走 host volume(绑到具体节点的目录,代价是绑死了不能随便迁)、CSI 卷(对接外部存储,可迁移性最好),或者把状态直接交给外部的分布式存储/数据库,让 job 保持无状态。最后这条路最省心,也最符合调度器的设计假设。
Q6:端口冲突了怎么办?
A6:先把静态端口换掉。static 写死等于给节点加了一条硬约束,同节点的第二个实例要么调度失败、要么运行时绑定失败并把 restart 次数耗光。改成动态端口,让任务从 NOMAD_PORT_
Q7:多少节点才值得上 Nomad?
A7:与其数机器,不如数"需要被管理的任务形态"。三台以内的机器、跑五六个长期稳定的服务,手写 systemd 加一份部署脚本可能更省事,Nomad 的收益体现不出来。真正的分水岭是这几个信号同时出现:任务类型开始杂(长服务 + 批处理 + 定时任务混在一起)、需要跨机器做故障自动恢复、需要按资源余量自动选机器而不是人工指定、团队开始有多个人要提交部署。这时候 Nomad 的价值才压过它的学习成本。机器数量上,一般五六台 client 以上、几十个实例以上,账就算得过来;再多一点,它省下的人力会比它消耗的运维精力明显更多。
Q8:和 Kubernetes 比,什么时候 Nomad 更简单?
A8:当你要调度的是"进程"而不是"平台"的时候。Nomad 只有一个二进制,server 和 client 都是它,没有 etcd、没有 CNI 插件、没有 CRD 生态,装完就能跑 job;它还能把非容器负载(裸二进制、虚拟机、批处理)和容器放在同一套调度逻辑里管,这在 K8s 里要靠一堆附加组件拼。反过来,一旦你需要成熟的 Ingress、Operator 生态、复杂的网络策略、声明式的有状态编排,K8s 的"重"就变成了"现成",而 Nomad 的"轻"会变成"要自己造"。简单说:负载形态杂、团队规模小、不想养平台团队,Nomad 更简单;要长在生态上、要一堆开箱即用的周边,K8s 更省事。这跟谁功能多没关系,是两种完全不同的取舍方向。
回到开头那个问题:job 文件里不写 resources,Nomad 不会拦你,它按默认的一百 MHz、三百 MB 把任务安排下去,集群状态一片绿。这台机器会在账本上一直显示"还有余量",于是更多没声明资源的任务被塞进来,直到某一天内存被打穿、心跳超时、节点失联,上面的几十个 alloc 一起被重调度到别的"看起来也很空"的节点上。
所以这篇反复强调的其实是一件事:Nomad 的调度器只认识写出来的数字,不认识进程的真实行为。你给它的账本准,它的装箱就准,你租的每一台机器都花在刀刃上;你给它的账本虚,它就老老实实按错的账本去装箱,然后在一个你没预料到的凌晨把账单寄回来。资源声明的三个层次(请求、cgroup、memory_max)、CPU 按 MHz 的可压缩语义、内存按 MB 的刚性语义、端口这个隐形资源、磁盘 IO 那块它明确不管的地带、Server 节点吃 fsync 延迟而不是 CPU——把这六件事理清楚,剩下的就是每次上线前跑一遍 nomad job plan,然后拿实测数据把声明值一轮轮校准。
我的立场很明确:Nomad 值得上,但别因为它上手快就跳过配置这一步。它把复杂度从"安装部署"挪到了"声明准确",这个交换对大多数团队是划算的,前提是你知道复杂度被挪到哪儿去了。
本文涉及的资源字段语义(resources 的 cpu/memory/memory_max/cores/disk、network 块的端口与 mbits、ephemeral_disk 的 size/sticky/migrate)、调度打分策略(binpack 为默认算法、spread 与 affinity 的作用位置)、driver 差异与 client.reserved 的行为,均以 HashiCorp Nomad 官方公开文档为准,不同版本之间存在细节差异,落地前请以你实际运行版本的文档和 nomad alloc status、nomad node status 的实际输出复核一次。文中提到的默认值、端口范围、预留经验值属于常见配置下的参考口径,具体环境请以实测为准,不保证与所有版本完全一致。
服务器租用配置部分参考自一万网络官网公开页面(https://www.idc10000.net/ ,物理机租用与裸金属相关栏目),文中给出的均为配置思路与规格方向,具体以签约时最新报价与合同为准。价格需实时询价,价格主要受 CPU、内存、存储、带宽和 IP 影响,本文不提供任何具体数字报价。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品