7性能之巅:系统、企业与云可观测性(第2版)

内存

虚拟内存、分配器与缓存

虚拟内存TLB缺页异常slabNUMAswap

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第7章 内存

虚拟内存、分配器与缓存

章节导读

内存是性能问题中最"沉默"的资源:CPU 饱和有使用率报警,磁盘饱和有 iowait,而内存问题往往以诡异的方式呈现——应用突然被 OOM killer 杀死、延迟出现周期性毛刺、机器整体"发黏"却找不到热点。理解内存性能需要同时掌握三层机制:硬件层(DRAM、缓存、TLB、NUMA)、内核层(虚拟内存、分页、回收、slab)、用户层(malloc 分配器、运行时堆、应用数据结构)。本章自底向上打通这三层。

先讲虚拟内存的动机与机制。物理内存有限且需要隔离,虚拟内存给每个进程一个独立的地址空间幻觉:进程看到的每个地址(虚拟地址)经 MMU(内存管理单元)翻译为物理地址。翻译按页进行(x86-64 默认 4KB,也支持 2MB/1GB 大页),页表是多级结构(四级:PGD→PUD→PMD→PTE),每次翻译理论上要走四次内存访问——这显然不可接受,于是 CPU 用 TLB(Translation Lookaside Buffer,页表项缓存)加速:L1 dTLB 约 64 项(覆盖 256KB),L2 sTLB 约 1536-2048 项(覆盖 6-8MB),命中时翻译近乎免费(<1ns),未命中(TLB miss)要走页表遍历(page walk,约 7-30 周期,若页表本身不在缓存则更糟)。性能含义:工作集超过 sTLB 覆盖范围后,随机访存模式会叠加 TLB miss 开销——这是大内存应用(数据库、JVM 大堆)需要大页(hugepage)的根本原因:2MB 页让 1536 项 TLB 覆盖 3GB 而非 6MB。缺页异常(page fault)是虚拟内存的"按需分配"机制:访问未映射的页触发异常,内核此时才分配物理页(minor fault,约 0.5-2μs)或从磁盘读入(major fault,约 2-10ms——慢百万倍!)。观测:ps -o min_flt,maj_flt、sar -B(pgfault/pgmajfault 速率)、perf 的 page-faults 事件采样。major fault 非零即警报:要么 swap 抖动、要么 mmap 文件随机读、要么代码段被回收后重新载入。

物理内存的内核管理分两套体系。伙伴系统(buddy allocator)管理页帧(4KB 页),按 2 的幂分组,分配/释放 O(1),但外部碎片会导致"总空闲足够却分不出连续页"——内核用页迁移(compaction)整理碎片,这个过程可能造成延迟毛刺(sar -B 的 pgmigrate 计数)。slab/slub 分配器在页之上管理小对象(task_struct、inode、dentry、sk_buff 等内核对象),按对象大小分 cache 复用,避免频繁页分配。观测:slabtop 或 /proc/slabinfo 显示每类对象的数量与内存占用——dentry 与 inode_cache 暴涨通常是"扫描海量文件"的副作用(备份任务、find、日志轮转风暴),kmalloc-* 异常增长可能是驱动泄漏。用户态的 malloc 是第三层:glibc 的 ptmalloc2 用 arena(每 CPU 一个,减少锁争用)+ bins(按大小分桶)管理,小块从堆(brk)切分,大块(>128KB 默认)直接 mmap。ptmalloc 的著名问题:多线程高频分配不同大小对象时 arena 碎片化,RSS 只增不减(不是泄漏,是碎片);free 的大块内存不还给 OS(trim 阈值控制)。替代分配器 jemalloc(Facebook,低碎片、可剖析)与 tcmalloc(Google,线程缓存)在大内存服务上常带来 10-30% 的 RSS 下降与分配性能提升。

内存回收是延迟毛刺的常见来源。当空闲内存低于水位(watermark),kswapd 内核线程后台回收(扫描 LRU 链表,把干净文件页直接丢弃、脏页回写、匿名页换出);回收速度跟不上分配速度时,分配路径上的进程被迫同步回收(direct reclaim)——这是应用线程"莫名卡顿几毫秒到几百毫秒"的经典根因(sar -B 的 pgscank 是 kswapd 扫描、pgscand 是直接回收扫描,后者非零即性能事件)。vm.swappiness(0-200)控制回收倾向:值高偏向换出匿名页(保留文件缓存),值低偏向丢文件页。透明大页(THP)是另一个毛刺源:khugepaged 后台合并 4KB 页为 2MB 页 + 分配时的同步 compaction,对延迟敏感服务(Redis、MongoDB、Oracle 官方都建议关闭 THP 的 always 模式改 madvise)常是隐形杀手。

swap 的正确认知:swap 不是洪水猛兽,它是"冷页卸载"机制——把长期不访问的匿名页换出,让文件缓存有更多空间,反而提升整体效率。病态的是 swap 抖动(thrashing):工作集超过物理内存,页刚换入又被换出,major fault 速率飙升,系统吞吐崩塌(vmstat 的 si/so 持续非零 + sar -B 的 pgmajfault 高)。处置:短期加内存或减负载;长期看 PSI memory 的 full 值做主动驱逐(Facebook 的 oomd、systemd-oomd 在 PSI 超阈值时杀掉可牺牲服务,避免整机僵死)。容器时代 swap 常被禁用(Kubernetes 默认不允许),内存超限直接 OOM Kill——这使"内存画像"(应用真实工作集测量)更加重要。

NUMA(非一致内存访问)是大内存服务器的必修课。多路服务器的内存挂在各自 CPU 插槽上,访问本地内存约 80-100ns,远端(跨 UPI 总线)约 1.5-2 倍延迟且带宽受限。内核默认"本地分配"策略(任务在哪核跑就分哪插槽的内存),但进程迁移或本地内存耗尽会导致远端分配。观测:numastat(每 NUMA 节点的分配与命中统计,numa_miss/numa_foreign 高即跨节点分配)、numastat -p PID(进程级)、perf 的 node-load-misses 事件。调优:numactl --cpunodebind=0 --membind=0 绑定关键服务到单节点;数据库(MySQL、PostgreSQL、Redis)通常建议 NUMA 绑定或 interleave 策略;内核参数 vm.zone_reclaim_mode 影响本地回收与远端分配的权衡。云虚拟机通常呈现为单 NUMA 节点(或 vNUMA 映射宿主机拓扑),大规格实例(如 128 vCPU)可能跨节点,购买大实例时要意识到 NUMA 拓扑的存在。

内存观测的工具矩阵:free(总量/可用/缓存,注意 available 才是关键列)、/proc/meminfo(全量计数器:AnonPages、Cached、Slab、PageTables、HugePages、Dirty、Shmem)、vmstat(si/so 换页、in/cs 中断切换)、sar -B(缺页与回收统计)、sar -r(内存水位)、ps/pidstat -r(进程 RSS、minflt/majflt)、/proc/PID/smaps_rollup(进程内存精细分解:Rss、Pss 按比例分摊共享页、Swap、PageTables)、smem 工具(PSS 视角的进程排行——比 RSS 排行更真实,因为共享库被重复计入 RSS)。泄漏诊断:用户态用 memleak/heaptrack/jmap(第 5 章),内核态用 /proc/slabinfo 趋势 + kmemleak(开发环境)。OOM 分析:dmesg 的 OOM 报告含被杀进程、内存全景与进程表——oom_score_adj 控制牺牲顺序。

本章的知识将支撑后续多章:第 8 章的页缓存是文件 I/O 性能的核心,第 10 章的容器内存限制与 OOM Kill 是云环境高频问题,第 12 章的大页与 NUMA 绑定是标准调优项。内存性能的心法一句话:让工作集适配缓存与 TLB 层次(快),让分配路径避开同步回收与锁争用(稳),让观测覆盖用户堆、内核 slab 与页缓存三个池子(全)。

核心概念

  • 虚拟内存
  • TLB
  • 缺页异常
  • slab
  • NUMA
  • swap

要点精析

缺页异常的完整分类与成本。minor fault(次缺页):页在内存中但未映射到当前页表——首次触碰 malloc 的内存(匿名页延迟分配)、COW(fork 后写时复制)、页缓存命中(文件页已在内存只需建映射),成本约 0.5-2μs(含陷入内核、分配页框、建页表、返回)。major fault(主缺页):页不在内存,需磁盘 I/O——swap 换入、mmap 文件首次读、代码段页被回收后重载,成本 2-10ms(HDD)或 50-200μs(SSD),比 minor 慢百倍以上。性能含义:(1)fork 大内存进程的 COW 风暴——Redis bgsave 时父进程写密集会导致大量 COW fault(Redis 的 latency 尖刺经典根因);(2)JVM 大堆启动的预热——堆是虚拟保留物理延迟分配,启动后 major/minor fault 密集,压测前必须预热;(3)mmap 大文件随机读——每次跨页都是潜在 major fault,数据库通常自管缓冲池(pread + 用户态缓存)而不用 mmap 正是为了控制缺页行为。观测:sar -B 的 majflt/s、ps -o maj_flt 差分、perf record -e major-faults 定位缺页代码路径、BCC 的 pgsub/pgfault 工具。

TLB 与大页的量化分析。TLB miss 的成本:page walk 约 7-30 周期(页表在缓存中),页表也不在缓存时可达 100+ 周期;对随机访存的大工作集应用,TLB miss 可占执行时间 10-30%。估算方法:工作集 W、页大小 P、TLB 项数 N——若 W/P > N(4KB 页 × 1536 项 = 6MB 覆盖),随机访问下 miss 率 ≈ 1 - N×P/W。大页的收益:2MB 页把覆盖范围扩大 512 倍(1536 项 × 2MB = 3GB),1GB 页更彻底且 page walk 层级更少。使用方式:透明大页 THP(/sys/kernel/mm/transparent_hugepage/enabled,always=激进、madvise=应用显式请求、never=关闭;always 模式的 khugepaged 合并与分配时 compaction 会造成延迟毛刺,延迟敏感服务建议 madvise)与显式大页 hugetlbfs(预留固定数量的 2MB/1GB 页,vm.nr_hugepages,JVM 的 -XX:+UseLargePages、DPDK、数据库都用此方式,无 compaction 抖动)。观测 TLB 压力:perf stat -e dTLB-load-misses,dTLB-loads 得 miss 率(>1% 且工作集大即候选优化点)。决策:JVM 大堆(>8GB)与数据库大缓冲池是大页的主要受益者;小内存应用收益甚微。

页缓存(page cache):文件 I/O 性能的第一因素。内核把所有文件数据缓存在空闲内存中(Cached),读文件先查缓存(命中则零磁盘 I/O,速度是磁盘的千倍),写文件默认写入缓存标记为脏页(Dirty),由回写机制异步刷盘。关键计数器(/proc/meminfo):Cached(文件缓存量)、Dirty(待回写脏页)、Writeback(正在回写)。脏页控制参数:vm.dirty_background_bytes/ratio(超过即 kworker 异步回写)、vm.dirty_bytes/ratio(超过即写进程被同步阻塞回写——延迟杀手)。大内存机器的陷阱:默认 ratio(10%/20%)在 512GB 内存上意味着 50GB 脏页才触发同步回写,一次刷盘风暴能让 I/O 延迟飙升秒级——生产建议改用绝对字节(dirty_bytes=2GB、dirty_background_bytes=512MB)。缓存回收:内存压力下文件页按 LRU 回收(干净页直接丢,脏页先回写),active/inactive 双链表防止一次性扫描冲掉热数据。观测缓存效率:/proc/PID/io 的 rchar 与 read_bytes 差值(应用读量 vs 真实磁盘读量 = 缓存命中收益)、fincore/vmtouch(文件的缓存驻留比例)、pcstat(每文件缓存状态)。方法论:数据库与文件服务的性能问题先问"缓存命中率如何",再谈磁盘。

内存回收路径与 direct reclaim 的延迟影响。回收触发链:空闲页低于 low 水位 → kswapd 异步回收(后台,不影响应用)→ 低于 min 水位或回收不及 → 分配路径同步执行 direct reclaim(应用线程陷入回收:扫 LRU、回写脏页、可能等 I/O——延迟从 ns 级跳到 ms 级)。诊断:sar -B 的 pgscank/s(kswapd 扫描)与 pgscand/s(直接回收扫描)——pgscand 非零即"内存压力已传导到应用延迟";/proc/zoneinfo 的各 zone 水位与空闲页对比;PSI memory 的 some/full 值(full 非零 = 所有任务都在等内存,严重)。相关毛刺源:(1)THP 分配触发的同步 compaction(defrag 策略为 always 时,/sys/kernel/mm/transparent_hugepage/defrag);(2)脏页比例过高时回收必须先回写(I/O 放大);(3)cgroup v1 的 per-cgroup 回收在容器内存逼近 limit 时高频触发(容器内 direct reclaim 是 K8s 延迟毛刺的常见根因,v2 的 memory.high 提供缓冲带)。调优思路:留足空闲水位(vm.min_free_kbytes 调大给回收留缓冲)、降低脏页阈值、关 THP defrag 同步路径、容器 limit 与真实需求留 20% 余量。

用户态分配器的行为与选型。glibc ptmalloc2:per-thread arena(上限 8×核数)+ fastbins/smallbins/largebins + tcache(2.26+ 的线程本地缓存,小分配无锁快路径)。问题模式:arena 碎片(线程迁移导致多 arena 各自持内存)、大块 free 不归还(M_TRIM_THRESHOLD 默认 128KB,且只 trim 堆顶)、fork 后 COW 放大。jemalloc:size-class 精细(减少内部碎片)、extent 管理、后台线程主动归还内存(dirty decay)、内建 heap profiling(MALLOC_CONF=prof:true)——Redis、Firefox、Android 的选择。tcmalloc:thread cache + central cache + page heap 三层,per-CPU 模式(新版)用 rseq 实现无锁快路径,Google 全家桶标配。选型依据:分配密集多线程服务(网络代理、缓存服务)换 jemalloc/tcmalloc 常见 10-30% RSS 下降与尾延迟改善;诊断碎片 vs 泄漏:RSS 高但分配器统计的 active 内存低 = 碎片(jemalloc 的 stats.allocated vs resident,tcmalloc 的 /pprof/heap)。观测分配行为:BCC memleak(uprobe malloc/free)、perf record -e 挂 malloc 的 uprobe、分配器的内建统计接口。

OOM killer 的机制与生存指南。内核内存耗尽且回收无效时,OOM killer 按 oom_score(基于 RSS 占比,可被 oom_score_adj 调整:-1000 免杀,+1000 优先杀)选择牺牲品杀死。dmesg 的 OOM 报告是事后分析的金矿:触发上下文(哪个进程在分配时触发)、内存全景(各 zone 水位、slab、页缓存)、候选进程表(RSS 排行)——先读报告再下结论。容器语境:cgroup 内存超限触发的是 memcg OOM(只杀 cgroup 内进程),K8s 显示为 OOMKilled(exit code 137)——注意 limit 覆盖 RSS + 页缓存(cgroup v1 计入缓存,v2 类似),文件 I/O 密集容器可能因缓存计入而"被 OOM"(内核会先回收缓存,回收不及才杀)。防御配置:关键服务 oom_score_adj=-999 + 牺牲品服务(如日志代理)设高值;vm.overcommit_memory 语义(0 启发式拒绝明显超额、1 总是允许——Redis 建议 1、2 严格不超 swap+ratio×RAM);监控 PSI memory 与 memcg 的 memory.events(low/high/max 计数)做提前预警。JVM 特别提示:-Xmx 之外留足堆外(Metaspace、线程栈、DirectBuffer、GC 结构),容器 limit ≥ Xmx × 1.3 是经验起点。

NUMA 的观测与调优实操。拓扑确认:numactl --hardware(节点数、每节点内存、距离矩阵——距离 10 为本地,21+ 为跨节点)、lscpu。运行时统计:numastat 全局(numa_hit 本地分配成功、numa_miss 本想本地却分到远端、numa_foreign 被其他节点借走、interleave_hit)——miss/(hit+miss) 超过 5% 即值得干预;numastat -p PID 进程级分布。进程级诊断:/proc/PID/numa_maps 逐段显示 N0=x N1=y(各节点页数),大段内存分布不均即绑定失败。调优手段:numactl --cpunodebind=N --membind=N 启动服务(CPU 与内存同节点);--interleave=all 均匀打散(适合无法分区的共享大内存,Redis 官方建议之一);内核 vm.zone_reclaim_mode=0(默认,本地不足时直接远端分配而非激进本地回收——多数场景正确);数据库的 NUMA 感知配置(PostgreSQL 无原生绑定需 numactl 包裹,MySQL 有 innodb_numa_interleave)。云注意:大规格 vCPU 实例(>64 核)大概率跨 NUMA,购买时选"单节点规格"或接受跨节点税;K8s 的 Topology Manager 与 CPU Manager(static 策略)可做 Pod 级 NUMA 对齐,需要 kubelet 配置。

内存观测的口径学:VSS/RSS/PSS/USS 与 shared。VSS(虚拟大小)含未分配物理页的保留地址空间,几乎无诊断价值(JVM 的 VSS 可以是堆的数倍)。RSS(常驻集)= 进程实际占用的物理页,但共享页(库、共享内存)被每个共享者全额计入——所有进程 RSS 之和可以远超物理内存!PSS(按比例分摊)= 私有页 + 共享页/共享者数,是"进程真实内存成本"的正确口径(smem 工具、/proc/PID/smaps 提供)。USS = 纯私有页(杀掉该进程能释放多少)。诊断场景对应:找"谁吃了内存"用 PSS 排行(smem -rk 或 smem -tk);评估杀进程收益用 USS;容器限额对应 memcg 的 memory.current(含该 cgroup 的匿名页+文件缓存+内核内存)。/proc/PID/smaps_rollup 是单进程汇总(Rss/Pss/Anonymous/Swap/PageTables),PageTables 项对大内存应用不可忽略——512GB 数据库的页表本身可占数 GB(大页可大幅缩减)。

内存泄漏的系统化诊断流程。确认泄漏:PSS(或 memcg current)随时间线性增长且 GC/回收后不回落——先画 24 小时趋势图区分"泄漏(无界增长)"与"缓存增长(有平台期)"。定位层次:(1)用户态堆——语言工具(jmap/MAT 支配树、Go pprof heap diff、memray、heaptrack)或 memleak(uprobe malloc/free 按栈聚合未释放量,语言无关);(2)运行时堆外——JVM NMT(Native Memory Tracking)、/proc/PID/smaps 的匿名段 vs 文件段分析;(3)内核态——slabtop 趋势(dentry/inode 暴涨是缓存不是泄漏,kmalloc-* 或特定驱动 cache 增长才是嫌疑)、kmemleak(需内核编译选项,开发环境用)、page_owner(页分配归因,/sys/kernel/debug/page_owner);(4)fd/线程泄漏的内存伴生——每 fd 与线程栈都吃内存,lsof 计数趋势。经典陷阱:glibc arena 碎片被误判为泄漏(换 jemalloc 验证)、tmpfs/shm 增长计入内存(df -h /dev/shm)、THP 导致的 RSS 虚高(按 2MB 粒度分配)。方法论:泄漏诊断是"存量差分"游戏——两个时间点的全量内存账本(用户堆、内核 slab、页表、shm)做差,增长项即线索。

swap 与内存压缩的现代实践。传统 swap 到磁盘的延迟太高,现代方案:(1)zram——内存中压缩块设备作 swap(压缩比 2-3 倍,访问延迟 μs 级),Android/ChromeOS 默认,服务器也可用(等效"用 CPU 换内存");(2)zswap——swap 的前置压缩缓存,热换出页留在内存压缩池,减少真实磁盘 swap;(3)分层内存(memory tiering)——CXL 内存、Intel Optane PMEM 作为慢速大容量层,内核 5.18+ 的 NUMA balancing 扩展支持自动冷热页迁移(damos/TPP)。swap 调优参数:vm.swappiness(0-200,服务器常用 10-60:允许换出冷匿名页但保守;0 在旧内核近似禁用)、vm.vfs_cache_pressure(dentry/inode 回收倾向)、vm.page-cluster(swap 一次读入的页数的 2 的幂,默认 3=8 页,SSD 上可调小减少放大)。swap 抖动的确认:vmstat si/so 持续非零 + majflt 高 + 应用延迟与 swap I/O 相关——此时任何参数调整都是止痛药,加内存或减工作集才是解药。K8s 语境:1.22+ 支持 NodeSwap 特性(beta),此前一律禁 swap 的设计正在松动,PSI 驱动的驱逐(memory QoS)是方向。

内存性能问题的模式库。模式一:RSS 缓涨后 OOM——泄漏或碎片,走泄漏诊断流程。模式二:延迟周期性毛刺 + pgscand 非零——direct reclaim,调水位/脏页/THP。模式三:fork 后延迟尖刺——COW 风暴(Redis bgsave、PHP-FPM),解法:减少 fork 频率、大页(COW 粒度变大反而更少 fault?不——大页 COW 拷贝更贵,需实测;Redis 官方建议关 THP 正因 COW 放大)、写时复制友好的数据结构。模式四:numa_miss 高 + 远端延迟——NUMA 失衡,绑定或 interleave。模式五:slab 中 dentry 数千万——文件扫描风暴(备份、find /、日志清理脚本),解法:限制扫描范围、vfs_cache_pressure 调高、negative dentry 限制(内核 5.10+ 的 sysctl)。模式六:PageTables 占数 GB——大内存 + 4KB 页,上大页。模式七:容器 OOMKilled 但进程 RSS 不高——memcg 计入了页缓存或内核内存(kmem),查 memory.stat 分解。每个模式都有"确认指标 + 处置手段",构成内存排障的决策树。

代码示例

free -m
cat /proc/meminfo | egrep "MemAvailable|Cached|Dirty|Writeback|Slab|AnonPages|PageTables|Shmem|HugePages_Total"
vmstat 1 5
/* si/so 列:swap 活动 */
sar -B 1 5
/* pgfault/pgmajfault/pgscank/pgscand/pgsteal */
cat /proc/pressure/memory
smem -rk -P myapp
/* PSS 视角的进程内存排行 */
内存 USE 检查的标准命令组。free -m 重点读 available(内核估算的可分配量 = 空闲 + 可回收缓存,比 free 列有意义得多)与 buff/cache。/proc/meminfo 的精选字段构成内存账本:AnonPages(应用匿名内存总量——泄漏观察对象)、Cached(页缓存)、Slab(内核对象,细分看 /proc/slabinfo)、PageTables(页表开销,大内存应用的隐形消耗)、Shmem(tmpfs 与共享内存——常被忘记的内存去向)、Dirty/Writeback(回写压力)。vmstat 的 si/so 持续非零 = swap 抖动进行中。sar -B 是回收活动的显微镜:pgmajfault/s(主缺页速率,>每秒几十即警报)、pgscand/s(direct reclaim 扫描,非零 = 应用线程在做回收)、pgsteal/s(回收页速率)——三者的组合直接区分"健康的缓存回收"与"病态的内存压力"。PSI memory 的 some avg10 >10% 说明显著任务在等内存,full 非零说明全面停滞(接近 OOM 状态)。smem -rk 按 PSS 排序(-r 反序 -k 人类可读,-P 按名字过滤),比 top 的 RES 列更真实——共享库只按份摊。这组命令 30 秒跑完,覆盖内存的利用率(账本)、饱和度(回收/swap/PSI)、错误(OOM 看 dmesg)三维度。
cat /proc/$(pgrep -f myapp)/smaps_rollup
cat /proc/$(pgrep -f myapp)/status | egrep "VmRSS|VmSwap|Threads"
pmap -x $(pgrep -f myapp) | sort -k3 -n -r | head -20
/* 按 RSS 排序的内存段地图 */
cat /proc/$(pgrep -f myapp)/numa_maps | head -20
vmtouch -v /path/to/datafile
/* 文件在页缓存中的驻留比例 */
进程级内存解剖工具链。smaps_rollup 是单进程内存的权威汇总:Rss、Pss、Anonymous(匿名页——堆与栈)、AnonHugePages(THP 使用量)、Swap(被换出的量——非零说明该进程的冷页在 swap)、PageTables(该进程页表开销)。status 的 VmRSS/VmSwap 适合脚本化趋势采集,Threads 数 × 栈大小是线程栈内存的估算(200 线程 × 8MB 默认栈 = 最多 1.6GB 虚拟,实际 RSS 看触碰量)。pmap -x 列出每个内存段(地址、大小、RSS、Dirty、映射文件)——按 RSS 排序后,你能看到内存的物理构成:heap 段(malloc 堆)、匿名段(mmap 分配、JVM 堆、线程栈)、文件映射段(共享库、mmap 文件);64MB 的整齐匿名段是 glibc arena 的指纹(每 arena 默认 64MB 地址空间),数十个 64MB 段 = arena 过多的证据。numa_maps 逐段显示 N0=/N1= 页分布与绑定策略(default/bind/interleave),大段内存全在 N1 而进程跑在 N0 的 CPU 上 = 跨节点访问税。vmtouch 检查特定文件的缓存驻留率(数据文件预热验证、"为什么重启后慢"的缓存冷启动确认),还能 -t 触摸预热、-e 驱逐。
/usr/share/bcc/tools/memleak -p $(pgrep -f myapp) -a 10 6
/* 每 10 秒输出未释放分配的栈聚合,共 6 轮,-a 按大小 */
/usr/share/bcc/tools/oomkill
/* 监听 OOM 事件,输出被杀进程与上下文 */
dmesg -T | grep -A 30 -i "out of memory"
slabtop -o | head -20
cat /proc/slabinfo | sort -k3 -n -r | head -15
/* 按活跃对象数排序的 slab cache */
泄漏与内核内存的诊断组合。memleak 的 -a 输出按分配大小聚合的未释放栈:连续多轮采样中同一栈的量单调增长 = 真泄漏(一次性增长是预热/缓存);对多线程程序注意 uprobe 挂的是 malloc/free 全量,高频分配下开销 5-15%,限时使用。oomkill 工具实时捕获 OOM 事件(挂 oom_kill_process 内核函数),输出被杀 PID、进程名、页数——比翻 dmesg 及时,适合常驻。dmesg 的 OOM 报告解读要点:头部是触发上下文(invoked oom-killer 的进程与 order——order 0 是单页分配失败即杀,说明真枯竭;order 3+ 失败可能只是碎片)、中间是内存全景(anon/file/slab/free 各多少)、尾部是候选进程表(total-vm、rss、score)——通读报告能还原"内存去哪了"。slabtop -o(一次性输出不刷新)按 cache 排序:dentry/inode_cache 巨大通常是文件扫描的缓存副作用(可回收,非泄漏);kmalloc-64 之类通用 cache 异常增长是内核/驱动泄漏嫌疑(进一步用 page_owner 或 kmemleak);特定子系统 cache(如 TCP 的 tcp_sock、xfrm)增长指向对应资源泄漏(socket 未关闭)。/proc/slabinfo 第 2-3 列是活跃对象数与对象总数,脚本化采集做趋势。
numactl --hardware
numastat
numastat -p $(pgrep -f mysqld)
numactl --cpunodebind=0 --membind=0 ./myapp
/* NUMA 绑定启动 */
numactl --interleave=all redis-server /etc/redis.conf
/* 内存跨节点打散(Redis 官方建议之一) */
perf stat -e node-loads,node-load-misses,node-stores,node-store-misses -p PID sleep 10
NUMA 诊断与控制的完整命令集。numactl --hardware 输出节点拓扑与距离矩阵(node 0 到 node 1 距离 21 = 跨 UPI,约 2 倍延迟)。numastat 全局计数:numa_hit(本地分配成功)、numa_miss(想本地但落到远端——本地内存不足或策略干预)、numa_foreign(本节点内存被外节点借走)、interleave_hit——miss 与 foreign 持续增长说明存在跨节点分配压力。numastat -p PID 给进程的每节点内存分布(Heap、Stack、Private 各在哪个节点多少 MB),数据库进程应重点看大堆段的分布。绑定启动(--cpunodebind + --membind 成对使用——只绑内存不绑 CPU 会因调度迁移失效)适合单节点放得下的服务;--interleave=all 把分配轮转打散到所有节点,适合共享大内存且访问模式均匀的服务(Redis、某些缓存),代价是本地命中率下降但消除单节点内存耗尽风险。perf 的 node-load-misses 事件直接计数远端内存访问(PMU 支持时),miss/loads 比例是"NUMA 税"的直接度量。K8s 环境:Topology Manager 的 single-numa-node 策略 + CPU Manager static 实现 Pod 级对齐(需要独占核的 Guaranteed QoS)。
sysctl vm.swappiness vm.dirty_ratio vm.dirty_background_ratio vm.min_free_kbytes vm.overcommit_memory vm.zone_reclaim_mode
cat /sys/kernel/mm/transparent_hugepage/enabled
cat /sys/kernel/mm/transparent_hugepage/defrag
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
sysctl vm.dirty_bytes=2147483648 vm.dirty_background_bytes=536870912
/* 大内存机器:脏页阈值改绝对字节 */
grep -E "pgmajfault|pgscand" /proc/vmstat
内存内核参数的审查与调优清单(静态分析部分)。swappiness:默认 60,数据库/延迟敏感服务常设 1-10(保守换出,优先回收文件缓存);设 0 在新内核仍有极保守换出而非禁用。dirty_ratio/dirty_background_ratio:默认 20%/10% 按内存比例——512GB 机器意味着 100GB 脏页才同步阻塞写者,刷盘风暴时 I/O 延迟秒级;改用 dirty_bytes/dirty_background_bytes 绝对值(如 2GB/512MB,按磁盘刷盘能力设定:阈值 ≈ 磁盘写带宽 × 可容忍的刷盘时长)。min_free_kbytes:空闲水位基数,默认按内存开方计算,大内存机器常偏小导致回收启动太晚触发 direct reclaim——调大(如 1-4GB)给 kswapd 留缓冲,代价是可用内存略减。overcommit_memory:0 启发式(默认)、1 总是允许(Redis 官方建议,避免 bgsave 时 fork 失败)、2 严格模式(commit limit = swap + ratio×RAM,金融场景)。zone_reclaim_mode:默认 0(直接远端分配),设 1 会先本地激进回收——多数场景 0 正确,除非跨节点带宽极贵。THP:enabled 建议 madvise(应用显式 opt-in,如 JVM/PostgreSQL 支持),defrag 建议 defer+madvise(避免分配路径同步 compaction 毛刺);always + 延迟敏感 = 经典事故组合。所有变更遵循第 12 章纪律:单项修改、基准验证、记录回滚。

实验笔记

实验七:内存机制全景观测——从缺页到 NUMA 的动手实验。

实验目标:(1)实测 minor/major fault 的成本与触发条件;(2)观察页缓存、脏页回写与 direct reclaim 的完整行为链;(3)复现并诊断 swap 抖动、COW 风暴、slab 增长三个经典内存问题;(4)(多 NUMA 环境可选)实测跨节点访问代价。

环境准备:Linux 虚拟机 4GB 内存(故意配小以便制造压力),安装 sysstat、stress-ng、smem、vmtouch、BCC tools、numactl。确认 swap 已配置(swapon --show,没有则创建 2GB swapfile)。物理机或多 NUMA 虚拟机可加做步骤五。

步骤一(缺页成本实测):编写 C 程序 fault_bench.c:malloc 1GB 后(1)逐页首次写入并计时(触发 minor fault,计算每次 fault 的平均 μs);(2)再遍历一次计时(无 fault,对比缓存后的速度);(3)madvise(MADV_DONTNEED) 释放物理页后重测首次写入。预期:首次触碰每页约 0.5-2μs(minor fault 成本),二次访问每页 ns 级——三个数量级的差距就是"延迟分配"的代价。用 perf stat -e minor-faults,major-faults 验证计数。进阶:用 perf record -e page-faults -g 生成缺页火焰图,看 fault 来自哪些代码路径。

步骤二(页缓存与回写观察):先 echo 3 > /proc/sys/vm/drop_caches 清空缓存(实验环境专用操作)。用 dd if=/dev/zero of=/tmp/testfile bs=1M count=2048 oflag=sync 写 2GB(sync 绕过缓存,记录耗时与带宽——这是磁盘裸速度);再不带 oflag 重写(进页缓存,速度应为内存级),期间另一终端 watch -n 0.5 "grep -E 'Dirty|Writeback' /proc/meminfo" 观察脏页堆积与回写;然后 dd if=/tmp/testfile of=/dev/null bs=1M 读回(第二次读应远快于第一次——缓存命中),用 vmtouch /tmp/testfile 确认驻留率 100%。最后 sar -B 1 观察全程的 pgfault 与回收活动。这个实验把"写缓存、脏页、回写、读缓存"的完整生命周期可视化。

步骤三(direct reclaim 与 swap 抖动复现):记录基线后,运行 stress-ng --vm 1 --vm-bytes 3500M --vm-keep --timeout 120(超过可用内存)。观察链:free 的 available 归零 → vmstat 的 si/so 开始跳动(swap 抖动)→ sar -B 的 pgscand/s 非零(direct reclaim)→ PSI memory 的 some/full 飙升 → 系统整体"发黏"(另开终端跑 ping 或 date 循环感受卡顿)。若配置了 oomkill 工具或 dmesg -w,可能观察到 OOM 杀 stress-ng 子进程。结束后写报告:按时间线排列各指标的变化顺序,标注"压力传导链":分配 → 缓存回收 → swap 换出 → 换入抖动 → direct reclaim → PSI → OOM。

步骤四(COW 风暴复现):编写 Python 或 C 程序:父进程分配并初始化 512MB 数组,fork 子进程后子进程 sleep 30,父进程持续随机写数组元素。用 perf stat -e page-faults -p 父进程PID 或 sar -B 观察 fork 后的 major/minor fault 激增(每次父进程写新页都触发 COW 复制)。对比实验:父进程 fork 后不写(fault 极少)。这就是 Redis bgsave 期间写流量大时延迟尖刺的机制复现。思考:为什么 Redis 建议关闭 THP?(提示:COW 时 2MB 大页的拷贝成本是 4KB 页的 512 倍。)

步骤五(slab 与 NUMA,可选):slab 实验:记录 slabtop 基线,运行 find /usr -type f > /dev/null 两遍,观察 dentry 与 inode_cache 对象数暴涨(文件扫描的缓存副作用),sysctl vm.vfs_cache_pressure=200 后重跑对比回收速度。NUMA 实验(多节点环境):编写流式内存读带宽程序,numactl --membind=0 --cpunodebind=0 与 numactl --membind=1 --cpunodebind=0 分别运行,对比带宽与延迟(远端应低 30-50%);numastat 观察 numa_miss 变化。

预期结果:一份内存行为实验报告,含缺页成本实测值、脏页回写时间线、swap 抖动的指标传导链图、COW fault 对比数据、(可选)NUMA 代价矩阵。

进阶挑战:(1)把 vm.swappiness 设为 0 与 100 重做步骤三,对比换出倾向的差异;(2)配置 1GB zram swap 重做步骤三,对比磁盘 swap 与压缩内存 swap 的抖动烈度(si/so 速率与应用响应);(3)用 memory.high(cgroup v2)给步骤三的负载设软限制,观察 throttle 回收行为与 PSI 的关系;(4)给一个真实应用(如 Redis)做 24 小时内存画像:PSS、smaps_rollup 各项、memcg 计数的时序采集,识别其内存构成与增长模式。

延伸阅读

  • 《Systems Performance》第 2 版第 7 章 Memory 原文。原书覆盖虚拟内存硬件(TLB、MMU)、内核内存管理(伙伴系统、slab、回收、swap)、分配器与观测工具,Solaris 与 Linux 对照。特别推荐其中"内存观测的口径"(RSS/PSS 之辨)与 direct reclaim 延迟分析的段落,以及章末的调优参数清单。
  • 《深入理解计算机系统》(CSAPP)第 9 章虚拟内存。虚拟内存机制的最佳教学文本:地址翻译的完整推导、TLB 与多级页表、Linux 的缺页处理、mmap 与动态内存分配(malloc 的实现层次)、以及"垃圾收集"的概念引入。9.8-9.9 节(内存映射与 malloc)与本章用户态分配器部分直接衔接,9.6 节的翻译过程图解是理解 TLB 实验的基础。
  • 《Understanding the Linux Virtual Memory Manager》(Mel Gorman,免费 PDF/在线)。Linux 内存管理子系统的深度专著:物理页分配、slab、高层 VM(VMA、缺页路径)、页回收与 swap、大页与 NUMA 的完整实现细节。比任何博客都系统,是读懂 /proc/vmstat 每个计数器语义的终极参考。年代基于 2.6 内核,核心机制依然成立,新特性(THP、MGLRU)需补 LWN 文章。
  • LWN.net 内存管理专题:MGLRU(Multi-Gen LRU,6.1 合入的新一代回收算法)系列、PSI 的设计文档、memory.high 与 cgroup v2 内存控制器文档、THP 的历年争论文章。Linux 内存子系统仍在快速演进(folio 重构、DAMON 访问监控、内存分层),LWN 是跟踪语义变化的第一渠道,调优前确认内核版本的实际行为。
  • Brendan Gregg 的内存分析博文与图表:《Linux Memory Analysis》、《The Missing Linux Observability Tool: Swap Ins》、以及内存火焰图(memleak --stacks)的使用案例。他的网站还有 Solaris 时代的经典《Memory Performance Analysis》方法论文章。配合本章实验判读指标时,Gregg 的图表能帮你建立"正常形态"的参照系。
  • Redis 官方文档的 Latency 章节(redis.io/docs/management/optimization/latency/)。生产级内存问题模式库的最佳案例文档:fork/COW 尖刺、THP 的危害、swap 的影响、透明大页与延迟的关系,每条都附诊断命令与处置建议。虽然以 Redis 为名,其中的机制分析适用于一切大内存单线程服务(Node.js、内存数据库、缓存代理)。
  • 《What Every Programmer Should Know About Memory》(Ulrich Drepper, LWN 2007 连载)。内存硬件与软件交互的百科全书:DRAM 物理、缓存层次、TLB、NUMA 的硬件细节,以及程序员侧的优化技术(数据布局、预取、大页)。第 3 章虚拟内存与第 6 章性能部分是本章硬件知识的深化。年代虽早,硬件原理部分依然有效,是"机械同情心"书单的核心。
  • jemalloc 与 tcmalloc 的设计文档:jemalloc 的 Jason Evans 演讲《A malloc Implementation for Multiple Threaded Caches》与 wiki(github.com/jemalloc/jemalloc,含 tuning 与 profiling 指南);tcmalloc 的官方文档(google.github.io/tcmalloc,含 per-CPU cache 与 heap profiling 用法)。分配器选型与调优(decay 时间、arena 数、后台线程)的一手资料,大内存 C/C++ 服务必读。
7/13内存
0%