- Published on
MySQL 8.0 InnoDB 데드락 폭증 원인 추적·튜닝
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버는 멀쩡해 보이는데 애플리케이션 에러 로그에 Deadlock found when trying to get lock 이 연속으로 찍히기 시작하면, 대부분의 팀은 "DB가 느려졌다" 정도로만 인지합니다. 하지만 데드락은 단순 성능 저하가 아니라 동시성 제어가 깨진 신호입니다. 특히 MySQL 8.0 InnoDB는 다양한 상황에서 레코드 락, 갭 락, 넥스트키 락이 복합적으로 걸리며, 특정 배포나 데이터 분포 변화(핫 키, 쏠림) 이후 데드락이 폭증하는 패턴이 자주 나옵니다.
이 글에서는 MySQL 8.0 기준으로 데드락 폭증을 재현 없이도 빠르게 원인 범위를 좁히는 관측 방법과, 효과가 큰 튜닝 포인트를 우선순위대로 정리합니다. 인덱스와 격리수준 중심의 튜닝은 이전 글인 MySQL InnoDB 데드락 폭증 대응 - 인덱스·격리수준 튜닝도 함께 보면 연결해서 이해하기 좋습니다.
1) 데드락 폭증의 전형적인 트리거
데드락은 "두 트랜잭션이 서로가 가진 락을 기다리는" 상황이지만, 실무에서 폭증은 대개 아래 트리거 중 하나로 시작됩니다.
1-1. 배포로 인해 쿼리 플랜이 바뀜
- 인덱스 힌트 제거, 조건절 변경, 정렬 추가로 인해 잠그는 범위가 커짐
UPDATE ... WHERE가 인덱스를 못 타며 더 많은 레코드를 스캔하고 락을 잡음
1-2. 데이터 분포 변화(핫 키, 쏠림)
- 특정
user_id,tenant_id,status값으로 트래픽이 집중 - 같은 인덱스 페이지에 경쟁이 몰려 락 경합과 교착이 급증
1-3. 트랜잭션이 길어짐
- 애플리케이션에서 불필요하게 트랜잭션 범위를 넓힘
- 외부 API 호출, 파일 IO, 메시지 발행을 트랜잭션 내부에서 수행
1-4. SELECT ... FOR UPDATE 남발 혹은 잠금 순서 불일치
- 서로 다른 코드 경로가 같은 테이블을 다른 순서로 잠금
- 예: A는
orders후order_items, B는order_items후orders
2) 관측 1단계: 데드락 로그를 “쓸모 있게” 남기기
데드락은 발생 순간의 정보가 가장 중요합니다. MySQL은 기본적으로 최근 데드락을 SHOW ENGINE INNODB STATUS 에 남기지만, 운영에서 놓치기 쉽습니다.
2-1. 즉시 확인: InnoDB Status
SHOW ENGINE INNODB STATUS\G
여기서 LATEST DETECTED DEADLOCK 섹션을 확인합니다.
- 어떤 트랜잭션이 어떤 인덱스 레코드를 잡고 있었는지
- 어떤 쿼리가 락을 기다렸는지
- victim(롤백된 트랜잭션)이 누구인지
문제는 이 정보가 "마지막 1건" 중심이라 폭증 상황에서는 금방 덮입니다.
2-2. 데드락을 에러 로그로 내보내기
MySQL 8.0에서 아래 설정을 켜면 데드락 정보를 에러 로그로 더 적극적으로 남깁니다.
SET GLOBAL innodb_print_all_deadlocks = ON;
- 장점: 데드락이 덮이지 않고 로그로 축적됨
- 주의: 폭증 시 로그가 커질 수 있으니, 수집 파이프라인과 보관 정책을 같이 점검
2-3. 락 대기 자체를 로그로 남기기(보조 신호)
데드락 직전에는 락 대기가 늘어나는 경우가 많습니다.
SET GLOBAL innodb_lock_wait_timeout = 5;
- 데드락 자체를 줄이기보다는, "긴 대기"를 빨리 실패시켜 장애 전파를 줄이는 목적
- 너무 낮추면 정상 트래픽도 실패할 수 있으니 서비스 특성에 맞춰 조정
3) 관측 2단계: performance_schema 로 “누가 누구를 막는지” 보기
MySQL 8.0에서는 performance_schema 가 락 경합을 구조적으로 보여줍니다. 데드락이 터지기 전후의 락 대기 그래프를 잡는 데 유용합니다.
3-1. 현재 락 대기 목록
SELECT
r.trx_id waiting_trx_id,
r.trx_mysql_thread_id waiting_thread,
r.trx_query waiting_query,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking_thread,
b.trx_query blocking_query
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx r
ON w.requesting_trx_id = r.trx_id
JOIN information_schema.innodb_trx b
ON w.blocking_trx_id = b.trx_id;
이 결과로 아래를 빠르게 분류합니다.
- 특정 API가 항상 blocker 인지
- 특정 배치가 대량 업데이트로 blocker 인지
- 동일 테이블에서만 경합이 나는지, 여러 테이블 체인인지
3-2. 트랜잭션이 얼마나 오래 열려있는지
SELECT
trx_id,
trx_state,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_age_sec,
trx_mysql_thread_id,
trx_query
FROM information_schema.innodb_trx
ORDER BY trx_age_sec DESC
LIMIT 20;
trx_age_sec 가 큰 트랜잭션이 자주 보이면, 데드락 이전에 이미 "긴 트랜잭션"이 락을 오래 잡고 있을 가능성이 큽니다.
4) 원인 분석: 데드락 로그에서 봐야 할 5가지
LATEST DETECTED DEADLOCK 을 보면 정보가 많지만, 실무적으로는 다음 5가지를 체크하면 원인 범위가 급격히 좁혀집니다.
4-1. 동일 테이블인지, 교차 테이블인지
- 동일 테이블 내 교착: 인덱스/조건/정렬/핫 키 문제일 확률이 높음
- 교차 테이블 교착: 잠금 순서 불일치(코드 설계 문제) 가능성이 높음
4-2. 락이 걸린 인덱스 이름
데드락 로그에는 종종 index 정보가 나옵니다.
- 의도한 인덱스가 맞는지
- 불필요한 보조 인덱스에서 락 경합이 생기는지
4-3. 레코드 락인지, 갭/넥스트키 락인지
격리수준이 REPEATABLE READ 인 경우 범위 조건에서 갭/넥스트키 락이 생기기 쉽습니다.
WHERE created_at BETWEEN ...같은 범위 업데이트- 보조 인덱스 범위 스캔
4-4. 쿼리 패턴이 UPDATE ... ORDER BY ... LIMIT ... 인지
이 패턴은 "어떤 레코드를 먼저 잡는지"가 중요합니다. 정렬 기준이 인덱스와 맞지 않으면 스캔 락이 커지고 교착이 늘 수 있습니다.
4-5. victim 트랜잭션이 항상 같은 유형인지
항상 같은 API가 victim 이면, 그 API의 트랜잭션 범위가 길거나 잠금 순서가 불리할 가능성이 큽니다.
5) 튜닝 우선순위: 효과가 큰 것부터
데드락은 "완전 제거"가 목표라기보다, 발생률을 낮추고 재시도 비용을 줄이는 것이 목표인 경우가 많습니다. 아래는 실무에서 효과가 큰 순서입니다.
5-1. 트랜잭션 범위 축소(가장 강력)
- 트랜잭션 내부에서 네트워크 호출, 파일 IO, 외부 락(분산락) 등을 제거
- 여러 테이블 업데이트를 꼭 한 트랜잭션에 묶어야 하는지 재검토
예시(나쁜 패턴):
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 10;
-- 여기서 외부 결제 승인 API 호출 같은 작업을 수행한다고 가정
UPDATE orders SET status = 'PAID' WHERE id = 555;
COMMIT;
개선 방향:
- 외부 호출은 트랜잭션 밖에서 수행하거나
- 상태 전이를 더 잘게 쪼개고(예:
PAYING), 재시도 가능한 설계로 바꾸기
5-2. 잠금 순서를 통일하기(교차 테이블 교착 해결)
여러 코드 경로에서 같은 자원을 다른 순서로 잠그면 데드락은 구조적으로 발생합니다.
원칙:
- 모든 경로에서 테이블 잠금 순서를 동일하게
- 같은 테이블 내에서도 "항상 PK 오름차순"으로 잠그는 습관
예시: 여러 행을 업데이트해야 한다면 먼저 대상 PK를 정렬해서 처리
-- 대상 id 목록을 먼저 정렬해 일관된 순서로 잠금
SELECT id
FROM orders
WHERE user_id = 10 AND status = 'PENDING'
ORDER BY id
LIMIT 50
FOR UPDATE;
-- 이후 id 단위로 업데이트(애플리케이션에서 순차 처리)
5-3. 인덱스로 “잠그는 범위”를 줄이기
데드락 폭증의 상당수는 인덱스 미스에서 시작합니다. 인덱스를 추가하는 목적은 성능뿐 아니라 락 범위 축소입니다.
체크리스트
UPDATE ... WHERE가 반드시 적절한 인덱스를 타는가- 조건절의 컬럼 순서가 복합 인덱스와 맞는가
- 불필요한 범위 조건으로 넥스트키 락이 커지지 않는가
예시: 상태 전이 업데이트
UPDATE jobs
SET status = 'RUNNING'
WHERE status = 'QUEUED'
AND scheduled_at <= NOW()
ORDER BY scheduled_at
LIMIT 1;
이 쿼리는 다음 인덱스가 없으면 락 스캔이 커질 수 있습니다.
CREATE INDEX idx_jobs_status_scheduled_at
ON jobs (status, scheduled_at);
또한 ORDER BY 컬럼이 인덱스 후행 컬럼에 있어야 정렬 비용과 락 범위가 줄어듭니다.
5-4. 격리수준 재검토: READ COMMITTED 고려
MySQL 기본은 REPEATABLE READ 이고, 이때 갭/넥스트키 락이 더 적극적으로 발생합니다. 워크로드가 "정합성보다 처리량"에 가깝고, 애플리케이션이 재시도/중복 처리를 견딜 수 있다면 READ COMMITTED 가 데드락을 줄이는 데 도움이 될 수 있습니다.
세션 단위 적용 예:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
-- 쿼리들
COMMIT;
전역 적용은 영향 범위가 크므로, 먼저 문제 API 경로에서 세션 단위로 실험하는 접근이 안전합니다.
5-5. 애플리케이션 재시도 전략(현실적인 방어선)
데드락은 InnoDB가 victim 을 롤백시키며 해소합니다. 따라서 애플리케이션이 데드락 에러를 "치명"으로 처리하면 사용자 장애로 바로 전파됩니다.
권장:
- 데드락 에러 코드에 한해 짧은 지수 백오프 재시도
- 재시도 횟수는 2~3회 정도로 제한
- 같은 요청이 중복 처리되어도 안전한 멱등성 확보
참고로 커넥션 풀/트랜잭션 경계가 꼬이면 문제를 더 키울 수 있는데, DB 이슈가 애플리케이션 풀 레벨 문제로 증폭되는 케이스는 Spring Boot HikariCP 커넥션 누수 진단·해결 같은 글의 관점(관측, 누수, 대기열)을 함께 적용하면 원인 분리가 빨라집니다.
6) 실전 플레이북: “폭증했다”를 30분 안에 정리하는 절차
6-1. 지금 당장 할 일
SHOW ENGINE INNODB STATUS\G로 최근 데드락 패턴 확인innodb_print_all_deadlocks를 켜서 로그 축적innodb_lock_waits와innodb_trx로 blocker 후보 식별
6-2. 1~2시간 내 할 일
- 데드락 로그에서 반복되는 테이블/인덱스/쿼리 Top N 정리
- 해당 쿼리에
EXPLAIN적용, 인덱스 미스 여부 확인 - 트랜잭션 범위(코드)에서 불필요한 작업이 있는지 점검
EXPLAIN 예시:
EXPLAIN
UPDATE jobs
SET status = 'RUNNING'
WHERE status = 'QUEUED'
AND scheduled_at <= NOW()
ORDER BY scheduled_at
LIMIT 1;
type이ALL에 가깝거나rows가 과도하게 크면 락 범위가 커질 가능성이 높습니다.
6-3. 하루 내 할 일
- 인덱스 추가 또는 쿼리 패턴 변경(대상 행을 먼저 좁히고 업데이트)
- 잠금 순서 통일(코드 규약화)
- 필요 시 문제 API에 한해
READ COMMITTED실험
7) 자주 나오는 케이스별 처방
케이스 A: 같은 테이블에서 상태 업데이트가 몰림
- 처방:
(status, scheduled_at)같은 복합 인덱스 ORDER BY와 인덱스 정렬 일치- 대상 행을
SELECT ... FOR UPDATE로 먼저 좁히고 PK 기반 업데이트
케이스 B: 부모-자식 테이블 교차 업데이트
- 처방: 잠금 순서를
parent후child로 고정 - 가능하면 한쪽을 "읽기 후 쓰기"가 아니라 "PK로 직접 쓰기"로 단순화
케이스 C: 범위 조건 업데이트가 많음
- 처방: 범위를 더 잘게 쪼개기(배치 크기 축소)
- 인덱스 재구성
READ COMMITTED검토
8) 마무리: 데드락은 “버그”가 아니라 “설계 신호”
InnoDB 데드락은 어느 정도는 정상입니다. 문제는 "갑자기 폭증"할 때이고, 그때는 거의 항상
- 인덱스/플랜 변화로 잠금 범위가 커졌거나
- 트랜잭션이 길어졌거나
- 잠금 순서가 섞였거나
- 핫 키로 경합이 급증한 상황입니다.
관측을 통해 반복 패턴을 잡고, 트랜잭션 범위 축소와 잠금 순서 통일, 인덱스 튜닝을 우선 적용하면 폭증은 대부분 안정화됩니다. 인덱스와 격리수준을 더 깊게 다루는 내용은 MySQL InnoDB 데드락 폭증 대응 - 인덱스·격리수준 튜닝에서 확장해 볼 수 있습니다.