多彩编程 多彩编程MZPH · CODE BLOG
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里干干净净——没有任何内存不足的提示因为进程是被内核直接杀掉的。第二种是接口偶发 502nginx 日志里写着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 报内存超限的排查手段。一、三个数字的分工以及它们必须满足的不等式层级配置项作用域超限后的表现内核 / Dockercgroupmem_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 等) 预留 容器内存上限举个最常见的翻车例子容器内存给 1Gpm.max_children 20memory_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--memory1g容器内存上限memswap_limit: 1g--memory-swap1g内存 swap 的总上限等于--memory表示禁用 swapcpus: 2.0--cpus2.0CPU 配额CFS 带宽pids_limit: 256--pids-limit256容器内最大进程 / 线程数ulimits.nofile--ulimit nofile65535:65535文件句柄上限memswap_limit这一项最容易被忽略如果只设--memory1g而不设--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_pingPHP 的 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 Offnginx 侧只需要把请求转给 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_types1); $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_limitPHP 错误日志没有内存相关记录有Allowed memory size of N bytes exhausted容器退出码137128 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 崩了❌ 只设--memory1g不管--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、OOMKilledtrueswapmemswap_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内存报错但改代码没用的问题绝大多数都能在配置层面定位清楚。
返回列表