용량 계획: CPU, memory, IOPS, connection, disk growth 예측
장애가 나기 전에 알아야 한다
디스크가 꽉 차서 PostgreSQL이 새 WAL을 쓰지 못한 순간, 서비스는 쓰기 불가 상태가 된다. max_connections에 도달해 "Too many connections" 에러가 쏟아지는 순간, 앱 서버는 DB에 붙지 못한다. 이 두 가지 장애의 공통점은 미리 알 수 있었다는 것이다.
용량 계획(capacity planning)은 현재 사용량을 측정하고, 성장 추세를 모델링해, 자원이 임계치에 도달하기 전에 대응하는 활동이다. 장애 대응보다 훨씬 싸다. 그리고 대부분의 경우, 다음 6개월치 성장은 지난 6개월 데이터에 이미 들어 있다.
네 가지 자원 축
집계, 연결 처리 포화 시: 쿼리 지연 증가
CPU wait ↑, load avg ↑
목표: 피크 70% 이하
정렬·해시 버퍼
연결당 스택·힙 포화 시: 캐시 미스 급증
IOPS 폭발, 스왑 사용
목표: 스왑 0MB
쓰기: 커밋, WAL/binlog
체크포인트, 플러시 포화 시: I/O wait 급증
쿼리 지연 수 배 증가
목표: 피크 70% 이하
max_connections 한도
커넥션 풀로 다중화 포화 시: 즉시 연결 거부
앱 전체 장애로 직결
목표: 피크 70% 이하
각 축은 독립적으로 포화될 수 있다. CPU는 여유롭지만 IOPS가 터지는 경우, 메모리는 충분한데 connection이 가득 차는 경우 모두 가능하다. 따라서 네 축을 개별적으로 추적하고 예측해야 한다.
CPU 용량 계획
무엇을 측정하는가
| 지표 | 도구 | 의미 |
|---|---|---|
| CPU 사용률 (%) | top, vmstat, Prometheus node_cpu_seconds_total | 전체 부하 수준 |
| CPU I/O wait (%) | iostat, vmstat wa | 스토리지 병목 여부 |
| DB process CPU | top -p <pid>, pidstat | DB 프로세스 점유율 |
| 쿼리별 CPU | MySQL: performance_schema, PG: pg_stat_statements | 상위 소비 쿼리 |
성장 모델링
CPU 사용률이 매달 3%p씩 증가한다면 12개월 후 현재보다 36%p 높다. 피크 기준 50%에서 출발한다면 12개월 후 86%로, 여유가 14%p밖에 없다.
-- PostgreSQL: 상위 CPU 소비 쿼리 (pg_stat_statements)
SELECT query,
calls,
total_exec_time / calls AS avg_ms,
rows / calls AS avg_rows
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;CPU가 선형으로 증가한다면 추가 쿼리 최적화 여지가 있는지 먼저 본다. 인덱스 누락, 풀스캔이 CPU를 많이 먹는 경우가 많다.
Memory 용량 계획
Buffer Pool / shared_buffers
MySQL과 PostgreSQL 모두 핵심 메모리 캐시가 있다.
| 엔진 | 파라미터 | 권장값 | 역할 |
|---|---|---|---|
| MySQL InnoDB | innodb_buffer_pool_size | 전체 RAM의 70~80% | 데이터·인덱스 캐시 |
| PostgreSQL | shared_buffers | 전체 RAM의 25~30% | 공유 버퍼 (OS 캐시도 활용) |
| PostgreSQL | effective_cache_size | 전체 RAM의 70~75% | 플래너 힌트 (실제 할당 아님) |
Buffer pool hit ratio가 낮아지기 시작하면 메모리 부족 신호다.
-- MySQL: buffer pool hit ratio
SELECT (1 - Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests) * 100
AS hit_ratio_pct
FROM (
SELECT variable_value AS Innodb_buffer_pool_reads
FROM performance_schema.global_status
WHERE variable_name = 'Innodb_buffer_pool_reads'
) r,
(
SELECT variable_value AS Innodb_buffer_pool_read_requests
FROM performance_schema.global_status
WHERE variable_name = 'Innodb_buffer_pool_read_requests'
) rr;
-- 목표: 99% 이상. 95% 아래면 buffer pool 증설 검토.
-- PostgreSQL: shared buffer hit ratio
SELECT blks_hit::float / (blks_hit + blks_read) * 100 AS hit_ratio_pct
FROM pg_stat_database
WHERE datname = current_database();연결당 메모리
MySQL은 연결당 평균 4~10 MB를 사용한다. PostgreSQL은 연결당 5~10 MB 정도다.
총 연결 메모리 = max_connections × 연결당 메모리
예: 500 × 8 MB = 4 GB → 이 외에 buffer pool/shared_buffers 메모리가 필요메모리 총량에서 OS 예비(10%), buffer pool, 그 외 용도를 빼고 남은 것이 연결용 메모리다.
IOPS 용량 계획
IOPS가 중요한 이유
데이터가 메모리 캐시에 없으면 디스크에서 읽어야 한다. 쓰기는 WAL/binlog 커밋마다, 체크포인트마다 발생한다. IOPS 포화는 쿼리 지연을 수 배~수십 배까지 올린다.
IOPS 예산 분배 (예시, NVMe SSD 10,000 IOPS 기준)
읽기 IOPS (캐시 미스) : ~3,000
쓰기 IOPS (WAL/binlog) : ~2,000
체크포인트 / 백그라운드 플러시: ~2,000
예비 헤드룸 : ~3,000
─────────
10,000측정 지표
# 시스템 레벨
iostat -x 1 # r/s, w/s, %iowait, await(ms)
iotop # 프로세스별 I/O
# MySQL
SHOW GLOBAL STATUS LIKE 'Innodb_data_reads';
SHOW GLOBAL STATUS LIKE 'Innodb_data_writes';
SHOW GLOBAL STATUS LIKE 'Innodb_os_log_written';
# PostgreSQL
SELECT * FROM pg_stat_bgwriter; -- buffers_clean, buffers_checkpoint
SELECT * FROM pg_stat_io; -- 16+: 엔진 내 I/O 상세innodb_io_capacity 설정
# MySQL: 스토리지 속도에 맞춰야 백그라운드 플러시가 제때 동작
innodb_io_capacity = 4000 # SATA SSD
innodb_io_capacity = 8000 # NVMe
innodb_io_capacity_max = 20000 # 피크 허용 상한Connection 용량 계획
Connection 포화는 가장 즉각적인 장애다. 연결 시도가 max_connections 초과하는 순간, 이후 모든 연결 시도는 에러를 받는다.
올바른 max_connections 산정
안전한 동시 연결 수 = 가용 RAM (연결용) / 연결당 메모리
예: 4 GB / 8 MB = 500 연결
단, MySQL은 동시 64~128개 연결에서 처리량이 피크에 달하고
그 이상에서는 컨텍스트 스위칭 비용으로 처리량이 하락한다.
max_connections은 1,000~2,000 이하로 유지하는 것이 실용적이다.-- MySQL: 현재 연결 사용률
SELECT variable_value AS max_connections
FROM performance_schema.global_variables
WHERE variable_name = 'max_connections';
SHOW GLOBAL STATUS LIKE 'Max_used_connections'; -- 역대 피크
SHOW GLOBAL STATUS LIKE 'Threads_connected'; -- 현재 연결 수
-- PostgreSQL
SHOW max_connections;
SELECT count(*) FROM pg_stat_activity;
SELECT count(*) FROM pg_stat_activity WHERE state = 'active';커넥션 풀로 다중화
앱 서버가 늘어날수록 DB 연결 수가 선형으로 증가한다. 커넥션 풀러(PgBouncer, ProxySQL)는 수백 개의 앱 연결을 수십 개의 DB 연결로 다중화한다.
10 conn 앱 서버 2
10 conn 앱 서버 N
10 conn
N×10 conn
(포화 위험)
10 conn 앱 서버 2
10 conn 앱 서버 N
10 conn
ProxySQL
(다중화) → DB
50~100 conn
(안정)
Disk Growth 예측
디스크는 성장이 가장 예측 가능하고, 포화 시 장애가 가장 치명적인 자원이다.
성장률 측정
-- MySQL: 데이터베이스별 크기 추이 (information_schema)
SELECT table_schema,
ROUND(SUM(data_length + index_length) / 1024 / 1024 / 1024, 2) AS size_gb
FROM information_schema.tables
GROUP BY table_schema
ORDER BY size_gb DESC;
-- PostgreSQL: 데이터베이스 크기
SELECT pg_database.datname,
pg_size_pretty(pg_database_size(pg_database.datname)) AS size
FROM pg_database
ORDER BY pg_database_size(pg_database.datname) DESC;
-- PostgreSQL: 테이블별 크기 상위 10
SELECT schemaname, tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS total_size
FROM pg_tables
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 10;Prometheus predict_linear로 디스크 소진 예측
-- 현재 추세가 이어지면 디스크 언제 꽉 차는가
predict_linear(node_filesystem_free_bytes{mountpoint="/var/lib/mysql"}[7d], 3600 * 24 * 30)
-- 30일 후 남은 공간 예측 (GB)
predict_linear(node_filesystem_free_bytes{mountpoint="/var/lib/mysql"}[7d], 86400 * 30)
/ (1024^3)
-- 알림: 7일 추세로 24시간 내 1GB 이하 예상 시 경보
predict_linear(node_filesystem_free_bytes[7d], 3600 * 24) / (1024^3) < 1성장 모델 선택
예: +20 GB/월
성숙한 서비스, 사용자 수 안정 예측: 현재 + (성장률 × 기간)
12개월 후 = 현재 + 240 GB
예: +5%/월
성장 중 서비스, 사용자 급증 예측: 현재 × (1 + 성장률)^기간
12개월 후 = 현재 × 1.05^12 ≈ ×1.8
헤드룸: 피크 기준 30% 이상
목표 사용률: ≤ 70% 불확실성 버퍼 = 지수-선형 차이
리드 타임(증설 소요 시간) 고려
클라우드: 몇 분 / 온프레미스: 몇 주
용량 알림 임계값 설계
경보는 문제가 생기기 전에 울려야 한다. 이미 100%에서 울리면 늦다.
| 자원 | 경고 임계값 | 위험 임계값 | 비고 |
|---|---|---|---|
| CPU 사용률 | 70% (5분 평균) | 90% (1분 평균) | 쿼리 최적화 or 스케일업 검토 |
| Memory 스왑 사용 | 1 MB 이상 | 100 MB 이상 | 스왑 자체가 경고 신호 |
| Buffer pool hit ratio | < 98% | < 95% | 메모리 증설 또는 쿼리 최적화 |
| IOPS 사용률 | 70% | 90% | 캐시 튜닝 or 스토리지 업그레이드 |
| 디스크 사용률 | 70% | 85% | 용량 증설 리드타임 고려 |
| 연결 사용률 | 70% | 85% | 풀러 도입 또는 max_connections 조정 |
| 복제 지연 | 10초 이상 | 60초 이상 | 피크 IOPS, long trx 원인 분석 |
용량 계획 주기와 루틴
월간 용량 리뷰 체크리스트
□ 지난 30일 각 자원(CPU/메모리/IOPS/연결/디스크)의 피크 추이 확인
□ 6개월 선형·지수 성장 예측 업데이트
□ 예측 소진 시점 계산: 모든 축에서 70% 도달 예상일
□ 증설 리드타임(클라우드 분~시간 / 온프레미스 주~월) 대비 여유 있는가
□ 최근 스키마 변경, 신규 배치 등 성장률 변화 요인 없는가
□ 이벤트(대규모 캠페인, 배치 작업) 앞두고 추가 준비 필요한가기억할 문장
용량 계획의 목표는 70% 이하를 유지하는 것이다. 자원이 포화되면 장애는 순식간에 오고, 회복하는 데 예상보다 오래 걸린다. "곧 증설할 거니까 괜찮다"는 함정이다. 리드타임 전에 경보가 울리도록 임계값을 미리 낮게 잡아야 한다.
다음 장에서는 스키마 변경, 인덱스, 파라미터 변경이 얼마나 위험한지, 그리고 안전하게 적용하고 롤백하는 방법을 다룬다.
References
- JusDB Blog, Database Capacity Planning: From Metrics to Growth Projections — https://www.jusdb.com/blog/database-capacity-planning-metrics-growth
- DBTA, Database Capacity Planning (DBA Corner) — https://www.dbta.com/Columns/DBA-Corner/Database-Capacity-Planning-156629.aspx
- ManageEngine, Database monitoring for capacity planning — https://www.manageengine.com/products/applications_manager/blog/database-monitoring-for-capacity-planning.html
- MinervaDB, Forecast MySQL IOPS — https://minervadb.xyz/forecast-mysql-iops/
- Crunchy Data Blog, Understanding Postgres IOPS — https://www.crunchydata.com/blog/understanding-postgres-iops
- MySQL Documentation, Connection Handling and Scaling — https://dev.mysql.com/blog-archive/mysql-connection-handling-and-scaling/
- JusDB Blog, How to Tune max_connections for Your MySQL Workload — https://oneuptime.com/blog/post/2026-03-31-mysql-how-to-tune-maxconnections-for-your-mysql-workload/view
- Sysdig Blog, Top metrics in PostgreSQL monitoring with Prometheus — https://www.sysdig.com/blog/postgresql-monitoring
- Oracle OCI, Database Capacity Planning — https://docs.oracle.com/en-us/iaas/operations-insights/doc/database-capacity-planning.html
- Alibaba Cloud, Cloud Capacity Planning Principles & Best Practices — https://www.alibabacloud.com/help/en/well-architected/latest/capacity-planning