
1. 先把三笔账分清楚物理内存、虚拟地址空间、swap 各自在管什么上周帮朋友看一台跑着 Redis 加两个容器的 Ubuntu 服务器他的原话是内存莫名其妙就满了重启一下又好。我登上去敲了条free -havailable 还剩 5 个 G可他坚持说 top 里已经飙到 90%。这就是 Ubuntu 内存分配话题里最常见的第一道坎大多数人脑子里只有内存这一个概念而系统里有三笔完全独立的账。把这三笔账分清楚后面所有关于 ubuntu 内存分配的问题都会变得可推理而不是靠重启碰运气。物理内存是真实插在主板上的那几条条子虚拟地址空间是内核发给每个进程的一张地址地图swap 是内核在磁盘上圈出来的一块备用空间。这三者之间没有一一对应的关系一个进程虚拟地址空间占了 40G物理内存可能只碰了 200M反过来一个看起来很老实的进程也可能把物理内存吃得干干净净。理解了这一点你才不会看到VSZ特别大就喊内存泄漏。1.1 free 命令里那个 available 才是你应该盯的数先看一条我随手在 Ubuntu 22.04 上抓的输出$ free -h total used free shared buff/cache available Mem: 31Gi 8.6Gi 1.2Gi 214Mi 21Gi 21Gi Swap: 2.0Gi 0.0Gi 2.0Gi很多人第一眼看到 free 只剩 1.2Gi 就慌了这是典型的误读。Ubuntu 会把空闲的物理内存几乎全部拿去做页缓存buff/cache因为空着的内存本身就是浪费。真正代表新起一个程序还能拿到多少内存的是最后一列available它是内核综合了 MemFree、可回收的页缓存和可回收的 slab 之后给出的估算值。上表里 available 有 21Gi说明这台机器其实很宽松。顺带说一句shared这一列。在 Ubuntu 22.04 及更新版本自带的 procps 3.3.17 里shared 显示的是 tmpfs包括 /dev/shm占用的内存量而不是老版本里那个含义模糊的共享内存。这个改动很多人没注意到导致在排查容器共享内存问题时看错地方。如果你的机器上 shared 是几个 G 还在涨八成是有程序在往 /dev/shm 里写大文件后面第 5 节会专门讲这个坑。1.2 虚拟内存不是内存不够时从硬盘借这是被讲烂了但依然被讲错的一个概念。虚拟内存virtual memory的核心价值是地址映射与隔离它让每个进程都觉得自己独占了一整块连续的地址空间同时让内核可以把物理页随意摆放、共享、回收。它顺带带来的好处才是可以换出到 swap。x86-64 上的 Ubuntu单个进程的用户态虚拟地址空间上限大约是 128TiB这个数字大得离谱所以你在 64 位系统上几乎不可能因为地址空间不够而 malloc 失败。但 32 位进程的天花板是 4GiB默认按 3G/1G 切分给用户态和内核态实际可用往往只有 2GiB 到 3GiB。这就是为什么同样一份大工程在 32 位工具链上编译到一半会报该进程已终止因为它无法分配更多的内存换到 64 位的 Ubuntu 上就没事——问题不在物理内存在地址空间。这个区别我在第 5 节还会展开。再看一个常被忽略的角色页表本身也要占内存。进程的虚拟地址空间映射得越碎页表项就越多。用cat /proc/meminfo | grep PageTables能看到当前页表占用跑着大量进程或者开了透明大页的机器上这一项轻松到几百 MB甚至上 G。这笔开销在容器里尤其容易被低估因为容器里 fork 出去的小进程往往特别多。1.3 buff/cache 和 slab 的真实身份free里的 buff/cache 是两部分的合并Buffers 是块设备元数据缓存通常很小Cached 是页缓存也就是你读过的文件内容被留在内存里下次读就不用碰磁盘了。除此之外还有一块不在这个数字里、但同样吃物理内存的角色——slab。slab 是内核自己用的对象分配器dentry目录项缓存和 inode索引节点缓存是里面最大的两块它们是可回收的而 socket 缓冲区、task_struct 这些属于 SUnreclaim不可回收。用sudo slabtop -s c按缓存大小排序看一眼能立刻判断出内存是被内核缓存吃掉了还是被应用真的申请了。提示如果你发现free里的 used 很高但 available 也很高同时slabtop里 dentry 占了几百 MB这属于完全正常的文件系统缓存行为不需要处理。真正要盯的是 available 的绝对值以及 Cached 里属于 shmem/tmpfs 的部分因为它不像普通页缓存那样容易被回收掉。2. 一次 malloc 背后发生了什么从 VMA 到缺页中断搞清楚三笔账之后第二个必须建立的直觉是malloc 返回成功并不等于内核真的把物理页交到你手里了。这个延迟交付机制是理解 Ubuntu 内存分配行为的关键也是很多看起来内存没满却 OOM 了这类怪现象的总根源。2.1 申请的那一刻内核只给你一张欠条当你调用malloc(200 * 1024 * 1024)时内核做的事情大致是在进程的地址空间里找一段没被占用的虚拟地址区间创建一个 VMAvm_area_struct结构记录这段地址从哪到哪、属于哪种类型、什么权限然后返回起始地址。这时候物理内存消耗几乎为零只有 VMA 结构本身几十到几百字节和可能扩展的页表。真正的物理内存分配发生在第一次写入时。CPU 访问一个还没有对应物理页的虚拟地址触发缺页中断page fault内核这时才去伙伴系统申请一个物理页框通常是 4KiB填上零建立页表映射。这个过程叫按需求页demand paging。所以一个进程的VmSize虚拟大小和VmRSS实际驻留物理内存经常差一个数量级看top的 VIRT 列就下结论是不靠谱的。2.2 brk 与 mmapglibc 那道 128KiB 的分界线glibc 的 malloc 并不是每次都直接找内核。对小块内存它在进程的数据段末尾用brk/sbrk扩展一块连续的堆区域之后所有小块分配都在这个堆里做分割和复用只有超过阈值的分配才会走mmap单独映射一段匿名内存释放时直接munmap还给内核。这个阈值默认是 128KiB有个动态调整机制一旦某个 mmap 块被释放阈值会临时抬高到那个块的大小上限在 64 位上是 32MiB。这个设计是为了避免频繁大块 mmap/munmap 导致缺页开销过大。你可以用环境变量覆盖# 让超过 32KiB 的分配都走 mmap适合排查内存碎片的场景 export MALLOC_MMAP_THRESHOLD_32768 # 限制每个进程的 arena 数量多线程程序省内存的经典手段 export MALLOC_ARENA_MAX2第二行值得单独说。glibc 为了减少多线程锁竞争会为每个线程分配独立的 arena64 位系统上默认上限是8 × CPU 核数。在一台 64 核机器上跑一个开了几百个线程的 Java 或 Node 服务你可能会有几十个 arena每个都预留着各自的空闲块RSS 就是这么涨起来的。MALLOC_ARENA_MAX2这类设置在很多线上事故里救过场代价是锁竞争略微上升。这是我个人认为最值得记住的一条 glibc 内存调优经验。还有一点free()只是把内存还给分配器不一定会还给内核。堆顶的大块空闲内存要靠malloc_trim()或者达到M_TRIM_THRESHOLD默认 128KiB才会收缩。所以你在/proc/PID/status里看到 VmRSS 释放后不下降未必是泄漏可能只是分配器留着复用。2.3 Overcommit 的三个档位以及 Redis fork 失败的真实原因这是 Ubuntu 内存分配里最有戏剧性的一环。内核有三个 overcommit 策略由vm.overcommit_memory控制默认值是 0取值策略名行为0启发式默认按空闲内存 可回收页缓存 可回收 slab - 保留页来判断明显过分的申请会被拒绝1总是允许从不拒绝任何申请风险全部推给 OOM Killer2从不超配严格按 CommitLimit 记账公式是 swap 物理内存 × overcommit_ratio / 100ratio 默认 50绝大多数人第一次注意到这个参数是因为 Redis 启动时打出的那条警告WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add vm.overcommit_memory 1 to /etc/sysctl.conf ...为什么会这样Redis 执行 BGSAVE 时要fork()出一个子进程靠写时复制COW共享父进程的内存。但 fork 的那一刻内核是按父进程整个虚拟地址空间来做 commit 记账的因为在策略 0 下它没法预知未来哪些页会被写。如果父进程 VSZ 有 20G而机器上空闲 可回收只有 10G这次 fork 就会被直接拒绝报fork: Cannot allocate memory。数据没丢但持久化失败了。设成 1 之后 fork 必然成功代价是把判断责任交给了 OOM Killer——真到内存紧张时内核会挑一个进程干掉。所以只在设了 1 却不做内存上限约束的机器上是很危险的某个进程的峰值很可能把整台机器上别的服务一起拖下水。正确的做法是设 1 的同时用 systemd 或 cgroup 给每个服务圈定内存上限第 4 节会给出具体配置让超限时死的是它自己。策略 2 一般只在跑 Oracle、SAP 这类有严格内存规划的数据库时才用且必须自己把overcommit_ratio算准。用grep -E CommitLimit|Committed_AS /proc/meminfo可以实时看到当前记账情况Committed_AS 长期贴着 CommitLimit 就说明该扩容或者该调策略了。3. 把内存账算清楚Ubuntu 上真正好用的观测手段知道原理之后接下来就是手上要有趁手的工具。这一节我按看得出来 → 分得清 → 定位到人的顺序把 Ubuntu 上几个真正好用的观测方式梳理一遍很多都是我把top丢掉之后才开始用的。3.1 RSS、PSS、USS同一个进程为什么数字不一样这是最容易把人绕晕的地方。共享库和 COW 页会被多个进程同时映射那么这部分内存该算给谁内核给了三种口径指标含义适用场景RSS进程映射的全部物理页共享部分被重复计算看单个进程的物理占用趋势PSS私有页 共享页 / 共享者数量把所有进程的 PSS 相加约等于真实内存占用USS只有私有页完全属于本进程判断杀掉它能省下多少内存所以当你用ps或top把所有进程的 RSS 加起来得到 40G而free显示只用了 12G这不是系统算错了是共享库被反复计数了。要算真实的账用 PSScat /proc/*/smaps_rollup | grep -i pss | awk {s$2} END {print s/1024 MB}。这条命令在 Ubuntu 20.04 及以上的内核上都可用比遍历 smaps 快得多。3.2 从 smaps_rollup 到 ps_mem 的组合拳需要看一个进程的内存构成时/proc/PID/smaps_rollup是我最先打开的文件它把整个进程的映射做了汇总重点看三行$ cat /proc/1234/smaps_rollup Rss: 1048576 kB Pss: 524288 kB Private_Dirty: 400000 kB Swap: 12000 kBPrivate_Dirty是真正属于这个进程、而且被改过的页通常对应它自己申请的内存如果它很大并且在持续增长基本可以认定是应用层问题。Swap那一行能看到这个进程有多少页被换出去了长时间非零说明机器内存压力已经在影响它了。再配上两个第三方小工具效率会高很多。ps_mem会自动按 PSS 排序输出适合快速找出谁在吃内存pmap -x PID可以列出每个映射段的详细情况特别适合定位到底是哪个 so 或者哪块 mmap 撑大了进程。这两个都能直接sudo apt install ps_mem装。3.3 容器和 cgroup v2 下的内存统计Ubuntu 21.10 之后默认启用了统一的 cgroup v2Docker、containerd、systemd 全都在这一套体系里记账。你会在/sys/fs/cgroup/下面看到一堆文件最常用的几个cat /sys/fs/cgroup/system.slice/redis.service/memory.current # 当前用量 cat /sys/fs/cgroup/system.slice/redis.service/memory.peak # 历史峰值 cat /sys/fs/cgroup/system.slice/redis.service/memory.max # 硬上限 cat /sys/fs/cgroup/system.slice/redis.service/memory.stat # 分项明细 cat /sys/fs/cgroup/system.slice/redis.service/memory.events # 是否触发过 OOMmemory.events里的oom_kill计数非常关键——它告诉你这个 cgroup 到底有没有被杀过进程。很多人查 OOM 时只看dmesg但容器内的 cgroup 级 OOM 不一定会打印到宿主机的 dmesg 里必须去看这个文件。memory.stat里的anon匿名页对应应用申请的内存和file页缓存分开显示排查内存被页缓存撑爆还是应用真的吃完了一眼就能分辨。还有一个细节cgroup 的内存统计包含了这个组里所有进程的页缓存。当一个容器反复读写文件时缓存会记在容器头上memory.current涨上去但实际可用内存还很多。这种情况在 cgroup v2 里会有自动的回写和回收不需要你手工drop_caches。顺便说一句echo 3 /proc/sys/vm/drop_caches这个操作我十分不建议在生产上随手敲它只丢干净页缓存丢完立刻被读回来性能反而更差而且对匿名页完全没用——人们想解决的used 太高的问题它根本解决不了。4. 进程被杀的完整排查链路从 dmesg 到 oom_score_adj好端端的服务突然没了日志里什么都没有这是 Ubuntu 内存分配话题下最常见的一类求助。十有八九是 OOM Killer 干的而且很多人都不知道日志在哪。这一节我把排查链路完整走一遍包括每一步为什么这么做。4.1 OOM Killer 到底按什么打分挑人内核要杀人的时候会给每个进程算一个 badness 分数最终落到/proc/PID/oom_score上范围 0 到 1000分越高越先死。这个分数在较新的内核上主要来自三块进程的 RSS、它用掉的 swap、以及它占的页表内存。也就是说谁实际占的物理内存多谁就更危险虚拟大小不参与打分这点非常符合直觉。在此之上还有一个人工可调的偏置/proc/PID/oom_score_adj范围 -1000 到 1000。写成 -1000 表示永远不选我写成 1000 表示优先杀我。把这个值和刚才的 badness 按比例合成就是最终分数。另外持有较高特权能力的进程会被内核稍微降低一点分数这是历史遗留的保护机制。你可以在动手之前先用一条命令看看当前谁最危险for p in /proc/[0-9]*; do printf %s %s\n $(cat $p/oom_score 2/dev/null) $(cat $p/comm 2/dev/null) done | sort -rn | head -104.2 给关键服务加保护的三种方式知道了打分规则保护手段也就清楚了。按我的经验优先级从高到低是先用 cgroup 给上限再用 oom_score_adj 调偏置最后才是设成不可杀。第一种也是最推荐的一种是用 systemd 直接给服务圈内存。Ubuntu 上几乎所有常驻服务都是 systemd 管的写个 drop-in 就行# /etc/systemd/system/redis.service.d/memory.conf [Service] MemoryAccountingyes MemoryHigh1500M MemoryMax2G MemorySwapMax512M OOMPolicycontinue这几个参数的区别值得说清楚。MemoryMax是硬顶一旦碰到cgroup 内部就会触发回收甚至 OOM死的是这个组里的进程不会波及别人。MemoryHigh是软顶超过之后内核会开始施压回收、拖慢这个组属于温和提醒我一般设成 Max 的 75% 左右。MemorySwapMax限制它能用多少 swap设成 0 就完全不允许换出对延迟敏感的服务很有用。OOMPolicycontinue表示组内进程被杀后 systemd 不重启整个单元默认的 stop 行为有时候反而会让故障扩散。第二种是调偏置。把最关键的那个进程比如监控 agent、sshd、数据库主进程设成 -900 左右让它在内存竞争中活下来echo -900 | sudo tee /proc/$(pgrep -o mysqld)/oom_score_adj第三种是设成 -1000也就是不可杀。我要提醒一句这招要慎用如果真的到了全局内存耗尽的地步内核发现所有候选都是不可杀行为会变得很难预测最坏的结果是整台机器卡死连 SSH 都进不去。我个人的原则是除了 SSH 和监控 agent其他都不设 -1000。4.3 一次真实的排查记录PyTorch 训练脚本把宿主机搞崩了说一个我自己遇到的案例过程挺典型。环境是 Ubuntu 22.04 加 NVMe跑一个 PyTorch 训练任务配置是 32G 内存加一张 24G 显存的卡。现象是训练跑十几分钟之后训练进程直接消失Python 那边没有任何 traceback只有Killed两个字。排查是这么一步步收窄的。第一步确认是被内核杀的。执行dmesg -T | grep -i -E oom|killed process果然看到Out of memory: Killed process 21345 (python3) total-vm:28xxxxxxkB, anon-rss:24xxxxxxkB。到这里就能确认是 OOM不是段错误也不是被容器编排杀掉的。第二步看是真不够还是被限制。free -h显示 available 只有 1G 不到说明物理内存是真不够了。如果是 available 很充裕还 OOM那就要怀疑是 ulimit 或者 cgroup 的硬顶在作怪排查方向完全不同。第三步定位到具体是谁在涨。用watch -n 2 grep -E VmRSS|VmSwap /proc/21345/status盯着看RSS 从 4G 一路涨到 24G。这时候要区分两种可能数据本身就该占这么多还是真的泄漏了。我又看了一眼cat /proc/21345/smaps_rollup | grep -E Rss|Pss|Private_DirtyPrivate_Dirty 几乎等于 RSS说明这些内存确实是这个进程私有的新分配不是共享库。第四步找到那块增长的内存。用pmap -x 21345 | sort -k3 -rn | head排序发现是一堆 256M 的匿名映射在增加数量正好等于 DataLoader 的 worker 数乘以预取批次数。到这里就清楚了num_workers16、prefetch_factor8、每批数据展平后约 200MB光是预取缓冲就吃掉了十几 G。同时 /dev/shm 也在被 worker 之间的张量共享吃掉一部分因为 tmpfs 是算在内存里的。最后是三处修改把num_workers降到 6、批大小减半、并且给训练服务加了个MemoryMax20G的 cgroup 上限让它在超限时自己被杀而不是拖垮整机。改完再跑峰值稳定在 16G 左右。这个案例里最值得记住的一点是GPU 训练任务的内存峰值往往不在模型上而在数据加载管道里因为它不起眼所以最容易失控。5. 无法分配更多内存背后的五种完全不同的根因编译报错、Redis 存盘失败、Java 起不来、容器里的进程莫名退出报的错可能都是同一句Cannot allocate memory但根因可能天差地别。这一节我按排查优先级把这几种情况拆开讲你可以当成一张对照表来用。现象最可能的根因第一个该查的东西32 位程序链接或编译中途失败用户态地址空间只有 3Gfile 程序名、ulimit -vfork 失败、进程数上不去策略 0 下的 commit 记账拒绝/proc/meminfo的 Committed_ASmmap 报 ENOMEM 但内存充足vm.max_map_count到顶cat /proc/sys/vm/max_map_count容器里写共享内存失败/dev/shm 太小df -h /dev/shmGPU、RDMA 程序报锁页失败RLIMIT_MEMLOCK太小ulimit -l5.1 地址空间不够为什么 32 位程序总是先撞墙先说最容易被忽略的一种。Ubuntu 上跑一个 32 位程序不管宿主机有多少内存它的用户态可用虚拟地址最多 3GiB操作系统、共享库、栈、堆全都要从这 3GiB 里分。一个链接了大量静态库的 32 位程序编译到链接阶段很容易就顶到天花板报的错正是该进程已终止因为它无法分配更多的内存。同样的工程换成 64 位工具链编译地址空间从 3GiB 变成 128TiB问题立刻消失。判断方法很直接file /path/to/binary看是不是ELF 32-bituname -m看系统架构。如果确实必须用 32 位可以考虑启用 Ubuntu 的 32 位大地址空间内核参数但这条路坑很多除非有强约束我一般不推荐。能上 64 位就上 64 位这是最省事的解法。顺带说ulimit -v。这是进程级虚拟地址空间的软限制单位是 KiB。有些发行版镜像或者容器基础镜像会默认设一个值比如 4G这时候哪怕机器有 128G 内存程序申请到 4G 虚拟空间就被拒。排查时一定要看ulimit -a全量输出而不是只看-m-m在 Linux 上其实是空操作很多人被这一点误导过。5.2 max_map_count 与内存碎片vm.max_map_count限制的是单个进程能拥有的 VMA 数量默认 65530。你可能会想一个进程怎么可能有六万个内存映射段真会。JVM、MongoDB、Elasticsearch 这类程序会创建大量 mmap内存碎片严重时分配器也会把一个大的逻辑区域切成很多小映射段。一旦到顶报出来的错就是mmap: Cannot allocate memory而此时free显示的可用内存可能还有几十 G非常具有迷惑性。# 查看与临时调整 cat /proc/sys/vm/max_map_count sudo sysctl -w vm.max_map_count262144 # 永久生效 echo vm.max_map_count262144 | sudo tee /etc/sysctl.d/99-mmap.conf碎片是另一条独立的线索。看cat /proc/buddyinfo如果高阶后面几列全是 0说明连续的大块物理内存已经没有了此时申请大页或者很大的连续缓冲会失败哪怕总量充足。看cat /proc/pagetypeinfo能进一步分辨是哪种类型的页被碎片化了。THP 的取值也会影响这一块注意不同 Ubuntu 版本出厂值不一样可能是always也可能是madvise先cat /sys/kernel/mm/transparent_hugepage/enabled确认现状再谈调优。跑 Redis 这类延迟敏感的库建议设成madvise它的启动警告里也会直接点名这件事。5.3 tmpfs、/dev/shm 和锁页内存/dev/shm 默认是内存的一半但它算在内存里写进去的数据直接消耗物理内存。Docker 给容器开的 /dev/shm 默认只有 64MB跑 PyTorch 的 DataLoader 或者某些基于共享内存的中间件时很容易撞到写共享内存失败。解法是启动时加--shm-size2g或者在容器里确认df -h /dev/shm的实际大小。锁页内存是另一类。用mlock或者 CUDA 的 pinned memory 时内存必须被锁在物理内存里不能换出受RLIMIT_MEMLOCK限制Ubuntu 上默认常见是 64KiB 到 64MiB 不等。这个限制在做 GPU 训练、RDMA 通信、或者用大页的数据库时会突然爆出来ulimit -l # 查看当前限制 ulimit -l unlimited # 临时放开需要 rootsystemd 管的服务要在单元文件里设LimitMEMLOCKinfinity只改/etc/security/limits.conf对 systemd 服务是无效的这个坑我踩过不止一次。6. swap、zram 与 swappiness 的取舍最后聊 swap。SSD 时代很多人第一反应是SSD 寿命有限干脆不挂 swap这个结论不能说错但也不够精确。真正需要判断的是这台机器的内存压力是持续性的还是突发性的以及它能不能接受延迟抖动。6.1 swappiness 的字面意思和真实含义vm.swappiness默认 60很多人理解成用 swap 的频率其实它描述的是内核在回收内存时回收匿名页相对于回收文件页的倾向权重。值越高内核越愿意把进程的匿名内存换出去值越低内核越倾向于丢页缓存保住进程内存。对一台主要跑数据库的机器我一般设成 1 到 10。这样做不是禁止换出而是让内核优先丢页缓存尽量保住热数据。注意即便设成 0在内核看来内存实在紧张时它仍然会换出这个参数不是开关。跑着大量文件读写的文件服务器则可以保持默认甚至调高。配套还可以看两个内核水位参数。vm.min_free_kbytes决定每个内存 zone 保留多少空闲页太小会导致回收发生得太频繁表现为系统卡顿vm.watermark_scale_factor默认 10表示水位间距是 zone 内存的 0.1%在突发分配频繁的机器上适当调大比如 100 到 200可以让内核更早开始后台回收减少卡顿。这两个参数属于调好了没感觉、调坏了很明显的类型改动前务必记下原值。6.2 zram现在我更推荐的方案如果你的机器内存不大、又确实需要一点交换空间来吸收突发峰值我现在的首选是zram 而不是磁盘 swap。zram 在内存里划一块区域做压缩块设备写进去的页会被实时压缩典型压缩比在 2:1 到 4:1 之间。它换出的是压缩后的页仍然在内存里所以读写延迟比磁盘低几个数量级同时不会磨损 SSD。Ubuntu 上可以用zram-generator包名随版本略有不同先apt search zram确认一下来配置sudo apt install systemd-zram-generator# /etc/systemd/zram-generator.conf [zram0] zram-size min(ram / 2, 4096) compression-algorithm zstd swap-priority 100配好之后sudo systemctl daemon-reload sudo systemctl start systemd-zram-setupzram0.service再用zramctl和swapon --show确认。swap-priority设成 100 是为了让系统优先用 zram再考虑磁盘 swap。手工配置也不复杂适合想搞清每一步在干什么的场景sudo modprobe zram sudo zramctl --find --size 4G --algorithm zstd sudo mkswap /dev/zram0 sudo swapon -p 100 /dev/zram0注意 zram 占用的是物理内存所以不要把它设得过大否则等于自己减少了可用内存。经验值是物理内存的 25% 到 50%上限 4G 到 8G写入的内容得是能压缩的它压不动视频、加密数据这类高熵内容那些放进 zram 反而亏。6.3 什么时候干脆不要 swap有三种情况我建议不要挂 swap 或者把 swappiness 压到极低一是延迟敏感的交易类服务任何换出都会带来不可控的抖动二是内存规划得很清楚、内存本身就够用的机器swap 反而会掩盖内存泄漏问题让故障从进程崩掉变成整机变慢这种更难查的形态三是用了 zram 但内存本来就吃紧的机器zram 会加剧内存压力这时候应该做的是减负载而不是加交换。判断一台机器是不是在换页看vmstat 1的 si/so 两列就够了持续非零就是真在换。偶尔冒一两个非零值没什么好担心的那可能只是内核在挪动冷页。注意给 systemd 服务设MemorySwapMax0是禁掉这个服务换出的最干净办法比全局调 swappiness 精准得多。我现在的习惯是全局保持默认逐个服务去设上限这样既不影响系统整体的弹性又能保证关键服务不被换出。调完这一轮回过头再看那台内存莫名满了的服务器答案往往就摆在几个文件里/proc/meminfo的 available 和 Committed_AS、cgroup 的 memory.events、还有dmesg里那几行 OOM 记录。Ubuntu 的内存分配从来不是玄学它只是一层套一层的记账规则。把这些账看清了你就不再需要在半夜重启机器了。