php-fpm max_children 경고: 워커 증설 전에 로그로 원인 판별

공정위 광고 문구 Ver 3.1
공정위 광고 문구 Ver 3.2(모바일 전용)
테스트 환경 RHEL 계열AlmaLinux 9.8root 계정2026-10-12 확인

php-fpm max_children 경고는 PHP 워커 프로세스가 전부 요청을 처리하는 중이라 새 요청이 대기열에 들어갔다는 신호입니다. 이 경고가 몇 주째 누적된 Nginx와 Apache, php-fpm 구성의 워드프레스 서버에서 로그로 워커를 채운 요청을 가려낸 결과, 설정값을 올리는 대신 nginx 속도 제한으로 해결하게 되었습니다.

주간 점검에서 경고는 하루 27~187건씩 기록되었지만 같은 기간 방문자가 받은 502·504 오류는 나흘 동안 0건이었으며, 워커를 채운 것은 방문자가 아니라 관리자 주소를 반복 요청한 봇과 존재하지 않는 경로를 요청한 스캐너였습니다.

1️⃣ php-fpm max_children & seems busy

경고가 하루 66건 누적되면 워커가 66번 부족했던 것으로 읽기 쉽습니다. php-fpm 로그에는 성격이 다른 두 경고가 섞여 있으며, 대부분은 상한에 도달하기 전에 기록되는 예비 경고입니다.

[09-Oct-2026 00:16:18] WARNING: [pool www] seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers), spawning 8 children, there are 0 idle, and 15 total children
[09-Oct-2026 13:02:05] WARNING: [pool www] server reached pm.max_children setting (26), consider raising it

[seems busy]는 대기 중인 워커가 0이 되어 새 프로세스를 생성하는 중이라는 안내이며, 프로세스 관리 방식이 dynamic이면 요청이 잠깐만 증가해도 기록됩니다. [server reached pm.max_children]은 상한 26개에 도달해 더 생성하지 못한 순간이며, 이때부터 새 요청이 대기하게 됩니다.

이는 식당에서 직원을 한 명 더 부르는 것과, 좌석이 다 차 손님을 밖에 세워 두는 것과 같습니다.

하루치를 종류별로 집계한 결과 seems busy 58건, max_children 6건이었습니다. 점검 루틴은 두 경고를 합산해 66건으로 보고했지만, 실제로 상한에 도달한 것은 여섯 번입니다.

grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'pool www' | grep -c 'seems busy'
58
grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'max_children' | cut -c2-21
09-Oct-2026 13:02:05
09-Oct-2026 13:52:10
09-Oct-2026 18:09:20
09-Oct-2026 18:42:17
09-Oct-2026 18:46:07
09-Oct-2026 19:12:44

두 번째 명령이 출력하는 여섯 개의 시각이 원인 추적의 기준이 되며, 다음 단계에서는 이 시각만 사용합니다.

2️⃣ max_children 증설이 답이 아닌 이유

상한 26을 올리면 경고 건수는 줄어들 수 있지만, 워커를 채운 요청이 무엇인지는 달라지지 않습니다.

일반적인 계산은 사용 가능한 메모리를 워커 하나의 평균 메모리로 나누는 방식입니다. 실측한 서버는 워커 26개가 평균 131MB, 최대 214MB를 사용해 합계 3.4GB였고, 메모리 15.7GB 중 4.1GB가 사용 가능해 숫자로는 30개 이상을 더 생성할 수 있습니다.

ps -o rss= -u www -C php-fpm | awk '{s+=$1; n++; if($1>m) m=$1} END{print n, s/n/1024, m/1024, s/1024}'
26 130.579 214.27 3395.05
free -m
               total        used        free      shared  buff/cache   available
Mem:           15783       11658         397         340        4383        4124
Swap:           8191        1615        6576
nproc
4

첫 번째 출력의 네 값은 워커 수, 평균 메모리(MB), 최대 메모리(MB), 합계(MB)이며, free 출력의 available 열이 사용 가능한 메모리입니다.

CPU가 4코어라 워커 수를 늘려도 동시에 처리되는 요청은 늘지 않으며, 요청 하나에 소요되는 시간만 늘어납니다. 방문자가 받은 502·504 오류가 0건인 상태에서는 상한을 올려도 얻는 것이 없고, 워커를 차지하던 봇과 스캐너에게 더 많은 프로세스를 할당합니다.

상한을 올리기 전에 확인할 사항은 경고가 기록된 시각에 워커를 차지한 요청이 방문자인지 아닌지이며, 이를 access.log로 확인하는 순서를 다음 장에 정리했습니다.

3️⃣ access.log 로 원인 추적: 시각, IP, 경로, 응답 코드 순서

경고 시각 하나를 선택하면 그 1~2분의 access.log에 답이 있습니다. 순서는 시각으로 줄을 추출하고, IP별로 집계하고, 상위 IP의 경로와 응답 코드를 확인하는 것입니다.

grep '09/Oct/2026:18:09' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head
    115 140.245.x.x
     61 187.15.x.x
     17 3.36.x.x
      8 91.239.x.x
      4 79.127.x.x
      3 109.248.x.x
      3 109.248.x.x
      3 109.248.x.x
      2 79.127.x.x
      2 40.77.x.x
grep '09/Oct/2026:18:09' /var/log/nginx/access.log | awk '{print $7, $9}' | sort | uniq -c | sort -rn | head
     55 / 200
     13 /wp-login.php 404
     13 /wp-admin/plugins.php 302
     13 /wp-admin/index.php 302
     13 /wp-admin/edit.php 302
     12 /wp-admin/profile.php 302
      4 / 499
      3 /xmlrpc.php 302
      2 /xmlrpc.php 499
      2 /wp-content/themes/kinsta/images/duos/small/wheelchair.svg 404

10월 9일 18시 09분의 경우 1분 동안 286건이 기록되었고, 그중 한 IP가 115건이었으며 경로는 /wp-admin/profile.php 같은 관리자 주소, 응답 코드는 전부 302였습니다.

응답 코드가 PHP 도달 여부를 의미합니다. 캐시된 HTML을 nginx가 직접 응답한 200과 444·400은 PHP를 거치지 않으며, 302·404와 캐시되지 않은 200은 워커 하나를 사용합니다. 비로그인 상태로 관리자 주소를 요청하면 워드프레스가 로그인 페이지로 302를 응답하는데, 그 302 하나가 PHP 실행 한 번입니다.

서버 하나에 사이트가 둘 이상이면 전역 access.log에 두 사이트의 줄이 함께 기록됩니다. 처음에는 이 봇이 첫 번째 사이트를 공격한 것으로 판단했지만, 사이트별 로그와 대조한 결과 22,391건 중 22,390건이 두 번째 사이트의 요청이었습니다.

4️⃣ 워커를 채운 세 가지 요청: 봇의 wp-admin, 스캐너의 404, 캐시 프리로드

같은 순서를 경고가 기록된 시각 전부에 적용한 결과 워커를 채운 요청은 세 종류였습니다.

첫 번째는 관리자 주소를 반복 요청한 봇입니다. 한 IP가 세 시간 동안 22,391건을 보냈고, 그중 9,944건이 /wp-admin/profile.php를 포함한 네 개 경로였으며, 비로그인 302 한 번이 PHP 실행 한 번이라 9,944번 워커를 사용했습니다.

두 번째는 존재하지 않는 경로를 요청하는 취약점 스캐너입니다. User-Agent에 Googlebot이 적혀 있어도 구글봇이 아니며, 하루 14,672건을 보낸 IP는 역방향 DNS가 googleusercontent.com으로 끝나는 구글 클라우드 가상 서버였고 구글봇은 googlebot.com으로 끝납니다. 같은 IP가 bingbot과 Applebot, GPTBot, ClaudeBot을 번갈아 사용했습니다.

.env 같은 파일 요청은 nginx가 444로 0.04초에 차단하지만, /userfiles나 /secrets.yml처럼 차단 규칙에 없는 평범한 경로는 워드프레스가 404 페이지를 생성하게 됩니다. 이 404 페이지가 266KB, 1.4~2.9초로 일반 글 생성과 같은 비용이며, 한 IP가 하루 4,069번 요청했습니다.

세 번째는 서버 자신입니다. 글이나 번역을 저장하면 캐시가 비워지고 WP Rocket 프리로드가 전체 주소를 다시 요청하며, 하루 8,489건에 분당 최대 60건이었습니다. Rank Math의 색인 상태 검사도 Action Scheduler를 통해 하루 754번 워커 하나를 사용했습니다. 두 가지는 작업 시간대에만 집중되고 방문자 오류로 이어지지 않아 조치 대상에서 제외했습니다.

아래 시뮬레이터에서 각 요청이 어느 층에서 끝나고 PHP 워커를 사용하는지 볼 수 있습니다.

🧭 요청 경로 시뮬레이터wp-admin 관리자 페이지설명 시뮬레이터
🗂️ 워커를 채운 요청
봇의 wp-admin 요청
비로그인 상태로 관리자 주소를 요청하면 워드프레스가 로그인 페이지로 302를 응답합니다. 그 302 하나가 PHP 실행 한 번입니다.
스캐너: .env 계열
.env 같은 파일 요청은 nginx 차단 규칙이 444로 연결을 끊습니다. 0.04초, PHP는 거치지 않습니다.
스캐너: 평범한 가짜 경로
/userfiles나 /secrets.yml처럼 차단 규칙에 없는 평범한 경로는 워드프레스가 404 페이지를 생성합니다. 266KB, 1.4~2.9초로 일반 글 생성과 같은 비용입니다.
캐시 프리로드
글이나 번역을 저장하면 캐시가 비워지고 WP Rocket 프리로드가 전체 주소를 다시 요청합니다. 분당 최대 60건이며 요청마다 워커 하나를 사용합니다.
캐시된 글 방문
캐시 파일이 있으면 nginx가 직접 응답하며 PHP를 거치지 않습니다.
캐시 안 된 글 방문
캐시 파일이 없으면 PHP가 페이지를 생성하고 그 HTML을 캐시 파일로 저장합니다.
전체 흐름
요청은 위에서 아래로 내려가다 응답이 끝나는 층에서 멈춥니다. PHP 워커를 사용하는 것은 워드프레스까지 내려간 요청뿐입니다.
요청멈추는 층응답워커
봇의 wp-admin워드프레스302(로그인 페이지로)1개 사용
스캐너 .env 계열nginx 차단 규칙444(0.04초)사용 안 함
스캐너 평범한 가짜 경로워드프레스404 페이지(266KB · 1.4~2.9초)1개 사용
캐시 프리로드워드프레스200 · 캐시 파일 저장1개 사용
캐시된 글 방문캐시 파일200(nginx 가 바로 응답)사용 안 함
캐시 안 된 글 방문워드프레스200 · 캐시 파일 저장1개 사용
브라우저 안에서만 동작하는 설명용 시뮬레이터입니다. 응답과 시간은 2026년 10월 Nginx + Apache 구성 서버의 로그와 실측에서 가져온 값입니다.

5️⃣ nginx 속도 제한: 비로그인 요청 제한과 진짜 IP 복원

차단 대신 속도 제한을 선택했습니다. 경로를 막으면 스캐너가 경로를 바꾸고, IP를 막으면 다음 날 다른 IP가 접속하기 때문입니다. 비로그인 상태로 PHP까지 도달하는 요청만 IP당 분당 30건, 순간 60건으로 제한하고 초과분은 429로 응답합니다.

CDN 뒤에 있는 서버에는 이 설정만으로는 맞지 않습니다. nginx가 보는 IP가 CDN 엣지 주소라 그 엣지를 거치는 방문자 전체가 한 IP로 묶이기 때문이며, 엣지 IP 목록을 set_real_ip_from으로 등록해 X-Forwarded-For의 진짜 IP를 먼저 복원해야 합니다. 로그인 쿠키가 있는 요청은 면제하고, User-Agent로는 면제하지 않습니다.

# http 맥락
real_ip_header X-Forwarded-For;
real_ip_recursive on;
set_real_ip_from 203.0.113.0/24;   # CDN 엣지 목록
map $http_cookie $php_anon_key {
    default                 $binary_remote_addr;
    ~*wordpress_logged_in   '';
}
limit_req_zone $php_anon_key zone=php_anon:10m rate=30r/m;

# PHP 로 보내는 location
location @apache {
    limit_req zone=php_anon burst=60 nodelay;
    limit_req_status 429;
    proxy_pass http://127.0.0.1:8080;
}

캐시된 페이지는 try_files가 nginx 안에서 응답하므로 이 location에 오지 않으며, admin-ajax.php는 별도 location이라 제한에 포함되지 않습니다.

검증은 동시 요청으로 합니다. 순차 요청은 404 하나가 1.5초라 분당 30건을 넘지 못해 제한에 걸리지 않으며, 20개를 동시에 보내 100건을 요청하면 404 67건, 429 33건으로 제한 동작을 확인할 수 있습니다.

seq 1 100 | xargs -P 20 -I{} curl -s -o /dev/null -w '%{http_code}\n' https://example.com/zz-{} | sort | uniq -c

출력에 429가 집계되면 제한이 동작하는 것이며, 서버 자신의 IP를 면제해 두었다면 면제를 잠시 해제하고 실행하거나 다른 네트워크에서 실행합니다.

6️⃣ 적용 전후 비교

비로그인 관리자 주소 제한은 10월 9일 저녁, PHP 도달 요청 제한은 11일 저녁에 적용했습니다. 관리자 주소 302는 9일 9,996건에서 10일 11건, 11일 1건으로 감소했고, 세 시간 동안 22,391건을 보낸 IP는 다음 날부터 요청이 0건입니다.

스캐너 쪽은 적용 직후라 하루치 숫자가 없으며, 워드프레스가 생성한 404 페이지 수(9일 2,563·10일 2,612·11일 1,630)가 다음 주 점검에서 어디까지 내려가는지로 판단하게 됩니다. 방문자 502·504 오류는 적용 전후 모두 0건입니다.

이 숫자는 Nginx와 Apache, php-fpm으로 운영되는 워드프레스 서버의 로그를 직접 집계한 값이며, 경고 건수가 아니라 방문자 오류와 PHP 도달 요청 수를 기준으로 판단했습니다.

📊속도 제한 적용 전후하루 단위 · 로그 집계
적용 전적용 후
관리자 주소 302 응답
비로그인 wp-admin 요청에 워드프레스가 보낸 302. 비로그인 관리자 주소 제한은 10월 9일 저녁 적용.
02,4994,9987,4979,996
10/99,996적용 전
10/1011적용 후
10/111적용 후
봇 IP 요청 수
세 시간 동안 22,391건을 보낸 IP 하나의 하루 요청 수.
05,59811,19616,79322,391
10/922,391적용 전
10/100요청 없음
10/110요청 없음
워드프레스가 생성한 404 페이지
스캐너의 평범한 가짜 경로 요청. PHP 도달 요청 제한은 10월 11일 저녁 적용이라 다음 주 점검 숫자로 판단.
06531,3061,9592,612
10/92,563적용 전
10/102,612적용 전
10/111,630적용 전 · 11일 저녁 적용
숫자는 2026년 10월 9일부터 11일까지의 nginx access.log 를 하루 단위로 집계한 값입니다.

🔢 FAQ & 추천 콘텐츠

dynamic은 요청량에 따라 워커를 생성·종료하며, 대기 워커가 0이 되면 seems busy 경고를 기록합니다. static은 max_children 개수를 항상 유지해 생성 지연과 경고가 없는 대신 메모리를 상시 점유합니다. 이 글의 서버는 dynamic 26개를 유지했으며, 메모리 여유가 있고 요청량이 하루 내내 일정한 서버라면 static이 맞습니다.

경고 시각에 특정 요청이 오래 실행되는 것으로 보일 때 켭니다. 풀 설정에 [slowlog] 경로와 [request_slowlog_timeout]을 지정하면 그 시간을 넘긴 요청의 실행 위치가 기록됩니다. 이 글의 사례는 요청 수가 원인이라 access.log 집계만으로 판별했고, slowlog는 사용하지 않았습니다.

제한은 캐시를 거치지 않고 PHP까지 도달하는 비로그인 요청에만 적용되며, 한 IP가 분당 30건과 순간 60건을 넘길 때 초과분만 429를 받습니다. 10월 11일 하루 집계에서 이 기준을 넘긴 IP는 스캐너 하나였고, 초과 상태는 1분 안에 해제되어 다음 요청부터 정상 응답됩니다.

줄이지 않았습니다. 프리로드는 글과 번역을 저장한 작업 시간대에만 집중되고 방문자 오류로 이어지지 않았기 때문입니다. 판단 기준은 경고 건수가 아니라 방문자가 받은 502·504 오류 유무이며, 그 숫자가 0이면 프리로드는 그대로 두게 됩니다.

ℹ️ 제휴 안내
본 사이트의 콘텐츠에는 제휴 링크가 포함되어 있습니다. 방문자가 이 링크를 통해 상품 또는 서비스를 구매하면 본 사이트는 판매처로부터 수수료를 지급받습니다. 이 과정에서 구매자가 지불하는 금액(이벤트 할인 시 금액이 내려갑니다↓)은 오르지 않습니다. 게시된 가격·할인·재고 정보는 작성 시점 기준이며 실제와 다를 수 있으므로, 구매 전 판매처에서 최종 확인하시기 바랍니다. 상품 선정과 평가는 자체 기준에 따라 작성되며, 수수료 지급 여부가 소개 순서나 평가 내용에 영향을 주지 않습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

인간임을 증명하세요: 4   +   6   =