一次 Docker OOM 与 Web 应用 Apache 内存问题排查实战
一、故障背景:监控频繁告警,SSH 无法登录
最近,服务器监控软件频繁发出异常告警,表现为服务不定时失联,一段时间后又自动恢复。起初以为是偶发的网络波动,但连续观察后发现,故障会反复出现,而且一旦告警,排查也会变得非常困难。
具体有几个明显特征:
监控频繁告警:服务可用性检测不定时失败,随后又自动恢复正常。
SSH 无法登录:告警期间尝试通过 SSH 连接服务器,往往无法成功,无法及时查看进程和内存状态。
无需人工干预即可恢复:过一段时间网站或后台又能访问,容易误以为只是短暂网络问题。
故障反复发生:不是单次偶发,已经影响了日常运维判断与服务稳定性。
服务器运行 Ubuntu,内存约 4GB、Swap 约 2GB,使用 Docker Compose 同时部署 某 Java Spring Boot 后台应用、某 Web 应用(基于 WordPress)、两个 MySQL、RabbitMQ、Redis 等服务。Java 后台需要长期提供业务服务,Web 应用则面向公网提供页面访问;两者共享同一台小内存服务器,一旦资源紧张就可能相互影响。
真正棘手的是:故障发生时无法 SSH 登录,等重新连上时服务往往已经恢复,实时现场已经消失。 因此,这次不再只看应用日志,而是从 Docker 历史事件、Linux 内核日志和 Web 应用的 Apache 进程入手,还原事故发生时的状态。
最终确认,2026 年 10 月 13 日服务器发生过一次宿主机全局 OOM(Out of Memory):Linux 内核杀死了 Java 进程,Docker 随后依据 restart: always 策略自动拉起 java-app。事故现场同时出现了大量 Web 应用 Apache 进程(约 138 个),而 mpm_prefork 的工作进程上限为 150。后续将该上限持久化调整为 38,降低 Web 应用在访问高峰时挤占整机内存的风险。
证据边界:已确认发生全局 OOM、Java 被强制终止、Apache 进程大量存在;该 Web 应用是重要的内存压力来源,但现有记录不能证明它是唯一根因。SSH 无法登录与内存枯竭的时间表现一致,但无法仅凭这些日志完全排除网络层面的其他原因。
二、发现 Docker 容器重新启动
查看 Java 服务启动日志:
11:23:36 Starting Application
12:59:54 Flyway migrate start
13:00:42 Started Application in 5832.648 seconds说明应用确实重新启动过,而且这次启动耗时约 97 分钟。启动卡顿是另一个需要调查的问题,不能直接归咎于 Flyway。
查看容器状态:
docker inspect java-app --format '
创建时间: {{.Created}}
启动时间: {{.State.StartedAt}}
退出时间: {{.State.FinishedAt}}
重启次数: {{.RestartCount}}
退出码: {{.State.ExitCode}}
OOMKilled: {{.State.OOMKilled}}
重启策略: {{.HostConfig.RestartPolicy.Name}}
'结果显示容器最近一次于北京时间 11:23:22 左右启动,重启策略是 always。单靠当前 docker inspect 的 ExitCode、OOMKilled 字段不能还原历史每一次退出原因。
接着查看当时的 Docker 事件:
docker events \
--since '2026-10-13T11:15:00+08:00' \
--until '2026-10-13T11:30:00+08:00' \
--filter container=java-app关键事件:
11:23:22 container die exitCode=137
11:23:22 container oom
11:23:24 container start其中 exitCode=137 表示进程遭到 SIGKILL;container oom 则提供了 OOM 的直接证据。容器随后按 always 策略自动重启,没有证据表明这次是手动执行 docker restart。
三、确认是宿主机全局 OOM
查看内核日志:
journalctl -k \
--since '2026-10-13 11:23:10' \
--until '2026-10-13 11:23:30' \
--no-pager核心输出:
oom-kill:constraint=CONSTRAINT_NONE,...,global_oom,...,task=java
Out of memory: Killed process 451165 (java)
Free swap = 244kB
Total swap = 2035708kB这意味着:
global_oom:宿主机整体内存紧张,而不是某个容器设置的独立内存上限触发;内核选择杀死的进程是
java-app内的java;约 2GB Swap 几乎耗尽,说明当时整机内存压力已经非常严重;
被杀的 Java 不一定就是造成压力最大的服务。
与此同时,docker inspect java-app 中的 HostConfig.Memory=0,表示该容器没有设置 Docker 内存硬上限。Java 与 Web 应用、MySQL 等服务共同竞争宿主机内存。
四、定位 Web 应用 Apache 的内存压力
1. 从 OOM 现场发现大量 Apache 进程
完整内核日志的进程快照中出现约 138 个 apache2 进程。其中部分 PID 与以下命令列出的 Web 应用容器进程一致:
docker top web-app因此,可以确认这些 Apache 进程确实有大量属于 Web 应用容器。
要特别注意:内核进程表中的 rss 单位通常是页面数量,例如:
apache2 rss=31198 swapents=1600若系统页面大小是 4 KiB,则这个进程 RSS 约为 122 MiB,Swap 页约为 6.25 MiB。不能将所有子进程的 RSS 简单相加,因为 RSS 可能重复计算共享页面。
2. 检查 Apache 的实际工作模式和配置
docker exec web-app apache2ctl -M | grep mpm结果:
mpm_prefork_module (shared)再查看配置:
docker exec web-app sh -c \
"grep -RinE 'MaxRequestWorkers|StartServers|MinSpareServers|MaxSpareServers|MaxConnectionsPerChild' /etc/apache2 2>/dev/null"事故前配置为:
StartServers 5
MinSpareServers 5
MaxSpareServers 10
MaxRequestWorkers 150
MaxConnectionsPerChild 0在 prefork 模式下,MaxRequestWorkers=150 允许最多 150 个工作子进程同时处理请求。对一台还要运行 Java 和数据库的 4GB 机器来说,这个上限偏高。MaxConnectionsPerChild=0 表示子进程不因累计处理连接数达到阈值而轮换,但空闲子进程仍可能被 Apache 回收。
3. HTTP 请求日志能证明爬虫导致 OOM 吗?
统计事故前 34 分钟的 WordPress 日志:
docker logs web-app \
--since '2026-10-13T10:50:00+08:00' \
--until '2026-10-13T11:24:00+08:00' 2>&1 | awk '
/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ {
ip[$1]++; total++
}
END {
print "总请求数:", total
for (i in ip) print ip[i], i
}' | sort -nr结果共计 102 条已记录请求,其中某来源 IP 有 63 条,还出现 多个搜索引擎与第三方爬虫 等爬虫标识。
这些日志表明存在自动化抓取行为,但102 条已完成请求不能解释 138 个 Apache 进程的全部原因:请求可能在等待或阻塞,部分尚未完成的请求不会即时写入访问日志;已有的子进程也可能在此前创建。因此应结合请求耗时、Apache 状态页和更长时间窗口继续分析。
五、解决方案:把 Apache 最大工作进程限制为 38
考虑两个系统必须共用 4GB 服务器,这次先把 WordPress MaxRequestWorkers 从 150 降到 38。注意:这限制的是工作子进程数,主进程不包括在内,因此正常情况下 Apache 总进程数可能达到约 39 个。
1. 创建宿主机配置文件
Web 应用部署目录:/opt/web-project。
cd /opt/web-project
mkdir -p apache
nano apache/mpm_prefork.conf内容:
<IfModule mpm_prefork_module>
StartServers 2
MinSpareServers 2
MaxSpareServers 5
MaxRequestWorkers 38
MaxConnectionsPerChild 300
</IfModule>这里 MaxConnectionsPerChild=300 使工作进程处理一定数量的连接后轮换,有助于限制长期运行进程的累计内存增长,但不能代替排查 PHP 插件性能问题。
2. 通过 Docker Compose 持久化挂载
编辑 /opt/web-project/docker-compose.yml,在现有 web-app 服务配置中保留数据库连接参数和其他设置,仅向 volumes 增加一行:
services:
web-app:
image: web-app:php8.2-apache
container_name: web-app
restart: always
# 其余原有配置保持不变
volumes:
- ./html:/var/www/html
- ./apache/mpm_prefork.conf:/etc/apache2/mods-enabled/mpm_prefork.conf:ro文中
java-app、web-app、web-mysql和/opt/web-project均为匿名化示例名称,请替换为实际容器名及路径。上述是示意片段,不要直接替换整个 Compose 文件,以免丢掉数据库配置。执行前应备份原文件,并确认当前项目目录确实是/opt/web-project。
3. 应用配置
cd /opt/web-project
docker compose config
docker compose up -d --no-deps --force-recreate web-app该操作会导致 WordPress 短暂中断;--no-deps 避免主动重建其依赖服务。
4. 验证生效
# 语法检查
docker exec web-app apache2ctl -t
# 查看最大工作进程配置
docker exec web-app sh -c \
"grep '^ *MaxRequestWorkers' /etc/apache2/mods-enabled/mpm_prefork.conf"
# 查看 Apache 当前总进程数
docker exec web-app sh -c \
"ps -eo comm | grep -c '^apache2$'"实际结果:
Syntax OK
MaxRequestWorkers 38
4这表明配置文件语法正确、设定为 38,当前只有 4 个 Apache 进程。若容器按挂载后的配置重新启动,则该限制已经加载。AH00558: Could not reliably determine the server's fully qualified domain name 是缺少全局 ServerName 的提示,不影响此次限制生效。
六、上线后监控与进一步优化
监控宿主机可用内存、Swap 和两个核心容器:
watch -n 5 '
free -h
docker stats --no-stream web-app web-mysql java-app
'也可以定期检查 Apache 进程数:
docker exec web-app sh -c "ps -eo comm | grep -c '^apache2$'"后续建议:
评估 Web 应用动态请求压力:分析慢请求、WP-Cron、插件、爬虫访问;可在 Nginx 层缓存静态资源和可缓存页面。
明确 JVM 和容器内存预算:当前
java-app启动命令为java -jar /app.jar,未显式指定-Xmx。在测量运行时堆和堆外使用后再配置,不要把整个容器内存额度全部给堆。检查异常启动耗时:Spring Boot 此次启动约 97 分钟,并出现
UnknownHostException: emqx,需要分别排查内存交换、依赖服务就绪状态和 Docker DNS/网络。保留系统资源余量:4GB 机器上两个系统共存存在容量风险。若 PHP 并发确实需要很高,优先升级内存或拆分 Web 应用,而不是单纯把并发上限重新提高。
七、总结
这次事件的排查链路是:
Java 服务中断
↓
Docker events:die(137) + oom + start
↓
内核日志:global_oom,Swap 几乎耗尽
↓
事故进程快照:大量 apache2 进程
↓
检查 WordPress:prefork / MaxRequestWorkers=150
↓
持久化调整为 MaxRequestWorkers=38
↓
验证 Syntax OK,当前 Apache 进程 4 个核心经验: Docker 的重启策略只能帮助服务自动恢复,不能解决全局内存争抢。排查 OOM 不要只盯着被杀死的进程;应同时检查宿主机内核日志、Swap、其他容器的进程数与实际内存压力。限制 Apache 工作进程上限是有效的风险控制措施,但仍需要监控和资源规划来验证长期稳定性。
