MicroService Architecture ‐ 2PC - thought-corner/Backend-PlayGround GitHub Wiki
MicroService Architecture - 2PC
- 2PC란, Two-Phase Commit Protocol의 약자로 분산 시스템에서 트랜잭션의 원자성을 보장하기 위해 사용하는 프로토콜이다.
- 트랜잭션을 다음과 같이 두 단계로 나누어 처리한다.
- Prepare 단계 : 트랜잭션 매니저가 참여자에게 작업 준비가 가능한지 묻는다.
- Commit 단게 : Prepare 단계에서 모든 참여자가 작업이 가능하다고 응답하면 실제로 커밋을 수행한다.
- 대표적인 구현으로는 XA 트랜잭션이 존재한다.
2PC의 장·단점
- 강력한 정합성을 보장한다.
- 사용하는 데이터베이스가 XA를 지원한다면 구현 난이도는 낮다.
- 그러나 사용하는 데이터베이스가 XA를 지원하지 않는다면 구현이 어렵다.
- prepare 단계 이후 commit이 완료될 때까지 Lock을 유지하기 때문에 가용성이 낮다.
- 장애 복구 시 수동으로 개입해 해결을 해야 해서 실용성이 낮다.
2PC의 한계
1. 자원 잠금에 따른 낮은 처리량
- Prepare부터 Commit까지의 구간 내내 참여자들은 락을 붙잡고 있다.
- 분산 환경이라 이 구간이 네트워크 왕복만큼 길어지게 된다.
- 단일 DB 트랜잭션이라면 수 ms에 끝날 일이 2PC에선 락 보유 시간이 몇 배로 늘어나게 된다.
- 락을 오래 잡을수록 같은 자원을 노리는 다른 트랜잭션은 더 오래 대기하게 되고 동시성·throughput이 저하됩니다.
2. 완벽한 원자성은 아니다.
- 2PC가 모든 실패를 막아주는 것은 아니다.
- Prepare는 전부 통과했는데 Commit 명령을 보내는 도중 문제가 생겨 일부 참여자만 Commit을 받고 나머지는 받지 못하는 경우가 생긴다.
- 이렇게 되면 일시적으로 시스템이 불일치 상태가 되고 복구 로그와 재전송으로 수습해야 한다.
- 이 빈틈을 줄이고자 3PC가 생겼는데 이 3PC도 네트워크 분단 상황에선 안전하지 않기 때문에 실무에서 거의 사용되지 않는다.
3. MSA·현대 인프라와의 부정합
- 2PC의 대표 구현인 XA는 모든 참여자가 XA 트랜잭션을 지원해야 작동한다.
- 그런데 현대 MSA의 구성요소 상당수가 이를 지원하지 않거나 권장하지 않는다.
- REST/gRPC와 같은 HTTP API엔 XA 개념이 없고, Kafka/RabbitMQ와 같은 메시지 브로커, 대부분의 NoSQL, 외부 PG사 결제 API는 2PC에 참여하지 못한다.
- 서비스마다 DB가 분리된 MSA에선 트랜잭션이 여러 DB·프로토콜에 걸치는데, 2PC는 이 이질적인 경계를 묶을 수단이 되지 못한다.
1. Phase 1(Prepare)
- 트랜잭션 매니저(TM, 코디네이터)가 각 참여자(Resource Manager, 자기 DB를 소유한 서비스)에게
prepare요청을 보낸다. - 요청을 받은 RM은 단순히 가능한가만 답하는 것이 아니라 실제 준비 작업을 수행한다.
- 트랜잭션의 모든 변경을 redo/undo 로그에 기록하고 디스크에 강제 플러시해 영속화한다.
- 관련 락을 획득해 계속 보유한다.
- "prepared" 상태를 로그에 남긴다. 이 때부터 "commit"하려면 반드시 할 수 있다고 보장한다.
- 준비가 끝나면 "yes"를, 제약 위반 혹은 실패면 "no"를 TM에 응답한다. "no"를 던진 RM은 곧바로 롤백한다.
- "yes"를 응답한 RM은 스스로 commit 혹은 abort도 할 수 없고, 락을 쥔 채 TM의 최종 결정만 기다리는 상태가 된다. 이 점이 2PC의 Blocking 한계가 된다.
2. Phase 2(Commit)
- TM이 1단계 투표를 집계해 모든 참여자가 "yes"면 commit을, 단 한 명이라도 "no"면 abort를 전체에 내려보낸다.
- 최종 결정을 내리기 직전 TM은 자신의 로그에 결정 레코드를 먼저 영속화한다. 이렇게 하는 이유는 TM이 죽었다 살아나도 같은 결정을 재전송하기 위함이다.
- commit을 받은 RM은 prepared 상태를 확정 처리한다. 1단계에서 디스크에 써둔 변경을 visible로 만들고 보유하던 락을 해제한 뒤 TM에 ack를 보낸다.
- 2단계의 commit은 이미 영속화된 것을 확정하고 락을 푸는 마무리 작업이라고 볼 수 있다.
- RM이 prepared인 상태에서 결정 메시지를 받기 전에 TM이 죽게 되면 RM은 commit인지 abort인지 알 방법이 없어 락을 쥔 채 무한 대기한다. commit을 일부 RM에만 보낸 체 TM이 죽으면 일시적 불일치가 생기고 TM 복구 로그로 재전송해 수습해야 한다.