KIMKYUTAE.COM · ©
WordPress 사이트가 갑자기 응답하지 않았다 – Apache MaxRequestWorkers 장애 분석
Jetpack에서 사이트 장애 알림이 왔다. 처음에는 모니터링 오탐인가 싶었지만 브라우저에서도 실제로 접속되지 않았다.
인스턴스를 바로 재부팅하면 사이트는 살아날 수 있어도 원인 확인에 필요한 단서가 사라질 수 있다. 그래서 서버 리소스, 웹 서버 상태, 로컬 HTTP 응답, Apache 로그 순서로 확인했다.
결론부터 말하면 직접 확인된 장애 상태는 Apache prefork 환경의 MaxRequestWorkers가 5로 매우 낮게 설정된 상태에서 worker가 모두 소진된 것이었다. 같은 시간대에 여러 외부 IP의 자동 취약점 스캔이 반복됐고, 로그에는 server reached MaxRequestWorkers setting 메시지가 여러 차례 남아 있었다. 시간대와 요청량을 보면 자동 스캔이 worker 고갈을 촉발하거나 악화한 주요 요인으로 보이지만, 로그만으로 각 요청의 처리 시간까지 재구성한 것은 아니므로 유일한 원인으로 단정하지는 않았다.
1. 서버 자체가 죽은 것은 아니었다
우선 CPU, 메모리, 디스크 상태부터 확인했다.
date
uptime
free -h
df -h
당시 상태는 대략 다음과 같았다.
Load Average : 0.05 / 0.05 / 0.05
RAM : 약 1GB
Available : 약 400MB
Disk Usage : 10% 미만
CPU 과부하도 아니었고 메모리나 디스크가 꽉 찬 것도 아니었다. 따라서 Lightsail 인스턴스 자체가 리소스 부족으로 멈춘 상황은 아니라고 판단했다.
2. Apache는 살아 있는데 HTTP 요청을 처리하지 못했다
다음으로 웹 서버 프로세스와 80/443 포트 상태를 확인했다.
ps aux | grep -E 'apache|httpd|nginx|php-fpm' | grep -v grep
sudo ss -lntp | grep -E ':80|:443'
Apache 프로세스는 살아 있었고 80/443 포트도 정상적으로 LISTEN 중이었다. 여기까지만 보면 웹 서버 자체는 정상처럼 보였다.
하지만 서버 내부에서 localhost로 요청을 보내보니 결과가 달랐다.
curl -I --max-time 5 http://127.0.0.1
5초 동안 응답이 오지 않고 timeout이 발생했다. 외부 DNS나 방화벽 문제였다면 localhost 요청은 정상이어야 한다. localhost 요청까지 처리되지 않는다는 것은 Apache가 포트는 열고 있지만 실제 요청을 처리할 worker를 내주지 못하고 있을 가능성이 높다는 의미였다.
3. Apache만 재시작하자 바로 복구됐다
인스턴스 전체를 재부팅하지 않고 Apache만 재시작했다.
sudo systemctl restart apache2
재시작 후 다시 확인했다.
curl -Ik --max-time 10 https://example.com
이번에는 HTTP/1.1 200 OK가 바로 반환됐다. OS나 Lightsail 전체 문제가 아니라 Apache 요청 처리 계층에서 발생한 장애라는 판단에 힘이 실렸다.
4. error.log에서 MaxRequestWorkers 고갈 확인
Apache 오류 로그에서 worker 관련 메시지를 검색했다.
sudo grep -Ei \
"MaxRequestWorkers|server reached|scoreboard|timeout|PHP Fatal|Allowed memory|proxy_fcgi" \
/var/log/apache2/error.log | tail -n 100
결정적인 로그가 있었다.
[mpm_prefork:error]
AH00161: server reached MaxRequestWorkers setting,
consider raising the MaxRequestWorkers setting
더 중요한 것은 이 메시지가 오늘 처음 발생한 것이 아니었다. 9월 15일, 16일, 18일에도 같은 로그가 남아 있었고 장애 당일에도 다시 기록됐다. 즉 이번 문제는 일회성 현상이라기보다 낮은 worker 한계가 이미 여러 차례 드러나고 있었던 셈이다.
5. 실제 MaxRequestWorkers 값은 5였다
Apache MPM 설정을 확인했다.
sudo grep -RniE \
"MaxRequestWorkers|ServerLimit|StartServers|MinSpareServers|MaxSpareServers" \
/etc/apache2 2>/dev/null
현재 활성화된 prefork 설정은 다음과 같았다.
StartServers 1
MinSpareServers 1
MaxSpareServers 3
MaxRequestWorkers 5
mpm_worker.conf와 mpm_event.conf에는 150이라는 값이 있었지만 실제로 활성화된 것은 prefork였다. 활성 모듈도 확인했다.
sudo /usr/sbin/apache2ctl -M | grep -E 'php|mpm|reqtimeout'
결과는 다음과 같았다.
mpm_prefork_module (shared)
php_module (shared)
reqtimeout_module (shared)
즉 Apache prefork + mod_php 구조였다. 이 구성에서 MaxRequestWorkers가 5라면 동시에 처리 가능한 요청도 최대 5개다. 느린 요청 몇 개만 겹쳐도 신규 요청이 대기하게 되고, 이번처럼 localhost의 curl까지 timeout될 수 있다.

6. 장애 시간대에는 자동 취약점 스캔이 반복되고 있었다
Access log를 분 단위로 집계해보니 특정 사용자 한 명의 접속이 아니라 여러 외부 IP가 순차적으로 대량 요청을 보내고 있었다. 실제 IP는 공개하지 않고 아래처럼 마스킹했다.
22:06 Scanner-A 273 requests/min
22:07 Scanner-B 242 requests/min
22:09 Scanner-C 198 requests/min
22:12 Scanner-D 143 requests/min
22:14 Scanner-E 169 requests/min
22:15 Scanner-E 85 requests/min
요청 URI를 보면 일반적인 검색엔진 크롤러가 아니라 자동 취약점 스캐너 패턴이었다.
/.env
/laravel/.env
/public/.env
/aws_credentials
/terraform.tfstate
/terraform.tfstate.backup
/wp-config.php.bak
/wp-config.php.old
/wp-content/mysql.sql
/wp_mail_smtp.ini
/.bash_history
/phpinfo.php
환경변수 파일, AWS credential, Terraform state, WordPress 설정 백업, DB dump 등 자주 노출되는 민감 파일을 무차별적으로 찾는 형태다.
의심 경로 중 일부는 실제로 외부에서 확인했다.
/wp_mail_smtp.ini -> HTTP 404
/wp-content/mysql.sql -> HTTP 404
확인한 범위에서는 민감 파일 노출 징후는 없었다. 스캔 요청이 들어왔다는 사실과 파일이 실제로 노출됐다는 것은 별개의 문제다.
7. 공격 IP 하나씩 차단하는 것만으로는 부족했다
처음에는 상위 요청 IP를 차단하는 방법도 생각했지만 로그를 보면 스캐너 IP가 계속 바뀌고 있었다. 특정 IP 몇 개만 방화벽에 넣어서는 지속적인 해결책이 되기 어렵다.
오히려 인터넷에 공개된 WordPress라면 이런 스캔은 상시 발생한다고 보고, 그 정도의 요청으로는 웹 서버가 멈추지 않도록 구성하는 것이 더 중요하다고 판단했다.
8. Timeout과 KeepAlive 설정 조정
기존 Apache 설정은 다음과 같았다.
Timeout 300
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 5
RequestReadTimeout header=20-40,minrate=500
RequestReadTimeout body=10,minrate=500
reqtimeout_module은 이미 활성화되어 있어 느리게 헤더나 본문을 보내는 전형적인 Slowloris 형태에 대한 기본 방어는 적용되어 있었다.
다만 worker가 5개뿐인 서버에서 KeepAliveTimeout 5초는 부담이 될 수 있고, 일부 요청이 최대 300초 동안 처리 슬롯을 잡을 수 있는 Timeout 300도 현재 규모에는 여유가 큰 편이었다. 다음과 같이 조정했다.
Timeout 60
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 2
변경 후에는 바로 재시작하지 않고 먼저 설정 문법을 검사했다.
sudo /usr/sbin/apache2ctl configtest
결과는 Syntax OK. 이후 graceful reload로 적용했다.
sudo systemctl reload apache2
9. MaxRequestWorkers는 5에서 8로 소폭 증가
worker가 부족하다고 해서 무작정 50이나 100으로 올리는 것도 위험하다. 이 서버는 RAM이 약 1GB이고, 실제 WordPress 요청을 처리하던 Apache 프로세스의 RSS는 80~100MB 수준까지 올라가기도 했다. 프로세스 간 공유 메모리가 있어 RSS를 단순히 더한 값이 실제 사용량과 정확히 같지는 않지만, OS와 DB까지 함께 사용하는 1GB 환경에서는 여유가 크지 않다.
그래서 일단 MaxRequestWorkers 5를 8로만 올렸다.
MaxRequestWorkers 8
설정 변경 후 다시 configtest를 수행하고 reload했으며, 활성 설정 파일에서도 8이 적용된 것을 확인했다.
10. 변경 후 메모리 상태 확인
설정 변경 후 메모리 상태도 다시 확인했다.
RAM Total 약 945MiB
Used 약 443MiB
Available 약 501MiB
Swap Total 약 649MiB
Swap Used 약 39MiB
Linux에서는 단순한 free 값보다 available 메모리를 보는 것이 중요하다. 약 500MB의 available 메모리가 남아 있었고 Swap 사용량도 이전과 동일해, 설정 변경 직후에는 메모리 압박이 없는 것으로 판단했다.
11. 장애 분석 중 발견한 별도 WordPress 설정 오류
이번 장애의 직접 원인은 아니지만 wp-config.php에서도 두 가지 잘못된 설정을 발견해 함께 정리했다.
첫 번째는 WP_CACHE가 두 번 정의되어 있었던 것이다.
define( 'WP_CACHE', true );
...
define( 'WP_CACHE', true );
이 때문에 Constant WP_CACHE already defined 경고가 반복되어 중복 정의 하나를 제거했다.
두 번째는 ABSPATH fallback 값이 잘못되어 있었다.
// 기존
define( 'ABSPATH', __DIR__ . 'wp-config.php/');
// 수정
define( 'ABSPATH', __DIR__ . '/' );
두 항목 모두 정리할 필요는 있었지만, Apache가 포트는 LISTEN하면서 localhost 요청까지 timeout되고 MaxRequestWorkers 고갈 로그가 반복된 이번 장애의 직접 원인으로 보지는 않았다.
정리
이번 장애에서 가장 인상적이었던 점은 프로세스가 살아 있다는 것과 서비스가 정상이라는 것은 다르다는 것이었다.
사이트 접속 불가
↓
CPU / RAM / Disk 정상
↓
Apache 프로세스 정상
↓
80 / 443 LISTEN 정상
↓
localhost HTTP 요청 timeout
↓
Apache 요청 처리 계층 의심
↓
error.log 확인
↓
MaxRequestWorkers 도달 확인
↓
access.log에서 반복적인 자동 스캔 확인
↓
Timeout / KeepAlive / worker 설정 조정
현재 적용한 값은 다음과 같다.
Apache MPM prefork
PHP mod_php
Timeout 60
KeepAlive On
KeepAliveTimeout 2
MaxKeepAliveRequests 100
MaxRequestWorkers 8
RequestReadTimeout
header=20-40,minrate=500
body=10,minrate=500
당분간은 이 설정으로 운영하면서 메모리와 Swap, 그리고 MaxRequestWorkers 재발 여부를 확인할 예정이다.
free -h
sudo grep "MaxRequestWorkers" /var/log/apache2/error.log | tail
같은 문제가 반복된다면 worker 숫자를 계속 올리기보다는 Apache mpm_event + PHP-FPM 구조로 전환하거나 앞단에 WAF/CDN을 두는 방향을 검토할 생각이다.
개인 WordPress 사이트라도 인터넷에 공개되는 순간부터 .env, wp-config.php.bak, DB dump, 클라우드 credential 등을 찾는 자동 스캔은 끊임없이 들어온다. 결국 중요한 것은 모든 스캔을 없애는 것이 아니라, 이런 흔한 외부 요청 정도로는 서비스 전체가 멈추지 않도록 구성하는 것이라고 생각한다.





