분산 트랜잭션 (Distributed transaction)

GwanMtCat·2023년 11월 18일
1

면접에서 분산 트랜잭션에 관해서 질문을 받았다. 자세하게 공부를 해보지 않은 영역이라 정리 해보려한다.
"트랜잭션 오케스트레이터와 보상 트랜잭션 같은 작업이 필요할 것 같습니다" 라고 어설프게 대답했고, 상세한 질문이 들어왔지만 역시 대답하지 못했다.
다만 좋게 봐주신건지 그 면접은 합격할 수 있었다.

그래서 물어보시는 것에 대답을 할 수 있을 정도로 조금 정리해보려고 한다.


개념

분산 트랜잭션(Distributed transaction)은 2개 그 이상의 네트워크 상의 시스템 간의 트랜잭션을 말한다.

일반적으로 시스템은 트랜잭션 리소스(transaction resource)의 역할을 하고, 트랜잭션 매니저(transaction manager)는 이러한 리소스에 관련된 모든 동작에 대해 트랜잭션의 생성 및 관리를 담당한다.

분산 트랜잭션은 다른 트랜잭션처럼 ACID(원자성, 일관성, 고립성, 지속성)을 갖춰야 하며, 여기에서 원자성은 일의 단위(UOW)를 위해 all-or-nothing 결과를 보증해야 한다. (위키백과)

UOW (Unit or Work)? - 작업 단위 패턴

"점차 애플리케이션의 비즈니스 로직이 복잡해지면 하나의 요청에 대해
여러 연관된 데이터들을 조작하게 되는 경우가 생기게 된다.
그리고 하나의 비즈니스 로직 흐름안에서 데이터 또는 발생되는 작업들 간의 무결성을 유지하기 위해서 수행되어야 할 작업들 중 하나라도 실패할 경우, 실패한 작업 전까지 실행됐던 모든 작업을 되돌려놓아야 할 필요성도 생길 수도 있다.
이를 위해서 데이터베이스의 트랜잭션처럼 비즈니스 계층에서도 트랜잭션의 개념을 도입 하기 위한 구조가 필요하다. 이를 위한 패턴이 있는데 바로 "Unit Of Work"이다.

쉽게 말하면 독립된 여러 데이터베이스에 동시에 접근하는 트랜잭션을 말하고 이것이 분리된 데이터베이스에서 동작하지만 하나의 작업인 것처럼 동작을 해야하는 것을 말한다.

단일 데이터베이스에서는 단일 트랜잭션 범위 내에서 이 작업이 모두 수행 가능하지만, 데이터베이스가 다르다면 어떻게 트랜잭션의 ACID 성질을 보장할 있을까?

이러한 분산 트랜잭션의 관리 방안으로는 2 Phase Commit과 Saga Pattern이라는 것이 있다.


Two Phase Commit (2Phase Commit, 2PC)

위의 그림을 한번 살펴보자.
코디네이터(Coordinator) 라는 객체가 존재하고, 데이터베이스 1,2 가 존재한다.

단일 트랜잭션에서 볼 수 없는 코디네이터가 있는데 이는 쉽게 말해 트랜잭션 관리자 혹은 오케스트레이터이다.

트랜잭션을 요청하는 같은 애플리케이션 프로세스 내 라이브러리에 구현되어 있으나, 분리된 서비스나 프로세스일수도 있다고 한다.

  1. 일단 각 데이터베이스에서 데이터를 쓰기 위한 작업을 수행한다. 이때 코디네이터에서 유니크한 트랜잭션 ID를 생성하고, 이 ID를 각 데이터베이스에 전달한다.

  2. 각 데이터베이스의 각 트랜잭션에서 락을 잡기 위한 작업을 수행한다.

  3. 쓰기 작업을 수행하면 각 트랜잭션이 코디네이터에게 작업을 완료했음을 알린다.

  4. 커밋할 준비가 되었다면, 코디네이터가 1단계 (phase 1) 을 시작한다.

  5. 각 데이터베이스에 준비 요청(prepare)을 보내어 커밋 가능 여부를 묻게 되고, 참가자의 응답에 따라서 커밋을 할지 롤백을 할지 결정한다.

  6. 데이터베이스는 코디네이터에게 커밋 가능 요청을 보낸다.

  7. 모든 데이터베이스에서 문제가 없다는 신호를 받으면 코디네이터는 2단계 (phase 2)를 실시한다.

  8. 실시하면서, 각 데이터베이스에 커밋 요청을 전송하고 커밋이 각각 수행된다.

  9. 요청이 실패하거나 타임아웃이되어도 코디네이터는 이것이 성공할 때까지 retry 하게 된다. 만약 데이터베이스 1개가 다운되어도, 다시 회복된다면 그 트랜잭션이 커밋이 된다.

중요한 것은
"번복이 없는 포인트"로

  1. 참여자가 yes를 응답하는 경우 반드시 커밋해야하고,

  2. 코디네이터가 결정을 하면 반드시 그 결정을 강제하여야한다고 한다.
    이러한 약속을 통해 2PC의 원자성을 보장할 수 있다.

보통 사용되는 경우는 다음과 같다.

  • 2개의 데이터베이스에 관련된 데이터를 쓰기 작업해야 하는 경우
  • 메시지큐와 해당 메시지를 처리하는 분산 노드에서 둘다 제대로 처리되었다는 확인이 필요한 경우(Exatly Once)

엄청 나게 마법처럼 보이지만 2PC에도 취약점은 있다.

코디네이터가 다운이 된다면 어떻게 될까?
참여자가 phase 1 단계로 prepare 요청 후 yes 응답을 보낸 이후에 코디네이터가 다운이 된다면 참여자는 혼자서 abort를 수행할 수가 없다. 그럼 커넥션과 레코드나 테이블에 걸어둔 락은 어떻게 되는걸까? 그대로 장애로 이어진다..

코디네이터의 지원 없이는 참여 중인 데이터베이스는 커밋되었는지 중단되었는지 알 수 있는 방법이 없으므로 각 데이터베이스가 통신을 통해 이러한 상황을 공유해야 하나 이 부분은 안타깝게도 2PC에 포함되지는 않는다고 한다.

이러한 문제가 발생되는 경우 해결할 방법은 오직 코디네이터가 회복되기를 기다리고,
커밋이나 중단 결정을 디스크에 존재하는 트랜잭션 로그에 기록해두므로 코디네이터가 회복할 때 트랜잭션 로그를 읽어들여서 상태를 회복할 수 있다.

스프링에서는 JTA(java Transaction API)라고 불리우는 플랫폼마다 다른 트랜잭션 매니저들과 어플리케이션이 상호작용할 수 있는 인터페이스를 정의하고 있다고 한다.
구현체로는 Atomikos, Bitronix 등이 있다.

JTA는 X/Open XA(eXtended Architecture)라는 X/Open 이 제정한 분산 트랜잭션 처리를 위한 표준을 따르고 있는데 여기에서는 지연되고 있는 트랜잭션을 회복시키는 방법도 포함하고 있다고 하니, 사용하게 될 경우 공부 해봐야겠다.

다만 2PC는 속도와 안정성이 부족한 경우가 많고, 오픈 소스 저장소 중에서는 이를 지원하지 않는 제품이 있어 잘 사용되지 않는다고 한다.

X/Open?
X/Open은 정보기술 분야에 개방형 표준을 식별, 장려하기 위해 1984년 유럽의 여러 유닉스 시스템 제조업체들이 설립한 연합체


SagaPattern

Saga?
이야기, 스토리텔링
트랜잭션끼리 주고 받는 사건, 이야기 라고 해석하면 좋다. 일단 2개 이상의 트랜잭션을 말함
2PC를 사용하지 않는다면 분산 트랜잭션에서는 어떻게 처리해야 할까? 이때 보상 트랜잭션이라는 개념이 있다.

보상 트랜잭션이란 정상적으로 수행된 서비스의 트랜잭션을 취소 혹은 되돌리는 트랜잭션으로 보통 INSERT 시 DELETE를, UPDATE시 재 UPDATE를, CREATE시 DROP을 할 수 있다.

한 서비스가 처리하면서 다른 서비스의 데이터를 같이 변경해야 하는 경우, 보상 트랜잭션이 필요하다.

트랜잭션을 통해 데이터베이스가 자동으로 해주던 롤백을 직접 구현해야 하기 때문에 구현이 매우 어려운 것처럼 느껴진다. 또 MSA와 같이 여러 서비스로 서버가 나누어지면 시스템 내부에서 실행되던 트랜잭션의 상당수가 서비스 간의 트랜잭션의 대체되니 보상 트랜잭션이 많이 증가할 것이라는 걱정이 생긴다.
근데 재밌는점은 실제로 보상 트랜잭션이 필요한 경우는 많지 않다고 한다. 이에 관해서는 나중에 다뤄보겠다.


이러한 보상 트랜잭션을 이벤트/메시지로 소싱하여, 서비스들이 이벤트/메시지를 주고 받아 처리하는 것을 SAGA패턴이라고 한다. 트랜잭션의 관리 주체가 데이터베이스가 아닌 어플리케이션에 있다.

대표적으로 2가지 방식이 있다.


Choreography(코레오그래피:연출) based SAGA pattern

분산 트랜잭션을 중계하는 중계자가 없는 패턴으로 다음과 같이 동작한다.

  1. 현재 서비스(Order Service) 에서 로컬 트랜잭션을 처리한 후, 다음 서비스(Customer Service)에게 처리할 이벤트를 전달한다.
  2. 다음 서비스에서 처리 후, 성공 및 실패 응답을 전달한다.
  3. 실패 하는 경우에는 보상 트랜잭션을 실행한다.

장점은 다음과 같다.

  • 이벤트를 통해 처리하므로 서비스 간 커플링이 준다.
  • 복잡하지 않다.

단점은 다음과 같다.

  • 서비스 마다 구현로직이 다양할 것이므로 워크플로우에 대한 정리 및 이해가 필요하다.
  • 잘못 구현하는 경우에 순환하는 일이 발생할 수 있다.
  • 다른 서비스에서 발생할 수 있는 모든 서비스에 대해 구독하고 있어야 한다.
  • 이러한 트랜잭션을 시뮬레이션하려면 모든 서비스가 실행되는 통합테스트를 실행하여야 한다.

서비스가 많지 않은 경우에 아마 사용을 고려할 수 있을 것 같다.


Orchestration(오케스트레이션) based SAGA pattern

분산 트랜잭션을 책임지는 별도의 중계자 (Saga Ochestrator)가 존재하는 패턴이다.
이 중계자는 모든 트랜잭션을 처리하고 이벤트에 따라 수행할 작업을 참가자에게 알려준다. Saga 요청을 실행하고, 각 작업의 상태를 저장 및 해석하여 보상 트랜잭션을 사용하여 오류 복구를 처리한다.

장점은 다음과 같다.

  • 많은 서비스가 있고 추가되어질 예정일 복잡한 워크플로우에 적합하다.
  • 모든 서비스를 제어하고 활동 흐름을 제어할 수 있다.
  • 오케스트레이터가 각 서비스에 의존하지 않으므로 순환 문제가 발생하지 않는다.
  • 각 서비스는 다른 서비스의 명령에 대해 알 필요가 없어, 비즈니스 논리를 최소화 시킬 수 있다.

단점은 다음과 같다.

  • 과다한 중앙 집중으로 인해 구현의 불편함이 생길 수 있다.

이러한 SAGA 패턴을 적용할 때는 다음 사항을 고려해야 한다고 한다.

  • SAGA 패턴은 트랜잭션을 조정하고, 여러 서비스에 걸친 비즈니스 프로세스에 대한 데이터 일관성을 유지하는 방법에 대해 새로운 사고 방식이 필요하여 러닝커브가 있다.

  • 특히 디버깅 하기가 어렵고, 서비스가 증가함에 따라 복잡성이 증가한다.

  • 격리(Isolation)에 대한 대책이 반드시 필요하다.


MSA를 해본적이 없으니 대강 무슨말인지는 알겠는데 역시 지식으로만 한계가 있다.
이러한 SAGA 패턴을 지원하기 위해 Axon framework 라는 자바 기반의 오픈 소스, Event Sourcing, CQRS, DDD를 중심으로 애플리케이션을 작성하게 도와주는 프레임워크, Axon Server라는 Ochecration Server가 있다고 한다.

추후 기회가 되면 공부해봐야 겠다.


참조한 책 및 사이트

https://harrislee.tistory.com/57
https://velog.io/@khh180cm/6.-%EC%9E%91%EC%97%85-%EB%8B%A8%EC%9C%84-%ED%8C%A8%ED%84%B4
https://d2.naver.com/helloworld/5812258
https://velog.io/@jungbumwoo/Two-Phase-commit-%EC%9D%B4%EB%9E%80-2PC
https://kadensungbincho.tistory.com/125
https://wannaqueen.gitbook.io/spring5/spring-boot/undefined-1/39.-jta-by-ys
https://learn.microsoft.com/ko-kr/azure/architecture/reference-architectures/saga/saga
https://www.youtube.com/watch?v=OnKYXmzRkKQ

0개의 댓글