Performance Schema overhead 관리: 필요한 instrument만 켜는 기준
MySQL Performance Schema의 계측 비용을 instrument·consumer·보존 범위별로 분해하고 안전하게 조정하는 운영 기준을 정리한다.
Performance Schema를 모두 끄면 관측 비용은 줄어들 수 있지만, 장애가 발생했을 때 원인을 설명할 증거도 사라진다. 반대로 진단에 도움이 될 것이라는 이유로 모든 instrument와 history consumer를 항상 활성화하면 고빈도 코드 경로의 계측, timer 호출, 이벤트 행 유지, summary 갱신, 메모리 사용이 불필요하게 늘어날 수 있다. 운영 목표는 “최소 비용”이 아니라 장애 대응에 필요한 관측 가능성을 허용 가능한 비용으로 유지하는 것이다.
이 판단은 performance_schema=ON 또는 OFF라는 단일 스위치로 끝나지 않는다. instrument가 어떤 서버 코드 경로를 측정하는지, ENABLED와 TIMED가 각각 어떤 일을 하는지, consumer가 개별 이벤트를 어디에 저장하는지, history 크기와 digest 수가 메모리에 어떤 영향을 주는지를 분리해서 보아야 한다.
이 글에서는 MySQL 8.0 이상을 기준으로 다음 내용을 다룬다.
- 계측 비용이 발생하는 경로와 비용의 종류
- 상시 활성화할 관측 범위와 사건 대응 시 임시로 넓힐 범위
- instrument의 빈도·timer·보존 필요성에 따른 선택 기준
- 변경 전 상태를 저장하고 대상 범위만 조정한 뒤 원복하는 절차
- Performance Schema 자체 메모리와 손실 카운터를 확인하는 방법
- Aurora MySQL에서 parameter group과 관리형 지표를 함께 해석하는 기준
예제는 전용 MySQL 8.0 검증 인스턴스에서 실행한다. instrument 목록, 기본 활성 상태, PROPERTIES, VOLATILITY, 메모리 사용량은 minor version, 빌드, 플러그인, workload에 따라 달라질 수 있다. 따라서 이 글의 개수와 바이트 값을 임계치로 복사하지 말고 대상 서버에서 직접 기준선을 만들어야 한다.
1. overhead는 한 종류가 아니다
Performance Schema의 비용을 논의할 때 “몇 퍼센트 느려진다”는 하나의 숫자만 찾으면 잘못된 결론에 도달하기 쉽다. 실제 비용은 다음 요소가 결합된 결과다.
| 비용 축 | 발생 지점 | 주요 영향 요인 |
|---|---|---|
| instrument 진입 비용 | 계측된 서버 코드 경로 | 호출 빈도, ENABLED, instrument 구현 |
| timer 비용 | 이벤트 시작·종료 시각 측정 | TIMED, timer 종류, 초당 이벤트 수 |
| event materialization | 이벤트 속성을 행 형태로 구성 | current/history consumer, SQL text·stack 등 속성 |
| consumer 저장 비용 | current·history·history_long 유지 | 활성 consumer, 이벤트 발생률, 버퍼 크기 |
| summary 갱신 비용 | digest·wait·stage 집계 갱신 | 집계 차원, 계정·호스트·스레드 수, digest 다양성 |
| 메모리 비용 | 내부 버퍼와 instance 저장 | sizing 변수, autoscaling, workload cardinality |
| 조회 비용 | 큰 Performance Schema 테이블 스캔 | 조회 주기, 필터·집계 방식, 수집기 동시성 |
즉, instrument를 켜는 비용과 history를 보존하는 비용은 같지 않다. 어떤 wait instrument를 활성화하되 TIMED='NO'로 두면 이벤트 수 집계는 가능하지만 시간 기반 병목 분석은 제한된다. instrument를 timing까지 활성화해도 개별 history consumer를 끄고 summary만 사용하면 상세 행 보존 비용을 줄일 수 있다. 반대로 instrument가 꺼져 있으면 consumer를 켜도 해당 이벤트는 생성되지 않는다.
1.1 비용이 이벤트 발생률에 비례하는 이유
저빈도 DDL stage와 초당 수십만 번 실행되는 mutex 또는 memory allocation 경로는 같은 ENABLED='YES'라도 비용 특성이 다르다. 계측 지점을 지날 때마다 조건 확인, timer 읽기, 속성 수집, summary 갱신 중 일부가 실행되기 때문이다.
따라서 instrument 이름 수만 세어 “1,000개 중 500개가 켜졌으니 절반의 비용”이라고 해석해서는 안 된다. 거의 실행되지 않는 instrument 수백 개보다 hot path의 고빈도 instrument 몇 개가 더 큰 영향을 줄 수 있다. 판단 단위는 instrument 개수가 아니라 workload가 해당 지점을 통과하는 빈도와 수집하는 정보의 깊이다.
2. 이벤트가 저장될 때까지의 비용 경로
다음 흐름은 계측 범위를 조정할 때 확인해야 할 순서를 보여 준다.
flowchart LR
A[서버 코드의 실행 지점] --> B{instrument ENABLED?}
B -- 아니요 --> X[계측하지 않음]
B -- 예 --> C{TIMED 지원·활성?}
C -- 아니요 --> D[횟수·바이트 등 비시간 속성]
C -- 예 --> E[timer 읽기와 경과 시간]
D --> F{상위 instrumentation과 consumer?}
E --> F
F --> G[current 이벤트]
F --> H[history·history_long]
F --> I[summary·digest 집계]
G --> J[현재 상황 진단]
H --> K[짧은 사건 흐름 재구성]
I --> L[장기 경향과 상위 항목]
이 구조에서 비용을 줄이는 순서는 보통 다음과 같다.
- 필요하지 않은
history_long처럼 보존 비용이 큰 consumer를 먼저 검토한다. - 개별 이벤트가 필요하지 않으면 summary 중심으로 관측한다.
- 시간 정보가 필요 없는 instrument는
TIMED='NO'가 가능한지 검토한다. - 대상 문제와 무관한 고빈도 instrument 범위를 좁힌다.
- Performance Schema 전체 비활성화는 관측 요구를 다른 수단으로 충족할 수 있는지 검토한 뒤 마지막에 판단한다.
무조건 instrument부터 대량으로 끄는 방식은 권장하지 않는다. statement digest, data lock, metadata lock, file I/O summary처럼 평상시 축적되어야 의미가 있는 증거가 사라질 수 있기 때문이다.
3. 먼저 현재 상태를 재고표로 만든다
운영 변경 전에 대상 버전의 실제 instrument와 consumer 상태를 보존해야 한다. 문서의 기본값이나 다른 서버의 결과를 기준으로 삼으면 플러그인, 배포판, 시작 옵션 차이를 놓친다.
다음 조회는 이벤트 class별 instrument 수와 활성·timing 범위, 주요 consumer의 현재 상태를 보여 준다.
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,
SUM(TIMED IS NULL) AS timing_unsupported_count
FROM performance_schema.setup_instruments
GROUP BY SUBSTRING_INDEX(NAME, '/', 1)
ORDER BY event_class;
SELECT NAME, ENABLED
FROM performance_schema.setup_consumers
ORDER BY NAME;
실행 결과(MySQL 8.0.x):
consumer 결과는 운영 판단에 필요한 주요 항목만 발췌했다.
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, ...
-> FROM performance_schema.setup_instruments
-> GROUP BY SUBSTRING_INDEX(NAME, '/', 1)
-> ORDER BY event_class;
+-------------+------------------+---------------+-------------+--------------------------+
| event_class | instrument_count | enabled_count | timed_count | timing_unsupported_count |
+-------------+------------------+---------------+-------------+--------------------------+
| error | 1 | 1 | NULL | 1 |
| idle | 1 | 1 | 1 | 0 |
| memory | 502 | 502 | NULL | 502 |
| stage | 132 | 16 | 16 | 0 |
| statement | 213 | 198 | 198 | 0 |
| transaction | 1 | 1 | 1 | 0 |
| wait | 400 | 54 | 54 | 0 |
+-------------+------------------+---------------+-------------+--------------------------+
7 rows in set (0.01 sec)
mysql> SELECT NAME, ENABLED
-> FROM performance_schema.setup_consumers
-> ORDER BY NAME;
+--------------------------------+---------+
| NAME | ENABLED |
+--------------------------------+---------+
| events_statements_current | YES |
| events_statements_history | YES |
| events_statements_history_long | NO |
| events_waits_current | NO |
| events_waits_history | NO |
| global_instrumentation | YES |
| statements_digest | YES |
| thread_instrumentation | YES |
+--------------------------------+---------+
이 재고표는 다음 질문의 출발점이다.
- statement digest가 활성화되어 있는가?
- wait, stage 중 현재 꺼진 범위는 무엇인가?
events_*_history_long이 상시 활성화되어 있는가?- memory instrument처럼 timing을 지원하지 않는 class를 timer 관점에서 잘못 평가하고 있지 않은가?
- 서버별 설정 차이가 표준에서 벗어났는가?
TIMED IS NULL은 오류가 아니다. 예를 들어 memory instrument는 할당·해제 횟수와 바이트를 집계하며 이벤트 경과 시간을 측정하지 않으므로 timing을 지원하지 않을 수 있다.
3.1 instrument 목록은 대상 서버가 정답이다
MySQL 소스 코드와 플러그인이 바뀌면 instrument 집합도 바뀐다. 따라서 자동화는 존재하지 않을 수 있는 exact name 하나를 무조건 갱신하기보다 다음 원칙을 따라야 한다.
- 변경 전에
SELECT NAME ... WHERE NAME LIKE '<접두사>%'로 대상을 확인한다. - 접두사가 지나치게 넓지 않은지 행 수와 구성 요소를 검토한다.
- 변경한 exact name, 이전
ENABLED, 이전TIMED를 감사 로그에 남긴다. - 버전 업그레이드 후에는 새 instrument가 접두사에 편입되는지 다시 검토한다.
4. 필요한 instrument를 고르는 다섯 가지 기준
instrument를 상시 켤지, 사건 대응 중에만 켤지, 끌지를 결정할 때는 다음 다섯 축을 함께 본다.
4.1 운영 질문과 직접 연결되는가
먼저 계측이 답해야 할 질문을 명확히 한다.
| 운영 질문 | 우선 관측 범위 | 일반적으로 필요한 결과 |
|---|---|---|
| 어떤 SQL 유형이 부하를 만드는가 | statement/%, statements_digest |
digest summary |
| 어느 파일 계층에서 I/O 대기가 큰가 | wait/io/file/% |
file·wait summary |
| 쿼리 내부 어느 단계가 느린가 | 관련 stage/% |
stage current/history 또는 summary |
| mutex·rwlock 경합이 있는가 | 제한한 wait/synch/% |
timing summary, 필요 시 history |
| 메모리가 어느 구성 요소에 귀속되는가 | memory/% |
memory summary |
| 현재 lock 관계가 무엇인가 | lock 관련 table·instrument | data_locks, data_lock_waits, metadata_locks |
질문과 연결되지 않는 계측을 “나중에 쓸 수도 있다”는 이유로 항상 확장하지 않는다. 반대로 장애 후 복원할 수 없는 증거는 상시 기준선에 포함해야 한다. digest 누적값이나 고수위처럼 과거부터 쌓여야 의미가 있는 정보가 대표적이다.
4.2 코드 경로의 호출 빈도가 높은가
호출 빈도가 높은 instrument일수록 좁은 범위에서 단계적으로 활성화한다. 특히 wait/synch/mutex/%, wait/synch/rwlock/%, intra-query 수준의 세부 stage는 workload에 따라 매우 자주 실행될 수 있다.
setup_instruments.VOLATILITY는 instrument의 예상 수명과 생성·소멸 특성을 이해하는 보조 정보다. 값이 높다고 곧바로 overhead가 높다는 뜻은 아니지만, query 또는 intra-query 수명에 가까운 항목은 workload 빈도와 함께 주의해서 평가할 수 있다.
다음 조회는 활성 wait instrument를 volatility별로 묶고, 동기화 instrument의 세부 분포를 확인한다.
SELECT VOLATILITY,
COUNT(*) AS instrument_count,
SUM(ENABLED = 'YES') AS enabled_count,
SUM(TIMED = 'YES') AS timed_count
FROM performance_schema.setup_instruments
WHERE NAME LIKE 'wait/%'
GROUP BY VOLATILITY
ORDER BY VOLATILITY;
SELECT SUBSTRING_INDEX(NAME, '/', 4) AS instrument_family,
COUNT(*) AS instrument_count,
SUM(ENABLED = 'YES') AS enabled_count,
SUM(TIMED = 'YES') AS timed_count
FROM performance_schema.setup_instruments
WHERE NAME LIKE 'wait/synch/%'
GROUP BY SUBSTRING_INDEX(NAME, '/', 4)
ORDER BY instrument_family;
실행 결과(MySQL 8.0.x):
두 번째 결과는 전체 27개 family 가운데 대표 항목만 발췌했다.
mysql> SELECT VOLATILITY, COUNT(*) AS instrument_count,
-> SUM(ENABLED = 'YES') AS enabled_count,
-> SUM(TIMED = 'YES') AS timed_count
-> FROM performance_schema.setup_instruments
-> WHERE NAME LIKE 'wait/%'
-> GROUP BY VOLATILITY ORDER BY VOLATILITY;
+------------+------------------+---------------+-------------+
| VOLATILITY | instrument_count | enabled_count | timed_count |
+------------+------------------+---------------+-------------+
| 0 | 391 | 54 | 54 |
| 1 | 2 | 0 | 0 |
| 5 | 7 | 0 | 0 |
+------------+------------------+---------------+-------------+
3 rows in set (0.00 sec)
mysql> SELECT SUBSTRING_INDEX(NAME, '/', 4) AS instrument_family, ...
-> FROM performance_schema.setup_instruments
-> WHERE NAME LIKE 'wait/synch/%'
-> GROUP BY SUBSTRING_INDEX(NAME, '/', 4)
-> ORDER BY instrument_family;
+--------------------------+------------------+---------------+-------------+
| instrument_family | instrument_count | enabled_count | timed_count |
+--------------------------+------------------+---------------+-------------+
| wait/synch/cond/sql | 43 | 0 | 0 |
| wait/synch/mutex/innodb | 88 | 0 | 0 |
| wait/synch/mutex/sql | 107 | 0 | 0 |
| wait/synch/rwlock/sql | 21 | 0 | 0 |
| wait/synch/sxlock/innodb | 15 | 0 | 0 |
+--------------------------+------------------+---------------+-------------+
이 결과는 변경 대상을 찾는 지도이지 overhead 측정 결과가 아니다. 실제 비용은 benchmark 또는 제한된 canary 변경으로 확인해야 한다.
4.3 시간 정보가 실제로 필요한가
ENABLED='YES', TIMED='NO'는 이벤트를 완전히 끄는 것과 다르다. timing을 지원하는 instrument에서 timer 수집을 생략하면 횟수나 비시간 속성을 유지할 수 있지만 SUM_TIMER_WAIT, AVG_TIMER_WAIT, MAX_TIMER_WAIT 같은 지연 분석은 사용할 수 없거나 NULL로 남는다.
선택 기준은 다음과 같다.
- 병목의 크기와 순위를 비교해야 한다면 timing이 필요하다.
- 단순 발생 여부나 횟수만 필요하고 hot path 비용이 우려되면 untimed 관측을 검토한다.
- 어떤 항목을 timing 없이 두었는지 대시보드와 runbook에 표시한다.
- timing이 꺼진 결과를 “대기 시간이 0”이라고 해석하지 않는다.
4.4 개별 event가 필요한가, summary면 충분한가
summary는 고빈도 이벤트를 압축하여 상위 항목과 장기 경향을 보여 준다. current/history는 SQL text, thread, event 순서처럼 사건 문맥을 보존한다. 일반적으로 평상시에는 summary를 유지하고, 짧은 흐름 재구성이 필요할 때 제한된 history를 추가하는 방식이 효율적이다.
- 상시 용량·부하 추세: summary와 digest 중심
- 현재 실행·대기 확인:
events_*_current - 특정 연결의 직전 흐름:
events_*_history - 서버 전체의 짧은 사건 재구성:
events_*_history_long
history_long은 영구 로그가 아니라 고정 크기 순환 버퍼다. 이벤트 발생률이 높으면 비용을 부담하고도 사건 행이 금방 덮어써질 수 있다. 장기 보존 요구는 필요한 열만 외부로 수집하거나 slow query log 등 다른 수단으로 충족해야 한다.
4.5 결과를 실제로 사용할 수 있는가
관측 데이터를 수집하지만 누구도 조회하지 않거나, 수집기가 너무 늦게 읽어 사건이 이미 사라진다면 비용 대비 가치가 낮다. 각 활성 범위에는 다음이 연결되어야 한다.
- 조회 SQL 또는 대시보드
- 수집 주기와 보존 기간
- 임계치가 아니라 기준선 대비 판단 규칙
- 이상 시 담당자와 후속 진단 절차
- 임시 활성화라면 자동 원복 시각
5. 권장 운영 프로필: 상시 기준선과 사건 대응
모든 서버에 동일한 profile을 기계적으로 적용할 수는 없지만, 운영 설계는 세 단계로 구분하면 관리하기 쉽다.
5.1 상시 기준선
상시 기준선은 장애가 난 뒤에는 되살릴 수 없는 누적 정보와 저비용 관측을 중심으로 구성한다.
- statement instrument와 digest summary
- transaction의 기본 current·summary
- 핵심 file I/O와 table I/O summary
- data lock·metadata lock 진단에 필요한 범위
- memory summary가 필요한 서버의 memory instrument
global_instrumentation,thread_instrumentation- 짧고 제한된 current 또는 per-thread history
상시 기준선의 목적은 모든 세부 사건을 저장하는 것이 아니라 이상 징후를 어느 class와 workload로 좁힐 수 있을 정도의 지도를 보존하는 것이다.
5.2 사건 대응 profile
원인이 특정 class로 좁혀지면 제한된 시간 동안 관측 깊이를 높인다.
- InnoDB file I/O 의심:
wait/io/file/innodb/%timing과 관련 summary - 내부 동기화 경합 의심: exact mutex/rwlock family만 timing 활성화
- 쿼리 단계 병목 의심: 관련
stage/sql/%와 stage current/history - 짧은 statement 흐름 필요: statement history 또는 제한된 history_long
변경에는 종료 조건을 반드시 둔다. 예를 들어 15분간 관측하거나 재현 3회가 끝나면 원복한다. “문제가 해결될 때까지”는 종료 조건이 아니다.
5.3 실험 profile
모든 instrument와 넓은 history를 활성화해야 하는 실험은 운영 primary가 아니라 부하 재현 환경 또는 traffic을 제한한 canary에서 수행하는 편이 안전하다. 이 profile에서는 관측 해상도보다 workload 재현성과 전후 비교의 신뢰성이 중요하다.
6. 변경 전 저장, 좁은 적용, 확인, 원복
Performance Schema setup 변경은 전역 영향을 줄 수 있다. 운영 세션 하나만의 진단 설정이라고 생각하면 안 된다. 다음 예제는 전용 검증 인스턴스에서 InnoDB file I/O instrument와 wait current/history consumer의 현재 상태를 임시 테이블에 저장하고, 대상 범위만 활성화한 뒤 원래 값으로 되돌리는 패턴을 보여 준다.
DROP TEMPORARY TABLE IF EXISTS ps_saved_instruments;
DROP TEMPORARY TABLE IF EXISTS ps_saved_consumers;
CREATE TEMPORARY TABLE ps_saved_instruments AS
SELECT NAME, ENABLED, TIMED
FROM performance_schema.setup_instruments
WHERE NAME LIKE 'wait/io/file/innodb/%';
CREATE TEMPORARY TABLE ps_saved_consumers AS
SELECT NAME, ENABLED
FROM performance_schema.setup_consumers
WHERE NAME IN ('events_waits_current', 'events_waits_history');
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME LIKE 'wait/io/file/innodb/%';
UPDATE performance_schema.setup_consumers
SET ENABLED = 'YES'
WHERE NAME IN ('events_waits_current', 'events_waits_history');
SELECT 'instrument_profile_enabled' AS phase,
COUNT(*) AS target_count,
SUM(ENABLED = 'YES') AS enabled_count,
SUM(TIMED = 'YES') AS timed_count
FROM performance_schema.setup_instruments
WHERE NAME LIKE 'wait/io/file/innodb/%';
SELECT NAME, ENABLED
FROM performance_schema.setup_consumers
WHERE NAME IN ('events_waits_current', 'events_waits_history')
ORDER BY NAME;
UPDATE performance_schema.setup_instruments AS current_state
JOIN ps_saved_instruments AS saved
ON saved.NAME = current_state.NAME
SET current_state.ENABLED = saved.ENABLED,
current_state.TIMED = saved.TIMED;
UPDATE performance_schema.setup_consumers AS current_state
JOIN ps_saved_consumers AS saved
ON saved.NAME = current_state.NAME
SET current_state.ENABLED = saved.ENABLED;
SELECT 'profile_restored' AS phase,
SUM(current_state.ENABLED = saved.ENABLED) AS enabled_restored,
SUM(current_state.TIMED <=> saved.TIMED) AS timed_restored,
COUNT(*) AS target_count
FROM performance_schema.setup_instruments AS current_state
JOIN ps_saved_instruments AS saved
ON saved.NAME = current_state.NAME;
실행 결과(MySQL 8.0.x):
준비용 temporary table 생성과 profile 활성화·원복 확인 결과를 발췌했다.
mysql> CREATE TEMPORARY TABLE ps_saved_instruments AS
-> SELECT NAME, ENABLED, TIMED
-> FROM performance_schema.setup_instruments
-> WHERE NAME LIKE 'wait/io/file/innodb/%';
Query OK, 8 rows affected (0.00 sec)
Records: 8 Duplicates: 0 Warnings: 0
mysql> CREATE TEMPORARY TABLE ps_saved_consumers AS
-> SELECT NAME, ENABLED
-> FROM performance_schema.setup_consumers
-> WHERE NAME IN ('events_waits_current', 'events_waits_history');
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> SELECT 'instrument_profile_enabled' AS phase, ...
+----------------------------+--------------+---------------+-------------+
| phase | target_count | enabled_count | timed_count |
+----------------------------+--------------+---------------+-------------+
| instrument_profile_enabled | 8 | 8 | 8 |
+----------------------------+--------------+---------------+-------------+
1 row in set (0.00 sec)
mysql> SELECT NAME, ENABLED
-> FROM performance_schema.setup_consumers
-> WHERE NAME IN ('events_waits_current', 'events_waits_history')
-> ORDER BY NAME;
+----------------------+---------+
| NAME | ENABLED |
+----------------------+---------+
| events_waits_current | YES |
| events_waits_history | YES |
+----------------------+---------+
2 rows in set (0.00 sec)
mysql> SELECT 'profile_restored' AS phase, ...
+------------------+------------------+----------------+--------------+
| phase | enabled_restored | timed_restored | target_count |
+------------------+------------------+----------------+--------------+
| profile_restored | 8 | 8 | 8 |
+------------------+------------------+----------------+--------------+
1 row in set (0.00 sec)
운영에서는 temporary table만 믿지 않는다. 세션이 끊어지면 저장본도 사라지기 때문이다. 자동화 시스템, 변경 티켓 또는 안전한 외부 저장소에 exact row의 이전값을 보존하고 finally 또는 timeout 기반 원복을 구현해야 한다.
또한 runtime 갱신이 모든 instrument instance에 즉시 같은 방식으로 적용된다고 가정해서는 안 된다. 일부 mutex, condition, rwlock 등은 instance 생성 시점 또는 server startup의 영향을 받을 수 있다. 변경 후에는 목적 테이블에 실제 데이터가 들어오는지 확인하고, 적용되지 않는 범위는 정확한 버전의 문서와 재시작 정책을 검토한다.
6.1 전체 UPDATE가 위험한 이유
다음과 같은 명령은 문법상 가능하더라도 운영 진단의 기본 절차로 사용하지 않는다.
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES';
이 명령은 다음 문제를 만든다.
- 조사 대상과 무관한 hot path까지 한 번에 변경한다.
- timing을 지원하지 않는 instrument와 적용 특성이 다른 instrument를 구분하지 않는다.
- 새 버전에서 추가된 instrument도 무심코 포함한다.
- 전후 비용 증가의 원인을 어느 class로 귀속하기 어렵다.
- 원복 기준이 없으면 진단 후 설정 drift가 남는다.
항상 SELECT로 대상을 미리 확인하고 exact prefix 또는 name으로 좁혀 변경한다.
7. Performance Schema 자체 메모리와 수집 손실 확인
Performance Schema는 데이터를 메모리에 유지한다. 일부 내부 버퍼는 startup에 할당되고, autoscaled 구조는 workload에 따라 추가로 성장할 수 있다. 운영 중 할당한 메모리가 재사용되더라도 즉시 운영체제에 반환되지 않을 수 있으므로, 현재 workload가 줄었다고 프로세스 RSS가 같은 폭으로 줄어들 것이라고 기대해서는 안 된다.
memory/performance_schema/% instrument를 이용하면 내부 버퍼별 계측 메모리를 확인할 수 있다. 함께 Performance_schema%lost 상태 변수를 확인하면 sizing 한도 때문에 instance나 digest 등이 기록되지 못하는지 알 수 있다.
SELECT EVENT_NAME,
CURRENT_NUMBER_OF_BYTES_USED,
HIGH_NUMBER_OF_BYTES_USED
FROM performance_schema.memory_summary_global_by_event_name
WHERE EVENT_NAME LIKE 'memory/performance_schema/%'
AND CURRENT_NUMBER_OF_BYTES_USED > 0
ORDER BY CURRENT_NUMBER_OF_BYTES_USED DESC
LIMIT 10;
SELECT VARIABLE_NAME, VARIABLE_VALUE
FROM performance_schema.global_status
WHERE VARIABLE_NAME LIKE 'Performance_schema%lost'
AND CAST(VARIABLE_VALUE AS UNSIGNED) > 0
ORDER BY VARIABLE_NAME;
실행 결과(MySQL 8.0.x):
메모리 결과는 현재 사용량 상위 5개 항목만 발췌했다. 값과 순서는 서버 상태에 따라 달라진다.
mysql> SELECT EVENT_NAME,
-> CURRENT_NUMBER_OF_BYTES_USED,
-> HIGH_NUMBER_OF_BYTES_USED
-> FROM performance_schema.memory_summary_global_by_event_name
-> WHERE EVENT_NAME LIKE 'memory/performance_schema/%'
-> AND CURRENT_NUMBER_OF_BYTES_USED > 0
-> ORDER BY CURRENT_NUMBER_OF_BYTES_USED DESC
-> LIMIT 10;
+-----------------------------------------------------------------------------+------------------------------+---------------------------+
| EVENT_NAME | CURRENT_NUMBER_OF_BYTES_USED | HIGH_NUMBER_OF_BYTES_USED |
+-----------------------------------------------------------------------------+------------------------------+---------------------------+
| memory/performance_schema/events_statements_summary_by_digest | 42240000 | 42240000 |
| memory/performance_schema/events_statements_history_long | 15120000 | 15120000 |
| memory/performance_schema/events_errors_summary_by_thread_by_error | 13455360 | 13455360 |
| memory/performance_schema/events_statements_summary_by_thread_by_event_name | 12109824 | 12109824 |
| memory/performance_schema/events_statements_summary_by_digest.digest_text | 10240000 | 10240000 |
+-----------------------------------------------------------------------------+------------------------------+---------------------------+
mysql> SELECT VARIABLE_NAME, VARIABLE_VALUE
-> FROM performance_schema.global_status
-> WHERE VARIABLE_NAME LIKE 'Performance_schema%lost'
-> AND CAST(VARIABLE_VALUE AS UNSIGNED) > 0
-> ORDER BY VARIABLE_NAME;
Empty set (0.00 sec)
첫 조회의 합계는 mysqld 전체 RSS가 아니다. Performance Schema에 귀속되어 계측된 내부 메모리다. Buffer Pool, connection buffer, allocator cache, 공유 라이브러리 등 다른 영역은 별도로 해석해야 한다.
두 번째 조회가 Empty set이면 검증 시점에 값이 0보다 큰 lost counter가 없다는 뜻이다. 하지만 다음을 의미하지는 않는다.
- 모든 필요한 instrument가 활성화되어 있다는 뜻이 아니다.
- history 순환으로 과거 이벤트가 덮어쓰이지 않았다는 뜻이 아니다.
- overhead가 낮다는 뜻이 아니다.
- 계측되지 않은 사건이 없다는 뜻이 아니다.
lost counter가 증가하면 무조건 sizing 값을 키우기 전에 어떤 데이터가 손실되었는지 확인한다. 예를 들어 digest 손실은 SQL 유형 분석 해상도에 직접 영향을 줄 수 있지만, 사용하지 않는 고차원 집계의 일부 손실은 비용을 늘려가며 복구할 가치가 낮을 수도 있다.
7.1 autoscaling과 명시적 한도
여러 performance_schema_*_size 또는 performance_schema_max_* 변수는 -1일 때 autoscaling될 수 있다. 명시적 양수 한도를 두면 한도까지 성장한 뒤 추가 데이터를 잃고 대응 lost counter가 증가할 수 있다. 0은 해당 내부 구조를 비활성화하는 의미가 될 수 있다.
따라서 sizing 정책은 다음 균형을 다룬다.
- 무제한에 가까운 성장으로 인한 메모리 압박
- 너무 작은 한도로 인한 관측 데이터 손실
- 사용하지 않는 dimension을 유지하는 비용
- 장애 시 필요한 최소 해상도
변수의 동적 변경 가능 여부와 적용 시점은 변수마다 다르다. startup sizing 변수는 parameter 변경만으로 즉시 내부 구조가 바뀌지 않을 수 있으므로 재시작 필요 여부를 확인한다.
8. CPU와 처리량 overhead를 측정하는 방법
Performance Schema 테이블만 조회해서 Performance Schema의 CPU 비용을 정확히 계산할 수는 없다. 계측을 측정하는 계측 자체가 workload 경로에 포함되며, server CPU에는 SQL 실행, I/O 처리, background thread도 함께 섞인다. 따라서 동일한 workload를 profile별로 반복하여 비교해야 한다.
8.1 최소 비교 설계
- 동일한 데이터, Buffer Pool warm 상태, 연결 수를 준비한다.
- 상시 기준선 profile로 충분한 시간 동안 workload를 실행한다.
- 처리량, p50·p95·p99 latency, mysqld CPU, context switch, RSS를 기록한다.
- instrument family 하나 또는 consumer 하나만 변경한다.
- 같은 workload를 같은 시간 동안 반복한다.
- 여러 차례 순서를 교차하여 cache·시간대 효과를 줄인다.
- 평균뿐 아니라 분산과 tail latency를 비교한다.
측정 항목은 다음처럼 구분한다.
| 계층 | 권장 지표 |
|---|---|
| 애플리케이션 | 성공 처리량, 오류율, p95·p99 latency |
| MySQL | QPS/TPS, threads running, statement latency, lock·I/O wait |
| 운영체제·컨테이너 | mysqld CPU, RSS, context switch, I/O latency |
| Performance Schema | 내부 memory, lost counter, 활성 instrument·consumer snapshot |
전후 차이가 작다면 “overhead가 0”이라고 결론내리지 않는다. 해당 workload와 측정 오차 범위에서는 차이를 분리하지 못했다고 기록한다. 반대로 latency가 증가해도 cache miss, checkpoint, background purge, noisy neighbor 같은 교란 변수를 제거하지 않았다면 Performance Schema만 원인으로 단정할 수 없다.
8.2 운영 canary에서의 안전 기준
production workload로 확인해야 한다면 traffic 일부를 받는 replica 또는 canary instance를 사용한다. 변경 전 중단 기준을 정한다.
- p99 latency가 기준선 대비 허용 범위를 초과
- CPU headroom이 운영 최소치 아래로 하락
- replication lag가 증가
- 메모리 여유가 감소하거나 OOM 위험 신호 발생
- lost counter가 증가해 정작 필요한 데이터가 누락
중단 기준을 충족하면 profile을 즉시 원복하고 수집한 snapshot을 보존한다.
9. 조회 쿼리도 overhead의 일부다
instrument와 consumer를 적절히 구성해도 수집기가 큰 테이블을 짧은 주기로 full scan하면 별도의 부하가 생긴다. 특히 events_statements_history_long, digest summary, thread별·account별 summary를 여러 대시보드가 동시에 집계하면 mutex, CPU, 메모리 bandwidth 사용이 늘 수 있다.
수집 쿼리는 다음 원칙을 따른다.
- 필요한 열만 선택한다.
- event class, schema, time 또는 상위 N 조건으로 범위를 줄인다.
- 같은 원시 테이블을 여러 수집기가 중복 조회하지 않게 한다.
- 초 단위 고빈도 polling이 실제 대응 속도에 필요한지 검토한다.
- 큰 순환 버퍼 전체를 매번 외부로 복제하지 않는다.
- 수집 쿼리 자체의 digest와 latency도 관찰한다.
Performance Schema는 일반 테이블과 인덱싱 특성이 다르므로 WHERE가 항상 일반 InnoDB 보조 인덱스처럼 동작한다고 기대해서는 안 된다. 결과 행을 줄이는 것과 내부 스캔 비용을 줄이는 것이 같은지 대상 테이블별로 확인해야 한다.
10. 흔한 실패 방식과 오해
10.1 “Performance Schema는 가볍다” 또는 “항상 무겁다”
둘 다 지나치게 단순한 주장이다. 실제 비용은 engine version, 활성 범위, workload의 hot path, event rate, consumer, sizing, 조회 패턴에 따라 달라진다. 운영 결정은 profile별 benchmark와 canary 관측으로 내려야 한다.
10.2 모든 wait를 끄고 장애 때 켠다
장애가 재현되지 않거나 켜기 전에 끝나면 증거가 남지 않는다. file I/O, lock, statement digest처럼 사후 복원이 어려운 핵심 summary는 상시 기준선을 유지하고, 세부 mutex timing과 긴 history를 사건 대응 profile로 분리한다.
10.3 history_long을 켜면 로그가 생긴다
history_long은 순환 버퍼다. 영구 audit log나 장기 시계열 저장소가 아니다. 이벤트 발생률과 버퍼 행 수를 고려하지 않으면 장애 감지 전에 필요한 행이 사라질 수 있다.
10.4 instrument 수로 overhead를 추정한다
instrument 100개가 모두 같은 빈도로 호출되는 것이 아니다. hot mutex 하나와 거의 실행되지 않는 stage 수십 개의 비용을 동일하게 셀 수 없다.
10.5 TIMED='NO'를 대기 시간 0으로 읽는다
timer가 꺼져 있으면 0초가 아니라 시간 정보가 수집되지 않은 것이다. 대시보드는 untimed 상태를 별도 표시해야 한다.
10.6 변경 전 상태를 저장하지 않는다
문제가 끝난 뒤 “기본값”으로 돌리는 것은 원복이 아니다. 원래 서버가 버전·역할별로 다른 profile을 사용했을 수 있다. exact previous value를 저장해야 한다.
10.7 메모리 summary 합계를 RSS로 해석한다
memory/performance_schema/%는 계측된 Performance Schema 내부 메모리다. 운영체제 RSS와 범위가 다르다. 둘을 함께 보되 같아야 한다고 가정하지 않는다.
11. Aurora MySQL에서의 운영 해석
Aurora MySQL도 Performance Schema를 사용하지만, 관리형 서비스라는 점이 변경·측정 절차에 영향을 준다.
첫째, startup 성격의 performance_schema_* 변수는 DB cluster parameter group 또는 DB parameter group의 적용 범위와 재시작 필요 여부를 확인해야 한다. runtime의 setup_instruments, setup_consumers 변경과 parameter group 변경은 지속성과 적용 시점이 다르다. runtime 변경은 재시작이나 failover 후 유지되지 않을 수 있으므로 영구 표준은 parameter 관리 및 초기화 자동화와 연결한다.
둘째, writer와 reader의 workload가 다르므로 한 인스턴스의 profile을 전체 cluster에 그대로 적용하지 않는다. writer는 commit, lock, 변경 statement가 중심이고 reader는 read query, buffer access, replica lag와 연계한 관측이 중요할 수 있다. 역할별 기준선을 만든다.
셋째, Performance Schema overhead 평가는 CloudWatch의 CPU, FreeableMemory, DatabaseConnections, replica lag 계열 지표와 함께 수행한다. Performance Insights 또는 Database Insights가 활성화된 환경에서는 해당 서비스가 요구하는 Performance Schema 설정과 사용 중인 engine version의 동작을 먼저 확인한다. 필요한 consumer를 임의로 끄면 관리형 분석 화면의 해상도가 낮아질 수 있다.
넷째, Aurora의 분산 스토리지 경로를 Community MySQL의 로컬 파일 I/O와 완전히 같은 의미로 해석하지 않는다. wait/io/file/innodb/%가 보여 주는 서버 내부 계측과 서비스 차원의 storage latency·I/O 지표를 함께 보아야 한다.
12. 운영 변경 체크리스트
변경 전
-
setup_instruments,setup_consumers
변경 중
-
Performance_schema%lost
변경 후
13. 결론
Performance Schema overhead 관리는 instrument를 많이 끄는 작업이 아니라, 운영 질문·이벤트 빈도·timing 필요성·보존 깊이·메모리 한도를 연결하는 관측 설계 작업이다. 상시 기준선은 사후에 복원할 수 없는 digest와 핵심 summary를 남기고, 사건 대응 profile은 원인이 좁혀진 범위의 timing과 history만 제한된 시간 동안 확장해야 한다.
가장 중요한 운영 습관은 변경 전 상태를 보존하고, 한 번에 하나의 범위만 바꾸며, 실제 workload의 처리량과 tail latency로 비용을 검증하고, 정해진 시각에 원복하는 것이다. 다음 기술노트에서는 이 관측 설계를 바탕으로 Performance Schema 수집 결과를 장기 시계열과 경보 체계에 연결할 때 어떤 지표를 어떤 주기로 외부화해야 하는지 다룬다.