ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux kswapd内存回收机制与watermark水位原理详解

Linux kswapd内存回收机制与watermark水位原理详解 1. Kswapd不是“ swap 的守护神”而是内核内存压力的守门人很多人第一次在top或ps里看到kswapd0进程时下意识会把它和“swap 分区”划等号——以为它是专门负责把内存页写到 swap 文件/分区的“搬运工”。这种理解看似合理实则偏差巨大甚至会误导后续对内存行为的诊断。Kswapd 的本质不是 swap 操作的执行者而是内核内存水位监控与主动回收策略的触发器。它不直接调用swap_writepage()也不决定某一页是否该 swap它只做一件事当系统空闲内存持续低于某个阈值即 watermark就启动一套标准化的页面回收流程而 swap 只是这个流程中可选的最后手段之一。这背后的核心逻辑源于 Linux 内核对内存管理的分层设计。物理内存被划分为多个 zone如 DMA、DMA32、Normal每个 zone 都维护三组 watermarkmin、low、high。它们不是固定数值而是根据当前 zone 的总内存大小动态计算得出例如low min (high - min) * 0.25。min是底线一旦空闲内存跌破min内核会立即触发同步式直接回收direct reclaim此时所有分配内存的进程都会被阻塞直到回收出足够内存——这就是用户感知到的“卡顿”根源。而kswapd的使命就是在内存降到low之前就介入以异步、后台、低优先级的方式悄悄地把那些“不太重要”的页面比如文件缓存页、匿名页清理掉把空闲内存维持在high和low之间从而避免触碰min线。你可以把它想象成一个写字楼里的物业巡检员他不会等到消防警报响了min触发才行动而是在楼道里发现烟雾浓度开始上升free memory low时就提前打开排风扇kswapd 启动让空气流通起来防止火情爆发。这个认知差异直接决定了你排查内存问题的路径。如果你盯着kswapd0的 CPU 占用率猛涨第一反应不该是“是不是 swap 分区太小”而应立刻检查/proc/zoneinfo中各 zone 的pages_low和pages_free数值。如果pages_free长期徘徊在pages_low附近说明系统已进入“亚健康”状态kswapd 正在疲于奔命地维持平衡而一旦pages_free跌破pages_min你看到的就不再是 kswapd 的忙碌而是大量进程的D状态不可中断睡眠和alloc_pages_slowpath的内核栈——这才是真正的危机信号。我曾在一台 64G 内存的数据库服务器上见过这样的场景kswapd0CPU 占用稳定在 15%但pages_free始终比pages_low高出不到 100MB表面看一切正常。直到某次批量导入数据瞬间压垮了缓冲区pages_free直线下坠min被击穿所有 MySQL 连接全部 hang 死dmesg里全是page allocation failure。事后复盘问题根源不是 swap而是vm.vfs_cache_pressure设置过低默认 100导致 dentry/inode 缓存过度膨胀挤占了本该留给应用的内存空间。Kswapd 在这里只是那个忠实地汇报水位、却无力改变水位设定的哨兵。2. Kswapd 的启动与唤醒机制从“休眠”到“狂奔”的完整链路Kswapd 是一个标准的内核线程kernel thread其生命周期完全由内核调度器管理。它并非随系统启动就一直满负荷运行而是遵循严格的“按需唤醒”原则。理解它的唤醒逻辑是读懂kswapd0行为的关键。整个过程可以拆解为三个阶段初始化、休眠等待、条件唤醒。2.1 初始化每个 NUMA 节点一个专属 kswapd在内核启动早期mm/vmscan.c中的kswapd_init()函数会被调用。它会为系统中的每一个NUMA 节点Node创建一个独立的 kswapd 线程。在单节点系统上你只会看到kswapd0而在双路服务器上则会有kswapd0和kswapd1分别负责各自节点的内存 zone。这种设计确保了内存回收的局部性locality避免跨节点访问内存带来的性能损耗。每个 kswapd 线程在创建后会立即进入kswapd()主循环函数并调用prepare_to_wait_event()将自己挂入一个名为kswapd_wait的等待队列然后执行schedule_timeout()进入深度休眠TASK_INTERRUPTIBLE 状态。此时它几乎不消耗任何 CPU就像一个关机待命的机器人。2.2 休眠等待内核如何让它“睡得安稳”Kswapd 的休眠并非简单的sleep(1)而是依赖内核的wait_event_freezable()机制。它等待的事件是pgdat-kswapd_wait这个等待队列上的wake_up()调用。而这个wake_up()的源头正是内核内存分配器buddy allocator的“告警系统”。当内核尝试通过alloc_pages()分配内存时会首先检查目标 zone 的空闲页数。如果pages_free pages_high分配器不会立刻失败而是先调用wake_all_kswapds()—— 这个函数会遍历所有与该 zone 关联的 pg_data_t 结构体即 NUMA 节点描述符并对其kswapd_wait队列执行wake_up()。注意这里唤醒的是“所有 kswapd”而非特定某一个因为一个 zone 的压力可能影响整个节点的内存健康。2.3 条件唤醒一次唤醒多轮扫描Kswapd 被唤醒后并非一蹴而就地完成所有回收工作。它的主循环kswapd()包含一个核心逻辑balance_pgdat()。这个函数会接收一个pg_data_t *pgdat参数代表一个 NUMA 节点并传入一个order参数表示需要满足的连续页块大小如order0为单页order9为 512KB。balance_pgdat()的目标是将该节点下所有 zone 的空闲内存提升到至少pages_high水位。它会循环扫描每个 zone执行shrink_zone()回收操作。关键在于shrink_zone()并非无限制地回收而是受两个硬性约束sc.nr_to_reclaim计数器这是一个“任务量”指标。每次成功回收一页无论是 file cache 还是 anon page计数器就减 1。初始值由sc.gfp_mask和当前内存压力动态计算通常在几百到几千页之间。sc.priority优先级这是一个从 0 到 12 的整数代表本次扫描的激进程度。priority0时最激进会扫描所有 LRU 链表active/inactive file/anon甚至考虑 swappriority12时最保守只扫描 inactive file list 的头部。Kswapd 的priority会随着扫描次数增加而逐步降低即越来越激进直到pages_free pages_high或nr_to_reclaim 0才退出本轮循环。这意味着一次wake_up()可能触发 kswapd 进行多轮、不同激进程度的扫描。例如第一轮priority10只回收了少量 file cache第二轮priority8开始回收部分 anon page第三轮priority4终于触发 swap 操作。整个过程是渐进式的、可控的这也是 kswapd 能在后台平稳运行而不拖垮系统的原因。我曾用perf record -e sched:sched_wakeup -p $(pgrep kswapd)抓取过一次高负载下的唤醒事件发现kswapd0在 1 秒内被唤醒了 17 次但每次唤醒后的实际工作时间perf script显示的kswapd函数耗时加起来不足 200ms大部分时间它都在schedule_timeout()的休眠中——这印证了其“按需、轻量、异步”的设计哲学。3. Watermark 的动态计算与“Universal Watermark Disabler”的真相Watermark水位线是 kswapd 行为的指挥棒但它绝非一组写死在代码里的常量。Linux 内核采用了一套精巧的动态计算模型让min、low、high三线能随系统内存总量、当前负载、甚至硬件特性如是否启用透明大页 THP而自适应调整。理解这套计算逻辑是破解kswapd异常行为的第一把钥匙。3.1 标准 watermark 计算公式watermark 的核心计算发生在mm/page_alloc.c的calculate_totalreserve_pages()函数中。其基本思想是预留的“安全内存”总量应该与系统的总内存规模成正比但又不能线性增长否则小内存机器会预留过多大内存机器又预留不足。最终采用的公式是totalreserve_pages 16MB (totalram_pages * 0.001)其中totalram_pages是系统物理内存总页数MemTotal / PAGE_SIZE。这个结果再被平均分配到各个 zone 上作为该 zone 的min水位基准。随后low和high通过固定比例偏移得到pages_min totalreserve_pages / number_of_zonespages_low pages_min (pages_high - pages_min) * 0.25pages_high pages_min (pages_high - pages_min) * 0.5提示pages_high的初始值并非直接计算而是基于pages_min和一个预设的“buffer”通常是pages_min * 2来确定最终形成一个三角形关系high low min且high - low low - min。举个具体例子一台拥有 16GB 内存约 4,000,000 页的机器totalreserve_pages ≈ 16MB/4KB 4,000,000*0.001 4000 4000 8000页即约 32MB。如果它只有一个Normalzone那么pages_min ≈ 8000pages_low ≈ 12000pages_high ≈ 16000。这意味着只要free memory 16MBkswapd 就不会被唤醒一旦跌到12MB以下kswapd 就开始工作跌破8MB则触发直接回收。3.2 “Universal Watermark Disabler” 是什么它为何危险网络上流传的所谓“Universal Watermark Disabler”本质上是一条修改内核参数的命令echo 0 /proc/sys/vm/zone_reclaim_mode或更激进的echo 1 /proc/sys/vm/swappiness这些操作并不能真正“禁用” watermark因为 watermark 是内核内存分配器的底层基础设施硬编码在 buddy allocator 的路径中无法通过 sysctl 动态关闭。zone_reclaim_mode0只是禁用了“本地节点回收”策略强制跨节点分配这反而可能加剧内存碎片而swappiness1只是大幅降低了 swap 的倾向性让 kswapd 更倾向于丢弃 file cache 而非 swap anon page但这并未改变min/low/high的数值本身。真正能“影响” watermark 的只有两个途径修改vm.min_free_kbytes这是唯一一个能直接干预totalreserve_pages计算的 sysctl 参数。增大它会强制提高所有 zone 的min水位给系统留出更多“安全余量”但也意味着可用内存减少。我曾在一个内存密集型渲染集群中将min_free_kbytes从默认的 6553664MB提升到 524288512MB显著降低了kswapd0的唤醒频率但代价是每台机器损失了近 0.5GB 可用内存。使用 cgroup v2 的 memory.low在容器化环境中memory.low为某个 cgroup 设置了一个“软性下限”。当该 cgroup 的内存使用接近low时内核会优先从该 cgroup 的内存中回收这相当于为特定 workload 创建了一个局部的、更高优先级的 watermark。这比全局修改min_free_kbytes更精细、更安全。注意“Universal Watermark Disabler” 这个名称本身就是一个误导性的营销话术。它既不 universal无法跨所有内核版本生效也无法真正 disablewatermark 逻辑无法绕过。盲目应用这类“优化”往往会导致系统在突发负载下毫无缓冲直接触发 OOM Killer后果比 kswapd 频繁唤醒更严重。4. Kswapd 与 Swap 的关系何时 swap谁来 swap这是关于 kswapd 最普遍的误解点。必须明确kswapd 本身不执行 swap 操作它只决定“是否有必要考虑 swap”。真正的 swap 执行者是内核的try_to_unmap()和swap_writepage()函数它们在 kswapd 的shrink_inactive_list()流程中被间接调用。4.1 Swap 的决策树kswapd 的“选择权”当 kswapd 扫描到一个处于LRU_INACTIVE_ANON链表上的匿名页anon page时它会调用shrink_page_list()对该页进行评估。这个评估是一个多层决策过程Page Refcount 检查如果页的引用计数page_count()大于 1说明它被多个进程共享如 fork 后的 copy-on-write 页kswapd 会跳过它因为 swap 共享页成本极高。Page Dirty 检查如果页是脏的PageDirtykswapd 不会直接 swap而是先将其回写到 backing store对于 file page 是磁盘文件对于 anon page 是 swap 分区/文件。这一步由writepage()完成它会将脏页加入 writeback 队列由pdflush或writeback内核线程异步处理。Page Mapped 检查如果页被映射到某个进程的页表中page_mapped()返回 truekswapd 会尝试try_to_unmap()即解除该页与所有进程页表的映射关系。只有当try_to_unmap()成功返回SWAP_SUCCESS且该页不再被任何进程引用时kswapd 才会将其标记为PG_swapcache并调用add_to_swap()将其加入 swap cache最终由swap_writepage()写入 swap 设备。这个过程清晰地表明swap 是一个高成本、高风险的操作。kswapd 会尽一切努力避免它优先丢弃干净的 file cache 页shrink_active_file()、优先回写脏的 file cache 页、只有在swappiness值较高且inactive_anon链表足够长时才会对 anon page 发起try_to_unmap()。swappiness的默认值是 60意味着内核在回收时会以 60:40 的权重优先考虑 swap anon page而非丢弃 file cache。将其设为 0并不意味着“永不 swap”而是将权重变为 0:100即只丢弃 file cache绝不 swap anon page——但这在内存极度紧张时可能导致OOM Killer被迫杀死进程因为没有 swap 作为缓冲。4.2 “audio-gateway your kernel does not support swap limit capabilities” 错误解析这个错误信息常见于 Docker 或 Podman 容器启动时与 kswapd 无直接关联但它揭示了一个重要的内核配置依赖。swap limit capabilities指的是内核对 cgroup v1 的memory.memsw.limit_in_bytes参数的支持。该参数允许为容器设置“内存 swap”的总上限。要启用它内核编译时必须开启CONFIG_MEMCG_SWAP选项。如果内核未开启此选项很多发行版默认关闭当你在docker run中指定--memory-swap参数时就会收到上述错误。这与 kswapd 的关系在于cgroup 的 swap limit 限制是作用于 kswapd 的回收决策之上的。当一个 cgroup 的memsw.limit被设为 2G而其memory.limit为 1G 时kswapd 在为该 cgroup 回收内存时最多只能 swap 出 1G 的 anon page因为memsw memory swap。一旦 swap 达到上限kswapd 就只能通过丢弃 file cache 或触发 OOM 来应对。因此这个错误不是 kswapd 的 bug而是你的容器运行时试图使用一个内核不支持的高级功能。解决方案要么是重新编译内核并开启CONFIG_MEMCG_SWAP要么是放弃--memory-swap参数改用 cgroup v2 的memory.swap.max它不依赖CONFIG_MEMCG_SWAP而是基于CONFIG_MEMCG_SWAP_ENABLED。5. 实战诊断从dmesg、/proc/zoneinfo到perf的全链路分析当kswapd0的 CPU 占用率异常飙升或系统响应明显变慢时你需要一套标准化的诊断流程而不是盲目重启或调参。这套流程的核心是沿着内存压力的传导路径逐层向上追溯。5.1 第一层确认压力源——/proc/zoneinfo与vmstat首先获取最权威的内存水位快照# 查看每个 zone 的详细水位 cat /proc/zoneinfo | grep -E (node|pages_min|pages_low|pages_high|pages_free) # 查看实时内存分配统计 vmstat 1 5重点关注pages_free与pages_low的差值。如果pages_free长期 pages_low说明 kswapd 已进入“追赶模式”。vmstat中的siswap in和soswap out列能告诉你 swap 是否真的在发生。如果so持续为 0而kswapd0却很忙那问题一定出在 file cache 回收上而非 swap。5.2 第二层定位回收对象——slabtop与cat /proc/meminfokswapd 主要回收两类对象slab 分配器管理的内核对象如 dentry、inode、ext4_inode_cache以及 page cache。slabtop是查看前者最直观的工具slabtop -o排序后重点关注ACTIVE / OBJ比值高的 cache。如果dentry或inode_cache的ACTIVE占比超过 90%说明大量目录项和 inode 处于活跃状态无法被回收这会挤压 page cache 的空间。/proc/meminfo中的SReclaimable字段就是所有可回收 slab 的总和其值过大如 1GB往往是内存压力的前兆。5.3 第三层追踪 kswapd 行为——perf与trace-cmd要深入 kswapd 的内部逻辑perf是无可替代的利器# 记录 kswapd 的函数调用栈 perf record -e syscalls:sys_enter_* -p $(pgrep kswapd) -- sleep 10 perf script | head -50 # 或者追踪内存分配事件 perf record -e mm_vmscan_kswapd_sleep,mm_vmscan_kswapd_wake,mm_vmscan_direct_reclaim_begin -- sleep 10mm_vmscan_kswapd_wake事件的频次直接反映了内存压力的强度mm_vmscan_direct_reclaim_begin的出现则是min水位被击穿的铁证。我曾用trace-cmd抓取过一次kswapd的完整生命周期发现其 90% 的时间都花在shrink_slab()上而shrink_slab()中 70% 的时间又消耗在super_cache_count()函数里——这最终指向了一个挂载了海量小文件的 NFS 共享目录其dentry缓存失控。问题解决后kswapd0的唤醒频率从每秒 5 次降到了每分钟 1 次。5.4 经验总结三个最有效的“止血”操作基于多年实战我总结出三条立竿见影的应急措施它们不修改内核不重启服务却能快速缓解 kswapd 压力清空 page cacheecho 3 /proc/sys/vm/drop_caches。这会释放PageCache、dentries和inodes。注意这只是临时缓解drop_caches不会影响正在使用的内存所以是安全的。我习惯在批量数据导入前执行它为新数据腾出空间。降低 vfs cache pressureecho 200 /proc/sys/vm/vfs_cache_pressure。默认值 100 表示内核会以与 page cache 相同的力度回收 dentry/inode。将其提高到 200意味着内核会更积极地丢弃这些缓存从而为更重要的应用内存让路。这在 web 服务器上效果尤为显著。临时禁用 THP透明大页echo never /sys/kernel/mm/transparent_hugepage/enabled。THP 在内存紧张时会因难以找到连续的大块内存而失败导致大量khugepaged线程争抢内存与 kswapd 形成恶性循环。禁用它能立刻平息这场内核线程间的“内卷”。这三条命令是我处理内存告警时的“黄金三连击”。它们不是根治方案但能为你争取出足够的时间去分析日志、定位真正的内存泄漏源头。毕竟kswapd 从来都不是问题的制造者它只是那个在风暴来临前一遍遍检查门窗是否关好的、最尽职的守夜人。
返回列表