Ubuntu20.04下dotnet core3.1/5.0不使用swap,占满内存抛OOM,调swappiness无效
问题根源拆解
- 首先纠正认知偏差:Linux的swap机制从来不是“物理内存100%占满后才启动换出”的兜底机制,它的核心作用是将长期不被访问的冷内存页迁移到磁盘,为高频访问的热内存腾出物理内存空间。swappiness参数仅控制系统有空闲内存时,主动换出冷页的倾向强度,和物理内存耗尽时的处理逻辑没有直接关联,这也是调整swappiness完全无效的核心原因。
- 测试程序的行为刚好完全触发了内核的“异常内存申请”判定逻辑:
- 测试代码在无间隔的紧循环中持续申请100MB的连续内存块,每一块刚申请的内存都被全局List的指针直接引用,内核页回收逻辑会将所有新映射的匿名内存页归类到“活跃内存链表”,根本不会将其判定为可换出的冷页。
- 紧循环的执行会占满核心CPU时间,内核后台负责扫描不活跃页、触发换出的页回收线程得不到调度运行机会,完全来不及将老的内存页标记为可换出状态。
- 当内核检测到进程的内存申请速度超过页回收、swap换出的处理速度时,会优先走OOM终止流程,直接杀掉这个持续申请内存的进程,根本不会等待swap换出完成——相比等待慢速的磁盘IO换出老内存,内核会优先选择低成本的进程终止操作。
- dotnet运行时的额外放大效应:
- dotnet Core 3.1之后默认对大对象堆(LOH)的内存分配启用透明大页(THP)映射,这类2MB大小的连续内存块如果被判定为活跃,内核会直接跳过换出流程,因为换出大页的IO成本远高于普通4KB页,不具备换出性价比。
- CLR的内存提交逻辑会在用户线程申请内存时直接同步向内核提交物理页,不会预留空闲的调度窗口给内核做后台页回收,进一步压缩了swap换出的触发机会。
对应解决方法
- 调整内存访问模式,匹配内核swap触发的前提条件:
不要在无间隔的紧循环中连续申请内存,每完成一轮内存分配后主动让出CPU时间,给内核页回收线程留出运行窗口,让老的内存页有机会被标记为不活跃状态。可以将测试代码修改为:static List<byte[]> l = new List<byte[]>(); static void Main(string[] args) { while (true) { var b = new byte[100 * 1000 * 1024]; l.Add(b); Thread.Sleep(100); // 主动让出CPU,让内核有机会触发页回收 } } - 关闭dotnet运行时的透明大页支持:
启动dotnet程序前设置对应环境变量,强制大内存分配使用普通4KB页映射,降低内核的换出判断门槛:export DOTNET_DisableTHP=1 dotnet YourTestApp.dll - 调整内核OOM触发逻辑,避免直接杀进程:
关闭“优先杀掉当前内存申请进程”的默认行为,让内核在内存申请路径上同步触发页回收、swap换出流程,而不是直接走OOM终止:sysctl -w vm.oom_kill_allocating_task=0 - 使用cgroup为dotnet进程设置合理的内存上限:
不要让进程一口气占满全部物理内存,预留10%-20%的物理内存给swap换出的IO缓存使用,避免内核因为可用内存不足直接触发OOM。
关键提醒:swap不是无限内存池,如果程序需要无限制申请内存且全程保持所有内存为活跃状态,无论怎么调整配置,内核都不会启用swap为其兜底,这是Linux内核的核心设计逻辑,和具体应用无关。
内容的提问来源于stack exchange,提问作者shortspider
相关产品推荐
相关产品推荐

