19. 2단계 커밋 코디네이터 장애와 in-doubt 블로킹 (C#)
난이도 최상해설 — 2단계 커밋 코디네이터 장애와 in-doubt 블로킹 (C#)
난이도: 최상
요약
2PC 코디네이터가 커밋 결정을 내구성 있게(durable) 기록하지 않고, 참여자에게는 타임아웃/종결 프로토콜이 없다. 그래서 (1) 참여자가 Prepare 후 찬성한 상태에서 코디네이터가 죽으면 참여자는 커밋/취소를 알 수 없어 자원을 무기한 잠근 채 멈춘다(in-doubt 블로킹). (2) 코디네이터가 (B)~(C) 사이(한쪽만 Commit)에서 죽고 재시작하면, 결정 로그가 없어 자신이 무엇을 결정했는지 몰라 한쪽만 반영된 불일치가 영구화된다.
문제점
-
[내구적 결정 로그 부재] 코디네이터 크래시 → 결정 유실/불일치
- 증상: 두 Prepare 가 찬성(
(A))한 뒤(B)직전/직후에 코디네이터가 죽는다. 재시작해도 "커밋하기로 했었나?"를 알 방법이 없다(로그 없음)._a는 Commit 됐는데_b는 안 된 상태로 방치. - 재현:
(B)후(C)전에 코디네이터 프로세스 강제 종료 → 아이템이 한 샤드에서만 이동(복제 또는 소실). - 근본원인: 2PC 의 핵심 규칙 — 코디네이터는 어떤 Commit 도 보내기 전에 "commit" 결정을 WAL 에 durable 하게 써야 한다. 그래야 재시작 후 재전송으로 완결할 수 있다. 그 로그가 없다.
- 증상: 두 Prepare 가 찬성(
-
[참여자 종결 프로토콜 부재] Prepare 후 무한 대기 → 자원 영구 잠금
- 증상: 참여자는 찬성표를 던진 순간부터 커밋/취소가 올 때까지 자원을 잠근다. 코디네이터가 사라지면 참여자는 혼자서는 안전하게 결정할 수 없다(코디네이터가 이미 커밋했을 수도, 취소했을 수도 있어 임의 결정은 불일치 위험). 잠금이 영구화되어 해당 아이템/캐릭터가 먹통이 된다.
- 근본원인: 참여자에게 타임아웃 후 코디네이터/동료에게 결정을 물어보는 협력적 종결(cooperative termination) 프로토콜과, 트랜잭션 상태의 내구적 보존이 없다.
-
[근본 한계] 2PC 는 본질적으로 블로킹
- 찬성 후 코디네이터가 도달 불가면 참여자는 안전하게 진행/취소를 결정할 수 없다. 이는 구현 버그가 아니라 2PC 프로토콜의 근본 성질이다.
수정안
2PC 를 제대로 하려면 내구적 로깅 + 복구 + 종결 프로토콜을 갖춰야 한다.
- 코디네이터: 모든 Prepare 찬성 확인 후, Commit 을 하나라도 보내기 전에
decision=commit을 durable 로그에 쓴다(force write). 재시작 시 로그를 읽어 미완결 트랜잭션의 Commit(또는 Abort)을 멱등하게 재전송한다. 참여자 Commit/Abort 는 재전송에 대비해 멱등해야 한다. - 참여자: Prepare 시 트랜잭션 상태(잠금 대상, 투표)를 durable 로 남긴다. 결정 대기가 타임아웃되면 코디네이터(또는 결정 로그/동료 참여자)에게 상태를 조회해 종결한다. 스스로 커밋/취소를 임의 결정하지 않는다.
public async Task<bool> Execute(TxOp op)
{
if (!await _a.Prepare(op) || !await _b.Prepare(op)) { await AbortAll(op); return false; }
await _log.WriteDecisionDurably(op.TxId, Decision.Commit); // 커밋 전 결정을 먼저 내구 기록
await CommitAllWithRetry(op); // 멱등 재시도 — 크래시 후 재시작에서도 완결
await _log.MarkComplete(op.TxId);
return true;
}
// 재시작 시: 로그의 미완결 트랜잭션에 대해 결정대로 Commit/Abort 재전송
더 나은 설계
- 분산 트랜잭션을 피한다(사가): 크로스-샤드 교환을 2PC 대신 보상 트랜잭션 기반 사가로 설계 — 각 단계는 로컬 커밋 + 멱등, 실패 시 보상. 원자성 대신 최종 일관성을 택해 블로킹을 없앤다(이 카테고리의 사가 문제와 짝). 게임 아이템 교환에선 보통 이쪽이 실용적.
- 비블로킹 합의 커밋: Paxos Commit/3PC 로 코디네이터 단일 장애점을 제거하면 블로킹을 완화할 수 있으나 복잡·고비용.
- 잠금 최소화: Prepare 잠금 시간을 짧게, 리스(lease) 기반으로 자동 만료 + 안전한 재해소(단, 임의 취소가 불일치를 안 만들도록 결정 로그와 연동).
면접 포인트
- 2PC 는 원자성을 주지만 가용성을 희생한다(블로킹 프로토콜). 찬성 후 코디네이터가 죽으면 참여자는 혼자 결정할 수 없어 자원이 잠긴다 — 이는 근본 한계다.
- 올바른 2PC 의 필수 요소: 커밋 전 코디네이터의 내구적 결정 로그, 멱등 재전송 복구, 참여자의 내구 상태 + 종결 프로토콜.
- 실무에선 크로스-샤드/서비스 경계에서 2PC 대신 사가+멱등+보상으로 가는 이유(블로킹 회피, 운영 단순성)와 그 대가(최종 일관성)를 설명할 수 있어야 한다.
해설 — 2단계 커밋 코디네이터 장애와 in-doubt 블로킹 (C++)
난이도: 최상
요약
2PC 코디네이터가 커밋 결정을 내구성 있게 기록하지 않고, 참여자에게는 타임아웃/종결 프로토콜이 없다. 참여자가 prepare 후 찬성한 상태에서 코디네이터가 죽으면 참여자는 결정을 알 수 없어 자원을 무기한 잠근 채 멈춘다(in-doubt). 코디네이터가 (B)~(C) 사이에서 죽고 재시작하면 결정 로그가 없어 한쪽만 반영된 불일치가 영구화된다. C++ 에서는 잠금을 RAII 로 잡았어도 프로세스가 죽으면 해제 코드가 돌지 않아, 재시작 후 상태 복구가 없으면 잠금이 되살아나지 못하거나 영구 점유된다.
문제점
-
[내구적 결정 로그 부재] 코디네이터 크래시 → 결정 유실/불일치
- 증상: 두 prepare 찬성(
(A)) 뒤(B)직전/직후 코디네이터가 죽는다. 재시작해도 "commit 하기로 했었나?"를 모른다.a_는 commit,b_는 미commit 상태로 방치 → 아이템이 한 샤드에서만 이동(복제/소실). - 재현:
(B)후(C)전 프로세스 강제 종료. - 근본원인: 코디네이터는 어떤 commit 도 보내기 전에 "commit" 결정을 durable(WAL, fsync) 로 써야 재시작 후 재전송으로 완결할 수 있다. 그 로그가 없다.
- 증상: 두 prepare 찬성(
-
[참여자 종결 프로토콜 부재] prepare 후 무한 대기 → 자원 영구 잠금
- 증상: 참여자는 찬성 순간부터 자원을 잠근다. 코디네이터가 사라지면 혼자 안전하게 결정할 수 없다(이미 commit 됐을 수도, abort 됐을 수도). 잠금 영구화로 해당 아이템/캐릭터가 먹통.
- 근본원인: 참여자에게 타임아웃 후 협력적 종결 프로토콜(코디네이터/동료에게 결정 조회)과 트랜잭션 상태의 내구적 보존이 없다. C++ 프로세스 크래시 시 RAII 잠금 해제도 실행되지 않으므로, 잠금 상태 자체를 외부 저장소에 durable 로 남겨야 재해소가 가능하다.
-
[근본 한계] 2PC 는 본질적으로 블로킹 — 찬성 후 코디네이터 도달 불가면 참여자는 안전하게 결정할 수 없다.
수정안
- 코디네이터: 모든 prepare 찬성 후, commit 을 하나라도 보내기 전에
decision=commit을 durable 로그에 force write. 재시작 시 미완결 트랜잭션의 commit/abort 를 멱등 재전송. 참여자 commit/abort 는 멱등해야 함. - 참여자: prepare 시 트랜잭션 상태(잠금 대상·투표)를 durable 로 남긴다(RAII 잠금만으로는 크래시에 못 버팀). 대기 타임아웃 시 코디네이터/결정 로그를 조회해 종결. 임의 결정 금지.
bool execute(const TxOp& op) {
if (!a_.prepare(op) || !b_.prepare(op)) { a_.abort(op); b_.abort(op); return false; }
log_.writeDecisionDurable(op.txId, Decision::Commit); // commit 전송 전에 결정을 먼저 내구 기록
commitAllWithRetry(op); // 멱등 재시도 — 크래시/재시작에서도 완결
log_.markComplete(op.txId);
return true;
}
// 재시작 훅: 로그의 미완결 트랜잭션에 대해 결정대로 commit/abort 재전송
더 나은 설계
- 분산 트랜잭션 회피(사가): 크로스-샤드 교환을 로컬 커밋 + 멱등 + 보상 트랜잭션으로 구성. 원자성 대신 최종 일관성을 택해 블로킹 제거(이 카테고리 사가 문제와 짝). 게임 아이템 교환엔 보통 실용적.
- 비블로킹 커밋: Paxos Commit/3PC 로 단일 장애점 완화(복잡·고비용).
- 잠금 최소화 + 리스: prepare 잠금을 리스 기반 자동 만료로 두되, 결정 로그와 연동해 임의 만료가 불일치를 만들지 않게 한다.
면접 포인트
- 2PC 는 원자성을 주지만 가용성을 희생(블로킹). 찬성 후 코디네이터가 죽으면 참여자는 혼자 결정 못 해 자원이 잠긴다 — 근본 한계.
- 올바른 2PC 필수 요소: 커밋 전 코디네이터 내구 결정 로그 + 멱등 재전송 복구 + 참여자 내구 상태·종결 프로토콜. C++ 은 RAII 잠금이 크래시를 못 버티므로 상태를 외부에 durable 로 남겨야 함을 짚는다.
- 실무에선 크로스-샤드 경계에서 2PC 대신 사가+멱등+보상으로 가는 이유와 대가(최종 일관성)를 설명할 수 있어야 한다.