SHOW GLOBAL STATUS 핵심 지표 읽기: Questions, Threads, Handler, Created_tmp
MySQL의 Questions, Threads, Handler, Created_tmp 상태값을 누적 카운터와 순간 게이지로 구분하고 구간 변화율과 후속 진단 절차를 설명한다.
SHOW GLOBAL STATUS 핵심 지표 읽기: Questions, Threads, Handler, Created_tmp
MySQL 장애 대응에서 SHOW GLOBAL STATUS는 서버가 지금까지 처리한 작업의 윤곽을 빠르게 확인하는 출발점이다. Questions는 SQL 요청량, Threads_*는 연결과 실행 동시성, Handler_read_*는 스토리지 엔진 접근 방식, Created_tmp_*는 내부 임시 테이블 사용량을 보여 준다. 그러나 대부분의 값은 “현재 초당 처리량”이 아니라 서버 시작 이후 누적값이다. 숫자가 크다는 사실만으로 장애를 판정하면 오래 실행된 정상 서버를 비정상으로 오인하기 쉽다.
이 글은 MySQL 8.0 이상을 기준으로 상태값을 카운터와 게이지로 나누고, 두 시점의 차이로 변화율을 계산하며, 이상 신호를 Performance Schema와 실행 계획으로 좁히는 운영 절차를 정리한다. Aurora MySQL에서는 DB 인스턴스와 역할 전환을 경계로 기준선이 달라질 수 있다는 점도 함께 다룬다.
1. 상태값은 원인이 아니라 서버 활동의 계기판이다
SHOW GLOBAL STATUS는 상태 변수 이름과 현재 값을 반환한다. 이 값은 SQL 문장, 연결, 메모리, 네트워크, 복제, InnoDB 등 여러 계층의 활동을 요약하지만 어떤 SQL이 원인을 만들었는지 직접 알려 주지는 않는다.
flowchart LR
A[애플리케이션 요청] --> B[연결과 서버 thread]
B --> C[Parser / Optimizer / Executor]
C --> D[Handler API]
D --> E[InnoDB 인덱스와 레코드]
C --> F[내부 임시 테이블]
B --> G[Threads 계열]
C --> H[Questions]
D --> I[Handler_read 계열]
F --> J[Created_tmp 계열]
G --> K[두 시점 snapshot]
H --> K
I --> K
J --> K
K --> L[delta / rate / ratio]
L --> M[Performance Schema와 EXPLAIN으로 원인 축소]
운영 해석은 다음 순서를 지켜야 한다.
- 상태값이 누적 카운터인지 순간 게이지인지 구분한다.
- 누적값은 동일 인스턴스에서 두 시점의 차이를 구한다.
- 트래픽, 배포, 배치, 장애 시각과 변화 구간을 맞춘다.
- statement digest, wait event, thread, 실행 계획으로 원인을 좁힌다.
- 설정 변경이나 SQL 튜닝 뒤 같은 방식으로 다시 측정한다.
즉, 상태 변수는 진단을 시작하게 하는 신호다. 단독으로 원인과 처방을 확정하는 지표가 아니다.
2. 먼저 확인할 핵심 상태값
다음 쿼리는 이 글에서 다루는 핵심 값과 서버 가동 시간을 한 번에 조회한다. SHOW GLOBAL STATUS의 LIKE는 한 패턴만 다루기 때문에 여러 항목을 함께 볼 때는 WHERE Variable_name IN (...)이 편리하다.
SELECT VERSION() AS mysql_version;
SHOW GLOBAL STATUS
WHERE Variable_name IN (
'Uptime',
'Questions',
'Connections',
'Aborted_connects',
'Threads_connected',
'Threads_running',
'Threads_created',
'Threads_cached',
'Handler_read_first',
'Handler_read_key',
'Handler_read_next',
'Handler_read_prev',
'Handler_read_rnd',
'Handler_read_rnd_next',
'Created_tmp_tables',
'Created_tmp_disk_tables',
'Created_tmp_files'
);
실행 결과(MySQL 8.0.x):
mysql> SELECT VERSION() AS mysql_version;
+---------------+
| mysql_version |
+---------------+
| 8.0.46 |
+---------------+
1 row in set (0.00 sec)
mysql> SHOW GLOBAL STATUS
-> WHERE Variable_name IN (
-> 'Uptime',
-> 'Questions',
-> 'Connections',
-> 'Aborted_connects',
-> 'Threads_connected',
-> 'Threads_running',
-> 'Threads_created',
-> 'Threads_cached',
-> 'Handler_read_first',
-> 'Handler_read_key',
-> 'Handler_read_next',
-> 'Handler_read_prev',
-> 'Handler_read_rnd',
-> 'Handler_read_rnd_next',
-> 'Created_tmp_tables',
-> 'Created_tmp_disk_tables',
-> 'Created_tmp_files'
-> );
+-------------------------+-------+
| Variable_name | Value |
+-------------------------+-------+
| Aborted_connects | 0 |
| Connections | 11 |
| Created_tmp_disk_tables | 0 |
| Created_tmp_files | 5 |
| Created_tmp_tables | 1 |
| Handler_read_first | 41 |
| Handler_read_key | 1726 |
| Handler_read_next | 4040 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 191 |
| Questions | 13 |
| Threads_cached | 0 |
| Threads_connected | 1 |
| Threads_created | 1 |
| Threads_running | 2 |
| Uptime | 5 |
+-------------------------+-------+
17 rows in set (0.00 sec)
조회 결과는 실행 시점과 서버 활동에 따라 달라진다. 중요한 것은 절대값을 복사해 정상 기준으로 삼는 것이 아니라, 각 변수의 성격을 알고 같은 서버의 이전 snapshot과 비교하는 것이다.
2.1 카운터와 게이지
| 유형 | 대표 변수 | 읽는 방법 |
|---|---|---|
| 누적 카운터 | Questions, Connections, Threads_created, Handler_read_*, Created_tmp_* |
구간의 delta와 초당 증가량을 계산한다. |
| 순간 게이지 | Threads_connected, Threads_running, Threads_cached |
현재값, 최대값, 백분위, 지속 시간을 함께 본다. |
| 기준 시간 | Uptime |
재시작과 카운터 기준선 변경 여부를 확인한다. |
누적 카운터가 감소했다면 workload가 줄어든 것이 아니라 서버 재시작, failover, 관측 대상 변경, reset 계열 작업을 먼저 의심해야 한다. 모니터링에서는 인스턴스 식별자와 시작 시각을 함께 저장해야 한다.
3. Questions: 요청량의 가장 넓은 출발점
Questions는 클라이언트가 서버에 보낸 statement 수를 누적한다. 일반적인 SQL 처리량의 큰 흐름을 볼 때 유용하지만, 업무 트랜잭션 수나 사용자 요청 수와 같지는 않다.
예를 들어 하나의 HTTP 요청이 다음 작업을 수행할 수 있다.
- connection pool 상태 확인 1회
- 세션 변수 설정 1회
- 조회 3회
- 갱신 1회
COMMIT1회
이 경우 사용자 요청 1건이 여러 Questions 증가로 나타난다. 반대로 stored program 내부에서 실행되는 문장의 집계 범위는 Queries, Com_*와 비교할 때 의미가 달라질 수 있다. 따라서 애플리케이션 TPS와 Questions를 일대일로 대응시키지 않는다.
3.1 현재 QPS는 구간 차이로 계산한다
두 시점 t1, t2의 값을 수집했다면 다음처럼 계산한다.
Questions_per_second = (Questions_t2 - Questions_t1) / (t2 - t1의 초)
예를 들어 60초 동안 Questions가 120,000 증가했다면 해당 구간 평균은 2,000 QPS다. 이 계산에서 필요한 것은 두 snapshot의 실제 관측 시각이다. 수집 지연이 있었는데 분모를 무조건 60으로 고정하면 rate가 왜곡된다.
3.2 Questions / Uptime의 한계
현재 snapshot만 있을 때 Questions / Uptime으로 서버 시작 이후 평균 QPS를 계산할 수 있다. 그러나 이는 최근 1분의 부하가 아니다. 새벽의 낮은 부하와 주간 peak가 모두 희석된 장기 평균이다. 장애 순간의 급증을 찾을 때는 반드시 짧은 구간 delta를 사용한다.
Questions가 증가했을 때 다음 질문으로 이어간다.
- 배포 후 같은 업무를 처리하는 SQL 수가 늘었는가?
- connection health check 또는 ORM 메타데이터 질의가 급증했는가?
- 읽기 replica로 보내야 할 조회가 writer에 집중되었는가?
- QPS는 같지만 statement latency가 증가했는가?
처리량과 지연 시간은 별개다. QPS가 평소와 같아도 느린 SQL, 잠금, I/O 지연 때문에 Threads_running이 증가할 수 있다.
4. Threads: 연결 수와 실제 실행 동시성을 분리한다
Threads_*는 이름이 비슷하지만 서로 다른 질문에 답한다.
| 변수 | 의미 | 운영 해석 |
|---|---|---|
Threads_connected |
현재 열린 연결 수 | connection pool 규모와 max_connections 여유를 확인한다. |
Threads_running |
현재 sleep 상태가 아닌 thread 수 | 실행 또는 대기 중인 동시 작업의 순간 압력을 본다. |
Threads_created |
연결 처리를 위해 생성한 thread의 누적 수 | Connections와 함께 thread cache 효율을 방향성 있게 본다. |
Threads_cached |
현재 thread cache에 있는 thread 수 | cache가 사용 중인지 보여 주지만 값 하나로 크기 적정성을 확정하지 않는다. |
4.1 Threads_connected가 높아도 과부하는 아닐 수 있다
connection pool은 연결 생성 비용을 피하기 위해 idle connection을 유지한다. 따라서 Threads_connected가 높고 Threads_running이 낮다면 많은 연결이 대기 중일 가능성이 있다. 이때 무조건 연결을 종료하기보다 다음을 확인한다.
- 애플리케이션 인스턴스 수 × pool 최대 크기
- 배포 중 구·신 버전 pool이 동시에 존재하는 시간
- 프록시 또는 middleware의 multiplexing 여부
max_connections와 운영 예약 연결의 여유- 장시간 열린 트랜잭션이 idle connection에 숨어 있는지 여부
반대로 Threads_connected는 평소 수준인데 Threads_running이 오래 높게 유지되면 CPU 포화, 잠금 대기, I/O 지연, 비효율적인 실행 계획을 의심한다. 순간 spike 한 번보다 일정 기간의 최대값과 지속 시간이 중요하다.
4.2 Threads_created / Connections는 절대 임계값이 아니다
재시작 이후 연결 수 가운데 새 thread 생성이 얼마나 동반되었는지 보는 방향성 지표는 다음과 같다.
thread_creation_ratio = Threads_created / Connections
비율이 계속 높다면 thread_cache_size, 연결 churn, pool 재사용 실패를 점검할 가치가 있다. 그러나 서버 시작 직후처럼 분모가 작을 때는 비율이 크게 흔들리고, thread 처리 모델과 버전에 따라 해석이 달라질 수 있다. Threads_created rate, 연결 생성 지연, Aborted_connects, 애플리케이션 pool 로그를 함께 봐야 한다.
Threads_running의 상세 원인은 전날 기술노트에서 설명한 performance_schema.threads, events_statements_current, lock/wait 자료로 내려가 확인한다. Threads_running=50이라는 숫자만으로 50개가 모두 CPU를 사용 중이라고 단정할 수 없다.
5. Handler_read: 행을 어떤 방식으로 찾았는가
스토리지 엔진의 handler API는 executor가 인덱스와 레코드를 읽는 경계다. Handler_read_*는 서버 전체에서 어떤 접근 방식이 얼마나 호출되었는지 보여 준다.
| 변수 | 대표적인 접근 의미 | 주의점 |
|---|---|---|
Handler_read_key |
인덱스 키를 이용한 행 접근 | 값이 크다고 무조건 좋은 것도, 작은 것이 무조건 나쁜 것도 아니다. |
Handler_read_next |
인덱스 순서의 다음 행 읽기 | range scan, ref 접근 뒤 반복, covering scan 등에서 증가할 수 있다. |
Handler_read_prev |
인덱스 역순의 이전 행 읽기 | 역방향 index scan에서 증가할 수 있다. |
Handler_read_first |
인덱스의 첫 항목 읽기 | index scan 시작 등의 활동과 연결될 수 있다. |
Handler_read_rnd |
고정 위치를 기준으로 행 읽기 | 정렬·조인 등 실행 방식과 함께 해석한다. |
Handler_read_rnd_next |
데이터 파일 또는 임시 결과의 다음 행 읽기 | table scan 신호가 될 수 있지만 단독으로 full scan을 확정하지 않는다. |
5.1 Handler_read_rnd_next 증가를 곧바로 “인덱스 미사용”으로 단정하지 않는다
이 값이 QPS 대비 빠르게 증가한다면 많은 행을 순차 조사하는 workload가 생겼는지 확인해야 한다. 그러나 다음 활동도 값에 영향을 줄 수 있다.
- 의도적인 작은 테이블 full scan
- 내부 임시 테이블 결과 읽기
- batch 또는 분석 쿼리
- 실행 계획이 바뀐 range/table scan
- 백업·점검 도구의 순차 접근
따라서 전역 Handler delta로 이상 구간을 찾은 뒤 다음 자료로 내려간다.
performance_schema.events_statements_summary_by_digest의SUM_ROWS_EXAMINED,SUM_ROWS_SENTsys.statement_analysis,sys.statements_with_full_table_scans- 의심 digest의
EXPLAIN, 가능하면 안전한 환경의EXPLAIN ANALYZE - 테이블별
performance_schema.table_io_waits_summary_by_table
Handler_read_key가 증가하고 있어도 비효율이 없다는 뜻은 아니다. 인덱스 lookup이 지나치게 반복되는 nested-loop join, N+1 query, 낮은 선택도의 반복 탐색도 높은 key 접근량을 만들 수 있다.
5.2 QPS로 정규화한다
서버 트래픽이 두 배가 되면 Handler 카운터도 자연스럽게 증가한다. 따라서 다음과 같은 구간 비율을 함께 본다.
handler_rnd_next_per_question = delta(Handler_read_rnd_next) / delta(Questions)
handler_key_per_question = delta(Handler_read_key) / delta(Questions)
이 비율은 statement 한 건당 평균적인 handler 접근량의 방향을 보여 준다. SQL 종류가 크게 바뀌면 workload mix의 영향도 받으므로, 전체 서버 비율과 digest별 ROWS_EXAMINED를 함께 해석해야 한다.
6. Created_tmp: 내부 임시 테이블 압력을 읽는 법
MySQL은 GROUP BY, DISTINCT, 일부 ORDER BY, UNION, derived table, CTE, window function 등의 실행 과정에서 내부 임시 테이블을 사용할 수 있다. 모든 내부 임시 테이블이 문제는 아니다. 중요한 것은 생성 속도, 디스크 전환 비율, statement latency, 메모리 압력이다.
| 변수 | 의미 | 운영 해석 |
|---|---|---|
Created_tmp_tables |
서버가 생성한 내부 임시 테이블 누적 수 | 전체 생성량의 구간 rate를 본다. |
Created_tmp_disk_tables |
디스크 기반으로 생성된 내부 임시 테이블 누적 수 | 전체 임시 테이블과 함께 비율·증가 속도를 본다. |
Created_tmp_files |
임시 파일 생성 누적 수 | 정렬·임시 처리 활동의 보조 신호로 사용한다. |
구간별 디스크 임시 테이블 비율은 다음처럼 계산한다.
tmp_disk_ratio = delta(Created_tmp_disk_tables)
/ NULLIF(delta(Created_tmp_tables), 0)
장기 누적값끼리 나눈 비율은 과거 workload가 섞여 현재 변화를 가릴 수 있다. 반드시 같은 짧은 구간의 delta를 사용한다.
6.1 디스크 임시 테이블이 생기는 이유
MySQL 8.0의 내부 임시 테이블 동작은 TempTable 엔진, row 구성, 자료형, 결과 크기, tmp_table_size, temptable_max_ram, temptable_max_mmap 등 여러 조건의 영향을 받는다. tmp_table_size 하나만 올리면 해결된다고 단정해서는 안 된다. 버전과 구성에 따라 메모리·mmap·디스크 경로의 경계가 달라질 수 있다.
우선 원인 SQL을 찾고 다음을 점검한다.
- 불필요한
DISTINCT, 넓은 projection, 큰 문자열 열이 있는가? GROUP BY또는ORDER BY가 적절한 인덱스 순서를 활용할 수 있는가?- 큰 derived table이나 CTE가 materialize되는가?
- 한 문장의 문제인가, 작은 임시 테이블이 매우 자주 만들어지는 문제인가?
- 메모리 상한을 올렸을 때 동시 실행 수만큼 메모리 위험이 커지는가?
운영 서버에서는 임시 테이블 설정을 먼저 크게 올리기보다 statement digest와 실행 계획을 먼저 확인하는 편이 안전하다.
7. 한 번의 snapshot으로 보는 요약 쿼리
다음 쿼리는 performance_schema.global_status를 이용해 핵심 상태를 한 행으로 요약한다. qps_since_start와 비율은 서버 시작 이후 평균 또는 누적 비율이며 현재 구간 지표가 아니다. 모니터링 시스템이 없는 상황에서 방향을 잡는 보조값으로만 사용한다.
WITH s AS (
SELECT VARIABLE_NAME,
CAST(VARIABLE_VALUE AS UNSIGNED) AS value_num
FROM performance_schema.global_status
WHERE VARIABLE_NAME IN (
'Uptime', 'Questions', 'Connections',
'Threads_connected', 'Threads_running',
'Threads_created', 'Threads_cached',
'Handler_read_key', 'Handler_read_rnd_next',
'Created_tmp_tables', 'Created_tmp_disk_tables'
)
), p AS (
SELECT
MAX(CASE WHEN VARIABLE_NAME = 'Uptime' THEN value_num END) AS uptime_s,
MAX(CASE WHEN VARIABLE_NAME = 'Questions' THEN value_num END) AS questions,
MAX(CASE WHEN VARIABLE_NAME = 'Connections' THEN value_num END) AS connections,
MAX(CASE WHEN VARIABLE_NAME = 'Threads_connected' THEN value_num END) AS threads_connected,
MAX(CASE WHEN VARIABLE_NAME = 'Threads_running' THEN value_num END) AS threads_running,
MAX(CASE WHEN VARIABLE_NAME = 'Threads_created' THEN value_num END) AS threads_created,
MAX(CASE WHEN VARIABLE_NAME = 'Threads_cached' THEN value_num END) AS threads_cached,
MAX(CASE WHEN VARIABLE_NAME = 'Handler_read_key' THEN value_num END) AS handler_read_key,
MAX(CASE WHEN VARIABLE_NAME = 'Handler_read_rnd_next' THEN value_num END) AS handler_read_rnd_next,
MAX(CASE WHEN VARIABLE_NAME = 'Created_tmp_tables' THEN value_num END) AS created_tmp_tables,
MAX(CASE WHEN VARIABLE_NAME = 'Created_tmp_disk_tables' THEN value_num END) AS created_tmp_disk_tables
FROM s
)
SELECT uptime_s,
questions,
ROUND(questions / NULLIF(uptime_s, 0), 2) AS qps_since_start,
threads_connected,
threads_running,
threads_cached,
ROUND(100 * threads_created / NULLIF(connections, 0), 2) AS thread_create_pct_since_start,
ROUND(handler_read_key / NULLIF(questions, 0), 2) AS handler_key_per_question_since_start,
ROUND(handler_read_rnd_next / NULLIF(questions, 0), 2) AS handler_rnd_next_per_question_since_start,
ROUND(100 * created_tmp_disk_tables / NULLIF(created_tmp_tables, 0), 2) AS tmp_disk_pct_since_start
FROM p;
실행 결과(MySQL 8.0.x):
다음은 검증 출력의 폭이 넓어 관련 열을 두 묶음으로 나눈 발췌다. 값은 서버를 시작한 직후의 임시 검증 인스턴스에서 관측되었으므로 운영 기준값으로 사용하지 않는다.
mysql> WITH s AS (...) SELECT ... FROM p;
+----------+-----------+-----------------+-------------------+-----------------+----------------+
| uptime_s | questions | qps_since_start | threads_connected | threads_running | threads_cached |
+----------+-----------+-----------------+-------------------+-----------------+----------------+
| 5 | 17 | 3.40 | 1 | 2 | 0 |
+----------+-----------+-----------------+-------------------+-----------------+----------------+
+-------------------------------+--------------------------------------+-------------------------------------------+--------------------------+
| thread_create_pct_since_start | handler_key_per_question_since_start | handler_rnd_next_per_question_since_start | tmp_disk_pct_since_start |
+-------------------------------+--------------------------------------+-------------------------------------------+--------------------------+
| 8.33 | 102.53 | 12.29 | 0.00 |
+-------------------------------+--------------------------------------+-------------------------------------------+--------------------------+
1 row in set (0.00 sec)
이 출력은 장애 판단표가 아니다. 예를 들어 tmp_disk_pct_since_start가 높아도 서버 시작 직후 실행된 한 번의 대형 batch가 비율을 지배할 수 있다. 반대로 장기 평균이 낮아도 최근 배포 이후 5분 동안 비율이 급증했을 수 있다.
8. 두 시점 snapshot으로 rate를 만드는 운영 패턴
상태 카운터를 모니터링에 사용할 때는 다음 필드를 한 묶음으로 저장한다.
observed_at
server_identity
server_role
server_start_time 또는 Uptime
Questions
Connections
Threads_connected
Threads_running
Threads_created
Handler_read_key
Handler_read_next
Handler_read_rnd_next
Created_tmp_tables
Created_tmp_disk_tables
수집기에서 두 연속 표본을 비교한다.
elapsed = observed_at_2 - observed_at_1
questions_rate = delta(Questions) / elapsed
threads_created_rate = delta(Threads_created) / elapsed
rnd_next_rate = delta(Handler_read_rnd_next) / elapsed
tmp_tables_rate = delta(Created_tmp_tables) / elapsed
tmp_disk_rate = delta(Created_tmp_disk_tables) / elapsed
다음 조건에서는 delta를 버리고 새 기준선을 시작한다.
Uptime이 이전보다 작다.- 누적 카운터가 이전보다 작다.
- 인스턴스 식별자 또는 writer 역할이 바뀌었다.
- 수집 간격이 비정상적으로 길어 해당 구간 평균이 의미를 잃었다.
Threads_connected와 Threads_running은 delta를 QPS처럼 계산하지 않는다. 각 시점의 gauge를 저장하고 구간 평균, 최대, 백분위, 임계 초과 지속 시간을 계산한다.
8.1 경보는 단일 숫자가 아니라 조합으로 만든다
| 관찰 조합 | 가능한 해석 | 다음 확인 |
|---|---|---|
QPS 증가 + Threads_running 안정 |
처리량 증가를 서버가 흡수하는 중일 수 있음 | latency, CPU, connection headroom |
QPS 안정 + Threads_running 지속 증가 |
문장 지연 또는 대기 증가 가능성 | wait event, lock, digest latency |
Threads_connected만 증가 |
pool 확대·배포 중복·연결 누수 가능성 | processlist/threads, pool 설정, 트랜잭션 수명 |
Threads_created rate 증가 + 연결 churn |
thread cache 또는 pool 재사용 문제 가능성 | Connections, Aborted_connects, 연결 지연 |
Handler_read_rnd_next/Q 증가 |
순차 조사량 또는 workload mix 변화 가능성 | digest rows examined, EXPLAIN |
Created_tmp_disk_tables rate 증가 + latency 증가 |
큰 중간 결과 또는 메모리 경계 변화 가능성 | digest, execution plan, TempTable 관련 설정 |
“가능성”이라는 표현이 중요하다. 이 표는 원인 목록의 우선순위를 정할 뿐, 자동 처방을 내리지 않는다.
9. 상태값에서 원인 SQL로 내려가는 진단 쿼리
Questions와 Handler, 임시 테이블 지표가 이상하다면 statement digest를 함께 확인한다. 다음 쿼리는 누적 실행 시간과 조사 행 수가 큰 digest를 제한된 열로 조회한다. Performance Schema가 활성화되어 있고 해당 consumer가 자료를 수집한 범위에서만 의미가 있다.
SELECT SCHEMA_NAME,
COUNT_STAR AS exec_count,
ROUND(SUM_TIMER_WAIT / 1000000000000, 3) AS total_seconds,
ROUND(AVG_TIMER_WAIT / 1000000000, 3) AS avg_ms,
SUM_ROWS_EXAMINED AS rows_examined,
SUM_ROWS_SENT AS rows_sent,
SUM_CREATED_TMP_TABLES AS tmp_tables,
SUM_CREATED_TMP_DISK_TABLES AS tmp_disk_tables,
LEFT(DIGEST_TEXT, 100) AS digest_text
FROM performance_schema.events_statements_summary_by_digest
WHERE DIGEST_TEXT IS NOT NULL
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
실행 결과(MySQL 8.0.x):
검증 컨테이너에서 앞선 예제 문장을 실행한 직후의 결과다. 아래에는 상위 3행과 핵심 열만 발췌했으며, 시간과 행 수는 workload에 따라 달라진다.
mysql> SELECT SCHEMA_NAME, COUNT_STAR AS exec_count, ...
-> FROM performance_schema.events_statements_summary_by_digest
-> WHERE DIGEST_TEXT IS NOT NULL
-> ORDER BY SUM_TIMER_WAIT DESC
-> LIMIT 10;
+-----------------+------------+---------------+--------+---------------+------------+-----------------+
| SCHEMA_NAME | exec_count | total_seconds | avg_ms | rows_examined | tmp_tables | tmp_disk_tables |
+-----------------+------------+---------------+--------+---------------+------------+-----------------+
| mysql_tech_note | 1 | 0.003 | 3.018 | 17 | 1 | 0 |
| mysql_tech_note | 1 | 0.001 | 0.710 | 12 | 1 | 0 |
| NULL | 3 | 0.000 | 0.130 | 3 | 0 | 0 |
+-----------------+------------+---------------+--------+---------------+------------+-----------------+
일부 행과 digest_text 열 생략
결과가 비어 있거나 기대보다 적다면 “문제가 없다”가 아니라 instrumentation과 consumer, digest 보존 한도, reset 시점을 확인한다. digest summary도 누적 자료이므로 최근 구간 분석에는 모니터링 snapshot 또는 reset 전후 비교가 필요하다. 운영 공동 지표를 임의로 TRUNCATE하면 다른 분석의 기준선을 파괴할 수 있으므로 사전 합의 없이 초기화하지 않는다.
원인 축소의 일반적인 순서는 다음과 같다.
Questionsrate와 application request rate를 비교한다.Threads_running이 높다면 현재 statement, wait, lock을 확인한다.- Handler per question이 변했다면 rows examined가 큰 digest를 찾는다.
- Created_tmp rate가 변했다면
SUM_CREATED_TMP_*가 큰 digest를 찾는다. - 대표 SQL의 실행 계획과 통계, 인덱스, 입력 cardinality를 확인한다.
- 변경 후 같은 시간대와 workload에서 rate·latency를 재측정한다.
10. 흔한 오해와 실패 패턴
10.1 큰 누적값을 장애로 판정한다
수개월 가동한 서버는 Questions와 Handler 값이 매우 큰 것이 정상이다. 대시보드는 raw counter보다 rate를 기본으로 표시하고, raw 값은 reset 확인과 감사 용도로 남긴다.
10.2 Threads_running을 CPU 사용 thread 수로 본다
실행 중이거나 여러 종류의 대기 상태인 thread가 포함될 수 있다. CPU 사용률, wait event, lock 관계를 함께 봐야 한다.
10.3 Handler_read_rnd_next만 보고 인덱스를 추가한다
어떤 SQL과 테이블에서 증가했는지 확인하지 않은 인덱스 추가는 쓰기 비용과 Buffer Pool 압력을 키울 수 있다. digest와 실행 계획으로 대상 query pattern을 먼저 특정한다.
10.4 디스크 임시 테이블 비율에 고정 임계값을 적용한다
workload가 다르면 정상 범위도 다르다. 짧고 작은 임시 테이블이 많은 OLTP와 큰 집계가 있는 reporting workload를 같은 임계값으로 평가할 수 없다. rate, latency, 메모리, 디스크 사용량, SLO를 함께 본다.
10.5 설정값부터 올린다
thread_cache_size나 임시 테이블 관련 메모리 상한을 먼저 확대하면 증상이 잠시 줄어도 연결 churn 또는 비효율적인 SQL이 남는다. 특히 per-session 또는 동시 작업 수에 비례하는 메모리 설정은 peak 동시성에서 총사용량을 계산해야 한다.
10.6 reset 경계를 연결해 거짓 spike를 만든다
재시작 또는 failover 전후의 카운터를 단순히 빼면 음수나 매우 큰 rate가 생길 수 있다. 수집기는 Uptime, 인스턴스 식별자, 역할 변경을 감지해 시계열을 분리해야 한다.
11. Aurora MySQL에서의 해석
Aurora MySQL에서도 MySQL 호환 status 변수는 유용하지만 다음 경계를 명확히 해야 한다.
SHOW GLOBAL STATUS는 접속한 DB 인스턴스의 mysqld 관점이다. cluster 전체를 하나의 누적 카운터처럼 해석하지 않는다.- writer와 reader는 서로 다른 인스턴스 기준선을 가진다. endpoint 뒤의 실제 대상과 역할을 수집 label에 남긴다.
- failover로 새 writer가 선택되면 이전 writer의 counter와 연속된 시계열로 계산하지 않는다.
- Aurora의 분산 스토리지 구조 때문에 Community MySQL의 로컬 데이터 파일 I/O 해석을 그대로 대입하지 않는다. Handler 값은 SQL 실행 경로의 논리적 접근 신호로 사용하고, 스토리지·지연 문제는 CloudWatch, Performance Insights 또는 Database Insights와 교차 확인한다.
- parameter group 변경은 적용 방식과 재부팅 필요 여부를 확인한다. status 신호 하나만 보고 cluster 전체에 설정을 확대하지 않는다.
특히 reader endpoint를 사용하는 환경에서는 reader별 부하 편차가 cluster 평균에 가려질 수 있다. Questions, Threads_running, Handler, Created_tmp rate를 인스턴스별로 수집한 뒤 역할과 endpoint routing을 함께 봐야 한다.
12. 운영 점검 체크리스트
수집 설계
-
Uptime
Questions와 Threads
-
Questions -
Threads_connected와Threads_running - connection pool 총합과
max_connections -
Threads_created를Connections - 높은
Threads_running
Handler와 Created_tmp
-
Handler_read_rnd_next
변경 안전성
13. 마무리
SHOW GLOBAL STATUS의 핵심은 많은 변수 이름을 외우는 데 있지 않다. Questions로 요청량의 변화를 찾고, Threads로 연결과 실행 동시성을 구분하며, Handler로 행 접근 방식의 변화를 감지하고, Created_tmp로 중간 결과 처리 압력을 확인하는 흐름이 중요하다.
누적 카운터는 두 시점의 delta와 rate로, 순간값은 지속 시간과 분포로 해석해야 한다. 그다음 Performance Schema digest, wait, lock, 테이블 I/O, 실행 계획으로 내려가야 비로소 조치 가능한 원인이 된다. 다음 기술노트에서는 이러한 상태값을 일정 간격으로 수집해 baseline, 추세, 변화율 경보를 설계하는 방법을 더 구체적으로 다룬다.