7편은 캐시로, 8편은 이미지로 속도를 잡았다. 이번 편은 서버 안쪽 — OPcache, PHP-FPM 워커 수, MariaDB 버퍼풀을 손볼 차례였다. 검색하면 “이 설정 복붙하면 빨라진다”는 글이 넘친다. 나도 그 값들을 그대로 넣고 before/after를 쟀다. 그리고 결론은 예상과 정반대였다. 튜닝했는데 안 빨라졌다. 그런데 이 “안 빨라졌다”가 사실 가장 중요한 발견이었다.
결론부터. 1 vCPU·2GB 서버에서 OPcache와 DB 버퍼풀은 이미 최적이라 손댈 게 없었고, PHP-FPM 워커를 10개로 늘리자 처리량이 오히려 살짝 떨어졌다(8.81 → 8.35 req/s). 워커를 4개로 줄이니 메모리를 아끼면서 처리량은 동등(최고 9.13)했다. 단일 응답 속도(MISS TTFB)는 튜닝 전후 중앙값이 121.9ms → 121.7ms, 사실상 변화 없음. 교훈은 하나다. 측정 없이 복붙 튜닝하지 마라. 작은 서버에선 성급한 튜닝이 독이 된다.
캐시가 안 통하는 순간
7편 FastCGI 캐시는 HIT일 때만 빠르다. 하지만 캐시가 없는 순간이 있다. 첫 방문자, 캐시 만료 직후, 로그인한 관리자. 이때는 PHP와 MariaDB가 페이지를 통째로 다시 조립한다. 이 MISS 경로의 속도가 이번 편의 대상이다.
측정은 URL 뒤에 ?nocache=난수를 붙여서 했다. 7편에서 “쿼리스트링이 붙으면 캐시를 건너뛴다”는 규칙을 넣어놨기 때문에, 이렇게 하면 매 요청이 캐시를 우회해 순수한 PHP 렌더링 시간을 반복해서 잴 수 있다.
튜닝 3종과 2GB 메모리 배분
손댄 건 세 가지다. OPcache(컴파일된 PHP를 메모리에 저장), PHP-FPM 워커 수(동시에 처리할 프로세스 개수), MariaDB 버퍼풀(DB 데이터를 램에 상주). 그런데 셋 다 같은 2GB를 나눠 쓴다. 그래서 각자 욕심내면 총합이 램을 넘어 스왑이 생기고, 스왑이 생기면 디스크를 읽느라 오히려 느려진다. 이 균형이 작은 서버 튜닝의 핵심이다.
| 구성요소 | 대략 배분 | 메모 |
|---|---|---|
| OS + Nginx | 약 300MB | 고정 |
| MariaDB (버퍼풀 256M 포함) | 약 400MB | DB가 148M라 전부 램에 |
| PHP-FPM 워커 | 4 × 78MB ≈ 312MB | 워커 수로 조절 |
| OS 파일 캐시 여유 | 나머지 약 1GB | 이게 있어야 빠름 |
워커 1개가 약 78MB를 먹는다(실측). 여기서 이미 답이 보인다. 워커를 10개로 잡으면 780MB, 12개면 936MB다. 2GB에서 그만큼을 PHP가 점유하면 OS가 파일을 캐싱할 여유가 사라진다. 그런데도 “max_children은 크게”라는 조언이 많다. 정말 그런지 재봤다.
실측 1. 단일 MISS TTFB — 거의 안 변했다
| 구분 | 평균 | 중앙값 | 최소~최대 |
|---|---|---|---|
| 튜닝 전 | 130.2ms | 121.9ms | 108~185ms |
| 튜닝 후 | 123.1ms | 121.7ms | 111~150ms |
평균이 조금 줄어 보이지만, 그건 튜닝 전의 185ms 튐값 하나 때문이다. 중앙값은 121.9 → 121.7로 사실상 동일하다. 단일 요청 속도는 튜닝으로 바뀌지 않았다.
왜? OPcache는 이미 켜져 있었다
튜닝 전에 현재 설정부터 확인했는데, 우분투 php8.3-fpm에는 OPcache가 처음부터 켜져 있었다.
grep -rE "opcache.enable|memory_consumption" /etc/php/8.3/...
# opcache.enable=1
# opcache.memory_consumption=128
이미 `enable=1`에 128MB가 잡혀 있었다. 내가 넣으려던 값과 사실상 같다. DB도 확인해보니 148MB밖에 안 됐다. 이 정도 크기는 리눅스가 알아서 OS 파일 캐시에 통째로 올려둔다. 즉 OPcache도 DB 캐싱도 이미 최적 상태였고, 내가 손댈 여지가 처음부터 없었던 것이다.
OPcache 상태는 opcache_get_status()로 실측할 수 있다. 튜닝 후 확인하니 hit_rate 74.5%, 캐시된 스크립트 1,069개, 사용 메모리 48MB였다. 128MB 중 48MB만 쓰니 여유도 충분하다. 이미 잘 돌고 있었다는 뜻이다.
실측 2. 부하 테스트 — 그리고 측정의 함정
단일 속도가 안 변했으니, 진짜 관심사는 동시 접속 처리량이다. ab -n 100 -c 10(동시 10, 총 100요청)으로 쟀다. 그런데 여기서 나는 하마터면 틀린 결론을 낼 뻔했다.
| 설정 | 처리량 | 상태 |
|---|---|---|
| 튜닝 전 (max_children=5 기본) | 8.81 req/s | warm |
| 튜닝 후 (max_children=10) | 5.98 req/s | ❌ cold (재시작 직후) |
| 튜닝 후 (max_children=10) | 8.30 / 8.39 req/s | warm 재측정 |
| 튜닝 후 (max_children=4) | 8.52 / 9.13 req/s | warm |
처음 튜닝 후 측정은 5.98이 나왔다. “튜닝했더니 32%나 느려졌다!”고 생각했다. 그런데 이 측정은 systemctl restart 바로 직후였다. 재시작하면 OPcache도 InnoDB 버퍼풀도 텅 빈 콜드 상태가 된다. 캐시가 데워지지 않은 서버를 잰 것이다.
사이트를 몇 번 눌러 데운 뒤 다시 재니 8.30, 8.39로 회복했다. 5.98은 튜닝 탓이 아니라 콜드 캐시 착시였다. 하마터면 “이 튜닝은 나쁘다”고 잘못 결론 낼 뻔했다.
재시작 직후 측정은 믿지 마라. 서버를 재시작하면 캐시가 비워진다. 이 상태의 성능은 실제 운영 상태(캐시가 데워진)와 다르다. before/after를 비교할 땐 반드시 양쪽 다 충분히 데운 warm 상태로 재야 한다. 이 하나를 놓치면 튜닝의 효과를 정반대로 읽게 된다.
1 vCPU에서 워커를 늘리면 왜 느려지나
공정하게(warm) 비교하니 그림이 분명해졌다. 워커 10개는 8.35, 4개는 8.82(최고 9.13). 기본 5개(8.81)와 비교하면, 늘린 쪽이 오히려 낮고 줄인 쪽이 미세하게 높다.
이유는 코어가 하나뿐이라서다. vCPU가 1개인데 워커를 10개 띄우면, 동시 요청 10개가 코어 하나를 두고 서로 밀친다. 실제 계산은 한 번에 하나씩만 되는데 프로세스만 많으니, 순서를 바꿔가며 처리하는 컨텍스트 스위칭 비용만 늘어난다. “워커 많이 = 빠름”은 코어가 여러 개일 때 이야기다. 1코어에선 반대로 작동한다.
그래서 뭘 남겼나 — 최종 설정
실측이 가리키는 대로 워커를 4개로 낮췄다. 처리량은 기본과 동등하거나 살짝 낫고, 메모리는 아낀다. OPcache와 버퍼풀은 이미 최적이라 그대로 뒀다.
| 항목 | 최종값 | 근거 |
|---|---|---|
| pm.max_children | 4 | 1 vCPU, 실측 최고 처리량 |
| opcache.enable | 1 (기존 유지) | 이미 켜져 있었음 |
| innodb_buffer_pool_size | 256M | DB 148M가 전부 램에 |
| 스왑 사용 | 거의 0 | 램 넘침 없음, 안전 |
튜닝 후 스왑을 확인하니 사실상 0이었다. 메모리를 무리하게 잡지 않아 스왑이 안 생겼고, 그래서 느려질 이유도 없다. 이 “안전한 배분”이 이번 편이 실제로 얻은 것이다.
솔직한 결론 — 측정 없이 튜닝하지 마라
이 글은 “이렇게 하면 빨라졌다”로 끝나지 않는다. 오히려 반대다. 재보니 튜닝할 게 없었고, 억지로 늘린 설정은 손해였다. 그리고 그게 잘못된 게 아니다. 우분투의 기본값과 이미 켜진 OPcache가 이 규모에는 충분했던 것뿐이다.
그러니 검색해서 나온 “고사양용 튜닝 값”을 작은 VPS에 그대로 복붙하는 건 위험하다. 남의 8코어·16GB 서버에 맞는 값이 내 1코어·2GB에선 독이 될 수 있다. 정답은 하나다. 내 서버에서 직접 before/after를 재보는 것. 그리고 잴 때는 콜드/웜을 반드시 맞출 것. 이 두 가지가 이번 편에서 값비싸게 배운 교훈이다.
다음 편 예고 — 백업과 재해복구 1편
속도는 세 편으로 마무리. 이제 안정성이다. 자체 서버를 굴린다는 건 백업도 내 몫이라는 뜻이다. 서버가 한 번 죽으면 그동안 쓴 글이 통째로 사라진다. 다음 편부터는 DB와 파일을 자동으로 백업하고, 원격에 보관하고, 실제로 날려본 뒤 복구까지 해본다. 역시 실전 기록으로.