小请求可以在大限制下完成调度,超过节点可分配容量的请求则保持 Pending。

调度器计算资源请求,而不是瞬时使用率

Kubernetes 调度判断的是 Pod 在资源请求和其他放置约束下是否适合某个节点,不是简单查看当前 CPU 曲线,然后把任务塞进看起来最空闲的位置。请求为未来需求提供记账输入,实际使用率则是另一种观测。决定一个表面空闲的集群是否有能力容纳特定 Pod 之前,需要同时理解两者,不能让其中一个指标替代全部容量判断。 以准入后的真实配置为依据 对于普通应用容器,调度器在判断节点是否容纳 Pod 时,会计算它们的请求。初始化需求、Pod 额外开销以及受支持的 Pod 级资源配置,也可能影响有效请求。最终经过准入的 Pod 才是实际输入,包括默认值和修改结果,而不只是操作者记得提交的源文件。排查时应读取保存下来的对象,确认控制面实际看到什么。 Node allocatable 也不同于硬件总容量,它表示按照节点配置可以提供给 Pod 的资源量。其他工作负载即使暂时空闲,其资源请求仍然占用调度记账中的空间。不过,请求并不表示应用已经实际分配了同样多的物理内存,也不表示独占相应数量的 CPU 核心。放置记账、实时消耗和独占策略属于不同机制,不能混用“预留”一词解释。 限制通过执行栈约束运行中的工作负载,通常不会在调度检查时替代已经显式填写的较小请求。因此,一个容器的 CPU limit 可以大于单个节点 CPU 容量,同时仍然凭较小 request 完成调度。这个被接受的限制不会制造额外处理器,也不承诺进程一定能够达到填写的上限;它只是配置了允许使用的边界。 坏掉的样子 Pending 可能源于请求资源不足,也可能由污点、亲和性、存储、拓扑或其他条件阻塞。改变资源值之前,应查看 PodScheduled 条件和调度事件。降低请求无法修复不匹配的节点选择器或不可用的卷;如果原请求本来真实,盲目降低还可能把调度失败转化成运行期争用,并没有建立合理容量方案。 默认值让省略字段尤其容易误读。填写 limit 而不填 request,且没有准入机制提供另一默认请求时,Kubernetes 会把该资源限制复制为请求。因此,只设置一个很大的 limit,也可能形成很大的调度需求。应查看实际对象中的两个映射,并考虑 LimitRange 的影响,再解释放置失败,不能假定省略请求就等于完全没有记账成本。 ...

四月 2, 2028 · 3 分钟 · XB Field Notes
控制器清理旧任务,避免集群被已完成的工作塞满

Kubernetes CronJob 需要历史清理策略

CronJob 本身不跑你的任务:它创建一个 Job,Job 再创建 Pod。Pod 跑完之后,Job 和 Pod 都不会自动消失。Pod 是故意留着的,好让你事后还能读到它的日志;Job 留着,是为了让你还能看到这次运行的状态。 这份默认对一次运行是对的,对一万次运行就是错的。每一个已完成的 Job 都是 etcd 里的一个对象、每次列表响应里的一行,以及所有关心 Job 的控制器都会收到的一次 watch 事件。给这堆东西设边界的有两套彼此独立的机制:CronJob 控制器按数量裁的历史上限,和 TTL 控制器按时间裁的 TTL。 已完成的 Job 并不免费 已完成的 Job 会留住它的 Pod,而 Pod 只要还在,容器日志就还在。于是保留策略成了你和“命名空间堆满墓碑”之间唯一的东西。按小时调度跑三十天就是 720 次运行:720 个 Job 加 720 个 Pod,每个都有自己的名字、状态、owner reference,以及你每次 kubectl get jobs 输出里的一行。已完成的 Job 也不是完全静止的:它的 status conditions 一直留在 API 里,被每个 watcher 反复读取。单个对象的开销微不足道,但它是随时间线性增长的,所以它长期保持隐形,直到某天突然变成问题。 ...

二月 25, 2028 · 2 分钟 · XB Field Notes
同一 Pod 的两个容器拥有不同 CPU 配额和内存上限,繁忙循环记录实际节流。

cgroup 在层级中执行容器资源预算

Kubernetes 资源字段只有经过节点执行栈落实,才成为实际约束。在 Linux 上,kubelet 与容器运行时通过 cgroup 组织进程并配置资源控制器。最终形成的是一个层级,而不是简单贴在 Pod 上的单个数字。判断进程真正可以消耗多少资源之前,需要检查对应容器控制组及继承的约束,避免把清单意图直接等同于有效运行预算。 读取实际执行资源约束的控制器 在 cgroup v2 中,cpu.max 用配额与周期表示 CPU 带宽,单位是微秒。10000 100000 表示该组每个周期可使用相当于十分之一个 CPU 的时间。它不会把进程固定在一个核心上,也不会降低处理器时钟频率。组内如果存在多个可运行线程,就可能在同一周期中更快消耗完共同额度,然后等待下一段可用预算。 CPU weight 负责另一件事:在发生争用时,影响相应层级中有资格竞争的组之间的相对分配。Kubernetes 的 CPU 请求既参与调度,也可以影响这个权重。请求本身不是运行时硬上限,权重也不保证应用的墙钟延迟或独占核心。应把放置需求、相对份额和带宽上限分别理解,而不是都称为已经预留的 CPU。 内存控制器分别暴露限制与使用量。memory.max 描述控制组的硬内存边界,memory.current 报告当前记账使用。回收与 OOM 属于内存机制,不是 CPU 那样按周期进行的节流。应用堆大小也不能解释全部记账内存,调查限制问题时需要把运行时指标与控制器视图对应起来,关注额外进程、缓存和其他开销。 坏掉的样子 层级约束可能比某个本地文件显示的值更严格。子组没有本地 CPU 上限,仍可能受到祖先限制;容器的 cgroup 命名空间也可能隐藏部分层级。因此,不能看到本地 max 就推断可以无限使用整个节点。有效预算还取决于节点上下文与父级约束,容器内部可见的信息未必足以解释所有争用和节流。 ...

二月 11, 2028 · 3 分钟 · XB Field Notes
两个步骤,把节点优雅地带出集群。

cordon 与 drain 让节点安全离线

cordon 和 drain 是两件事,只是大家习惯一口气说完。cordon 给节点打上 spec.unschedulable,让调度器不再选它;drain 则把已经在上面跑的 Pod 请走。前者瞬时完成、随时可逆,后者是一个循环,要逐个 Pod 去跟驱逐 API 打交道。 矛盾在于:drain 是一个带安全护栏的客户端流程。它在删除任何东西之前会先拒绝,而每一个能越过拒绝的参数,都在拿掉一部分护栏。搞清楚哪个参数对应哪一类拒绝,就是维护窗口和线上事故之间的差别。 cordon 关门,drain 清场 kubectl cordon node-1 把 spec.unschedulable 置为 true,调度器随即把该节点从新 Pod 的候选里滤掉。已经在跑的 Pod 照常服务,这正是要先 cordon 的原因:先停止接收新工作,再开始搬走已有的工作。cordon 也是让 drain 有意义的前提——不封门,调度器会把替代副本直接放回你正在清空的那台机器上。kubectl drain 自己也会打这个标记,所以单独先执行一次 cordon 只是稳妥,不是必需。kubectl uncordon 就是反向的同一个补丁,调度器立刻会重新使用这台节点——先确认 kubelet 已经 Ready 再放它回去。 ...

二月 3, 2028 · 2 分钟 · XB Field Notes
一个让实测指标尽量贴近设定目标、从而自动调整副本数的控制器。

Horizontal Pod Autoscaler 按目标利用率扩缩容

由于它依据一段时间序列的平均值而不是单个尖峰来决定动作,你往往还要配一个最大副本数,避免扩张失控。 把平滑后的均值当作决策依据 HPA 不会拿一个瞬时读数当依据;它先按窗口对观测值取平均、做平滑,再拿去和期望值比较。这样是为了避免一次一秒的尖峰就重画副本数:先聚合,等到平滑后的数值真正越过了目标,再去动副本。因此真正要调的是三件事:指标源、聚合口径(按 Pod 平均还是集群总量),以及取样的窗口。窗口太短会放大噪声,太长又追赶不上需求。动手改 HPA 之前,先把文档里「怎么聚合、多久算一次」那段读明白,再做任何别的。 选一个你真正能改进的指标 选指标才是设计核心,因为伸缩的上限由目标定。CPU 在任何集群上都好取、对 CPU 密集的负载也顺,可它是个代理:真正排队等锁、等数据库的服务,CPU 往往是闲着的,变坏的是延迟。请求速率、或一个自定义的 queue_depth 这类指标测起来更麻烦,反映的却是用户真正感受到的东西。选一个你能对得上的指标:当它越线时,真实的体验确实变了;并且让 HPA 用和应用同样的口径去定义负载,于是它收敛的是那个让体验变差的数字,而不是那个恰好便宜的抄近道数。 自动化与初始值无关 replicas 的初值是起点,不是 HPA 的依据。一旦 HPA 接管,副本数归它管,初值只是下限或出发点。二者在最好的意义上互不相关:关掉控制器,你仍然有一个稳定、明码的陈述基数;开着一个无人调整的 HPA,它也只管自己那部分。所以下限要现实(它才是那个地板),上限要挡住成本风险的峰值,而初值只负责让最开始的几秒体面一点——在控制器的头几次采样落地之前,它撑起第一段安全期。 坏掉的样子 自定义指标不可用时不会自动回退到中央处理器。接口返回错误或空序列时,控制器把对应指标标为无效,把扩缩条件置为假,然后保持当前副本数而不是猜一个数。只配了队列深度的工作负载,会在最需要扩容时原地冻结。正确做法是把中央处理器与自定义指标并列配置,取算出副本数更多的一方,这样队列信号丢失时仍有降级可用的输入,至少不会把服务钉在原地。 适配器链路还会引入延迟。抓取间隔与缓存叠加后,读到的可能是几分钟前的队列深度,于是对已排空的工作继续加副本,造成超调。先看序列时间戳与查询延迟,再决定是否放宽目标值,每个工作负载只暴露一条预聚合序列,不要把原始高基数序列直接喂给扩缩器。 基数爆炸会从另一侧压垮同一条链路。按用户或按请求打标签,会把一个信号膨胀成数万条序列,拖慢接口,让拉取超时,读数翻转为未知。应先在监控侧聚合,只暴露工作负载级的平均值或总量,收敛序列规模后再对接自动扩缩。 目标类型配错同样致命。自定义指标只支持数值与平均值,利用率只适用于配好资源申请的资源指标。没有资源申请就用中央处理器利用率,或拿内存利用率当延迟代理,都会长期未知。先确认哪一路报未知,再把类型改成链路真正提供的那种,并补齐缺失的资源申请。 快速验证 把下面的代码存为水平扩缩模拟脚本并运行。它只用标准库,不碰任何集群,复刻两条核心规则:多指标取算出副本数更多的一方,缺失指标直接跳过,全部缺失时保持副本数不变,便于本地复现线上行为。 import math import sys def desired_for(current, value, target): """单个指标算出的期望副本数;返回空表示该路指标不可用。""" if value is None or target is None or target <= 0: return None return math.ceil(current * value / target) def decide(current, metrics, min_replicas, max_replicas): """多指标取最大值;全部缺失时保持当前副本数不变。""" wants = [] for _, value, target in metrics: want = desired_for(current, value, target) if want is not None: wants.append(want) if not wants: return current return max(min_replicas, min(max_replicas, max(wants))) def check(actual, expected, message): if actual != expected: raise AssertionError(f"{message}:实际 {actual},期望 {expected}") print(f"OK: {message} -> {actual}") def main(): current = 4 cpu = ("cpu", 80, 50) # 4 * 80/50 = 6.4 -> 7 queue = ("queue", 900, 300) # 4 * 900/300 = 12 check(decide(current, [cpu, queue], 1, 20), 12, "多指标取最大值") check(decide(current, [cpu, ("queue", None, 300)], 1, 20), 7, "一路缺失仍按可用指标扩容") check(decide(current, [("cpu", None, 50), ("queue", None, 300)], 1, 20), 4, "全部缺失时保持副本数") check(decide(current, [cpu, queue], 1, 10), 10, "最大副本数依然限幅") print("PASS: 4 scenarios; max-wins and hold-on-missing verified") print(sys.version.split()[0]) if __name__ == "__main__": main() 四个场景锁定关键行为:合并场景证明中央处理器要七个而队列要十二个时取十二个;单个缺失证明一路失败仍由中央处理器接管;全部缺失证明保持不动而非缩零;限幅场景证明算完后最大副本数依然生效。预期输出以通过信息结尾,说明四组场景成立,最大值优先与缺失保持均已验证,并打印解释器版本。 ...

十二月 22, 2027 · 1 分钟 · XB Field Notes
按 cron 时间表启动一个 Job 的 Kubernetes 对象。

CronJob 把时间表变成一次 Pod 运行

CronJob 就是一份 Job 模板加一条 cron 表达式。表达式每次匹配,控制器就新建一个 Job,由它把 Pod 跑到完成再停掉。两轮之间没有任何常驻进程,这正是它吸引人的地方:批处理任务直接继承集群的重试、资源与调度机制,不需要谁养一个守护进程来触发它。 代价是时间表只被“近似”兑现。Kubernetes 承诺每个执行时刻“大约”创建一个 Job,这句措辞把两个方向的偏差都包了进去——同一时刻冒出两个 Job,或者一个都没有。凡是被排进 CronJob 的活都必须能安全跑两遍,因为你真正拿到的保证是至少一次,而不是恰好一次。 一张时间表催生一个 Job schedule 用熟悉的五个字段——分、时、日、月、星期——支持 Vixie cron 的区间与步长写法,所以 */10 * * * * 每十分钟触发一次,*/15 9-17 * * mon-fri 只落在工作时段内。? 与 * 等价,@daily、@hourly 这类宏会展开成对应的表达式。没有设置 timeZone 的 CronJob 跟随控制器管理器所在时区的本地时钟,控制面一换地方,任务的执行时刻就跟着变。少写一个字段是校验错误,这也让它成为 CronJob 少数会大声失败的配置错误之一。 ...

十二月 17, 2027 · 1 分钟 · XB Field Notes
让调度优先选择或强制要求某些节点标签的规则。

节点亲和把工作负载放到合适的节点

亲和性是放置匹配:可以"偏好"某个标签也可以"强制"它,再用反亲和把副本散开,换取更强的韧性。硬件从来不会长期均匀——这边一池 GPU,那边高速本地 NVMe,还有合规数据的隔离区——而工作负载悄悄依赖落到对的地方。没有显式规则,调度器只按空闲容量铺,需要 GPU 的 Pod 落到没有 GPU 的节点,在运行时而不是调度时才失败。 亲和把这次失败前移到它该在的位置。 placements 写成节点上的数据(标签)配 Pod 上的规则,可 review 的配置代替了"哪些节点是特殊的"部落知识。读 manifest 的人一眼看出工作负载必须跑在哪、为什么。 按标签匹配节点 亲和性规则读取节点标签,让你说明某个 Pod 应该、甚至必须落在带某个标签的节点上。把稳定的标签一次性打在节点上,剩下的就让调度器去匹配。accelerator=nvidia-a100、storage=nvme、zone=regulated 这类标签描述机器的持久属性;因为它们住在 node 对象上,换机器也不影响引用它的 Pod 规则——新硬件打同样的标签,老规则继续生效。 标签名要像 API 字段一样认真起,因为改一个被广泛引用的标签名,就是跨所有匹配它的 manifest 的迁移。约定小而有文档的词表,胜过每队自创 key:workload-class、hardware、compliance 覆盖大多数家底,再配半页约定文档,免得 GPU 标签拼出五种写法。像校验镜像 tag 一样在 CI 里校验标签——要求一个没有任何节点携带的标签的 Pod,是该死在流水线里的拼写错误,而不是凌晨三点的发布。 偏好还是强制 偏好型规则尽力而行,条件允许就把负载摊开;强制型规则则硬性拒绝那些满足不了标签的 Pod。只有放置真地有讲究才用强制,否则偏好能留给调度器更多弹性。成本模型很直白:强制拿可用性换正确——没有匹配节点有位子,Pod 就 Pending,也不去错的地方;偏好拿正确换可用——落到次优但活着的地方。 ...

十二月 9, 2027 · 1 分钟 · XB Field Notes
分批替换实例、而不是一次性全换的发布策略。

Kubernetes 滚动更新逐步替换 Pod

maxSurge 与 maxUnavailable 两个窗口,给一次发布过程中最多能多出或缺少的 Pod 数定了上限,让升级始终在容量可控的前提下进行。 一次只换一小批 滚动更新不会把整份负载一次换掉,而是一批一批地先起新、再撤旧,并且整条链路都尊重 readiness。控制器会等新的 Pod 变就绪,才认为这次发布往前挪了一步,于是那些始终就绪不了的 Pod 会卡住这次发布,而不是偷偷用坏版本扛流量。这正是旧行为:任何时刻都不会从旧版一步跨到新版、中间一刻也没有空窗。因为切换被 readiness 拦住,你会在第一批新 Pod 就发现问题、中断发布,而不是等全量上线才知道砸了。 用 surge 与 drain 两扇窗 maxSurge 与 maxUnavailable 两扇窗,规定了整个发布的形状。maxSurge 允许多少个高于期望的额外 Pod,用于边排旧边补新;maxUnavailable 允许替换期间低于期望多少个。它们一组合,既定速度也定余量:surge 给得多就跑得快,代价是要有抵抗它的容量;而 maxUnavailable:1 + maxSurge:0 则偏向容量不动,却慢,并且发布窗口可以拉得很长。挑这两扇窗,看的应该是「如果这次发坏了我能容忍多坏」,而不是挑一组看起来左右均衡的数字,因为它们字面上就限定了这次发布能触碰多少 Pod。 回滚只是重新部署 因为一次发布本身只是另一张 spec,回滚不过是把镜像指回上一次跑的那份,然后同样受 readiness 约束的批次扰动,倒着再来一遍。把上一个版本保留着、随时能起,并优先用一个你读过、测过的旧镜像,而不是现场打的补丁——这样回滚路径本身就是一条被测试过的路径。审计一致、Pod 更替受同样的窗口约束,而团队也确切知道「回滚」是什么意思,它就是一次对最后已知好版本的重新部署,没有任何别的。 先选形状,再调节奏 strategy.type 只有两个值,而默认值把这个选择藏了起来。RollingUpdate 让新旧 Pod 在 surge 与 unavailable 两扇窗里重叠;Recreate 先把旧 Pod 删光,再建新 Pod,最小可用数按构造就是零,两扇窗直接被忽略。当两个版本绝不能并存,就选 Recreate:旧代码读不回去的单写迁移、排他文件锁或单例 worker、新旧互不兼容的有线协议。姊妹篇调的是波次节奏,这篇定的是发布形状。无状态层默认 RollingUpdate;一旦选了 Recreate,就要把停机排进计划,而不是等它找上门:测一次冷启动、预拉镜像、提前公告窗口。选之前先答三问:两个版本能同时 serving 吗,旧版本占着新版本要独占的资源吗,停机窗口能量化吗。第一问答否,直接 Recreate;第二问答是也倾向 Recreate,比如旧 Pod 占着的 hostPort 或 ReadWriteOnce 卷,新 Pod 根本起不来;第三问答否,说明还没测冷启动,先测再选。 ...

十一月 27, 2027 · 2 分钟 · XB Field Notes