트래픽이 급증할 때 웹 서버가 갑자기 멈추거나 502 Bad Gateway 에러를 뱉으며 먹통이 되는 현상을 경험해봤을 것이다.
다만 대부분의 개발자들은 Nginx 설정만 변경하거나 서버 스펙을 무작정 올릴 뿐, 원인이 PHP-FPM 프로세스 관리자에 있다는 사실을 모르는 경우가 많다.
이번에는 PHP-FPM의 pm 설정이 정확히 무엇인지, 서비스 메모리에 맞는 최적 프로세스 수 계산법은 왜 필요한지, 그리고 실무 서버 환경에서 어떻게 설정해야 하는지 완벽하게 정리해서 소개하겠다.
PHP-FPM(FastCGI Process Manager)은 PHP 요청을 처리하기 위한 작업자(Worker) 프로세스를 관리한다. 이 프로세스를 관리하는 방식을 pm(Process Manager)이라고 부르며, 설정 파일(www.conf)에서 static, dynamic, ondemand 세 가지 중 하나를 선택할 수 있다.
| PM 방식 | 동작 원리 | 장점 | 단점 및 추천 환경 |
|---|---|---|---|
| pm = static | 고정된 수의 프로세스를 상시 유지 | 프로세스 생성 비용이 없어 응답 속도가 빠름 | 메모리를 항상 고정 점유함(고트래픽 전용 서버) |
| pm = dynamic | 트래픽 변화에 따라 프로세스 수를 동적으로 조절 | 메모리를 효율적으로 관리함 | 프로세스 생성 시 포크 비용 발생(일반적인 웹 서비스) |
| pm = ondemand | 요청이 들어올 때만 프로세스를 생성 | 평소 메모리 사용량이 극히 적음 | 첫 요청 시 약간의 지연 발생(저트래픽 및 개발 서버) |
대부분의 서버 환경에서는 dynamic 방식을 기본으로 사용한다. 하지만 프로세스 최대 생성 개수인 pm.max_children 값을 적절히 계산하지 않고 방치하면 서버 메모리가 고갈되어 OOM(Out Of Memory) Killer가 프로세스를 강제로 종료하는 사태가 벌어진다.
pm.max_children 숫자를 정할 때는 감으로 적어 넣으면 안 된다. 서버의 실제 가용 메모리와 PHP 프로세스 하나당 소비하는 평균 메모리를 측정해서 수학적으로 계산해야 한다.
먼저 리눅스 터미널에서 현재 PHP-FPM 프로세스들이 평균적으로 얼마의 메모리를 사용하는지 측정한다.
# PHP-FPM 자식 프로세스 1개당 평균 메모리 사용량 측정 (MB 단위)
ps aux | grep php-fpm | grep -v grep | awk '{sum += $6} END {print sum/NR/1024 " MB"}'예를 들어 프로세스 1개당 평균 35MB를 사용하고, 전체 서버 RAM 8GB 중 OS와 DB(MySQL 등) 용도로 3GB를 제외한 5GB(5120MB)를 PHP-FPM에 할당할 수 있다면 산출식은 다음과 같다.
max_children = 할당 가능 메모리(5120MB) / 프로세스당 평균 메모리(35MB) = 약 146
계산된 수치를 바탕으로 PHP-FPM 풀(Pool) 설정 파일을 수정한다. Ubuntu 기준 경로는 /etc/php/8.2/fpm/pool.d/www.conf 이다.
서버 메모리 용량을 고려하지 않고 기본값을 그대로 방치하거나, 과도하게 크게 설정한 경우다.
; 잘못된 설정: 8GB 메모리 서버에서 무작정 500개 설정
pm = dynamic
pm.max_children = 500
pm.start_servers = 20
pm.min_spare_servers = 10
pm.max_spare_servers = 30
pm.max_requests = 0이 설정은 트래픽 급증 시 500개의 프로세스가 생성되면서 약 17GB의 메모리를 요구하게 된다. 결국 서버 RAM이 부족해져 SWAP 스래싱이 일어나고, 서버 전체가 먹통이 된다.
실측한 메모리 기반으로 계산된 값을 적용하고, 메모리 누수를 방지하기 위한 안전장치를 추가한 설정이다.
; 올바른 설정: 가용 메모리 5GB 기준 계산 값 적용
pm = dynamic
pm.max_children = 140
pm.start_servers = 15
pm.min_spare_servers = 10
pm.max_spare_servers = 25
pm.max_requests = 1000설정을 마친 후에는 반드시 PHP-FPM 서비스를 재시작하여 변경 사항을 적용한다.
sudo systemctl reload php8.2-fpm
PHP-FPM status 페이지를 활성화하거나 로그를 확인해보면 아래와 같이 프로세스가 안정적으로 제어되는 것을 확인할 수 있다.
# PHP-FPM 로그 확인 시 프로세스 부족 경고(server seems busy)가 사라짐
tail -f /var/log/php8.2-fpm.log
# 출력 예시:
# [01-Nov-2024 10:15:20] NOTICE: fpm is running, pid 1234
# [01-Nov-2024 10:15:20] NOTICE: ready to handle connections
✗ pm.max_requests 옵션을 0(무제한)으로 설정하는 실수는 피해야 한다. PHP 애플리케이션이나 서드파티 C 확장 모듈에서 발생하는 미세한 메모리 누수(Memory Leak)가 누적되어 프로세스가 오랫동안 떠 있을수록 메모리를 점점 더 많이 잡아먹기 때문이다.
✓ pm.max_requests = 500 또는 1000 으로 설정하면 자식 프로세스가 해당 횟수만큼 요청을 처리한 뒤 스스로 종료하고 새 프로세스로 교체된다. 이를 통해 누적된 메모리 누수를 깨끗하게 정리할 수 있다.
또한, MySQL이나 Redis 같은 데이터베이스 연결 수(max_connections)도 pm.max_children 수치에 맞춰 함께 상향 조정해야 'Too many connections' 에러가 발생하는 2차 장애를 막을 수 있다.
PHP-FPM 프로세스 최적화는 서버 안정성과 응답 속도를 결정짓는 핵심 백엔드 설정이다. 작은 자원 산출 습관이 모여서 장애 없는 견고한 서버 인프라를 만든다는 점을 잊지 말자. 이 글의 max_children 계산식을 참고해 서버의 www.conf 파일을 수정해보면, 갑작스러운 트래픽 폭주에도 멈추지 않는 안정적인 웹 서비스를 얻을 수 있을 것이다.