Linux内核页面回收与OOM Killer深度实战:从LRU链表熔断到cgroup内存爆炸
引言
在Linux内核的庞大体系中,内存管理子系统是最复杂、最核心的子系统之一。如果说SLUB分配器解决了"如何高效分配小对象"的问题,那么页面回收(Page Reclaim)机制则回答了一个同样关键的命题:当物理内存不足时,内核如何将哪驱逐出境?
页面回收是Linux内存管理的"刹车系统"。当一个进程请求分配内存(无论是用户态的malloc还是内核态的kmalloc),而系统当前没有足够的空闲页面时,页面回收机制就会被触发,通过将不活跃的页面写入交换分区(swap)或回写脏页到磁盘,来腾出物理内存空间。这个过程如果处理不当,会导致系统卡顿、性能急剧下降,甚至触发OOM(Out Of Memory) Killer随机杀死进程。
本文将从LRU链表的底层数据结构入手,逐步深入页面回收的完整路径——从kswapd后台守护进程的异步回收,到进程因内存不足陷入的直接回收(direct reclaim),再到最后的终极大招OOM Killer。同时会覆盖cgroup v1/v2的内存限制机制、swapiness调优、以及实际生产环境中遇到的OOM实战案例分析。
一、LRU链表:页面回收的数据基础
1.1 为什么需要LRU?
操作系统面临的核心矛盾是:物理内存远小于进程的虚拟地址空间需求。为了让更多进程"感觉"自己拥有充足内存,内核必须将暂时不用的页面换出到磁盘。但哪些页面是"暂时不用"的呢?这就需要一种机制来追踪页面的访问频率——LRU(Least Recently Used,最近最少使用)算法应运而生。
理想LRU需要为每个页面维护一个精确的时间戳并按时间排序,但这在工程上不现实(需要在每次内存访问时更新数据结构)。Linux采用了一种近似LRU方案——双链表(Active/Inactive)算法,通过两个链表和一个访问位(Referenced/Referenced bit)来实现近似最近最少使用的判断。
1.2 五种LRU链表类型
从内核2.6.28开始,Linux维护五种独立的LRU链表:
| LRU链表 | 类型 | 说明 | 可回收性 |
|---|---|---|---|
| LRU_INACTIVE_ANON | 匿名页 | 非活跃匿名页(进程堆/栈) | 需写入swap |
| LRU_ACTIVE_ANON | 活跃匿名页 | 活跃匿名页 | 需先demote |
| LRU_INACTIVE_FILE | 文件缓存 | 非活跃文件页(page cache) | 直接丢弃 |
| LRU_ACTIVE_FILE | 文件缓存 | 活跃文件页 | 需先demote |
| LRU_UNEVICTABLE | 不可驱逐 | mlocked或Bolted页面 | 永远不回收 |
匿名页(Anonymous Page):没有后备存储的页面,如进程的堆内存、栈空间。这类页面一旦被释放就无法恢复(除非重新计算),所以若要回收必须写入swap分区。
文件页(File-backed Page):有后备文件映射的页面(如通过mmap映射的文件、page cache)。干净的页面可以直接丢弃(下次从文件读回即可),脏页需要先回写到磁盘。因此文件页的回收代价通常远低于匿名页。
SHMEM(共享内存/tmpfs)是一个特殊情况——它看起来像文件页,但被放在LRU_ANON链表中,因为它也没有真正的磁盘后备存储(除非tmpfs挂载在swap上)。
1.3 zone_reclaim_mode与NUMA感知
在NUMA系统中,还需要考虑页面所在的NUMA节点。每个NUMA节点维护独立的LRU链表(通过pglist_data→lruvec)。zone_reclaim_mode(/proc/sys/vm/zone_reclaim_mode)控制是否在回收时优先回收本节点页面:
- 0:不启用zone reclaim,从所有节点回收
- 1:优先从本节点回收(适合内存绑定型应用如数据库)
- 2:只在本地节点耗尽时才fallback
- 4:结合swap的zone reclaim
对于MySQL、PostgreSQL等NUMA敏感型数据库,通常建议设置zone_reclaim_mode=1,避免跨NUMA节点访问内存带来的延迟惩罚。
1.4 页面如何在Active/Inactive之间移动
参考位(PG_referenced flag)是页面在双链表间移动的判断依据:
晋升(Promotion):Inactive页被访问时,referenced bit被置位。在第二轮扫描时如果referenced仍为1,该页被提升到Active链表。这意味着一个页面需要被连续访问两次才能进入Active链表。
降级(Demotion):Active页如果经过一轮扫描后referenced bit为0(即没有被访问),则被降级到Inactive链表。Inactive链表中的页面是回收的首要目标。
这两个操作构成了LRU的"时钟算法"——时钟指针持续扫描页面,根据访问情况决定页面的去留。在 Linux 5.15+ 中,多 Gen LRU(mgLRU)进一步优化了这个机制,引入了代龄(generation)概念来更精准地区分热点页面。
二、页面回收机制:kswapd与Direct Reclaim
2.1 水位线(Watermark)体系
页面回收的触发由三条水位线控制,它们定义在每个NUMA节点的pglist_data中:
enum zone_watermarks {
WMARK_MIN, // 最低水位,直接回收的触发点
WMARK_LOW, // 低水位,kswapd唤醒阈值
WMARK_HIGH // 高水位,kswapd睡眠阈值
};
WMARK_MIN:危险线。当空闲页面低于此值时,分配者必须自己执行直接回收(direct reclaim),即分配操作将阻塞直到回收足够页面。每次kswapd唤醒时会留有少量"min"级空闲页,但它不是给普通分配的。
WMARK_LOW:警戒线。kswapd守护进程被唤醒的阈值。kswapd会持续回收页面直到空闲页面达到WMARK_HIGH。从WMARK_LOW到WMARK_HIGH的差值即为kswapd一次回收的目标量。
WMARK_HIGH:安全线。kswapd停止回收并进入睡眠的阈值。kswapd的目标是将空闲页面维持在WMARK_HIGH以上。
水位线的计算公式(简化版):
WMARK_MIN = managed_pages * min_percent / 100
WMARK_LOW = WMARK_MIN + (WMARK_MIN / 4) // 约高25%
WMARK_HIGH = WMARK_LOW + (WMARK_MIN / 4) // 约高50%
其中min_percent可通过/proc/sys/vm/min_free_kbytes直接指定WMARK_MIN(单位为KB)。生产服务器通常建议min_free_kbytes=65536~262144(64MB~256MB),用于应对突发内存分配需求。
2.2 kswapd:后台异步守护进程
kswapd是每个NUMA节点上的后台守护进程(kswapd0、kswapd1...),负责在内存水位低于WMARK_LOW时异步回收页面。它的核心循环可以简化为:
// mm/vmscan.c: kswapd()
for ( ; ; ) {
// 1. 检查是否需要回收(某个zone的free < LOW watermark)
if (all_zones_ok())
kswapd_try_to_sleep(); // 睡眠等待唤醒
// 2. 平衡各zone的压力
balance_pgdat(pgdat, order, classzone_idx);
// 3. 回收直到满足HIGH水位
}
kswapd的回收流程:
- 对每个zone,计算回收目标(基于当前水位和请求order)
- 按比例在各LRU链表间分配回收配额(匿名页 vs 文件页比例由swappiness决定)
- 调用shrink_lruvec()对每个LRU链表执行扫描和回收
- 重复直到达标或没有任何可回收页面
kswapd的回收是异步且不阻塞调用者的,但由于需要在持有zone锁的情况下扫描LRU链表,在高并发场景下会带来一定开销。Linux 6.x引入了per-cgroup LRU维护,进一步降低了锁竞争。
2.3 Direct Reclaim:同步回收的痛苦
当进程的内存分配请求无法满足(空闲页低于WMARK_MIN),且kswapd尚未回收足够页面时,分配者进程必须自己执行页面回收,这就是直接回收(direct reclaim)。
直接回收发生在分配者的上下文中,意味着:
- 分配者被阻塞:进程在allocate→reclaim之间无法继续执行,增加延迟
- 可能递归回收:reclaim过程中可能触发新的内存分配(如创建shrinker控制结构),形成递归
- THP分配更容易触发:分配2MB的大页需要的连续页面多,失败概率更高,更容易触发direct reclaim
direct reclaim的入口函数:
alloc_pages()
└── __alloc_pages_nodemask()
└── __alloc_pages_slowpath() // 慢路径
└── __alloc_pages_direct_reclaim() // direct reclaim入口
└── try_to_free_pages() // 尝试释放pages
└── do_try_to_free_pages()
└── shrink_zones()
└── shrink_lruvec()
诊断direct reclaim:观察/proc/vmstat中的pgsteal_direct字段增长、vmstat中的si/so字段(swap频率)、或使用blktrace追踪swap I/O。perf sched latency也能看到进程因reclaim导致的调度延迟。
2.4 页面回收的扫描与驱逐决策
shrink_lruvec()是核心回收函数,它决定哪些页面被扫描、哪些被驱逐:
shrink_lruvec()
├── shrink_list(LRU_INACTIVE_ANON) // 首先扫描非活跃匿名页
├── shrink_list(LRU_ACTIVE_ANON) // 非活跃不够时扫描活跃匿名页
├── shrink_list(LRU_INACTIVE_FILE) // 接着扫描非活跃文件页
├── shrink_list(LRU_ACTIVE_FILE) // 最后扫描活跃文件页
└── shrink_list() 内部:
├── isolate_lru_pages() // 从LRU链表中隔离一批页面
├── shrink_page_list() // 对隔离的页面做驱逐决策
│ ├── 页面正在被其他路径使用 → 放回LRU(unmap失败)
│ ├── 脏文件页 → 启动回写(writeback)
│ ├── 匿名页 → 写入swap
│ └── 干净未映射页 → 直接释放
└── putback_lru_page() // 无法回收的页面放回LRU
页面扫描优先级由swappiness和scanning比例控制:每个LRU链表有一个独立的扫描比例系数。shrink_lruvec()根据目标页数和各链表的优先级,按比例分配扫描量。
三、Swap机制与Swappiness调优
3.1 Swap的本质
Swap(交换空间)是磁盘上的一块区域(分区或文件),用于存放被驱逐的匿名页内容。当进程再次访问被swap-out的页面时,触发page-fault,内核将数据从swap区域读回内存(swap-in)。
Swap的代价极高——磁盘访问延迟大约是内存的10万~100万倍:
| 存储类型 | 读取延迟 | 吞吐量 |
|---|---|---|
| DDR4 内存 | ~100ns | ~50 GB/s |
| NVMe SSD | ~10μs | ~3 GB/s |
| SAS HDD | ~5ms | ~200 MB/s |
因此,频繁swap(称为swap thrashing)会直接导致系统瘫痪。Linux的内存管理策略是:尽可能避免swap,只在真正缺页时才会swap-out。但在内存紧张时,将不活跃的匿名页swap-out可以腾出物理页给活跃的文件缓存,反而提升整体性能。
3.2 swappiness参数详解
swappiness(/proc/sys/vm/swappiness, 默认值60)控制swap回收与文件页回收之间的权重比例:
核心逻辑:swappiness不是简单的概率值,而是两个子系统(匿名页回收 vs 文件页回收)扫描量之间的比例系数。它被转换为两个数值:文件页基准值200,匿名页基准值 = 200 + swappiness。扫描量之比为:anon_scan : file_scan = (200 + swappiness) : 200 ≈ (200+swappiness)/200。
换算后的效果:
- swappiness=0:几乎不swap,优先回收文件页。但注意:如果文件页都已回收完且仍缺水,仍会swap。使用special file-backed内存(如MAP_SHARED)的系统不应设为0。
- swappiness=1:极其保守的swap策略,仅在使用explicit madvise(MADV_PAGEOUT)时swap
- swappiness=60(默认):anon_scan : file_scan ≈ 260:200,轻微偏向回收文件页
- swappiness=100:匿名页和文件页被同等对待
- swappiness=200(最大值):双倍于文件页扫描匿名页,积极swap
经典调优建议:
- 数据库服务器(MySQL、Redis):swappiness=1~10,减少swap对延迟的影响
- 桌面系统:swappiness=60(默认)
- 批处理/大数据(Hadoop、Spark):swappiness=10~30
- 使用zram的容器环境:swappiness=150~200
3.3 zram:压缩内存交换
zram(压缩内存块设备)提供了一种替代传统磁盘swap的方案——使用一部分RAM作为交换空间,但其中的数据被实时压缩。优点如下:
- 避免磁盘I/O延迟(CPU换IO)
- 压缩比通常为2:1到3:1
- 适合内存小但CPU强的场景(如嵌入式、树莓派、低配云服务器)
zram配置示例:
# 初始化一个2GB zram设备(压缩后约600MB物理占用)
modprobe zram
echo lz4 > /sys/block/zram0/comp_algorithm # 选择lz4压缩(速度快)
echo 2G > /sys/block/zram0/disksize # 设置2GB压缩交换空间
mkswap /dev/zram0
swapon /dev/zram0 -p 10 # 设置优先级高于磁盘swap
注意zram的优先级(priority参数),更高的优先级意味着内核优先使用zram而非磁盘swap。在容器和云计算环境中,zram已成为标配——Kubernetes默认推荐使用zram替代磁盘swap。
四、OOM Killer:最后的终极大招
4.1 什么情况下触发OOM?
当系统内存极度紧张、direct reclaim无法回收足够页面、kswapd也无法满足需求、且所有页面都是不可回收的(例如全被mlock)时,内核会触发OOM Killer。简而言之:所有页面分配路径都已失败,内核只能杀死进程来释放内存。
OOM Killer的调用路径:
alloc_pages()
└── out_of_memory()
└── select_bad_process() // 选择"最坏"的进程
└── oom_kill_process() // 杀死选中的进程
└── force_sig(SIGKILL)
└── memory.oom_group // 同时杀死cgroup中所有进程
4.2 OOM Score:谁是"最坏"的进程?
OOM Killer通过一个启发式评分系统决定杀死谁。评分存储在/proc/[pid]/oom_score(0~1000),值越高越容易被杀。评分基于以下因素:
基础分(oom_score)= 内存占用百分比 × 1000:占用内存越多,分数越高。具体计算:
// mm/oom_kill.c: oom_badness()
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) + mm_pages;
// 上述值占总内存的比例乘以1000,得出基础分
调整因子:
- oom_score_adj(/proc/[pid]/oom_score_adj):管理员可手动调整,范围为-1000到+1000。设为-1000表示禁止OOM Killer杀死该进程(oom_score=0)。设为+1000表示优先杀死。
- oom_adj(旧版,已废弃):范围-17到+15
- root进程默认-30分:防止杀死关键系统进程
- 运行时间短的进程更容易被杀:新进程的oom_score被平方后乘以一个与运行时间相关的因子
- 交互式/低优先级进程更易被杀:nice值每+1约减少1个oom_score点
实际案例:手动保护关键服务:
# 保护mysqld,将其oom_score_adj设为-1000
echo -1000 > /proc/$(pidof mysqld)/oom_score_adj
# 通过systemd服务配置(推荐)
[Service]
OOMScoreAdjust=-1000
# 让Java进程更容易被杀(常见做法)
echo 700 > /proc/$(pidof java)/oom_score_adj
4.3 OOM Killer日志解读
当OOM Killer触发时,内核会打印详细信息到dmesg(/var/log/syslog),以下是一个典型日志:
Out of memory: Killed process 12345 (java) total-vm:8GB, anon-rss:6GB, file-rss:512MB, shmem-rss:0B, oom_score_adj:0
├── 进程名: java
├── PID: 12345
├── 总虚拟内存: 8GB
├── 实际RSS: 6.5GB
├── OOM Score: 高 (由内核计算)
└── 内核会选择score最高的进程杀死
查看历史上的OOM事件:
dmesg | grep -i "out of memory"
journalctl -k | grep -i "oom"
注意:OOM Killer使用SIGKILL(信号9),进程无法捕获或忽略它。进程在被杀前没有任何"优雅关闭"的机会,因此数据库等关键服务必须设置合理的oom_score_adj。
4.4 oom.cgroup:cgroup内存限制触发的独立OOM
在container(容器)环境中,即使系统总体有足够内存,但如果某个cgroup的memory.limit_in_bytes被耗尽,也会触发cgroup级别的OOM。这种OOM仅发生在该cgroup内部,不会杀死其他cgroup的进程。
cgroup OOM event通过memory.oom_control或cgroup.event_control监听:
# 监听cgroup OOM事件
echo 1 > /sys/fs/cgroup/memory/mygroup/memory.oom_control # 禁止自动杀进程
# 然后通过eventfd监听,当OOM发生时触发通知
生产建议:
- 为每个容器的memory.limit留出10%~20%的buffer,避免频繁OOM
- kubernetes的resources.requests.memory应小于resources.limits.memory
- OOM优先级:cgroup OOM → 全系统OOM(按严重程度递进)
五、cgroup内存限制与回收机制
5.1 cgroup v1 内存子系统
cgroup v1为每个cgroup提供独立的内存使用统计和限制:
| 文件 | 说明 |
|---|---|
| memory.limit_in_bytes | 内存硬限制,超出会触发OOM |
| memory.soft_limit_in_bytes | 软限制,仅在系统内存紧张时生效 |
| memory.memsw.limit_in_bytes | memory+swap的总限制 |
| memory.kmem.limit_in_bytes | 内核内存限制(含slab, page tables) |
| memory.usage_in_bytes | 当前使用量 |
| memory.oom_control | 是否禁止OOM Killer(1=禁止) |
| memory.swappiness | cgroup v1.5+独立swappiness |
注意事项:cgroup v1的memory controller存在"内核内存不可限制"的问题——kmem accounting默认不开启,开启后又有性能开销。这是v2解决的重要问题之一。
5.2 cgroup v2 内存控制器
cgroup v2重新设计了内存控制器,提供独立的min/max/high/low四级水位:
| 文件 | 说明 |
|---|---|
| memory.min | 保证不被回收的最低内存量(硬保护) |
| memory.low | 尽力而为的低水位保护 |
| memory.max | 硬限制,触发cgroup OOM |
| memory.high | 软限制,触发throttling(节流) |
| memory.swap.max | swap使用上限 |
| memory.current | 当前内存使用量(含文件缓存) |
| memory.stat | 详细统计(anon, file, kernel_stack, slab等) |
cgroup v2优势:
- memory.high实现主动节流:当cgroup使用量超过high,内核对其进行回收(throttle),而非等待OOM。配合cpu.max可实现"多用CPU多用内存"的公平调度。
- 透明内核内存管控:v2的memcg自动统计每个cgroup的内核内存使用(slab、kernel stack等)
- 内存压力事件通知:通过压力停滞信息(PSI, Pressure Stall Information)接口可提前预警
5.3 PSI: 压力停滞信息(Pressure Stall Information)
PSI(Linux 4.20+)提供了细粒度的内存压力指标,能比水位线更早地预测OOM:
# 查看系统级内存压力
cat /proc/pressure/memory
some avg10=0.00 avg60=0.00 avg300=0.00 total=0
full avg10=0.00 avg60=0.00 avg300=0.00 total=0
# some: 至少一个进程因内存压力停顿
# full: 所有可运行进程同时因内存压力停顿(最严重)
PSI输出解读:
- avg10/avg60/avg300:过去10秒/60秒/300秒内因内存压力停顿的百分比
- total:累计停顿时间(μs)
- some表示"部分内存压力",full表示"系统级内存瓶颈"
PSI与cgroup结合:cgroup v2自动提供per-group的PSI指标:
cat /sys/fs/cgroup/mygroup/memory.pressure
some avg10=5.23 avg60=2.10 avg300=0.85 total=12345
full avg10=0.00 avg60=0.12 avg300=0.05 total=567
配合PSI设置自动告警(avg10 > 20时发送通知),可以在OOM发生前主动干预。
六、实战案例分析
6.1 案例一:Redis内存不足导致频繁direct reclaim
现象:Redis实例响应延迟从0.5ms飙升到50ms,监控显示上下文切换激增。
诊断:vmstat 1显示si/so持续大于0,perf sched latency显示大量时间消耗在shrink_lruvec上。/proc/vmstat中pgsteal_direct每秒增长数千。
根因:服务器同时运行了监控代理(如Zabbix Agent)和日志收集(Fluentd),两者合计占用了大量内存。Redis的used_memory接近物理内存上限,分配缓冲区时触发direct reclaim。
修复:
- 设置swappiness=1(允许文件页回收但尽量不swap匿名页,减少Redis匿名页被swap-out)
- 修改Redis配置:maxmemory=6GB(留2GB给系统和其他进程)
- 调整overmemory-policy=allkeys-lfu(内存满时自动淘汰冷key)
- 调整Redis的oom_score_adj=-1000(保护Redis不被OOM杀死)
- 最终:direct reclaim消失,延迟恢复到0.5ms以内
6.2 案例二:容器OOM引发的服务雪崩
现象:Kubernetes集群中某个Node上,多个Pod因OOM被杀死,引发服务雪崩。
诊断:kubectl describe pod显示Last State: OOMKilled。dmesg -T显示大量"Memory cgroup out of memory"日志。Node内存监控显示使用率最高85%,但某些Pod的RSS突然激增。
根因:
- Pod的memory.limit设置为2Gi,但应用因内存泄漏导致RSS缓慢增长至接近limit
- 当RSS达到limit时,触发cgroup OOM
- OOM Killer杀死的进程恰好是应用的主进程(Java主线程),触发kubelet重启
- 重启后紧接着又OOM → CrashLoopBackOff
修复:
- 短期:将memory.limit提高到4Gi,同时设置resources.requests=2Gi
- 中期:通过heap dump分析Java堆内存泄漏
- 长期:引入sidecar容器的memory.max限制,并为关键服务设置memory.min保证
- 监控告警:配置PSI监控,设置avg10 > 30时触发告警
6.3 案例三:数据库系统的zone_reclaim_mode调优
现象:MySQL服务器在64GB内存的2路NUMA机器上运行,但性能不如预期。
诊断:numastat -m显示大量Node 1的内存跨节点访问,perf stat -e node-load-misses,node-store-misses计数极高。
行级检查:cat /proc/sys/vm/zone_reclaim_mode 返回0,说明未启用zone reclaim。MySQL工作集约50GB,跨两个NUMA节点随机访问,大量远程访问带来延迟惩罚。
修复:
- 设置zone_reclaim_mode=1:内存分配优先从本地节点回收,减少跨节点访问
- 配置innodb_buffer_pool_size=48GB,确保缓冲池完全位于本地节点
- 使用numactl --interleave=all启动MySQL(均匀分布内存)
- 监控改进后node-load-misses降低60%,TPS提升30%
七、性能调优核心参数清单
| 参数 | 说明 | 推荐场景 |
|---|---|---|
| /proc/sys/vm/swappiness | 磁盘swap积极性 | 数据库=1, 默认=60, zram=150-200 |
| /proc/sys/vm/vfs_cache_pressure | dentry/inode缓存回收权重 | 数据库=50, 文件服务器=100 |
| /proc/sys/vm/min_free_kbytes | 最低保留空闲内存 | 大内存服务器=262144 |
| /proc/sys/vm/zone_reclaim_mode | NUMA本地回收 | 数据库=1, 普通=0 |
| /proc/sys/vm/extfrag_threshold | 内存碎片避免阈值 | THP=100, 默认=500 |
| /proc/sys/vm/laptop_mode | 延迟磁盘写入 | 笔记本=5, 服务器=0 |
| /proc/sys/vm/page-cluster | swap预读页数 | 默认=3, SSD=0~1 |
| /proc/sys/vm/oom_dump_tasks | OOM时打印所有task | 调试=1, 关闭=0 |
THP(Transparent Huge Pages)与页面回收
THP(透明大页)使用2MB大页代替4KB小页,减少TLB miss和页表开销。但在内存碎片化场景下,THP经常因无法分配连续2MB空间而失败(khugepaged碎片整理开销大)。数据库场景通常建议关闭THP:
# 关闭THP echo never > /sys/kernel/mm/transparent_hugepage/enabled echo never > /sys/kernel/mm/transparent_hugepage/defrag # 某些数据库(如MongoDB)在碎片场景下开启THP反而更高效 echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
八、可观测工具全景
| 工具 | 用途 |
|---|---|
| vmstat | 实时显示swap/内存/block活动 |
| sar -r | 历史内存使用统计 |
| numastat | NUMA内存分布统计 |
| slabtop | SLAB/SLUB分配器实时状态 |
| perf c2c | NUMA缓存一致性分析 |
| bpftrace -e 'kprobe:oom_kill_process' | OOM事件动态追踪 |
| cat /proc/pressure/memory | PSI内存压力 |
| cat /sys/fs/cgroup/memory/*/memory.usage_in_bytes | cgroup内存使用 |
| dmesg | grep -i oom | 历史OOM日志 |
九、总结
Linux页面回收与OOM Killer构成了内存管理的最后防线,核心要点可以归纳为:
LRU设计:双链表(Active/Inactive)+ Referenced位实现了近似LRU,区分匿名页(需swap)和文件页(可直接丢弃)。Linux 5.15+ 的mgLRU进一步提升了大规模系统的LRU效率。
回收路径:kswapd异步回收(WMARK_LOW触发)是首选;direct reclaim(WMARK_MIN触发)是同步痛苦路径;OOM Killer是所有手段失败后的最后手段。系统设计应尽量避免触发direct reclaim和OOM。
swap策略:swappiness平衡匿名页与文件页回收比例;用zram替代磁盘swap可以减少I/O等待;合理的swap使用不代表系统不健康——适度swap可以将冷内存让给热缓存。
OOM防护:oom_score_adj保护关键服务(-1000禁止杀死);cgroup v2 memory.max和memory.high提供分级保护;PSI提前预警避免OOM发生。
生产必做:设置合理的swappiness和min_free_kbytes;为关键服务配置oom_score_adj;使用PSI监控内存压力;在cgroup中为容器memory.limit留有余量。
物理内存终究有限,Page Reclaim就是把有限的资源分配给最需要的人的艺术。理解它,才能在内存风暴中守住性能的底线。

发表评论 取消回复