- Published on
MySQL 8.0 히스토리 리스트 폭증으로 TPS 급락 대응
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
MySQL 8.0(InnoDB)에서 History list length가 갑자기 치솟고 TPS가 급락하는 장애는 대개 “정리(purge)가 따라가지 못해 언두(undo) 히스토리가 누적되는 상황”에서 시작합니다. 겉으로는 CPU가 애매하게 높거나, I/O가 튀거나, 락 대기가 늘어난 것처럼 보이지만, 실제 핵심은 긴 트랜잭션/긴 읽기 뷰(read view) 가 purge를 막아 언두 체인을 끝없이 늘리는 데 있습니다.
이 글은 다음 순서로 정리합니다.
History list length가 의미하는 것과 TPS가 떨어지는 이유- 장애 시 즉시 확인할 지표(어떤 쿼리가 purge를 막는지)
- 즉시 완화(응급 처치)와 안전한 롤백/종료 기준
- 재발 방지(트랜잭션 설계, 격리수준, 파라미터, 운영 가드레일)
운영 장애 분석/재발 방지 관점은 리눅스 서비스 트러블슈팅과도 결이 비슷합니다. 원인 추적 프레임은 아래 글도 참고할 만합니다.
1) History list length가 폭증하면 왜 TPS가 급락하나
InnoDB는 MVCC를 위해 변경 전 이미지(undo)를 유지합니다. 각 트랜잭션은 “읽기 뷰”를 가지고, 그 뷰가 참조할 수 있는 과거 버전이 남아있는 동안 InnoDB는 해당 undo를 지우지 못합니다. 이때 purge 스레드는 “더 이상 어떤 읽기 뷰에서도 필요하지 않은 undo”만 제거할 수 있습니다.
History list length는 간단히 말해 purge 대기 중인 undo 레코드(또는 그에 준하는 히스토리)의 누적량을 나타내는 대표 지표입니다. 이 값이 커지면 다음 문제가 연쇄적으로 발생합니다.
- 일관 읽기(consistent read) 비용 증가: SELECT가 현재 버전을 바로 읽지 못하면 undo 체인을 따라가며 과거 버전을 재구성해야 합니다.
- B+Tree 페이지 정리/버퍼풀 효율 저하: 변경이 많고 purge가 밀리면 버퍼풀에 더 많은 “정리 전 상태”가 남아 캐시 효율이 떨어집니다.
- undo tablespace I/O 증가: undo가 쌓이면 undo 테이블스페이스가 커지고 읽기/쓰기 I/O가 증가합니다.
- 락/대기 악화처럼 보이는 2차 증상: 실제 원인은 purge 정체인데, 결과적으로 쿼리 지연이 늘며 락 경합이 더 커지는 악순환이 생깁니다.
즉, TPS 급락은 “단일 원인”이 아니라 undo 누적이 만든 시스템 전반의 마찰 증가로 나타납니다.
2) 장애 시 5분 안에 보는 체크리스트
2.1 InnoDB 트랜잭션/히스토리 지표
가장 먼저 SHOW ENGINE INNODB STATUS를 봅니다.
SHOW ENGINE INNODB STATUS\G
여기서 확인 포인트는 다음입니다.
History list length값이 비정상적으로 큰가Purge done for trx's n:o진행이 멈춘 것처럼 보이는가TRANSACTIONS섹션에 오래 열린 트랜잭션이 있는가
2.2 “purge를 막는 범인” 찾기: 오래 열린 트랜잭션
MySQL 8.0에서는 performance_schema와 information_schema.innodb_trx를 함께 봅니다.
SELECT
t.trx_id,
t.trx_state,
t.trx_started,
TIMESTAMPDIFF(SECOND, t.trx_started, NOW()) AS trx_age_sec,
t.trx_mysql_thread_id,
t.trx_query
FROM information_schema.innodb_trx AS t
ORDER BY trx_age_sec DESC
LIMIT 20;
여기서 trx_age_sec가 큰 세션이 purge를 막는 1순위 후보입니다. 특히 다음 패턴이 위험합니다.
- 애플리케이션이 트랜잭션을 열고 커밋을 안 함(커넥션 풀 반환 누락)
- 배치가 큰 범위를 갱신하면서 수 분~수십 분 커밋을 미룸
REPEATABLE READ에서 장시간 SELECT를 유지(긴 읽기 뷰)
2.3 어떤 SQL/클라이언트인지 매핑
스레드 ID로 프로세스리스트를 매핑합니다.
SELECT
p.ID,
p.USER,
p.HOST,
p.DB,
p.COMMAND,
p.TIME,
p.STATE,
LEFT(p.INFO, 200) AS info
FROM information_schema.PROCESSLIST AS p
WHERE p.ID IN (
SELECT trx_mysql_thread_id FROM information_schema.innodb_trx
)
ORDER BY p.TIME DESC;
여기서 HOST(애플리케이션 서버), USER(서비스 계정), INFO(쿼리)로 “누가 잡고 있나”를 특정합니다.
2.4 undo 테이블스페이스/파일 증가 확인
운영체제 레벨에서 undo 파일이 커지고 있는지, 디스크가 압박받는지 확인합니다. 디스크가 100%에 근접하면 장애가 다른 형태로 폭발합니다(쓰기 실패, fsync 지연 등). 로그 파일 삭제 후 공간 회수 이슈처럼 “보이는데 안 줄어드는” 상황도 같이 점검해야 합니다.
3) 즉시 완화(응급 처치) 시나리오
핵심 목표는 하나입니다.
- purge를 막는 오래 열린 트랜잭션을 종료하거나 정상 커밋/롤백시키기
3.1 가장 안전한 1순위: 범인 세션 종료
오래 열린 트랜잭션이 “읽기 전용 SELECT”라면 종료 비용이 상대적으로 낮습니다.
KILL 12345;
하지만 “대량 UPDATE/DELETE 중인 트랜잭션”을 죽이면 롤백이 오래 걸릴 수 있고, 오히려 부하가 더 커질 수 있습니다. 이때는 다음을 같이 판단합니다.
- 해당 트랜잭션이 얼마나 많은 row를 변경했는가
- 롤백 시 undo를 더 쓰며 I/O를 폭발시키지 않는가
- 서비스가 이미 죽어있는가, 부분 장애인가
실무 팁: 대량 쓰기 트랜잭션이 원인이라면, 무작정 KILL하기보다 애플리케이션/배치 측에서 즉시 커밋/중단 로직을 넣어 “정상 종료”를 유도하는 편이 더 안전한 경우가 많습니다.
3.2 트래픽/쓰기 부하를 일시적으로 줄이기
purge가 따라잡으려면 “새로운 변경”이 줄어야 합니다.
- 쓰기 API를 일시 차단(큐잉, 429, 기능 플래그)
- 배치 중단
- 핫 테이블에 대한 대량 갱신 작업 중지
읽기 트래픽도 문제를 키울 수 있습니다. undo 체인이 길어지면 읽기 쿼리도 느려져 CPU를 태우며 purge가 더 밀릴 수 있습니다. 상황에 따라 캐시 우회/강제 캐시 같은 임시 조치도 고려합니다.
3.3 purge 스레드 관련 파라미터는 “마지막 카드”
innodb_purge_threads를 늘리면 도움이 되는 케이스가 있지만, purge를 막는 트랜잭션이 존재하는 상태에서는 근본 해결이 아닙니다.
- 막고 있는 읽기 뷰가 계속 살아있으면 purge 스레드를 늘려도 지울 수 있는 게 없습니다.
- 오히려 CPU 경쟁만 늘 수 있습니다.
즉, 파라미터 튜닝은 “범인 제거 후 회복 속도”를 높이는 용도로 접근하는 게 안전합니다.
4) 재발 방지: 원인별 설계/운영 가드레일
여기서부터가 진짜입니다. History list length 폭증은 대부분 “긴 트랜잭션을 허용하는 설계/운영”에서 재발합니다.
4.1 애플리케이션 트랜잭션 경계 재점검
가장 흔한 원인 TOP 3:
- 커넥션 풀에서 트랜잭션을 연 채로 반환
- 예외 처리 누락으로 커밋/롤백이 실행되지 않음
- 외부 호출(HTTP, RPC)을 트랜잭션 안에서 수행
트랜잭션 안에서 외부 호출을 하면 네트워크 지연이 곧 트랜잭션 시간으로 늘어납니다. 아래처럼 트랜잭션 범위를 DB 작업으로만 최소화합니다.
# 나쁜 예: 외부 호출이 트랜잭션 안에 있음
BEGIN;
SELECT ... FOR UPDATE;
call_external_api();
UPDATE ...;
COMMIT;
# 좋은 예: 외부 호출은 트랜잭션 밖
call_external_api();
BEGIN;
SELECT ... FOR UPDATE;
UPDATE ...;
COMMIT;
위 의사코드는 BEGIN/COMMIT을 보여주기 위한 것이고, 실제 구현에서는 프레임워크(예: Spring) 트랜잭션 어노테이션 범위를 줄이는 식으로 적용합니다.
4.2 긴 읽기(리포트/덤프/ETL) 쿼리의 격리
REPEATABLE READ에서 장시간 SELECT는 오래된 읽기 뷰를 유지해 purge를 막을 수 있습니다. 다음과 같이 분리합니다.
- 리포트/분석 쿼리는 리드 레플리카로 분리
- 온라인 서비스 트래픽과 다른 계정/리소스 그룹으로 격리
- 필요 시 세션 격리수준을
READ COMMITTED로 낮춰 읽기 뷰 지속 시간을 줄임
세션 단위로 격리수준을 바꾸는 예:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION READ ONLY;
SELECT ...;
COMMIT;
주의: 격리수준 변경은 정합성 요구사항과 트레이드오프입니다. “팬텀/반복읽기” 요구가 있는 기능에는 그대로 REPEATABLE READ가 필요할 수 있습니다.
4.3 대량 UPDATE/DELETE는 “작게 쪼개서 커밋”
대량 갱신을 한 방에 커밋하면 undo가 폭발하고, 롤백도 지옥이 됩니다. 다음 패턴을 권장합니다.
- PK 범위 또는 인덱스를 이용해 배치를 청크로 나눔
- 청크마다 커밋
- 청크 사이에 짧은 sleep로 I/O 버스트 완화
예시(청크 삭제):
DELETE FROM events
WHERE created_at < NOW() - INTERVAL 30 DAY
LIMIT 5000;
이 쿼리를 애플리케이션/배치에서 반복 실행하면서 매 반복마다 커밋되게 만듭니다(오토커밋 또는 명시 커밋). LIMIT 삭제는 실행 계획/락 범위가 환경마다 달라질 수 있으니, 가능하면 “PK 범위 기반”이 더 예측 가능합니다.
4.4 운영 가드레일: 타임아웃/관측/킬 스위치
(1) 관측: 임계치 기반 알람
다음 지표를 최소 알람 세트로 추천합니다.
History list length(급증률 포함)- 오래 열린 트랜잭션 수(예: 60초 초과)
- undo tablespace 크기 증가율
- p95/p99 쿼리 지연, row lock time
performance_schema를 기반으로 “트랜잭션 나이”를 주기적으로 수집해도 좋습니다.
(2) 애플리케이션 타임아웃
- DB 트랜잭션 타임아웃(프레임워크 레벨)
- 커넥션 풀 leak detection
- 쿼리 타임아웃
이 조합이 없으면 “조용히 오래 열린 트랜잭션”이 계속 생깁니다.
(3) 킬 스위치
장애 시 배치/관리 작업을 즉시 중단할 수 있는 스위치를 둡니다.
- 배치 실행 플래그
- 쓰기 기능 플래그
- 관리용 계정의 권한 분리
5) 실전 진단 흐름(Runbook) 예시
장애 대응 문서에 그대로 붙여넣기 좋은 형태로 정리하면 다음과 같습니다.
SHOW ENGINE INNODB STATUS\G에서History list length와 오래 열린 트랜잭션 징후 확인information_schema.innodb_trx로trx_age_sec상위 세션 식별PROCESSLIST로 서비스/호스트/쿼리 매핑- 읽기 트랜잭션이면 우선
KILL로 제거 - 대량 쓰기 트랜잭션이면 롤백 비용 평가 후
- 가능하면 애플리케이션에서 정상 종료(커밋/중단) 유도
- 불가하면
KILL후 롤백 부하 모니터링
- 쓰기 트래픽/배치 일시 차단으로 purge가 따라잡게 함
- 정상화 후 재발 방지 항목(트랜잭션 경계, 배치 청크, 리포트 격리, 알람) 적용
6) 자주 묻는 함정
6.1 “SELECT만 했는데 왜 히스토리가 늘지?”
SELECT 자체가 undo를 만들지는 않지만, 긴 읽기 뷰는 “지워도 되는 undo”를 지우지 못하게 만듭니다. 즉, 쓰기는 다른 곳에서 발생하고 있는데, 오래 열린 SELECT가 purge를 막아 히스토리가 누적될 수 있습니다.
6.2 “purge 스레드 늘리면 해결 아닌가?”
막고 있는 트랜잭션이 있으면 purge는 지울 수 있는 게 제한됩니다. 원인 제거가 먼저이고, 그 다음이 purge 처리량 튜닝입니다.
6.3 “레플리카 지연이 같이 커졌는데 관련 있나?”
있습니다. 마스터에서 대량 변경과 undo 누적이 발생하면 쿼리 지연과 I/O 경합이 커지고, 바이너리 로그 생성/플러시도 영향을 받아 레플리카 지연으로 이어질 수 있습니다. 또한 레플리카에서 리포트 쿼리가 길게 돌며 자체적으로도 purge가 밀릴 수 있습니다.
7) 마무리
MySQL 8.0에서 History list length 폭증은 “InnoDB purge가 막히는 구조”를 이해하면 대응이 단순해집니다.
- 지표로 현상을 확인하고
- 오래 열린 트랜잭션(특히 긴 읽기 뷰)을 찾아 제거하고
- 쓰기/배치/리포트를 분리하며
- 배치 작업은 청크 커밋으로 바꾸고
- 관측/타임아웃/킬 스위치로 재발을 막습니다.
TPS 급락은 결과이고, 원인은 거의 항상 “트랜잭션을 오래 잡는 습관”입니다. 이 습관을 설계와 운영 가드레일로 교정하면 같은 유형의 장애를 크게 줄일 수 있습니다.