php-fpm 튜닝 요령: p99를 좌우하는 5가지 설정

발행: (2026년 5월 24일 PM 05:53 GMT+9)
10 분 소요
원문: Dev.to

Source: Dev.to

Book: Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework
또한 저서: Thinking in Go (2권 시리즈) — Go 프로그래밍 완전 정복 + Go 헥사고날 아키텍처

내 프로젝트: Hermes IDE | GitHub — Claude Code 등 AI 코딩 도구와 함께 배포하는 개발자를 위한 IDE
Me: xgabriel.com | GitHub


당신의 Laravel 애플리케이션이 부하가 걸릴 때 느려집니다. CPU 사용량은 30% 수준이고 메모리는 충분합니다. 데이터베이스도 큰 문제가 없는데, 화요일 아침 러시 동안 p99 응답 시간이 8초까지 급등합니다.

Grafana를 열어보고, 쿼리를 살펴보고, 캐시 히트 비율을 확인하고, Redis 노드를 하나 더 추가해 봅니다. 하지만 상황은 변하지 않습니다.

문제는 코드가 아니라 php-fpm에 있습니다. 정확히는 라즈베리 파이용으로 기본값이 설정된 www.conf 파일의 다섯 줄이 원인입니다.

php-fpm은 워커 프로세스 풀을 운영합니다. 웹 요청이 Nginx에 들어오면, Nginx는 FastCGI를 통해 풀에 전달하고, 워커가 이를 받아 PHP 코드를 실행한 뒤 응답을 반환하고 다음 요청을 기다립니다.

핵심은 몇 개의 워커를 두고, 언제 생성하고, 언제 재활용할 것인가 입니다.

www.conf에서 선택할 수 있는 세 가지 프로세스 관리자를 살펴보세요.

; /etc/php/8.4/fpm/pool.d/www.conf
pm = dynamic
  • pm = static : 워커 수가 고정됩니다. 메모리 사용량이 예측 가능하고, 스폰 지연이 없습니다. 트래픽이 감소하면 RAM을 낭비합니다.
  • pm = dynamic : 최소/최대 범위를 지정해 수요에 따라 자동으로 스케일 업/다운합니다. 대부분의 배포판에서 기본값이며, 경계에서 스폰 지연이 발생합니다.
  • pm = ondemand : 요청이 들어올 때만 워커를 생성하고, 유휴 타임아웃이 지나면 종료합니다. 메모리 사용량이 가장 낮지만, 콜드 요청에 5~50ms 정도 추가 지연이 생깁니다.

실제 트래픽을 처리하는 단일 목적 API 서버라면 static이 거의 항상 최선의 선택입니다. 버스트가 발생했을 때 스폰 비용이 사용자가 체감하는 가장 큰 레이턴시 아웃라이어가 되기 때문입니다. 자세한 내용은 뒤에서 다시 다룹니다.


pm.max_children (대부분의 팀이 놓치는 설정)

이 값은 동시에 실행될 수 있는 PHP 프로세스의 최대 개수를 정의합니다. 너무 낮게 설정하면 요청이 대기열에 쌓이고, 너무 높게 설정하면 커널이 OOM(Out‑Of‑Memory)으로 워커와 Redis 사이드카를 강제 종료시킵니다.

배포판 문서에서 흔히 빠지는 공식:

pm.max_children = (전체 RAM - OS 및 사이드카용 예약 메모리) / 평균 워커 메모리

필요한 두 가지 측정값:

  • avg_worker_memory : 몇 개의 요청을 처리한 뒤 안정된 워커의 실제 RSS. 부팅 시값이 아니라, 오토로더가 클래스를 로드하고, OPcache가 워밍업되며, Laravel이 서비스 컨테이너를 히드레이션한 뒤의 값입니다.
  • reserved RAM : OS 자체, Nginx, 로컬 Redis, OPcache 공유 메모리(기본 128 MB), APM 에이전트 등. 4 GB 서버라면 보통 700 MB~1 GB 정도를 예약합니다.

평균 워커 RSS 측정 예시

# settled RSS per worker, in MB
ps -ylC php-fpm8.4 --sort:rss \
  | awk 'NR>1 { sum+=$8; n++ } END { printf "avg=%.1f MB\n", sum/n/1024 }'
  • 기본 Laravel 12 워커: 50‑80 MB
  • 200개의 라우트와 Telescope, Filament 리소스를 가진 모놀리식: 120‑180 MB
  • 이미지 변환을 많이 하는 워커는 일시적으로 400 MB를 초과할 수 있습니다.

실제 적용 예시 (c5.large EC2)

  • 총 RAM: 4096 MB
  • 예약(RAM): OS + Nginx + Redis + OPcache + 버퍼 ≈ 900 MB
  • php‑fpm에 할당 가능한 RAM: ≈ 3200 MB
  • 측정된 평균 워커 RSS: 140 MB

계산: pm.max_children = 3200 / 140 ≈ 22

pm = static
pm.max_children = 22

흔한 실수

많은 사람들이 “2014년 Stack Overflow 답변”을 그대로 따라 pm.max_children = 50 으로 설정합니다. 23번째 워커가 스와핑을 일으키면서 레이턴시가 급증하고, 엔지니어가 워커 수를 더 늘리지만 상황은 오히려 악화됩니다.

CPU 측면 체크

평균 요청이 CPU 바운드이고 50 ms 이하라면 vCPU × 2‑4 보다 많은 워커를 두는 의미가 없습니다. 스케줄러가 과부하될 뿐이죠. c5.large (2 vCPU) 기준으로는 4‑8 워커가 상한선이 됩니다. RAM 제한과 CPU 제한 중 더 작은 값을 선택하세요.


pm.max_requests (메모리 누수 방지)

PHP 자체는 C처럼 메모리를 “누수”하지 않지만, php‑fpm 워커는 성장합니다. Composer 오토로드, OPcache, 서비스 컨테이너 히드레이션, 사용자 코드에 의한 정적 캐시 등이 누적됩니다. 50 000 요청이 지나면 80 MB였던 워커가 220 MB가 될 수 있습니다.

pm.max_requestsN 요청마다 워커를 재생성하도록 합니다. 워커는 깨끗하게 종료되고, php‑fpm은 새 워커를 스폰합니다.

pm.max_requests = 500
  • 적정값: 재생성 비용이 눈에 띄지 않을 정도로 충분히 높고, 메모리 증가가 무시될 정도로 충분히 낮은 값. 대부분의 애플리케이션은 500‑1000 사이가 적당합니다.
  • PDF 생성, 이미지 처리, 캐시 레이어가 있는 HTTP 클라이언트 등 메모리를 많이 잡아먹는 라이브러리를 사용하는 경우는 하한에 가깝게 설정하고, 가벼운 API 워커는 2000+ 로 올릴 수 있습니다.

주의: 일부 배포판에서는 기본값이 0이며, 이는 “워커를 절대 재생성하지 않는다”는 의미입니다. 0으로 두고 튜닝하지 않았다면 눈에 보이지 않는 메모리 증가가 진행되고 있을 가능성이 높습니다. 최소 2000 정도라도 설정하는 것이 안전합니다.


request_terminate_timeout (실제로 작동하는 타임아웃)

PHP에는 max_execution_time(php.ini), Nginx에는 fastcgi_read_timeout, php‑fpm에는 request_terminate_timeout이라는 세 가지 타임아웃이 존재합니다. 가장 짧은 타임아웃이 우선 적용됩니다.

request_terminate_timeout = 30s

흔한 실수 시나리오

  • 느린 쿼리 실행에 60 s가 걸림
  • Nginx fastcgi_read_timeout = 60s
  • PHP max_execution_time = 30s 이지만, sleep() 혹은 블로킹 소켓 호출 안에서는 타이머가 체크되지 않음 → PHP는 30 s를 넘겨도 종료되지 않음

결과: 워커가 전체 60 s 동안 블로킹되고, 이런 상황이 22번 반복되면 풀 전체가 고정됩니다.

request_terminate_timeout은 php‑fpm 마스터 프로세스가 시그널을 보내 강제로 워커를 종료하므로, 워커가 무엇을 하든 무조건 작동합니다. 고장이 난 워커를 해제하는 가장 확실한 방법이죠.

설정 권장

  1. php.inimax_execution_time (예: 30 s)
  2. www.confrequest_terminate_timeout을 약간 더 크게 (예: 35 s)
  3. Nginxfastcgi_read_timeout을 더 크게 (예: 40 s)
; www.conf
request_terminate_timeout = 35s

; php.ini
max_execution_time = 30

# Nginx server block
fastcgi_read_timeout = 40s;

동작 순서

  • PHP는 30 s에 Maximum execution time exceeded 예외를 발생시켜 스택 트레이스를 남깁니다.
  • php‑fpm은 35 s에 워커를 강제 종료해 풀을 복구합니다.
  • Nginx는 40 s에 클라이언트 연결을 닫
0 조회
Back to Blog

관련 글

더 보기 »

내 스킬

프로젝트를 위한 AI 지시문을 만들고, 설치하고, 관리하세요 — 코딩이 필요 없습니다. CREATE 이름을 정하고, 카테고리를 선택하고, 원하는 것을 설명하세요 — 마법사가 자동으로 구성합니다.