Ubuntu服务器内存溢出通常不是突然发生的,而是由内存泄漏、进程异常增长、缓存设计不合理或配置错误累积而成。运维人员最常遇到的场景是:服务器运行一段时间后响应变慢,SSH登录困难,dmesg或/var/log/syslog中出现"Out of memory: Killed process"记录,甚至整机触发OOM Killer强制终止进程。要有效防护,必须从监控、限制、内核参数、应用层和应急预案五个层面同时下手,而不是等内存耗尽后再处理。

先确认系统当前的内存压力来自哪里

排查内存溢出风险的第一步是定位内存消耗大户。使用free -h查看内存与交换分区的整体使用情况,注意available列比free列更能反映实际可用内存。执行ps aux --sort=-%mem | head -20可以按内存占用率列出前20个进程。如果发现某个进程的RSS持续增长且不回落,基本可以判断存在内存泄漏。对于Java类应用,还需要用jstat -gcutil查看堆内存和GC频率;对于MySQL或PostgreSQL,要检查innodb_buffer_pool_size或shared_buffers是否设置过大,挤占了系统其余内存。不要忽略tmpfs挂载点,/dev/shm和/run如果被写入大量临时文件,同样会消耗物理内存。

用systemd限制关键服务的内存使用上限

Ubuntu 16.04之后默认使用systemd管理服务,可以通过MemoryMax和MemoryHigh两个指令为服务设置内存配额。MemoryHigh是软限制,当服务内存使用超过该值时,内核会开始回收该服务的内存,但不会立即杀死进程;MemoryMax是硬限制,一旦触及会触发OOM Killer终止该服务。编辑服务单元文件或使用systemctl edit创建覆盖配置:

[Service]
MemoryHigh=1G
MemoryMax=1.5G

对于Nginx、PHP-FPM这类容易出现内存波动的服务,设置MemoryHigh可以促使它们在压力高峰时主动释放缓存和连接,而不是直接崩溃。修改后执行systemctl daemon-reload并重启服务生效。需要注意,MemoryMax设置过低会导致服务频繁被OOM Killer杀死,反而影响可用性,建议根据服务正常峰值的1.2到1.5倍来设定。

调整内核OOM行为,避免误杀关键进程

Linux内核在内存耗尽时会根据oom_score选择进程终止。每个进程的oom_score越高,越容易被选中。可以通过/proc/PID/oom_score查看当前分数,通过/proc/PID/oom_score_adj手动调整。对于数据库、SSH等关键服务,建议降低其oom_score_adj值,例如设置为-500,使它们更不容易被OOM Killer选中。对于已知可能内存泄漏的辅助进程,可以调高其oom_score_adj,让系统优先终止它们。执行命令:

echo -500 > /proc/$(pgrep -f mysqld)/oom_score_adj

更彻底的做法是设置vm.panic_on_oom=0保持默认,让OOM Killer继续工作,同时通过vm.overcommit_memory=2和vm.overcommit_ratio=80限制内存过量分配。这样系统在内存承诺超出物理内存一定比例时会直接拒绝新的内存申请,而不是等到真正耗尽才处理。该参数适合内存资源紧张且不希望系统进入不可控状态的场景,但需要谨慎调整,否则可能导致程序无法正常分配内存。

配置交换分区作为缓冲,但不要依赖它救命

很多Ubuntu服务器在云环境中默认没有启用swap,这会让内存在压力下直接触发OOM。建议创建2GB到4GB的交换文件作为缓冲。执行以下命令创建并启用swap文件:

fallocate -l 4G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab

同时调整vm.swappiness值。Ubuntu桌面版默认swappiness为60,服务器建议设置为10到20,降低内核将内存页交换到swap的倾向,让系统优先使用物理内存,只有在接近耗尽时才使用swap。执行sysctl vm.swappiness=10并写入/etc/sysctl.conf。如果服务器内存非常充足,比如64GB以上,可以保持swappiness=1,让swap仅作为最后的应急手段。但完全关闭swap并不推荐,因为某些应用在分配大块连续内存时仍需要swap作为后备。

用cgroup v2实现更细粒度的内存隔离

Ubuntu 22.04及更新版本默认启用cgroup v2,这为内存防护提供了比systemd MemoryMax更灵活的机制。可以针对一组进程创建独立的cgroup,并设置memory.max和memory.high。例如为某个用户的所有进程限制总内存不超过8GB:

mkdir -p /sys/fs/cgroup/user_limit
echo "8G" > /sys/fs/cgroup/user_limit/memory.max
echo "6G" > /sys/fs/cgroup/user_limit/memory.high
echo "PID" > /sys/fs/cgroup/user_limit/cgroup.procs

这种方式特别适合多租户环境或需要隔离不同业务进程的服务器。当cgroup内进程总内存达到memory.high时,内核会开始节流和回收;达到memory.max时,会触发该cgroup内部的OOM,而不会影响系统其他部分。对于容器化部署,Docker和Podman在底层也是通过cgroup限制内存,运维人员可以在启动容器时直接使用--memory和--memory-swap参数,比在宿主机层面处理更安全。

监控内存趋势,提前发现泄漏苗头

内存溢出防护不能只靠事后限制,必须建立持续监控。使用Prometheus配合node_exporter采集内存指标,重点关注node_memory_MemAvailable_bytes的下降趋势和node_memory_SwapFree_bytes的变化。如果可用内存在业务低峰期仍然持续下降,说明存在泄漏。对于进程级监控,可以配置process-exporter或自定义脚本每分钟记录关键进程的RSS,当某个进程RSS在24小时内增长超过30%且没有回落时触发告警。简单的脚本示例如下:

#!/bin/bash
while true; do
  ps -eo pid,comm,rss --sort=-rss | head -10 >> /var/log/mem_top.log
  sleep 60
done

配合logrotate控制日志大小,避免监控脚本自身造成磁盘问题。对于Java应用,还要监控GC日志中Full GC的频率和耗时,如果Full GC间隔越来越短且每次回收后堆内存仍接近上限,说明堆配置不合理或存在内存泄漏,需要结合heap dump分析对象引用链。

应用层防护:优化配置减少不必要的内存消耗

很多内存溢出问题源于应用配置不合理。Nginx的worker_connections设置过高会预留大量内存,每个连接大约占用0.5KB到1KB,如果设置为100000而实际并发只有几千,就是浪费。PHP-FPM的pm.max_children直接决定最大进程数,每个进程可能占用30MB到80MB,设置过大必然导致内存耗尽。MySQL的performance_schema默认开启会占用数百MB内存,如果不需要可以关闭。Redis的maxmemory必须设置,并配合maxmemory-policy决定淘汰策略,否则数据量增长会无限制占用内存。这些配置调整需要结合业务实际负载,而不是简单调小,否则会牺牲性能换稳定性。

应急预案:内存即将耗尽时的操作顺序

即使做了防护,也要准备内存压力突增时的处理流程。当监控显示可用内存低于10%且swap使用率超过80%时,按以下顺序操作:先通过systemctl stop或kill -TERM停止非核心服务,例如定时任务、日志收集agent、辅助分析进程;如果内存继续下降,重启内存占用最高的无状态服务,如Nginx或PHP-FPM,释放其缓存和连接;最后才考虑重启数据库,因为数据库重启会导致短暂的不可用。执行kill时优先使用SIGTERM让进程有机会清理资源,而不是直接kill -9。同时记录OOM Killer日志,分析/var/log/kern.log中的"Out of memory"条目,找出被终止的进程和触发时的内存分布,为后续调整提供依据。

定期压力测试验证防护效果

防护配置是否有效,需要通过模拟内存压力来验证。可以使用stress-ng工具制造内存负载:

stress-ng --vm 4 --vm-bytes 2G --timeout 60s

该命令会启动4个进程,每个尝试分配2GB内存,观察系统在压力下的表现:OOM Killer是否按预期终止了正确的进程,systemd限制是否生效,swap是否正常启用,监控告警是否及时触发。测试应在非生产环境或业务低峰期进行,避免影响正常服务。通过测试可以校准MemoryMax、oom_score_adj和cgroup限制的具体数值,确保真实内存溢出时系统行为符合预期,而不是在事故中才发现配置错误。