🖥️ CS/Architecture

[마이크로서비스 아키텍처 구축] 6. 분산 트랜잭션

한국의 메타몽 2024. 11. 2. 18:25

book

해당 글은 마이크로서비스 아키텍처 구축에서 학습한 내용을 다룹니다.



목차

  1. DB 트랜잭션
  2. 분산 트랜잭션 - 2PC
    1. 문제점 1 - 격리성 위배
    2. 문제점 2 - 원자성을 지키기 어려움
  3. 분산 트랜잭션, 그냥 안된다고 하라
  4. 사가 (SAGA) 패턴
    1. 사가 실패 모드
    2. 사가 롤백
    3. 롤백을 줄이기 위해 워크플로 단계를 재정렬
  5. 사가 패턴 구현
    1. 오케스트레이션형 사가
    2. 코레오그래피형 사가
    3. 혼합 (+ 분산 트레이싱)
    4. 무엇을 사용해야 할까?
    5. 사가 vs 분산 트랜잭션
  6. 부록 - DB 분산 트랜잭션


1. DB 트랜잭션


기존 Monolithic에서 한 번에 동시에 2개의 데이터를 변경했을 경우,
1개의 DB로 그림 6-1에서 단일 트랜잭션을 유도할 수 있다.


그러나 MSA 분산환경에서는 그림 6-2 처럼 서로 다른 DB에 데이터가 적재되고, 이는 동일 트랜잭션 내에서 트랜잭션의 ACID 중 A, 즉, 원자성 (Atomicity) 을 보장할 수 없음을 뜻한다.


원자성 (Atomictity) : All or Nothing의 개념으로, 작업 단위를 일부만 수행하지 않는다는 것을 의미

이를 해결하기 위해 분산 트랜잭션을 구현하는 방법에 대해 알아보자.
그러나 분산 트랜잭션 역시 올바른 방향이 아닐 수 있으며, 이와 관련된 문제도 존재한다는 것을 이해해야 한다.



2. 분산 트랜잭션 - 2단계 커밋

2단계 커밋 (Two-Phase Commit) 알고리즘, 줄여서 2PC라고 불린다.

2PC는 투표 단계커밋 단계라는 2단계로 나뉜다. (그래서 2단계 커밋이다)


아래와 같은 상황이 있을 경우,

환불 로직과 관련하여 A 테이블에 유저의 상태값을 CANCELED로, B 테이블에서는 구매 이력 데이터를 제거하라.

대략적으로 2PC는 하기 과정을 거치게 된다.


1. 투표 단계를 거친다. 이는 중앙 조정자(Coordinator)가 트랜잭션에 참가할 모든 워커(worker)에게 연락해서 각각 A 테이블에서 상태값 변경이 가능한지, B 테이블에서 구매 이력 데이터 제거가 가능한지를 묻는다.
2. 모든 워커거 가능하다고 동의하면 커밋 단계를 진행한다. 만약 한 개의 워커라도 동의하지 않으면 커밋 단계를 실행되지 않는다.

만약 동시에 다른 작업이 동일한 A 테이블의 데이터를 변경하려면 어떻게 될까?
다른 트랜잭션의 간섭을 막기 위해, 2PC가 수행되는 시점부터 (성공이건 실패건) 종료되는 시점까지 해당 레코드에 을 걸어야 할 것이다.


때문에 커밋에 찬성하지 않은 워커가 하나라도 존재할 경우, 모든 워커에게 롤백 메세지를 보내 로컬에서 정리하도록 보장함으로써, 워커가 잡고있을지 모르는 을 해제하도록 만들어야 한다.


반대로 모든 워커가 동의하여 커밋 단계로 이동할 경우, 각 레코드들의 값 변경이 일어나고 잠금은 해제된다.



문제점 (1) - 격리성 (Isolation) 위배

두 커밋이 정확히 동시에 발생할 것이라고 보장하기란 어떤 식으로도 불가능하다.
조정자는 커밋 요청을 모든 참가자에게 보내야 하며, 해당 메시지는 서로 다른 시간에 도착해 처리될 수 있다.


워커 사이의 지연 시간이 길수록, 또한 워커의 응답을 처리하는 속도가 느릴수록 불일치 구간은 더 커질 수 있다.


ACID의 정의로 돌아가보면 격리성 (Isolation) 은 트랜잭션 중에 중간 상태를 볼 수 없게 보장하지만, 2단계 커밋에서 이 보장이 사라졌다.
(생각해보자. 2PC를 활용해 분산 트랜잭션에서 A는 성공했고 B는 아직 진행중인데, A의 상태를 우리는 확인이 가능하다. 이는 격리성을 위배하게 된다.)


때문에 2단계 커밋이 작동할 때, 분산 잠금을 조정하는 경우가 많다.
워커는 커밋이 두 번째 단계에서 이뤄질 수 있도록 로컬 자원을 잠글 필요가 있다.
만약 워커가 많을수록 이는 여간 까다로운 일이 아니다.



문제점 (2) - 원자성(Atomicity)를 지키기 어려움

2단계 커밋까지 간다하더라도 실패 모드는 존재한다.
트랜잭션을 진행하려고 투표를 했고, 모든 워커가 동의했다고 가정하자.


그런데 한 워커가 커밋 중 응답하지 않는다면 어떻게 될까?
이런 실패 모드 중 일부는 자동으로 처리되지만, 일부는 운영자가 수동으로 문제를 해결해야 할 수 있다.


더군다나 워커가 많을수록 2단계 커밋에서 더 많은 문제가 발생한다.
특히 잠금의 범위가 크거나 트랜잭션 지속 시간이 긴 경우, 2단계 커밋은 시스템 지연 시간의 원인이 된다.


때문에 2PC는 수명이 매우 짧은 작업(= 작업 소요 시간이 짧다고 고려되는 작업)에만 사용된다.



3. 분산 트랜잭션, 그냥 안된다고 하라

본문에서는 2PC와 같은 분산 트랜잭션은 피해야 한다고 강력히 제안한다.
대신 아래와 같은 방법을 제안한다.


처음부터 DB를 분리하지 말자

정말 원자적이고 처리되어야 하는 데이터가 있을 경우, 해당 데이터를 단일 DB에 남겨두고 단일 서비스(또는 모놀리스)에 해당 상태를 관리하는 기능도 그대로 남겨두자.


그럼에도 데이터를 분해해야 한다면

여러 서비스에서 분산 DB에 작업을 수행하면서도 잠금을 피할 수 있는 방법이 필요하다.
그렇다면 사가(SAGA) 패턴을 고려할 수 있다.



4. 사가(SAGA) 패턴

사가는 여러 상태 변경을 조정할 수 있지만 자원을 잠금 필요가 없는 알고리즘으로 설계됐다.


사가는 원래 단일 DB에 작동하는 LLT(Long Lived transaction 장기 트랜잭션)를 지원하기 위한 메커니즘으로 구상됐지만, 여러 서비스에 걸친 변경 사항을 조정할 경우에도 효과적이다.


단, 사가는 일반적인 DB 트랜잭션의 ACID 관점에서 원자성을 제공하지는 않는다.
LLT를 개별 트랜잭션으로 나누기 때문에 사가 자체 수준에서는 원자성을 갖지 않는다.
필요한 경우 각 트랜잭션이 ACID 트랜잭션 변경과 연관될 수 있으므로 전체 사가 내에서 개별 트랜잭션 각각에 대한 원자성은 있다.


사가 패턴의 예시를 이해하기 위해 아래와 같은 결제 흐름이 있다고 하자.



여기서 주문 프로세스는 하나의 사가로 표현되며, 이 흐름의 각 단계는 각각 서로 다른 MSA 서비스에서 처리된다.


각각 서비스 내부의 모든 상태 변경은 로컬 ACID 트랜잭션 내에서 처리될 수 있다.
예를 들어 창고 서비스를 사용해 재고를 확인하고 예약하는 경우, 일반 트랜잭션 내에서 처리가 가능하다.



(1) 사가 실패 모드

사가를 개별 트랜잭션으로 분해하려면, 실패 처리 방법을 고려해야 한다.


여기에는 두 가지 종류가 있는데, 역방향 복구순방향 복구, 이렇게 2가지가 있다.


  • 역방향 복구 : 실패 복구와 이후에 일어나는 정리 작업인 롤백이 포함
    • 이 작업을 수행하려면 이전에 커밋된 트랜잭션을 취소하는 보상 조치를 정의해야 함
  • 정방향 복구 : 실패가 발생한 지점에서 데이터를 가져와 계속 처리할 수 있음
    • 트랜잭션을 재시도할 필요가 있으며, 이 말은 즉, 시스템이 재시도를 위한 충분한 정보를 보유하고 있음을 뜻함

필요시 2가지 복구 모두를 혼합해야하는 경우가 발생할 수 있다.


또한 사가를 통해 기술적인 실패가 아닌 비즈니스 실패로부터 복구할 수 있다는 점을 기억해야 한다.
예시로 아래와 같은 사례가 있다.


예시 1) 고객이 결제를 시도했지만 고객의 자금이 부족하면? -> 사가가 처리해야하는 비즈니스 실패 O

예시 2) 결제 게이트웨이가 타임아웃이 되거나 500 Internal Service Error가 발생? -> 사가가 처리해야하는 비즈니스 실패 X, 별도로 처리해야하는 기술적 실패 O


(2) 사가 롤백

ACID 트랜잭션을 사용할 때 문제가 발생하면 커밋에 앞서 롤백을 한다.
하지만 MSA 분산 환경에서는 여러 트랜잭션이 관련되어 있으며, 일부 트랜잭션은 전체 연산을 롤백하기로 결정하기 전에 이미 커밋됐을지도 모른다.



위의 시나리오에서 주문 단계에서 실패했을 경우, 단일 DB 트랜잭션이라면 간단한 롤백으로 모든 처리 과정을 정리할 수 있다.

차라리 롤백 구현을 원한다면 보상 트랜잭션을 구현해야 한다.

보상 트랜잭션은 커밋된 트랜잭션을 취소하는 연산이다.


다만 이런 보상 트랜잭션은 일반적인 DB 롤백과는 정확한 동작을 수행하지 않을 수도 있다.


DB 롤백은 커밋 전에 발생하며, 롤백 후 트랜잭션이 전혀 발생하지 않은 것 처럼 취급할 수 있다.
그러나 사가 패턴에서는 이미 트랜잭션이 발생했다를 고려하여 보상 트랜잭션을 설계하고, 이는 변경을 되돌리는 새로운 트랜잭션을 만들고 있음을 뜻한다.


쉬운 비유로 아래와 같은 프로세스가 이루어진다고 보면 된다.

주문을 진행중인 유저에게 주문 처리 단계 중 주문이 진행 중임을 알리는 메일을 보낸다.
롤백하기로 결정된다 하더라도 한 번 발송된 메일은 취소할 수 없다.
때문에 롤백이 발생하면, 보상 트랜잭션으로 고객에게 두 번째 메일을 보내 주문이 취소됐음을 알릴 수 있다.

(3) 롤백을 줄이기 위해 워크플로 단계를 재정렬

위와 같이 아예 복잡한 트랜잭션 단계를 뒤쪽으로 이동하면 실패할 경우 롤백해야하는 내용을 줄일 수 있다.



5. 사가 패턴 구현

사가 패턴에는 크게 2가지 종류가 있다.
오케스트레이션형 사가는 중앙 집중식 조정과 추적에 의존한다.
코레오그래피형 사가는 더 느슨한 결합된 모델을 선호하므로, 중앙 집중식 조정이 필요하진 않지만 사가의 진행을 추척하는 작업을 더 복잡하게 만들 수 있다.



(1) 오케스트레이션형 사가

중앙 조정자 (= 오케스트레이터)를 사용해 실행 순서를 정의하고 필요한 보상 조치를 트리거한다.


중앙 주문 처리기는 연산을 수행하는 데 어떤 서비스가 필요한지 알고 있으며, 언제 해당 서비스를 호출해야 할지 결정한다.
호출이 실패하면 그 결과에 따라 어떤 작업을 수행할지 결정할 수 있다.
이렇게 오케스트레이션형 기반의 처리기는 서비스간 요청 / 응답 호출을 많이 사용하는 편이다.


즉, 주문 처리기는 서비스(ex : 지불 게이트웨이)에 요청을 보내고, 요청이 성공했는지 여부를 알려주는 응답을 기대하며 요청 결과를 제공한다.


장점

  • 시스템 한 곳만 바라봐도 이 프로세스가 어떻게 작동하는지 이해 가능
  • 신규 업무 투입자가 프로세스를 쉽게 이해할 수 있음

단점

  • 도메인 결합도가 높은데, 이는 중앙 주문 처리기가 모든 서비스에 대해 알아야하기 때문
  • MSA 서비스가 위임해야하는 로직이 오케스트레이터에 흡수되기 시작할 수 있음

한줄 요약하면 로직이 중앙 집중화 될 수 있다는 것이 단점이 될 수 있다.

너무 많은 중앙 집중화를 피하고자, 서로 다른 흐름에 대해 서로 다른 MSA 서비스가 오케스트레이터 역할을 수행하도록 역할을 분산시킬 수 있다.



(2) 코레오그래피형 사가

코레오그래피형 사가는 여러 MSA 서비스 사이에서 사가 운영에 대한 책임을 분산시키는 것을 목표로 한다.


오케스트레이션형 사가가 명령과 제어 방식이라면, 코레오그래피형 사가는 신뢰하지만 검증된 아키텍처를 나타낸다.
상기 이미지에서 볼 수 있듯, 코레오그래피형 사가는 서비스간 협업을 위해 이벤트를 많이 사용한다.


안정적인 메시지 브로드캐스트와 이벤트 전달을 관리하기 위해, 일종의 메시지 브로커를 사용한다.
여기서는 토픽을 사용한다. 특정 유형의 이벤트에 관심이 있는 당사자는 특정 토픽을 구독하여 메시지가 발생하면 이를 수신한다.



장점

  • 도메인 결합도가 낮음
    • 오케스트레이션형 사가에서는 중앙 조정자가 모든 서비스에 대해 알고 있어야 했으나, 코레오그래피형 사가는 상대 서비스에 대해 전혀 모름
    • 특정 이벤트가 수신될 때 자신이 할 일만 잘 파악하면 되며, 이는 도메인의 느슨한 결합도를 유도

단점

  • 도메인 결합도가 낮아서 어떤 일이 일어나고 있는지 파악하기 더 어려울 수 있음
    • 이는 보상 조치를 취할 기회조차 놓칠 수 있음을 뜻함


단점을 타파하기 위해 발행된 이벤트를 사용해 사가의 상태에 대한 뷰(view) 를 투영하는 방법이 있다.
사가에 대한 고유 ID, 즉, 상관관계 ID를 생성하면, 이 ID를 사가의 일부로 방출되는 모든 이벤트를 넣을 수 있다.


서비스 중 하나가 이벤트에 반응한다면 상관관계 ID가 추출돼 로컬 로깅 프로세스에 사용되고, 추가적인 호출이나 발송된 이벤트들과 함께 다운스트림으로 전달된다.


그런 다음 이러한 모든 이벤트를 정리하고 각 주문의 상태 보기를 제공하면, 다른 서비스가 스스로 처리하지 못할 경우에는 풀필먼트 (fulfillment) 프로세스의 일부로 문제를 해결하기 위한 조치로서 프로그래밍 방식으로 수행하는 별도 서비스를 만들 수 있다.
이는 추후 10장 챕터에서 디테일하게 다룰 예정이다.



(3) 혼합 (+ 분산 트레이싱)

꼭 하나만 사용할 필요는 없다. 오케스트레이션형 - 코레오그래피형 사가 모두를 활용할수도 있다.


예를 들어 풀필먼트 사례의 경우, 창고 서비스의 경계 내에서 포장과 발송을 관리할 때 원래 요청이 규모가 좀 더 큰 코레오그래피형 사가의 일부로 이루어진 경우에도 오케스트레이션형 흐름을 사용할 수 있다.


그리고 코레오그래피 방식을 사용하건, 오케스트레이션 방식을 사용하건, 또는 혼합을 사용하건 분산 트레이싱 역시 중요하다. 이는 비즈니스 프로세스가 올바르게 작동하는지 판단하거나 문제를 진단하는데 도움이 되기 때문이다.
마찬가지로 후속 파트에서 이를 다룰 예정이다.



(4) 무엇을 사용해야 할까?

저자의 후기에 의하면, 사가의 진행 상황을 추적하는 것과 추가적인 복잡성은 느슨하게 결합된 아키텍처를 통해 얻을 수 있는 이점보다 큰 경우가 대부분이다. (즉, 사가를 쓰는게 안쓰는것보다는 더 나았다.)


저자는 개인적으로 아래와 같은 상황에서 각각의 도입을 추천한다.


  • 오케스트레이션형 사가
    • 한 팀이 전체 사가의 구현을 담당할 경우
    • 요청 - 응답 - 호출의 방식이 중심인 경우

  • 코레오그래피형 사가
    • 여러 팀이 관여하는 경우, 더더욱 세분화된 코레오그래피형 사가를 추천
      • 느슨하게 결합된 아키텍처를 통해 각 팀이 독립적으로 작업할 수 있음
    • 이벤트 기반 상호작용 모델이 중심인 경우


(5) 사가 vs 분산 트랜잭션

분산 트랜잭션 구현과 관련된 근본적인 문제로 펫 헬런드(Pat Helland)는 아래와 같이 언급했다.


대다수 분산 트랜잭션 시스템에서 단일 노드에 장애가 발생하면 트랜잭션 커밋은 중단된다.
이로인해 어플리케이션은 쐐기가 박힌 상태가 된다. 
이런 시스템에서는 그 크기가 커질수록 시스템이 죽을 가능성도 높아진다.
모든 엔진이 작동해야 하는 비행기가 비행할 때, 엔진을 하나 추가하면 비행기의 가용성은 감소한다.

저자도 마찬가지로 분산 트랜잭션은 매우 특정한 몇가지 상황을 제외하면 가급적 피하는 편이다.
대신 비즈니스 프로세스를 사가로 명시적으로 모델링하면 분산 트랜잭션으로 인한 다양한 문제를 피할 수 있으며, 암시적으로 모델링된 프로세스가 개발자에게 더욱 명확하게 보여줄 수 있는 추가적인 이점이 있다.



5. 부록 - DB 분산 트랜잭션

본문에서는 MSA 전반에 걸쳐 상태 변화를 조정하려고 분산 트랜잭션을 사용하는 것에 반대하는 입장이다.
일부 큰 기업에서는 이러한 분산 트랜잭션을 성공적으로 달성하고 있는데, 대표적인 분산 트랜잭션 알고리즘으로 Google의 Spanner(스패너)가 있다.