18 KiB
웹 계층(Apache) 부하·용량 분석
2026-04-14 AWS 인스턴스 업그레이드의 배경을 현재 사양에서 재검증하고, Apache 워커·KeepAlive·DB 연결이 실제로 병목인지 실측으로 판정합니다.
- 작성일: 2026-09-16
- 대상: AWS EC2
t3a.medium(2 vCPU, 4GB) — Apache 2.4.58 + mod_php7 + MariaDB- 선행 작업: 260915-improve-production (인덱스·쿼리 개선, 9/15 23:00 적용)
- 관련 문서: 00-check-methods.md (측정 방법), 09-aws-db-separation.md (DB 분리 검토)
1. 배경: 2026-04-14 업그레이드는 왜 했나
당시 파악된 장애 경로입니다.
동시 접속 증가
→ Apache MaxRequestWorkers 가 감당 못 함
→ 서버 waiting 과 CPU steal 급등
→ t2.micro 의 CPU 크레딧 소진
→ 약 10초간 무응답
당시 분석이 내린 결론은 t3.micro + MaxRequestWorkers 50 이었습니다. 근거는 "서비스 사이클 34초 중 서버 점유 1초(약 3%)" 라는 점유율 모델이었고, 이를 근거로 평상시 동시 사용자 약 800명까지 안정적이라고 추정했습니다.
실제로는 그보다 큰 t3a.medium 으로 갔습니다. 즉 문서의 권고안과 실제 구성이 다르므로, 그 문서의 수치는 현재 상태를 설명하지 못합니다.
| t2.micro (당시) | t3a.medium (현재) | |
|---|---|---|
| vCPU / 메모리 | 1 / 1GB | 2 / 4GB |
| 크레딧 베이스라인 | 10% | 20% |
| 시간당 크레딧 | 6 (최대 144) | 24 (최대 576) |
1-1. 당시 계산 모델의 한계
기록해둡니다. 같은 모델을 다시 쓰지 않기 위해서입니다.
- "1 페이지 로드 = 1 요청"이 아닙니다.
index.html하나에 script/link 11개 + jQuery.load()로 header/section/footer 3개를 더 받고, 그 뒤에 PHP 엔드포인트가 붙습니다 (src/web/main/index.html:36). - 0.5초는 CPU 시간이 아니라 응답 시간입니다. 3% 를 "CPU 여유"로 읽으면 안 되고 "워커 점유율"로만 읽어야 합니다.
- 평균 모델은 버스트를 설명하지 못합니다. 문제는 평균이 아니라 수업 시작 시각에 몰리는 순간입니다.
- "평상시 500명 → 800명"은 측정값이 아니라 위 모델에서 나온 추정치입니다.
2. 측정 결과
2-1. CPU 크레딧 / steal — 문제 없음 ✅
피크 시간대(12:50~15:00) vmstat 로그 779개 샘플에서 st(steal) 컬럼을 직접 집계했습니다.
원본: result/peaktime_steal_summary.txt
| 9/14 (개선 전) | 9/15 (개선 전) | 9/16 (개선 후) | |
|---|---|---|---|
| steal 평균 | 0.003% | 0.021% | 0.010% |
| steal 최대 | 1% | 2% | 1% |
| us+sy 평균 | 13.50% | 9.48% | 2.79% |
| 베이스라인(20%) 초과 샘플 | 109개 (14.0%) | 65개 (8.3%) | 1개 (0.1%) |
| swpd / si / so | 0 / 0 / 0 | 0 / 0 / 0 | 0 / 0 / 0 |
t3a.medium 의 크레딧 베이스라인은 인스턴스 전체 CPU의 20% 이고, 이는 vmstat 의 us+sy 20% 와 같은 값입니다. 개선 전에도 피크 평균이 13.5% 로 베이스라인 미만이었으므로 피크 시간에도 크레딧은 순증하고 있었습니다.
→ 4월 장애의 직접 원인(크레딧 소진 → 스로틀링)은 현재 사양에서 재현되지 않습니다.
⚠️ 스왑이 없습니다(
swpd/si/so모두 0). 메모리 한계에 닿으면 느려지는 게 아니라 OOM Killer 가 동작하고, RSS 가 가장 큰mysqld가 1순위 표적입니다. 메모리 여유는 별도 감시 대상입니다.
2-2. Apache 설정 현황
apache2ctl -V | grep -i "Server MPM"
ls /etc/apache2/mods-enabled/ | grep mpm
grep -RiE "^[[:space:]]*(MaxRequestWorkers|MaxClients|ServerLimit|StartServers|MaxConnectionsPerChild)" /etc/apache2/
grep -rihE '^\s*(KeepAlive|MaxKeepAliveRequests|KeepAliveTimeout)' /etc/apache2/apache2.conf /etc/apache2/conf-enabled/ /etc/apache2/sites-enabled/
apache2ctl -M | grep -iE 'php|proxy_fcgi|http2|status'
| 항목 | 값 | 비고 |
|---|---|---|
| Apache | 2.4.58 (Ubuntu) | 호스트 설치. 도커 아님 |
| MPM | prefork | mod_php 때문에 고정 |
StartServers |
5 | |
ServerLimit |
400 | Debian 기본 150에서 상향돼 있음 |
MaxRequestWorkers |
400 | 〃 |
MaxConnectionsPerChild |
0 | 자식 프로세스 무한 재사용 |
KeepAlive |
On | |
KeepAliveTimeout |
5 | 워커 점유의 주 원인 (2-5 참조) |
MaxKeepAliveRequests |
100 | |
| PHP | php7_module (mod_php) |
PHP-FPM 없음 |
| HTTP/2 | 미적용 | mod_http2 없음 → 브라우저가 origin당 연결 최대 6개 |
mod_status |
있음 | Require local (/etc/apache2/mods-enabled/status.conf) |
mods-available/mpm_event.conf·mpm_worker.conf 의 150은 비활성 MPM 이므로 무관합니다.
2-3. Apache 실부하 — 병목 아님 ✅
curl -s http://localhost/server-status?auto 누적값 (RestartTime 9/12 06:44 이후 394,325초 = 4일 13시간)
| 지표 | 계산 | 값 |
|---|---|---|
| 총 요청 | Total Accesses 1,398,433 |
3.55 req/s |
| 평균 응답 시간 | Total Duration 21,596,481ms ÷ 요청 수 |
15.4 ms |
| 요청당 CPU | (CPUChildrenUser 822.34 + CPUChildrenSystem 835.02)초 ÷ 요청 수 |
1.19 ms |
| Apache 총 CPU 점유 | 1,657초 ÷ 394,325초 | 코어 1개의 0.42% |
| 응답 1건 평균 크기 | 4,093,249KB ÷ 요청 수 | 2.9 KB |
메모리 (/proc/*/smaps_rollup 의 PSS 합계, 유휴 시각 기준):
apache2 14개 / PSS 합계 29MB / 1개당 평균 2MB
PSS 합계는 공유 페이지를 중복 없이 한 번만 계산하므로 Apache 전체의 실제 메모리 비용입니다. 단, 이 값은 유휴 자식 기준입니다. PHP 요청을 처리한 자식은 더 커지므로 피크 재측정이 필요합니다.
2-4. DB 연결 — 제약 아님 ✅
| 지표 | 값 |
|---|---|
max_connections |
151 |
Max_used_connections (MariaDB 시작 이래 고수위) |
27 |
| 요청 대비 신규 DB 연결 비율 | 요청 130건당 36건 = 28% |
요청의 28%만 DB를 타고(나머지는 정적 파일), 연결 하나가 수 ms만 삽니다. 동시 DB 연결은 0.04개 수준으로, 151에 닿으려면 초당 만 단위 신규 연결이 필요합니다.
→ MaxRequestWorkers 400 이 max_connections 151 을 넘는 것은 실무상 문제가 되지 않습니다.
2-5. KeepAlive 점유 구조 — 확정 ✅ (2026-09-17 측정)
scoreboard 분해 샘플 (curl -s http://localhost/server-status?auto 의 Scoreboard: 행)
| 시각 | busy | idle | K(KeepAlive 대기) | W(전송) | R(수신) | 구간 요청률 |
|---|---|---|---|---|---|---|
| 20:16 | 2 | 8 | 1 | 1 | 0 | — |
| 20:30:25 | 15 | 3 | 11 | 1 | 3 | — |
| 20:30:30 | 15 | 5 | 6 | 1 | 2 | 2.2 req/s |
| 20:30:40 | 7 | 7 | 5 | 1 | 0 | 2.2 req/s |
| 20:33:12 | 9 | 6 | 8 | 1 | 0 | — |
| 20:33:20 | 5 | 9 | 3 | 1 | 0 | 1.5 req/s |
| 20:33:30 | 14 | 0 | 11 | 1 | 2 | 13 req/s |
busy 워커의 70~80%가 K(KeepAlive 대기) 입니다. 실제 작업 중인 W 는 일관되게 1개입니다.
prefork 에서는 워커가 요청 처리 중만이 아니라 TCP 연결이 열려 있는 내내 점유됩니다. 응답을 다 보낸 뒤에도 KeepAliveTimeout(5초) 동안 자식 프로세스가 연결을 붙잡습니다. 워커 점유 효율은 15ms / 5,000ms = 0.3% 입니다.
2026-09-17 피크 780샘플로 관계가 확정됐습니다. 5장 참조 — K 는 요청률에 선형 비례합니다.
(위 저녁 샘플만 보고 "비례하지 않는다"고 판단했던 것은 표본 부족에 따른 오판이었습니다.)
3. 결론
3-1. 확정된 것
| 항목 | 결론 |
|---|---|
| CPU 크레딧 / steal | 문제 없음. steal 최대 2%, 피크에도 베이스라인 미달 |
| Apache CPU | 코어의 0.42% |
| Apache 메모리 | 14프로세스 29MB (유휴 기준) |
| 평상시 워커 | 400 중 2~15개 사용 |
| DB 연결 상한 151 | 제약 아님. 고수위 27 |
MaxRequestWorkers 400 |
유지. 9/17 피크 busy 204(51%)로 적정 (5장) |
현재 지표는 전부 여유입니다. 지금 설정을 바꿀 이유가 없습니다.
3-2. 분석 중 폐기한 가설
같은 오류를 반복하지 않기 위해 기록합니다.
| 가설 | 왜 틀렸나 |
|---|---|
| mod_php 자식 1개당 10~30MB → 400개면 OOM | 실측 PSS 2MB (유휴). 400개여도 800MB 수준 |
| 워커 1개 = DB 연결 1개 → 400 > 151 은 결함 | 요청의 28%만 DB 사용. K 상태 워커는 DB 연결을 안 씀. 고수위 27 |
| Little's law 로 동시 요청 0.055개 → 120이면 13배 여유 | Total Duration 은 처리 시간만 포함. KeepAlive 점유가 빠져 실측(busy 2~15)과 100배 차이 |
K ≈ 요청률 × KeepAliveTimeout 선형 외삽 |
2.2 req/s 와 13 req/s 모두 K=11. 버스트에서 연결 재사용이 작동 |
MaxRequestWorkers 120 권장 |
9/17 피크 busy 최대 204. 120으로 조였다면 수업 시간에 요청이 큐에 쌓였음 (5-3절) |
K 는 요청률에 비례하지 않는다 (저녁 샘플 7개 기준) |
780샘플 회귀에서 K = 1.28 × req/s, 상관 0.862. 표본 부족에 따른 오판 |
외부에서 /server-status 403 확인 |
도메인 오류로 무효. SSL 인증서는 www_chocomae_com (ssl/README:7), chocomae.jinaju.com 은 http 전용 |
3-3. DB 분리 검토(09번 문서)에 주는 시사점
4월 장애가 Apache 워커 + 인스턴스 크레딧에서 비롯됐다면, DB를 RDS로 분리해도 웹 계층 문제는 그대로 남습니다. 반면 9/16 실측은 DB 개선만으로 피크 CPU가 3.4배 줄었음을 보여줍니다.
→ 09번 문서의 "DB 분리 불필요" 결론을 강화합니다.
다만 09번 문서 2장의 원인 진단이 "DB와 PHP의 리소스 경쟁"으로만 서술돼 있고 인스턴스 크레딧이라는 세 번째 원인이 빠져 있습니다. 4월 이력을 반영한 보완이 필요합니다.
4. 남은 측정
4-1. 피크 모니터링 (등록 완료)
/home/ubuntu/temp/apache-peak.sh — 평일 12:50부터 10초 간격 780회(130분). 기존 vmstat 모니터링과 같은 시각에 돌아 로그를 나란히 대조할 수 있습니다.
50 12 * * 1-5 /home/ubuntu/temp/chocomae-peaktime-monitoring.sh
50 12 * * 1-5 /home/ubuntu/temp/apache-peak.sh >> /home/ubuntu/temp/apache-peak.cron.log 2>&1
0 0 * * 1 find /home/ubuntu/temp \( -name "vmstat_*.log" -o -name "apache_*.log" \) -mtime +30 -delete
수집 컬럼: time busy idle K W R total_accesses db_conn db_max db_total
sleep $(( 10 - $(date +%s) % 10 ))으로 10초 격자에 고정 — vmstat 로그와 시각이 어긋나지 않습니다.- DB 조회는
sudo -n /usr/bin/mariadb경유 (OS 계정ubuntu는 DB 직접 접속 권한 없음, NOPASSWD sudo 가능).db_conn(Threads_connected) 은 순간값이라 짧은 PHP 연결을 대부분 놓칩니다. 판정에는db_max(Max_used_connections, 고수위)를 쓰세요.
4-2. 판정 기준
| 관측 | 판단 |
|---|---|
피크 busy 최대가 400에 근접 |
워커가 병목. KeepAliveTimeout 단축 우선 검토 |
K 가 busy 의 대부분이고 요청률과 무관하게 큼 |
KeepAliveTimeout 5→2초. 워커 수요 감소, CPU 여유(0.42%)로 TLS 재핸드셰이크 흡수 가능 |
피크 busy 최대가 100 미만 |
400은 과하지만 해가 없음. 변경 불필요 |
idle=0 샘플이 잦음 |
StartServers/MinSpareServers 상향 검토 |
db_max 가 27에서 오름 |
DB 연결 재평가 |
4-3. 미확인 항목
- 외부에서
/server-status접근 차단 확인 (도메인 정정 후 재시도)curl -s -o /dev/null -w 'jinaju http : %{http_code}\n' http://chocomae.jinaju.com/server-status curl -s -o /dev/null -w 'chocomae https: %{http_code}\n' https://www.chocomae.com/server-status403이면 정상. 리버스 프록시를 거치면Require local이 프록시 IP를 통과시킬 수 있어 실제 응답 확인이 필요합니다. - EC2 크레딧 모드가 Unlimited 인지 Standard 인지 (콘솔 → EC2 → 인스턴스 → 크레딧 사양, 또는 CloudWatch
CPUSurplusCreditsCharged) - PHP
opcache.enable/memory_consumption/memory_limitgrep -rhE '^\s*(memory_limit|opcache\.enable|opcache\.memory_consumption)' /etc/php/*/apache2/php.ini /etc/php/*/apache2/conf.d/ - 피크 시각 자식 프로세스 PSS 재측정 (유휴 2MB 가 아니라 실제 값)
5. 피크 측정 결과 (2026-09-17)
원본: result/peaktime/apache_20260917-1250.log, result/peaktime/vmstat_20260917-1250.log 측정 구간 12:50:01~14:59:50, 10초 간격 780샘플.
5-1. 요약
| 항목 | 값 | 상한 대비 |
|---|---|---|
| busy 워커 최대 | 204 (13:54:00) | MaxRequestWorkers 400의 51% |
| 자식 프로세스 최대 (busy+idle) | 230 | ServerLimit 400의 57.5% |
| busy 평균 | 52.9 | |
| busy ≥ 100 샘플 | 110개 (14.1%) | |
| busy ≥ 150 샘플 | 50개 (6.4%) | |
| busy ≥ 200 샘플 | 3개 | |
K(KeepAlive 대기) 최대 |
152 (13:52:50) | busy 의 69% |
W(전송) 최대 / 평균 |
4 / 1.06 | |
idle=0 샘플 |
39개 (5.0%) | |
| 요청률 최대 | 132.4 req/s (14:01:40) | 평균 22.3 req/s |
| DB 신규 연결 최대 | 31.7 conn/s | 평균 8.1 conn/s |
Max_used_connections |
27 (변동 없음) | max_connections 151의 18% |
같은 시각 vmstat:
| 항목 | 값 |
|---|---|
| us+sy 평균 / p95 / 최대 | 2.14% / 5% / 9% |
| 크레딧 베이스라인(20%) 초과 샘플 | 0개 |
| steal 평균 / 최대 | 0.000% / 0% |
| swpd / si / so | 0 / 0 / 0 |
| free 최소 / cache 평균 | 396MB / 2,455MB |
5-2. 시간대별 추이
수업 교시에 맞춰 뚜렷한 파형이 나옵니다.
| 구간 | busy 평균 | busy 최대 | K 평균 | req/s 평균 |
|---|---|---|---|---|
| 12:50 | 13.3 | 25 | 8.0 | 7.3 |
| 13:00 | 17.5 | 32 | 9.4 | 6.2 |
| 13:10 | 4.6 | 16 | 1.8 | 0.8 |
| 13:20 | 4.3 | 14 | 2.1 | 1.3 |
| 13:30 | 10.3 | 26 | 5.4 | 5.5 |
| 13:40 | 46.9 | 94 | 33.6 | 33.0 |
| 13:50 | 155.9 | 204 | 111.6 | 66.9 |
| 14:00 | 115.2 | 176 | 79.7 | 46.6 |
| 14:10 | 52.9 | 84 | 36.2 | 16.9 |
| 14:20 | 65.7 | 106 | 46.0 | 26.1 |
| 14:30 | 42.8 | 79 | 30.6 | 15.9 |
| 14:40 | 83.4 | 121 | 56.9 | 33.3 |
| 14:50 | 74.5 | 120 | 50.9 | 29.6 |
최대 부하 구간(13:50~14:00)은 busy 평균 156 / 최대 204, K 평균 112, 67 req/s 입니다.
피크 요청률 132 req/s 는 4일 누적 평균 3.55 req/s 의 37배입니다. 누적 평균으로 용량을 계산하면 안 되는 이유가 이 숫자입니다.
5-3. 워커 수요 모델 (780샘플 회귀)
| 관계 | 회귀식 | 상관계수 |
|---|---|---|
요청률 → K |
K = 1.28 × req/s + 7.9 |
0.862 |
요청률 → busy |
busy ≈ 1.74 × req/s |
0.860 |
K 는 요청률에 선형 비례합니다. 9/16 저녁 샘플 7개로 "비례하지 않는다"고 본 것은 오판이었습니다.
KeepAliveTimeout 이 5초인데 계수가 1.28 이라는 것은, 연결 하나가 평균 약 3.9개 요청을 처리하고 끊긴다는 뜻입니다(5 ÷ 1.28). MaxKeepAliveRequests 100 설정 대비로는 재사용률이 낮지만, 재사용 자체는 작동하고 있습니다.
이 모델로 상한 도달점을 계산하면:
현재 (KeepAliveTimeout 5초): 400 워커 ÷ 1.74 = 약 230 req/s 에서 포화
9/17 실측 피크 = 132 req/s
→ 여유 약 1.74배
5-4. 판정
| 항목 | 판정 |
|---|---|
MaxRequestWorkers 400 |
✅ 적정. 유지. 피크 51% 사용 |
max_connections 151 |
✅ 적정. 고수위 27 (18%) |
| CPU / 메모리 / steal | ✅ 전부 여유. 피크에도 us+sy 최대 9%, steal 0% |
KeepAliveTimeout 5 |
⚠️ 워커의 69%를 소비. 여유가 1.74배뿐이므로 성장 대비 검토 대상 |
idle=0 5.0% |
⚠️ 예비 워커 소진 구간 존재. MinSpareServers 검토 대상 |
120으로 낮췄다면 피크에 요청이 큐에 쌓였습니다. 실측 204 > 120 이고, busy≥150 샘플이 50개(6.4%)였습니다. 4월 분석의 50은 더 말할 것도 없습니다. MaxRequestWorkers 400 은 근거 있는 설정이었습니다.
6. 권고
6-1. 지금 할 것 — 없음 (필수 변경 사항 없음)
모든 지표가 상한 대비 여유입니다. 서비스에 문제가 없으므로 긴급히 바꿀 설정은 없습니다.
6-2. 선택 — 성장 대비 (트래픽 1.7배 이상 증가가 예상될 때)
A. KeepAliveTimeout 5초 → 2초
KeepAliveTimeout 2
K 계수가 1.28 → 약 0.51 로 줄어 busy ≈ 0.97 × req/s 가 됩니다.
| 포화 요청률 | 9/17 피크 대비 여유 | |
|---|---|---|
| 현재 (5초) | 약 230 req/s | 1.74배 |
| 변경 후 (2초) | 약 410 req/s | 3.1배 |
메모리도 DB 연결도 늘지 않고 여유만 1.8배가 됩니다. 비용은 TCP·TLS 재핸드셰이크 증가인데, 피크 CPU 가 us+sy 최대 9% 라 충분히 흡수됩니다.
B. MaxConnectionsPerChild 0 → 10000
MaxConnectionsPerChild 10000
자식 프로세스를 영원히 재활용하지 않는 현재 설정은 mod_php 누수가 누적됩니다. 위험도 낮은 위생 조치입니다.
C. MinSpareServers 검토
idle=0 샘플이 39개(5.0%)이고 그때 busy 평균이 89였습니다. 예비 워커가 떨어지면 다음 요청은 fork 를 기다립니다. 13:40~13:50 구간처럼 busy 가 10 → 204 로 급등할 때 특히 영향이 있습니다.
현재 값을 먼저 확인해야 합니다.
grep -E 'MinSpareServers|MaxSpareServers' /etc/apache2/mods-enabled/mpm_prefork.conf
Debian 기본값(5 / 10)이라면 피크 busy 평균 53 에 비해 지나치게 작습니다. 20 / 50 정도로 올리면 fork 급증 구간이 완만해집니다.
적용은 무중단입니다.
sudo cp /etc/apache2/mods-available/mpm_prefork.conf /etc/apache2/mods-available/mpm_prefork.conf.bak-$(date +%Y%m%d)
sudo apache2ctl configtest && sudo systemctl reload apache2
6-3. 재측정
설정을 바꾸면 같은 스크립트로 하루 더 측정해 K 계수가 예상대로 내려갔는지 확인하세요. cron 은 평일 12:50 에 계속 돌고 있습니다.
6-4. 남은 확인 항목
- EC2 크레딧 모드 (Unlimited / Standard) — 9/17 steal 0%, 베이스라인 초과 0샘플이라 실질 위험은 낮음
- 외부에서
/server-status접근 차단 확인 - PHP
opcache.enable여부