---
title: "memory instrument로 MySQL 내부 메모리 사용량 추적하기"
description: "MySQL Performance Schema의 memory instrument로 전역·스레드·계정별 메모리 사용량과 고수위 값을 해석하는 방법을 정리한다."
tags: [ MySQL, 성능최적화, 운영, DBA ]
image: "mysql-report-bg.png"
published: "2026-08-20"
updated: "2026-08-20"
author: "MySQL 기술 노트"
source_url: ""
---

MySQL 프로세스의 RSS가 계속 증가하거나, 특정 배치가 실행된 뒤 메모리가 이전 수준으로 돌아오지 않으면 운영자는 먼저 “어느 구성 요소가 메모리를 사용했는가”를 알아야 한다. 그러나 `innodb_buffer_pool_size` 같은 설정값과 운영체제의 프로세스 메모리만 비교해서는 원인을 충분히 좁히기 어렵다. 설정값은 허용량이나 목표 크기를 나타내고, RSS는 MySQL뿐 아니라 메모리 할당자와 공유 라이브러리까지 포함한 프로세스 관측값이기 때문이다.

Performance Schema의 `memory/%` instrument와 `memory_summary_*` 테이블은 이 간극을 줄이는 서버 내부 관측 수단이다. 계측 가능한 할당과 해제를 구성 요소별로 집계하여 현재 사용량, 누적 할당량, 고수위 값을 보여 준다. 이 글에서는 memory instrument의 동작 원리, 전역·스레드·계정 관점의 조회 방법, RSS와 수치가 다른 이유, 메모리 증가 사고의 진단 순서와 Aurora MySQL에서의 해석 차이를 다룬다.

예제는 MySQL 8.0 계열의 전용 검증 인스턴스에서 실행한다. instrument 목록과 실제 바이트 값은 버전, 빌드 옵션, 활성 기능, 실행 시점에 따라 달라지므로 결과의 특정 숫자를 기준값으로 사용하지 않는다.

## 1. memory instrument가 답할 수 있는 질문

memory instrument는 MySQL 서버 코드의 메모리 할당 지점에 이름을 붙이고, 해당 지점을 통해 일어난 할당과 해제를 집계한다. 대표적인 질문은 다음과 같다.

- 현재 어떤 내부 구성 요소가 계측 범위 안에서 가장 많은 메모리를 보유하는가?
- 특정 시점 이후 어느 구성 요소의 현재 사용량 또는 고수위가 증가했는가?
- 연결 또는 실행 스레드별 메모리 사용량이 비정상적으로 치우쳤는가?
- 특정 사용자·호스트 조합의 workload가 세션 메모리 증가와 연관되는가?
- 설정 변경이나 배포 전후에 메모리 사용 구조가 어떻게 달라졌는가?

반대로 다음 질문에는 memory instrument만으로 완전한 답을 낼 수 없다.

- mysqld의 전체 RSS가 정확히 몇 바이트인가?
- 운영체제가 회수 가능한 page cache와 실제 메모리 압박은 어느 정도인가?
- 메모리 할당자의 arena, 캐시, 단편화가 얼마나 남아 있는가?
- 계측되지 않은 플러그인, 라이브러리, thread stack이 얼마나 사용하는가?
- 컨테이너 또는 호스트 전체에서 다른 프로세스와 어떤 자원 경쟁이 있는가?

따라서 memory instrument는 **MySQL 내부 귀속 분석**에 사용하고, RSS·cgroup·운영체제 메모리 지표는 **프로세스와 호스트의 실제 압박 분석**에 사용해야 한다. 두 관측값은 경쟁 관계가 아니라 서로 다른 계층의 증거다.

## 2. 할당과 해제가 summary 값이 되는 과정

memory instrument에는 대기 시간처럼 지속 시간을 측정하는 사건이 아니라, 할당과 해제의 증감 정보가 전달된다. Performance Schema는 이 정보를 event name과 집계 차원별로 누적한다.

```mermaid
flowchart LR
    A[MySQL 구성 요소] --> B[메모리 할당 또는 해제]
    B --> C{memory instrument 활성화?}
    C -- 아니요 --> D[Performance Schema 집계 밖]
    C -- 예 --> E[할당 횟수와 바이트 증감]
    E --> F[global summary]
    E --> G[thread summary]
    E --> H[account·user·host summary]
    F --> I[현재 사용량과 고수위]
    G --> J[세션·작업 스레드 귀속]
    H --> K[workload 집단 귀속]
    I --> L[OS·cgroup 지표와 대조]
    J --> L
    K --> L
```

핵심 산식은 개념적으로 다음과 같다.

```text
CURRENT_COUNT_USED
= COUNT_ALLOC - COUNT_FREE

CURRENT_NUMBER_OF_BYTES_USED
= SUM_NUMBER_OF_BYTES_ALLOC - SUM_NUMBER_OF_BYTES_FREE
```

`HIGH_*` 열은 관측 구간의 고수위(high-water mark)다. 현재 사용량이 낮더라도 고수위가 컸다면 일시적인 메모리 피크가 있었다는 뜻이다. 반대로 누적 할당 바이트가 매우 커도 현재 사용량이 작다면 같은 메모리가 반복적으로 할당되고 해제된 churn일 수 있다.

### 2.1 instrument와 consumer의 관계

statement나 wait 이벤트를 조사할 때는 instrument뿐 아니라 `events_*_current`, `events_*_history` 같은 consumer를 함께 확인한다. memory summary는 성격이 다르다. 메모리 할당 이벤트를 한 건씩 history 테이블에 저장하는 것이 아니라 summary에 직접 집계하므로, 일반적인 memory 분석에서 별도의 memory history consumer를 찾을 필요가 없다.

다만 대상 `memory/%` instrument가 비활성화되어 있으면 해당 지점의 할당과 해제를 온전히 추적할 수 없다. 장애가 발생한 뒤 instrument를 켜도 과거 할당 내역이 복원되는 것은 아니다. 관측 공백이 생기지 않도록 기본 활성 상태를 평상시에 확인하는 편이 중요하다.

### 2.2 이름은 구현 계층을 나타낸다

memory event name은 보통 다음과 같은 계층을 가진다.

```text
memory/<구성 요소>/<세부 할당 지점>
```

예를 들어 `memory/sql/...`, `memory/innodb/...`, `memory/performance_schema/...` 같은 접두사는 SQL 계층, InnoDB, Performance Schema 자체의 할당을 구분하는 단서다. 다만 event name은 안정적인 업무 API가 아니라 내부 구현과 가까운 이름이다. minor version에서 항목이 추가되거나 세분화될 수 있으므로, 자동화에서는 특정 이름 하나만 고정하기보다 접두사 집계와 존재 여부 확인을 함께 사용한다.

## 3. 대상 서버의 계측 범위 확인

먼저 대상 버전, memory instrument 수, 활성화 상태, summary 테이블 존재 여부를 확인한다.

```sql
SELECT VERSION() AS mysql_version;

SELECT ENABLED, COUNT(*) AS instrument_count
FROM performance_schema.setup_instruments
WHERE NAME LIKE 'memory/%'
GROUP BY ENABLED
ORDER BY ENABLED;

SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(NAME, '/', 2), '/', -1) AS component,
       COUNT(*) AS instrument_count
FROM performance_schema.setup_instruments
WHERE NAME LIKE 'memory/%'
GROUP BY component
ORDER BY instrument_count DESC, component
LIMIT 10;

SHOW TABLES FROM performance_schema LIKE 'memory_summary%';
```

실행 결과(MySQL 8.0.x):

구성 요소 집계는 상위 5개 행만 발췌했다.

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

mysql> SELECT ENABLED, COUNT(*) AS instrument_count
    -> FROM performance_schema.setup_instruments
    -> WHERE NAME LIKE 'memory/%'
    -> GROUP BY ENABLED ORDER BY ENABLED;
+---------+------------------+
| ENABLED | instrument_count |
+---------+------------------+
| YES     |              502 |
+---------+------------------+
1 row in set (0.00 sec)

mysql> SELECT ... AS component, COUNT(*) AS instrument_count
    -> FROM performance_schema.setup_instruments
    -> WHERE NAME LIKE 'memory/%'
    -> GROUP BY component ORDER BY instrument_count DESC, component LIMIT 10;
+--------------------+------------------+
| component          | instrument_count |
+--------------------+------------------+
| innodb             |              216 |
| sql                |              137 |
| performance_schema |               76 |
| myisam             |               22 |
| mysys              |               18 |
+--------------------+------------------+

mysql> SHOW TABLES FROM performance_schema LIKE 'memory_summary%';
+------------------------------------------------+
| Tables_in_performance_schema (memory_summary%) |
+------------------------------------------------+
| memory_summary_by_account_by_event_name        |
| memory_summary_by_host_by_event_name           |
| memory_summary_by_thread_by_event_name         |
| memory_summary_by_user_by_event_name           |
| memory_summary_global_by_event_name            |
+------------------------------------------------+
5 rows in set (0.01 sec)
```

`TIMED`는 memory instrument의 핵심 판단 열이 아니다. 메모리 계측은 대기 시간보다 할당·해제 횟수와 바이트를 집계하는 것이 목적이므로 `ENABLED`와 summary 값을 중심으로 확인한다.

기본값과 instrument 개수는 버전과 배포판에 따라 달라질 수 있다. 예제 결과에서 모든 항목이 활성화되어 있더라도 운영 서버가 반드시 같다고 가정해서는 안 된다. 다음과 같은 시작 옵션이나 런타임 설정 정책이 계측 범위를 바꿀 수 있다.

```text
performance-schema-instrument='memory/%=COUNTED'
performance-schema-instrument='memory/some/component=OFF'
```

설정 예시는 의미를 설명하기 위한 구조다. 실제 시작 옵션 문법과 대상 event name은 운영 중인 정확한 MySQL 버전에서 확인하고, parameter 관리 체계를 통해 적용한다. 조사 직전에 무차별적으로 전체 instrument를 변경하기보다 현재 상태를 먼저 보존하고 필요한 범위만 조정해야 한다.

## 4. summary 테이블의 열을 정확히 읽기

`performance_schema.memory_summary_global_by_event_name`의 주요 열은 다음 네 범주로 나뉜다.

| 범주 | 열 | 해석 |
|---|---|---|
| 누적 할당 | `COUNT_ALLOC`, `SUM_NUMBER_OF_BYTES_ALLOC` | 관측 구간에 발생한 할당 횟수와 바이트 합계 |
| 누적 해제 | `COUNT_FREE`, `SUM_NUMBER_OF_BYTES_FREE` | 관측 구간에 발생한 해제 횟수와 바이트 합계 |
| 현재 사용 | `CURRENT_COUNT_USED`, `CURRENT_NUMBER_OF_BYTES_USED` | 아직 해제되지 않은 계측 객체 수와 바이트 |
| 수위 | `LOW_*`, `HIGH_*` | 관측 구간의 최저·최고 사용량 |

현재 사용량이 큰 항목을 찾는 기본 조회는 다음과 같다.

```sql
SELECT EVENT_NAME,
       CURRENT_COUNT_USED,
       CURRENT_NUMBER_OF_BYTES_USED,
       HIGH_NUMBER_OF_BYTES_USED
FROM performance_schema.memory_summary_global_by_event_name
WHERE CURRENT_NUMBER_OF_BYTES_USED > 0
ORDER BY CURRENT_NUMBER_OF_BYTES_USED DESC
LIMIT 10;
```

실행 결과(MySQL 8.0.x):

검증 시점의 상위 5개 행만 발췌했다. 값과 순서는 서버 상태에 따라 달라진다.

```text
mysql> SELECT EVENT_NAME,
    ->        CURRENT_COUNT_USED,
    ->        CURRENT_NUMBER_OF_BYTES_USED,
    ->        HIGH_NUMBER_OF_BYTES_USED
    -> FROM performance_schema.memory_summary_global_by_event_name
    -> WHERE CURRENT_NUMBER_OF_BYTES_USED > 0
    -> ORDER BY CURRENT_NUMBER_OF_BYTES_USED DESC
    -> LIMIT 10;
+---------------------------------------------------------------+--------------------+------------------------------+---------------------------+
| EVENT_NAME                                                    | CURRENT_COUNT_USED | CURRENT_NUMBER_OF_BYTES_USED | HIGH_NUMBER_OF_BYTES_USED |
+---------------------------------------------------------------+--------------------+------------------------------+---------------------------+
| memory/innodb/buf_buf_pool                                    |                  1 |                     68620288 |                  68620288 |
| memory/performance_schema/events_statements_summary_by_digest |                  1 |                     42240000 |                  42240000 |
| memory/innodb/ut0link_buf                                     |                  2 |                     25165888 |                  25165888 |
| memory/innodb/log_buffer_memory                               |                  1 |                     16778224 |                  16778224 |
| memory/performance_schema/events_statements_history_long      |                  1 |                     15120000 |                  15120000 |
+---------------------------------------------------------------+--------------------+------------------------------+---------------------------+
```

이 결과는 “현재 계측된 메모리의 상위 event name”이다. 다음처럼 과도하게 해석해서는 안 된다.

- 1위 항목이 곧 memory leak의 원인이라는 뜻은 아니다.
- `HIGH_NUMBER_OF_BYTES_USED`가 현재값보다 크다는 사실만으로 장애가 있었다고 단정할 수 없다.
- 현재값의 합계가 mysqld RSS와 같아야 하는 것은 아니다.
- 이름에 `innodb`가 포함된 값을 단순 합산하여 Buffer Pool 전체 크기로 간주해서는 안 된다.

고수위와 현재값의 차이는 “피크 후 해제되었는가”를 보는 데 유용하다. 현재값과 고수위가 함께 계단식으로 상승하고 workload가 끝난 뒤에도 내려오지 않는다면 추가 조사가 필요하다. 반면 고수위만 높고 현재값이 정상 범위로 회복되었다면 일시적인 작업 메모리 피크였을 가능성이 크다.

### 4.1 누적 churn과 현재 보유량을 분리하기

누적 할당량은 장시간 실행한 서버에서 매우 커질 수 있다. 이를 현재 사용량으로 오해하면 안 된다. 할당과 해제가 빈번한 항목을 찾을 때는 현재값과 누적값을 함께 본다.

```sql
SELECT EVENT_NAME,
       COUNT_ALLOC,
       COUNT_FREE,
       SUM_NUMBER_OF_BYTES_ALLOC,
       SUM_NUMBER_OF_BYTES_FREE,
       CURRENT_NUMBER_OF_BYTES_USED
FROM performance_schema.memory_summary_global_by_event_name
WHERE COUNT_ALLOC > 0
ORDER BY SUM_NUMBER_OF_BYTES_ALLOC DESC
LIMIT 10;
```

실행 결과(MySQL 8.0.x):

누적 할당 상위 3개 행을 발췌했다. 세 번째 행은 많은 할당과 해제가 반복된 경우를 보여 준다.

```text
mysql> SELECT EVENT_NAME, COUNT_ALLOC, COUNT_FREE,
    ->        SUM_NUMBER_OF_BYTES_ALLOC, SUM_NUMBER_OF_BYTES_FREE,
    ->        CURRENT_NUMBER_OF_BYTES_USED
    -> FROM performance_schema.memory_summary_global_by_event_name
    -> WHERE COUNT_ALLOC > 0
    -> ORDER BY SUM_NUMBER_OF_BYTES_ALLOC DESC
    -> LIMIT 10;
+---------------------------------------------------------------+-------------+------------+---------------------------+--------------------------+------------------------------+
| EVENT_NAME                                                    | COUNT_ALLOC | COUNT_FREE | SUM_NUMBER_OF_BYTES_ALLOC | SUM_NUMBER_OF_BYTES_FREE | CURRENT_NUMBER_OF_BYTES_USED |
+---------------------------------------------------------------+-------------+------------+---------------------------+--------------------------+------------------------------+
| memory/innodb/buf_buf_pool                                    |           1 |          0 |                  68620288 |                        0 |                     68620288 |
| memory/performance_schema/events_statements_summary_by_digest |           1 |          0 |                  42240000 |                        0 |                     42240000 |
| memory/sql/dd::String_type                                    |       42303 |      38320 |                  34952832 |                 34389571 |                       563261 |
+---------------------------------------------------------------+-------------+------------+---------------------------+--------------------------+------------------------------+
```

`SUM_NUMBER_OF_BYTES_ALLOC`이 크고 `CURRENT_NUMBER_OF_BYTES_USED`가 상대적으로 작다면 반복 할당·해제가 많은 경로일 수 있다. 이 현상은 CPU 비용이나 메모리 할당자 contention과 관계될 수 있지만, 누적값만으로 문제라고 판정할 수는 없다. 서버 uptime과 workload 처리량으로 정규화하고 정상 시간대의 기준선과 비교해야 한다.

## 5. 구성 요소 단위로 좁히기

세부 event name 수백 개를 처음부터 읽으면 전체 구조를 놓치기 쉽다. 먼저 두 번째 경로 조각을 구성 요소로 보고 합산한 뒤, 큰 구성 요소 안에서 세부 event를 다시 확인하는 방식이 효율적이다.

```sql
SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(EVENT_NAME, '/', 2), '/', -1) AS component,
       SUM(CURRENT_NUMBER_OF_BYTES_USED) AS current_bytes,
       SUM(HIGH_NUMBER_OF_BYTES_USED) AS high_bytes,
       SUM(SUM_NUMBER_OF_BYTES_ALLOC) AS cumulative_alloc_bytes
FROM performance_schema.memory_summary_global_by_event_name
GROUP BY component
HAVING current_bytes > 0
ORDER BY current_bytes DESC
LIMIT 10;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(EVENT_NAME, '/', 2), '/', -1) AS component,
    ->        SUM(CURRENT_NUMBER_OF_BYTES_USED) AS current_bytes,
    ->        SUM(HIGH_NUMBER_OF_BYTES_USED) AS high_bytes,
    ->        SUM(SUM_NUMBER_OF_BYTES_ALLOC) AS cumulative_alloc_bytes
    -> FROM performance_schema.memory_summary_global_by_event_name
    -> GROUP BY component
    -> HAVING current_bytes > 0
    -> ORDER BY current_bytes DESC
    -> LIMIT 10;

+--------------------+---------------+------------+------------------------+
| component          | current_bytes | high_bytes | cumulative_alloc_bytes |
+--------------------+---------------+------------+------------------------+
| performance_schema |     229383592 |  229383592 |              229383592 |
| innodb             |     135127724 |  137377067 |              145564935 |
| sql                |      10348794 |   13752042 |               60142546 |
| mysys              |       8999901 |    9054178 |                9055417 |
| temptable          |       1048608 |    1048608 |                2097216 |
| mysqld_openssl     |        640031 |     662742 |                2192030 |
| mysqlx             |          3328 |       3328 |                   3328 |
| myisam             |           728 |        728 |                    728 |
| csv                |           120 |        120 |                    120 |
| blackhole          |           120 |        120 |                    120 |
+--------------------+---------------+------------+------------------------+
10 rows in set (0.00 sec)
```

이 집계는 원인 확정 쿼리가 아니라 **탐색 순서를 정하는 쿼리**다. `sql`이 크다면 prepared statement, table definition, parser, protocol, connection 관련 세부 항목을 본다. `performance_schema`가 크다면 history·digest·account·thread 크기 설정과 instrument 범위를 검토한다. `innodb`가 크다면 Buffer Pool 설정값과 InnoDB 내부 캐시·dictionary·lock·transaction 관련 세부 항목을 분리한다.

구성 요소 이름별 합계도 계측 범위 안의 값이다. 누락된 할당이나 event name 분류 차이가 있을 수 있으므로 총합을 물리 메모리 사용량으로 바꾸어 부르지 않는다.

## 6. 스레드별 메모리 귀속 분석

전역 합계가 증가했다면 다음 질문은 “어떤 실행 주체와 관련되는가”이다. `memory_summary_by_thread_by_event_name`을 `performance_schema.threads`와 결합하면 현재 연결 또는 백그라운드 스레드별 계측 메모리를 집계할 수 있다.

```sql
SELECT m.THREAD_ID,
       COALESCE(t.PROCESSLIST_USER, t.NAME) AS thread_owner,
       t.PROCESSLIST_ID,
       t.PROCESSLIST_COMMAND,
       SUM(m.CURRENT_NUMBER_OF_BYTES_USED) AS current_bytes,
       SUM(m.HIGH_NUMBER_OF_BYTES_USED) AS high_bytes
FROM performance_schema.memory_summary_by_thread_by_event_name AS m
JOIN performance_schema.threads AS t
  ON t.THREAD_ID = m.THREAD_ID
GROUP BY m.THREAD_ID,
         COALESCE(t.PROCESSLIST_USER, t.NAME),
         t.PROCESSLIST_ID,
         t.PROCESSLIST_COMMAND
HAVING current_bytes > 0
ORDER BY current_bytes DESC
LIMIT 10;
```

실행 결과(MySQL 8.0.x):

검증 시점의 상위 5개 thread를 발췌했다. `THREAD_ID`와 바이트 값은 실행마다 달라질 수 있다.

```text
mysql> SELECT m.THREAD_ID, COALESCE(t.PROCESSLIST_USER, t.NAME) AS thread_owner,
    ->        t.PROCESSLIST_ID, t.PROCESSLIST_COMMAND,
    ->        SUM(m.CURRENT_NUMBER_OF_BYTES_USED) AS current_bytes,
    ->        SUM(m.HIGH_NUMBER_OF_BYTES_USED) AS high_bytes
    -> FROM performance_schema.memory_summary_by_thread_by_event_name AS m
    -> JOIN performance_schema.threads AS t ON t.THREAD_ID = m.THREAD_ID
    -> GROUP BY m.THREAD_ID, thread_owner, t.PROCESSLIST_ID, t.PROCESSLIST_COMMAND
    -> HAVING current_bytes > 0 ORDER BY current_bytes DESC LIMIT 10;
+-----------+---------------------------------+----------------+---------------------+---------------+------------+
| THREAD_ID | thread_owner                    | PROCESSLIST_ID | PROCESSLIST_COMMAND | current_bytes | high_bytes |
+-----------+---------------------------------+----------------+---------------------+---------------+------------+
|        53 | root                            |             15 | Query               |       4499749 |    4571668 |
|         1 | thread/sql/main                 |           NULL | NULL                |       1823983 |    2240613 |
|        36 | thread/innodb/clone_gtid_thread |           NULL | NULL                |        518573 |     628666 |
|        41 | event_scheduler                 |              5 | Daemon              |         16665 |      16665 |
|        45 | thread/sql/compress_gtid_table  |              7 | Daemon              |         14432 |      15984 |
+-----------+---------------------------------+----------------+---------------------+---------------+------------+
```

결과를 해석할 때는 다음을 함께 고려한다.

1. `PROCESSLIST_ID`가 있는 foreground thread와 내부 background thread를 구분한다.
2. 쿼리가 이미 끝났는지, 연결이 idle 상태로 남았는지 확인한다.
3. prepared statement, session temporary table, 큰 패킷, 사용자 변수처럼 연결 수명에 묶인 상태를 조사한다.
4. connection pool의 연결 수명과 초기화·정리 정책을 확인한다.
5. 해당 스레드의 상위 event name을 다시 조회하여 어떤 계층이 큰지 확인한다.

특정 스레드를 세부적으로 볼 때는 `THREAD_ID`를 고정하여 조회한다. 다음 예시는 현재 진단 SQL을 실행하는 자기 세션의 상위 memory event를 확인한다.

```sql
SELECT m.EVENT_NAME,
       m.CURRENT_COUNT_USED,
       m.CURRENT_NUMBER_OF_BYTES_USED,
       m.HIGH_NUMBER_OF_BYTES_USED
FROM performance_schema.memory_summary_by_thread_by_event_name AS m
JOIN performance_schema.threads AS t
  ON t.THREAD_ID = m.THREAD_ID
WHERE t.PROCESSLIST_ID = CONNECTION_ID()
  AND m.CURRENT_NUMBER_OF_BYTES_USED > 0
ORDER BY m.CURRENT_NUMBER_OF_BYTES_USED DESC
LIMIT 10;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT m.EVENT_NAME,
    ->        m.CURRENT_COUNT_USED,
    ->        m.CURRENT_NUMBER_OF_BYTES_USED,
    ->        m.HIGH_NUMBER_OF_BYTES_USED
    -> FROM performance_schema.memory_summary_by_thread_by_event_name AS m
    -> JOIN performance_schema.threads AS t ON t.THREAD_ID = m.THREAD_ID
    -> WHERE t.PROCESSLIST_ID = CONNECTION_ID()
    ->   AND m.CURRENT_NUMBER_OF_BYTES_USED > 0
    -> ORDER BY m.CURRENT_NUMBER_OF_BYTES_USED DESC
    -> LIMIT 10;
+-------------------------------------------+--------------------+------------------------------+---------------------------+
| EVENT_NAME                                | CURRENT_COUNT_USED | CURRENT_NUMBER_OF_BYTES_USED | HIGH_NUMBER_OF_BYTES_USED |
+-------------------------------------------+--------------------+------------------------------+---------------------------+
| memory/sql/THD::main_mem_root             |                  3 |                        58512 |                     58512 |
| memory/sql/String::value                  |                  2 |                        16456 |                     32880 |
| memory/sql/Filesort_buffer::sort_keys     |                  1 |                         6038 |                      6038 |
| memory/sql/TABLE::sort_io_cache           |                  1 |                          336 |                       336 |
| memory/sql/THD::variables                 |                  1 |                          224 |                       224 |
| memory/sql/Filesort_info::record_pointers |                  1 |                          120 |                       120 |
| memory/sql/MYSQL_LOCK                     |                  1 |                          104 |                       104 |
| memory/sql/THD::db                        |                  1 |                           48 |                        48 |
+-------------------------------------------+--------------------+------------------------------+---------------------------+
8 rows in set (0.00 sec)
```

자기 세션을 조회하면 진단 쿼리 자체가 메모리를 사용하여 관측값에 영향을 줄 수 있다. 또한 매우 짧은 작업 메모리는 조회 시점 전에 해제될 수 있다. 순간 피크를 조사할 때는 일정 주기의 외부 수집, 고수위 변화, statement digest와 workload 시간대를 함께 사용한다.

### 6.1 종료된 스레드와 귀속의 한계

스레드별 summary는 현재 살아 있는 실행 주체를 좁히는 데 유용하지만, 모든 장기 메모리를 영구히 특정 연결에 귀속하는 감사 장부는 아니다. 연결 종료, 내부 소유권 이전, 전역 캐시 편입, 계측되지 않은 할당이 개입하면 현재 thread 목록만으로 과거 원인을 복원하기 어렵다.

따라서 사고 대응에서는 다음 세 가지를 같은 시각축에 보존하는 것이 좋다.

- 전역 memory summary의 현재값과 고수위
- thread·account 단위 summary의 상위 항목
- 연결 수, 실행 statement, OS 또는 관리형 서비스 메모리 지표

## 7. account·user·host 관점으로 workload를 묶기

개별 thread는 connection pool 교체나 재접속으로 수명이 짧을 수 있다. 특정 애플리케이션 계정이나 접속 출처 단위로 패턴을 보려면 다음 summary를 사용한다.

- `memory_summary_by_account_by_event_name`
- `memory_summary_by_user_by_event_name`
- `memory_summary_by_host_by_event_name`

계정별 현재값을 집계하는 예시는 다음과 같다.

```sql
SELECT USER AS account_user,
       HOST AS account_host,
       SUM(CURRENT_NUMBER_OF_BYTES_USED) AS current_bytes,
       SUM(HIGH_NUMBER_OF_BYTES_USED) AS high_bytes
FROM performance_schema.memory_summary_by_account_by_event_name
GROUP BY USER, HOST
HAVING current_bytes > 0
ORDER BY current_bytes DESC
LIMIT 10;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT USER AS account_user,
    ->        HOST AS account_host,
    ->        SUM(CURRENT_NUMBER_OF_BYTES_USED) AS current_bytes,
    ->        SUM(HIGH_NUMBER_OF_BYTES_USED) AS high_bytes
    -> FROM performance_schema.memory_summary_by_account_by_event_name
    -> GROUP BY USER, HOST
    -> HAVING current_bytes > 0
    -> ORDER BY current_bytes DESC
    -> LIMIT 10;

+-----------------+--------------+---------------+------------+
| account_user    | account_host | current_bytes | high_bytes |
+-----------------+--------------+---------------+------------+
| NULL            | NULL         |       2373556 |    6646551 |
| root            | localhost    |       1275961 |    5304690 |
| event_scheduler | localhost    |         16665 |      16665 |
+-----------------+--------------+---------------+------------+
3 rows in set (0.00 sec)
```

이 값은 업무별 자원 사용을 완벽하게 배부하는 회계 지표가 아니다. 동일 DB 계정을 여러 서비스가 공유하면 workload가 섞이고, background thread의 메모리는 일반 애플리케이션 계정으로 귀속되지 않을 수 있다. 운영 환경에서는 애플리케이션 또는 workload 역할별 DB 계정을 분리해야 진단의 해상도도 높아진다.

## 8. sys schema를 빠른 탐색에 활용하기

`sys` schema는 원시 Performance Schema 값을 읽기 쉬운 단위로 가공한 view를 제공한다. 대표적으로 다음 view를 사용할 수 있다.

| view | 용도 |
|---|---|
| `sys.memory_global_by_current_bytes` | 전역 현재 사용량 상위 event 확인 |
| `sys.memory_global_total` | 계측된 전역 현재 사용량 합계 확인 |
| `sys.memory_by_thread_by_current_bytes` | thread별 현재 메모리 탐색 |
| `sys.memory_by_user_by_current_bytes` | user별 현재 메모리 탐색 |
| `sys.memory_by_host_by_current_bytes` | host별 현재 메모리 탐색 |

```sql
SELECT event_name, current_count, current_alloc, high_alloc
FROM sys.memory_global_by_current_bytes
LIMIT 10;

SELECT total_allocated
FROM sys.memory_global_total;
```

실행 결과(MySQL 8.0.x):

첫 번째 결과는 상위 5개 행만 발췌했다.

```text
mysql> SELECT event_name, current_count, current_alloc, high_alloc
    -> FROM sys.memory_global_by_current_bytes
    -> LIMIT 10;
+---------------------------------------------------------------+---------------+---------------+------------+
| event_name                                                    | current_count | current_alloc | high_alloc |
+---------------------------------------------------------------+---------------+---------------+------------+
| memory/innodb/buf_buf_pool                                    |             1 | 65.44 MiB     | 65.44 MiB  |
| memory/performance_schema/events_statements_summary_by_digest |             1 | 40.28 MiB     | 40.28 MiB  |
| memory/innodb/ut0link_buf                                     |             2 | 24.00 MiB     | 24.00 MiB  |
| memory/innodb/log_buffer_memory                               |             1 | 16.00 MiB     | 16.00 MiB  |
| memory/performance_schema/events_statements_history_long      |             1 | 14.42 MiB     | 14.42 MiB  |
+---------------------------------------------------------------+---------------+---------------+------------+

mysql> SELECT total_allocated
    -> FROM sys.memory_global_total;
+-----------------+
| total_allocated |
+-----------------+
| 369.26 MiB      |
+-----------------+
1 row in set (0.00 sec)
```

`sys` view의 `current_alloc`, `high_alloc`, `total_allocated`는 사람이 읽기 쉬운 단위를 제공하므로 대화형 진단에 편리하다. 자동 수집과 임계값 비교에는 문자열 단위를 다시 파싱하기보다 Performance Schema 원본의 숫자형 byte 열이나 `sys.x$...` 원시 view를 사용하는 편이 안전하다.

또한 `sys.memory_global_total`의 값은 이름 때문에 전체 mysqld 메모리처럼 보이지만, 실제로는 **memory instrument가 계측한 현재 할당의 합계**다. 운영체제 RSS와 같은 의미로 사용해서는 안 된다.

## 9. 왜 계측 합계와 RSS가 다른가

memory summary를 올바르게 사용하려면 RSS와의 불일치를 정상적인 관측 경계로 이해해야 한다.

### 9.1 계측되지 않은 메모리

모든 `malloc()`, 라이브러리 내부 버퍼, plugin 메모리, thread stack이 memory instrument에 반드시 포함되는 것은 아니다. 버전이 올라가며 계측 범위가 확대되더라도 “모든 프로세스 바이트를 완전히 분류한다”는 보장은 없다.

### 9.2 메모리 할당자와 단편화

MySQL이 내부 객체를 해제해 `CURRENT_NUMBER_OF_BYTES_USED`가 감소해도 allocator가 해당 page를 운영체제에 즉시 반환하지 않을 수 있다. thread별 arena, 캐시, 단편화 때문에 RSS가 높은 수준에 머무를 수 있다. 이 경우 Performance Schema상 현재 사용량은 줄었지만 프로세스 RSS는 천천히 줄거나 그대로일 수 있다.

### 9.3 공유 메모리와 mmap 영역

운영체제 도구의 RSS, PSS, VSZ는 서로 다른 의미를 가진다. 공유 라이브러리 page, file-backed mapping, anonymous mapping을 어떤 방식으로 합산하는지에 따라 값이 달라진다. 컨테이너에서는 cgroup의 current·working set·limit도 함께 봐야 한다.

### 9.4 관측 시점의 차이

Performance Schema 조회와 외부 metric scrape가 몇 초만 어긋나도 순간적인 정렬, hash, 임시 테이블, 큰 result 전송의 피크를 서로 다르게 볼 수 있다. 사고 분석에서는 절대값 한 번보다 동일한 시간 해상도의 추세와 변화량이 더 중요하다.

운영 판단은 다음 관계를 사용한다.

```text
mysqld RSS 또는 cgroup 사용량
= 계측된 MySQL 현재 할당
+ 계측되지 않은 MySQL 영역
+ allocator 보유·단편화
+ 공유·매핑 영역의 관측 방식 차이
```

이는 정확한 산식이 아니라 서로 다른 관측 계층을 빠뜨리지 않기 위한 점검 틀이다.

## 10. 스냅샷과 변화량으로 분석하기

메모리 사고는 상위 목록 한 번보다 **증가 속도와 회복 여부**가 중요하다. 평상시 기준선과 문제 시점 스냅샷을 같은 쿼리로 수집하여 다음 값을 비교한다.

- `CURRENT_NUMBER_OF_BYTES_USED`의 증가량
- `HIGH_NUMBER_OF_BYTES_USED`의 신규 고수위
- `COUNT_ALLOC - COUNT_FREE`의 변화
- workload 처리량 대비 `SUM_NUMBER_OF_BYTES_ALLOC` 증가율
- thread·account별 상위 항목의 구성 변화

권장 수집 간격은 workload와 사고 특성에 따라 정한다. 수 초 안에 끝나는 피크를 분 단위로 수집하면 놓칠 수 있고, 수백 개 event를 지나치게 짧은 간격으로 외부 저장하면 관측 비용과 시계열 cardinality가 커진다. 일반적으로 다음 두 단계를 분리한다.

1. **상시 수집**: 전역 구성 요소별 합계, 전체 고수위, 연결 수, RSS·관리형 서비스 메모리 지표를 낮은 cardinality로 보존한다.
2. **사건 대응 수집**: 제한된 시간 동안 event name, thread, account 해상도를 높여 원인을 좁힌다.

### 10.1 summary 초기화는 해제가 아니다

Performance Schema summary 테이블을 `TRUNCATE TABLE`하여 통계를 초기화하는 운영 절차를 사용할 수 있지만, 이것은 MySQL 구성 요소가 보유한 실제 메모리를 해제하는 명령이 아니다. 또한 기준선을 초기화하면 사고 전 고수위와 누적 정보가 사라질 수 있다.

초기화가 필요하다면 먼저 결과를 외부에 저장하고, 대상 테이블의 reset semantics를 정확한 버전 문서에서 확인하며, 변경 시각을 기록한다. 원인 분석 중 편의를 위해 무심코 초기화하면 가장 중요한 비교 증거를 잃을 수 있다.

## 11. 대표적인 진단 시나리오

### 11.1 연결 수와 함께 `memory/sql/...`이 증가한다

다음 항목을 순서대로 확인한다.

- `Threads_connected`, `Threads_running`, connection pool 상한
- thread별 현재 메모리 상위값
- prepared statement, session temporary table, 큰 패킷, 사용자 변수 사용
- idle 연결이 업무 종료 후에도 장시간 유지되는지
- 연결 반환 시 세션 상태를 정리하거나 연결을 적절히 재생성하는지

`max_connections`만 높이면 접속 실패는 잠시 줄어도 세션 메모리 곱셈 효과가 커질 수 있다. 연결 폭증의 원인과 pool backpressure를 먼저 바로잡아야 한다.

### 11.2 고수위는 증가했지만 현재값은 회복한다

대형 배치, 통계 수집, 복잡한 정렬·집계, 백업 또는 관리 작업의 순간 피크일 수 있다. 현재값이 정상으로 돌아왔더라도 RSS가 즉시 내려오지 않는다면 allocator 보유와 단편화를 함께 조사한다. 메모리 limit에 근접한 피크라면 평균이 정상이라는 이유로 무시하지 말고 동시 실행 제한이나 workload 분리를 검토한다.

### 11.3 현재값과 RSS가 함께 계단식으로 증가한다

- 증가한 event name과 component를 찾는다.
- thread·account 귀속 여부를 확인한다.
- 동일 workload가 종료된 뒤 현재값이 감소하는지 본다.
- 신규 배포, plugin, schema 수 증가, prepared statement 수 증가와 시간을 대조한다.
- 계측 합계의 증가보다 RSS 증가가 훨씬 크면 allocator·계측 밖 영역·외부 라이브러리를 조사한다.

증가했다는 사실만으로 leak이라고 부르지 않는다. dictionary, table cache, prepared statement cache처럼 workload 범위가 넓어지면서 정상적으로 채워지는 캐시일 수 있다. 충분한 안정화 시간 뒤 plateau가 형성되는지 확인한다.

### 11.4 Performance Schema 자체가 상위에 나타난다

Performance Schema도 관측 데이터를 저장하기 위해 메모리를 사용한다. 다음을 점검한다.

- statement digest 수와 관련 크기 설정
- history와 history_long 크기
- thread, account, user, host별 summary cardinality
- 불필요하게 활성화한 instrument와 consumer
- incident 대응 후 원복하지 않은 관측 프로필

진단 기능을 통째로 끄는 것은 다음 장애의 원인 규명 능력을 잃게 만든다. 필요한 관측 신호는 유지하고, 고비용 history와 불필요한 cardinality를 조정하는 방식이 바람직하다.

## 12. 흔한 오해와 주의사항

### 12.1 “current bytes 합계가 실제 사용 메모리다”

계측 범위의 현재값일 뿐이다. RSS, cgroup, 관리형 서비스 지표와 반드시 대조한다.

### 12.2 “누적 할당량이 크면 memory leak이다”

누적 할당량은 서버 uptime과 처리량에 따라 계속 증가한다. leak 판단에는 현재 보유량의 지속 증가와 workload 종료 후 미회복이 더 중요하다.

### 12.3 “high bytes가 크면 현재도 위험하다”

고수위는 과거 피크다. 현재값, 피크 시각의 workload, 메모리 limit까지 남은 여유를 함께 본다.

### 12.4 “사고가 난 뒤 instrument를 켜면 과거가 복원된다”

비활성 기간의 과거 할당 내역은 자동 복원되지 않는다. 이미 할당된 객체가 나중에 해제되는 경우에는 관측 시작점이 달라 해석이 더 어려워질 수 있다. 상시 기준선을 유지하는 이유다.

### 12.5 “상위 thread를 종료하면 문제가 해결된다”

종료는 업무 실패와 rollback을 유발할 수 있으며, 메모리가 전역 캐시나 다른 소유권으로 이동했다면 기대한 만큼 RSS가 줄지 않을 수도 있다. 먼저 SQL·업무 영향·트랜잭션 상태를 확인하고 승인된 장애 대응 절차를 따른다.

### 12.6 event name을 영구 API처럼 고정한다

내부 event name은 버전별로 추가·변경될 수 있다. 수집기는 존재 여부를 확인하고, 세부 이름뿐 아니라 component 집계를 함께 보존해야 한다.

## 13. Aurora MySQL에서의 운영 해석

Aurora MySQL에서도 호환 버전과 설정이 제공하는 Performance Schema memory summary를 진단에 활용할 수 있다. 다만 관리형 서비스의 관측 경계가 다르다.

- DB 호스트의 `/proc`나 mysqld 프로세스를 직접 조사할 수 없으므로 CloudWatch의 `FreeableMemory`, 연결 수, swap 관련 지표와 Performance Insights 또는 Database Insights의 workload 정보를 함께 본다.
- writer와 reader는 서로 다른 DB instance이므로 memory summary와 고수위도 인스턴스별로 수집한다.
- failover나 instance restart 후 서버 로컬 summary와 고수위가 이어진다고 가정하지 않는다. 외부 시계열에 인스턴스 식별자와 role 변경 시각을 남긴다.
- Performance Schema 관련 설정은 DB cluster parameter와 DB parameter의 적용 범위, 재시작 필요 여부를 확인한다.
- Aurora의 분산 스토리지 구조 때문에 Community MySQL과 메모리 구성의 비중이 같다고 가정하지 않는다. event name의 실제 상위 항목을 해당 엔진 버전에서 확인한다.

특히 `FreeableMemory`가 감소하는데 계측된 현재 메모리 합계가 거의 변하지 않는다면 곧바로 Performance Schema 오류라고 단정하지 않는다. 운영체제·엔진 관리 영역, allocator, 계측 밖 메모리, page cache 성격의 차이를 고려하고 AWS가 제공하는 인스턴스 지표와 엔진 로그를 함께 분석한다.

## 14. 메모리 증가 사고 대응 체크리스트

### 관측 준비

- [ ] `performance_schema`가 활성화되어 있는지 확인한다.
- [ ] `memory/%` instrument의 존재와 `ENABLED` 분포를 버전별로 기록한다.
- [ ] 전역 component별 현재값과 고수위의 평상시 기준선을 보존한다.
- [ ] mysqld RSS, cgroup 또는 관리형 서비스 메모리 지표를 같은 시간축에 수집한다.
- [ ] 서버 재시작·failover·summary 초기화 시각을 외부에 기록한다.

### 사고 시 진단

- [ ] `memory_summary_global_by_event_name`에서 현재값과 고수위 상위 항목을 구분한다.
- [ ] 누적 할당량과 현재 보유량을 혼동하지 않는다.
- [ ] component 집계로 큰 계층을 찾은 뒤 세부 event name으로 좁힌다.
- [ ] thread·account·user·host summary로 workload 귀속 가능성을 확인한다.
- [ ] 연결 수, 실행 statement, 배치·백업·DDL·배포 시각을 대조한다.
- [ ] 계측 합계와 RSS 차이를 allocator·단편화·계측 밖 영역 관점에서 분석한다.
- [ ] 원인 분석 전에 summary를 초기화하거나 스레드를 즉시 종료하지 않는다.

### 조치와 검증

- [ ] 설정값 확대보다 workload 동시성, connection pool, 쿼리 계획을 먼저 검토한다.
- [ ] instrument나 consumer 변경은 범위·시작·원복 시각을 기록한다.
- [ ] 조치 후 현재값 증가율, 고수위, RSS와 업무 지연을 함께 비교한다.
- [ ] event name과 임계값이 업그레이드 후에도 유효한지 재검증한다.
- [ ] Aurora MySQL은 writer·reader를 분리하고 failover 경계를 반영한다.

## 15. 결론

Performance Schema의 memory instrument는 MySQL 내부 메모리를 구성 요소와 실행 주체에 귀속시키는 가장 중요한 출발점이다. `CURRENT_*`는 현재 보유량, `HIGH_*`는 관측 구간의 피크, 누적 alloc/free는 churn을 설명한다. 그러나 이 값은 계측 범위의 내부 장부이지 mysqld RSS의 대체물이 아니다.

안정적인 운영 분석은 전역 상위 event를 찾고, component로 범위를 줄이며, thread·account 차원으로 workload를 연결한 뒤, 같은 시간대의 RSS·연결 수·statement 지표와 대조하는 순서로 진행한다. 다음 기술노트에서는 이러한 계측 결과를 주기적으로 수집할 때 필요한 기준선, 변화율, cardinality와 보존 정책을 더 구체적으로 다룰 수 있다.
