wait/io/table/sql/handler로 테이블 접근 병목 진단하기
Performance Schema의 table handler 대기와 테이블별 I/O 요약을 연결해 접근량, 지연 시간, 읽기 유형별 병목을 진단하는 방법을 정리한다.
1. 왜 이 지표를 확인해야 하는가
MySQL에서 특정 테이블을 사용하는 문장이 느리다는 사실만으로는 원인을 좁히기 어렵다. 스토리지 장치가 느린 것인지, 인덱스를 타지 못해 지나치게 많은 행을 읽는 것인지, 짧은 조회가 너무 자주 실행되는 것인지, 쓰기 작업이 한 테이블에 집중되는 것인지 구분해야 한다.
Performance Schema의 wait/io/table/sql/handler는 SQL 계층이 스토리지 엔진의 table handler를 통해 행을 읽고 쓰는 작업을 계측하는 wait instrument다. 이 지표와 performance_schema.table_io_waits_summary_by_table을 함께 보면 다음 질문에 답할 수 있다.
- 어느 테이블에 handler 작업이 집중되는가?
- 시간 합계가 큰 이유가 호출 횟수 때문인가, 호출당 지연 때문인가?
- 읽기와 쓰기 중 어느 방향이 지배적인가?
- 읽기라면
FETCH, 쓰기라면INSERT,UPDATE,DELETE중 무엇이 많은가? - 튜닝 전후에 접근량과 지연이 실제로 줄었는가?
다만 이름에 io가 포함되어 있어도 이 값을 물리 디스크 I/O 시간으로 곧바로 해석해서는 안 된다. handler 계측은 SQL 계층과 스토리지 엔진 사이의 테이블 접근 작업을 나타낸다. InnoDB Buffer Pool에서 처리된 논리적 읽기, 인덱스 탐색, 행 접근량, 동시성 영향이 함께 반영될 수 있다. 디스크 병목 여부는 별도의 file I/O, Buffer Pool, 운영체제 지표와 교차 검증해야 한다.
2. 계측 경로와 요약 테이블의 관계
SQL 문장이 테이블을 읽을 때 서버는 handler API를 통해 스토리지 엔진에 행 탐색을 요청한다. Performance Schema는 이 경계에서 발생한 작업을 계측하고 여러 수준의 summary table에 누적한다.
flowchart LR
A[클라이언트 SQL] --> B[Parser와 Optimizer]
B --> C[Executor]
C --> D[table handler API]
D --> E[InnoDB 인덱스와 레코드 접근]
E --> F[Buffer Pool]
F -. miss .-> G[데이터 파일 I/O]
D --> H[wait/io/table/sql/handler]
H --> I[events_waits_summary_global_by_event_name]
H --> J[table_io_waits_summary_by_table]
J --> K[sys.schema_table_statistics]
세 관측점의 역할은 서로 다르다.
| 관측점 | 답하는 질문 | 주요 한계 |
|---|---|---|
events_waits_summary_global_by_event_name |
서버 전체에서 table handler 계측이 얼마나 누적됐는가 | 어느 테이블인지 알 수 없다 |
table_io_waits_summary_by_table |
스키마·테이블별 접근 횟수와 시간이 어떻게 분포하는가 | 어떤 SQL이 유발했는지 직접 보여주지 않는다 |
sys.schema_table_statistics |
원시 timer를 사람이 읽기 쉬운 지연 시간으로 어떻게 볼 것인가 | sys view이므로 원본 컬럼 전체를 제공하지 않는다 |
실무 진단에서는 전역 event로 현상이 존재하는지 확인하고, 테이블별 summary로 범위를 좁힌 다음, statement digest와 실행 계획으로 원인 SQL을 찾는 순서가 효율적이다.
3. instrument 활성화 상태 확인
먼저 대상 instrument가 존재하고 ENABLED, TIMED 상태인지 확인한다. wait/lock/table/sql/handler는 이름이 비슷하지만 테이블 잠금 계측이며, 이 글의 table I/O 계측과 구분해야 한다.
SELECT VERSION() AS mysql_version;
SELECT NAME, ENABLED, TIMED
FROM performance_schema.setup_instruments
WHERE NAME IN (
'wait/io/table/sql/handler',
'wait/lock/table/sql/handler'
)
ORDER BY NAME;
실행 결과(MySQL 8.0.x):
mysql> SELECT VERSION() AS mysql_version;
+---------------+
| mysql_version |
+---------------+
| 8.0.46 |
+---------------+
1 row in set (0.00 sec)
mysql> SELECT NAME, ENABLED, TIMED
-> FROM performance_schema.setup_instruments
-> WHERE NAME IN (
-> 'wait/io/table/sql/handler',
-> 'wait/lock/table/sql/handler'
-> )
-> ORDER BY NAME;
+-----------------------------+---------+-------+
| NAME | ENABLED | TIMED |
+-----------------------------+---------+-------+
| wait/io/table/sql/handler | YES | YES |
| wait/lock/table/sql/handler | YES | YES |
+-----------------------------+---------+-------+
2 rows in set (0.00 sec)
ENABLED='YES'이면 event가 집계되며, TIMED='YES'이면 횟수뿐 아니라 시간도 측정한다. 운영 환경에서 값을 바꾸기 전에 현재 Performance Schema 설정이 자동 구성인지, 명시적 startup option 또는 관리 도구에 의해 관리되는지 확인해야 한다. 계측 비활성 상태에서 누적된 과거 값을 복원할 수는 없다.
4. 핵심 컬럼을 읽는 법
table_io_waits_summary_by_table의 중심은 횟수와 timer다.
COUNT_STAR,SUM_TIMER_WAIT: 전체 handler 작업의 횟수와 누적 시간MIN_TIMER_WAIT,AVG_TIMER_WAIT,MAX_TIMER_WAIT: 작업 단위 시간의 최소·평균·최대COUNT_READ,SUM_TIMER_READ: 읽기 작업 합계COUNT_WRITE,SUM_TIMER_WRITE: 쓰기 작업 합계COUNT_FETCH,SUM_TIMER_FETCH: handler가 행을 가져온 작업COUNT_INSERT,COUNT_UPDATE,COUNT_DELETE: 쓰기 유형별 작업
Performance Schema timer 값의 단위는 피코초다. 사람이 읽기 쉬운 초 또는 밀리초로 변환할 때 각각 1e12, 1e9로 나눌 수 있다. 다만 서버와 계측 종류에 따라 timer 정밀도와 비용이 다르므로 매우 작은 차이를 절대값으로 과도하게 해석하지 않는다.
4.1 합계와 평균을 함께 봐야 하는 이유
다음 두 상황은 같은 SUM_TIMER_WAIT를 만들 수 있다.
- 호출당 0.1밀리초인 작업이 백만 번 실행된 경우
- 호출당 100밀리초인 작업이 천 번 실행된 경우
첫 번째는 과도한 접근량, 비효율적인 실행 계획, 호출 빈도 문제일 가능성이 높다. 두 번째는 경합, 비정상적인 지연, 일부 무거운 접근을 먼저 의심할 수 있다. 따라서 SUM_TIMER_WAIT만 정렬하지 말고 COUNT_STAR, AVG_TIMER_WAIT, MAX_TIMER_WAIT를 함께 봐야 한다.
4.2 COUNT_FETCH는 반환 행 수가 아니다
COUNT_FETCH는 handler 계층의 fetch 작업 수다. 애플리케이션에 최종 반환된 행 수와 같지 않다. 조건을 평가하기 위해 읽었지만 버린 행, 조인 과정에서 반복 탐색한 행도 포함될 수 있다. 결과 집합은 작은데 COUNT_FETCH가 빠르게 증가한다면 실행 계획이 많은 후보 행을 조사하는지 확인할 가치가 있다.
5. 재현 예제로 지표의 의미 확인하기
다음 예제는 작은 테이블을 만든 뒤 table I/O summary를 초기화하고, 인덱스를 의도적으로 무시한 조회와 쓰기를 수행한다. TRUNCATE TABLE performance_schema.table_io_waits_summary_by_table은 해당 summary의 누적값을 전체 서버 범위에서 초기화하므로, 실제 운영 서버에서는 공동 관측 기준을 깨뜨릴 수 있다. 아래 절차는 격리된 검증 환경에서만 실행해야 한다.
DROP TABLE IF EXISTS pfs_handler_demo;
CREATE TABLE pfs_handler_demo (
id BIGINT NOT NULL PRIMARY KEY,
status VARCHAR(16) NOT NULL,
amount DECIMAL(10,2) NOT NULL,
KEY idx_status (status)
) ENGINE=InnoDB;
INSERT INTO pfs_handler_demo (id, status, amount) VALUES
(1, 'READY', 100.00),
(2, 'READY', 120.00),
(3, 'DONE', 130.00),
(4, 'READY', 140.00),
(5, 'DONE', 150.00),
(6, 'READY', 160.00),
(7, 'DONE', 170.00),
(8, 'READY', 180.00);
TRUNCATE TABLE performance_schema.table_io_waits_summary_by_table;
SELECT SUM(amount) AS ready_amount
FROM pfs_handler_demo IGNORE INDEX (idx_status)
WHERE status = 'READY';
UPDATE pfs_handler_demo
SET amount = amount + 10
WHERE id = 1;
SELECT OBJECT_SCHEMA,
OBJECT_NAME,
COUNT_STAR,
COUNT_READ,
COUNT_WRITE,
COUNT_FETCH,
COUNT_INSERT,
COUNT_UPDATE,
COUNT_DELETE,
ROUND(SUM_TIMER_WAIT / 1000000000, 3) AS total_ms,
ROUND(AVG_TIMER_WAIT / 1000000, 3) AS avg_us
FROM performance_schema.table_io_waits_summary_by_table
WHERE OBJECT_SCHEMA = DATABASE()
AND OBJECT_NAME = 'pfs_handler_demo';
실행 결과(MySQL 8.0.x):
mysql> CREATE TABLE pfs_handler_demo (...);
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO pfs_handler_demo (id, status, amount) VALUES (...);
Query OK, 8 rows affected (0.01 sec)
Records: 8 Duplicates: 0 Warnings: 0
mysql> TRUNCATE TABLE performance_schema.table_io_waits_summary_by_table;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT SUM(amount) AS ready_amount
-> FROM pfs_handler_demo IGNORE INDEX (idx_status)
-> WHERE status = 'READY';
+--------------+
| ready_amount |
+--------------+
| 700.00 |
+--------------+
1 row in set (0.00 sec)
mysql> UPDATE pfs_handler_demo SET amount = amount + 10 WHERE id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> SELECT OBJECT_SCHEMA, OBJECT_NAME, COUNT_STAR, COUNT_READ,
-> COUNT_WRITE, COUNT_FETCH, COUNT_INSERT, COUNT_UPDATE,
-> COUNT_DELETE,
-> ROUND(SUM_TIMER_WAIT / 1000000000, 3) AS total_ms,
-> ROUND(AVG_TIMER_WAIT / 1000000, 3) AS avg_us
-> FROM performance_schema.table_io_waits_summary_by_table
-> WHERE OBJECT_SCHEMA = DATABASE()
-> AND OBJECT_NAME = 'pfs_handler_demo';
+-----------------+------------------+------------+------------+-------------+-------------+--------------+--------------+--------------+----------+--------+
| OBJECT_SCHEMA | OBJECT_NAME | COUNT_STAR | COUNT_READ | COUNT_WRITE | COUNT_FETCH | COUNT_INSERT | COUNT_UPDATE | COUNT_DELETE | total_ms | avg_us |
+-----------------+------------------+------------+------------+-------------+-------------+--------------+--------------+--------------+----------+--------+
| mysql_tech_note | pfs_handler_demo | 10 | 9 | 1 | 9 | 0 | 1 | 0 | 0.050 | 5.045 |
+-----------------+------------------+------------+------------+-------------+-------------+--------------+--------------+--------------+----------+--------+
1 row in set (0.00 sec)
준비 DDL과 INSERT는 길이를 줄여 표시했다. timer 값은 이 검증 실행에서 관측된 값이다.
이 결과에서는 준비용 INSERT를 summary 초기화 전에 수행했으므로 초기 적재 횟수는 제외된다. 초기화 후 실행한 full scan 성격의 조회가 COUNT_FETCH와 COUNT_READ에, 기본 키 갱신이 COUNT_UPDATE와 COUNT_WRITE에 반영되는지 확인한다. 정확한 timer 값은 하드웨어와 실행 시점에 따라 달라지므로 고정 기준으로 사용하지 않는다.
6. 운영 서버에서 상위 테이블 찾기
운영 환경에서는 summary를 초기화하지 않고 현재 누적값을 읽는 것이 안전하다. 다음 쿼리는 시스템 스키마를 제외하고 누적 handler 시간이 큰 테이블을 찾는다.
SELECT OBJECT_SCHEMA,
OBJECT_NAME,
COUNT_STAR,
COUNT_READ,
COUNT_WRITE,
ROUND(SUM_TIMER_WAIT / 1000000000000, 3) AS total_seconds,
ROUND(AVG_TIMER_WAIT / 1000000, 3) AS avg_microseconds,
ROUND(MAX_TIMER_WAIT / 1000000000, 3) AS max_milliseconds
FROM performance_schema.table_io_waits_summary_by_table
WHERE OBJECT_SCHEMA NOT IN ('mysql', 'performance_schema', 'sys')
AND COUNT_STAR > 0
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
실행 결과(MySQL 8.0.x):
mysql> SELECT OBJECT_SCHEMA,
-> OBJECT_NAME,
-> COUNT_STAR,
-> COUNT_READ,
-> COUNT_WRITE,
-> ROUND(SUM_TIMER_WAIT / 1000000000000, 3) AS total_seconds,
-> ROUND(AVG_TIMER_WAIT / 1000000, 3) AS avg_microseconds,
-> ROUND(MAX_TIMER_WAIT / 1000000000, 3) AS max_milliseconds
-> FROM performance_schema.table_io_waits_summary_by_table
-> WHERE OBJECT_SCHEMA NOT IN ('mysql', 'performance_schema', 'sys')
-> AND COUNT_STAR > 0
-> ORDER BY SUM_TIMER_WAIT DESC
-> LIMIT 10;
+-----------------+------------------+------------+------------+-------------+---------------+------------------+------------------+
| OBJECT_SCHEMA | OBJECT_NAME | COUNT_STAR | COUNT_READ | COUNT_WRITE | total_seconds | avg_microseconds | max_milliseconds |
+-----------------+------------------+------------+------------+-------------+---------------+------------------+------------------+
| mysql_tech_note | pfs_handler_demo | 10 | 9 | 1 | 0.000 | 5.045 | 0.027 |
+-----------------+------------------+------------+------------+-------------+---------------+------------------+------------------+
1 row in set (0.00 sec)
이 목록은 원인 SQL 목록이 아니라 조사 우선순위다. 큰 테이블이 상위에 있다는 사실만으로 잘못된 설계라고 결론 내리면 안 된다. 핵심 업무 테이블은 정상적으로도 접근량이 많다. 중요한 것은 동일한 관측 구간에서 트래픽, 처리량, 지연 시간과 비교했을 때 증가율이 비정상적인지 여부다.
읽기와 쓰기를 분리하면 다음 판단에 도움이 된다.
COUNT_READ와COUNT_FETCH가 크다: scan, 반복 lookup, 조인 순서, N+1 호출을 조사한다.COUNT_WRITE가 크다: write amplification, 불필요한 갱신, 과도한 secondary index를 조사한다.- 평균은 작고 합계가 크다: 요청당 접근량과 호출 빈도를 줄이는 방향을 우선한다.
- 평균·최대가 함께 크다: 경합, 비정상 실행, 자원 포화와 긴 tail latency를 조사한다.
7. 전역 handler event와 테이블별 합계를 연결하기
전역 summary는 테이블 이름 없이 instrument 전체의 누적 상태를 보여준다.
SELECT EVENT_NAME,
COUNT_STAR,
ROUND(SUM_TIMER_WAIT / 1000000000000, 6) AS total_seconds,
ROUND(AVG_TIMER_WAIT / 1000000, 3) AS avg_microseconds
FROM performance_schema.events_waits_summary_global_by_event_name
WHERE EVENT_NAME IN (
'wait/io/table/sql/handler',
'wait/lock/table/sql/handler'
)
ORDER BY EVENT_NAME;
실행 결과(MySQL 8.0.x):
mysql> SELECT EVENT_NAME, COUNT_STAR,
-> ROUND(SUM_TIMER_WAIT / 1000000000000, 6) AS total_seconds,
-> ROUND(AVG_TIMER_WAIT / 1000000, 3) AS avg_microseconds
-> FROM performance_schema.events_waits_summary_global_by_event_name
-> WHERE EVENT_NAME IN (
-> 'wait/io/table/sql/handler',
-> 'wait/lock/table/sql/handler'
-> )
-> ORDER BY EVENT_NAME;
+-----------------------------+------------+---------------+------------------+
| EVENT_NAME | COUNT_STAR | total_seconds | avg_microseconds |
+-----------------------------+------------+---------------+------------------+
| wait/io/table/sql/handler | 18 | 0.0006 | 31.791 |
| wait/lock/table/sql/handler | 3 | 0.0000 | 1.042 |
+-----------------------------+------------+---------------+------------------+
2 rows in set (0.00 sec)
횟수와 timer는 같은 검증 컨테이너에서 앞선 재현 절차까지 실행한 뒤 얻은 한 번의 관측값이다.
전역 wait/io/table/sql/handler가 높지만 특정 사용자 테이블이 보이지 않는다면 시스템 스키마를 필터링했는지, summary가 서로 다른 시점에 초기화됐는지, instrument 설정이 중간에 변경됐는지 확인한다. 전역 table I/O와 테이블별 summary의 누적 범위가 항상 완전히 동일하다고 가정해서는 안 된다.
wait/lock/table/sql/handler가 높다는 이유만으로 InnoDB row lock 병목이라고 판단해서도 안 된다. row lock 대기는 performance_schema.data_lock_waits, data_locks, information_schema.INNODB_TRX 등으로 별도 확인한다. 이름이 비슷한 계측점을 섞으면 조사 방향이 틀어질 수 있다.
8. 원인 SQL로 좁히는 절차
테이블별 summary는 “어디에서” 비용이 발생했는지 알려주지만 “어떤 문장이” 비용을 만들었는지는 알려주지 않는다. 원인 SQL은 events_statements_summary_by_digest와 실행 계획을 연결해 찾는다.
8.1 단계별 진단 흐름
- 최소 5~15분처럼 일정한 관측 구간을 정한다.
- 상위 테이블의
COUNT_*,SUM_TIMER_*시작값을 저장한다. - 구간 종료 후 delta와 초당 증가율을 계산한다.
- 같은 구간의 QPS, 응답 시간, CPU, Buffer Pool hit/miss, file I/O를 비교한다.
- digest summary에서 rows examined가 많고 자주 실행된 SQL을 찾는다.
EXPLAIN ANALYZE를 안전한 환경에서 실행해 실제 행 흐름을 확인한다.- 인덱스 또는 SQL을 변경한 뒤 같은 길이의 구간으로 재측정한다.
8.2 digest 조사의 기본 방향
아래 쿼리는 현재 인스턴스의 statement digest 중 누적 실행 시간이 큰 항목을 좁힌다. digest text에는 리터럴이 정규화되어 있으며, 특정 테이블 이름을 문자열 검색하는 방식은 CTE, view, 동적 SQL 등에서 완전하지 않을 수 있다. 후보 생성용으로 사용한다.
SELECT SCHEMA_NAME,
DIGEST_TEXT,
COUNT_STAR AS executions,
SUM_ROWS_EXAMINED AS rows_examined,
SUM_ROWS_SENT AS rows_sent,
ROUND(SUM_TIMER_WAIT / 1000000000000, 3) AS total_seconds
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME = DATABASE()
AND DIGEST_TEXT IS NOT NULL
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
실행 결과(MySQL 8.0.x):
다음 결과는 핵심 열과 상위 2개 행만 발췌한 것이다.
mysql> SELECT SCHEMA_NAME, DIGEST_TEXT, COUNT_STAR AS executions,
-> SUM_ROWS_EXAMINED AS rows_examined,
-> SUM_ROWS_SENT AS rows_sent,
-> ROUND(SUM_TIMER_WAIT / 1000000000000, 3) AS total_seconds
-> FROM performance_schema.events_statements_summary_by_digest
-> WHERE SCHEMA_NAME = DATABASE()
-> AND DIGEST_TEXT IS NOT NULL
-> ORDER BY SUM_TIMER_WAIT DESC
-> LIMIT 10;
+-----------------+-------------------------------------------------------------+------------+---------------+-----------+---------------+
| SCHEMA_NAME | DIGEST_TEXT | executions | rows_examined | rows_sent | total_seconds |
+-----------------+-------------------------------------------------------------+------------+---------------+-----------+---------------+
| mysql_tech_note | SELECT NAME, `ENABLED`, `TIMED` FROM `performance_schema`... | 1 | 4 | 2 | 0.005 |
| mysql_tech_note | INSERT INTO `pfs_handler_demo` (...) VALUES (...) | 1 | 0 | 0 | 0.005 |
+-----------------+-------------------------------------------------------------+------------+---------------+-----------+---------------+
원래 결과는 10개 행과 긴 정규화 SQL을 포함하므로 핵심 열과 상위 2개 행만 발췌했다. 실제 운영에서는 이 표의 고정 숫자보다 실행 횟수, 조사 행 수, 반환 행 수의 비율을 해석한다.
테이블 handler 접근이 높으면서 SUM_ROWS_EXAMINED / COUNT_STAR가 큰 digest는 scan 또는 비효율적인 조인 후보가 된다. 반대로 실행당 examined row는 작지만 COUNT_STAR가 매우 크다면 N+1 호출, cache 부재, 지나치게 세분화된 요청을 의심할 수 있다.
9. 지표를 잘못 해석하는 대표 사례
9.1 handler 시간이 높으므로 디스크가 느리다고 단정한다
Buffer Pool hit인 논리 읽기도 handler 접근에 포함될 수 있다. 디스크 문제를 확인하려면 wait/io/file/innodb/*, Buffer Pool read 요청과 물리 read, 운영체제의 IOPS·latency·queue depth를 함께 본다.
9.2 누적값을 서로 다른 기간끼리 비교한다
summary 값은 누적치다. 서버 재시작, TRUNCATE, instrument 변경 시 기준점이 달라진다. 시작 timestamp와 uptime을 기록하지 않은 절대값 비교는 의미가 약하다.
9.3 COUNT_FETCH를 애플리케이션 반환 행으로 해석한다
handler가 조사한 행과 클라이언트에 반환한 행은 다르다. rows examined, rows sent, 실행 계획의 actual rows를 함께 봐야 한다.
9.4 상위 테이블에 무조건 인덱스를 추가한다
읽기 문제를 줄이려 추가한 인덱스는 모든 쓰기에서 유지 비용을 발생시킨다. COUNT_WRITE가 큰 테이블에서는 secondary index 추가가 handler write 시간과 redo 발생량을 늘릴 수 있다. 후보 쿼리와 읽기·쓰기 비율을 확인한 뒤 결정한다.
9.5 AVG_TIMER_WAIT만 보고 작은 테이블을 무시한다
호출당 시간은 작아도 초당 수십만 번 발생하면 서버 전체 CPU와 동시성 비용이 커질 수 있다. 평균, 합계, 횟수, 증가율을 함께 본다.
9.6 table I/O와 row lock을 같은 지표로 취급한다
wait/io/table/sql/handler는 테이블 handler 작업이고 row lock 대기 관계는 data_lock_waits가 담당한다. handler 시간이 길어진 원인 중 하나가 경합일 수는 있지만, 이 지표만으로 blocking transaction을 식별할 수 없다.
10. Aurora MySQL에서의 운영 해석
Aurora MySQL도 MySQL 호환 Performance Schema를 제공하므로 기본적인 instrument와 summary 해석 원리는 같다. 그러나 저장 계층과 관측 도구가 Community MySQL과 다르므로 다음 차이를 고려해야 한다.
- Aurora의 분산 스토리지 구조 때문에 로컬 데이터 파일 I/O 관점만으로 병목을 설명할 수 없다.
- writer와 reader instance는 각자의 Performance Schema 메모리와 누적값을 가진다. 클러스터 전체 값으로 합산된다고 가정하지 않는다.
- failover 또는 instance restart 후 summary 기준점이 바뀐다. instance 식별자와 관측 시작 시각을 함께 보존한다.
- Performance Insights 또는 Database Insights의 DB load, wait 분류, top SQL과 table handler delta를 같은 시간축으로 비교한다.
- reader endpoint로 읽기 트래픽이 분산되면 writer의 table summary만으로 전체 읽기 접근량을 판단할 수 없다.
- 파라미터 그룹에서 Performance Schema 관련 설정을 바꿀 때 적용 유형과 재부팅 필요 여부를 확인한다.
Aurora에서 handler 시간이 상승할 때도 첫 결론은 “스토리지가 느리다”가 아니라 “어느 instance에서 어떤 SQL이 어느 테이블에 얼마나 접근했는가”여야 한다. 이후 Aurora 지표의 읽기·쓰기 latency, cache, DB load와 교차 검증한다.
11. 운영 수집 설계
지속적인 관측에는 절대 누적값보다 delta가 적합하다. 수집기는 다음 필드를 함께 저장하는 편이 좋다.
- 수집 시각과 MySQL instance 식별자
- 서버 uptime 또는 restart 식별 정보
OBJECT_SCHEMA,OBJECT_NAMECOUNT_READ,COUNT_WRITE,COUNT_FETCHSUM_TIMER_READ,SUM_TIMER_WRITE,SUM_TIMER_FETCHMAX_TIMER_WAIT- 같은 구간의 QPS, rows examined, CPU, Buffer Pool physical read
테이블이 많다면 모든 행을 높은 빈도로 수집하는 것보다 상위 N개와 중요 테이블 allowlist를 결합한다. 이름 변경, DROP TABLE, truncate summary, failover로 시계열이 끊기는 경우도 처리해야 한다. 음수 delta가 나오면 감소가 아니라 reset 또는 instance 교체로 간주하고 새 기준점을 만든다.
12. 진단 체크리스트
계측 상태
-
wait/io/table/sql/handler가ENABLED=YES,TIMED=YES -
wait/io/table/sql/handler와wait/lock/table/sql/handler
범위 축소
-
SUM_TIMER_WAIT뿐 아니라COUNT_STAR - 읽기와 쓰기,
FETCH·INSERT·UPDATE·DELETE
원인 검증
Aurora MySQL
13. 정리
wait/io/table/sql/handler는 테이블 접근 비용의 출발점을 찾는 지표다. 이 값은 물리 디스크 지연의 동의어가 아니라 SQL executor가 스토리지 엔진의 table handler를 호출해 행을 읽고 쓴 활동의 계측 결과다. 따라서 전역 event로 현상을 확인하고, table_io_waits_summary_by_table로 대상 테이블과 읽기·쓰기 유형을 좁힌 뒤, statement digest와 실행 계획으로 원인 SQL을 찾아야 한다.
가장 중요한 운영 원칙은 누적 합계를 단독으로 해석하지 않는 것이다. 횟수·합계·평균·최대와 관측 구간의 delta를 함께 보고, Buffer Pool·file I/O·CPU·lock 지표를 같은 시간축에서 비교해야 한다. 다음 단계에서는 statement digest의 rows examined와 실행 계획을 결합해 “테이블 접근량이 왜 증가했는가”를 SQL 수준에서 추적하는 방법으로 확장할 수 있다.
DROP TABLE pfs_handler_demo;
실행 결과(MySQL 8.0.x):
mysql> DROP TABLE pfs_handler_demo;
Query OK, 0 rows affected (0.00 sec)