Metadata Lock: ALTER, DDL, 긴 SELECT가 서로 막히는 원리
MySQL Metadata Lock의 획득과 대기 원리를 이해하고 긴 트랜잭션과 DDL 차단을 진단하고 예방하는 방법을 정리한다.
1. 왜 ALTER TABLE이 갑자기 멈추는가
MySQL에서 ALTER TABLE이 오래 걸린다고 해서 항상 테이블 복사나 인덱스 생성이 느린 것은 아니다. 실제 작업을 시작하기도 전에 Metadata Lock(MDL) 을 얻지 못해 대기하는 경우가 많다. 반대로 DDL이 대기열에 들어온 뒤 새로 실행한 짧은 SELECT까지 줄줄이 멈출 수 있다. 이때 CPU 사용률과 디스크 처리량은 낮고, InnoDB row lock 조회에도 원인이 나타나지 않을 수 있다.
MDL은 테이블 이름, 정의, trigger, stored program 같은 데이터베이스 객체의 메타데이터가 사용 중에 바뀌지 않도록 보호한다. 실행 중인 문장이 참조하는 테이블을 다른 세션이 제거하거나, 트랜잭션이 같은 테이블을 계속 사용해야 하는 동안 정의가 바뀌면 SQL의 의미와 실행 계획을 안전하게 유지할 수 없기 때문이다.
운영에서 특히 중요한 사실은 다음 세 가지다.
- 평범한
SELECT도 MDL을 획득한다. - 명시적
BEGIN뿐 아니라autocommit=0에서 끝나지 않은 트랜잭션도 MDL을 오래 보유할 수 있다. ALGORITHM=INSTANT또는LOCK=NONE인 DDL도 MDL이 전혀 필요 없는 것은 아니다.
따라서 MDL 장애는 “DDL 문제”가 아니라 세션 수명, 트랜잭션 경계, 배포 절차가 만나는 운영 문제로 보아야 한다.
2. MDL이 보호하는 범위
InnoDB row lock과 MDL은 보호 대상이 다르다.
| 구분 | 보호 대상 | 대표 대기 원인 | 주요 관측 위치 |
|---|---|---|---|
| Metadata Lock | 테이블·스키마·프로그램 객체의 정의와 이름 | 긴 트랜잭션 뒤의 DDL, DDL 뒤의 후속 쿼리 | performance_schema.metadata_locks |
| InnoDB record lock | 인덱스 record와 gap | 같은 행의 UPDATE, 범위 잠금, 외래 키 검사 |
performance_schema.data_locks, data_lock_waits |
| Table lock | storage engine 또는 SQL 계층의 테이블 접근 | LOCK TABLES, 일부 엔진 작업 |
performance_schema.table_handles 등 |
SELECT * FROM orders WHERE id = 10은 행을 변경하지 않더라도 orders의 정의가 실행 중에 사라지거나 바뀌지 않도록 보통 공유 성격의 MDL을 얻는다. ALTER TABLE orders ...는 정의 변경을 위해 더 강한 배타 성격의 MDL이 필요하다. 기존 공유 lock과 배타 lock이 양립하지 않으면 DDL은 기다린다.
MDL의 이름과 조합은 SQL 종류와 실행 단계에 따라 SHARED_READ, SHARED_WRITE, SHARED_UPGRADABLE, EXCLUSIVE 등으로 나타날 수 있다. 운영자가 모든 조합을 암기하기보다는 다음 세 열을 우선 해석하는 편이 실용적이다.
LOCK_TYPE: 요청하거나 보유한 lock의 성격LOCK_DURATION:STATEMENT,TRANSACTION,EXPLICIT중 수명 범위LOCK_STATUS: 현재 획득한GRANTED인지 기다리는PENDING인지
2.1 문장 수명과 트랜잭션 수명
MDL은 항상 SQL 문장이 끝날 때 해제되는 것이 아니다. 트랜잭션의 일관성과 객체 정의 안정성을 위해 트랜잭션 종료까지 유지되는 lock이 있다. 다음과 같은 애플리케이션 패턴이 위험하다.
세션 A
SET autocommit = 0;
SELECT * FROM orders WHERE order_id = 1001;
-- 애플리케이션이 외부 API 응답을 기다림
-- COMMIT 또는 ROLLBACK이 아직 실행되지 않음
세션 B
ALTER TABLE orders ADD COLUMN source_code varchar(20);
-- 세션 A의 트랜잭션이 끝날 때까지 MDL 대기 가능
세션 A의 SELECT가 이미 끝났더라도 트랜잭션이 열려 있으면 세션 B가 기다릴 수 있다. “현재 실행 중인 긴 SQL이 없다”는 사실만으로 MDL blocker가 없다고 판단하면 안 되는 이유다.
3. 대기열이 만드는 연쇄 차단
MDL 장애가 어려운 이유는 최초 blocker 하나보다 대기열 뒤에 쌓이는 후속 요청의 영향이 더 크게 보이기 때문이다.
sequenceDiagram
participant A as 세션 A: 긴 트랜잭션
participant M as MDL 관리자
participant B as 세션 B: ALTER TABLE
participant C as 세션 C: 새 SELECT
A->>M: SHARED_READ 획득
M-->>A: GRANTED
B->>M: EXCLUSIVE 요청
M-->>B: PENDING
C->>M: SHARED_READ 요청
Note over M,C: 선행 DDL과 스케줄링 정책 때문에<br/>후속 요청도 대기할 수 있음
A->>M: COMMIT
M-->>B: EXCLUSIVE GRANTED
B->>M: DDL 완료 후 해제
M-->>C: SHARED_READ GRANTED
현재 공유 lock과 새 SELECT의 공유 요청만 비교하면 서로 양립할 수 있어 보인다. 그러나 이미 배타 DDL이 기다리는 상황에서 공유 요청을 계속 통과시키면 DDL이 영원히 굶을 수 있다. MySQL의 MDL 스케줄링은 일반적으로 쓰기 성격 요청을 우선해 기아를 방지하므로, DDL 뒤에 도착한 읽기까지 기다리는 현상이 발생할 수 있다. max_write_lock_count는 이 정책과 관련되지만, 운영 장애를 이 변수 조정으로 해결하는 것은 권장하지 않는다. 긴 트랜잭션과 무제한 DDL 대기라는 원인을 제거하는 것이 우선이다.
이 연쇄는 다음 순서로 서비스 장애를 확대한다.
- 오래 열린 트랜잭션이 테이블 MDL을 보유한다.
- 배포 작업의
ALTER TABLE이 배타 MDL을 기다린다. - 같은 테이블에 접근하는 신규 요청이 DDL 뒤에서 대기한다.
- connection pool이 소진되고 애플리케이션 timeout이 증가한다.
- 재시도가 새 connection과 대기를 더 만들어 장애를 증폭한다.
4. Performance Schema에서 MDL을 관측하는 방법
MySQL 8.0에서는 performance_schema.metadata_locks를 기본 관측점으로 사용한다. 먼저 대상 버전과 instrumentation 상태를 확인한다.
SELECT VERSION() AS mysql_version;
SHOW TABLES FROM performance_schema LIKE 'metadata_locks';
SELECT NAME, ENABLED, TIMED
FROM performance_schema.setup_instruments
WHERE NAME = 'wait/lock/metadata/sql/mdl';
실행 결과(MySQL 8.0.x):
mysql> SELECT VERSION() AS mysql_version;
+---------------+
| mysql_version |
+---------------+
| 8.0.46 |
+---------------+
1 row in set (0.00 sec)
mysql> SHOW TABLES FROM performance_schema LIKE 'metadata_locks';
+-----------------------------------------------+
| Tables_in_performance_schema (metadata_locks) |
+-----------------------------------------------+
| metadata_locks |
+-----------------------------------------------+
1 row in set (0.01 sec)
mysql> SELECT NAME, ENABLED, TIMED
-> FROM performance_schema.setup_instruments
-> WHERE NAME = 'wait/lock/metadata/sql/mdl';
+----------------------------+---------+-------+
| NAME | ENABLED | TIMED |
+----------------------------+---------+-------+
| wait/lock/metadata/sql/mdl | YES | YES |
+----------------------------+---------+-------+
1 row in set (0.00 sec)
metadata_locks가 존재하더라도 instrumentation이 비활성화되어 있으면 필요한 행이 보이지 않을 수 있다. 운영 서버에서 설정을 바꾸기 전에는 성능 영향과 사내 변경 절차를 검토해야 한다. 일반적인 MySQL 8.0 배포에서는 해당 instrument를 사용할 수 있지만, 관리형 서비스의 버전과 parameter 정책은 별도로 확인한다.
4.1 재현: 긴 읽기와 대기 중인 ALTER
다음 예제는 MySQL Event Scheduler를 두 번째 서버 세션처럼 사용한다. 첫 event가 테이블을 읽으면서 SLEEP()하고, 두 번째 event가 같은 테이블을 변경하려고 기다리는 동안 현재 세션이 MDL 상태를 조회한다. 학습용 임시 인스턴스에서 실행하는 예제이며, 운영 서버에서 Event Scheduler를 임의로 활성화하지 않는다.
SET GLOBAL event_scheduler = ON;
DROP EVENT IF EXISTS mdl_note_long_read;
DROP EVENT IF EXISTS mdl_note_waiting_alter;
DROP TABLE IF EXISTS mdl_note_demo;
CREATE TABLE mdl_note_demo (
id BIGINT PRIMARY KEY,
payload VARCHAR(100) NOT NULL
) ENGINE = InnoDB;
INSERT INTO mdl_note_demo VALUES (1, 'metadata lock demo');
CREATE EVENT mdl_note_long_read
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND
DO
SELECT SLEEP(8), payload
FROM mdl_note_demo
WHERE id = 1;
CREATE EVENT mdl_note_waiting_alter
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 2 SECOND
DO
ALTER TABLE mdl_note_demo
ADD COLUMN created_at TIMESTAMP NULL;
SELECT SLEEP(4) AS wait_for_events;
SELECT ml.OBJECT_TYPE,
CONCAT_WS('.', ml.OBJECT_SCHEMA, ml.OBJECT_NAME) AS object_name,
ml.LOCK_TYPE,
ml.LOCK_DURATION,
ml.LOCK_STATUS,
t.PROCESSLIST_ID,
LEFT(t.PROCESSLIST_INFO, 100) AS current_sql
FROM performance_schema.metadata_locks AS ml
LEFT JOIN performance_schema.threads AS t
ON t.THREAD_ID = ml.OWNER_THREAD_ID
WHERE ml.OBJECT_SCHEMA = DATABASE()
AND ml.OBJECT_NAME = 'mdl_note_demo'
ORDER BY FIELD(ml.LOCK_STATUS, 'PENDING', 'GRANTED'),
t.PROCESSLIST_ID;
SELECT SLEEP(7) AS wait_for_completion;
SHOW COLUMNS FROM mdl_note_demo LIKE 'created_at';
DROP EVENT IF EXISTS mdl_note_long_read;
DROP EVENT IF EXISTS mdl_note_waiting_alter;
DROP TABLE mdl_note_demo;
실행 결과(MySQL 8.0.x):
다음은 검증 출력에서 준비·정리 문장을 줄이고 MDL 상태와 DDL 완료 여부를 중심으로 발췌한 결과다. PROCESSLIST_ID는 실행마다 달라질 수 있다.
mysql> CREATE TABLE mdl_note_demo (...);
Query OK, 0 rows affected (0.01 sec)
mysql> INSERT INTO mdl_note_demo VALUES (1, 'metadata lock demo');
Query OK, 1 row affected (0.00 sec)
mysql> CREATE EVENT mdl_note_long_read ...;
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE EVENT mdl_note_waiting_alter ...;
Query OK, 0 rows affected (0.01 sec)
mysql> SELECT ml.OBJECT_TYPE, ..., ml.LOCK_TYPE,
-> ml.LOCK_DURATION, ml.LOCK_STATUS, ...
-> FROM performance_schema.metadata_locks AS ml ...;
+-------------+-------------------------------+-------------------+---------------+-------------+
| OBJECT_TYPE | object_name | LOCK_TYPE | LOCK_DURATION | LOCK_STATUS |
+-------------+-------------------------------+-------------------+---------------+-------------+
| TABLE | mysql_tech_note.mdl_note_demo | EXCLUSIVE | TRANSACTION | PENDING |
| TABLE | mysql_tech_note.mdl_note_demo | SHARED_READ | TRANSACTION | GRANTED |
| TABLE | mysql_tech_note.mdl_note_demo | SHARED_UPGRADABLE | TRANSACTION | GRANTED |
+-------------+-------------------------------+-------------------+---------------+-------------+
3 rows in set (0.00 sec)
mysql> SHOW COLUMNS FROM mdl_note_demo LIKE 'created_at';
+------------+-----------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+-----------+------+-----+---------+-------+
| created_at | timestamp | YES | | NULL | |
+------------+-----------+------+-----+---------+-------+
1 row in set (0.01 sec)
mysql> DROP TABLE mdl_note_demo;
Query OK, 0 rows affected (0.01 sec)
조회 시점에는 보통 긴 읽기가 가진 GRANTED lock과 ALTER TABLE이 요청한 PENDING lock을 함께 볼 수 있다. ALTER TABLE은 실행 단계에서 복수의 MDL을 획득하거나 lock을 승격할 수 있으므로 행 수와 LOCK_TYPE 조합은 minor version과 관측 순간에 따라 달라질 수 있다. 고정된 행 수보다 다음 증거를 연결해서 읽어야 한다.
- 같은
OBJECT_SCHEMA,OBJECT_NAME에GRANTED와PENDING이 공존하는가 OWNER_THREAD_ID를performance_schema.threads와 연결했을 때 어느 connection인가- blocker가 현재 SQL을 실행 중인가, 트랜잭션만 열린 채 idle 상태인가
- 대기 문장이 실제 DDL인지, DDL 뒤에서 기다리는 후속 DML인지
5. 실전 진단 절차
5.1 먼저 증상을 분류한다
ALTER TABLE이 보이지 않는다고 바로 종료해서는 안 된다. 대기 중인 DDL이 애플리케이션 timeout으로 이미 사라졌더라도 오래 열린 트랜잭션은 남아 있을 수 있다. 다음 순서로 범위를 좁힌다.
SHOW FULL PROCESSLIST또는performance_schema.threads에서 대기 세션과 실행 시간을 확인한다.performance_schema.metadata_locks에서 대상 객체의PENDING/GRANTED관계를 확인한다.information_schema.INNODB_TRX에서 열린 트랜잭션의 시작 시각, 상태, 변경 행 수를 확인한다.performance_schema.events_statements_current와 history가 활성화되어 있다면 최근 문장을 보완한다.- 배포 시스템, migration 도구, application connection pool의 owner를 식별한다.
PROCESSLIST_INFO가 NULL이라고 해서 무해한 세션은 아니다. Sleep 상태인 connection도 commit되지 않은 트랜잭션과 MDL을 보유할 수 있다. 반대로 INNODB_TRX에 보이는 모든 오래된 행이 현재 MDL blocker인 것도 아니다. 반드시 metadata_locks.OWNER_THREAD_ID와 thread 정보를 연결해 대상 객체의 보유 관계를 확인한다.
5.2 sys.schema_table_lock_waits 활용
sys schema를 사용할 수 있는 환경이라면 sys.schema_table_lock_waits가 waiting session, blocking session, 대기 SQL을 읽기 쉬운 형태로 정리해 준다. 다만 운영 표준 쿼리를 만들 때는 view 정의와 권한, MySQL minor version을 확인하고, 최종 근거는 performance_schema.metadata_locks와 thread/transaction 정보로 교차 검증하는 편이 안전하다.
5.3 종료 대상은 blocker와 waiter 중 무엇인가
장애 대응에서 가장 위험한 행동은 process ID만 보고 무조건 KILL하는 것이다.
- 대기 중 DDL을 중단하면 후속 application query가 빠르게 회복될 수 있다. 온라인 서비스 복구가 우선일 때 보통 첫 선택이다.
- blocker 세션을 종료하면 rollback이 발생할 수 있다. 변경 행이 많은 트랜잭션이면 rollback 자체가 오래 걸리고 부하가 커질 수 있다.
- blocker가 중요한 업무 트랜잭션이면 owner와 영향도를 확인한 뒤 정상 commit/rollback을 유도하는 편이 낫다.
- DDL client의 timeout만 끝났어도 서버에서 작업이 계속되는지 반드시 확인한다. client 연결 종료 방식과 도구 동작에 따라 판단이 달라질 수 있다.
즉, 복구 순서는 “가장 오래된 connection 종료”가 아니라 서비스 연쇄 차단을 만든 waiter를 먼저 제거할지, 원래 blocker를 끝낼지 영향도를 비교하는 결정이다.
6. Online DDL과 ALGORITHM=INSTANT에 대한 오해
LOCK=NONE은 DDL 전체 수명 동안 모든 lock이 없다는 뜻이 아니다. 많은 online DDL은 대략 다음 단계를 거친다.
- 작업 준비를 위해 MDL을 얻는다.
- 허용되는 경우 동시 DML과 함께 내부 작업을 수행한다.
- 새 정의를 게시하고 dictionary 상태를 전환하는 마지막 단계에서 다시 강한 MDL이 필요할 수 있다.
ALGORITHM=INSTANT도 데이터 page를 전부 다시 쓰지 않아 실행 시간이 짧을 뿐, schema 정의를 원자적으로 바꾸기 위한 MDL은 필요하다. 따라서 아주 짧을 것으로 예상한 DDL도 기존 긴 트랜잭션 때문에 시작 지점에서 오래 기다릴 수 있다.
DDL 명령의 LOCK 절은 “기다리지 말라”는 timeout 설정도 아니다. DDL 대기 시간을 제한하려면 작업 세션에서 lock_wait_timeout을 명시적으로 검토한다. 이 변수는 InnoDB row lock의 innodb_lock_wait_timeout과 다른 범위를 다룬다. 배포 도구가 무제한에 가까운 기본값을 그대로 사용하면 대기 중 DDL이 서비스 query를 막는 시간이 길어질 수 있다.
온라인 schema 변경 도구도 MDL을 완전히 제거하지 않는다. shadow table 생성, trigger 또는 복제형 변경, 마지막 rename/swap 단계에서 짧고 강한 MDL 구간이 남는다. 도구 이름보다 다음 항목을 확인해야 한다.
- 최종 cutover가 획득할 MDL과 timeout
- 긴 트랜잭션 사전 점검 여부
- 실패 시 재시도와 cleanup 방식
- replica lag 또는 Aurora reader 영향에 따른 중단 기준
- DDL을 취소했을 때 원본·임시 객체의 상태
7. 흔한 실패 모드와 오해
7.1 SHOW PROCESSLIST에 긴 SELECT가 없으니 blocker가 없다는 판단
문장은 끝났지만 트랜잭션이 끝나지 않은 idle connection이 원인일 수 있다. application의 transaction boundary와 autocommit 설정을 함께 확인한다.
7.2 DDL이 pending인데 계속 기다리는 운영
“언젠가 끝날 것”이라고 기다리는 동안 신규 query가 DDL 뒤에 쌓일 수 있다. connection pool 포화 전에 DDL을 취소하는 자동 안전장치가 필요하다.
7.3 innodb_lock_wait_timeout만 조정
MDL 대기는 InnoDB record lock 대기와 같은 문제가 아니다. DDL용 lock_wait_timeout, migration 도구 timeout, application statement timeout을 별도로 설계한다.
7.4 KILL QUERY와 KILL CONNECTION의 차이를 무시
현재 문장만 중단해도 열린 트랜잭션과 보유 자원이 기대대로 정리되는지 확인해야 한다. 명시적 트랜잭션을 보유한 connection을 남겨 두면 장애 원인이 지속될 수 있다. 반대로 connection 종료는 rollback 비용을 유발한다.
7.5 관측 쿼리의 권한 부족을 “대기가 없음”으로 해석
관리 계정의 권한과 Performance Schema 설정에 따라 다른 세션의 SQL text가 가려지거나 instrumentation 정보가 부족할 수 있다. 빈 결과를 곧바로 정상 상태로 해석하지 말고 object 존재, instrument 활성화, 계정 권한을 먼저 확인한다.
8. Aurora MySQL에서의 운영 해석
Aurora MySQL도 MySQL 호환 SQL 계층에서 MDL을 사용하므로, writer instance의 긴 트랜잭션과 DDL 사이에서 발생하는 기본 원리는 같다. 분산 storage가 DDL의 MDL 필요성을 없애 주지는 않는다.
다만 topology를 함께 고려해야 한다.
- DDL은 writer에서 실행되므로 우선 writer의
performance_schema.metadata_locks, thread, transaction을 조사한다. - reader에서 실행한 일반 읽기는 writer의 동일 process 내 MDL holder와 같다고 단정할 수 없다. 그러나 schema 변경 전파, reader 재시작 가능성, 장기 query 처리 방식은 Aurora MySQL 버전과 DDL 종류에 따라 달라질 수 있으므로 변경 창에서 별도로 검증한다.
- Performance Insights 또는 Database Insights의 wait 차원은 급증 시점을 찾는 데 유용하지만, wait 이름만으로 blocker를 확정하지 않는다. Performance Schema의 객체·thread 관계와 연결한다.
- failover는 connection을 끊어 lock을 사라지게 할 수 있지만 정상적인 MDL 해소 수단이 아니다. transaction 중단, 재연결 폭증, DDL 상태 확인 등 더 큰 위험을 만든다.
- cluster parameter group과 instance parameter group의 적용 범위, 재부팅 필요 여부를 확인한 뒤 instrumentation 또는 timeout 관련 설정을 변경한다.
Aurora에서 안전한 DDL 운영을 설계할 때는 writer의 MDL 사전 점검뿐 아니라 reader lag, long-running read, failover 가능성, schema 변경 후 reader 상태까지 하나의 change plan에 포함한다.
9. 배포 전·장애 대응 체크리스트
배포 전
- DDL의
ALGORITHM과LOCK - 배포 세션의
lock_wait_timeout
장애 대응
-
metadata_locks의 객체명,LOCK_STATUS
10. 결론
Metadata Lock은 MySQL이 객체 정의를 일관되게 유지하기 위한 필수 안전장치다. 문제는 lock 자체보다 트랜잭션이 의도보다 오래 열리고, 배타 DDL이 제한 없이 기다리며, 그 뒤로 신규 요청이 쌓이는 운영 경로에서 발생한다.
MDL 장애를 정확히 해석하려면 performance_schema.metadata_locks의 GRANTED와 PENDING, owner thread, 열린 트랜잭션을 하나의 관계로 읽어야 한다. INSTANT와 online DDL도 MDL에서 자유롭지 않으므로, 안전한 schema 변경은 알고리즘 선택뿐 아니라 사전 blocker 점검, 짧은 timeout, 빠른 취소, 재시도 통제를 포함해야 한다.
다음 단계에서는 실제 운영에서 DDL 대기를 줄이는 online schema change 전략과 cutover 시점의 안전 조건을 더 구체적으로 다룰 수 있다.