한 줄 요약: InnoDB의 MVCC는 각 row에 숨겨진
DB_TRX_ID/DB_ROLL_PTR컬럼과 언두 로그로 구성된 버전 체인, 그리고 읽기 시점의 활성 트랜잭션 목록인 ReadView 를 조합해 동작함. Purge 스레드는 더 이상 어떤 ReadView도 필요로 하지 않는 버전을 지워가는데, 롱 트랜잭션이 이 삭제를 막아History list length를 키움.
개요
InnoDB가 REPEATABLE READ / READ COMMITTED 격리 수준을 구현할 수 있는 핵심 메커니즘은 MVCC(Multi-Version Concurrency Control) 임.
MVCC의 구성 요소는 세 가지임.
- 언두 로그(Undo Log) — 변경 이전 데이터(before-image)를 보관하는 저장소
- 버전 체인(Version Chain) — 여러 번 수정된 row의 과거 버전들을 연결하는 링크드 리스트
- ReadView — consistent read 시점의 “활성 트랜잭션 스냅샷”으로, 어느 버전이 현재 트랜잭션에게 보여야 하는지 판단하는 기준
innodb-transaction-acid 에서 rollback 구현을 위해 언두 로그를 개괄한 바 있음 (innodb-transaction-acid 참조). 이 글은 그보다 한 단계 깊이 들어가 구조 · 가시성 알고리즘 · Purge 동작 을 다룸.
결론
핵심 설계 Q&A
| 질문 | 답변 |
|---|---|
| 언두 로그는 어디에 저장되는가 | undo tablespace → rollback segment → undo log segment 계층 구조로 저장됨. MySQL 8.4 기본 초기화 시 undo_001, undo_002 두 default undo tablespace가 생성됨 |
| Insert undo와 Update undo는 왜 다른가 | Insert undo는 commit 직후 삭제 가능. Update undo는 다른 트랜잭션의 consistent read가 필요로 하는 동안 보존해야 함 |
| 버전 체인은 어떻게 연결되는가 | 각 row의 DB_ROLL_PTR 가 이전 버전의 undo log record를 가리킴. 체인을 따라가면 임의 시점의 before-image 재구성 가능 |
| ReadView는 어떻게 가시성을 판단하는가 | trx_id < m_up_limit_id → 항상 보임. trx_id >= m_low_limit_id → 항상 안 보임. 중간 값은 active list 포함 여부로 결정 |
REPEATABLE READ vs READ COMMITTED 차이는 | REPEATABLE READ는 첫 consistent read 시 ReadView를 생성해 트랜잭션 내내 유지. READ COMMITTED는 각 consistent read마다 새로 생성 |
| Purge가 지연되면 어떤 문제가 생기는가 | 아직 purge되지 못한 committed transaction의 undo log가 history list에 남아 History list length 가 증가함. undo tablespace 파일이 커지고 오래된 버전 재구성 경로가 길어질 수 있음 |
운영 시 기억할 것
- 롱 트랜잭션은 Purge를 막는 직접 원인임. Purge는 가장 오래된 active ReadView를 복제한 purge view의
low_limit_no를 넘지 못함.information_schema.INNODB_TRX에서trx_started가 오래된 트랜잭션을 정기적으로 확인해야 함. SHOW ENGINE INNODB STATUS\G의History list length를 모니터링해야 함. 절대값 하나보다 평소 baseline 대비 지속 증가하는지가 중요함. write-heavy workload, 롱 트랜잭션, purge 처리 부족을 함께 확인해야 함.innodb_undo_log_truncate = ON(기본값) 을 유지하는 편이 안전함. undo tablespace가innodb_max_undo_log_size임계치를 넘으면 truncate 대상으로 표시됨. 단innodb_undo_tablespaces는 MySQL 8.4에서 deprecated이며 설정해도 효과가 없으므로, undo tablespace 추가·삭제는 SQL 문으로 관리하는 쪽이 현재 방식임.
상세
1. 언두 로그 구조
InnoDB의 일반 테이블 undo log는 undo tablespace → rollback segment → undo log segment → undo log record 계층으로 배치됨.
innodb_rollback_segments 는 각 undo tablespace와 global temporary tablespace에 할당되는 rollback segment 수를 제어함. MySQL 8.4의 기본값은 128이고 이 값이 최대값이므로, 일반적인 8.4 환경에서는 이 값을 더 키우는 튜닝 여지는 없음.
Insert Undo vs Update Undo
두 타입의 결정적 차이는 보존 기간 임.
| 구분 | 생성 시점 | 삭제 가능 시점 | 이유 |
|---|---|---|---|
| Insert Undo | INSERT 실행 시 | 트랜잭션 commit 직후 | Insert 이전 버전은 존재하지 않으므로 consistent read에 불필요 |
| Update Undo | UPDATE / DELETE 실행 시 | 해당 버전을 필요로 하는 ReadView가 모두 사라진 뒤 | 다른 트랜잭션의 consistent read가 이전 버전을 요구할 수 있음 |
Insert undo는 롤백이 발생하지 않는 한 commit 즉시 해제됨. Update undo는 Purge 스레드가 “이제 아무도 이 버전을 보지 않는다”고 판단할 때까지 남아 있음.
2. 버전 체인 (Version Chain)
InnoDB는 각 clustered index row에 세 개의 숨겨진 컬럼을 추가함.
| 컬럼 | 크기 | 역할 |
|---|---|---|
DB_TRX_ID | 6 bytes | 이 row를 마지막으로 수정한 트랜잭션 ID |
DB_ROLL_PTR | 7 bytes | 이전 버전의 undo log record를 가리키는 포인터 |
DB_ROW_ID | 6 bytes | InnoDB가 자동 생성한 clustered index가 없을 때만 내부 row ID로 사용 |
DB_ROLL_PTR 가 버전 체인의 핵심임. row가 수정될 때마다 이전 버전이 undo log에 기록되고, DB_ROLL_PTR 는 그 레코드를 가리킴. 체인을 따라가면 임의 시점의 row 상태를 재구성할 수 있음.
아래는 id=1 row가 세 번 수정된 뒤 형성되는 버전 체인 예시임.
ReadView를 보유한 트랜잭션이 id=1을 읽으려 하면, 현재 row의 DB_TRX_ID 가 보이지 않는 버전이면 DB_ROLL_PTR 을 따라 체인을 내려가며 보이는 첫 번째 버전 을 반환함.
3. ReadView와 가시성 판단
ReadView는 consistent read(locking read가 아닌 일반 SELECT) 실행 시점에 생성되는 활성 트랜잭션 ID 목록 스냅샷 임. 세 가지 값을 갖고 있음.
| 필드 | 의미 |
|---|---|
m_low_limit_id (high water mark) | ReadView가 보지 않아야 하는 트랜잭션 ID의 하한. trx_id >= m_low_limit_id 이면 ReadView 생성 뒤 시작된 미래 트랜잭션으로 판단함 |
m_up_limit_id (low water mark) | ReadView가 항상 볼 수 있는 트랜잭션 ID의 상한. trx_id < m_up_limit_id 이면 이미 안전하게 보이는 버전으로 판단함 |
m_ids (active list) | ReadView 생성 시점에 아직 commit되지 않은 read-write 트랜잭션 ID 목록 |
가시성 판단 알고리즘은 다음과 같음.
자기 트랜잭션이 만든 변경(T == creator_trx_id)은 예외적으로 항상 보임.
REPEATABLE READ vs READ COMMITTED
두 격리 수준의 차이는 ReadView를 언제 생성하는가 임.
- REPEATABLE READ: 트랜잭션 내 첫 consistent read 시 ReadView를 한 번 생성하고, 이후 같은 트랜잭션에서 재사용함. 트랜잭션 내내 동일한 스냅샷으로 읽기 때문에 다른 트랜잭션이 중간에 commit해도 그 변경이 보이지 않음.
- READ COMMITTED: consistent read를 실행할 때마다 새 ReadView를 생성함. 따라서 각
SELECT실행 시점 직전에 commit된 데이터가 보임.
4. Purge 스레드
Update undo log는 “더 이상 어떤 ReadView도 이 버전을 필요로 하지 않는다”고 판단될 때 삭제됨. 이 작업을 담당하는 것이 Purge 스레드 임.
innodb_purge_threads 는 병렬 Purge 스레드 수의 상한을 제어함. MySQL 8.4 문서 기준 기본값은 사용 가능한 logical processor 수가 16 이하이면 1, 그보다 많으면 4임. 최대값은 32이고, 실제 사용 스레드 수는 purge system이 자동 조정함.
롱 트랜잭션이 Purge를 막는 이유
Purge 스레드는 삭제 대상을 결정할 때 시스템 전체에서 가장 오래된 active ReadView를 복제한 purge view 를 기준으로 삼음. MySQL 8.4.8 소스에서는 이 경계가 ReadView::low_limit_no() 로 표현되고, purge iterator의 trx_no 가 이 값 이상이면 더 진행하지 않음.
여기서 주의할 점은 ReadView 가시성 판단의 m_low_limit_id 와 purge 경계인 m_low_limit_no 가 서로 다른 필드라는 점임. m_low_limit_id 는 row version의 DB_TRX_ID 가 보이는지 판단하는 high water mark이고, m_low_limit_no 는 undo log record를 purge해도 되는지 판단하는 transaction number 경계임.
오래된 ReadView를 보유한 롱 트랜잭션이 하나라도 살아 있으면, 그 트랜잭션이 commit되거나 rollback될 때까지 purge view의 low_limit_no 가 앞으로 이동하지 않음. 결과적으로 그 경계 이후에 commit된 여러 트랜잭션의 update undo가 history list에 남게 됨.
이 현상은 SHOW ENGINE INNODB STATUS\G 의 History list length 로 관찰 가능함. 값이 계속 커질수록 오래된 버전 재구성 경로가 길어질 수 있고 undo tablespace 파일도 커질 수 있음.
5. Undo Tablespace
MySQL 8.4에서 일반 테이블의 undo log는 별도의 undo tablespace 파일 에 저장됨.
기본 초기화 시 undo_001, undo_002 두 default undo tablespace 파일이 생성됨. innodb_undo_tablespaces 의 기본값도 2로 표시되지만, MySQL 8.4에서는 deprecated이며 설정해도 효과가 없음. 추가 undo tablespace는 CREATE UNDO TABLESPACE 로 런타임에 만들 수 있음.
innodb_undo_log_truncate (기본값: ON): undo tablespace 파일이 innodb_max_undo_log_size 임계치(기본 1,073,741,824 bytes = 1024 MiB)를 초과하면 truncate 대상으로 표시함. 자동 truncation에는 최소 두 개의 active undo tablespace가 필요함.
테스트
MySQL 8.4.8 에서 직접 확인
1. 기본 설정값 확인
SELECT VERSION() AS version;
SELECT @@innodb_rollback_segments AS rollback_segments,
@@innodb_purge_threads AS purge_threads,
@@innodb_undo_tablespaces AS undo_tablespaces,
@@innodb_undo_log_truncate AS undo_log_truncate,
@@innodb_max_undo_log_size AS max_undo_log_size;version
8.4.8
rollback_segments purge_threads undo_tablespaces undo_log_truncate max_undo_log_size
128 1 2 1 10737418242. Undo Tablespace 파일 확인
docker exec blog-test-mysql-8.4-undo ls -la /var/lib/mysql/ | grep -E 'undo|ibdata'-rw-r----- 1 mysql mysql 12582912 May 29 13:28 ibdata1
-rw-r----- 1 mysql mysql 16777216 May 29 13:28 undo_001
-rw-r----- 1 mysql mysql 16777216 May 29 13:28 undo_002ibdata1 과 분리된 undo_001, undo_002 파일이 각 16MB로 초기화되어 있음.
3. SHOW ENGINE INNODB STATUS — History list length 확인
초기 상태(부하 없음):
docker exec blog-test-mysql-8.4-undo mysql -uroot -p... -e "SHOW ENGINE INNODB STATUS\G"TRANSACTIONS
------------
Trx id counter 1816
Purge done for trx's n:o < 1816 undo n:o < 0 state: running but idle
History list length 74. 롱 트랜잭션이 History list를 키우는 것 확인
테스트 절차:
테스트 테이블은 아래 상태에서 시작함.
CREATE TABLE t (id INT PRIMARY KEY, val INT) ENGINE=InnoDB;
INSERT INTO t VALUES (1,100),(2,200);
SELECT * FROM t;id val
1 100
2 200Connection A에서 트랜잭션을 열고 닫지 않음 (rollback / commit 없이 유지):
-- Connection A
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM t;id val
1 100
2 200이 세션은 COMMIT / ROLLBACK 없이 열린 상태로 유지함.
Connection B에서 여러 UPDATE 를 commit함:
-- Connection B (각각 autocommit으로 commit됨)
UPDATE t SET val = val + 1 WHERE id = 1;
-- 같은 UPDATE를 30회 반복UPDATE 자체는 history list 관찰을 위한 부하 생성 단계라서 개별 row-count 출력은 생략함.
Connection A가 열려 있는 동안 INFORMATION_SCHEMA.INNODB_TRX 에서 열린 트랜잭션을 확인함.
SELECT trx_state, trx_started, trx_isolation_level
FROM information_schema.INNODB_TRX\G*************************** 1. row ***************************
trx_state: RUNNING
trx_started: 2026-05-29 13:28:45
trx_isolation_level: REPEATABLE READ같은 시점에 SHOW ENGINE INNODB STATUS\G 를 확인함.
TRANSACTIONS
------------
Trx id counter 1876
Purge done for trx's n:o < 1817 undo n:o < 0 state: running but idle
History list length 37Connection B의 update undo가 열린 ReadView 때문에 바로 삭제되지 못해 History list length 가 7 → 37로 증가함.
Connection A를 닫으면 해당 ReadView가 사라져 밀린 undo log가 purge 대상이 됨. 실제 History list length 감소가 표시되는 시점은 purge batch 실행 타이밍에 따라 지연될 수 있음.
참고
- MySQL 8.4 — Undo Logs (17.6.6) — undo log / rollback segment 구조, undo slot 용량 공식
- MySQL 8.4 — InnoDB Multi-Versioning (17.3) — 숨겨진 컬럼(
DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID), insert / update undo 구분 - MySQL 8.4 — Consistent Nonlocking Reads (17.7.2.3) —
REPEATABLE READ/READ COMMITTED의 consistent read snapshot 생성 시점 - MySQL 8.4 — Purge Configuration (17.8.9) — purge thread 기본값,
History list length, purge lag 설명 - MySQL 8.4 — Undo Tablespaces (17.6.3.4) — default undo tablespace, truncation, rollback segment 수
- MySQL 8.4 — InnoDB Parameters —
innodb_undo_tablespacesdeprecation,innodb_undo_log_truncate,innodb_max_undo_log_size기본값 - MySQL 8.4.8 source code (
mysql-servertagmysql-8.4.8)storage/innobase/include/read0types.h—ReadView::changes_visible()가시성 판단storage/innobase/include/read0types.h—m_low_limit_id,m_up_limit_id주석storage/innobase/read/read0read.cc—ReadView::prepare()에서 ReadView 경계값 설정storage/innobase/include/read0types.h— purge 판단에 쓰이는m_low_limit_no주석storage/innobase/read/read0read.cc— Purge가 가장 오래된 ReadView를 clone하는 경로storage/innobase/trx/trx0purge.cc—purge_sys->iter.trx_no >= purge_sys->view.low_limit_no()이면 purge 진행 중단storage/innobase/read/read0read.cc— purge가 활성 ReadView에 보이는 delete-marked row를 제거하지 않는 조건