Files
chocomae/doc/plan/db/260916-load-and-capacity-check/01-web-tier-analysis.md
T

382 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 웹 계층(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](../260915-improve-production/07-post-analysis.md) (인덱스·쿼리 개선, 9/15 23:00 적용)
> - 관련 문서: [00-check-methods.md](00-check-methods.md) (측정 방법), [09-aws-db-separation.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](../../../../src/web/main/index.html#L36)).
- **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](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 설정 현황
```bash
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](../../../../ssl/README#L7)), `chocomae.jinaju.com` 은 http 전용 |
### 3-3. DB 분리 검토([09번 문서](../09-aws-db-separation.md))에 주는 시사점
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` 접근 차단 확인 (도메인 정정 후 재시도)
```bash
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-status
```
`403` 이면 정상. 리버스 프록시를 거치면 `Require local` 이 프록시 IP를 통과시킬 수 있어 실제 응답 확인이 필요합니다.
- [ ] EC2 크레딧 모드가 Unlimited 인지 Standard 인지 (콘솔 → EC2 → 인스턴스 → 크레딧 사양, 또는 CloudWatch `CPUSurplusCreditsCharged`)
- [ ] PHP `opcache.enable` / `memory_consumption` / `memory_limit`
```bash
grep -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/apache_20260917-1250.log), [result/peaktime/vmstat_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초**
```apache
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**
```apache
MaxConnectionsPerChild 10000
```
자식 프로세스를 영원히 재활용하지 않는 현재 설정은 mod_php 누수가 누적됩니다. 위험도 낮은 위생 조치입니다.
**C. `MinSpareServers` 검토**
`idle=0` 샘플이 39개(5.0%)이고 그때 busy 평균이 89였습니다. 예비 워커가 떨어지면 다음 요청은 fork 를 기다립니다. 13:40~13:50 구간처럼 busy 가 10 → 204 로 급등할 때 특히 영향이 있습니다.
현재 값을 먼저 확인해야 합니다.
```bash
grep -E 'MinSpareServers|MaxSpareServers' /etc/apache2/mods-enabled/mpm_prefork.conf
```
Debian 기본값(5 / 10)이라면 피크 busy 평균 53 에 비해 지나치게 작습니다. 20 / 50 정도로 올리면 fork 급증 구간이 완만해집니다.
적용은 무중단입니다.
```bash
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` 여부