---
title: "Performance Schema 개요: instrument, consumer, event table 구조"
description: "MySQL Performance Schema의 instrument, consumer, event table 연결 구조와 운영 시 관측·비용 제어 원칙을 정리한다."
tags: [ MySQL, 아키텍처, 운영, DBA ]
image: "mysql-report-bg.png"
published: "2026-08-15"
updated: "2026-08-15"
author: "MySQL 기술 노트"
source_url: ""
---

MySQL 장애 분석에서 가장 어려운 질문은 “서버가 느리다”가 아니라 **어느 실행 단계에서, 어떤 작업이, 얼마나 자원을 사용하며 기다리고 있는가**이다. `SHOW PROCESSLIST`는 현재 연결의 큰 윤곽을 보여 주지만, 파일 I/O·mutex·stage·statement·transaction 단위의 원인을 일관된 구조로 연결하기에는 부족하다. Performance Schema는 이 간극을 메우는 서버 내장 관측 계층이다.

Performance Schema를 효과적으로 사용하려면 테이블 이름을 외우기보다 다음 세 요소의 관계를 이해해야 한다.

- **instrument**: 측정 가능한 서버 내부 지점을 정의한다.
- **consumer**: 수집된 이벤트를 어느 저장·집계 경로로 보낼지 결정한다.
- **event table**: 현재값, 짧은 이력, 장기 이력, 집계 결과를 SQL로 노출한다.

이 글에서는 세 요소가 이벤트를 만드는 경로, 설정의 적용 순서, 데이터 보존 특성, 운영 비용과 진단 절차를 구조적으로 설명한다. 예제는 MySQL 8.0 계열의 임시 검증 환경에서 실행하며, 설치 빌드와 minor version에 따라 instrument 목록과 개수는 달라질 수 있다.

## 1. Performance Schema의 역할과 경계

Performance Schema는 일반 업무 데이터를 저장하는 스토리지 엔진이 아니다. 서버 코드에 심어진 계측 지점에서 이벤트를 받아 메모리 기반 테이블과 집계 테이블로 노출하는 **관측 시스템**이다. 다음 질문에 특히 적합하다.

- 현재 어떤 statement가 실행 중인가?
- 어떤 statement 유형 또는 digest가 전체 부하를 만들었는가?
- 세션이 file I/O, socket I/O, metadata lock, InnoDB lock 중 무엇을 기다리는가?
- 특정 계정·호스트·스레드가 사용한 시간과 호출 횟수는 얼마인가?
- 메모리 instrument가 활성화된 범위에서 어느 구성 요소의 메모리 사용량이 증가했는가?

반대로 Performance Schema만으로 모든 상황을 해결할 수는 없다. 이미 순환 버퍼에서 밀려난 과거 이벤트, OS 전체의 CPU run queue, 디스크 장치 자체의 지연, 네트워크 구간별 손실은 별도 지표와 로그가 필요하다. 따라서 Performance Schema는 error log, slow query log, OS·컨테이너 지표, 애플리케이션 추적과 결합해야 한다.

## 2. 이벤트가 SQL 테이블에 도달하는 과정

Performance Schema의 관측 경로는 “측정 가능 여부”와 “저장 여부”가 분리되어 있다는 점이 핵심이다.

```mermaid
flowchart LR
    A[MySQL 서버 코드의 계측 지점] --> B{instrument 활성화?}
    B -- 아니요 --> X[이벤트를 측정하지 않음]
    B -- 예 --> C[타이머·바이트·행 수 등 이벤트 속성 수집]
    C --> D{thread instrumentation과 consumer 활성화?}
    D -- 아니요 --> Y[해당 소비 경로에 저장하지 않음]
    D -- 예 --> E[현재 이벤트 테이블]
    D -- 예 --> F[세션별 history]
    D -- 예 --> G[전역 history_long]
    C --> H[summary·digest 집계]
    E --> I[운영 진단 SQL]
    F --> I
    G --> I
    H --> I
```

이 흐름을 운영 관점에서 네 단계로 나누면 다음과 같다.

1. 서버 코드의 계측 지점이 특정 동작을 감싼다.
2. `setup_instruments`의 `ENABLED`가 해당 지점을 측정할지 결정한다.
3. `setup_consumers`가 측정된 이벤트를 current, history, history_long 같은 목적지로 보낼지 결정한다.
4. 목적지 테이블 또는 summary 테이블을 SQL로 조회한다.

따라서 history consumer만 켠다고 비활성 instrument의 이벤트가 생기지는 않는다. 반대로 instrument가 켜져 있어도 필요한 consumer가 꺼져 있으면 기대한 history 테이블은 비어 있을 수 있다.

## 3. instrument: 무엇을 측정할 것인가

### 3.1 계층형 이름

instrument 이름은 `/`로 구분된 계층을 사용한다. 첫 부분은 이벤트 클래스이고 뒤쪽은 서버 구성 요소와 구체적인 대상을 나타낸다.

| 대표 접두사 | 관측 대상 | 운영 질문 |
|---|---|---|
| `wait/io/file/` | 파일 I/O 대기 | 어느 파일 계층에서 대기 시간이 누적되는가? |
| `wait/io/socket/` | 네트워크 소켓 I/O | 연결 처리와 네트워크 송수신이 병목인가? |
| `wait/lock/` | mutex·rwlock 외의 잠금 대기 | metadata lock 또는 data lock이 작업을 막는가? |
| `stage/` | statement 내부 실행 단계 | 정렬·임시 테이블·테이블 열기 단계가 지연되는가? |
| `statement/` | SQL 및 command 실행 | 어떤 SQL 유형과 digest가 부하를 만드는가? |
| `transaction` | 트랜잭션 | 트랜잭션 시간과 상태를 어떻게 집계할 것인가? |
| `memory/` | 계측 가능한 메모리 할당 | 어느 구성 요소의 메모리 사용량이 증가하는가? |

실제 instrument 존재 여부와 활성 상태는 서버에서 직접 확인해야 한다.

```sql
SELECT VERSION() AS mysql_version;

SELECT SUBSTRING_INDEX(NAME, '/', 1) AS event_class,
       COUNT(*) AS instrument_count,
       SUM(ENABLED = 'YES') AS enabled_count,
       SUM(TIMED = 'YES') AS timed_count
FROM performance_schema.setup_instruments
GROUP BY SUBSTRING_INDEX(NAME, '/', 1)
ORDER BY event_class;

SELECT NAME, ENABLED, TIMED
FROM performance_schema.setup_instruments
WHERE NAME LIKE 'wait/io/file/innodb/%'
ORDER BY NAME
LIMIT 5;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT VERSION() AS mysql_version;
+---------------+
| mysql_version |
+---------------+
| 8.0.46        |
+---------------+
1 row in set (0.00 sec)

mysql> SELECT SUBSTRING_INDEX(NAME, '/', 1) AS event_class,
    ->        COUNT(*) AS instrument_count,
    ->        SUM(ENABLED = 'YES') AS enabled_count,
    ->        SUM(TIMED = 'YES') AS timed_count
    -> FROM performance_schema.setup_instruments
    -> GROUP BY SUBSTRING_INDEX(NAME, '/', 1)
    -> ORDER BY event_class;
+-------------+------------------+---------------+-------------+
| event_class | instrument_count | enabled_count | timed_count |
+-------------+------------------+---------------+-------------+
| error       |                1 |             1 |        NULL |
| idle        |                1 |             1 |           1 |
| memory      |              502 |           502 |        NULL |
| stage       |              132 |            16 |          16 |
| statement   |              213 |           198 |         198 |
| transaction |                1 |             1 |           1 |
| wait        |              400 |            54 |          54 |
+-------------+------------------+---------------+-------------+
7 rows in set (0.01 sec)

mysql> SELECT NAME, ENABLED, TIMED
    -> FROM performance_schema.setup_instruments
    -> WHERE NAME LIKE 'wait/io/file/innodb/%'
    -> ORDER BY NAME
    -> LIMIT 5;
+---------------------------------------+---------+-------+
| NAME                                  | ENABLED | TIMED |
+---------------------------------------+---------+-------+
| wait/io/file/innodb/innodb_arch_file  | YES     | YES   |
| wait/io/file/innodb/innodb_clone_file | YES     | YES   |
| wait/io/file/innodb/innodb_data_file  | YES     | YES   |
| wait/io/file/innodb/innodb_dblwr_file | YES     | YES   |
| wait/io/file/innodb/innodb_log_file   | YES     | YES   |
+---------------------------------------+---------+-------+
5 rows in set (0.00 sec)
```

첫 집계에서 `enabled_count`는 수집 가능 범위, `timed_count`는 시간 측정 범위를 나타낸다. instrument가 존재해도 `TIMED='NO'`이면 호출 횟수나 바이트 같은 속성은 수집할 수 있지만 시간 기반 분석에는 제한이 생길 수 있다. 실제 목록과 기본값은 배포판, 버전, 시작 옵션에 따라 달라질 수 있으므로 예제의 개수를 고정된 기준값으로 사용해서는 안 된다.

### 3.2 ENABLED와 TIMED의 차이

- `ENABLED='NO'`: 해당 instrument 이벤트를 수집하지 않는다.
- `ENABLED='YES', TIMED='NO'`: 이벤트는 계측하지만 타이머 비용과 시간 값은 사용하지 않거나 제한한다.
- `ENABLED='YES', TIMED='YES'`: 이벤트와 경과 시간을 함께 측정한다.

문제를 조사할 때 모든 instrument를 무조건 활성화하는 방식은 피해야 한다. 우선 statement와 필요한 wait class를 선택하고, 관측 결과가 부족할 때 범위를 확장하는 편이 안전하다. 특히 고빈도 mutex나 메모리 instrument는 많은 행과 집계를 만들 수 있으므로 변경 전후의 CPU, 처리량, 메모리 사용량을 함께 비교해야 한다.

### 3.3 startup 설정과 runtime 변경

`setup_instruments`와 `setup_consumers`는 runtime에서 갱신할 수 있지만, 모든 계측 자원의 크기가 동적으로 늘어나는 것은 아니다. history 크기, 최대 스레드·계정·digest 수와 관련된 `performance_schema_*` 시스템 변수 중 다수는 시작 시 메모리 구조를 결정한다. 운영 표준은 다음 두 층으로 나누는 것이 좋다.

- **상시 관측 기준선**: 부하가 낮고 항상 필요한 statement, transaction, 핵심 wait 집계
- **사건 대응 프로필**: 제한된 시간 동안 특정 instrument와 history consumer를 추가 활성화

사건 대응 변경에는 시작 시각, 변경한 행, 원복 시각을 남겨야 한다. 그렇지 않으면 진단이 끝난 뒤에도 불필요한 계측 비용이 지속될 수 있다.

## 4. consumer: 이벤트를 어디에 보관할 것인가

consumer는 event class 자체가 아니라 수집 결과의 목적지를 제어한다. 주요 consumer는 다음 의미를 가진다.

| consumer 유형 | 의미 | 보존 특성 |
|---|---|---|
| `events_*_current` | instrumented thread별 현재 이벤트 | 실행 종료와 함께 교체됨 |
| `events_*_history` | thread별 최근 완료 이벤트 | 작은 순환 버퍼 |
| `events_*_history_long` | 서버 전체의 최근 완료 이벤트 | 전역 순환 버퍼 |
| `global_instrumentation` | 전역 계측 경로 | 다른 consumer의 상위 조건이 될 수 있음 |
| `thread_instrumentation` | 계측 대상 스레드 경로 | thread별 이벤트 수집의 상위 조건 |
| `statements_digest` | 정규화된 statement digest 집계 | 개별 SQL 대신 유형별 누적 분석 |

현재 설정은 다음과 같이 좁혀 조회할 수 있다.

```sql
SELECT NAME, ENABLED
FROM performance_schema.setup_consumers
WHERE NAME IN (
  'global_instrumentation',
  'thread_instrumentation',
  'events_statements_current',
  'events_statements_history',
  'events_statements_history_long',
  'statements_digest'
)
ORDER BY NAME;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT NAME, ENABLED
    -> FROM performance_schema.setup_consumers
    -> WHERE NAME IN (
    ->   'global_instrumentation',
    ->   'thread_instrumentation',
    ->   'events_statements_current',
    ->   'events_statements_history',
    ->   'events_statements_history_long',
    ->   'statements_digest'
    -> )
    -> ORDER BY NAME;

+--------------------------------+---------+
| NAME                           | ENABLED |
+--------------------------------+---------+
| events_statements_current      | YES     |
| events_statements_history      | YES     |
| events_statements_history_long | NO      |
| global_instrumentation         | YES     |
| statements_digest              | YES     |
| thread_instrumentation         | YES     |
+--------------------------------+---------+
6 rows in set (0.00 sec)
```

### 4.1 current, history, history_long 선택 기준

`current`는 “지금 무엇이 실행 중인가”에 적합하지만 매우 짧은 statement는 포착하기 어렵다. `history`는 특정 연결의 직전 흐름을 볼 때 유용하며, `history_long`은 연결을 넘나드는 최근 이벤트를 시간순으로 조사할 때 적합하다. 그러나 둘 다 영구 감사 로그가 아니라 고정 크기 순환 버퍼다. 이벤트 발생률이 높으면 조사 대상 이벤트가 빠르게 덮어써진다.

이 때문에 history_long을 켰다는 사실만으로 사후 분석 보존성이 확보되지는 않는다. 다음 값을 함께 고려해야 한다.

- 초당 대상 이벤트 수
- history 행 수
- 장애 감지에서 조회·외부 저장까지의 시간
- 저장하는 이벤트 한 행의 평균 크기
- 서버 메모리 여유와 계측 오버헤드

장기 보존이 필요하면 필요한 열만 주기적으로 외부 관측 시스템에 수집하거나 slow query log, audit 요구에 맞는 별도 기능을 사용해야 한다.

### 4.2 consumer 의존 관계를 놓치지 않기

하위 history consumer가 `YES`여도 상위 instrumentation 경로 또는 관련 instrument가 꺼져 있으면 행이 쌓이지 않는다. “테이블이 비어 있다”는 현상을 “서버에 문제가 없었다”로 해석하기 전에 다음 순서로 확인한다.

1. `performance_schema`가 활성화되어 있는가?
2. 대상 instrument가 존재하고 `ENABLED='YES'`인가?
3. thread와 global instrumentation이 필요한 경로에서 활성화되어 있는가?
4. 대상 current/history consumer가 활성화되어 있는가?
5. 대상 스레드가 `performance_schema.threads.INSTRUMENTED='YES'`인가?
6. 버퍼가 순환하여 사건 시점의 행이 이미 사라지지 않았는가?

## 5. event table: current, history, summary의 구조

Performance Schema 테이블은 같은 이벤트 class에 대해 유사한 열을 반복한다. statement event라면 보통 다음 범주를 확인한다.

- **식별자**: `THREAD_ID`, `EVENT_ID`, `END_EVENT_ID`
- **이벤트 종류**: `EVENT_NAME`
- **시간**: `TIMER_START`, `TIMER_END`, `TIMER_WAIT`, `LOCK_TIME`
- **SQL 문맥**: `SQL_TEXT`, `DIGEST`, `DIGEST_TEXT`
- **작업량**: `ROWS_AFFECTED`, `ROWS_SENT`, `ROWS_EXAMINED`
- **실행 부수 효과**: 임시 테이블, 정렬, 인덱스 미사용 관련 카운터

MySQL 8.0 대상 서버에서 실제 열을 확인하는 방법은 다음과 같다.

```sql
SELECT COLUMN_NAME, DATA_TYPE, ORDINAL_POSITION
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'performance_schema'
  AND TABLE_NAME = 'events_statements_history_long'
  AND COLUMN_NAME IN (
    'THREAD_ID', 'EVENT_ID', 'EVENT_NAME',
    'TIMER_WAIT', 'SQL_TEXT', 'DIGEST_TEXT',
    'ROWS_SENT', 'ROWS_EXAMINED'
  )
ORDER BY ORDINAL_POSITION;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT COLUMN_NAME, DATA_TYPE, ORDINAL_POSITION
    -> FROM information_schema.COLUMNS
    -> WHERE TABLE_SCHEMA = 'performance_schema'
    ->   AND TABLE_NAME = 'events_statements_history_long'
    ->   AND COLUMN_NAME IN (
    ->     'THREAD_ID', 'EVENT_ID', 'EVENT_NAME',
    ->     'TIMER_WAIT', 'SQL_TEXT', 'DIGEST_TEXT',
    ->     'ROWS_SENT', 'ROWS_EXAMINED'
    ->   )
    -> ORDER BY ORDINAL_POSITION;

+---------------+-----------+------------------+
| COLUMN_NAME   | DATA_TYPE | ORDINAL_POSITION |
+---------------+-----------+------------------+
| THREAD_ID     | bigint    |                1 |
| EVENT_ID      | bigint    |                2 |
| EVENT_NAME    | varchar   |                4 |
| TIMER_WAIT    | bigint    |                8 |
| SQL_TEXT      | longtext  |               10 |
| DIGEST_TEXT   | longtext  |               12 |
| ROWS_SENT     | bigint    |               24 |
| ROWS_EXAMINED | bigint    |               25 |
+---------------+-----------+------------------+
8 rows in set (0.00 sec)
```

`TIMER_WAIT` 같은 timer 값은 picosecond 단위 정수로 노출되는 경우가 많다. 사람이 읽을 때는 Performance Schema summary 테이블의 평균·합계 열을 단위 변환하거나 `sys` schema의 사람이 읽기 쉬운 view를 활용할 수 있다. 다만 단위 변환 전에 해당 열의 의미와 null 가능성을 확인해야 한다.

### 5.1 event table과 summary table의 차이

개별 event table은 “무슨 일이 있었는가”를 행 단위로 보여 준다. summary table은 account, host, user, thread, event name, object, digest 같은 차원으로 누적값을 집계한다.

- 짧은 장애 흐름 재구성: `events_*_current`, `events_*_history_long`
- 상위 SQL 유형과 누적 부하: `events_statements_summary_by_digest`
- 파일별 I/O: `file_summary_by_instance`, `file_summary_by_event_name`
- wait class별 누적 대기: `events_waits_summary_global_by_event_name`
- 계정·사용자별 비교: 이름에 `_by_account_`, `_by_user_`, `_by_host_`가 포함된 summary

summary의 장점은 고빈도 이벤트를 압축해 장기간의 방향성을 보기 쉽다는 점이다. 단점은 개별 발생 시각과 한 번의 비정상 실행 문맥을 잃는다는 점이다. 평균만 보면 일부 매우 느린 실행이 숨을 수 있으므로 `COUNT_STAR`, `SUM_TIMER_WAIT`, `AVG_TIMER_WAIT`, `MAX_TIMER_WAIT`, row 관련 카운터를 함께 읽어야 한다.

## 6. digest로 statement 부하를 묶어 보기

statement digest는 리터럴을 정규화하여 구조가 같은 SQL을 하나의 유형으로 집계한다. 애플리케이션이 매번 다른 주문 번호나 사용자 ID를 사용해도 동일한 SQL 형태를 찾을 수 있다.

다음 예제는 작은 테이블에서 statement를 실행한 뒤 digest summary에 해당 테이블을 참조한 행이 나타나는지 확인한다. `DIGEST_TEXT` 표기와 timer 값은 minor version 및 실행 환경에 따라 달라질 수 있으므로, 운영에서는 정확한 문자열 전체보다 schema, digest, 호출 횟수, 작업량을 함께 사용한다.

```sql
DROP TABLE IF EXISTS ps_note_demo;
CREATE TABLE ps_note_demo (
  id BIGINT PRIMARY KEY,
  state VARCHAR(20) NOT NULL,
  KEY ix_state (state)
) ENGINE = InnoDB;

INSERT INTO ps_note_demo VALUES
  (1, 'READY'), (2, 'READY'), (3, 'DONE');

SELECT COUNT(*) AS row_count
FROM ps_note_demo
WHERE state = 'READY';

SELECT SCHEMA_NAME,
       COUNT_STAR,
       SUM_ROWS_EXAMINED,
       SUM_ROWS_SENT,
       LEFT(DIGEST_TEXT, 100) AS digest_text
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME = DATABASE()
  AND DIGEST_TEXT LIKE '%`ps_note_demo`%'
  AND DIGEST_TEXT LIKE 'SELECT%'
ORDER BY COUNT_STAR DESC
LIMIT 3;

DROP TABLE ps_note_demo;
```

실행 결과(MySQL 8.0.x):

아래 출력은 준비·정리 절차를 줄이고 핵심 DDL/DML과 조회 결과를 발췌한 것이다.

```text
mysql> CREATE TABLE ps_note_demo (..., KEY ix_state (state)) ENGINE = InnoDB;
Query OK, 0 rows affected (0.01 sec)

mysql> INSERT INTO ps_note_demo VALUES
    ->   (1, 'READY'), (2, 'READY'), (3, 'DONE');
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> SELECT COUNT(*) AS row_count
    -> FROM ps_note_demo
    -> WHERE state = 'READY';
+-----------+
| row_count |
+-----------+
|         2 |
+-----------+
1 row in set (0.00 sec)

mysql> SELECT SCHEMA_NAME, COUNT_STAR, SUM_ROWS_EXAMINED,
    ->        SUM_ROWS_SENT, LEFT(DIGEST_TEXT, 100) AS digest_text
    -> FROM performance_schema.events_statements_summary_by_digest
    -> WHERE SCHEMA_NAME = DATABASE()
    ->   AND DIGEST_TEXT LIKE '%`ps_note_demo`%'
    ->   AND DIGEST_TEXT LIKE 'SELECT%'
    -> ORDER BY COUNT_STAR DESC LIMIT 3;
+-----------------+------------+-------------------+---------------+-----------------------------------------------------------------------+
| SCHEMA_NAME     | COUNT_STAR | SUM_ROWS_EXAMINED | SUM_ROWS_SENT | digest_text                                                           |
+-----------------+------------+-------------------+---------------+-----------------------------------------------------------------------+
| mysql_tech_note |          1 |                 2 |             1 | SELECT COUNT ( * ) AS ROW_COUNT FROM `ps_note_demo` WHERE `state` = ? |
+-----------------+------------+-------------------+---------------+-----------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> DROP TABLE ps_note_demo;
Query OK, 0 rows affected (0.01 sec)
```

이 결과를 해석할 때 `COUNT_STAR`가 크다는 이유만으로 최우선 튜닝 대상으로 결정해서는 안 된다. 호출당 비용이 작아도 빈도가 높을 수 있고, 호출 횟수는 낮지만 `MAX_TIMER_WAIT`나 `SUM_ROWS_EXAMINED`가 큰 SQL이 더 위험할 수 있다. 다음 축을 함께 비교한다.

- 빈도: `COUNT_STAR`
- 누적 시간: `SUM_TIMER_WAIT`
- 평균·최대 시간: `AVG_TIMER_WAIT`, `MAX_TIMER_WAIT`
- 읽은 행 대비 반환 행: `SUM_ROWS_EXAMINED`, `SUM_ROWS_SENT`
- 임시 테이블·디스크 임시 테이블·정렬 관련 카운터
- digest 표본 SQL: `QUERY_SAMPLE_TEXT`가 제공되는 버전과 설정에서 확인

## 7. thread와 event를 연결하는 방법

Performance Schema의 `THREAD_ID`는 운영체제 PID나 MySQL connection ID와 동일하다고 가정하면 안 된다. 일반 클라이언트 연결을 연결하려면 `performance_schema.threads`의 열을 사용한다.

```sql
SELECT THREAD_ID,
       PROCESSLIST_ID,
       PROCESSLIST_USER,
       PROCESSLIST_COMMAND,
       INSTRUMENTED,
       HISTORY
FROM performance_schema.threads
WHERE TYPE = 'FOREGROUND'
ORDER BY THREAD_ID
LIMIT 10;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT THREAD_ID,
    ->        PROCESSLIST_ID,
    ->        PROCESSLIST_USER,
    ->        PROCESSLIST_COMMAND,
    ->        INSTRUMENTED,
    ->        HISTORY
    -> FROM performance_schema.threads
    -> WHERE TYPE = 'FOREGROUND'
    -> ORDER BY THREAD_ID
    -> LIMIT 10;

+-----------+----------------+------------------+---------------------+--------------+---------+
| THREAD_ID | PROCESSLIST_ID | PROCESSLIST_USER | PROCESSLIST_COMMAND | INSTRUMENTED | HISTORY |
+-----------+----------------+------------------+---------------------+--------------+---------+
|        41 |              5 | event_scheduler  | Daemon              | YES          | YES     |
|        45 |              7 | NULL             | Daemon              | YES          | YES     |
|        53 |             15 | root             | Query               | YES          | YES     |
+-----------+----------------+------------------+---------------------+--------------+---------+
3 rows in set (0.00 sec)
```

- `THREAD_ID`: Performance Schema 내부 조인의 기본 키
- `PROCESSLIST_ID`: 클라이언트가 보는 connection ID와 연결할 때 사용
- `INSTRUMENTED`: 해당 스레드 계측 여부
- `HISTORY`: 해당 스레드의 history 수집 여부

실무 진단에서는 statement event의 `THREAD_ID`를 `threads.THREAD_ID`와 조인하고, 필요하면 account·host·processlist 정보로 확장한다. connection pool 환경에서는 하나의 DB 연결이 시간에 따라 여러 애플리케이션 요청을 처리하므로, DB thread만으로 최종 사용자 요청을 식별할 수 없다. 애플리케이션 trace ID, connection attributes, query tag 정책을 함께 설계해야 한다.

## 8. 운영 비용과 데이터 품질

### 8.1 계측 비용은 0이 아니다

Performance Schema는 운영 사용을 전제로 설계되었지만, 활성 instrument 수·호출 빈도·타이밍·history 크기에 따라 CPU와 메모리 비용이 달라진다. 특히 다음 조건에서 비용을 점검한다.

- 초당 statement 수가 매우 높다.
- 짧고 빈번한 mutex·stage·wait instrument를 대량 활성화한다.
- history와 history_long 크기를 크게 잡는다.
- digest 종류가 많아 `performance_schema_digests_size` 한도에 접근한다.
- memory instrument와 account·host·thread 차원의 집계가 크게 늘어난다.

변경 전후에 같은 부하 구간의 처리량, p95/p99 지연 시간, CPU, Performance Schema 메모리 사용량을 비교해야 한다. 계측 확대 자체가 사건의 지연 특성을 바꾸지 않았는지도 검토한다.

### 8.2 0과 NULL과 Empty set은 서로 다르다

- `0`: 계측·집계되었으나 관측값이 0일 수 있다.
- `NULL`: 해당 열에 값이 적용되지 않거나 수집할 수 없을 수 있다.
- `Empty set`: 조건과 일치하는 행이 없다는 뜻이며, 사건이 없었다는 증거는 아니다.

consumer 비활성, instrument 비활성, thread 비계측, history 순환, 권한 부족, digest 한도 초과도 빈 결과의 원인이 될 수 있다. 따라서 진단 쿼리 결과와 계측 설정을 함께 보존해야 한다.

### 8.3 초기화와 기준 시점

여러 summary 테이블은 `TRUNCATE TABLE`로 카운터를 초기화할 수 있지만, 이는 관측 기준선을 바꾸는 운영 행위다. 공유 서버에서 임의로 초기화하면 다른 분석자의 증거를 지울 수 있다. 초기화가 필요하다면 변경 승인, 정확한 대상 테이블, 초기화 시각, 수집된 사전 스냅샷을 남긴다.

## 9. Aurora MySQL에서의 해석

Aurora MySQL도 Performance Schema 인터페이스를 제공하지만, Community MySQL과 운영 환경이 동일하지는 않다.

1. **파라미터 적용 방식**: 변경 가능한 설정과 재시작이 필요한 설정을 DB parameter group의 적용 유형과 함께 확인한다. writer와 reader 인스턴스의 parameter group 및 적용 상태가 같은지도 점검한다.
2. **분산 스토리지 경계**: Aurora의 스토리지 계층은 Community MySQL의 로컬 InnoDB 파일·장치 모델과 다르다. 파일 I/O instrument만으로 분산 스토리지 전체 지연을 설명하지 말고 CloudWatch 지표와 Aurora 전용 wait 해석을 함께 사용한다.
3. **인스턴스별 관측**: Performance Schema 메모리와 event table은 접속한 DB 인스턴스의 관측 결과다. failover 전 writer의 메모리 내 이력이 새 writer로 영구 승계된다고 가정해서는 안 된다.
4. **Database Insights·Performance Insights 연계**: 관리형 관측 화면의 DB load와 wait dimension은 원인 범위를 좁히는 데 유용하지만, 실제 SQL 구조·row 작업량·thread 상태는 Performance Schema 및 실행 계획과 교차 확인한다.
5. **reader 부하 분리**: writer만 조사하면 읽기 endpoint 뒤의 특정 reader 병목을 놓칠 수 있다. 인스턴스별 연결 분포와 관측 시각을 기록한다.

Aurora 장애 대응에서는 “클러스터 상태”와 “현재 접속한 인스턴스의 Performance Schema 상태”를 구분해 기록하는 것이 중요하다.

## 10. 흔한 오해와 실패 모드

### 오해 1: Performance Schema가 켜져 있으면 모든 이벤트가 수집된다

서버 기능 활성화, instrument 활성화, thread instrumentation, consumer 활성화는 서로 다른 조건이다. 필요한 경로를 단계별로 확인해야 한다.

### 오해 2: history_long은 장기 로그다

이름의 `long`은 thread별 history보다 큰 전역 이력을 뜻할 뿐, 영구 보존을 보장하지 않는다. 고부하 서버에서는 짧은 시간 안에 순환할 수 있다.

### 오해 3: 평균 시간이 낮으면 안전하다

평균은 긴 꼬리 지연을 숨긴다. 최대값, 호출 횟수, 누적 시간, row 작업량과 함께 판단한다.

### 오해 4: summary를 초기화하면 서버 상태도 초기화된다

summary 카운터의 기준점만 바뀐다. 실제 캐시, lock, transaction, workload 상태를 초기화하는 동작과 혼동해서는 안 된다.

### 오해 5: 파일 wait만으로 스토리지 병목을 확정할 수 있다

파일 wait는 MySQL이 관측한 대기다. OS queue, 장치 지연, 가상화 계층, Aurora 분산 스토리지 지표와 교차 검증해야 한다.

### 오해 6: 테이블이 비어 있으므로 문제가 없었다

비활성 설정, 덮어쓰기, 권한, 조회 시점 때문에 증거가 없을 수 있다. 빈 결과는 계측 경로 점검의 시작점이다.

## 11. 운영 점검표

### 상시 기준선 설계

- [ ] `performance_schema` 활성 여부와 시작 옵션을 자산 정보에 기록한다.
- [ ] 상시 활성 instrument와 consumer의 최소 집합을 정의한다.
- [ ] statement digest 용량과 digest 소진 징후를 주기적으로 점검한다.
- [ ] current, history, history_long의 보존 목적과 예상 유지 시간을 구분한다.
- [ ] summary table의 초기화 권한과 절차를 제한한다.
- [ ] OS·slow log·애플리케이션 trace와 연결할 시간 기준을 통일한다.

### 사건 대응

- [ ] 대상 event class와 필요한 instrument가 활성화되어 있는지 먼저 확인한다.
- [ ] `setup_consumers`, `threads.INSTRUMENTED`, `threads.HISTORY`를 함께 확인한다.
- [ ] 설정 변경 전 현재값을 저장하고 원복 조건과 종료 시각을 정한다.
- [ ] current와 history 결과를 summary·digest 결과와 교차 확인한다.
- [ ] `Empty set`을 “문제 없음”으로 단정하지 않는다.
- [ ] 긴 출력 전체보다 사건과 관련된 열·시간 범위·thread를 좁혀 수집한다.
- [ ] Aurora에서는 writer/reader와 failover 전후 인스턴스를 구분한다.

### 비용과 보존

- [ ] 계측 확대 전후의 CPU, 처리량, 지연 시간, 메모리를 비교한다.
- [ ] history가 순환하기 전 외부로 보존할 핵심 열을 정의한다.
- [ ] 민감한 SQL literal과 개인 정보가 수집·반출되지 않도록 접근 통제를 적용한다.
- [ ] 임시 진단 설정을 사건 종료 후 원복했는지 확인한다.

## 12. 결론

Performance Schema의 핵심은 많은 테이블을 암기하는 데 있지 않다. **instrument가 측정 범위를 정하고, consumer가 저장 경로를 선택하며, event·summary table이 서로 다른 시간 해상도와 집계 수준으로 결과를 노출한다**는 연결 구조를 이해하는 데 있다.

운영자는 먼저 필요한 질문을 정한 뒤 event class, instrument, consumer, table을 역순으로 선택해야 한다. 그리고 빈 결과와 평균값을 과신하지 않고, 순환 버퍼의 보존 시간과 계측 비용을 함께 관리해야 한다. 다음 기술노트에서는 Performance Schema 설정을 실제 운영 기준선과 사건 대응 프로필로 나누어 설계하고, statement·wait·thread 정보를 조합하는 진단 패턴을 더 구체적으로 다룬다.
