拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PHP8.5配置Docker容器资源限制怎么设置

PHP8.5配置Docker容器资源限制怎么设置

前言

先说清楚版本关系:Docker 容器的资源限制由内核 cgroup、Docker 引擎和 docker-compose.yml 决定,PHP-FPM 的进程池由www.conf决定,两者都与 PHP 的语言版本无关。PHP 8.5 并没有新增任何容器资源限制配置项。本文之所以仍以 8.5 为例,是因为 8.5 已经发布(2025 年 11 月 20 日),线上新项目大多在用它,而它在容器里确实有两个值得注意的差异点:致命错误默认带回溯(fatal_error_backtraces),以及 JIT / opcache 的常驻内存必须计入容器限额的预算。

容器里 PHP 出问题时的症状很有辨识度,但也特别容易被误判。第一种是容器周期性重启,docker ps显示退出码 137,而 PHP 自己的error_log里干干净净——没有任何"内存不足"的提示,因为进程是被内核直接杀掉的。第二种是接口偶发 502,nginx 日志里写着connect() to unix:/run/php-fpm.sock failed (11: Resource temporarily unavailable),看起来像 PHP 崩了,实际是 FPM 的进程池被占满、请求在队列里溢出了。第三种最迷惑:改小了memory_limit之后容器确实不重启了,但接口开始报Allowed memory size exhausted,业务侧以为是代码问题,其实是三个数字没对齐。

本文把"容器 cgroup 限制、PHP 的memory_limit、FPM 的pm.max_children"这三个数字的关系讲清楚,给出可复制的 compose 与www.conf配置,以及区分"被 OOMKill"和"PHP 报内存超限"的排查手段。

一、三个数字的分工,以及它们必须满足的不等式

层级配置项作用域超限后的表现
内核 / Docker(cgroup)mem_limit、--memory、pids_limit整个容器(含 nginx、日志缓冲、共享内存)进程被 SIGKILL,退出码 137,无 PHP 日志
PHP INImemory_limit单个 PHP 进程的堆内存PHP Fatal error: Allowed memory size ... exhausted,可捕获可记录
PHP-FPMpm.max_children进程池的并发上限请求排队,队列溢出后 nginx 返回 502

必须满足的不等式是这条:

pm.max_children × memory_limit + 共享内存(opcache 等) + 预留 < 容器内存上限

举个最常见的翻车例子:容器内存给 1G,pm.max_children = 20,memory_limit = 256M。20 × 256M 已经是 5G,理论上永远轮不到 PHP 自己报"内存不足"——只要并发上来,内核会先把进程杀掉。此时你看到的是 137 和重启,而不是一条能定位的致命错误。

反过来也要成立:memory_limit不能大于容器的可用内存。否则 PHP 永远来不及抛出可捕获的致命错误,问题就从"能记录、能告警、能定位"退化成"进程凭空消失"。

第三个数是pm.max_children,它决定了并发承载能力。设小了,请求在 FPM 的监听队列里排队,队列满了 nginx 直接 502;设大了,就是上面的 OOM。它必须由实测的单进程峰值倒推,不能凭感觉。

二、Docker 侧怎么写

# docker-compose.yml —— Compose V2 services: php: image: php:8.5-fpm-alpine deploy: resources: limits: cpus: "2.0" memory: 1g reservations: cpus: "0.5" memory: 256m # 传统字段;与 deploy.resources.limits 同时存在时以前者为准,实际项目里二选一 mem_limit: 1g memswap_limit: 1g # 与 mem_limit 相等 = 禁用 swap pids_limit: 256 # 兜底,防止进程数爆炸 ulimits: nofile: soft: 65535 hard: 65535 environment: TZ: Asia/Shanghai volumes: - ./conf/php.ini:/usr/local/etc/php/conf.d/zz-app.ini:ro - ./conf/www.conf:/usr/local/etc/php-fpm.d/zz-app.conf:ro - ./src:/var/www/html healthcheck: test: ["CMD", "php-fpm", "-t"] interval: 30s timeout: 5s retries: 3

对应的docker run参数是这样映射的:

compose 字段docker run参数作用
mem_limit: 1g--memory=1g容器内存上限
memswap_limit: 1g--memory-swap=1g内存 + swap 的总上限,等于--memory表示禁用 swap
cpus: "2.0"--cpus=2.0CPU 配额(CFS 带宽)
pids_limit: 256--pids-limit=256容器内最大进程 / 线程数
ulimits.nofile--ulimit nofile=65535:65535文件句柄上限

memswap_limit这一项最容易被忽略:如果只设--memory=1g而不设--memory-swap,容器还能再用一份等量 swap,实际可用内存变成 2G;在 swap 慢速的设备上,这会让"内存不足"表现为长时间卡顿而不是干脆被杀掉,更难排查。要禁用 swap 就显式设成与内存相等。

三、PHP-FPM 侧怎么和容器对齐

FPM 的进程池配置(www.conf)需要显式设置pm.max_children,并配上与容器匹配的资源约束:

; conf/www.conf —— 与上面的容器限制对齐 [www] user = www-data group = www-data listen = 0.0.0.0:9000 listen.backlog = 511 ; 队列长度;太小会在并发时直接 502 ; 固定进程数在容器里比 dynamic 更可预测,内存占用等于 max_children × 单进程常驻 pm = static pm.max_children = 12 pm.max_requests = 500 ; 定期回收,防止内存碎片与缓慢泄漏累积 request_terminate_timeout = 30s ; FPM 强制杀掉跑飞的 worker request_slowlog_timeout = 5s slowlog = /var/log/php-fpm/slow.log rlimit_files = 65535 ; 输出到容器的 stdout/stderr,交给 docker logs 收集 catch_workers_output = yes decorate_workers_output = no php_admin_value[error_log] = /proc/self/fd/2 php_admin_flag[log_errors] = on ; 状态页,供健康检查与监控使用 pm.status_path = /_fpm_status ping.path = /_fpm_ping

PHP 的 INI 配置:

; conf/php.ini —— 挂载到 conf.d/zz-app.ini memory_limit = 128M max_execution_time = 30 error_reporting = E_ALL display_errors = Off log_errors = On ; opcache 的共享内存是所有子进程共享的一份,只计一次,别乘以 max_children opcache.enable = 1 opcache.memory_consumption = 128 opcache.max_accelerated_files = 20000 opcache.validate_timestamps = 0 ; PHP 8.5 起致命错误默认带回溯,日志量大时可以关掉以省一点开销 fatal_error_backtraces = Off

nginx 侧只需要把请求转给 FPM,并把状态页挡在内网:

location ~ \.php$ { include fastcgi_params; fastcgi_pass php:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name; fastcgi_read_timeout 35s; # 要比 request_terminate_timeout 略大 } location = /_fpm_status { allow 172.16.0.0/12; deny all; include fastcgi_params; fastcgi_pass php:9000; fastcgi_param SCRIPT_NAME /_fpm_status; }

四、max_children 应该填多少:用实测值倒推

不要凭感觉,也不要照抄博客。用历史的峰值常驻内存(RSS)来算——Linux 内核已经帮每个进程记好了这个数:读进程的 status 文件里的VmHWM字段即可(脚本见下)。

# 在有代表性流量之后执行,读取每个 FPM worker 的历史峰值 RSS for pid in $(pgrep -f 'php-fpm: pool www'); do printf 'pid %s ' "$pid" grep VmHWM "/proc/$pid/status" done

拿到单进程峰值后,用下面这个脚本算建议值。注意memory_limit要大于实测峰值(留出余量,让 PHP 有机会报可捕获的致命错误),而max_children × memory_limit要小于容器内存。

<?php // calc_pool.php —— 需 PHP 8.0+,在宿主机或容器里用 CLI 跑 declare(strict_types=1); $containerBytes = 1024 ** 3; // 容器内存上限 1 GiB $peakPerChild = (int) ($argv[1] ?? 60 * 1024 * 1024); // 实测单进程峰值,默认 60 MiB $opcacheShared = 128 * 1024 * 1024; // opcache 共享内存,只计一次 $reserveRatio = 0.15; // 预留给日志、nginx、内核缓存 $safety = 1.5; // memory_limit 相对峰值的余量系数 // 1) 先决定 memory_limit:比实测峰值高,且不能超过容器的可用内存 $usable = (int) ($containerBytes * (1 - $reserveRatio)) - $opcacheShared; $memoryLimit = (int) (ceil($peakPerChild * $safety / 1048576) * 1048576); // 2) 再让 max_children × memory_limit 落在可用内存之内 $maxChildren = max(1, intdiv($usable, $memoryLimit)); printf("容器内存上限 : %6.0f MiB\n", $containerBytes / 1048576); printf("opcache 共享内存 : %6.0f MiB\n", $opcacheShared / 1048576); printf("可用给进程池 : %6.0f MiB\n", $usable / 1048576); printf("实测单进程峰值 : %6.0f MiB\n", $peakPerChild / 1048576); printf("建议 memory_limit : %6d MiB\n", $memoryLimit / 1048576); printf("建议 max_children : %6d\n", $maxChildren); printf("预算占用 : %6.0f MiB(应小于可用值)\n", ($memoryLimit * $maxChildren) / 1048576);

输出示例(数值仅供演示算法,实际请用你自己的实测值):

容器内存上限 : 1024 MiB opcache 共享内存 : 128 MiB 可用给进程池 : 742 MiB 实测单进程峰值 : 60 MiB 建议 memory_limit : 90 MiB 建议 max_children : 8 预算占用 : 720 MiB(应小于可用值)

算完之后一定要压测验证:按目标 QPS 打一轮,同时观察docker stats与容器的memory.events。算法只能给出起点,真实的峰值取决于业务里最大的那次查询、最大的一张图片、最深的一次递归。

五、区分"被 OOMKill"和"PHP 报内存超限"

这是排查时最该先做的一步,因为两者的应对动作完全相反:前者要减并发或加限制,后者要改代码或加memory_limit。

观测项被 cgroup OOMKillPHP 触及memory_limit
PHP 错误日志没有内存相关记录有Allowed memory size of N bytes exhausted
容器退出码137(128 + SIGKILL 9)通常 255 或被 FPM 记为 worker 异常退出
触发统计cgroup 的oom_kill计数增加PHP 自己抛出致命错误
覆盖范围整容器(含 nginx、日志、共享内存)仅单个 PHP 进程的堆
首要动作降pm.max_children或加容器内存加memory_limit或优化代码

对应的命令:

# 1) 容器是否被 OOM 杀掉、退出码是多少 docker inspect --format 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}' php # 2) cgroup v2 的事件计数:oom_kill 不为 0 就说明有进程被内存杀过 docker exec php cat /sys/fs/cgroup/memory.events # 3) 当前限制与历史峰值 docker exec php cat /sys/fs/cgroup/memory.max docker exec php cat /sys/fs/cgroup/memory.peak # 4) 实时占用(注意这是瞬时值,峰值看不到) docker stats --no-stream php # 5) 从宿主机侧看内核的 OOM 记录 dmesg | grep -i -E 'oom|killed process'

一个常见误判是"docker stats里内存用了不到一半,所以不会有 OOM"——docker stats显示的是采样瞬间的值,而 OOM 往往发生在某个大请求的峰值时刻,几秒钟就结束了。判断有没有发生过 OOM,唯一可靠的依据是memory.events里的oom_kill计数和State.OOMKilled。

常见坑点


  • ❌pm.max_children用模板里的默认值或照抄博客,同时把memory_limit设成 256M


✅ 先算不等式:max_children × memory_limit + 共享内存 + 预留 < 容器上限。容器 1G 时,max_children通常是个位数,不是几十


  • ❌ 容器mem_limit比 PHP 的memory_limit还小


✅ 这样 PHP 永远来不及报可捕获的致命错误,进程会被 SIGKILL,问题从"能告警、能定位"退化成"凭空消失"。memory_limit必须小于容器可分给单个进程的上限


  • ❌pm.max_children设得过小、listen.backlog也用默认值


✅ 会看到 nginx 日志里的connect() to ... failed (11: Resource temporarily unavailable),用户侧就是 502。这个报错的意思是"FPM 来不及接连接",不是 PHP 崩了


  • ❌ 只设--memory=1g,不管--memory-swap


✅ 此时容器还能再用一份等量 swap,内存问题的表现会从"干脆被杀"变成"长时间卡顿"。要禁用 swap,把memswap_limit设成与mem_limit相等


  • ❌ 容器被限到 1 核,却不管镜像里库的线程数


✅ 部分扩展与图像处理库按宿主机的逻辑核数创建线程,配额只有 1 核时线程争抢反而更慢。用--cpuset-cpus限定核,或设置OMP_NUM_THREADS之类的环境变量对齐


  • ❌ 只设内存限制,不设pids_limit与ulimits.nofile


✅ 进程或句柄耗尽时报的是fork: Cannot allocate memory、Too many open files,和内存问题长得完全不一样。容器里这两个必须显式兜底


  • ❌ 用--oom-kill-disable来"避免容器被杀"


✅ 这会让最该被回收的进程活得最久,把压力转嫁给宿主机。正确做法是降低max_children或提高内存限制


  • ❌ 只设max_execution_time,不设request_terminate_timeout


✅max_execution_time不统计脚本之外的等待时间(数据库查询、流操作、系统调用),慢查询把 worker 挂住时它形同虚设;必须由 FPM 的request_terminate_timeout强制回收

总结

关注点配置位置建议值来源失败表现
容器内存mem_limit/--memory按业务峰值的 1.5~2 倍起步退出码 137、OOMKilled=true
swapmemswap_limit等于mem_limit以禁用卡顿而非重启,更难查
单进程堆上限memory_limit实测峰值 × 1.5,且小于容器可用内存Allowed memory size exhausted
进程池并发pm.max_children(容器内存 × 0.85 − 共享内存) ÷memory_limit排队溢出,nginx 502
进程数兜底pids_limit、ulimits.nofile按 worker 数 × 2~4 起步fork 失败、句柄耗尽
请求强制回收request_terminate_timeout略小于 nginx 的fastcgi_read_timeoutworker 被挂死、接口超时

资源限制的本质是"用三个互相约束的数字换取可预测性":容器限制是硬墙,memory_limit是软墙,pm.max_children是并发闸门。把它们的先后顺序排对——先量出单进程峰值,再定memory_limit,最后按容器内存倒推max_children——容器里那些"无缘无故重启""偶发 502""内存报错但改代码没用"的问题,绝大多数都能在配置层面定位清楚。

返回列表