第76章:内存优化
12 分钟阅读
第七十六章:内存优化
76.1 先学会"正确地看内存"
76.1.1 free -h 到底怎么读
| |
| |
很多人第一次看这个输出会紧张:“31G 内存用了 8.5G,只剩 1.2G 空闲了!"——这是误读。Linux 的设计理念是"空闲的内存就是浪费的内存”,它会主动把用不上的内存拿去做页缓存(buff/cache),加速文件读写。
真正要看的只有两个数:
available:还能给新程序用多少内存(已经算上了可以随时回收的缓存)。这个数才是"还剩多少内存"的答案free:完全没被使用的内存。它很小甚至接近 0 都是正常的
所以判断"内存够不够",看 available,不要看 free,更不要看 used。
| |
76.1.2 /proc/meminfo 里的关键字段
| |
| |
几个值得盯的:
MemAvailable持续下降:才是"内存真的在变紧"的信号Dirty/Writeback很高:说明有大量数据在等写盘,往往伴着 IO 抖动Slab异常大:内核对象缓存膨胀,可以用sudo slabtop看是哪个子系统AnonPages很大:说明应用的堆内存占用高(而不是被缓存占了),这时候才是真的要扩容或查内存泄漏Committed_AS远超CommitLimit:说明内存严重超额承诺,有触发 OOM 的风险(与 76.4 的 overcommit 有关)
76.1.3 进程内存:VSZ、RSS、PSS 别搞混
| |
| 指标 | 含义 | 注意 |
|---|---|---|
| VSZ / VmSize | 虚拟内存大小 | 含大量未真正分配的空间,看它容易吓到自己 |
| RSS / VmRSS | 常驻物理内存 | 常用指标,但共享库和共享内存会被重复计算 |
| PSS | 按共享比例分摊后的物理内存 | 要"把多个进程加起来"时用 PSS,最准 |
| USS | 该进程独占的物理内存 | 判断"杀掉它能释放多少"时用它 |
| VmSwap | 被换出到 swap 的量 | 不为 0 说明这个进程正被换页 |
为什么把所有进程的 RSS 加起来常常超过总内存? 因为共享库(如
libc)、共享内存被每个进程各算了一遍。要准确统计,用smem:
1 2 3sudo apt install smem sudo smem -t -k # 显示总量,并按 PSS 计算 sudo smem -r -k | head # 按 PSS 排序另外注意:在容器里用
ps/top看到的内存也不是容器限额,那是宿主机的数字(有些容器运行时已做修正)。容器内的内存限制要看 cgroup(见 76.5)。
76.2 判断"内存到底是不是瓶颈"
光看 available 不够,因为内存不足的表现往往在别处。按顺序查这几项:
| |
| |
si(从 swap 读入)和 so(写入 swap)持续非零,说明内存不足,系统正在频繁换页——这会让服务响应时间明显变差。
| |
| |
看到这条就实锤了:内核因为内存不足,直接杀掉了进程。
| |
| |
/proc/pressure/memory(PSI)是判断内存压力最直接的指标:avg10持续偏高,说明进程经常因为等内存而停顿。它比"内存使用率 90%“这种数字有意义得多——使用率高但都是可回收缓存时,其实毫无压力。
76.3 Swap:该不该用、怎么用
76.3.1 swappiness
vm.swappiness 控制"内核有多愿意把匿名页换出到 swap”,取值 0-100,默认 60。
| |
| 场景 | swappiness | 说明 |
|---|---|---|
| 数据库(MySQL、PostgreSQL) | 1-10 | 宁可回收缓存,也不要把数据库的内存换出去 |
| Redis、内存数据库 | 1 | 换出会带来灾难性的延迟抖动 |
| 通用服务器 | 10-30 | 常见折中值 |
| 桌面 / 交互式系统 | 60(默认) | 保留默认即可,让系统在内存紧张时更从容 |
| 内存严重不足 | 60-100 | 这是"没办法的办法",根本解法还是加内存 |
关于
swappiness=0:它并不是"禁用 swap",而是"只有在快 OOM 时才用"。在某些内核版本上,0反而会导致内存回收更激进(因为更倾向丢弃文件缓存),所以 1 比 0 更稳妥。现代 Linux 还有一个
vm.swapiness的补充项:内存压力大时内核可能选择换出而不是丢弃缓存,这解释了为什么有时swappiness设得很低,si/so还是不为零。判断标准始终是 PSI 和si/so,而不是单看这个数值。
76.3.2 Swap 分区 vs Swap 文件
| 形式 | 优点 | 缺点 |
|---|---|---|
| 独立分区 | 性能稍好、不受文件系统影响 | 大小固定,事后调整麻烦 |
| 交换文件 | 随时创建、调整大小、扩容方便 | 需要在支持的文件系统上创建,性能略低(差距很小) |
现代部署普遍用交换文件。创建步骤:
| |
永久启用写进 /etc/fstab:
| |
| |
| |
文件系统上的坑(原稿没提到,但很常见):
- Btrfs:从 Linux 5.0 起支持 swap 文件,但必须关闭该文件的 COW:先
sudo chattr +C /swapfile(要在一个空文件上设置)再写入内容- ZFS:不支持 swap 文件,只能用 zvol
- RHEL 系:除了
chmod 600,还要注意 SELinux 上下文,必要时sudo chcon -t swapfile_t /swapfile- 交换文件不能放在 Btrfs 的 RAID 或多设备 profile 上
76.3.3 Swap 该分多大
“swap 要等于 2 倍内存"是老机器时代的经验,早就不适用了。现在的原则是:
- 大部分现代服务器:给 2-4GB 就够,它的作用只是"兜底"和"让内核有地方放冷页”,而不是真的拿来做内存
- 需要休眠(hibernate):swap 必须 ≥ 内存容量(这是硬要求)
- 内存很小的机器 / 桌面:按内存的 1-2 倍给比较从容
76.3.4 更现代的方案:zram 与 zswap
如果不希望数据真的落到慢速磁盘上,有两个更现代的选择:
- zswap:在内存里开一块压缩缓存,被换出的页先压缩存在这里,只有压不下时才写进磁盘。对 SSD 寿命和延迟都有好处
- zram:把一部分内存做成压缩的块设备当 swap 用。压缩后能存 2-3 倍的数据,而且完全不碰磁盘,在容器、桌面和低内存场景非常流行
| |
Fedora、部分桌面发行版默认就启用了 zram。但不要在服务器上盲目启用:zram 会占用内存并消耗 CPU 做压缩,对已经内存紧张的服务来说,这可能是雪上加霜。
76.4 overcommit:内存"超额承诺"策略
vm.overcommit_memory 决定内核如何对待内存申请:
| 取值 | 含义 | 说明 |
|---|---|---|
| 0 | 启发式检查(默认) | 明显不合理的申请会被拒绝,绝大多数服务器就用它 |
| 1 | 总是允许超额申请 | 申请必然成功,风险推迟到真正写内存时(可能触发 OOM) |
| 2 | 按 CommitLimit 严格限制 | 超额直接拒绝,适合想"提前失败"的场景 |
| |
⚠️ 别把
overcommit_memory=1当成"通用优化"。 它的含义是"内核不再认真检查是否超额",好处是避免某些程序fork时申请不到内存,坏处是把风险推迟到真正分配的那一刻——最坏的结果就是 OOM Killer 直接杀掉你的业务进程。典型的确有必要的场景只有几个:Redis 做后台保存(
BGSAVE)时会 fork 子进程,官方明确建议把它设为 1;此外是一些会 fork 出大子进程的工具(部分 JVM 场景、某些数据库备份工具)。其它情况请保持默认的 0。
76.5 用 cgroup 给进程设定内存上限
单个进程内存失控时,与其等 OOM Killer 随机挑一个"倒霉蛋"(可能正是你的核心业务),不如主动给服务划出内存限额,让超限只影响它自己。
systemd 服务直接加配置项(底层是 cgroup v2):
| |
| |
为什么推荐给关键服务加限额? 因为有
MemoryMax之后,一个服务内存涨疯时,内核只会在这个 cgroup 内触发 OOM,杀掉这个服务自己的进程,而不是把整机拖垮、误杀掉别的服务。这是一种"用可控的小事故换取整机稳定"的做法。cgroup v2 的对应文件是
/sys/fs/cgroup/<路径>/memory.max、memory.high、memory.current、memory.events(oom计数就在这里)。容器同理,Docker 的--memory、Kubernetes 的resources.limits.memory都是这个机制。
76.6 透明大页(THP)与 HugePages
CPU 管理内存是按"页"来的,默认页大小 4KB。大页(HugePages,通常 2MB 或 1GB) 能显著减少页表项数量,提升 TLB 命中率。
问题出在"透明"大页(THP)上——它由内核自动合并,某些应用(Redis、部分数据库、JVM)会因此出现不可预测的延迟抖动。
| |
madvise通常是更好的选择:只有程序显式调用madvise(MADV_HUGEPAGE)时才启用大页,既保留了性能收益,又不会让 Redis 这类程序无端抖动。现代发行版很多已经默认使用madvise。THP 的改动重启后会失效,要持久化得写进 systemd 单元或
rc.local(也可以通过tuned的 profile 管理)。如果某个应用确实需要确定性的大页性能(如 Oracle、DPDK),可以用静态 HugePages:在 GRUB 里加
hugepages=1024,或运行时设置vm.nr_hugepages,然后由应用显式申请。代价是这部分内存被预留后无法给普通程序使用,必须按需规划。
76.7 其它值得了解的参数
| |
运行时相关:
MALLOC_ARENA_MAX=2:glibc 在多线程程序里会为每个线程准备内存池(arena),可能让进程占用大量虚拟内存。把它设为2(或 4)常能显著降低 Java、Python 服务的 VSZ,对内存紧张的容器很有用sudo slabtop:实时查看内核 slab 缓存的构成,排查"内核吃了很多内存"memory.stat:容器/cgroup 场景下用cat /sys/fs/cgroup/<path>/memory.stat看内存具体花在哪(file、anon、slab)
76.8 一份排查脚本
| |
顺带说一句
vm.drop_caches:它可以手工清掉页缓存(echo 3 | sudo tee /proc/sys/vm/drop_caches),但页缓存本来就是用来加速的,清掉之后所有读都要重新走磁盘,性能会短暂变差。它只适合"做 IO 基准测试前把环境弄干净"这一种用途,日常生产不要跑,更不要放进任何定时任务。
本章小结
| 参数 / 工具 | 说明 | 建议 |
|---|---|---|
free -h 的 available | 真正可用的内存 | 判断内存是否够用只看它 |
/proc/pressure/memory | 内存压力(PSI) | 比"使用率"更能说明问题 |
swappiness | 换出匿名页的倾向 | 数据库/Redis 取 1-10,通用取 10-60 |
overcommit_memory | 超额承诺策略 | 保持 0,Redis 等特殊场景才用 1 |
cgroup MemoryMax | 给服务设硬限额 | 关键服务建议设置,避免拖垮整机 |
| THP | 透明大页 | 一般用 madvise,Redis/DB 可设 never |
MALLOC_ARENA_MAX | glibc 内存池数量 | 多线程服务设 2 可省下大量虚拟内存 |
vfs_cache_pressure | inode/dentry 缓存回收倾向 | 大量小文件场景可调小 |
vm.drop_caches | 手工清缓存 | 仅用于性能测试,不要日常使用 |
排查思路:
graph LR
A["疑似内存问题"] --> B{"free: available 还够吗?"}
B -->|"够,但服务慢"| C["看 PSI 与 si/so<br/>可能是换页导致的抖动"]
B -->|"不够"| D["查谁在占内存<br/>ps / smem 按 PSS 排序"]
D --> E{"是自己写的服务吗?"}
E -->|"是"| F["查泄漏或加 cgroup 限额"]
E -->|"否"| G["内核缓存或其它服务<br/>看 slabtop / smem -t"]
C --> H["调整 swappiness / 关闭 THP"]
F --> I["加内存或优化用量"]
G --> I
A --> J{"dmesg 里有 OOM 吗?"}
J -->|"有"| I一句话提醒:Linux 会把内存尽量拿来做缓存,所以"使用率高"本身不是问题。真正要盯的是
available是否持续下降、PSI 压力是否升高、有没有出现 swap 换入换出和 OOM 记录。看到used高就急着清缓存、加内存,往往是在解决一个并不存在的问题。
下一章我们看磁盘 IO 优化。