들어가며
MySQL을 운영하면서 “왜 갑자기 느려졌지?”라는 상황을 마주하게 됩니다. 이 글에서는 MySQL(특히 InnoDB)의 핵심 모니터링 지표들과 문제 상황을 빠르게 파악하는 방법을 정리합니다.
모니터링 아키텍처
MySQL → mysqld_exporter → Prometheus → Grafana- mysqld_exporter: MySQL 메트릭을 Prometheus 형식으로 노출
- Prometheus: 메트릭 수집 및 저장
- Grafana: 시각화 및 알람
핵심 모니터링 지표
1. 커넥션 (Connections)
MySQL은 PostgreSQL과 달리 스레드 기반 아키텍처입니다. 커넥션당 OS 프로세스가 아닌 스레드를 할당하므로 메모리 오버헤드가 상대적으로 적습니다. 하지만 그렇다고 무한정 커넥션을 열 수 있는 것은 아닙니다.
커넥션 관련해서 두 가지 지표를 구분해야 합니다:
- Threads_connected: 현재 연결되어 있는 커넥션 수 (idle 포함)
- Threads_running: 실제로 쿼리를 실행 중인 스레드 수
Threads_connected가 높아도 대부분 idle 상태라면 큰 문제가 아닐 수 있습니다. 하지만 Threads_running이 높다면 실제로 동시에 많은 쿼리가 실행되고 있다는 의미이므로, CPU 경합이 발생할 수 있습니다.
-- 현재 커넥션 상태
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Threads_running';
SHOW VARIABLES LIKE 'max_connections';주요 지표:
mysql_global_status_threads_connected: 현재 연결된 클라이언트 수mysql_global_status_threads_running: 실제 쿼리 실행 중인 스레드 수mysql_global_variables_max_connections: 최대 허용 커넥션
알람 기준:
- 커넥션 사용률 70% 이상: Warning
- 커넥션 사용률 85% 이상: Critical
- Threads_running이 CPU 코어 수의 2배 이상: 조사 필요 (락 경합 또는 슬로우 쿼리 가능성)
# PromQL - 커넥션 사용률
mysql_global_status_threads_connected / mysql_global_variables_max_connections * 1002. 쿼리 처리량 (Query Throughput)
QPS(Queries Per Second)는 MySQL의 처리량을 나타내는 기본 지표입니다. 하지만 단순 QPS보다 쿼리 유형별 분포를 보는 것이 더 유용합니다.
Read/Write 비율을 알면:
- 읽기 위주 워크로드: Replica로 읽기 분산, 캐시 레이어 도입 고려
- 쓰기 위주 워크로드: Buffer Pool 튜닝, SSD 도입, 배치 처리 최적화
갑자기 특정 쿼리 유형이 급증하면 애플리케이션 변경이나 비정상 트래픽을 의심해볼 수 있습니다.
-- 쿼리 타입별 통계
SHOW GLOBAL STATUS LIKE 'Com_select';
SHOW GLOBAL STATUS LIKE 'Com_insert';
SHOW GLOBAL STATUS LIKE 'Com_update';
SHOW GLOBAL STATUS LIKE 'Com_delete';주요 지표:
mysql_global_status_commands_total{command="select"}: SELECT 쿼리 수mysql_global_status_commands_total{command="insert"}: INSERT 쿼리 수mysql_global_status_commands_total{command="update"}: UPDATE 쿼리 수mysql_global_status_commands_total{command="delete"}: DELETE 쿼리 수- QPS (Queries Per Second): 전체 쿼리 처리량
# PromQL - QPS
rate(mysql_global_status_queries[5m])
# Read/Write 비율
sum(rate(mysql_global_status_commands_total{command=~"select|show"}[5m])) /
sum(rate(mysql_global_status_commands_total{command=~"insert|update|delete"}[5m]))3. InnoDB Buffer Pool
Buffer Pool은 InnoDB의 핵심 캐시 영역입니다. 데이터 페이지와 인덱스를 메모리에 캐싱하여 디스크 I/O를 줄입니다. MySQL 성능의 80%는 Buffer Pool 설정에 달려있다고 해도 과언이 아닙니다.
Buffer Pool 크기 설정 가이드:
- 전용 DB 서버: 전체 RAM의 70~80%
- 다른 애플리케이션과 공유: 전체 RAM의 50% 이하
- 단, OS 파일 캐시를 위한 여유 공간도 필요
Buffer Pool Hit Ratio가 낮다는 것은 필요한 데이터가 메모리에 없어서 디스크를 읽는다는 의미입니다. 히트율 99% 이상을 목표로 하는 것이 좋습니다.
-- Buffer Pool 상태
SHOW ENGINE INNODB STATUS\G
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%';주요 지표:
mysql_global_status_innodb_buffer_pool_read_requests: 버퍼 풀 읽기 요청mysql_global_status_innodb_buffer_pool_reads: 디스크에서 읽은 횟수- Buffer Pool Hit Ratio = (read_requests - reads) / read_requests × 100
알람 기준:
- Buffer Pool Hit Ratio 99% 미만: Warning
- Buffer Pool Hit Ratio 95% 미만: Critical (innodb_buffer_pool_size 증설 검토)
# PromQL - Buffer Pool Hit Ratio
(mysql_global_status_innodb_buffer_pool_read_requests -
mysql_global_status_innodb_buffer_pool_reads) /
mysql_global_status_innodb_buffer_pool_read_requests * 1004. InnoDB Row Operations
Row 작업량은 실제 데이터 처리량을 보여줍니다. 쿼리 수(QPS)와 함께 봐야 의미가 있습니다.
예를 들어:
- QPS는 낮은데 rows_read가 높다면 → 한 쿼리가 많은 행을 스캔하고 있음 (풀 테이블 스캔 의심)
- QPS도 높고 rows_read도 높다면 → 정상적인 고부하 상태
rows_read와 실제 반환되는 행 수의 차이가 크다면, 인덱스가 제대로 활용되지 않거나 WHERE 조건이 비효율적일 수 있습니다.
SHOW GLOBAL STATUS LIKE 'Innodb_rows%';주요 지표:
mysql_global_status_innodb_rows_read: 읽은 행 수mysql_global_status_innodb_rows_inserted: 삽입한 행 수mysql_global_status_innodb_rows_updated: 갱신한 행 수mysql_global_status_innodb_rows_deleted: 삭제한 행 수
# PromQL - 초당 Row 작업량
rate(mysql_global_status_innodb_rows_read[5m])
rate(mysql_global_status_innodb_rows_inserted[5m])5. InnoDB I/O
디스크 I/O는 데이터베이스 성능의 최대 병목입니다. SSD를 사용하더라도 메모리 접근보다는 수십~수백 배 느립니다.
I/O 관련 지표를 볼 때 주의할 점:
- fsync는 데이터를 디스크에 확실히 기록하는 연산으로, 매우 비쌈
innodb_flush_log_at_trx_commit=1(기본값)은 트랜잭션마다 fsync → 안전하지만 느림innodb_flush_log_at_trx_commit=2는 1초마다 fsync → 빠르지만 장애 시 1초 데이터 유실 가능
I/O가 병목이라면:
- Buffer Pool 크기 증설로 캐시 히트율 높이기
- SSD로 스토리지 업그레이드
- 쿼리 최적화로 불필요한 I/O 줄이기
SHOW GLOBAL STATUS LIKE 'Innodb_data%';
SHOW GLOBAL STATUS LIKE 'Innodb_os_log%';주요 지표:
mysql_global_status_innodb_data_reads: 데이터 읽기 횟수mysql_global_status_innodb_data_writes: 데이터 쓰기 횟수mysql_global_status_innodb_data_fsyncs: fsync 호출 횟수mysql_global_status_innodb_os_log_fsyncs: 로그 fsync 횟수
# PromQL - IOPS
rate(mysql_global_status_innodb_data_reads[5m]) +
rate(mysql_global_status_innodb_data_writes[5m])6. 락과 데드락 (Locks & Deadlocks)
InnoDB는 Row-level locking을 사용합니다. 테이블 전체가 아닌 특정 행만 잠그므로 동시성이 좋지만, 여러 트랜잭션이 서로의 락을 기다리면 데드락이 발생합니다.
데드락 자체는 InnoDB가 자동으로 감지하고 한 트랜잭션을 롤백시켜 해결합니다. 문제는 데드락이 빈번하게 발생하면:
- 트랜잭션 재시도로 인한 성능 저하
- 애플리케이션 에러 로그 폭증
- 사용자 경험 악화
데드락을 줄이려면:
- 트랜잭션을 짧게 유지
- 여러 테이블 접근 시 항상 같은 순서로 접근
- 적절한 인덱스로 락 범위 최소화
-- 락 대기 상황
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
-- 데드락 정보
SHOW ENGINE INNODB STATUS\G -- LATEST DETECTED DEADLOCK 섹션 확인주요 지표:
mysql_global_status_innodb_row_lock_waits: Row 락 대기 횟수mysql_global_status_innodb_row_lock_time: Row 락 대기 시간 (ms)mysql_global_status_innodb_deadlocks: 데드락 발생 횟수
알람 기준:
- 데드락 발생 시 알람
- 평균 락 대기 시간 100ms 이상: Warning
# PromQL - 락 대기 발생률
rate(mysql_global_status_innodb_row_lock_waits[5m])
# 평균 락 대기 시간
mysql_global_status_innodb_row_lock_time / mysql_global_status_innodb_row_lock_waits7. 복제 지연 (Replication Lag)
MySQL 복제는 두 개의 스레드로 동작합니다:
- IO Thread: Primary의 binlog를 가져와서 Replica의 relay log에 저장
- SQL Thread: relay log를 읽어서 실제로 쿼리를 실행
Seconds_Behind_Master는 SQL Thread가 처리 중인 이벤트의 타임스탬프와 현재 시간의 차이입니다. 이 값이 크면 Replica에서 읽는 데이터가 오래된 것입니다.
복제 지연 원인:
- Primary의 쓰기 부하가 높아 binlog가 빠르게 생성됨
- Replica의 단일 SQL Thread가 병목 (MySQL 5.7+에서는 병렬 복제 가능)
- Replica에서 무거운 쿼리 실행 중
- 네트워크 지연
IO/SQL 스레드가 중단되면 복제가 완전히 멈추므로 Critical 알람이 필요합니다.
-- Replica에서 확인
SHOW SLAVE STATUS\G
-- Seconds_Behind_Master 확인주요 지표:
mysql_slave_status_seconds_behind_master: 복제 지연 시간mysql_slave_status_slave_io_running: IO 스레드 상태mysql_slave_status_slave_sql_running: SQL 스레드 상태
알람 기준:
- 복제 지연 10초 이상: Warning
- 복제 지연 60초 이상: Critical
- IO/SQL 스레드 중단: Critical
# PromQL - 복제 지연
mysql_slave_status_seconds_behind_master8. 슬로우 쿼리
슬로우 쿼리는 long_query_time(기본값 10초)보다 오래 걸린 쿼리입니다. 10초는 너무 길기 때문에 보통 1~2초로 낮춰서 설정합니다.
슬로우 쿼리 모니터링은 두 단계로 이루어집니다:
- 발생 감지: 슬로우 쿼리 카운트 모니터링
- 원인 분석: 슬로우 쿼리 로그 분석 (pt-query-digest 등 활용)
슬로우 쿼리가 급증했다면:
- 인덱스 누락 또는 통계 정보 오래됨
- 데이터 증가로 인한 쿼리 플랜 변경
- 락 경합으로 인한 대기 시간 증가
SHOW GLOBAL STATUS LIKE 'Slow_queries';주요 지표:
mysql_global_status_slow_queries: 슬로우 쿼리 누적 수
# PromQL - 슬로우 쿼리 발생률
rate(mysql_global_status_slow_queries[5m])9. 임시 테이블과 정렬
MySQL은 복잡한 쿼리(GROUP BY, ORDER BY, DISTINCT 등)를 처리할 때 임시 테이블을 생성합니다. 이 임시 테이블이 메모리(tmp_table_size, max_heap_table_size 중 작은 값)에 들어가면 빠르지만, 초과하면 디스크에 생성됩니다.
디스크 임시 테이블은 메모리 대비 수십~수백 배 느리므로, 디스크 임시 테이블 비율이 높다면:
tmp_table_size와max_heap_table_size증설- 쿼리 최적화로 임시 테이블 사용 줄이기
- TEXT/BLOB 컬럼이 있으면 무조건 디스크 사용하므로 SELECT에서 제외
SHOW GLOBAL STATUS LIKE 'Created_tmp%';
SHOW GLOBAL STATUS LIKE 'Sort_merge_passes';주요 지표:
mysql_global_status_created_tmp_tables: 생성된 임시 테이블 수mysql_global_status_created_tmp_disk_tables: 디스크에 생성된 임시 테이블 수mysql_global_status_sort_merge_passes: 정렬 시 머지 패스 횟수
알람 기준:
- 디스크 임시 테이블 비율 25% 이상: tmp_table_size, max_heap_table_size 증설 검토
# PromQL - 디스크 임시 테이블 비율
mysql_global_status_created_tmp_disk_tables /
mysql_global_status_created_tmp_tables * 100부하 상황에서의 모니터링
부하 유형별 관찰 포인트
읽기 부하 (Read-Heavy)
-- 읽기 부하 시 확인할 지표
SHOW GLOBAL STATUS LIKE 'Handler_read%';| 지표 | 의미 | 증가 시 의심 |
|---|---|---|
| Handler_read_rnd_next | 풀 테이블 스캔 | 인덱스 누락 |
| Handler_read_first | 인덱스 첫 항목 읽기 | 풀 인덱스 스캔 |
| Handler_read_key | 인덱스 키 조회 | 정상 (좋음) |
관찰 포인트:
- Buffer Pool Hit Ratio 하락
Handler_read_rnd_next급증 → 풀 테이블 스캔 발생- Threads_running 증가
쓰기 부하 (Write-Heavy)
-- 쓰기 부하 시 확인할 지표
SHOW ENGINE INNODB STATUS\G -- LOG 섹션 확인
SHOW GLOBAL STATUS LIKE 'Innodb_log_waits';관찰 포인트:
Innodb_log_waits증가: Redo Log 공간 부족Innodb_buffer_pool_wait_free증가: Buffer Pool에서 빈 페이지 대기- 복제 지연 증가
# PromQL - Log Waits 발생률
rate(mysql_global_status_innodb_log_waits[5m])혼합 부하
-- 락 경합 확인 (MySQL 8.0+)
SELECT * FROM performance_schema.data_locks WHERE LOCK_STATUS = 'WAITING';
-- 현재 실행 중인 쿼리
SELECT
id,
user,
host,
db,
time,
state,
LEFT(info, 100) as query
FROM information_schema.processlist
WHERE command != 'Sleep'
ORDER BY time DESC;부하 시 병목 지점 식별
-- 쿼리 실행 시간 분포 (Performance Schema)
SELECT
schema_name,
digest_text,
count_star as exec_count,
avg_timer_wait / 1000000000 as avg_latency_ms,
sum_rows_examined / count_star as avg_rows_examined
FROM performance_schema.events_statements_summary_by_digest
ORDER BY sum_timer_wait DESC
LIMIT 10;주요 확인 사항:
avg_rows_examinedvs 실제 반환 행 수 → 인덱스 효율성avg_latency_ms→ 쿼리 최적화 대상 선정
대용량 테이블 DDL 작업 모니터링
MySQL의 DDL은 버전과 상황에 따라 Online DDL 지원 여부가 다릅니다.
MySQL DDL 알고리즘
| 알고리즘 | 테이블 복사 | 동시 DML | 메타데이터 락 |
|---|---|---|---|
| INSTANT | X | O | 최소 |
| INPLACE | X | O (대부분) | 시작/끝에만 |
| COPY | O (전체) | X | 작업 전체 |
-- 알고리즘 명시적 지정
ALTER TABLE large_table ADD COLUMN new_col VARCHAR(100), ALGORITHM=INSTANT;
ALTER TABLE large_table ADD INDEX idx_col(column), ALGORITHM=INPLACE, LOCK=NONE;컬럼 추가 (ADD COLUMN)
-- MySQL 8.0.12+ INSTANT 지원 (테이블 끝에 추가 시)
ALTER TABLE large_table ADD COLUMN new_col VARCHAR(100); -- INSTANT
-- 기본값 있는 NOT NULL (INPLACE)
ALTER TABLE large_table ADD COLUMN new_col VARCHAR(100) NOT NULL DEFAULT 'value';
-- 특정 위치에 추가 (COPY - 느림!)
ALTER TABLE large_table ADD COLUMN new_col VARCHAR(100) AFTER existing_col;INSTANT 지원 조건 (MySQL 8.0+):
- 테이블 끝에 컬럼 추가
- NULL 허용 또는 DEFAULT 값 있음
- 가상 생성 컬럼
- 컬럼 삭제 (8.0.29+)
인덱스 추가 (ADD INDEX)
-- Online DDL로 인덱스 생성 (INPLACE)
ALTER TABLE large_table ADD INDEX idx_col(column), ALGORITHM=INPLACE, LOCK=NONE;
-- 또는
CREATE INDEX idx_col ON large_table(column) LOCK=NONE;모니터링 포인트:
-- DDL 진행 상황 확인 (MySQL 8.0+ Performance Schema)
SELECT
EVENT_NAME,
WORK_COMPLETED,
WORK_ESTIMATED,
ROUND(WORK_COMPLETED / WORK_ESTIMATED * 100, 2) as progress_pct
FROM performance_schema.events_stages_current
WHERE EVENT_NAME LIKE '%alter%';
-- 또는 SHOW PROCESSLIST에서 확인
SHOW PROCESSLIST\G
-- State: "altering table", "copy to tmp table" 등 확인pt-online-schema-change 사용 시
대용량 테이블에서는 Percona의 pt-online-schema-change를 사용하기도 합니다.
pt-online-schema-change \
--alter "ADD COLUMN new_col VARCHAR(100)" \
--execute \
D=mydb,t=large_table모니터링 포인트:
- 트리거 기반이므로 쓰기 부하 증가
- 복제 지연 체크 (—max-lag 옵션)
- 디스크 사용량 (임시 테이블)
-- pt-osc 진행 상황 확인
SHOW PROCESSLIST;
-- "_large_table_new" 테이블에 INSERT 진행 확인
-- 트리거 확인
SHOW TRIGGERS LIKE 'large_table'\GDDL 작업 시 알람 설정
# prometheus-rules.yml 추가
groups:
- name: mysql-ddl
rules:
- alert: MySQLLongRunningDDL
expr: |
mysql_info_schema_processlist_seconds{command="Query",state=~".*alter.*|.*copy.*"} > 300
for: 1m
labels:
severity: warning
annotations:
summary: "DDL 작업 5분 이상 진행 중"
- alert: MySQLHighMetadataLockWait
expr: |
increase(mysql_global_status_table_locks_waited[5m]) > 100
labels:
severity: warning
annotations:
summary: "메타데이터 락 대기 급증"안전한 DDL 작업을 위한 팁
-- 1. 테이블 크기 확인
SELECT
table_name,
ROUND(data_length / 1024 / 1024, 2) as data_mb,
ROUND(index_length / 1024 / 1024, 2) as index_mb,
table_rows
FROM information_schema.tables
WHERE table_schema = 'mydb' AND table_name = 'large_table';
-- 2. 메타데이터 락 대기 중인 세션 확인 (MySQL 8.0+)
SELECT * FROM performance_schema.metadata_locks
WHERE OBJECT_TYPE = 'TABLE' AND OBJECT_NAME = 'large_table';
-- 3. 락 타임아웃 설정
SET SESSION lock_wait_timeout = 5;
-- 4. DDL 전 장시간 트랜잭션 확인
SELECT
trx_id,
trx_state,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) as duration_sec,
trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;MySQL 8.0 Online DDL 지원 매트릭스
| 작업 | INSTANT | INPLACE | COPY |
|---|---|---|---|
| 끝에 컬럼 추가 | O | O | O |
| 중간에 컬럼 추가 | X | X | O |
| 컬럼 삭제 (8.0.29+) | O | O | O |
| 컬럼명 변경 | X | O | O |
| 인덱스 추가 | X | O | O |
| 인덱스 삭제 | X | O | O |
| PK 추가/삭제 | X | O (일부) | O |
Grafana 대시보드 구성
추천 패널 레이아웃
┌─────────────────────────────────────────────────────────┐
│ Overview: QPS, 커넥션, Buffer Pool Hit Ratio │
├─────────────────────────────────────────────────────────┤
│ Connections │ Query Rate (CRUD) │
├───────────────────────┼────────────────────────────────┤
│ Buffer Pool Usage │ InnoDB Row Operations │
├───────────────────────┼────────────────────────────────┤
│ InnoDB I/O │ Lock Waits / Deadlocks │
├─────────────────────────────────────────────────────────┤
│ Replication Status │ Slow Queries │
└─────────────────────────────────────────────────────────┘커뮤니티 대시보드
- MySQL Overview (ID: 7362)
- MySQL InnoDB Metrics (ID: 7991)
- Percona PMM MySQL (ID: 7371)
mysqld_exporter 설정
# docker-compose.yml
services:
mysqld_exporter:
image: prom/mysqld-exporter
environment:
DATA_SOURCE_NAME: "exporter:password@(mysql:3306)/"
ports:
- "9104:9104"
command:
- "--collect.info_schema.innodb_metrics"
- "--collect.info_schema.processlist"
- "--collect.info_schema.tables"
- "--collect.slave_status"MySQL 유저 생성
CREATE USER 'exporter'@'%' IDENTIFIED BY 'password';
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'%';
FLUSH PRIVILEGES;알람 규칙 예시
# prometheus-rules.yml
groups:
- name: mysql
rules:
- alert: MySQLHighConnections
expr: mysql_global_status_threads_connected / mysql_global_variables_max_connections > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "MySQL 커넥션 사용률 80% 초과"
- alert: MySQLBufferPoolLow
expr: |
(mysql_global_status_innodb_buffer_pool_read_requests -
mysql_global_status_innodb_buffer_pool_reads) /
mysql_global_status_innodb_buffer_pool_read_requests < 0.95
for: 10m
labels:
severity: warning
annotations:
summary: "MySQL Buffer Pool Hit Ratio 95% 미만"
- alert: MySQLReplicationLag
expr: mysql_slave_status_seconds_behind_master > 30
for: 1m
labels:
severity: critical
annotations:
summary: "MySQL 복제 지연 30초 초과"
- alert: MySQLDeadlock
expr: increase(mysql_global_status_innodb_deadlocks[5m]) > 0
labels:
severity: warning
annotations:
summary: "MySQL 데드락 발생"PostgreSQL과의 차이점
| 항목 | PostgreSQL | MySQL (InnoDB) |
|---|---|---|
| 캐시 | shared_buffers | Buffer Pool |
| MVCC 정리 | VACUUM | Purge Thread (자동) |
| 프로세스 모델 | 프로세스 기반 | 스레드 기반 |
| 복제 확인 | pg_stat_replication | SHOW SLAVE STATUS |
정리
MySQL(InnoDB) 모니터링의 핵심 포인트:
- 커넥션: max_connections 대비 사용률, Threads_running 주시
- Buffer Pool: 99% 이상의 히트율 유지
- 락: Row 락 대기와 데드락 모니터링
- 복제: Seconds_Behind_Master와 스레드 상태 확인
- I/O: 디스크 임시 테이블 비율과 fsync 횟수 추적
다음 글에서는 Redis 모니터링에 대해 다룹니다.