引言
Linux 内核的内存管理是操作系统最核心的子系统之一。无论是容器化部署中的 OOM 问题、数据库场景的 HugePages 调优、还是高性能计算中的 NUMA 亲和性,都直接依赖对内核内存管理机制的理解。本文将从伙伴系统(Buddy System)出发,完整剖析 Slab/SLUB 分配器、反向映射与页面回收算法、OOM Killer 选择策略、HugePages 与透明大页、KSM 内存去重以及 NUMA 内存策略,并在每一节给出生产环境中的调优命令、参数配置和排错案例。
一、Buddy System 伙伴系统
1.1 核心原理
伙伴系统是 Linux 内核管理物理页面的基础框架。内核将物理内存划分为多个区域(Zone),如 ZONE_DMA、ZONE_DMA32、ZONE_NORMAL、ZONE_MOVABLE,每个区域维护 11 个空闲页面链表(order 0 到 order 10),分别管理 1、2、4、...、1024 个连续物理页面。
分配时从对应 order 的链表中取出一个页面块;若该 order 无空闲,则从更高 order 拆分。释放时检查"伙伴"(地址连续、相同大小)是否空闲,若空闲则合并到更高 order。这种分配策略保证了 O(1) 的分配效率和最小的外部碎片。
1.2 碎片问题与解决方案
长期运行后,伙伴系统会产生外部碎片——空闲页面总量充足但无足够大的连续物理块。内核通过以下手段缓解:
- 可移动/不可移动页面分类:ZONE_MOVABLE 专门存放可回收和可移动页面,确保在需要大块连续内存时有足够的可迁移页面。
- 页面迁移(Page Migration): compaction 机制在分配失败时将可移动页面搬移,腾出连续物理块。
/proc/sys/vm/extfrag_threshold控制碎片指数阈值。 - order-0 页面隔离:将不可移动的 order-0 页面(如内核栈)与可移动页面隔离,防止大块分配被打断。
1.3 实战:查看伙伴系统状态
Node 0, zone Normal 12 8 4 2 1 3 2 1 1 0 1
cat /proc/pagetypeinfo | head -30
/proc/buddyinfo 显示每个 Zone 各 order 的空闲块数量。如果高位 order 持续为 0 说明碎片严重。
二、Slab / SLUB 分配器
2.1 为什么需要 Slab
伙伴系统以页面(通常 4KB)为粒度分配。但内核大部分内存请求远小于 4KB(如 task_struct 约 7KB、inode 约 600B)。如果直接通过伙伴系统分配会产生严重的内部碎片。Slab 分配器在伙伴系统之上构建对象缓存,预先分配多个对象在页面中,实现高效的小对象分配。
2.2 SLUB 分配器架构
SLUB(Unqueued Slab Allocator)是当前 Linux 默认的 Slab 实现,相比 SLAB 简化了每-CPU 队列和 NUMA 节点队列的管理:
- kmem_cache:每种对象类型对应一个 kmem_cache,维护对象大小、对齐方式、构造函数等元数据。
- CPU Partial 链表:每个 CPU 维护一个专属的 slab 列表,分配/释放无锁操作,性能最优。
- Node Partial 链表:每个 NUMA 节点维护 partial slab 列表,作为 CPU partial 的补充。
- Full Slab:完全使用的 slab,不参与分配,等待释放后变为 partial。
2.3 kmalloc 与 kmem_cache_alloc
kmalloc() 使用预定义的通用缓存(kmalloc-8、kmalloc-16 到 kmalloc-8192 共 13 种大小)。如果对象大小不在预定义范围或需要特殊初始化,应使用 kmem_cache_create() 创建专用缓存:
struct kmem_cache *my_cache = kmem_cache_create("my_obj", sizeof(struct my_type), 0, SLAB_HWCACHE_ALIGN, NULL);
// 分配对象
struct my_type *obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
// 释放
kmem_cache_free(my_cache, obj);
2.4 实战:Slab 内存泄漏排查
slabtop -o --sort=c | head -25
# 通过 /proc/slabinfo 查看具体缓存
cat /proc/slabinfo | awk 'NR==1 || * > 1000000' | column -t
如果发现某个 kmem_cache 的 total objects(第2列)持续增长且 active objects(第1列)极少,通常意味着该缓存的分配/释放不匹配,存在泄漏。使用 kmemleak 可以进一步定位未释放的调用栈:
cat /sys/kernel/debug/kmemleak
三、页面回收与 Swap 机制
3.1 LRU 算法与两个链表
Linux 使用改进的双链 LRU 算法管理页面回收。每个 LRU 列表包含五种类型:LRU_INACTIVE_ANON、LRU_ACTIVE_ANON、LRU_INACTIVE_FILE、LRU_ACTIVE_FILE、LRRU_UNEVICTABLE。Active 列表中的页面被认为近期被访问过,Inactive 列表中的页面是回收候选。
页面访问时通过 PG_referenced 标志和 PTE 中的 Accessed 位决定升降级:第一次访问移到 active,第二次访问确认活跃状态;后台回收线程 kswapd 将 inactive 尾部的页面回收。
3.2 页面回收的触发条件与流程
页面回收在三种场景下触发:
- Direct Reclaim(直接回收):进程分配内存时发现空闲低于 min_wmark,同步阻塞在回收路径上。这会导致性能抖动。
- kswapd 后台回收:当空闲内存低于 low_wmark 时唤醒 kswapd,积极回收直到达到 high_wmark。
- OOM Killer:当 kswapd 和 direct reclaim 都无法释放足够内存时触发。
3.3 回收参数调优
三个水位的计算公式为:min = min_free_kbytes,low = min * 5/4,high = min * 3/2。调优关键参数:
| 参数 | 说明 | 建议值 |
|---|---|---|
vm.min_free_kbytes | 最小保留内存,防止 direct reclaim | 物理内存的 3%~5%,不少于 65536KB |
vm.swappiness | Swap 倾向(0-100) | 数据库:1~10;桌面:60;默认 60 |
vm.dirty_ratio | 脏页占系统内存%时强制刷盘 | 高性能存储:40;SSD:20 |
vm.dirty_background_ratio | 后台 pdflush 启动阈值 | 1/2 of dirty_ratio |
vm.zone_reclaim_mode | NUMA 节点本地回收 | NUMA 计算密集:1;通用:0 |
四、OOM Killer 深度剖析
4.1 选择算法
当系统内存耗尽且回收失败时,OOM Killer 通过 oom_badness() 函数为每个进程打分(0-1000),分数越高越可能被 kill:
- 基础分数:进程占用的物理内存(RSS + Swap + Page Table + mmap)占总内存的比例乘以 1000。
- 调整因子:根进程(-75%)、oom_score_adj 直接设置、运行时间长的进程减分、nice 值高的进程加分。
- 核心豁免:init(PID 1)、内核线程、OOM_SCORE_ADJ_MIN(-1000)的进程不会被 kill。
4.2 cgroup OOM vs 系统 OOM
在容器化场景中,每个 cgroup 有自己的内存上限。当 cgroup 内存超限时触发 cgroup OOM,只 kill 该 cgroup 内的进程,不影响整个系统。systemd 和 Kubernetes 的 OOMKilled 事件就是基于此机制。
4.3 实战:OOM 诊断与防护
ps -eo pid,comm,oom_score,oom_score_adj --sort=-oom_score | head
# 保护关键进程(如数据库主进程)
echo -1000 > /proc//oom_score_adj
# 查看 OOM 事件历史
dmesg | grep -i "out of memory"
journalctl -k | grep -i "oom\|killed process"
# cgroup OOM 事件监控
cat /sys/fs/cgroup/memory/memory.oom_control
echo 1 > /sys/fs/cgroup/memory/memory.oom_control # 禁用自动 OOM(需手动处理)
4.4 生产环境最佳实践
- 为数据库和缓存服务进程设置
oom_score_adj = -1000或直接通过 systemdOOMPolicy=continue配置。 - 启用 cgroup 内存限制
memory.limit_in_bytes和memory.soft_limit_in_bytes,避免单服务耗尽系统内存。 - 监控
oom_kill指标(/proc/vmstat 中的 oom_kill 计数器),Prometheus 告警阈值设为 >0。
五、HugePages 与透明大页
5.1 为什么需要大页
标准 x86_64 页大小 4KB,TLB 通常只有 64-128 个条目。访问 1GB 内存需要约 26万 次页表查找,其中绝大部分因 TLB miss 需要多步页表遍历。2MB HugePage 将减少 512 倍页表项,大幅降低 TLB miss 率。对于数据库(PostgreSQL、Redis)和科学计算场景,性能提升可达 10%~30%。
5.2 静态 HugePages
静态 HugePages 需要在系统启动时预留,分配后资源被锁定,普通进程不可使用,但确保了大页不会被碎片化影响。配置方式:
echo 4096 > /proc/sys/vm/nr_hugepages
# 或 sysctl 持久化
echo "vm.nr_hugepages = 4096" >> /etc/sysctl.d/99-hugepages.conf
# PostgreSQL 配置
# postgresql.conf
huge_pages = on # 启用大页
shared_buffers = 16GB # 应与 HugePages 内存匹配
5.3 透明大页(Transparent HugePages)
THP 是内核通过 khugepaged 后台线程自动将连续 4KB 页合并为 2MB 大页的机制,无需应用修改。但对数据库场景,khugepaged 的频繁合并/拆分操作反而造成延迟抖动。MongoDB、PostgreSQL、Redis 官方文档均建议关闭 THP:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
# 持久化(grub 参数)
# GRUB_CMDLINE_LINUX="transparent_hugepage=never"
grub2-mkconfig -o /boot/grub2/grub.cfg
5.4 监控 HugePages 使用
# HugePages_Total: 4096
# HugePages_Free: 1024
# HugePages_Rsvd: 200
# Hugepagesize: 2048 kB
持续关注 HugePages_Free,若接近 0 会导致分配失败并 fallback 到普通页。建议设置 vm.nr_hugepages 时留 10%~15% 冗余。
六、KSM 内存去重(Kernel Samepage Merging)
6.1 工作原理
KSM 是内核的内存去重服务,通过 ksmd 后台线程扫描页面内容,找到相同内容的页面并合并为一个只读页面(Copy-on-Write),在写入时再复制。KSM 特别适用于运行大量相似虚拟机/容器的场景,可节省 30%~60% 内存。
6.2 KSM 配置与调优
echo 1 > /sys/kernel/mm/ksm/run
# 每秒扫描页数(越大越积极,CPU 消耗越高)
echo 100 > /sys/kernel/mm/ksm/pages_to_scan
# 扫描间隔(毫秒)
echo 200 > /sys/kernel/mm/ksm/sleep_millisecs
# 查看节省的内存
grep -E "pages_shared|pages_sharing|pages_unshared|pages_volatile" /sys/kernel/mm/ksm/
pages_sharing 表示当前被共享的页面数,pages_shared 表示已被合并的唯一页面数。节省的内存约为 (pages_sharing - pages_shared) * 4KB。
6.3 应用场景与权衡
- 适合场景:VDI 桌面虚拟化、KVM 相同 OS 容器、大量相同基础镜像的 Docker 容器。
- 不适合场景:容器内容差异大(去重率低)、CPU 敏感型延迟应用(ksmd 消耗 CPU)。
- 安全考虑:KSM 可能被用于侧信道攻击(如通过计时差异推测其他 VM 的内容),高安全环境建议关闭。
七、NUMA 内存策略
7.1 NUMA 架构简介
NUMA(Non-Uniform Memory Access)架构将 CPU 和内存分为多个节点,每个节点内的内存访问速度最快(本地访问),跨节点访问需要通过 QPI/UPI 总线,延迟高 1.5~3 倍。现代多路服务器几乎都是 NUMA 架构。
7.2 NUMA 内存分配策略
内核提供四种 NUMA 内存策略:
- Default(默认):在请求进程运行的 CPU 所在节点的本地分配。
- Bind:强制从指定节点集合分配,若节点内存不足则触发 OOM。
- Preferred:优先从指定节点分配,不足时 fallback 到其他节点。
- Interleaved:在所有节点间轮询分配,均衡负载。
7.3 进程 NUMA 绑定实战
numactl --hardware
numastat -m
# 绑定进程到 NUMA node 0 的 CPU 和内存
numactl --cpunodebind=0 --membind=0 ./my_db_server
# 查看进程的 NUMA 策略
numactl -p
# 查看进程各节点的内存使用
numastat
7.4 数据库 NUMA 调优
- PostgreSQL:设置
shared_buffers不超过单节点内存的 25%,确保主线程和 worker 在同一节点。numa=on时 PG 会交错分配 shared buffers。 - Redis:单实例建议绑定单节点(
numactl --membind=0);避免跨节点访问导致 30% 性能损失。 - InfluxDB / ClickHouse:OLAP 查询可以跨节点并行,但 memtable 和 write buffer 尽量本地化。
- 内核参数:
numa_balancing内核特性自动迁移页面到访问 CPU 所在节点,数据库场景建议关闭(echo 0 > /proc/sys/kernel/numa_balancing)。
八、生产环境监控与排错综合指南
8.1 关键监控指标
| 指标 | 来源 | 告警阈值 |
|---|---|---|
| 内存使用率 | /proc/meminfo | > 85% 持续 5 分钟 |
| 可用内存 | MemAvailable | < 总内存 10% |
| 页扫描率 | pgscan_kswapd / pgscan_direct | direct > 100/s 持续 2 分钟 |
| OOM Kill 次数 | vmstat.oom_kill | 增量 > 0 |
| HugePages 使用率 | HugePages_Free | < 总页数 5% |
| NUMA 本地访问率 | numastat | < 80> |
| Slab 占用 | /proc/meminfo Slab | > 总内存 30% |
8.2 高效排错流程图
系统响应变慢 / OOM → 查看 dmesg | grep -i 'oom\|kill' → 如果是 OOM:检查 dmesg 中的 OOM 日志确认被 kill 进程 → 检查 cgroup 内存限制 → 检查 vmstat 1 确认 si/so(swap 交换) → 检查 top RSS 占用 → 如果是 Slab 泄漏:slabtop + kmemleak;如果是应用内存增长:valgrind 或 --leak-check。
8.3 sysctl 调优模板
vm.min_free_kbytes = 262144
vm.swappiness = 10
vm.dirty_ratio = 20
vm.dirty_background_ratio = 10
vm.overcommit_memory = 0
vm.overcommit_ratio = 80
vm.zone_reclaim_mode = 0
vm.numa_balancing = 0
kernel.panic_on_oom = 0
vm.panic_on_oom = 0
sysctl -p /etc/sysctl.d/99-memory-tuning.conf
总结
Linux 内核内存管理是一个精密的分层系统:伙伴系统管理物理页面,Slab/SLUB 服务小对象分配,LRU 算法驱动页面回收,HugePages 减少 TLB miss,KSM 实现页面去重,NUMA 策略优化多路架构下的访问延迟。理解这些机制不仅有助于诊断 OOM、内存泄漏和性能抖动,更能在数据库部署、容器调优和高性能计算场景中做出正确的参数配置决策。建议读者结合自身场景,逐步建立内存指标的常态化监控体系,在问题发生前完成预防性调优。

发表评论 取消回复