ARTICLE DETAIL

资讯详情

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

Linux进程内存管理:核心链路与线上排查实战

Linux进程内存管理:核心链路与线上排查实战 每次看到线上进程内存飙起来报警一条接一条后台群里开始刷屏我都知道接下来免不了又是一轮焦头烂额的排查。说实话Linux 的进程内存管理概念层面并不难难的是它层层嵌套虚拟地址空间、页表、缺页中断、堆和 mmap、写时复制、页面回收、swap、OOM Killer每一层单独看都能讲明白拼在一起就让人犯晕。我早年排查过好几次内存问题被 top 里的 RES 和 /proc 下的各种数字误导过很多回后来把那套机制彻底理清楚再回来看这些现象思路才真正打开。这篇文章算是我个人的整理和翻译不搞那种教科书式的逐章节铺开只讲我在实际排查和优化中真正用得上、绕不开的核心链路顺便把那些常规文档里不会写的经验和坑也交代清楚。适合刚接触 Linux 服务端开发的朋友也适合被莫名 OOM、RSS 只涨不跌折磨过的运维和后台同学。1. 进程内存管理到底在管理什么1.1 一个进程“眼中”的内存和物理内存根本不是一回事每个跑起来的进程都以为自己是这台机器唯一的主人。它写一个指针、访问一段看似连续的内存全程丝滑完全不需要关心隔壁进程在干什么。这种“错觉”来自虚拟地址空间也是理解 Linux 内存管理的第一块基石。64 位 x86-64 平台上Linux 给每个进程画了一张巨大的虚拟地图用户空间最大可以到 128TB内核空间还有另外 128TB。这个数字远大于物理内存所以进程在申请地址空间时压根不需要关心机器上到底插了多少内存条。你声明一个 1GB 的数组在进程眼里就是拿到了 1GB 的地址范围至于这些地址背后有没有真实物理页支撑操作系统一点都不着急。用生活里的话说地址空间是“购房合同”物理内存是“真金白银”而把合同和现金对应起来的工作全部由内核完成进程自己甚至感知不到这个过程。这里有个很容易被忽略的点虚拟地址空间的大小和实际内存使用之间没有必然关系。一个进程 VSZ 显示几十 GB可能它真实占用的物理内存只有几百 MB。反过来一个进程 RSS 不高也不代表它不危险因为它可能随时通过访问那些“只签合同没付款”的页面把系统内存瞬间打满。搞懂这层关系后面看 RSS 和 OOM 才会有正确的直觉。1.2 进程的地址空间里到底装了什么东西一张标准的进程虚拟地址空间地图从低地址到高地址大约是这样排列的代码段存放编译后的机器指令权限通常是 r-x只读可执行。数据段已初始化的全局变量和静态变量。BSS 段未初始化或零初始化的全局变量。很关键的一点是BSS 段在 ELF 文件里并不占实际空间它只是一个“将来要清零”的声明真正分配物理页是进程运行起来之后的事。堆从低地址向上生长malloc 的小块内存主要在这里活动。内存映射区共享库、mmap 文件、匿名映射都在这片区域通常从高地址向下扩展。栈高地址往下生长存函数调用栈、局部变量。栈和堆相向生长中间留出一大片未映射区域这一方面是安全隔离防止栈溢出直接冲进堆里另一方面也给两边扩展留了缓冲。如果你打开 /proc/PID/maps上面说的每个段都会以一种“地址区间-权限-映射文件”的格式列出来。我自己看多了之后基本瞟一眼首行列就能猜出这个进程是什么类型几十行 maps 的基本是普通 C 程序几百行且充满 .so 的多是 Python、Java 这类带运行时和大量依赖的程序。理解这些区域的语义是后续一切排查工作的基础。2. 从虚拟地址到物理页内核怎么完成翻译2.1 页表与 TLB一次内存访问的完整旅程进程访问一个虚拟地址时CPU 需要把它翻译成物理地址这个翻译动作依赖页表。页表不是一张大平表而是一棵存放在内存里的多级树。x86-64 传统的 4 级页表下一次翻译要依次遍历 PGD、PUD、PMD、PTE 四层最后一层拿到物理页号再拼上页内偏移才算得到真正的物理地址。听到四层遍历你的第一反应可能是“这得多慢”。确实如果每次都去内存里翻这四张表性能早就崩了。CPU 硬件为此准备了一个专门缓存地址翻译结果的组件叫 TLB。绝大多数地址翻译在 TLB 里命中后一条指令就能拿到物理地址完全不额外访问内存。但 TLB 的容量很有限所以页表层级越多、页数量越庞大TLB 失效的概率就越高。这里就引出了大页HugePages的用武之地。默认内存页是 4KB4GB 内存就会有上百万个页如果用 2MB 的大页页数量直接缩小 512 倍TLB 覆盖能力暴涨。数据库、JVM 这类内存大户开启大页之后性能提升可能很明显。但要注意大页需要预先从系统内存里划分配置大于实际需求就会造成另一种浪费这个坑日常环境中也经常见到。2.2 缺页中断极端懒加载的体现进程刚启动时它的虚拟空间很大但物理页几乎都没有建立。只有当代码真正执行到某个地址区间时CPU 才发现“这个虚拟页还没有对应的物理页”于是触发缺页异常进入内核由内核完成物理页分配、数据读取、页表建立再返回到用户态继续执行。这种机制叫按需分页本质是极致的懒加载没执行到的代码不加载没碰过的数据不分配。所以一个大型程序尽管二进制文件有几个 GB刚启动时 RSS 往往远低于文件体积因为只有真正被访问的页才会被搬进物理内存。缺页中断分成两种常见类型主缺页major fault意味着数据确实要从磁盘读入代价是毫秒级 I/O次缺页minor fault则只是页表没建好物理页早已存在比如共享库首次被映射进进程时。冷启动大型服务时系统 I/O 飙高多半就是大量主缺页造成的。排查启动慢的问题时我习惯直接看 /proc/PID/stat 里的 maj_flt 和 min_flt这两个字段分别记录主缺页和次缺页的次数。如果 maj_flt 在几十万以上基本就是启动阶段发生了海量磁盘读优化方向也该往“减少冷启动加载量”上面引。3. 内存分配的两条路堆与 mmap3.1 堆malloc 背后的“包工头”传统的小内存分配走堆。第一次 malloc 时glibc 会通过 brk 系统调用把堆的结尾向上推然后自己把这块连续区域切成小块管理起来。为什么不让程序每次分配都直接调 brk因为系统调用成本不低而且频繁在堆尾切来切去会把内存切得支离破碎碎片化会让后续大块分配失败。glibc 内部为此实现了一套精细的缓存体系。小对象从 tcache、fastbin 这类高速缓存里拿release 之后的内存也先回到这些缓存里整个过程基本不碰内核速度极快。这也是为什么 malloc 和 free 在大多数场景下都很快的原因之一它并不是每次分配都找内核要内存。这里有个经典误区堆里 free 掉的内存并不等于还给操作系统。glibc 只是把这块区域的“占用标记”去掉虚拟地址空间还在进程手里物理页大多也还挂在页表上。所以你看到进程 RES 一直不降可能并不是泄漏只是因为 glibc 把释放的内存攥在手里备用。真正判断泄漏需要结合多次采样看趋势而不是看到某一次 free 后就指望 RSS 立刻下跌。3.2 mmap不只是映射文件大内存分配的另一个出口大块内存分配的另一个通道是 mmap。glibc 的 malloc 对超过一定阈值的请求会改用匿名映射初始阈值大约是 128KB内核会根据进程行为动态调整。匿名映射跟堆最大的不同是munmap 之后地址空间立刻释放物理页也随即归还系统所以大块内存释放后RSS 是真的能降下去。这样既避免了堆的碎片化也让“大内存一次性分配释放”这种场景可以干净利落。用 pmap 查看进程时会看到大量anon映射很多就是这么来的。排查内存问题时我喜欢先看增长的到底是堆还是匿名映射思路会立刻清晰很多。如果是堆增长重点看 glibc 的分配缓存和业务侧的长期对象如果是匿名映射增长则大概率是大块 buffer 或线程栈这类东西出了问题。顺带一提mmap 也能用于文件映射map 一个文件后读写文件就跟读写普通内存一样内核负责把脏页写回磁盘。很多高性能应用读配置文件、加载索引都用这个方式省去了用户态到内核态的拷贝开销。3.3 写时复制fork 的免费午餐与隐藏成本fork 创建一个子进程时传统想法是把父进程的内存完整复制一份。Linux 的做法完全不同父子进程共享同一批物理页同时把所有这些页标记为只读。一旦有人尝试写入就触发一次保护性缺页内核才在那一刻复制这个页。这就是写时复制也就是 COW。绝大多数 fork 完马上 exec 的场景根本不需要复制任何内存开销小到令人发指。但 COW 也有反噬的时候。Redis 做 RDB 持久化或 AOF 重写时会 fork 子进程父进程继续服务请求持续修改自己的内存页就会不断触发 COW 复制。结果就是明明只是 fork 了一下整个进程的内存占用可能短时间暴涨一倍以上。我有一次压测 Redis4GB 的实例 fork 之后内存一度逼近 7GB如果不懂 COW很可能误判成内存泄漏。关于这个案例后面排查实战部分我还会详细讲。4. 进程内存怎么“看”工具、数字和 /proc 档案4.1 VSZ、RSS、PSS三个让人犯晕的数字先说结论查单个进程到底“该为它付出多少内存”PSS 最接近真实RSS 适合初筛但会把共享内存重复计算VSZ 在大多数排查场景里价值有限它只是纸面地址空间大小。VSZ 是整个虚拟地址空间的范围单位是 KB等于进程持有地址空间的总量。RSS 是常驻物理内存大小也就是进程正占着的物理页总和但你如果同时跑 10 个进程共享同一个 glibc 库这个库的物理页会被每个进程的 RSS 各算一次重复计算很严重。PSS 解决了这个问题它把共享页按共享进程数分摊比如一个被 10 个进程用的库页算单个进程时只计 1/10。看单进程内存消耗PSS 可以说最接近“真实账单”。平时我最常用的快速排序命令是ps -eo pid,comm,vsz,rss,pmem --sort-rss | head -n 20拿到可疑进程后再进 /proc 看细账。注意一点top 里的 RES 列和 ps 里的 RSS 本质是同一个量但不同版本工具统计口径略有差异跨工具对比时不要钻牛角尖。4.2 /proc/PID/ 目录下的侦察工具Linux 把进程的几乎所有运行时状态都放在 /proc/PID/ 下面这是排查内存问题的第一现场。maps展示每一段虚拟地址区间的起止、权限、映射对象是看清地址空间布局的底图。statm用页为单位给出总大小、RSS、共享页等数字脚本里用起来很方便。status以人类可读的字段展示 VmRSS、VmSize、VmSwap、VmPTE 等重点看 VmSwap这是进程换出到 swap 的量。smaps把每一段映射的细节全部展开包括 Rss、Pss、Shared_Clean、Shared_Dirty、Private_Dirty、Swap 等字段极其丰富但大进程直接 cat 会输出非常长。smaps_rollup这是 smaps 的汇总版本内核 4.14 之后才有一条命令就能拿到整个进程的内存汇总就是为排查场景准备的。我定位进程内存增长时会先执行cat /proc/PID/smaps_rollup拿到 Rss、Pss 的总量再对照 maps 找到对应区间。比如发现某个匿名映射段增长剧烈就用 pmap 进一步确认pmap -x PID | sort -k3 -nr | head -n 30按 RSS 排序后哪些段占大头一目了然。4.3 系统级内存读数free 和 /proc/meminfo进程看完了还要回到系统层面看整体水位。很多人一执行 free -h 看到 free 列很小就开始慌其实是对 Linux 内存策略的误解。Linux 默认会尽量把空闲内存拿去做 page cache用来加速文件读写这部分内存在需要时可以立刻回收所以真正值得关注的是 available 这一列它表示“在不触发明显性能问题时可供分配的内存”。/proc/meminfo 是系统内存的完整账本重点看这几个字段MemTotal、MemAvailable、Buffers、Cached、SwapTotal、SwapFree。其中 Cached 包含了 page cache 和 tmpfs 的内存Buffers 多是块设备缓存。做容量规划时我更习惯用 MemAvailable 作为真实水位而不是 free 那一列。有一个常见误区是看到 page cache 占用很高就想去清掉。其实 cache 高恰恰说明系统内存效率高不该干预。除非你遇到“内存够用但分配失败”的特殊场景否则 echo 3 /proc/sys/vm/drop_caches 这种操作在生产上少做它不但没解决根本问题还会让之后的读写性能短暂下降。5. 内存真的不够时回收、swap 与 OOM Killer5.1 内存回收的两条路径kswapd 和 direct reclaim当系统内存开始紧张内核必须把一些页收回来重新分配。回收主要有两个触发路径kswapd 内核线程在后台周期性地做尽量平滑如果内存消耗太快后台线程来不及进程在分配内存时就会被卡住由内核直接执行回收这就是 direct reclaim也是很多性能毛刺的来源。回收对象主要分两大类。一类是 page cache干净页可以直接丢弃脏页要先写回磁盘再丢弃代价最小。另一类是匿名页也就是进程实际使用的物理页这种不能直接丢只能换出到 swap。内核按照 LRU 的思路维护多条链表优先回收那些“很久没被访问”的页。这个设计很符合直觉冷数据先走热数据留下。线上环境里更常见的表现是系统明明有大量 cache但某个进程申请大内存时还是变慢了。就是因为 cache 回收也要时间而且脏页写回速度受磁盘限制。这种情况不能简单归咎于“内存不足”很多时候是“回收不过来”。5.2 swap 到底要不要开怎么开才不坑关于 swap我的态度一直很明确不要完全关闭哪怕你有大内存。swap 的定位不是“用不完的备用内存”而是给系统一个缓冲垫挡住那些短期的内存毛刺也给 OOM Killer 争取更多反应时间。Linux 默认的 vm.swappiness 值是 60表示内核在回收匿名页和 page cache 之间的倾向程度。值越高越倾向用 swap值越低越倾向回收 page cache。如果你的机器磁盘是机械盘换入换出代价很大建议调低到 10 左右如果是 SSD 或 NVMe保持默认或调到 20 都问题不大。注意 swappiness 不是“超过这个百分比才用 swap”它只是一个倾向权重别理解偏了。swap 的形态可以是磁盘分区也可以是一个文件。用文件做 swap 的好处是调整方便我甚至见过有人临时创建一个 swap 文件救急的场景。创建步骤大概是这样dd if/dev/zero of/swapfile bs1M count8192 chmod 600 /swapfile mkswap /swapfile swapon /swapfile这个 8GB 的 swap 文件建完后再用 free -h 确认一下。需要说明的是这种方式对性能敏感的数据库业务不算最优数据库还是优先靠足够的内存和合理的 cache 策略swap 只是保命的底线。5.3 OOM Killer 的裁决逻辑与自救手段当内存彻底不够回收也解决不了问题时OOM Killer 就登场了。它会给每个进程打分分越高越容易被杀。打分主要看内存占用大小同时参考 oom_score_adj 这个可调参数。root 进程、直接访问大数据块的进程往往会拿高分但也有不少优化空间。给重要服务设置免杀很简单echo -800 /proc/PID/oom_score_adj设置为 -1000 表示完全免杀一般不建议所有进程都这么搞否则系统真的无路可退时只能选别的倒霉蛋。我自己的原则是核心数据库设到 -800 到 -1000 之间中间件设到 -500 左右普通业务进程不干预。跟 OOM 强相关的另一个参数是/proc/sys/vm/overcommit_memory。它控制内核是否允许虚拟内存超额分配。默认 0 时内核用启发式规则决定是否批准分配结果就是 malloc 可能成功申请到远超物理内存的地址空间等真正访问时才发现没有物理页可用紧接着就是 OOM。生产环境如果要更强的可预期性可以设成 2也就是禁止超额分配这样申请内存时就会严格按“物理内存 swap”来计算。但要注意改成 2 之后有些过度申请内存的应用可能启动就会失败需要结合自己的业务特征谨慎测试不是无脑照搬。6. 一次线上排查实战与避坑清单6.1 从 RSS 飙高到定位根因的完整流程我处理过好几次“进程内存突然暴涨”的线上问题基本套路已经固定下来分享给你第一步先用 free -h 看系统整体水位确认是不是真的物理内存紧张还是只是某个进程自己的问题。第二步ps 按 RSS 排序找嫌疑进程ps -eo pid,comm,rss,vsz --sort-rss | head -n 15第三步进入 /proc/PID/smaps_rollup 看整体快速确认 Pss 和 Rss 的数值。第四步用 pmap 按 RSS 排序看具体映射段pmap -x PID | sort -k3 -nr | head -n 30这一步基本能看出是堆、匿名映射还是文件映射在涨。比如看到一条anon映射占了几个 GB那基本确定是业务代码里大块分配导致如果是堆涨就配合 gdb 或者 valgrind 继续深挖。第五步确认增长跟时间的关系。我想看趋势时会隔段时间执行一次同一个进程的 smaps_rollup或者写个简单脚本每分钟记录一次 Pss。这个数据比单次快照有价值得多能帮你区分“瞬时峰值”还是“持续泄漏”。针对不同语言还可以更精细Java 服务用 jcmd PID VM.native_memory 查看堆外内存C/C 程序用 valgrind massif 或者 jemalloc 的 profiling 功能。这些工具各有侧重但前提是前四步已经帮你找到可疑方向别一开始就上重量级工具。6.2 我自己踩过的几个内存坑先说 Redis fork 那个案例。有一次压测Redis 实例 RSS 从 4GB 涨到接近 7GB第一反应是出 bug 了。后来看了子进程的 COW 状态又查了系统日志才发现是 RDB 持久化 fork 之后父进程的写流量触发大量写时复制。这个现象不是泄漏但如果你不懂 COW很容易被带偏方向甚至在业务流量已经很大的情况下贸然重启实例造成更严重的问题。第二个坑是 glibc 的 malloc arena。高并发的多线程程序glibc 会为每个 CPU 创建独立的分配区减少线程间锁竞争。线程很多时arena 数量可能高达 cpu 核数的 8 倍每个 arena 会预分配不少内存最终表现就是进程 RSS 远高于实际业务数据量。遇到这种情况可以设置环境变量 MALLOC_ARENA_MAX2 或者 4再对比 RSS。我之前调过一个服务从默认的 arena 数量改成 4RSS 直接降了近三分之一业务性能几乎没变化。注意这个调整最好在测试环境先压测因为分配竞争剧烈时arena 太少可能让性能下降。第三个坑是透明大页 THP。THP 是为了让普通进程自动享受大页红利但对后台延迟敏感的服务THP 的异步内存整理经常带来不可控的毛刺。我维护过的一些数据库服务最后都会建议关闭 THP 或至少设置成 madvise 模式让只有显式建议的内存在使用大页。操作方式echo never /sys/kernel/mm/transparent_hugepage/enabled很多安装脚本里也有这一步但它确实有效值得作为数据库类服务的基础调优项。6.3 一份写给自己的配置备忘以下是我在实践中整理出来、反复参考的配置清单贴出来给你当速查表参数建议值原因vm.swappinessSSD 设 10-20HDD 设 5-10降低匿名页被过早换出概率vm.overcommit_memory默认 0 或按需设 2生产求稳定时禁止超额分配oom_score_adj核心服务 -800 以上降低核心进程被误杀概率THPnever 或 madvise减少延迟毛刺MALLOC_ARENA_MAX2-4控制多线程下 glibc 内存膨胀需要反复强调的是以上任何一项改动都会牵连其他模块尤其是 overcommit 和 THP 的调整必须先在测试环境做压力验证再上生产。没有任何一套配置是一劳永逸的每台机器、每个业务的模型都不一样。排查内存问题这么多年我最大的感受是把虚拟地址、物理页、回收决策这三个层次在脑子里分开比记住任何工具和命令都更管用。遇到一个内存异常先问自己三个问题地址空间是不是异常扩张了物理页是不是单调增长不回头系统是不是在持续做回收这三个问题的答案基本就能定位八九成。从业者之间交流往往不靠堆术语靠的是这种能落地的分析框架希望这份整理对你有同样的启发。
返回列表