VPS内存总是OOM:从内核日志找到真凶
来源: 飞翔小编 2026/11/4 15:50:30 浏览量:0
一、OOM 不是「内存不够」,是内核替你杀了进程

很多人一看到 OOM 就去买更大的机器,但真正的 OOM 里藏着一条完整的故事:内存不是突然不够的,是某个进程在某时刻越过了内核设定的红线,内核主动挑了一个进程杀掉。而内核挑谁、按什么标准挑,读懂日志你才知道该动哪里。
先看内核到底有没有杀进程。这是判断「是我的应用吃爆了」还是「是别的东西挤爆了内存」的分水岭:
dmesg -T | grep -i -E "out of memory|oom-kill|killed process"
如果这条命令有输出,说明内核确实介入了。如果没有输出,那你的问题很可能是容器内的 cgroup 限制触发的,而不是宿主机内核的 OOM——这两种的处理方式完全不同,第八节会专门讲这个区别。
如果日志里是 Killed process 1234 (nginx) total-vm:...,说明内核杀掉了这个进程。日志里最重要的三段信息:
| 日志片段 | 含义 | 你要做什么 |
|---|---|---|
Out of memory: Killed process 1234 (nginx) |
被杀的进程 PID 和名字 | 确认是谁吃爆了 |
total-vm:XXXXXXkB, anon-rss:XXXXkB |
该进程虚拟内存和常驻内存 | 判断是真占用还是虚高 |
pages RAM, swap, ... |
内存和 swap 的剩余情况 | 确认 swap 有没有起作用 |
最关键的一条认识:内核 OOM 时优先杀的是「最占内存且最不重要」的进程,排序依据是 oom_score。也就是说,OOM killer 本身已经在帮你找真凶了——被杀的那个进程,几乎总是内存占用最大的那个。被杀的进程就是真凶,这是 OOM 排查最省时间的一条捷径。
另一个必须知道的事实:内存不足和 OOM 是两回事。内存不足的表现是系统变慢、开始频繁读 swap、磁盘 IO 打满;OOM 是内核主动杀进程,通常在日志里留下明确记录。先分清这两者,能省掉一半的无效排查。
第三个必须知道的机制细节:OOM killer 在内核里有两套触发路径。一是全局 OOM,即整台机器的物理内存加 swap 全部耗尽,触发的是 Out of memory: Killed process 这类日志。二是cgroup OOM,即某个 cgroup 的内存限制被突破,触发的是 Memory cgroup out of memory 这类日志,被杀的是那个 cgroup 里占用最大的进程。两者的日志文本不同,排查时先 grep 关键词就能分流。
第四个细节:内核在杀进程前会先尝试回收内存。它会先扫掉可回收的文件缓存页,再尝试换出干净的内存页,只有这些都不够时才真正杀进程。所以日志里出现 OOM 记录,意味着前面所有回收手段都已经用尽了——这时候 swap 早就满了或者压根没启用,这也是为什么 swap 是这个问题里绕不开的一环。
二、日志在哪:四个位置,按优先级找

OOM 日志可能不在你以为的地方。四个位置,从最可能到最不可能:
位置一,dmesg 缓冲区(最常用)。
dmesg -T | tail -100
-T 参数把时间戳转成人类可读格式,否则你会看到一堆 [12345.678] 这样的秒数,对不上事故发生时间。注意 dmesg 缓冲区会滚动的,机器 uptime 很长时,昨天的事故日志可能已经被冲掉了。这时候要看持久化日志。
位置二,systemd journal(重启后仍在)。
journalctl -k --since "2 hours ago" | grep -i -E "oom|out of memory"
journalctl -k --since "yesterday" -p warning --no-pager | tail -50
journalctl 的最大优势是日志存到了磁盘,重启也不丢。这是排查「昨天半夜服务莫名其妙重启」的唯一可靠途径。
位置三,重启记录(进程重启但机器没重启)。
last -x | head -20
who -b
如果 last -x 显示系统没有重启,但某个服务的时间戳变了,说明服务是被 OOM 杀掉后被守护进程自动拉起的。
位置四,容器环境的 cgroup 日志。如果你跑在 Docker 或 LXC 里,内核 OOM 记录可能在宿主机上,你在自己容器里 dmesg 是空的。判断方法:cat /sys/fs/cgroup/memory.events 或 cat /sys/fs/cgroup/memory/memory.failcnt,非零就说明触发了限制。
一条命令定位全部:
dmesg -T 2>/dev/null | grep -i -E "oom|out of memory|killed process" | tail -20
journalctl -k --since "24 hours ago" --no-pager 2>/dev/null | grep -i -E "oom|killed" | tail -20
两条都没输出但服务仍然被杀,八成是 cgroup 限制,去看 /sys/fs/cgroup/memory.events 的 oom_kill 计数。
三、OOM 日志怎么读:五个字段逐个拆

一条典型的 OOM 日志长这样(数值是示意格式):
[Sat Nov 1 03:14:22 2026] Out of memory: Killed process 4711 (node) total-vm:2845432kB, anon-rss:1834528kB, file-rss:65536kB, shmem-rss:0kB
字段一,进程名和 PID。(node) 是进程名,4711 是 PID。先确认这个进程是不是你的。如果被杀的是 mongod、redis、java,那它可能只是「最胖的那个受害者」,真正的问题在别处。
字段二,total-vm:虚拟内存总量。这是进程申请的所有地址空间,包括没真正使用的部分。Node.js、Java 这类运行时的 total-vm 经常是实际常驻内存的好几倍。所以 total-vm 大不等于内存泄漏。
字段三,anon-rss:匿名常驻内存。这是进程真正占用的物理内存里、来自 malloc 分配的那部分。这个数字才是你要盯的。
字段四,file-rss:文件映射的常驻内存。来自 mmap 读取的文件页。这部分内存是可回收的——内核在内存紧张时会优先把它清掉。
字段五,shmem-rss:共享内存。多个进程共享的内存段,通常是 IPC 用的。
判读的关键:把 anon-rss 和你的物理内存比。假设你有 1 GB 内存,被杀的进程 anon-rss 是 1834528 kB(约 1.75 GB),那它一个进程就超了总量——必然被杀。但如果是 700 MB,占 1 GB 内存的 70%,那说明同时还有别的进程在占,或者内核有额外开销。
更实用的判读法:看被杀进程的 anon-rss 减去其他所有进程之和。如果被杀的那个明显是最大的,它就是元凶;如果几个进程差不多大,那说明是总量超了,不是某个进程泄漏。
还有一个必须看的字段:pages RAM, swap。这行告诉你内存和 swap 各剩多少。如果 swap 也用光了,说明 swap 根本没起到缓冲作用,这时候 swap 就该加大,或者该关掉某些吃内存的服务。
四、真凶排序:六类常见元凶,按出现频率排

按实际出现频率从高到低:
元凶一:内存泄漏的常驻进程。特征是内存占用随时间单调上升,从不回落。典型是 Node.js 应用里没释放的缓存、Python 进程里累积的列表、Java 里没关闭的连接池。
元凶二:突发的大对象分配。特征是占用在某个时刻突然跳上去,然后被杀。典型是一次请求加载了超大文件到内存、一次 readFile 读了整个 2 GB 文件、日志切割时把全部日志读进内存。
元凶三:数据库或缓存的内存配置超限。特征是服务启动后很快就被杀,而且日志里的进程是 mysqld、redis-server、mongod。这类服务默认会按可用内存的百分比分配缓存。
元凶四:容器多副本叠加。特征是宿主机内存明明够,但容器被杀。同一个容器里跑了 5 个进程,每个都按「机器总内存」而不是「容器内存限制」来分配,全加起来超了限制。
元凶五:大量小进程累积。特征是单个进程都不大,但数量极多。每个进程自己占 10 MB 看着不多,500 个就是 5 GB。
元凶六:swap 被占满后的连锁反应。特征是系统整体变慢,OOM 发生在 swap 用尽之后。
元凶七:进程数失控。特征是进程数量异常增长,常见于定时任务没清理、僵死进程堆积、或者某个脚本反复 fork 子进程。这类问题的隐蔽之处在于每个进程单看都不大,内存是在数量维度上被吃掉的。
元凶八:内存碎片化导致分配失败。特征是free 显示还有几百 MB 可用,但分配大块内存时失败。这在长期运行的机器上比较常见,/proc/meminfo 里的 MemAvailable 会明显小于 free 看到的数值。
元凶九:日志切割或备份脚本的临时峰值。特征是OOM 时间和定时任务时间高度吻合。这类脚本会把整个日志文件读进内存处理,文件越大瞬时占用越高。临时解法是把切割周期缩短,让单个文件不至于太大;根本解法是用流式处理而不是全量读取。
用一条命令定位到具体进程(按内存排序):
ps -eo pid,ppid,user,pmem,rss,vsz,comm --sort=-rss | head -20
看内存增长趋势(判断是不是泄漏):
watch -n 5 "ps -eo pid,rss,comm --sort=-rss | head -5"
或者用 smem 看更直观的占比:
smem -s rss -p ".*" | head -20
看进程的私有内存和虚拟内存区别(定位 Java/Node 这类运行时):
pmap -x 4711 | tail -3
最后一行是 total,第二行是 RSS(真实占用),第一行是 VSZ(虚拟)。对 Node.js 和 Java 来说,VSZ 远大于 RSS 是正常现象,不是泄漏的证据。
五、swap 该不该开:这是个取舍,不是越多越好

swap 的作用是把暂时不用的内存页挪到磁盘上,让内存压力缓解。它的代价是磁盘 IO 慢几个数量级——内存是纳秒级,随机写磁盘是毫秒级,差了几千倍。
结论先行:VPS 上应该开 swap,但应该用小而精的策略,不该无脑开 2 倍内存。
该开的理由:第一,它能避免内核在内存瞬间紧张时直接杀进程,给你一个「缓冲窗口」。第二,很多云主机的镜像默认就带 1 GB swap,关掉它反而容易在突发时直接崩。
不该开大 swap 的理由:一旦系统开始大量读写 swap,你的服务延迟会从毫秒级劣化到秒级,而且磁盘 IO 打满会连带影响同机的其他业务。更糟的情况是 swap 在磁盘上而磁盘本身是网络存储(很多 VPS 是),swap 写在网络存储上等于把内存瓶颈换成了网络瓶颈。
推荐的配置:1 GB 到 2 GB swap,加上低换页倾向。
# 创建 1 GB swap
fallocate -l 1G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
# 写入开机自动挂载
echo "/swapfile none swap sw 0 0" >> /etc/fstab
关键是调整换页倾向。默认值 60 意味着内存用到 60% 就开始换页,太激进:
# 查看当前值
sysctl vm.swappiness
# 改为 10:只在内存快用满时才换页
sysctl -w vm.swappiness=10
# 永久生效
echo "vm.swappiness=10" >> /etc/sysctl.conf
swappiness 设为 10 的意思是:优先把「很久没用的干净页」清掉换出,只有在实在腾不出空间时才动 swap。这样能把 swap 当成应急缓冲,而不是常规内存的延伸。
还有一个更彻底的做法:直接关掉 swap。如果你的服务有明确的内存上限(比如用容器限制),内核在内存紧张时直接杀进程,比疯狂读写磁盘更可控。判断标准:如果你的机器磁盘是网络存储,或者 swap 写满了会触发 OOM(cgroup 环境下 memory.swap.max 常常就是 0),那就关掉。
关 swap 之前先确认一件事:你是否真的需要它兜底。一个务实的做法是先开着 swap 但把 swappiness 压到 10,观察一周。如果这一周里 OOM 次数为零,说明你的内存是够的,swap 从来没被真正用到,可以放心关掉。如果 swap 使用量在持续上涨,说明你的工作集确实超过了物理内存,这时候关掉 swap 只是把「慢慢变慢」变成「立刻被杀」,正确的做法是加内存或者降工作集。
swap 文件放在哪个盘也很关键。如果你的系统盘和网络数据盘是分开的,把 swap 放在系统盘上,不要放在数据盘上。原因是数据盘的 IO 通常更繁忙,把 swap 和业务数据放同一个设备上,两者会互相拖慢——业务请求等磁盘,磁盘等换页,最后谁都不快。
六、进程内存上限的正确设法:四道防线

别用 ulimit -v。这是最常见的错误做法。ulimit -v 限制的是虚拟内存,而 Node.js、Java、JVM 这类运行时会预留大量虚拟地址空间,哪怕实际只用几百 MB,虚拟内存也可能上 GB。用 ulimit -v 限制它们会直接导致启动失败或异常崩溃。
第一道防线:ulimit -m 不可靠。Linux 不支持真正限制常驻内存,这个参数基本无效,别用。
第二道防线:systemd 服务的 MemoryMax。这是最推荐的方案:
[Service]
MemoryMax=512M
MemoryHigh=400M
写入 service 文件后:
systemctl daemon-reload
systemctl restart your-service
MemoryMax 是硬上限,超过就触发 cgroup OOM。MemoryHigh 是软阈值,超过后系统开始回收和降速。关键点:MemoryMax 触发的是 cgroup OOM 而不是全局 OOM,日志位置和杀进程策略都不同,所以排查时要注意区分。
第三道防线:systemd-oomd。这是针对「内存压力」而非「内存耗尽」的守护进程,在内存压力持续偏高时会主动杀掉占用最多的服务,防止整机卡死:
systemctl status systemd-oomd
Ubuntu 和 Debian 的较新版本自带它。开启后可以配置:
[Service]
ManagedOOMSwap=kill
ManagedOOMMemoryPressure=kill
ManagedOOMMemoryPressureLimit=60%
第四道防线:容器内的 cgroup 限制。如果你用 Docker:
docker run -d --memory=512m --memory-swap=512m your-image
--memory 限制物理内存加 swap 的总量。注意 --memory-swap 的默认值是内存的两倍,等于默认开了 512 MB 的 swap,很多人在这里 unintentionally 开了 swap。
四道防线的优先级:容器限制(最外层)→ systemd MemoryMax(服务级)→ 进程内部限制(最精细)。从上到下逐层收紧,从下到上逐层兜底。
查当前生效的限制:
systemctl show your-service -p MemoryMax -p MemoryHigh
cat /sys/fs/cgroup/memory.max
cat /sys/fs/cgroup/memory.current
第五道防线:给数据库和缓存单独设限。这类服务默认会按可用内存的百分比分配缓存,是最常见的 OOM 元凶。MySQL 的 innodb_buffer_pool_size、Redis 的 maxmemory 都应该设成物理内存的一定比例,比如 50% 到 60%,而不是让它自适应。一个简单的原则:任何单个服务的内存上限,都不应该超过物理内存的一半,否则一旦它出问题,整台机器都会被拖下水。
验证限制是否生效的方法:重启服务后用 systemd-cgtop 看实时占用,或者直接读 memory.current 对照 memory.max,两个数接近就说明限制起作用了。
七、算账:多大内存跑什么业务,看这张表

先看真实在售机型的内存单价,再算每档内存的「服务承载数」。
按月付费的真实机型:
| 机型 | 月费 | 内存 | 系统盘 | 月流量 | 内存单价 | 磁盘单价 | 流量单价 |
|---|---|---|---|---|---|---|---|
| RosCloud-01 | 19.99 美元 | 1 GB | 20 GB | 1024 GB | 19.99 美元/GB | 0.999 美元/GB | 19.99 美元/TB |
| RosCloud-02 | 29.99 美元 | 2 GB | 40 GB | 2048 GB | 14.99 美元/GB | 0.75 美元/GB | 14.995 美元/TB |
| RosCloud-03 | 39.99 美元 | 4 GB | 80 GB | 3072 GB | 10.00 美元/GB | 0.5 美元/GB | 13.33 美元/TB |
| RosCloud-04 | 49.99 美元 | 8 GB | 160 GB | 4096 GB | 6.25 美元/GB | 0.312 美元/GB | 12.498 美元/TB |
| RosCloud-05 | 59.99 美元 | 16 GB | 240 GB | 5120 GB | 3.75 美元/GB | 0.25 美元/GB | 11.998 美元/TB |
| RosCloud-06 | 69.99 美元 | 24 GB | 320 GB | 6144 GB | 2.92 美元/GB | 0.219 美元/GB | 11.665 美元/TB |
内存单价从 19.99 美元/GB 一路降到 2.92 美元/GB,降幅 85%。这直接推出一个结论:在低内存档位上,你的容错空间极小,一次泄漏就足以让服务完全不可用。
用「安全线算法」算出每档机器能跑几个服务。算法是:物理内存减去系统和基础服务开销(约 120 MB),乘以 70% 的安全系数,得到可分配内存;再除以单个服务的稳态占用,得到可承载服务数。
| 机型 | 可分配内存(70% 安全线) | 单服务按 250 MB 算,可承载数 | 单服务按 500 MB 算,可承载数 |
|---|---|---|---|
| RosCloud-01 | 633 MB | 2 个 | 1 个 |
| RosCloud-02 | 1317 MB | 5 个 | 2 个 |
| RosCloud-03 | 2683 MB | 10 个 | 5 个 |
| RosCloud-04 | 5665 MB | 22 个 | 11 个 |
| RosCloud-05 | 11381 MB | 45 个 | 22 个 |
| RosCloud-06 | 17084 MB | 68 个 | 34 个 |
读这张表的正确方式:不是看「能跑几个」,是看「出问题时还剩多少余量」。RosCloud-01 那档,可分配 633 MB,放两个 250 MB 的服务之后只剩 133 MB。任何一次内存波动、一次日志切割、一次依赖升级的临时占用,都会触发 OOM。同样两个服务放到 RosCloud-03 上,可分配 2683 MB,用掉 500 MB 还剩 2183 MB,余量相差 16 倍。
年付线的单位成本对照:
| 机型 | 年费 | 折合月费 | 内存 | 系统盘 | 月流量 | 内存单价 | 磁盘单价 | 流量单价 |
|---|---|---|---|---|---|---|---|---|
| LAX-STARTER | 200 美元 | 16.67 美元 | 2 GB | 40 GB | 5000 GB | 8.33 美元/GB | 0.417 美元/GB | 3.413 美元/TB |
| LAX-MICRO | 400 美元 | 33.33 美元 | 4 GB | 80 GB | 10000 GB | 8.33 美元/GB | 0.417 美元/GB | 3.413 美元/TB |
| LAX-Storage-500G | 400 美元 | 33.33 美元 | 4 GB | 500 GB | 10000 GB | 8.33 美元/GB | 0.067 美元/GB | 3.413 美元/TB |
| LAX-MEDIUM | 800 美元 | 66.67 美元 | 8 GB | 160 GB | 20000 GB | 8.33 美元/GB | 0.417 美元/GB | 3.413 美元/TB |
| LAX-LARGE | 1600 美元 | 133.33 美元 | 16 GB | 320 GB | 40000 GB | 8.33 美元/GB | 0.417 美元/GB | 3.413 美元/TB |
LAX-STARTER 折合月费 16.67 美元,内存单价 8.33 美元/GB,比 RosCloud-01 的 19.99 美元/GB 低 58%。同样是 2 GB 内存,年付这台的单位成本只有月付 RosCloud-02 的一半多。如果你的业务对内存余量敏感,年付线在同一内存规格下明显更划算——前提是接受年付的锁定成本。
八、什么情况下不该继续在原机器上修:四种明确场景

有些情况,调参救不回来,必须换配置或者换架构。以下四种情况,判断依据全部是公开事实,不涉及对任何商家的评价:
场景一:业务负载已经超出机器规格,且负载还会继续涨。依据是内存单价结构——小内存档位单价高,长期运行的业务在低配机器上既没有余量也没有性价比。替代方案:直接上 4 GB 或 8 GB 档,或按第八节的换机清单迁移,不要在 1 GB 机器上继续调参。
场景二:日志里显示的是 cgroup OOM 而非全局 OOM。依据是内核机制——cgroup 限制是容器层面的硬边界,你在容器内加 swap、改 swappiness 都改不了宿主机给的限制。替代方案:调整容器的 memory 限制参数,或者换成没有容器隔离限制的普通 VPS。
场景三:数据密集型服务且机器磁盘是网络存储。依据是 swap 机制——swap 落在网络存储上时,换页延迟等于网络延迟,配置再高也没用,反而会掩盖真实问题。替代方案:把数据库和计算拆到不同机器,数据库那台不依赖 swap。
场景四:频繁遭遇 OOM 且已经加了内存限制仍被杀。依据是分配机制——当服务被设了 MemoryMax 之后仍然 OOM,说明它的工作集本身就超过了限制,加限制只是让被杀得更早。替代方案:先把工作集降下来(分批处理、流式读取而不是全量加载),降不下来再升配。
这四种场景的共同判断逻辑是:先分清「是配置不够」还是「是架构不对」。配置不够可以加钱解决,架构不对加钱也解决不了,只是把钱花在错误的地方。
九、完整排查流程:六步定位 OOM 真凶

第一步,确认是不是内核 OOM(1 分钟)。
dmesg -T 2>/dev/null | grep -i -E "oom|out of memory|killed process" | tail -20
第二步,确认是不是 cgroup OOM(1 分钟)。
cat /sys/fs/cgroup/memory.events 2>/dev/null
cat /sys/fs/cgroup/memory.max 2>/dev/null
cat /sys/fs/cgroup/memory.current 2>/dev/null
第三步,看当前内存分布(1 分钟)。
free -m
ps -eo pid,ppid,user,pmem,rss,vsz,comm --sort=-rss | head -15
第四步,看内存是不是在持续增长(5 分钟)。
watch -n 5 "ps -eo pid,rss,comm --sort=-rss | head -5; free -m | grep Mem"
盯两个数:free 里的 available 和榜首进程的 rss。如果 available 在持续下降且没有回升,就是泄漏;如果 available 稳定但服务被杀,那是突发分配。
第五步,确认 swap 状态(30 秒)。
swapon --show
free -m
sysctl vm.swappiness
第六步,加限制防止再次被杀(持续生效)。
# 给某个 systemd 服务加内存上限
systemctl edit your-service
# 写入:
# [Service]
# MemoryMax=512M
# MemoryHigh=400M
systemctl daemon-reload && systemctl restart your-service
第七步(可选但推荐):把 OOM 监控做成常态。装一个轻量的监控,把 memory.events 的 oom_kill 计数和 free 的 available 一起采集,出问题前就能看到趋势。OOM 这种问题最重要的不是修好一次,而是让它下次发生时你能第一时间知道。
按你的情况,下一步该看哪家
上面讲的是判断标准,但多数人最后还是想问一句「那我该买谁」。按你的实际情况对照下面几条,直接去看对应的机型页比继续比价更快:
- 主要访客在国内、在意打开速度:先确认回程线路,再谈配置。看 iwebfusion 当前机型与价格
- 跑游戏服或实时业务:看上行抖动与防御能力,普通套餐往往不达标。看 racknerd 当前机型与价格
- 只是开发测试、临时用:按小时或月付,用完就删,别买年付。看 cloudcone 当前机型与价格
价格与库存以商家页面当日显示为准,下单前建议先确认退款窗口与是否支持按月付费。
十、常见问题

Q:日志里说 Killed process 1234 (java),是不是 Java 程序有问题?不一定。被杀的进程只是当时内存占用最大的那个,它可能是受害者而不是元凶。真正的元凶可能是另一个占内存的进程导致内存耗尽,而 Java 因为自身占用大被挑中。继续往下查:对比被杀进程的 anon-rss 和其他进程占用,如果它明显最大,它就是元凶;如果几个差不多大,那是总量问题。
Q:为什么开了 swap 还是会被 OOM kill?三个常见原因。第一,swappiness 默认值 60 太高,内存用到六成就开始换页,但 swap 也很快满了。第二,swap 文件所在的磁盘满了或者太慢,换页跟不上分配速度。第三,你用的是 MemoryMax 这种 cgroup 限制,而 cgroup 的 swap 限制常常是 0,这种情况下内核没有缓冲空间可用,直接杀进程。查 cat /sys/fs/cgroup/memory.swap.max,如果是 0,那 swap 在你的环境里根本不参与缓冲。
Q:怎么判断是内存泄漏还是突发的大对象分配?看内存曲线形状。泄漏是单调上升的阶梯状,每一级台阶对应一次业务操作后没释放的内存,几天甚至几周都不回落。突发分配是尖峰,平时稳定在低位,某次操作瞬间冲上去然后被杀。最可靠的做法是开一个 5 分钟一次的采样脚本存成日志,跑一天再看曲线形状。
Q:ulimit -v 设多少合适?这个参数不该用来限制内存。它限制的是虚拟内存地址空间,Node.js、Java、JVM 这类运行时会预留大量虚拟地址,用它限制会导致启动失败或运行异常。正确做法是用 systemd 的 MemoryMax(限制 cgroup 内存)或容器参数。如果非要用 shell 限制,可以用 ulimit -d 限制数据段,它比 -v 安全一些,但依然不如 cgroup 方案精确。**
Q:为什么我的 VPS 有 2 GB 内存,free -h 显示可用只有几百 MB?三个常见原因。第一,Linux 把「已分配但未使用」的内存算作 used,这部分随时可回收,看 available 才是真实可用量。第二,磁盘缓存占了内存,这部分同样可回收,available 里已经算进去了。第三,确实有进程在吃内存,用 ps --sort=-rss 看榜首。只有 available 持续偏低且不回升,才是真有问题。
Q:Docker 里的容器被 OOM killed,但宿主机内存还有很多?这是 cgroup 限制的典型表现。容器有独立的内存上限,宿主机内存再多也没用。查 docker inspect 里的 Memory 字段,或者 cat /sys/fs/cgroup/memory.max 看容器实际的限制值。解决方向是调高容器限制,或者减少容器内的进程数——很多镜像里 supervisor 拉起了多个进程,每个都以为自己独占内存。
Q:被 OOM kill 之后服务自动重启了,为什么还是有问题?因为根因没解决。自动重启只是把服务拉起来,内存占用的元凶还在,下一次照样被杀。而且频繁重启本身会造成流量抖动和错误率上升。正确做法是:先按第九节定位真凶并限制它,再考虑重启策略。如果只是临时止血,可以在启动脚本里加 --max-old-space-size 之类参数给运行时设上限,让它主动报错而不是被内核杀掉。
Q:升级内存到 2 GB 就一定能解决 OOM 吗?不一定。如果根因是泄漏,2 GB 只是把崩溃时间从两天延到四天。升级前先确认三件事:内存占用是不是单调上升(泄漏)、被杀的是不是单个进程(架构问题)、有没有 cgroup 限制在生效(容器环境)。这三个问题都能排除,再谈加内存才有意义。
Q:怎么在买之前就知道够不够用?用可分配内存算法倒推。物理内存减去 120 MB 的系统和基础服务开销,乘以 70% 的安全系数,再除以你实测的单服务内存占用。如果算出来的余量低于 30%,说明这台机器没有容错空间,再高的配置才算稳妥。关键是要用实测值而不是估估值——上线前压一次,把 ps --sort=-rss 的榜首数字记下来,这个数字就是你以后扩容的依据。
