搬瓦工 VPS 内存不足怎么办?Swap、OOM 和进程排查

搬瓦工 VPS 内存不足怎么办?Swap、OOM 和进程排查
先区分缓存、内存压力与进程限额,再决定是否增加 Swap。

网站开始返回 502、数据库突然退出,SSH 也变得迟缓,第一步不是清缓存或重启。先看可用内存,再查同一时间的进程和日志:缓存占用、整台 VPS 内存紧张,以及某个容器碰到上限,处理方法并不一样。

Swap 能为短时内存压力提供缓冲,但不会把低配 VPS 变成大内存服务器。如果程序本身持续占用超出物理内存,增加 Swap 后也可能只是从“进程退出”变成“服务很慢”。 [8]

下面以 Debian / Ubuntu 为例,先做只读排查,再讨论 Swap、WordPress 和 Docker。涉及写入配置的步骤已单独标明;操作前保留现有 SSH 窗口,并确认 KiwiVM 控制台入口。系统已经有 Swap 时,先核对现有配置,不要重复覆盖。本文目录
判断内存压力 · 确认 OOM · 恢复访问 · 添加 Swap · 参数与停用 · 容器限额 · 网站优化 · 复查与扩容 · 常见问题

一、先看 available,别把缓存当成故障

在 VPS 系统中执行:内存、Swap 和连续采样

free -h
swapon --show
vmstat 1 6

free 是当前完全空闲的内存;available 是估计还能用于启动新应用、而无需交换的内存,其中考虑了一部分可回收缓存。buff/cache 并不全是不能再用的内存,也不能全部当成马上能释放的空间。 [1]

例如,free 只剩 96 MiB、available 还有 860 MiB,不能仅凭前一个数字判定内存不足。这只是字段解释示例,不是本次实测。更值得留意的是 available 长期偏低,同时网站变慢、换页活跃,或日志出现内存相关退出。

free、available、Swap 活动与 OOM 日志的判断方法
Swap 已用不为零,并不能单独说明当前内存不足。
观察项怎么理解下一步
available 较充足,只是 free 很低可能只是正常的缓存利用先看业务响应,不急着清缓存
available 很低,si/so 持续活跃系统正在频繁换入、换出页面定位进程,降低并发和后台任务负载
VPS 还有余量,但某个容器反复退出可能达到容器或服务单独设置的限额查 OOMKilled、MemoryMax 和相关日志
有 Swap 已用,si/so 基本安静部分页面可能仍留在 Swap 中不能据此判断当前仍在频繁换页

vmstat 1 6 的第一行速率统计通常反映开机以来的平均值,后面才是逐秒采样;重点看后续行的 si、so。wa 偏高只说明有 I/O 等待,不能单凭它认定是 Swap。 [2]

CPU 高、负载高也未必是内存不足。CPU 使用规则和进程定位可以对照 搬瓦工 CPU 占用过高导致 VPS 暂停怎么办,不要把两类限制混在一起。

二、谁占内存,谁被终止,要分开查

按常驻内存排序;RSS 单位为 KiB

ps -eo pid,ppid,user,comm,rss,pmem --sort=-rss | head -n 21

RSS 反映进程当前驻留在 RAM 中的部分,不是它所有虚拟地址空间。多个进程可能映射同一份共享页,因此把 RSS 逐项相加只是粗略参考;不能拿 VIRT 的大数值直接当实际内存消耗。 [3]

先保存出错时间附近的内核记录

sudo journalctl -k -b --since "2 hours ago" --no-pager -n 300从记录中筛选 OOM 相关信息
sudo journalctl -k -b --since "2 hours ago" --no-pager -n 300 
| grep -Ei 'out of memory|oom-kill|killed process'

查到记录后,对照时间、PID 和进程名。被杀进程不一定是内存增长的最初来源,进程退出后的当前内存也可能已经恢复。没有匹配结果时,继续确认日志权限、保留范围和启动轮次;“没查到”不能直接等同于“没发生”。

内核 OOM、容器限额、systemd-oomd 与应用限制的区别
退出码 137 不能单独证明 OOM;需要结合日志和容器状态。

整机 OOM、容器 OOM 和 systemd-oomd

整台 VPS 的内存紧张,与容器所在 cgroup 的限制是两回事。容器或服务达到 memory.max 后,即使其他服务还有可用资源,也可能在该组内触发 OOM;cgroup v2 的 memory.events 会记录 oom、oom_kill 等计数。计数需要结合故障时间和前后变化看。 [4]

检查 systemd-oomd;服务未安装时会提示不存在

systemctl is-active systemd-oomd
sudo journalctl -u systemd-oomd --since "2 hours ago" --no-pager -n 100

启用了 systemd-oomd 的系统,可能根据内存压力在内核 OOM 之前终止一组进程。这类情况要查它自己的日志,而不是只找内核里的 Killed process。不要看到它参与处理就直接禁用;先查应用负载和限制是否合理。 [5]

已经重启过时,查看上一轮启动;前提是旧日志仍在

sudo journalctl --list-boots
sudo journalctl -k -b -1 --no-pager -n 300

若只是 PHP 报 Allowed memory size exhausted,通常是该请求碰到 PHP 的内存限制,不代表整个 Linux 系统已经触发 OOM。提高限制前,仍要判断请求是否异常,以及服务器是否有相应余量。 [11][12]

三、服务已经卡住,先恢复访问,再做调整

VPS 内存不足时的现场保留与恢复顺序
控制台绕过 SSH 连接方式,但不能凭空补足已耗尽的系统资源。

还能登录时,先暂停已经确认可中断的批处理、备份、图片处理或导入任务,再观察内存是否回升。由 systemd 或 Docker 管理的任务,优先通过对应服务管理器停止,避免进程被守护程序立即拉起。停止动作会中断任务,操作前要确认业务能否接受。

仅在确认目标后执行;替换为真实服务名或容器名

sudo systemctl stop your-worker.serviceDocker 场景二选一,不要对不相关服务一并执行
docker stop --time 30 your-worker

不要照着内存排名直接杀掉 MySQL、Redis 或 sshd。对数据库应先停止上游高并发、导入或后台任务,再评估是否需要正常停库。系统非常紧张时也不建议马上做全量压缩备份,因为新任务可能进一步放大压力。

SSH 进不去,可以尝试 KiwiVM 的控制台。它不依赖当前 SSH 连接,但仍需要虚拟机内部有资源响应;若系统完全无响应,才在确认影响后考虑一次受控重启。重启可能暂时释放内存,也可能丢失现场,不能代替原因分析。面板入口见 搬瓦工 KiwiVM 控制面板完整使用教程。 [15]

恢复访问后,先导出数据库和关键配置,再做后续变更。回滚方案可以参考 搬瓦工快照与自动备份教程;若发现不明进程、陌生账号或异常自启动项,则按入侵事件处理,并对照 搬瓦工 VPS 安全设置:SSH Key、防火墙、Fail2ban,不要只加 Swap 让异常程序继续运行。

四、怎样安全地增加 Swap

Swap 可以把部分可交换页面暂时放到其他存储介质。本文只演示磁盘 Swap 文件,不涉及 zram 或 zswap。它适合为短时峰值留一点缓冲;如果应用每次请求都要频繁从磁盘换回数据,延迟仍可能明显上升。 [6][8]

Swap 文件创建、启用、持久化和验证顺序
先检查和备份。命令示例仅用于 ext4/XFS,不修改已有 Swap。

1. 先检查文件系统和已有配置

这些检查不会修改系统

swapon --show
findmnt -n -o FSTYPE -T /
df -h /
grep -E '^[^#].*[[:space:]]swap[[:space:]]' /etc/fstab

本文的创建示例只用于 ext4 / XFS。Btrfs 有单独的 Swap 文件要求,网络文件系统和容器环境也不能照搬。不要用 truncate 制作稀疏文件后直接当 Swap;这里采用 dd 写入实际空间,以减少文件系统兼容性问题。 [6]

本文以新建 2 GiB 为示例,并要求至少有 6 GiB 可用磁盘,创建后保留至少 4 GiB。这是本例的保护条件,不是通用容量公式;网站增长、数据库和备份需要更多空间时,应继续提高预留量。已有 Swap 满足需求就不要重复创建。

2. 保存检查脚本,确认后再创建

把下面内容保存为 scripts/create-swap-example.sh。脚本固定使用新路径 /swapfile-bwh,默认仅检查;它拒绝同名文件、符号链接、已有同路径开机条目和不支持的文件系统,不会关闭已有 Swap。展开完整脚本:先保存,再检查

以下仅把脚本保存到当前目录,不会创建或启用 Swap

mkdir -p scripts
cat > scripts/create-swap-example.sh <<'BWH_SWAP_SCRIPT'
#!/usr/bin/env bash
# 文章中的受保护示例:只支持 ext4/XFS;拒绝覆盖既有文件;不改 /etc/fstab。
# 默认仅检查,必须显式传入 --apply 才会新建并启用 2 GiB Swap。
set -euo pipefail
export LC_ALL=C
FILE=/swapfile-bwh
SIZE_MIB=2048
RESERVE_MIB=4096abort() { printf '%s\n' "$*" >&2; exit 1; }
check_environment() {
[[ $EUID -eq 0 ]] || abort '请使用 sudo bash create-swap-example.sh [--apply]。'
local cmd
for cmd in findmnt df awk swapon dd chmod mkswap; do
command -v "$cmd" >/dev/null 2>&1 || abort "缺少命令:$cmd"
done
local fs available need
fs=$(findmnt -n -o FSTYPE -T /)
case "$fs" in
ext4|xfs) ;;
) abort "本示例不处理文件系统 $fs;请勿强行套用。" ;;
esac
[[ ! -e "$FILE" && ! -L "$FILE" ]] || abort "$FILE 已存在;停止,未覆盖。"
[[ -f /etc/fstab ]] || abort '/etc/fstab 不存在,停止。'
if awk -v p="$FILE" '$0 !~ /^[[:space:]]
#/ && $1==p {found=1} END {exit !found}' /etc/fstab; then
abort '/etc/fstab 已存在该路径的条目;先人工核对。'
fi
if swapon --show=NAME --noheadings --raw | grep -Fxq "$FILE"; then
abort '该路径已经是活动 Swap;停止。'
fi
available=$(df -B1 --output=avail / | awk 'NR==2 {print $1}')
[[ "$available" =~ ^[0-9]+$ ]] || abort '无法判断剩余空间。'
need=$(( (SIZE_MIB + RESERVE_MIB) * 1024 * 1024 ))
(( available >= need )) || abort '示例要求至少 6 GiB 可用空间,创建后保留至少 4 GiB;业务还需更多空间时请停止。'
printf '检查通过:%s;新文件 %s;计划大小 2 GiB。\n' "$fs" "$FILE"
printf '当前 Swap(已有 Swap 不会被关闭或覆盖):\n'
swapon --show
} create_swap() {
printf '即将创建并启用 %s;不修改任何已有 Swap。\n' "$FILE"
umask 077
# excl 确保检查之后若同名路径出现,dd 仍拒绝覆盖。
dd if=/dev/zero of="$FILE" bs=1M count="$SIZE_MIB" conv=excl status=progress
chmod 600 "$FILE"
mkswap "$FILE"
swapon "$FILE"
swapon --show
printf '本次仅启用运行时 Swap;先观察负载,开机启用另按正文操作。\n'
} main() {
[[ $# -le 1 ]] || abort '用法:sudo bash create-swap-example.sh [--apply]'
case "${1:-}" in
'') check_environment; printf '仅完成检查,未创建文件、未启用 Swap。\n' ;;
--apply) check_environment; create_swap ;;
*) abort '未知选项;只接受 --apply,或不带参数仅检查。' ;;
esac
}
main "$@"
BWH_SWAP_SCRIPT

先阅读已保存的脚本,再执行检查

less scripts/create-swap-example.sh
sudo bash scripts/create-swap-example.sh

确定需要新增 2 GiB Swap 后,才执行写入操作

sudo bash scripts/create-swap-example.sh --apply
swapon --show
free -h

脚本实际执行顺序是 dd → chmod 600 → mkswap → swapon。只有显式传入 --apply 才会创建文件;任何一步报错就停止,不会继续格式化其他磁盘或自动清理文件。执行前请先把内存压力降下来,并确认这段额外 I/O 不影响业务。 [16]

看到 swapon --show 中出现该文件,说明这次运行已经启用;它还不代表重启后一定可用。若中途失败,先确认文件状态与错误原因,不要改成更危险的磁盘设备路径重试。

3. 确认运行正常,再设置开机启用

先备份开机挂载配置

sudo cp -a /etc/fstab "/etc/fstab.before-bwh-swap.$(date +%Y%m%d-%H%M%S)"
sudoedit /etc/fstab

在已有配置末尾增加下面一行;如果已经有相同路径的条目,不要重复添加。其他分区和原有 Swap 条目保持不变。

新增的 /etc/fstab 条目

/swapfile-bwh none swap sw,nofail 0 0

检查配置并重新读取 systemd 挂载信息

sudo findmnt --verify --verbose
sudo systemctl daemon-reload
swapon --show

nofail 用于避免缺少该文件时阻断正常启动,并不是文件丢失后还能继续提供 Swap。检查有报错就先修复;不要为了验证一条开机配置贸然重启生产机。等维护窗口安排受控重启后,再检查 swapon --show。 [17]

五、swappiness、清缓存和删除 Swap,别急着一起改

先只读取当前值

sysctl vm.swappiness

swappiness 影响交换与文件页回收之间的相对取舍,不是“内存用了多少百分比才开始使用 Swap”。设为 0 也不等于彻底禁止交换。先保留现有设置观察;有必要时再做单项调整,并记录原值。 [7]

也不建议把 drop_caches 写成定时“释放内存”的任务。它不能修复应用泄漏,反复清理有用缓存反而可能带来额外 I/O。类似地,不要一边内存紧张,一边执行 swapoff -a 试图“清空 Swap”。 [7][6]

以后不需要这个文件时怎么处理?

先确认负载已经降低、有足够物理内存接回页面,再只停用本文创建的文件:

仅停用本文的新文件,不停用其他 Swap

sudo swapoff /swapfile-bwh

命令成功后,通过 swapon --show 确认该文件已不再活动,再备份并删除 /etc/fstab 中对应的那一行,执行 systemctl daemon-reload,最后删除文件。停用失败时不要继续删文件;内存不足本身就可能导致 swapoff 失败。 [6]

六、Docker 容器 OOM,先检查限额

只读检查容器用量和退出状态

docker stats --no-stream
docker inspect --format 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}} Memory={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}}' your-container

把 your-container 换成实际名称。退出码 137 只能作为进程被强制终止的线索,不能单独认定为 OOM;要结合 OOMKilled、相应时间的日志和重启记录。容器内的 free 也不一定能准确表达该容器获准使用的 Swap 上限。 [8]

Docker memory 与 memory-swap 参数的总额计算示例
两个参数不能相加理解为额外获得的物理内存。

--memory-swap 的正值表示 内存与 Swap 的总额,不是额外 Swap。比如内存上限 512 MiB,总额 1 GiB,最多还有 512 MiB Swap 空间可用;前提是 VPS 本身有 Swap 且内核支持相关限制。两个值相等时,该容器不能使用 Swap。 [8]

合并到现有 Compose 对应服务下;只是字段示例,不是完整部署文件

services:
  worker:
    mem_limit: 512m
    memswap_limit: 1g

这里的数值只是演示含义,不能给所有数据库或站点照抄。worker 应替换为现有服务名,原有镜像、挂载卷、端口等配置不能丢。修改后先用 docker compose config --quiet 检查,再在维护窗口应用;重建容器会中断服务,数据必须持久化。 [9]

直接降低一个正在大量占用内存的容器上限,也可能立即造成回收压力或 OOM。应先降低业务并发,再逐步调整;多个容器的预算合计还要给系统和容器引擎留余量。

不是 Docker,而是 systemd 管理的服务

查看具体服务的资源限制;服务名须替换

systemctl show your-app.service \
  -p MemoryCurrent -p MemoryHigh -p MemoryMax -p MemorySwapMax -p Result

在支持这些内存控制的环境中,MemoryHigh 主要触发回收和节流,MemoryMax 是硬边界。VPS 有空闲内存而应用被杀时,应核查该服务及父级限制,不能仅扩大 Swap。 [14]

七、WordPress、PHP 和数据库怎么减轻压力

WordPress PHP-FPM 并发内存预算示例
数字仅用于解释估算方法,不是本次实测,也不是通用配置。

小内存机器同时放面板、PHP、数据库、Redis 和多个站点,很容易在导入、备份或访问峰值时耗尽余量。先核对最近加的插件、后台任务和并发变化,比直接把所有缓存调大更有效。

PHP-FPM:限制并发,而不是无限加 worker

pm.max_children 控制 PHP-FPM 子进程数量上限,也限制同时处理的请求数。先估算可以留给 PHP 的内存,再结合忙时 worker 的实际占用设置并发,调整后要带载观察。 [10]

假设在一台 2 GiB VPS 上,扣除系统、数据库和必要余量后,愿意给 PHP 768 MiB;忙时每个 worker 粗略占 96 MiB,则 768 ÷ 96 = 8 只是粗略上界。可以先从更保守的并发试起,并检查排队延迟。RSS 存在共享页重复计数,这个计算不是精确容量规划,更不是所有 WordPress 都适用的配置。

pm.max_requests 可让 worker 处理一定数量请求后重建,有时能缓解第三方库导致的持续增长,但不能代替查清泄漏原因。修改 FPM 配置后,应先使用对应版本的配置检查,再在维护窗口重载;不统一写死 PHP 版本和服务名。 [10]

应用内存上限:先查请求本身

WordPress 的 WP_MEMORY_LIMIT、WP_MAX_MEMORY_LIMIT 与 PHP 的 memory_limit 是应用层限制。它们不是 VPS 的物理内存配额,也不保证实际 PHP 配置允许提高到所填数值。把每个请求的上限随意抬高,在并发增加时可能放大整机内存风险。 [11][12]

数据库与缓存:别把“专用数据库”的经验套到混合机器

MySQL 的 InnoDB Buffer Pool 会占用内存。网站、PHP 和数据库共用一台 VPS 时,不应把面向专用数据库服务器的比例直接照搬;还要为连接、其他内部缓冲和同机服务留空间。先查用量与峰值连接,再决定是否调整。 [13]

整页缓存、降低后台任务并发和错峰执行导入,可以作为优化方向。购物车、结账页和用户私有页面不能无差别缓存。若最近安装某个插件后内存持续增长,先在测试环境复现,再做停用或替换,不要在生产库里直接删除数据。

八、怎样判断已经恢复,什么时候该扩容

内存故障恢复后应核查的四项结果
覆盖一轮真实业务高峰后,再决定 Swap 大小和套餐资源是否合适。

恢复后重复前面的只读采样,并覆盖一轮真实业务高峰。至少同时检查 available、si/so、服务重启记录、OOM 日志和实际请求响应。没有固定的“available 大于多少就绝对安全”;阈值需要和这台机器平时的基线、突发负载一起比较。

如果只是一次导入碰到峰值,结束任务、降低并发并补一点 Swap 可能就够;如果每天都出现频繁换页、响应明显变慢或重复 OOM,应优先增加物理内存、拆分服务或修复应用。延长卡顿时间不算解决问题。

选择套餐时,先看需要多少内存,再看线路与预算。不要以为线路等级更高,1 GB 内存就能承载与 4 GB 相同的应用。轻量项目的定位可参考 搬瓦工 Basic VPS 深度介绍与适用场景;这篇不使用未经实测的访问量上限,也不把加 Swap 当成扩容替代品。

需要一次收集排查信息时,可把下面脚本保存为 scripts/memory-readonly.sh。它只做简短的内存、Swap、进程和日志采样,不修改系统。进程名称、主机信息与日志仍可能包含业务细节,分享前请先脱敏。展开只读排查脚本

保存脚本;之后按需运行

mkdir -p scripts
cat > scripts/memory-readonly.sh <<'BWH_MEMORY_SCRIPT'
#!/usr/bin/env bash
# 只读采样:不创建 Swap,不结束进程,不修改服务和防火墙。
set -u
export LC_ALL=Csection() {
printf '\n===== %s =====\n' "$1"
} run() {
local status=0
if ! command -v "$1" >/dev/null 2>&1; then
printf '[skip] command not installed: %s\n' "$1"
return 0
fi
if command -v timeout >/dev/null 2>&1; then
timeout 12s "$@" || status=$?
else
"$@" || status=$?
fi
if (( status != 0 )); then
printf '[notice] command returned %s; permissions or runtime may be limited.\n' "$status"
fi
return 0
} show_kernel_events() {
if ! command -v journalctl >/dev/null 2>&1; then
printf '[skip] journalctl is not installed.\n'
return 0
fi
# 同时保留 journalctl 自身的错误,避免把“无权限”误认为“无 OOM”。
local output
output=$(journalctl -k -b --since '24 hours ago' --no-pager -n 250 2>&1)
local status=$?
if (( status != 0 )); then
printf '%s\n' "$output"
return 0
fi
printf '%s\n' "$output" | grep -Ei 'out of memory|oom-kill|killed process|permission|no entries|no journal'
|| printf 'No matching OOM entry in this bounded sample; this is not proof that no OOM occurred.\n'
} show_docker() {
section 'Optional Docker snapshot (read-only)'
run docker stats --no-stream --format '{{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}'
run docker ps -a --format '{{.Names}}\t{{.Status}}'
printf 'For a specific container, inspect State.OOMKilled, ExitCode and memory limits separately.\n'
} main() {
local include_docker=0
case "${1:-}" in
'') ;;
--docker) include_docker=1 ;;
-h|--help)
printf 'Usage: bash memory-readonly.sh [--docker]\nRead-only; output may contain host and process names. Sanitize before sharing.\n'
return 0 ;;
*) printf 'Unknown option: %s\n' "$1" >&2; return 2 ;;
esac
(( $# <= 1 )) || { printf 'Too many arguments.\n' >&2; return 2; }
section 'Time / kernel'
run date -Is
run uname -sr
section 'RAM and Swap'
run free -h
run swapon --show --bytes
section 'Memory pressure'
if [[ -r /proc/pressure/memory ]]; then
run cat /proc/pressure/memory
else
printf '[skip] PSI memory information is unavailable.\n'
fi
section 'Five short interval samples (first row omitted)'
run vmstat -y 1 5
section 'Largest RSS processes; RSS is KiB, argv/env are not collected'
if command -v ps >/dev/null 2>&1; then
ps -eo pid,ppid,user,comm,rss,pmem --sort=-rss | head -n 21
fi
section 'Kernel OOM events, current boot, bounded sample'
show_kernel_events
section 'systemd-oomd log, if installed'
run journalctl -u systemd-oomd --since '24 hours ago' --no-pager -n 40
section 'Root filesystem and free space'
run findmnt -no FSTYPE,SOURCE -T /
run df -h /
run df -i /
if (( include_docker )); then
show_docker
fi
printf '\nRead-only collection completed. No service or Swap configuration was changed.\n'
}
main "$@"
BWH_MEMORY_SCRIPT

运行只读采样脚本

bash scripts/memory-readonly.sh需要附带 Docker 用量时再加此参数
bash scripts/memory-readonly.sh --docker

常见问题

free 很小,是否一定要增加内存?

不一定。先看 available、交换活动和业务响应。文件缓存可以占用空闲内存,不能只凭 free 一列判断不足。增加 Swap 会提升网站速度吗?

它主要提供短时内存缓冲。频繁换页可能增加磁盘等待,并不能代替物理内存或修复应用问题。1 GB 内存的 VPS 应该设置多少 Swap?

没有通用倍数。结合现有 Swap、磁盘余量和业务峰值决定;本文 2 GiB 只是操作示例,不是所有套餐的推荐值。主机有空闲内存,容器为什么还会 OOM?

容器或其父级可能有独立内存上限。检查容器状态、限额和日志,不能只看整机 free 输出。容器退出码 137 能证明 OOM 吗?

不能。它也可能与其他强制终止有关。应结合 OOMKilled、同一时间的日志和事件判断。已经有 Swap,还需要重建一个文件吗?

不需要因为看到教程就重建。先确认现有容量、路径和使用情况;不要覆盖活动 Swap 或重复写入开机配置。设置完成后需要立即重启吗?

不需要用重启来启用刚创建的 Swap,swapon 可在运行时启用。持久化配置先检查,重启验证安排在维护窗口。

相关阅读

微信订阅号

VPS收割者微信公众号

QQ群(全员禁言)

VPS收割者QQ群(全员禁言)