Undo Log 개요: rollback, consistent read, MVCC의 기반
MySQL InnoDB Undo Log가 rollback, consistent read, MVCC, purge와 운영 안정성에 미치는 영향을 정리한다.
1. 왜 Undo Log를 운영 관점에서 이해해야 하는가
InnoDB에서 Undo Log는 트랜잭션을 되돌리기 위한 보조 기록으로만 설명되기 쉽다. 그러나 실제 운영에서 Undo Log는 단순한 rollback 자료가 아니다. ROLLBACK을 수행할 때 변경 전 값을 복원하고, 다른 세션이 과거 시점의 일관된 데이터를 읽을 수 있게 하며, MVCC의 read view와 결합해 consistent read를 가능하게 하는 핵심 구조다. 또한 오래 열린 트랜잭션이 Undo Log 정리를 막으면 purge 지연, 테이블스페이스 증가, 복구 시간 증가, 복제 지연, DDL 대기 같은 문제가 연쇄적으로 발생할 수 있다.
운영자는 Undo Log를 “평소에는 보이지 않지만 문제가 생기면 데이터베이스 전체를 느리게 만드는 내부 부채”로 이해해야 한다. 쓰기 트랜잭션이 많을수록 변경 전 버전이 누적되고, 오래된 읽기 트랜잭션이 남아 있으면 InnoDB는 그 버전을 바로 폐기할 수 없다. 이때 모니터링 지표에는 history list length 증가, purge thread 활동 증가, undo tablespace 사용량 증가, 긴 트랜잭션, information_schema.INNODB_TRX의 오래된 시작 시간이 나타난다.
이번 글은 Undo Log의 내부 역할을 rollback, consistent read, MVCC, purge의 흐름으로 연결해 설명한다. 이어서 MySQL 8.0 이상에서 확인할 수 있는 진단 SQL과 운영 체크리스트를 제시한다. Aurora MySQL에서는 스토리지 계층과 백업/복구 구조가 Community MySQL과 다르지만, InnoDB 트랜잭션 버전 관리와 긴 트랜잭션이 purge를 지연시킨다는 기본 원리는 동일하게 중요하다.
2. Undo Log의 핵심 역할: 변경 전 버전을 보존한다
InnoDB는 행을 변경할 때 새 값만 기록하지 않는다. 변경 전 값을 Undo Log에 남기고, 실제 clustered index record에는 트랜잭션 ID와 Undo Log를 따라갈 수 있는 포인터를 유지한다. 이 구조 덕분에 InnoDB는 다음 세 가지 요구를 동시에 만족한다.
- 트랜잭션이 실패하거나 사용자가
ROLLBACK을 요청하면 변경 전 상태로 되돌린다. - 다른 트랜잭션이 과거 read view 기준으로 데이터를 읽어야 할 때 이전 버전을 찾아 보여준다.
- 더 이상 어떤 read view에서도 필요하지 않은 이전 버전을 purge가 안전하게 제거한다.
아래 그림은 Undo Log가 쓰기, 읽기, purge 사이에서 어떤 위치를 차지하는지 단순화한 것이다.
flowchart TD
A[UPDATE/DELETE/INSERT 실행] --> B[현재 행 버전 변경]
A --> C[Undo Log에 이전 버전 기록]
B --> D[행의 DB_TRX_ID 갱신]
B --> E[Undo 포인터 유지]
F[Consistent Read] --> G{현재 행이 read view보다 최신인가?}
G -- 아니오 --> H[현재 버전 반환]
G -- 예 --> I[Undo Log 체인을 따라 이전 버전 탐색]
I --> J[read view에 맞는 버전 반환]
K[Purge Thread] --> L{오래된 read view가 참조 중인가?}
L -- 예 --> M[Undo 보존]
L -- 아니오 --> N[불필요한 Undo/이전 버전 정리]
Undo Log는 redo log와 목적이 다르다. Redo Log는 장애 후 이미 커밋된 변경을 다시 적용하기 위한 물리적/논리적 재실행 기록에 가깝고, Undo Log는 트랜잭션 이전 상태 또는 과거 읽기 버전을 구성하기 위한 반대 방향 기록이다. 두 로그는 서로 대체 관계가 아니다. InnoDB는 변경을 수행하면서 Undo Log 자체의 변경도 redo로 보호한다. 따라서 장애 복구 후에도 커밋되지 않은 트랜잭션을 rollback하거나 필요한 내부 정리를 계속할 수 있다.
3. Rollback에서 Undo Log가 동작하는 방식
ROLLBACK은 단순히 “마지막 SQL을 취소한다”는 개념이 아니다. InnoDB는 트랜잭션이 남긴 Undo Log record를 역순으로 적용하여 변경을 되돌린다. 예를 들어 한 트랜잭션에서 여러 행을 UPDATE했다면, 각 행의 이전 값 또는 삭제/삽입 반대 동작을 Undo Log를 통해 재구성한다.
다음 예제는 단일 세션에서 Undo Log가 rollback의 기반으로 사용되는 상황을 확인하기 위한 최소 재현이다. 실제 내부 Undo Log record를 직접 조회하는 예제는 아니지만, InnoDB가 트랜잭션 중 변경한 값을 rollback 후 원래 상태로 되돌리는 동작을 보여준다.
DROP TABLE IF EXISTS undo_note_accounts;
CREATE TABLE undo_note_accounts (
account_id INT PRIMARY KEY,
balance INT NOT NULL
) ENGINE=InnoDB;
INSERT INTO undo_note_accounts VALUES (1, 1000), (2, 1000);
START TRANSACTION;
UPDATE undo_note_accounts
SET balance = balance - 300
WHERE account_id = 1;
UPDATE undo_note_accounts
SET balance = balance + 300
WHERE account_id = 2;
SELECT account_id, balance
FROM undo_note_accounts
ORDER BY account_id;
ROLLBACK;
SELECT account_id, balance
FROM undo_note_accounts
ORDER BY account_id;
DROP TABLE undo_note_accounts;
실행 결과(MySQL 8.0.46):
mysql> DROP TABLE IF EXISTS undo_note_accounts;
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE TABLE undo_note_accounts (
-> account_id INT PRIMARY KEY,
-> balance INT NOT NULL
-> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO undo_note_accounts VALUES (1, 1000), (2, 1000);
Query OK, 2 rows affected (0.01 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE undo_note_accounts
-> SET balance = balance - 300
-> WHERE account_id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> UPDATE undo_note_accounts
-> SET balance = balance + 300
-> WHERE account_id = 2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> SELECT account_id, balance
-> FROM undo_note_accounts
-> ORDER BY account_id;
+------------+---------+
| account_id | balance |
+------------+---------+
| 1 | 700 |
| 2 | 1300 |
+------------+---------+
2 rows in set (0.00 sec)
mysql> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT account_id, balance
-> FROM undo_note_accounts
-> ORDER BY account_id;
+------------+---------+
| account_id | balance |
+------------+---------+
| 1 | 1000 |
| 2 | 1000 |
+------------+---------+
2 rows in set (0.00 sec)
mysql> DROP TABLE undo_note_accounts;
Query OK, 0 rows affected (0.00 sec)
이 예제에서 첫 번째 SELECT는 트랜잭션 내부에서 변경된 값을 보여준다. 그러나 ROLLBACK 이후 두 번째 SELECT는 원래 값으로 돌아간 결과를 보여준다. 운영 환경에서 rollback 비용은 변경량에 비례한다. 대량 UPDATE나 DELETE를 실행한 뒤 뒤늦게 rollback하면, 취소 작업 자체가 긴 시간 동안 I/O와 CPU를 사용하며 다른 작업과 경합할 수 있다. “아직 commit하지 않았으니 안전하다”가 아니라 “commit하지 않은 큰 변경도 이미 Undo Log와 buffer pool, lock, redo에 부담을 만들었다”고 보는 편이 정확하다.
4. Consistent Read와 MVCC: 읽기는 왜 잠금 없이 과거를 볼 수 있는가
InnoDB의 기본 격리 수준인 REPEATABLE READ에서 일반 SELECT는 locking read가 아니라 consistent read다. 트랜잭션이 처음 읽기를 시작할 때 read view를 만들고, 이후 같은 트랜잭션의 일반 SELECT는 그 read view에 보이는 버전을 반환한다. 다른 세션이 이미 commit한 최신 값이 있더라도, read view 기준으로 너무 최신이면 Undo Log 체인을 따라 이전 버전을 찾아 보여준다.
이 방식이 MVCC다. Multi-Version Concurrency Control이라는 이름처럼 하나의 논리적 행에 여러 시점의 버전이 존재할 수 있고, 각 읽기 트랜잭션은 자신의 read view에 맞는 버전을 선택한다. 덕분에 일반 읽기와 쓰기가 매번 서로를 막지 않는다. OLTP 시스템에서 읽기 처리량이 높은 이유 중 하나가 이 구조다.
그러나 MVCC는 공짜가 아니다. 오래 열린 트랜잭션이 과거 read view를 계속 유지하면, InnoDB는 그 read view가 참조할 수 있는 이전 버전을 지울 수 없다. 이때 purge가 따라가지 못하고 history list가 증가한다. 특히 다음과 같은 작업은 Undo Log 누적을 악화시키기 쉽다.
- 애플리케이션 커넥션이 트랜잭션을 시작한 뒤 commit/rollback하지 않고 오래 유지하는 경우
- 대용량 배치가 긴 트랜잭션 하나로 많은 행을 변경하는 경우
- 백업, 리포팅, ETL, 관리 콘솔 조회가 repeatable read 트랜잭션을 장시간 유지하는 경우
- 복제 지연 또는 읽기 복제본에서 오래된 스냅샷 읽기가 누적되는 경우
READ COMMITTED에서는 statement마다 read view가 새로 만들어지므로 일부 워크로드에서 오래된 버전 보존 압박이 줄어들 수 있다. 그러나 격리 수준 변경은 phantom, 재조회 결과 변화, 애플리케이션 동시성 가정에 영향을 주므로 Undo Log 문제를 해결하기 위한 단순 처방으로 적용해서는 안 된다.
5. Undo Log와 purge: 내부 부채를 언제 갚는가
Undo Log는 rollback과 consistent read에 필요하지만, 영원히 보존되어서는 안 된다. 더 이상 어떤 활성 read view에서도 참조하지 않는 이전 버전은 purge thread가 정리한다. purge는 삭제 표시된 레코드, 오래된 update version, 관련 undo record를 정리하면서 인덱스와 테이블스페이스의 내부 부채를 줄인다.
문제는 purge가 항상 쓰기 속도를 따라잡는다는 보장이 없다는 점이다. purge가 지연되면 다음 현상이 나타날 수 있다.
history list length가 계속 증가한다.- Undo tablespace 또는 undo 관련 디스크 사용량이 늘어난다.
- 오래된 행 버전을 따라가야 하는 consistent read 비용이 증가한다.
- purge thread와 사용자 트랜잭션이 I/O, CPU, buffer pool을 두고 경합한다.
- 대량 삭제 후에도 공간이 즉시 반환되지 않아 운영자가 “삭제했는데 왜 디스크가 줄지 않는가”라는 상황을 만난다.
MySQL 8.0에서는 undo tablespace 관리가 5.7 시대와 달라졌다. 시스템 테이블스페이스에만 의존하던 오래된 구성과 달리 별도 undo tablespace와 truncate 메커니즘이 사용된다. 다만 undo tablespace truncate가 가능하더라도, 활성 트랜잭션이 오래된 버전을 붙잡고 있으면 정리는 지연된다. 설정 변수만 조정한다고 해결되는 문제가 아니라, 긴 트랜잭션과 쓰기 패턴을 함께 관리해야 한다.
6. MySQL 8.0 이상에서 확인할 기본 진단 SQL
먼저 현재 서버의 버전과 Undo 관련 주요 변수를 확인한다. MySQL minor version과 배포판에 따라 표시되는 변수는 다를 수 있으므로, 운영 점검 스크립트는 특정 변수 하나만 단정하지 말고 대상 버전에서 실제 존재 여부를 확인해야 한다.
SELECT VERSION() AS mysql_version;
SHOW VARIABLES WHERE Variable_name IN (
'transaction_isolation',
'innodb_undo_log_truncate',
'innodb_max_undo_log_size',
'innodb_purge_threads',
'innodb_purge_batch_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 WHERE Variable_name IN (
-> 'transaction_isolation',
-> 'innodb_undo_log_truncate',
-> 'innodb_max_undo_log_size',
-> 'innodb_purge_threads',
-> 'innodb_purge_batch_size'
-> );
+--------------------------+-----------------+
| Variable_name | Value |
+--------------------------+-----------------+
| innodb_max_undo_log_size | 1073741824 |
| innodb_purge_batch_size | 300 |
| innodb_purge_threads | 4 |
| innodb_undo_log_truncate | ON |
| transaction_isolation | REPEATABLE-READ |
+--------------------------+-----------------+
5 rows in set (0.00 sec)
다음 쿼리는 현재 열린 InnoDB 트랜잭션을 확인한다. 오래 실행 중인 트랜잭션은 Undo Log와 purge 지연의 출발점일 수 있다.
SELECT trx_id,
trx_state,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_age_seconds,
trx_rows_locked,
trx_rows_modified
FROM information_schema.INNODB_TRX
ORDER BY trx_started;
실행 결과(MySQL 8.0.46):
mysql> SELECT trx_id,
-> trx_state,
-> trx_started,
-> TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_age_seconds,
-> trx_rows_locked,
-> trx_rows_modified
-> FROM information_schema.INNODB_TRX
-> ORDER BY trx_started;
Empty set (0.00 sec)
테스트 컨테이너처럼 활성 트랜잭션이 없는 환경에서는 빈 결과가 정상이다. 운영 환경에서는 trx_age_seconds가 비정상적으로 큰 세션, 많은 trx_rows_modified를 가진 트랜잭션, 애플리케이션 요청 수명보다 오래 살아 있는 트랜잭션을 우선 조사한다.
Undo tablespace와 관련 파일은 서버 구성에 따라 직접 파일시스템에서 확인할 수도 있다. 다만 컨테이너, RDS, Aurora처럼 파일시스템 접근이 제한된 환경에서는 SQL 변수, CloudWatch/Performance Insights, 스토리지 사용량 지표를 함께 본다. 아래 SQL은 MySQL 내부에서 undo tablespace로 식별되는 공간 정보를 확인하는 예시다.
SELECT SPACE,
NAME,
ROW_FORMAT,
SPACE_TYPE,
FILE_SIZE,
ALLOCATED_SIZE,
STATE
FROM information_schema.INNODB_TABLESPACES
WHERE SPACE_TYPE = 'Undo'
ORDER BY SPACE;
실행 결과(MySQL 8.0.46):
mysql> SELECT SPACE,
-> NAME,
-> ROW_FORMAT,
-> SPACE_TYPE,
-> FILE_SIZE,
-> ALLOCATED_SIZE,
-> STATE
-> FROM information_schema.INNODB_TABLESPACES
-> WHERE SPACE_TYPE = 'Undo'
-> ORDER BY SPACE;
+------------+-----------------+------------+------------+-----------+----------------+--------+
| SPACE | NAME | ROW_FORMAT | SPACE_TYPE | FILE_SIZE | ALLOCATED_SIZE | STATE |
+------------+-----------------+------------+------------+-----------+----------------+--------+
| 4294967278 | innodb_undo_002 | Undo | Undo | 16777216 | 16777216 | active |
| 4294967279 | innodb_undo_001 | Undo | Undo | 16777216 | 16777216 | active |
+------------+-----------------+------------+------------+-----------+----------------+--------+
2 rows in set (0.00 sec)
일부 MySQL 8.0 환경에서는 INNODB_TABLESPACES의 컬럼 구성이나 undo tablespace 표시 방식이 다를 수 있다. 이 경우에는 SHOW CREATE TABLE information_schema.INNODB_TABLESPACES 또는 DESCRIBE information_schema.INNODB_TABLESPACES로 컬럼을 확인한 뒤 운영 표준 쿼리를 조정한다.
7. Aurora MySQL에서의 해석 차이
Aurora MySQL은 InnoDB 호환 SQL 엔진을 제공하지만, 저장소 계층은 Community MySQL의 로컬 데이터 파일 구성과 다르다. 자동 확장 스토리지, 분산 로그 구조, 빠른 복구 특성 때문에 운영자가 직접 undo 파일을 관리하는 방식은 제한적이다. 그렇다고 Undo Log와 MVCC 문제가 사라지는 것은 아니다.
Aurora에서도 오래 열린 트랜잭션은 purge 지연과 스토리지 증가를 만들 수 있다. writer 인스턴스에서 대량 변경이 발생하고 reader 인스턴스에서 오래된 스냅샷 쿼리가 유지되면, 엔진은 일관된 읽기를 제공하기 위해 이전 버전을 보존해야 한다. 이 현상은 Aurora의 history list length, Performance Insights의 대기 이벤트, 스토리지 증가, reader 지연, 장시간 실행 쿼리로 관찰될 수 있다.
Aurora 운영에서는 다음 관점이 특히 중요하다.
- 파일 단위 undo tablespace 축소보다 긴 트랜잭션 제거와 purge 진행 여부가 우선이다.
- reader에서 실행되는 리포팅 쿼리도 writer의 버전 정리에 영향을 줄 수 있는지 확인한다.
- 스토리지가 자동 확장된 뒤 즉시 줄어들지 않는 특성을 고려해 대량 변경 작업 전후 비용을 산정한다.
- Performance Insights에서 긴 트랜잭션, 대기 이벤트, SQL별 실행 시간을 함께 본다.
즉 Aurora에서는 “파일을 줄이는 방법”보다 “이전 버전이 계속 필요해지는 원인을 줄이는 방법”이 더 중요한 운영 질문이 된다.
8. 장애와 성능 문제로 이어지는 흔한 오해
Undo Log 관련 장애는 대개 특정 SQL 하나보다 운영 습관에서 시작된다. 다음 오해는 특히 자주 반복된다.
오해 1: 읽기 트랜잭션은 쓰지 않으므로 안전하다
읽기만 수행하는 트랜잭션도 오래 열려 있으면 purge가 이전 버전을 제거하지 못하게 만들 수 있다. START TRANSACTION 후 여러 조회를 수행하고 커넥션 풀에 반환하지 않는 코드, 관리 도구에서 autocommit을 끈 상태로 세션을 방치하는 습관은 모두 위험하다.
오해 2: 대량 변경은 commit 전에 취소하면 비용이 없다
대량 변경은 commit 전에도 이미 Undo Log, Redo Log, lock, buffer pool 변경, secondary index 작업을 발생시킨다. rollback은 무료 작업이 아니며, 때로는 commit보다 더 긴 시간이 걸릴 수 있다. 대량 변경은 작은 배치로 나누고, 각 배치의 영향 범위와 복구 방법을 사전에 설계해야 한다.
오해 3: purge thread 수를 늘리면 항상 해결된다
innodb_purge_threads는 purge 병렬성에 영향을 줄 수 있지만, 오래 열린 read view가 남아 있으면 purge는 안전하게 지울 수 없다. 또한 purge thread를 늘리면 I/O와 CPU 경합이 커질 수 있다. 원인 트랜잭션 제거, 배치 크기 조정, 인덱스 설계, 쓰기 피크 분산이 함께 필요하다.
오해 4: 디스크 공간이 늘었으니 테이블 최적화만 하면 된다
대량 삭제 후 공간이 바로 줄지 않는다고 해서 곧바로 OPTIMIZE TABLE을 실행하면 더 큰 I/O와 잠금 부담을 만들 수 있다. 먼저 purge가 지연되는지, 긴 트랜잭션이 있는지, undo tablespace truncate 조건이 충족되는지 확인해야 한다. 공간 회수 작업은 서비스 부하, 복제, 백업, DDL 알고리즘을 함께 고려해야 한다.
9. 운영 체크리스트
Undo Log 문제를 예방하려면 설정값 하나보다 트랜잭션 수명 관리가 중요하다. 다음 항목을 정기 점검 기준으로 삼을 수 있다.
- 관리 도구, 배치, 리포팅 작업이
autocommit=0 - 대량
UPDATE,DELETE,INSERT ... SELECT -
information_schema.INNODB_TRX -
history list length
긴 트랜잭션을 발견했을 때는 무조건 KILL부터 실행하지 않는다. 해당 세션이 결제, 정산, 데이터 이관처럼 업무적으로 중요한 작업일 수 있기 때문이다. 먼저 SQL, 사용자, 호스트, 실행 시간, 변경 행 수, 애플리케이션 요청 ID를 확인하고, 정상 종료 가능 여부를 판단한다. 다만 purge 지연이 서비스 전체를 위협하는 상황이라면 영향 범위를 승인받고 세션 종료를 실행해야 한다.
10. 결론: Undo Log는 MVCC의 비용 장부다
Undo Log는 rollback을 위한 안전장치이자 consistent read를 위한 과거 버전 저장소이며, MVCC가 높은 동시성을 제공하기 위해 지불하는 비용 장부다. 정상 상태에서는 사용자가 직접 볼 일이 적지만, 긴 트랜잭션과 대량 변경이 겹치면 purge 지연과 스토리지 증가, 읽기 비용 증가, rollback 장기화로 운영 리스크가 빠르게 커진다.
따라서 MySQL 운영자는 Undo Log를 단순 내부 구현이 아니라 트랜잭션 설계의 결과로 보아야 한다. 짧은 트랜잭션, 작은 배치, 명확한 commit/rollback, 긴 조회의 통제, purge 지표 모니터링이 Undo Log 운영의 핵심이다. 다음 글들에서 MVCC read view, purge thread, 격리 수준, 잠금 대기 진단을 더 세밀하게 다루면 InnoDB의 동시성 모델을 한층 입체적으로 이해할 수 있다.