MySQL 모니터링 - InnoDB 핵심 지표와 대시보드 구성

2025년 12월 05일

monitoring

# MySQL# InnoDB# Monitoring# Grafana# Prometheus

들어가며

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 * 100

2. 쿼리 처리량 (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 * 100

4. 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_waits

7. 복제 지연 (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_master

8. 슬로우 쿼리

슬로우 쿼리는 long_query_time(기본값 10초)보다 오래 걸린 쿼리입니다. 10초는 너무 길기 때문에 보통 1~2초로 낮춰서 설정합니다.

슬로우 쿼리 모니터링은 두 단계로 이루어집니다:

  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_sizemax_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_examined vs 실제 반환 행 수 → 인덱스 효율성
  • 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'\G

DDL 작업 시 알람 설정

# 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                  │
└─────────────────────────────────────────────────────────┘

커뮤니티 대시보드

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) 모니터링의 핵심 포인트:

  1. 커넥션: max_connections 대비 사용률, Threads_running 주시
  2. Buffer Pool: 99% 이상의 히트율 유지
  3. : Row 락 대기와 데드락 모니터링
  4. 복제: Seconds_Behind_Master와 스레드 상태 확인
  5. I/O: 디스크 임시 테이블 비율과 fsync 횟수 추적

다음 글에서는 Redis 모니터링에 대해 다룹니다.


참고 자료

© 2025, 미나리와 함께 만들었음