카테고리 : MySQL/기술노트

Change Buffer의 역할: secondary index 변경 지연과 운영상 장단점

InnoDB Change Buffer가 secondary index 변경을 지연 병합하는 방식과 성능 이점, 장애 복구 비용, 운영상 주의점을 정리한다.

저자: MySQL 기술 노트 작성: 2026.06.04 약 13분 7,368자
다운로드

1. 왜 Change Buffer를 이해해야 하는가

InnoDB는 데이터 변경이 발생할 때 clustered index와 secondary index를 함께 관리한다. 문제는 secondary index의 leaf page가 항상 buffer pool에 올라와 있지는 않다는 점이다. 대량 INSERT, UPDATE, DELETE가 임의의 secondary key 값을 만들면 InnoDB는 많은 인덱스 페이지를 디스크에서 읽어 와야 한다. 변경 자체는 작은데, 변경할 위치를 찾기 위한 random read가 지배적인 비용이 될 수 있다.

Change Buffer는 이 비용을 줄이기 위한 InnoDB의 지연 반영 메커니즘이다. buffer pool에 없는 non-unique secondary index page에 대한 일부 변경을 즉시 해당 leaf page에 반영하지 않고, system tablespace 안의 change buffer 영역에 먼저 기록한다. 이후 해당 secondary index page가 읽히거나 백그라운드 merge가 진행될 때 원래 index page에 병합한다.

운영 관점에서 Change Buffer는 단순한 성능 옵션이 아니다. 쓰기 부하를 낮추는 장치이면서, 동시에 나중에 처리해야 할 merge backlog를 만든다. 쓰기 많은 시스템에서 순간 처리량을 높일 수 있지만, 읽기 지연, checkpoint 압력, shutdown 지연, 장애 복구 시간 증가로 되돌아올 수 있다. 따라서 Change Buffer는 “켜면 빨라지는 기능”이 아니라 “random secondary index I/O를 어느 시점에 부담할 것인가”를 조절하는 기능으로 이해해야 한다.

2. Change Buffer의 핵심 구조

InnoDB 테이블은 primary key를 기준으로 clustered index를 구성한다. 실제 row는 clustered index leaf page에 저장되고, secondary index leaf entry는 secondary key와 primary key 값을 포함해 clustered index row를 찾아갈 수 있게 한다.

secondary index 변경 시 InnoDB는 다음 두 가지 상황을 구분한다.

  • 대상 secondary index page가 buffer pool에 이미 있음: 즉시 page를 수정하고 dirty page로 관리한다.
  • 대상 secondary index page가 buffer pool에 없음: 조건을 만족하면 change buffer에 변경 내용을 기록하고, 실제 secondary index page 읽기를 나중으로 미룬다.

Change Buffer가 적용되는 대상은 일반적으로 non-unique secondary index 변경이다. unique secondary index는 중복 여부를 확인해야 하므로 대상 index page를 읽지 않고 변경을 지연하기 어렵다. foreign key 검증이나 unique 제약 확인이 필요한 경우에도 단순 지연 반영만으로는 정확성을 보장할 수 없다.

flowchart TD
    A[DML: INSERT/UPDATE/DELETE] --> B{대상 secondary index page가 buffer pool에 있는가?}
    B -- 예 --> C[secondary index leaf page 즉시 수정]
    B -- 아니오 --> D{Change Buffer 적용 가능한가?\nnon-unique secondary index 등}
    D -- 예 --> E[change buffer에 변경 기록]
    D -- 아니오 --> F[index page를 읽고 즉시 수정]
    E --> G[나중에 page read 또는 background merge 시 병합]
    C --> H[redo 기록 및 dirty page flush]
    F --> H
    G --> H

중요한 점은 Change Buffer가 redo log를 대체하지 않는다는 것이다. change buffer에 기록되는 변경도 crash recovery를 위해 redo log 보호를 받는다. 즉, Change Buffer는 durability 장치가 아니라 I/O 스케줄링 장치다. 디스크에서 secondary index page를 즉시 읽어 오는 비용을 줄이고, 변경 반영을 더 유리한 시점으로 옮긴다.

3. 어떤 변경이 지연되는가

innodb_change_buffering 변수는 어떤 종류의 secondary index 변경을 buffering할지 제어한다. 대표 값은 all, inserts, deletes, changes, purges, none이다. 실제 운영에서는 기본값과 워크로드 특성을 함께 봐야 한다.

  • inserts: secondary index insert entry를 지연한다.
  • deletes: delete-mark 관련 변경을 지연한다.
  • purges: purge 단계의 물리 제거를 지연한다.
  • changes: insert와 delete 관련 변경을 포함한다.
  • all: 지원되는 변경을 모두 buffering한다.
  • none: Change Buffer를 사용하지 않는다.

설정 자체는 서버 변수로 확인할 수 있다.

SELECT VERSION() AS mysql_version;
SHOW VARIABLES LIKE 'innodb_change_buffering';
SHOW VARIABLES LIKE 'innodb_change_buffer_max_size';

실행 결과(MySQL 8.0.46):

mysql> SELECT VERSION() AS mysql_version;

+---------------+
| mysql_version |
+---------------+
| 8.0.46        |
+---------------+
1 row in set (0.00 sec)

mysql> SHOW VARIABLES LIKE 'innodb_change_buffering';

+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| innodb_change_buffering | all   |
+-------------------------+-------+
1 row in set (0.00 sec)

mysql> SHOW VARIABLES LIKE 'innodb_change_buffer_max_size';

+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| innodb_change_buffer_max_size | 25    |
+-------------------------------+-------+
1 row in set (0.00 sec)

innodb_change_buffer_max_size는 buffer pool 대비 change buffer가 사용할 수 있는 최대 비율을 의미한다. Change Buffer가 커질수록 더 많은 지연 변경을 담을 수 있지만, buffer pool의 다른 용도와 간접적으로 경쟁한다. 매우 쓰기 많은 시스템에서 값을 크게 하면 쓰기 순간 처리량은 좋아질 수 있으나, merge가 늦어져 읽기 지연과 복구 비용이 커질 수 있다.

4. DML 실행 경로에서 보는 동작 원리

다음 예제는 Change Buffer 자체를 강제로 관측하기 위한 재현이 아니라, secondary index 변경이 어떤 구조의 테이블에서 발생하는지 확인하기 위한 축소 예제다. 실제 Change Buffer 사용 여부는 page가 buffer pool에 존재하는지, index가 unique인지, 워크로드와 서버 상태가 어떤지에 따라 달라진다.

DROP TABLE IF EXISTS change_buffer_note;
CREATE TABLE change_buffer_note (
  id BIGINT NOT NULL AUTO_INCREMENT,
  customer_id BIGINT NOT NULL,
  status VARCHAR(20) NOT NULL,
  created_at DATETIME NOT NULL,
  amount DECIMAL(10,2) NOT NULL,
  PRIMARY KEY (id),
  KEY idx_customer_status (customer_id, status),
  KEY idx_created_at (created_at)
) ENGINE=InnoDB;

INSERT INTO change_buffer_note (customer_id, status, created_at, amount) VALUES
  (101, 'READY', '2026-06-04 09:00:00', 150.00),
  (102, 'READY', '2026-06-04 09:01:00', 210.50),
  (101, 'PAID',  '2026-06-04 09:02:00', 150.00);

EXPLAIN SELECT id, amount
FROM change_buffer_note
WHERE customer_id = 101 AND status = 'READY';

DROP TABLE change_buffer_note;

실행 결과(MySQL 8.0.46):

mysql> CREATE TABLE change_buffer_note (...);
Query OK, 0 rows affected (0.01 sec)

mysql> INSERT INTO change_buffer_note (customer_id, status, created_at, amount) VALUES (...);
Query OK, 3 rows affected (0.00 sec)
Records: 3  Duplicates: 0  Warnings: 0

mysql> EXPLAIN SELECT id, amount
    -> FROM change_buffer_note
    -> WHERE customer_id = 101 AND status = 'READY';
+----+-------------+--------------------+------------+------+---------------------+---------------------+---------+-------------+------+----------+-------+
| id | select_type | table              | partitions | type | possible_keys       | key                 | key_len | ref         | rows | filtered | Extra |
+----+-------------+--------------------+------------+------+---------------------+---------------------+---------+-------------+------+----------+-------+
|  1 | SIMPLE      | change_buffer_note | NULL       | ref  | idx_customer_status | idx_customer_status | 90      | const,const |    1 |   100.00 | NULL  |
+----+-------------+--------------------+------------+------+---------------------+---------------------+---------+-------------+------+----------+-------+
1 row in set (0.00 sec)

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

이 테이블에서 한 row를 insert하면 clustered index인 PRIMARY에는 row 자체가 들어가고, idx_customer_status, idx_created_at에는 각각 secondary index entry가 추가된다. secondary index page가 buffer pool에 없고 index가 non-unique라면 InnoDB는 page를 즉시 읽지 않고 Change Buffer에 entry를 남길 수 있다.

5. merge는 언제 일어나는가

Change Buffer에 쌓인 변경은 영구히 남아 있지 않는다. 언젠가는 실제 secondary index page에 병합되어야 한다. 대표적인 merge 계기는 다음과 같다.

  1. 쿼리가 해당 secondary index page를 읽어야 할 때
  2. InnoDB 백그라운드 thread가 여유 I/O를 사용해 change buffer merge를 수행할 때
  3. shutdown, checkpoint, purge, buffer pool 압력 등으로 병합이 진행될 때
  4. crash recovery 이후 일관성을 회복하는 과정에서 필요한 병합이 수행될 때

이 구조는 쓰기 부하와 읽기 부하의 비용 분포를 바꾼다. 대량 insert 직후 바로 같은 secondary index 범위를 조회하면, 조회 시점에 merge 비용을 함께 부담할 수 있다. 반대로 쓰기 후 해당 index page가 한동안 읽히지 않는다면 random read를 피한 효과가 크게 나타난다.

sequenceDiagram
    participant DML as DML 세션
    participant IBuf as Change Buffer
    participant BPool as Buffer Pool
    participant Index as Secondary Index Page
    participant BG as Background Merge

    DML->>BPool: secondary index page 확인
    BPool-->>DML: page 없음
    DML->>IBuf: 변경 entry 기록
    DML-->>DML: commit 진행
    alt 나중에 쿼리가 index page 읽음
        DML->>Index: page read
        DML->>IBuf: 관련 변경 조회
        IBuf->>Index: 변경 병합
    else 백그라운드 병합
        BG->>IBuf: merge 대상 선택
        BG->>Index: page read 후 병합
    end

6. 운영에서 확인할 지표와 해석

Change Buffer의 상태는 INFORMATION_SCHEMA.INNODB_METRICS에서 관련 metric을 확인하는 방식이 일반적이다. metric 이름은 버전과 활성화 상태에 따라 차이가 있을 수 있으므로, 먼저 관련 이름을 검색해 보는 방식이 안전하다.

SELECT NAME, SUBSYSTEM, COUNT, COMMENT
FROM information_schema.INNODB_METRICS
WHERE NAME LIKE 'ibuf%'
ORDER BY NAME
LIMIT 10;

실행 결과(MySQL 8.0.46):

mysql> SELECT NAME, SUBSYSTEM, COUNT, COMMENT
    -> FROM information_schema.INNODB_METRICS
    -> WHERE NAME LIKE 'ibuf%'
    -> ORDER BY NAME
    -> LIMIT 10;

+---------------------------------+---------------+-------+-------------------------------------------------------+
| NAME                            | SUBSYSTEM     | COUNT | COMMENT                                               |
+---------------------------------+---------------+-------+-------------------------------------------------------+
| ibuf_merges                     | change_buffer |     0 | Number of change buffer merges                        |
| ibuf_merges_delete              | change_buffer |     0 | Number of purge records merged by change buffering    |
| ibuf_merges_delete_mark         | change_buffer |     0 | Number of deleted records merged by change buffering  |
| ibuf_merges_discard_delete      | change_buffer |     0 | Number of purge merged operations discarded           |
| ibuf_merges_discard_delete_mark | change_buffer |     0 | Number of deleted merged operations discarded         |
| ibuf_merges_discard_insert      | change_buffer |     0 | Number of insert merged operations discarded          |
| ibuf_merges_insert              | change_buffer |     0 | Number of inserted records merged by change buffering |
| ibuf_size                       | change_buffer |     1 | Change buffer size in pages                           |
+---------------------------------+---------------+-------+-------------------------------------------------------+
8 rows in set (0.01 sec)

이 결과를 해석할 때는 단일 값보다 추세가 중요하다. write burst 동안 insert 관련 counter가 빠르게 증가하고 merge 관련 counter가 따라오지 못하면 backlog가 쌓이고 있을 가능성이 있다. 반대로 읽기 쿼리 지연이 특정 secondary index range에서 튄다면, 해당 page를 읽는 과정에서 change buffer merge가 함께 발생했는지 의심할 수 있다.

운영 진단에서는 다음 자료를 함께 본다.

  • SHOW ENGINE INNODB STATUS의 insert buffer/change buffer 관련 출력
  • INFORMATION_SCHEMA.INNODB_METRICSibuf 계열 metric
  • buffer pool hit ratio와 read I/O 추세
  • 쓰기량, purge lag, checkpoint age, redo log pressure
  • 쿼리 지연이 발생한 시점의 slow log 또는 Performance Schema statement 지표

SHOW ENGINE INNODB STATUS는 운영 상황에 따라 출력이 길고 매번 다르다. 자동 문서 예제로 전체 출력을 고정하기보다는, 실제 점검 시 INSERT BUFFER AND ADAPTIVE HASH INDEX 또는 change buffer 관련 문구를 찾아 해석하는 방식이 적절하다.

7. 장점: random read를 피하고 쓰기 burst를 흡수한다

Change Buffer의 가장 큰 장점은 secondary index page를 즉시 읽지 않아도 된다는 것이다. 특히 다음 조건에서 효과가 크다.

  • non-unique secondary index가 여러 개 있는 테이블에 대량 insert가 발생한다.
  • secondary key 값이 넓은 범위에 분산되어 random page 접근이 많다.
  • buffer pool이 전체 working set을 담기에 부족하다.
  • insert 후 해당 secondary index range를 즉시 조회하지 않는다.
  • 스토리지 random read 비용이 상대적으로 크다.

예를 들어 로그성 테이블에 created_at, tenant_id, status 같은 secondary index가 있고, 여러 tenant의 row가 섞여 들어오면 각 secondary index leaf page가 넓게 분산될 수 있다. 이때 모든 page를 즉시 읽어 수정하면 insert 처리량이 급격히 떨어질 수 있다. Change Buffer는 이 random read를 지연시켜 commit 경로의 I/O 부담을 줄인다.

8. 단점: 비용은 사라지지 않고 뒤로 이동한다

Change Buffer가 줄이는 것은 “총 작업량”이 아니라 “즉시 필요한 random read”다. 지연된 변경은 언젠가 merge되어야 한다. 따라서 다음 부작용이 가능하다.

8.1 읽기 지연의 국소적 증가

쓰기 후 특정 secondary index range를 처음 읽는 쿼리는 page read와 change buffer merge를 함께 수행할 수 있다. 평소에는 빠르던 조회가 특정 시점에 튀는 현상이 생길 수 있다.

8.2 shutdown과 crash recovery 시간 증가

change buffer backlog가 많으면 정상 종료나 장애 복구 과정에서 추가 작업이 필요할 수 있다. 특히 쓰기 burst 직후 장애가 발생하면 redo 적용과 change buffer 관련 병합이 함께 부담이 된다.

8.3 buffer pool 및 checkpoint 압력

Change Buffer 자체도 공간과 관리 비용을 사용한다. merge가 늦어지고 dirty page가 늘면 checkpoint와 flush 정책에 영향을 줄 수 있다. 이는 redo log 압력, flush burst, 응답 시간 변동으로 이어질 수 있다.

8.4 unique index에는 기대한 효과가 없다

unique secondary index는 중복 검사가 필요하므로 Change Buffer의 대표적 수혜 대상이 아니다. 대량 적재 성능을 기대하고 많은 unique secondary index를 유지한다면, Change Buffer보다 schema/index 설계와 적재 순서 조정이 더 중요하다.

9. Aurora MySQL에서의 해석

Aurora MySQL은 InnoDB 호환 SQL 계층을 제공하지만, 스토리지 계층과 redo 처리 방식은 Community MySQL과 다르다. Aurora의 분산 스토리지는 page flush, redo 전파, 복구 모델에서 일반 MySQL과 차이가 있으므로 Change Buffer의 체감 효과와 병목 위치도 다를 수 있다.

운영적으로는 다음 차이를 고려한다.

  • Aurora에서는 storage I/O latency와 commit path가 Community MySQL의 로컬 디스크 모델과 다르게 나타난다.
  • Performance Insights, CloudWatch 지표, wait event를 함께 봐야 한다.
  • writer failover 이후 backlog가 어떤 방식으로 체감되는지는 엔진 버전과 워크로드에 따라 달라진다.
  • parameter group에서 innodb_change_buffering, innodb_change_buffer_max_size 조정 가능 여부와 적용 방식은 Aurora MySQL 버전에 따라 확인해야 한다.

따라서 Aurora에서 Change Buffer를 조정할 때는 “Community MySQL에서 효과가 있었다”는 이유만으로 동일하게 적용하지 않는다. writer 인스턴스의 wait event, write latency, read latency, buffer cache 지표, failover 이후 안정화 시간을 함께 비교해야 한다.

10. 설정 변경 판단 기준

대부분의 시스템에서는 기본값을 먼저 유지하고, 명확한 증거가 있을 때만 조정하는 편이 안전하다. 변경 후보는 다음과 같이 나눌 수 있다.

10.1 Change Buffer를 유지하거나 확대할 수 있는 경우

  • 대량 insert/update가 많고 secondary index page random read가 병목이다.
  • insert 직후 동일 secondary index range를 읽는 패턴이 적다.
  • write burst를 흡수하는 것이 읽기 일관 지연보다 중요하다.
  • ibuf 관련 backlog가 증가해도 merge가 운영 시간 안에 안정적으로 따라온다.

10.2 축소하거나 비활성화를 검토할 수 있는 경우

  • 쓰기 직후 secondary index 조회가 많아 merge 비용이 사용자 쿼리에 전가된다.
  • shutdown, restart, crash recovery 시간이 길어지는 문제가 있다.
  • buffer pool이 충분히 커서 대상 index page가 대부분 memory resident이다.
  • unique secondary index 중심이라 Change Buffer 효과가 제한적이다.
  • 읽기 지연 변동성이 쓰기 처리량보다 더 중요한 서비스다.

설정 변경은 반드시 짧은 관찰이 아니라 workload window 단위로 비교해야 한다. 예를 들어 none으로 바꾸면 write latency가 즉시 나빠질 수 있지만 read latency tail이 안정될 수도 있다. 반대로 all을 유지하면 write throughput은 좋아지지만 merge debt가 쌓일 수 있다.

11. 실무 점검 절차

운영 환경에서 Change Buffer를 점검할 때는 다음 순서를 권장한다.

  1. workload가 secondary index random write 중심인지 확인한다.
  2. 테이블별 secondary index 수, unique index 비율, insert/update/delete 패턴을 확인한다.
  3. innodb_change_buffering, innodb_change_buffer_max_size 현재 값을 기록한다.
  4. INNODB_METRICSibuf 계열 지표를 시간대별로 수집한다.
  5. write burst 직후 읽기 tail latency가 증가하는지 확인한다.
  6. restart, failover, crash recovery 시간과 change buffer backlog의 상관관계를 본다.
  7. 조정이 필요하면 staging 또는 낮은 위험 구간에서 한 번에 하나의 변수만 바꾼다.
  8. 변경 전후의 TPS, p95/p99 latency, read IOPS, write IOPS, redo/checkpoint 지표를 비교한다.

12. 흔한 오해와 주의점

  • Change Buffer는 query cache가 아니다. 읽기 결과를 저장하지 않는다.
  • Change Buffer는 redo log를 대체하지 않는다. durability는 여전히 redo와 doublewrite, flush 정책의 영역이다.
  • Change Buffer는 모든 index 변경에 적용되지 않는다. unique secondary index와 제약 검사가 필요한 경우 효과가 제한된다.
  • Change Buffer가 커졌다는 사실만으로 장애는 아니다. merge가 따라오는지, 사용자 쿼리에 비용이 전가되는지가 더 중요하다.
  • 대량 적재 성능 문제를 Change Buffer만으로 해결하려고 하면 안 된다. 불필요한 secondary index 제거, 적재 후 index 생성, batch 크기 조정, redo/checkpoint 여유, buffer pool 크기 조정이 함께 검토되어야 한다.

13. DBA 체크리스트

  • innodb_change_bufferinginnodb_change_buffer_max_size
  • INNODB_METRICS에서 ibuf

14. 결론

Change Buffer는 InnoDB가 secondary index random I/O를 다루는 중요한 완충 장치다. non-unique secondary index 변경을 즉시 leaf page에 반영하지 않고 나중에 병합함으로써 쓰기 경로의 random read를 줄인다. 그러나 지연된 비용은 사라지지 않는다. 읽기 시점, 백그라운드 merge, shutdown, 복구 과정에서 다시 나타날 수 있다.

따라서 Change Buffer를 운영할 때는 “쓰기 성능 개선 기능”이라는 단일 관점보다 “secondary index 변경 비용의 시간 이동”이라는 관점이 필요하다. 다음 글에서는 이와 연결해 InnoDB의 doublewrite buffer와 page flush가 장애 복구 및 쓰기 안정성에 어떤 역할을 하는지 살펴볼 수 있다.