首页 / 资讯中心 / 文章详情

K8s 为什么要关闭 Swap?从资源模型到 OOM 行为深度解析

K8s 为什么要关闭 Swap?从资源模型到 OOM 行为深度解析 ★ FEATURED ARTICLE
我第一次真正理解“K8s 要关闭交换分区”这句话不是在安装文档里而是在一次线上事故中。当时某台节点上所有 Pod 突然变得异常缓慢CPU 并不高内存看起来也还有余量但接口耗时从几十毫秒涨到几十秒。排查到最后问题源头就是 swap 被某个新装的系统组件偷偷启用了。那一刻我才意识到K8s 里千叮咛万嘱咐的这条硬检查不是运维上的洁癖而是一套资源模型在底层机制上的必然要求。这篇学习笔记我会把这个“为什么”拆开讲清楚Kubelet 为什么默认拒绝带 swap 的节点swap 对 Pod 的内存 limit、QoS 和 OOM 行为到底做了什么以及实际运维中怎么把 swap 关干净。如果你正在学习 K8s、第一次搭集群或者被“明明关了 swap 还是报错”这类问题折磨过这篇文章应该能帮你省下不少时间。1. 这个禁令从哪来Kubelet 用一条硬检查拦住了 swap1.1 第一次装集群时你大概率见过这个报错如果你是用 kubeadm 这类工具初始化集群在安装前检查阶段就会看到类似这样的一句话[ERROR] Swap is enabled but kubelet is configured to fail if swap is enabled或者你在手动启动 kubelet 时日志里直接出现failed to run Kubelet: swap is enabled but kubelet configured to fail很多新手会下意识觉得“这是不是版本里的 bug我关掉这个检查不就行了”。实际上不是。这一条是 kubelet 从设计层面主动做出的拒绝行为。Kubelet 有一个开关叫failSwapOn默认值是true意思是只要节点上存在 swap 分区或 swapfilekubelet 就拒绝启动。你可以在 kubelet 启动参数里看到它也可以把它显式写成failSwapOn: false去绕过但绕过之前你必须先想清楚后面这几个原理问题。1.2 硬检查背后的第一个理由调度假设不能靠 swap 圆场K8s 的调度器在决定一个 Pod 放到哪个节点时看的是你为每个容器声明的requests。它会拿这些请求量去跟节点的allocatable比较判断节点还能不能塞下新 Pod。这里有个关键点Kubelet 在报告节点内存容量时不会把 swap 算进去它上报的就是物理内存和系统预留之后剩余的部分。如果节点上实际开着 swap运行时的内存行为就跟调度器假设的不一致了。一个容器的内存请求是 1Gi调度器以为它最多占节点物理内存 1Gi 的量但进程一旦把匿名页面换到磁盘上它占用的物理内存可能更小但并不意味着它不消耗资源。等到这些页面被重新访问时又会产生换入换出的颠簸。结果就是节点的“真正可用性”变得极难判断调度器在做容量规划时手里拿的是一套失效的账本。所以 Kubelet 选择最保守的策略不让 swap 参与游戏。这是第一个层面的“为什么”。2. 从内存管理看默认关 swap 的四个层次2.1 Linux 的虚拟内存和 swap 到底在干什么要理解这个问题先得简单梳理 Linux 内存机制。进程看到的是一段虚拟地址空间这些地址最终映射到物理页框。内存页大致分两类一类是文件页比如代码段、文件映射缓存另一类是匿名页也就是进程运行时动态分配的堆和栈没有磁盘文件做支撑。当物理内存吃紧时内核需要腾出空间。文件页可以直接丢弃下次用的时候再从文件读回来而匿名页如果想腾出来就必须找地方放swap 就是给匿名页准备的“备胎仓库”。如果 swap 开着内核可以把一部分匿名页写到 swap 分区或 swapfile 里这样进程依然活在虚拟内存里但实际上有一部分数据已经落盘了。可问题在于当进程马上要访问那些被换出的页面时内核必须把页面从磁盘换回来。如果整个节点的内存压力一直很高就会形成一种循环不断换出、换入、再换出、再换入。这就是 Linux 上最经典的 thrashing。你可以把它想象成一个小房间同时有十个人要用办公桌系统不停地把文件从桌上搬到仓库下一秒又有人要用文件再从仓库搬回结果所有人都耗在走路和等电梯上真正干活的时间几乎没有。K8s 要的是可预测的调度和可控制的延迟它最不希望看到的就是这种全员陪跑的颠簸状态。2.2 有了 swapPod 的 memory limit 会失去“硬边界”很多人的第一个疑问是K8s 不是有resources.limits.memory吗容器超过内存 limit 不是会被杀掉吗正常情况下这个逻辑成立。容器运行时会把 Pod/容器的内存 limit 落到 cgroup 的硬限制上超过限制就会触发回收甚至 OOM Kill。但是当节点全局启用 swap 后这个边界就变得不干净了。内核面对内存压力时除了回收文件页还可以把匿名页换到磁盘里去。对 cgroup 来说等于多了一张“可透支的信用卡”。你可以试想一个容器设置了 256Mi 内存 limit实际运行中它不断触顶。没有 swap 的情况下内核很快会回收它内部能回收的页然后走 OOM 路径把超限的容器杀掉。这是 K8s 和用户都能预期到的结果。但有了 swap进程可以继续分配更多虚拟内存内核把旧页面挤出到磁盘进程还在跑只是跑得很慢。从 cgroup 的视角看它可能还没突破硬限制因为一部分内存已经被“藏”到磁盘上了。也就是说内存超限没有转换成一次干净利落的杀进程动作而是转换成一次不可预测的性能雪崩。这恰恰违背了 K8s 要用 limit 给业务画边界、用 QoS 保障优先级的初衷。2.3 关掉 swap 后内核会走一条更可预测的路径把 swap 关掉之后物理内存不足时内核能做的事情就少了先尝试回收文件页如果还不够就直接触发 OOM Killer。对 K8s 来说一次容器重启是可接受的故障恢复手段甚至可以说是设计的一部分。Pod 被 OOM Kill 之后Kubelet 可以把它重新拉起容器变成 CrashLoopBackOff 也只是业务层的问题。更重要的是K8s 会在 OOM 打分上做手脚比如给 Guaranteed QoS 的容器更低的 OOM 优先级给 BestEffort 容器更高的被打杀概率。这套机制让“杀谁”变得可控从而把爆炸半径控制在最小的容器级别。而 swap 会干扰这套机制当数据都堆在磁盘上时OOM Killer 可能很长时间都不会被触发直到整个节点被颠簸拖垮。一个节点级的故障比单个容器重启严重得多。这就是 K8s 选择“宁杀勿拖”的根本原因可预测的失败远好于不可预测的慢性死亡。2.4 swap 还会让故障变得非常难排查除了稳定性可观测性也是一个大问题。线上排查内存问题时大家都会习惯看一眼free -h、top、kubectl top node。如果 swap 开着你会经常看到一种很奇怪的画面节点物理内存占用并不高swap 的 used 却一直在涨进程的 RSS 也没多大但业务就是慢得像在爬。这是因为一部分本该在物理内存里的数据被换到了磁盘上。你看着 RSS 觉得进程不占内存实际上它正经受频繁缺页中断每次都去读磁盘。监控层面的 CPU、内存、磁盘 I/O 分开看都“不高”合在一起却让人一头雾水。相比之下swap 关掉之后内存泄漏会直接表现为容器内存持续增长直到被 OOM Kill问题发生得非常直接定位起来也容易得多。3. 实操把节点上的 swap 彻底关掉并验证3.1 先确认节点当前的 swap 状态不管你是新装节点还是排查已有节点第一步永远是确认现状。这几个命令可以交替使用free -h swapon --show cat /proc/swapsfree -h里如果 Swap 一行的 used 和 total 都是 0说明当前没在用。swapon --show最直观如果输出里除了表头没有第二行说明也没有可用的 swap 设备。/proc/swaps的格式和swapon --show类似适合写脚本时读取。注意一点有些云镜像或衍生发行版虽然默认没在/etc/fstab里写 swap却会通过 systemd 单元或者其他初始化脚本创建 swapfile。只看一个文件可能会漏所以建议三个命令都扫一遍。3.2 临时关闭与永久关闭两条命令是配套的先临时关闭所有 swap 设备sudo swapoff -a这条命令会让内核把已经换出的数据尽量搬回物理内存。如果你的节点内存压力很大swapoff 可能需要一点时间甚至会感觉卡顿。这是正常现象别中途强行 kill 进程。然后处理持久化配置避免重启之后 swap 又自动起来。最常见的入口是/etc/fstab。找到里面带 swap 的行例如/dev/mapper/swap none swap sw 0 0把它注释掉或者直接用 sed 批量处理sudo cp /etc/fstab /etc/fstab.bak sudo sed -i / swap / s/^/#/ /etc/fstab用注释而不是直接删行是为了方便以后回滚。如果发行版用的是 systemd swap 单元还要单独检查systemctl list-units --typeswap --all sudo systemctl disable --now 对应的swap单元有些环境里还会遇到 zram它本质上也是 swap 设备只是把数据压缩后放在内存里而不是写到磁盘。Kubelet 认不认只要swapon --show里有名字就一样被认定为 swap 已启用。处理方式和普通 swap 单元类似找到对应 service 或内核模块配置确认永久停用。3.3 验证 Kubelet 能正常启动处理完 swap 后重启 kubelet 并观察状态sudo systemctl restart kubelet journalctl -u kubelet -f看到不再出现 swap 相关报错之后再在控制平面上确认节点状态kubectl get nodes如果节点变成 Ready说明 kubelet 已经接受了当前环境。最后别忘了重启一次机器再跑一遍swapon --show确认重启后 swap 没有卷土重来。这一步非常关键因为很多人只在当前会话里执行了swapoff -a没有清理/etc/fstab结果机器一重启kubelet 又开始罢工。3.4 别把 vm.swappiness0 当成免死金牌网上经常有人问我不关 swap把vm.swappiness调成 0 不就行了实验上可以但语义上不一样。vm.swappiness0只是让内核“尽量”不使用 swap并不是完全禁用。在极端内存压力下内核仍然可能换出匿名页。更致命的是Kubelet 检查的是当前系统里有没有启用的 swap 设备只要swapon --show里还有条目它就可能直接拒绝启动。所以别把 swappiness 当作绕过方案要么关得干净要么你就必须走后面第 4 节说的受控开启路径。4. 新版 K8s 不是完全禁止 swap但门槛非常高4.1 NodeSwap 特性到底在给什么场景开门看到这里有人可能会问既然 swap 这么可怕为什么新版本 K8s 又提出 NodeSwap开始实验性支持了呢社区不是突然改主意而是看到了另一类工作负载的需求批处理任务、离线分析、低优先级 job。这类负载对延迟不敏感本身又可以随时被杀掉重跑。让它们利用节点上闲置的内存甚至 swap可以显著提高资源利用率。换句话说K8s 想区分“需要严格 QoS 的在线业务”和“可以承受驱逐的批量任务”而不是一刀切地让所有人共享同一套内存策略。但请注意这个能力是带条件的而且默认行为仍然是关闭。普通生产集群如果没有明确的工作负载场景不要为了“功能尝鲜”去随便开启。4.2 cgroup v2 是硬前提swap 要变成可度量的资源边界在我看到的社区方案里受控使用 swap 的关键在于让它成为“可度量、可隔离”的资源边界而不是一颗不受控的炸弹。这里的核心基础设施就是 cgroup v2。和旧版 cgroup v1 相比cgroup v2 提供了更明确的memory.max、memory.swap.max之类的控制项内核可以对每个 cgroup 的 swap 使用量单独设限。这意味着即便你开了 swap也可以只允许某个低优先级 cgroup 使用少量 swap而对高优先级 QoS 的容器继续实施硬隔离。要想做到这一点你的容器运行时、kubelet 以及整个系统都需要在 cgroup v2 模式下运行。如果你的节点仍然挂在 cgroup v1 那一套上还是老老实实保持 swap 关闭强行开启只会得到一个语义混乱的混合体。4.3 如果非要开至少要把这些配置显式地配出来不同 K8s 版本对 NodeSwap 的支持程度不同。据我了解这个特性在 v1.22 附近进入 alpha后续版本再逐步迭代。所以在动手之前第一件事是查你当前 kubelet 版本里有没有 NodeSwap 这个 feature gate以及failSwapOn字段是否还叫这个名字。如果版本支持通常会涉及类似这样的配置# kubelet 配置示例不要照抄以你当前版本的 API 文档为准 failSwapOn: false featureGates: NodeSwap: true光把这几个参数写对还远远不够。你还需要确认容器运行时的 cgroup driver 是 systemd内核支持 cgroup v2并且对哪些 QoS 可以使用 swap 做明确规划。我的建议是在专门压测过的测试集群里先跑一轮批处理任务观察内存、磁盘 I/O、延迟和 OOM 行为确认可控之后再谈上线。没有充分验证之前生产环境请维持swapoff -a。5. 常见问题、坑位与排查手册5.1 kubelet 一直说 swap enabled但我明明执行过 swapoff -a这是最高频的困惑。常见原因有三个第一你只临时关了 swap但/etc/fstab里还有持久化配置重启后又回来了第二系统里存在独立的 systemd swap 单元或初始化脚本比如某些云镜像会在启动时自动创建 swapfile第三节点用的是 zram 或类似设备你用swapoff -a关掉的只是普通 swap但 zram 又被某个服务重新拉了回来。处理方式就是把这三处都查一遍。命令可以参考swapon --show grep swap /etc/fstab systemctl list-units --typeswap --all哪个条目还在就把哪个禁用掉。特别是云环境建议检查一下 cloud-init 相关的脚本看是不是每次开机都会重新生成 swap。5.2 关掉 swap 后 Pod 被 OOMKilled这是不是不正常不一定不正常。关闭 swap 后内存超限的 Pod 会更快地走向 OOM Kill这是一种有预期的失败。你要做的是看事件是不是集中在某个容器上kubectl describe pod pod-name journalctl -k | grep -i Out of memory如果某个容器频繁 OOMKilled大概率是它的memory.limit设置得低于实际需求或者容器存在内存泄漏。先排查应用本身再考虑调大 limit、增加副本、配置 HPA。不要把“开 swap”当成治内存不足的药方那只是把急性病转成慢性病。5.3 节点内存明明很多为什么 kubelet 还是报 swap 错误因为 Kubelet 检查的不是“内存够不够”而是“swap 是否启用”。哪怕你的物理内存 512Giswap 只有 64Mi只要系统里有 swap 设备在运行它就认为条件不符合设计预期。这是很典型的“非黑即白”检查没必要跟它讨价还价直接把 swap 关掉即可。5.4 常见问题速查表表现出来的现象可能的原因建议处理动作kubelet 启动失败日志提示 swap enabled/etc/fstab或 systemd swap 单元仍然启用注释 fstab 相关行禁用相关 systemd 单元重启机器后 swap 又出现持久化配置没清理干净检查 fstab、systemd、cloud-init、zram 配置Pod 频繁 OOMKilled容器内存 limit 偏低或应用内存泄漏优化应用调整 limit评估副本数节点 CPU 不高但业务延迟高可能 swap 颠簸导致频繁换入换出立即 swapoff 并确认是否出现过内存压力vm.swappiness0后 kubelet 仍拒绝Kubelet 检查的是 swap 设备是否存在执行swapoff -a并清理持久化配置6. 最后关于这条硬检查我的一点实际操作体会关 swap 这个动作看起来简单真正难的是理解它背后的资源哲学。K8s 花了很大力气去建立 requests、limits、QoS、驱逐、OOM 这一整套可预测的资源秩序而 swap 天然会打乱这套秩序。它让内存限制失去刚性让故障定位变得模糊让节点级颠簸吞掉所有容器的性能。对在线业务来说一次干净的容器重启远好过一场没有期限的慢性卡顿。我现在的习惯是新节点交付时第一件事就是检查swapon --show和/etc/fstab在监控大盘上也会专门加一个 swap 使用量面板防止有人或某个初始化工具偷偷把 swap 打开。如果你正在搭建自己的集群也建议把“无 swap”写进节点初始化 checklist 里。这条硬检查不是故意给运维添麻烦它是在替整个集群守住一条资源底线。
阅读完成 · 觉得有帮助?
咨询建站