본문 바로가기

분산 트랜잭션과 XA 복구

2단계 커밋의 준비 투표와 영속적인 결정, 결정을 모르는 동안의 대기를 실패 시점별로 추적하고, MySQL XA의 PREPARED 상태가 엔진 프로세스의 장애와 재시작 뒤에도 남아 외부의 결정을 기다린다는 것을 확인합니다.

단편화와 복제는 데이터를 여러 곳에 두는 선택입니다. 여러 데이터베이스의 변경을 전체 성공 아니면 전체 취소의 한 단위로 확정하려면, 별도의 결정 프로토콜이 필요합니다. 로컬 COMMIT 두 개를 차례로 실행하면, 첫 데이터베이스만 확정되고 두 번째는 실패할 수 있기 때문입니다.

이 글은 8장의 트랜잭션과 멱등성과 로그·체크포인트와 장애 회복을 읽은 뒤 진행하며, 관리자 연결과 학습 전용 서버의 준비는 계정·역할과 최소 권한의 접속 안내를 따릅니다.

준비와 최종 결정을 나누는 2PC

재고 데이터베이스 A의 수량 감소와 주문 데이터베이스 B의 주문 생성을 같은 전역 트랜잭션 order-7로 묶는다고 합시다. 조정자(coordinator)는 참여자의 목록과 최종 결정을 관리하고, 참여자(resource manager)는 자기 로컬 변경을 준비하고 확정합니다.

단계조정자참여자 A와 B
준비 요청모두에게 PREPARE를 요청한다로컬 검증과 변경을 수행한다
투표READY나 거절을 모은다복구할 수 있는 PREPARED 상태를 저장한 뒤에 READY를 보낸다
결정 저장모두 READY면 COMMIT, 거절이나 준비 시간 초과면 ABORT를 영속적으로 기록한다PREPARED는 아직 최종 커밋이 아니며, 관련 잠금과 자원을 유지한다
결정 전달저장한 COMMIT이나 ABORT를 다시 보낼 수 있다그 결정을 적용하고 완료를 응답한다

READY는 "이미 커밋했다"가 아니라, 나중에 어느 결정이 와도 따를 수 있도록 준비를 저장했다는 뜻입니다. 투표 메시지인 READY와 참여자가 저장한 상태인 PREPARED를 구별합니다.

조정자는 COMMIT을 참여자에게 보내기 전에 그 결정을 먼저 저장해야 합니다. 한 참여자가 이미 커밋한 뒤에 조정자가 재시작해 기억만으로 ABORT를 고르면, 전체 성공 아니면 전체 취소라는 약속이 깨지기 때문입니다. 저장한 COMMIT은 마음대로 ABORT로 바꿀 수 없습니다.

재시작할 때 결정을 찾는 곳

실패 시점남은 기록안전한 처리
참여자가 READY를 보내기 전에 실패준비가 끝났다는 보장이 없다모두 READY가 아니므로 조정자는 COMMIT하지 않고, 끝나지 않은 로컬 변경은 복구된다
참여자가 PREPARED가 된 뒤 실패참여자의 준비 기록재시작해 XID와 상태를 되살리고, 조정자의 결정을 조회한다
조정자가 결정을 저장하기 전에 실패일부나 전체 참여자가 PREPARED, 결정은 모름참여자는 시간 초과만으로 임의로 COMMIT이나 ABORT하지 않고, 조정자의 복구나 다른 확실한 결정 기록을 기다린다
조정자가 COMMIT을 저장한 뒤 전달 중에 실패COMMIT 결정과 응답하지 않은 참여자 목록재시작한 뒤 같은 COMMIT을 다시 보내고, 이미 적용된 결과와 대조한다
완료 응답만 잃음참여자는 최종 상태, 조정자는 응답을 모름확정 여부를 조회하고 기록과 대조하며, XA 목록에서 사라졌다는 것만으로 COMMIT과 ABORT를 구별하지 않는다

기본적인 2PC에서는, 조정자의 결정에 접근할 수 없는 PREPARED 참여자가 결정을 모르는 상태로 막힐 수 있습니다. 시간 초과는 상대의 응답을 받지 못했다는 사실일 뿐, 상대가 다른 참여자에게 결정을 보냈는지를 알려 주지 않습니다. 결정의 전달을 다시 시도할 때는 저장한 같은 결정을 따릅니다.

고가용성의 조정자는 결정 로그의 복제와 리더의 유일성 같은 설계가 더 필요하며, 이 글은 합의 프로토콜의 구현이나 중단 없는 2PC를 주장하지 않습니다(Gray와 Lamport의 논문, 2PC와 차단).

MySQL 참여자의 상태 추적

MySQL 8.4에서 XA START는 트랜잭션을 ACTIVE로, XA END는 IDLE로, XA PREPARE는 PREPARED로 옮기고, PREPARED에서 XA COMMIT이나 XA ROLLBACK이 최종 상태를 만듭니다. XA RECOVER는 PREPARED인 XID를 나열합니다. SQL의 준비문 PREPARE와 트랜잭션의 XA PREPARE는 서로 다른 문장입니다.

PREPARED는 엔진 재시작 뒤에도 최종 결정을 기다린다

XA 준비가 영속 상태로 남는다. 조정자의 한 최종 결정을 따른다. 각 단계의 값과 전환 이유를 비교합니다.

PREPARED는 엔진 재시작 뒤에도 최종 결정을 기다린다

한 MySQL 참여자의 준비 상태 복구입니다. READY는 준비 저장 뒤의 투표이며 COMMIT 완료가 아닙니다.

PREPARED는 엔진 재시작 뒤에도 최종 결정을 기다린다한 MySQL 참여자의 준비 상태 복구입니다. READY는 준비 저장 뒤의 투표이며 COMMIT 완료가 아닙니다. 실선 화살표는 설명용 사건 순서, 점선은 엔진 재시작 경계입니다. 조정자 결정 파일과 참여자 datadir은 보존됩니다. 최종 결정이 불명하면 PREPARED를 timeout만으로 취소하지 않으며 이 단일 참여자 그림은 다중 서버/네트워크 fault 실험이 아닙니다.재시작 경계 · 저장소 존속crashrestartdecisionPREPAREDREADY 전 저장자체 엔진 종료datadir 보존재시작·조회XA RECOVER에 남음저장 결정 적용COMMIT / ABORT실선: 사건 순서 · 점선: 재시작 경계
PREPARED는 엔진 재시작 뒤에도 최종 결정을 기다린다한 MySQL 참여자의 준비 상태 복구입니다. READY는 준비 저장 뒤의 투표이며 COMMIT 완료가 아닙니다. 실선 화살표는 설명용 사건 순서, 점선은 엔진 재시작 경계입니다. 조정자 결정 파일과 참여자 datadir은 보존됩니다. 최종 결정이 불명하면 PREPARED를 timeout만으로 취소하지 않으며 이 단일 참여자 그림은 다중 서버/네트워크 fault 실험이 아닙니다.재시작 경계저장소 존속crashrestartdecisionPREPAREDREADY 전 저장자체 엔진 종료datadir 보존재시작·조회XA RECOVER에 남음저장 결정 적용COMMIT / ABORT실선: 사건 순서점선: 재시작 경계
실선 화살표는 설명용 사건 순서, 점선은 엔진 재시작 경계입니다. 조정자 결정 파일과 참여자 datadir은 보존됩니다. 최종 결정이 불명하면 PREPARED를 timeout만으로 취소하지 않으며 이 단일 참여자 그림은 다중 서버/네트워크 fault 실험이 아닙니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 PREPARED부터 저장 결정 적용까지 진행합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
PREPARED

XA 준비가 영속 상태로 남는다.

  • XA·저장 결정 1: XA RECOVER: cs_order_7
  • XA·저장 결정 2: 다른 일반 읽기 100
  • XA·저장 결정 3: XA 내부 변경 70
  • READY 투표 전 저장
엔진 재시작

엔진은 멈춰도 저장소는 남는다.

  • XA·저장 결정 1: datadir 보존
  • XA·저장 결정 2: 결정 파일 보존
  • 재시작 경계 통과
복구 조회

XID가 여전히 준비 상태다.

  • XA·저장 결정 1: XA RECOVER에 남음
  • XA·저장 결정 2: locking read 대기 가능
  • timeout만으로 취소 금지
저장 결정 적용

조정자의 한 최종 결정을 따른다.

  • XA·저장 결정 1: COMMIT이면 70/ABORT이면 100
  • XA·저장 결정 2: RECOVER에서 제거
  • 둘 중 하나 적용; 반복 1397만으로 과거 결정 판별 불가

PREPARED는 엔진 재시작 뒤에도 최종 결정을 기다린다

XA 준비가 영속 상태로 남는다. 조정자의 한 최종 결정을 따른다. 각 단계의 값과 전환 이유를 비교합니다.

PREPARED는 엔진 재시작 뒤에도 최종 결정을 기다린다

한 MySQL 참여자의 준비 상태 복구입니다. READY는 준비 저장 뒤의 투표이며 COMMIT 완료가 아닙니다.

PREPARED는 엔진 재시작 뒤에도 최종 결정을 기다린다한 MySQL 참여자의 준비 상태 복구입니다. READY는 준비 저장 뒤의 투표이며 COMMIT 완료가 아닙니다. 실선 화살표는 설명용 사건 순서, 점선은 엔진 재시작 경계입니다. 조정자 결정 파일과 참여자 datadir은 보존됩니다. 최종 결정이 불명하면 PREPARED를 timeout만으로 취소하지 않으며 이 단일 참여자 그림은 다중 서버/네트워크 fault 실험이 아닙니다.재시작 경계 · 저장소 존속crashrestartdecisionPREPAREDREADY 전 저장자체 엔진 종료datadir 보존재시작·조회XA RECOVER에 남음저장 결정 적용COMMIT / ABORT실선: 사건 순서 · 점선: 재시작 경계
PREPARED는 엔진 재시작 뒤에도 최종 결정을 기다린다한 MySQL 참여자의 준비 상태 복구입니다. READY는 준비 저장 뒤의 투표이며 COMMIT 완료가 아닙니다. 실선 화살표는 설명용 사건 순서, 점선은 엔진 재시작 경계입니다. 조정자 결정 파일과 참여자 datadir은 보존됩니다. 최종 결정이 불명하면 PREPARED를 timeout만으로 취소하지 않으며 이 단일 참여자 그림은 다중 서버/네트워크 fault 실험이 아닙니다.재시작 경계저장소 존속crashrestartdecisionPREPAREDREADY 전 저장자체 엔진 종료datadir 보존재시작·조회XA RECOVER에 남음저장 결정 적용COMMIT / ABORT실선: 사건 순서점선: 재시작 경계
실선 화살표는 설명용 사건 순서, 점선은 엔진 재시작 경계입니다. 조정자 결정 파일과 참여자 datadir은 보존됩니다. 최종 결정이 불명하면 PREPARED를 timeout만으로 취소하지 않으며 이 단일 참여자 그림은 다중 서버/네트워크 fault 실험이 아닙니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 PREPARED부터 저장 결정 적용까지 진행합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
PREPARED

XA 준비가 영속 상태로 남는다.

  • XA·저장 결정 1: XA RECOVER: cs_order_7
  • XA·저장 결정 2: 다른 일반 읽기 100
  • XA·저장 결정 3: XA 내부 변경 70
  • READY 투표 전 저장
엔진 재시작

엔진은 멈춰도 저장소는 남는다.

  • XA·저장 결정 1: datadir 보존
  • XA·저장 결정 2: 결정 파일 보존
  • 재시작 경계 통과
복구 조회

XID가 여전히 준비 상태다.

  • XA·저장 결정 1: XA RECOVER에 남음
  • XA·저장 결정 2: locking read 대기 가능
  • timeout만으로 취소 금지
저장 결정 적용

조정자의 한 최종 결정을 따른다.

  • XA·저장 결정 1: COMMIT이면 70/ABORT이면 100
  • XA·저장 결정 2: RECOVER에서 제거
  • 둘 중 하나 적용; 반복 1397만으로 과거 결정 판별 불가

아래는 학습 전용 서버에서 XA 하나의 상태를 따라가는 예제입니다. 운영 서버에서 PREPARED 트랜잭션을 남겨 두면 잠금이 계속 유지되므로 실행하지 않습니다.

학습 전용 서버: XA 한 가지의 상태 추적
CREATE DATABASE IF NOT EXISTS cs_xa_lab;
DROP TABLE IF EXISTS cs_xa_lab.account;
CREATE TABLE cs_xa_lab.account (
  id INT PRIMARY KEY,
  balance INT NOT NULL
) ENGINE=InnoDB;
INSERT INTO cs_xa_lab.account VALUES (1, 100);

XA START 'cs_order_7';
UPDATE cs_xa_lab.account SET balance = 70 WHERE id = 1;
SELECT balance FROM cs_xa_lab.account WHERE id = 1;
XA END 'cs_order_7';
XA PREPARE 'cs_order_7';
XA RECOVER CONVERT XID;
balance
70

formatID | gtrid_length | bqual_length | data
1        | 10           | 0            | 0x63735F6F726465725F37

XA 트랜잭션 안의 같은 연결은 바꾼 값 70을 봅니다. PREPARE 뒤 XA RECOVER는 이 XID를 PREPARED 목록에 보여 주며, CONVERT XID는 data를 16진수로 바꾸어 cs_order_7의 열 바이트를 0x63735F...로 나타냅니다. 형식 ID는 지정하지 않았을 때의 1이고, 분기 한정자(bqual)는 비어 있어 길이가 0입니다.

기본값인 xa_detach_on_prepare=ON에서는 PREPARE 뒤에 다른 연결도 이 XID의 결정을 적용할 수 있습니다. 다른 연결의 일반적인 일관된 읽기는 아직 확정되지 않았으므로 원래 값 100을 보고, 같은 행의 잠금 읽기나 UPDATE는 준비된 트랜잭션의 잠금 때문에 기다릴 수 있습니다. 준비가 끝났다는 것과 변경이 공개되었다는 것은 같지 않습니다(XA 트랜잭션의 상태).

조정자가 COMMIT을 영속적으로 기록한 경우에만 XA COMMIT 'cs_order_7';로 확정하고, ABORT를 기록했다면 XA ROLLBACK 'cs_order_7';로 취소합니다. 같은 XID에 두 명령을 차례로 실행하지 않습니다. 한 번 최종 적용한 XID는 XA RECOVER에서 사라지고, 같은 COMMIT을 다시 보내면 ERROR 1397 (XAE04): XAER_NOTA: Unknown XID가 날 수 있는데, 이 오류만으로는 과거에 어떤 결정이 적용되었는지 알 수 없으므로 결정 로그와 업무의 결과를 대조합니다.

더 깊이: XA 트랜잭션과 바이너리 로그, 복제

MySQL에서 XA를 지원하는 저장 엔진은 InnoDB뿐입니다. MySQL 8.4 매뉴얼에 따르면 XA 트랜잭션은 바이너리 로그에 두 부분으로 나뉘어 기록되는데, XA PREPARE까지의 변경이 첫 GTID로, 이어지는 XA COMMIT이나 XA ROLLBACK이 둘째 GTID로 기록됩니다. 그래서 PREPARED 상태는 명시적인 XA COMMIT이나 XA ROLLBACK이 올 때까지 유지되고, 복제와도 어긋나지 않습니다. 복제 서버에서도 XA 트랜잭션은 준비되는 즉시 복제 적용 스레드에서 분리되어, 그 서버의 어느 스레드든 결정을 적용할 수 있습니다.

복제와 함께 쓸 때의 제약도 있습니다. 복제 필터나 바이너리 로그 필터를 XA 트랜잭션과 함께 쓰는 것은 지원하지 않는데, 테이블을 거르다 보면 복제 서버에서 XA 트랜잭션이 비어 버릴 수 있고 빈 XA 트랜잭션은 지원하지 않기 때문입니다. XA 트랜잭션은 문장 기반 복제에서 안전하지 않은 것으로 다루어져, binlog_format=STATEMENT에서는 경고가 나고 MIXED나 ROW에서는 안의 DML이 행 기반으로 기록됩니다.

XID에 관한 MySQL만의 규칙도 있습니다. 한 전역 트랜잭션 안에서도 XA 트랜잭션마다 bqual이 달라야 하는데, 매뉴얼은 이것이 XA 명세가 아니라 지금 MySQL 구현의 제한이라고 밝힙니다. 또 같은 XID로 XA 트랜잭션을 차례로 실행하다가 XA COMMIT ... ONE PHASE를 처리하는 도중에 중단되면, 바이너리 로그와 저장 엔진의 상태를 다시 맞추지 못할 수 있다는 알려진 문제가 있습니다. 전역 트랜잭션마다 다시 쓰지 않는 고유한 XID를 만드는 편이 안전한 이유입니다.

엔진을 종료한 뒤의 PREPARED 복구 실험

운영 서버의 서비스를 종료해 실험하지 않습니다. 저장소의 docs/cs-textbooks/db-os-labs/mysql_xa_crash.py는 Windows용 MySQL 8.4 바이너리로 새 데이터 디렉터리와 고유한 이름의 named pipe를 만듭니다. skip-networking과 mysqlx OFF로 TCP 포트를 열지 않고, --no-monitor와 자체 PID 파일을 대조하며, 이미 있는 데이터 폴더는 받지 않고 새 결과 폴더만 만듭니다. Python 3의 표준 라이브러리와 mysql.exe, mysqld.exe가 필요합니다.

저장소 루트: 준비된 MySQL bin 경로와 새 결과 경로
python docs/cs-textbooks/db-os-labs/mysql_xa_crash.py --bin-dir "C:\확인한경로\mysql-8.4\bin" --out ".\xa-lab-new"

경로는 자신의 바이너리 위치로 바꿉니다. named pipe에 접근하다 클라이언트 오류 2017로 거부되면, 서버의 시작과 pipe의 이름, Windows의 실행 자격부터 확인합니다. 관리자와 클라이언트의 실행 자격이 다른 환경이라고 인증 플러그인을 바꾸거나 기존 서비스의 권한을 넓혀 실험하지는 않습니다.

실험은 MySQL 참여자 하나에 서로 다른 전역 XID 두 개를 준비합니다. deep_commit은 계정 1을 100에서 70으로, deep_abort는 계정 2를 100에서 50으로 바꿉니다. 바깥의 제어 파일에 XID마다 따로 COMMIT과 ABORT의 결정을 저장하고 flush와 fsync를 마친 뒤, 스크립트가 띄운 mysqld 프로세스만 강제로 종료합니다. 두 XID는 같은 전역 작업의 서로 다른 투표가 아니라 독립된 두 작업입니다.

확인 순서예상
PREPARE 뒤의 XA RECOVER두 XID가 있다
다른 연결의 일반 읽기계정 1과 2가 모두 100이다
계정 1의 잠금 읽기, 대기 시간 1초준비된 잠금 때문에 ERROR 1205로 끝난다
엔진을 종료하고 같은 데이터 디렉터리로 재시작한 뒤의 XA RECOVER두 PREPARED XID가 그대로 있다
바깥에 기록한 COMMIT과 ABORT의 적용계정 1은 70, 계정 2는 100이다
마지막 XA RECOVER목록이 비어 있다

종료 직전의 before-engine-kill.json에는 PID와 데이터 디렉터리, 입력, 따로 계산한 예상과 확인한 값이 남고, coordinator-decisions.json이 결정의 기록입니다. 결과 JSON과 재시작의 로그를 함께 읽으며, 프로세스가 죽은 뒤 PREPARED가 일반적인 미커밋 트랜잭션처럼 모두 취소된다고 가정하지 않습니다(InnoDB의 장애 복구).

확인한 것과 남은 범위

이 실험은 실제 MySQL XA의 준비 상태가 엔진 프로세스의 장애를 견디고 복구된다는 것을 확인합니다. 조정자의 결정은 같은 컴퓨터의 영속 파일로 나타낼 뿐, 실제 네트워크 메시지나 여러 서버의 조정자를 실행하지 않습니다. 두 데이터베이스의 원자적 커밋과 네트워크의 분할, 전원이나 저장 장치의 손실, 복제된 조정자의 복구는 따로 검증할 대상입니다.

데이터베이스 하나의 변경과 완료 기록을 함께 묶는 경우라면, 운영체제 교재의 지속 저장과 안전한 재시도처럼 로컬 트랜잭션만으로 해결할 수 있고, 이를 2PC라고 부를 필요도 없습니다.

재시작한 뒤의 질문은 "프로세스가 다시 떴는가"에서 끝나지 않습니다. 남은 PREPARED는 무엇인지, 최종 결정의 영속적인 기록은 어디에 있는지, 결정을 모르는 동안 어떤 작업이 막히는지까지 추적해야 합니다.

정리

  • 2PC는 참여자의 준비 투표와 조정자의 최종 결정을 나누며, READY는 커밋이 아니라 준비를 저장했다는 뜻입니다.
  • 조정자는 결정을 보내기 전에 저장해야 하고, 저장한 결정은 바꾸지 않습니다.
  • 결정을 모르는 PREPARED 참여자는 시간 초과만으로 임의로 커밋하거나 취소하지 않고 기다립니다.
  • MySQL의 XA PREPARE는 PREPARED 상태를 만들고, 그 상태는 엔진의 재시작 뒤에도 XA RECOVER에 남습니다.
  • 준비가 끝난 변경은 아직 공개되지 않았지만 잠금은 유지하므로, PREPARED를 오래 남기지 않습니다.

연습 문제

1. 응답이 끊긴 PREPARED 참여자

참여자 A와 B가 모두 PREPARED인데 조정자의 응답이 끊겼습니다. A는 시간 초과만 보고 ROLLBACK해도 될까요? 재시작한 조정자의 파일에 COMMIT이 있고 B만 이미 확정했다면, 다음에 무엇을 전달해야 하나요?

해설 보기

PREPARED인 A가 시간 초과만으로 임의로 취소하면, B가 이미 커밋한 경우 원자성이 깨집니다. A가 보지 못한 결정이 B에게는 도착했을 수 있기 때문이며, 결정의 기록을 조회할 수 없다면 A는 결정을 모르는 채 기다릴 수밖에 없습니다.

복구한 조정자는 저장한 COMMIT을 A에게 다시 보내고, B가 완료한 상태와 대조합니다. 같은 XID를 ABORT로 바꾸지 않으며, B에 다시 COMMIT을 보내 1397이 돌아와도 그것을 실패로 해석하지 않고 B의 업무 결과로 확정 여부를 확인합니다.

2. 준비된 변경이 보이는가

본문의 XA 예제에서 XA PREPARE까지 마친 뒤, 다른 연결이 SELECT balance FROM cs_xa_lab.account WHERE id = 1;과 SELECT balance FROM cs_xa_lab.account WHERE id = 1 FOR UPDATE;를 차례로 실행하면 각각 어떻게 될까요?

해설 보기

첫 SELECT는 일관된 읽기이므로, 아직 확정되지 않은 70이 아니라 원래 값 100을 돌려줍니다. 두 번째의 FOR UPDATE는 잠금 읽기이므로, 준비된 트랜잭션이 잡고 있는 행 잠금을 기다리고, innodb_lock_wait_timeout이 지나면 ERROR 1205로 끝납니다.

PREPARED는 변경을 공개하지 않으면서도 잠금을 유지하므로, 결정을 오래 미루면 같은 행을 쓰려는 다른 작업이 막힙니다. 실습을 마쳤다면 조정자의 결정에 맞추어 XA COMMIT이나 XA ROLLBACK으로 끝냅니다.

이것으로 데이터베이스 교재의 본문을 마칩니다. 처음부터 다시 훑어보고 싶다면 데이터베이스 목차로 돌아가, 각 장의 개념이 서로 어떻게 이어지는지 확인해 보세요.