모놀리식 애플리케이션에서는 하나의 로컬 DB 트랜잭션으로 여러 하위 도메인의 데이터를 ACID 하게 변경 가능하다.
ex) 주식거래 앱
⇒ 사용자가 했던 주문이 체결되었을 때 하나의 로컬 트랜잭션으로 사용자가 보유한 현금을 줄이고, 주문 상태를 “체결됨” 상태로 변경하며, 보유 주식을 늘리는 것이 가능하다.
MSA 에서는 각 서비스가 DB 를 가지고, 기존에 하나의 DB 에 모두 저장되었던 데이터를 각 서비스에 나뉘어져 관리된다.
ex) 주식거래 앱이 주문 서비스, 주식 잔고 서비스, 회계 서비스 등으로 구성된다.
각 서비스에서 실행되는 여러 로컬 트랜잭션을 하나의 글로벌 트랜잭션으로 묶어 ACID 하게 할 필요가 있다.
💡 분산 트랜잭션 없이 매번 HTTP/gRPC 등으로 다른 서비스에 요청하여 데이터를 변경시키면 되는 것이 아닌가?
⇒ 데이터 변경이 Atomic 하지 않기 때문에 서버나 인프라 오류 등 발생 시 데이터 정합성이 깨질 수 있고 , 트랜잭션 컨텍스트도 유실 될 가능성이 있다.
분산 트랜잭션을 처리하는 방안으로는 몇가지가 있는데 아래 정리를 해보겠다.
- 2PC
- Saga 패턴
- ACD 패턴
- 코레오그래피(Choreography) Saga
- 오케스트레이션(Orchestration) Saga
- Transactional Messaging
- Transaction Outbox
- CDC(Change Data Capture)
- Event Sourcing
- CDC 및 Saga 패턴의 한계
- 이벤트 소싱패턴에서 이벤트 전달
- 장점
- 단점
- Event Sourcing 과 CDC 방식의 차이
1. 2PC
2 Phase Commit 의 약자로 간단하게 말하자면...
분산 트랜잭션을 관장하는 코디네이터(트랜잭션 관리 주체가 DBMS)가
- 분산 트랜잭션에 참여하는 컴포넌트에게 로컬 트랜잭션 커밋을 요청하는 1단계와
- 실제로 로컬 트랜잭션을 커밋하는 2단계로 이루어진 방식이다.
2PC는 단점이 있는데 가용성이 낮고, 트랜잭션 참여하는 컴포넌트들에게 강한 의존성을 갖는다.
참고로, NoSQL DB(Mongo DB 등)와 메시지 브로커(RabbitMQ, 카프카 등)는 분산 트랜잭션을 지원하지 않기때문에 2PC 에 사용할 수 없음.
💡 2PC 의 단점
⇒ 가용성이 낮다.
분산 트랜잭션을 성공적으로 수행하기 위해서는 해당 트랜잭션에 포함 된 모든 컴포넌트가 성공적으로 실행 중이어야 한다. 만약, 한 컴포넌트라도 실행중이지 않다면 트랜잭션은 성공할 수 없다.
2. Saga 패턴
마이크로서비스 아키텍처에서 분산 트랜잭션 없이 데이터 일관성을 유지하는 매커니즘이다.
트랜잭션에 참여하는 컴포넌트들 간의 결합을 느슨하게 하여, 높은 가용성을 갖는다.
Saga 패턴에서는 각 로컬 트랜잭션이 비동기 메시지를 통해 순차적으로 커밋이 된다.
만약, 중간에 문제가 발생하면 지금까지와 반대의 순서로 보상 트랜잭션을 실행한다.
💡 2PC 보다 좋은 가용성
2PC 의 가용성은 모든 컴포넌트가 구동 중이어야 하므로… 각 컴포넌트의 가용성 곱이 된다.
반면, Saga 는 어떤 트랜잭션 참여자(서비스)가 다운 되었을지라도 나중에 재구동 되면 멈춰진 부분부터 시작 가능하다.
보통 Saga 에 참여하는 컴포넌트는 메시지 브로커를 통해 통신한다.
⇒ 높은 가용성
ACD 트랜잭션
Saga 패턴은 ACID 에서 I(Isolation)가 빠진 ACD 트랜잭션이다.
즉, 여러개의 트랜잭션이 동시에 실행 될 경우 격리가 되지 않아 서로에게 영향을 미칠 수 있다.
대표적으로 한 트랜잭션이 변경한 데이터를 다른 트랜잭션이 덮어쓰는 lost updates 나 한 트랜잭션이 실행되는 도중에 다른 트랜잭션이 변경한 데이터를 읽는 dirty reads 와 같은 것들이 있다.
💡 비격리로 인해 발생하는 문제를 해결하기 위해 높은 일관성을 요구하는 데이터만 2PC 방식을 혼용하여 사용할 수도 있다.
코레오그래피(Choreography) Saga
의사 결정과 순서화를 사가 참여자에게 맡긴다. 사가 참여자는 주로 이벤트 교환 방식으로 통신한다.
- 장점
- 단순함
=> 비즈니스 객체를 생성, 수정, 삭제할때 이벤트를 발행한다. - 느슨한 결합
=> saga 참여자는 이벤트를 구독할 뿐 직접 알지는 못한다.
- 단순함
- 단점
- 이해하기 어려움
=> 어느 한곳에 정의한 것이 아니라 여러 서비스에 구현 로직이 흩어져있다. 그래서 어떤 saga 가 어떻게 작동하는지 개발자가 이해하기 어려운 편이다. - 서비스 간 순환 의존성
=> 참여자가 서로 이벤트를 구독하는 특성상, 순환 의존성이 발생할 수 있다. - Step 추가 시, 혼란스러울 수 있다.
- 이해하기 어려움
오케스트레이션(Orchestration) Saga
참여자가 할 일을 알려주는 오케스트레이터 클래스를 정의한다.
saga 오케스트레이터는 커맨드 및 비동기 응답 상호작용을 하며 참여자와 소통한다.
saga 참여자가 작업을 마치고 응답메시지를 오케스트레이터에 주면, 오케스트레이터는 응답 처리 후 다음 saga 단계 수행할 참여자를 결정한다.
- 장점
- 의존 관계 단순화
=> 참여자는 오케스트레이터를 호출하지 않는다. 즉, 순환 의존성이 발생하지 않는다. - 낮은 결합도
=> 참여자는 오케스트레이터의 지시에 따라 API 만 구현하면 된다. 다른 참여자가 발행하는 이벤트는 몰라도 된다. - 비즈니스 로직 단순화
=> 결합도가 낮기 때문에 비즈니스 로직은 단순해진다.
- 의존 관계 단순화
- 단점
- [주의] 너무 많은 중앙화는 똑똑한 오케스트레이터 하나가 깡통 서비스에 일일히 지시하는 형태가 됨.
3. Transactional Messaging
💡 Saga 패턴이나 도메인 이벤트 발행 시에는 일관성을 위해 트랜잭셔날 메시징을 고려해야한다.
DB 업데이트와 메시지 전송 이 한 트랜잭션으로 묶여야 한다.
ex) DB 는 업데이트 후 메시지는 아직 전송되지 않은 상태에서 서비스가 중단되면 일관성을 지킬 수 없다.
⇒ DB 던 메시지 브로커던 둘 중 하나에만 쓰기를 수행해야 한다.
- 메시지 브로커에만 쓰기를 하는 방식 (비추)
메시지 브로커에 데이터 변경 이벤트를 쓰고, 서버가 이를 구독하여 DB 를 업데이트함
⇒ 메시지 발행과 서버의 구독 사이에 지연시간이 존재하기 때문에 유저는 변경 요청한 데이터를 즉시 볼 수 없는 문제가 있음. 그래서 DB 에만 쓰기를 하는 방식이 더 선호됨. - DB 에만 쓰기를 하는 방식 (선호됨)
DB 테이블을 임시 메시지 큐로 활용하는 방안이다. 이를 Transactional Outbox 패턴이라고 한다.
Transaction Outbox
RDBMS 라면.. OUTBOX 라는 테이블을 만들고 생성/수정/삭제 하는 DB 트랜잭션의 일부로 OUTBOX 테이블에 메시지를 삽입한다. (이부분은 로컬 트랜잭션 보장됨)
메시지 릴레이는 OUTBOX 테이블을 읽어 메시지 브로커에 메시지를 발행한다.
CDC(Change Data Capture)
Saga 의 트랜잭셔널 메시징을 Outbox 패턴을 사용하기로 결정했다면... 새로운 이벤트를 메시지 브로커에 발행하기 위하여 CDC 를 함께 사용한다.
(메시지 릴레이) 아래 두 가지 방식을 사용하여 구현할 수 있다.
- polling 방식
주기적으로 DB 쿼리를 하는 방식.
구현이 간단한 대신 DB에 불필요한 부담을 줄수있다. 또한, 데이터 변경을 포착하는데 delay 가 있을 수 있다. - transaction log tailing 방식 (추천)
DB 트랜잭션 로그를 테일링하는 방식.
구현이 복잡하고 별도의 운영 프로세스가 늘어나지만...
DB 에 불필요한 부담을 주지않고 데이터 변경 이벤트를 실시간으로 메시지 브로커에 보낼 수 있기때문에 장기적인 관점에서 polling 방식보다 좋다.
4. Event Sourcing
비즈니스 로직 자체를 이벤트 기반으로 구현하고, 순차적으로 발생하는 이벤트를 DB 에 저장하는 기법이다.
CDC 및 Saga 패턴의 한계
💡 데이터 일관성을 위해 Saga 패턴 등을 통해 트랜잭션 유지하는 방법들이 있지만, 이벤트 전달 결과만을 DB 에 저장하는 특성상 오류에 대한 완벽한 보장을 하기는 어렵다. 또한, 이벤트 흐름의 추적도 어렵다.
다음과 같은 단점과 한계를 발견할 수 있다.
- 임피던스 불일치 (Impedance Mismatches): 데이터베이스 모델과 프로그래밍 언어 사이의 차이때문에 발생
- 애그리거트 로그의 부재: 수행 이력을 저장할 필요가 있다.
위와 같은 단점을 보완하기 나온 것이 이벤트 소싱 패턴이다.
기존에는 이벤트를 직접 전달하는 방식이었다면… 이벤트 소싱은 수행되는 모든 이벤트를 이벤트 스토어에서 관리해 줌으로써 수행 로그가 되고, 발행 조건이 된다.
예를들면 아래와 같은 형태의 이벤트 저장소가 존재하게 된다.
이벤트 소싱 패턴에서 이벤트 전달
이벤트 저장소의 변경을 확인 후, 서비스로 이벤트를 전달시켜 주어야 된다.
이 역할을 이벤트 핸들러가 수행한다. (저장소 변경 확인 및 이벤트 전달)
이벤트 전달은 위의 CDC 에서와 같이 폴링 방식과 로그 테일링 방식을 사용한다. 그럼 아래와 같은 구조로 이벤트가 전달된다.
장점
- 상태 변화시 확실한 이벤트 발생: DB 에 의한 이벤트 관리로, 영속성을 보장하고 확실하게 이벤트를 발생시킨다.
- 이벤트 처리: 이벤트 수행 실패 시, 재발행 or 보상 이벤트 수행
- 애그리거트 로그 보존: 이벤트 전체 이력이 보존 → 임시 쿼리를 통해 상태변화 확인이 가능
- 타임머신: 수행 된 이벤트 롤백 가능
단점
- 이벤트 핸들러의 복잡성: 중복 이벤트를 고려해야함 → 멱등성이 고려된 비즈니스 로직 or 중복 방지 로직
- 이벤트 확장의 어려움: 이벤트는 영구 저장되야함 → 이벤트 변경 및 확장에 대응이 힘듬.
- 데이터 삭제의 어려움: 이벤트는 영구 저장되야함 → 이벤트 스토어를 거치지않고 임의의 데이터 삭제 = 로그 불일치
오케스트레이션 Saga + 이벤트 소싱 패턴
💡 Event Sourcing 패턴은 Write 에 집중된 전략이다.
이를 다시 불러오기 위해서는 수많은 절차(이벤트)들이 필요하며 그 부분을 검색하는 것은 REST API 로는 매우 시간적 소모가 크다. 그래서 Event Sourcing 을 위해서는 CQRS 가 필수이다.
하지만, CQRS 를 위해서 Event Sourcing 이 필수는 아니다.
5. Event Sourcing 과 CDC 방식의 차이
💡 CDC(Change Data Capture) 는 데이터의 변경 정보를 DB 에 저장한다.
CQRS 에서는 커맨드 실행 시, DB 변경이 완료되고 관련하여 이벤트를 발행한다.
이때 발행하는 이벤트를 이벤트 스토어(저장소 혹은 DB)에 저장하는 것이 이벤트 소싱이다.
참고
https://sgc109.github.io/2021/10/22/about-msa/
https://sarc.io/index.php/cloud/2245-eventuate
https://microservices.io/patterns/data/transactional-outbox.html
https://microservices.io/patterns/data/saga.html
'MSA > MSA 분산 트랜잭션' 카테고리의 다른 글
MSA 트랜잭션 개선 경험 및 세미나 (0) | 2022.10.10 |
---|