Yunseok's Dev Blog

배운 것을 적는 블로그입니다.

마이크로 서비스에서의 트랜잭션과 질의

마이크로 서비스 인 액션 5장을 읽고 정리해보았습니다.

모놀리식 애플리케이션은 상태를 변경할 때 일관성과 격리를 보장하기 위해 트랜잭션에 의지한다.

마이크로 서비스 애플리케이션에서는 각기 독립적인 서비스가 특정 역량을 담당한다. 각기 데이터 소유권을 가지고 있어 결합을 제거해 자율성을 얻지만 애플리케이션 수준에서 일관성 문제가 있고 데이터 조회를 복잡하게 만든다. 또한 가용성은 애플리케이션 설계에 영향을 준다. 서비스 간의 상호작용이 실패하면 비즈니스 프로세스가 멈추고 시스템이 일관성을 잃은 상태로 남게된다. 이러한 여러 서비스 간에 복잡합 트랜잭션을 조율하기 위해 사가(sagas)패턴과 이벤트 소싱(event sourcing)과 같은 이벤트 기반 아키텍처로 해결할 수 있다.

분산 애플리케이션에서 일관된 트랜잭션

주식을 매도하는 애플리케이션을 가정해보자.

  1. 주문을 생성한다.
  2. 주식 수량을 검증하고 예약한다.
  3. 수수료를 부과한다.
  4. 마켓에 주문을 제출한다.

고객의 입장에서는 모든 과정이 한 번에 일어난다. 가지고 있지 않은 주식을 매도하거나 두 번 이상 매도할 수 없다.

모놀리식 애플리케이션에서는 간단하다. ACID트랜잭션에서 데이터베이스 동작을 감싸고 잘못된 상태를 유발하는 에러는 원래 상태로 되돌아 가면 되기 때문이다.

마이크로 서비스 애플리케이션에서는 주문 서비스가 주식을 예약하고 수수료를 부과하기 위해 수수료 서비스를 호출 했을 때 이 트랜잭션이 실패할 수 있다. 분산된 데이터 소유권이 독립적이고 느슨하게 연결되는 것을 보장하지만 애플리케이션 수준에서 전체적인 데이터의 일관성을 유지하는 메커니즘을 구축할 필요가 있다.

왜 분산 트랜잭션을 사용할 수 없을까

여러 서비스에 걸쳐 트랜잭션을 보장하도록 시스템을 설계 할 수 있다. 일반적으로 two-phase commit이다. 이 방식은 여러 리소스에 걸친 동작을 준비하고 커밋하는 트랜잭션 관리자를 사용한다.

단점

  • 관리자와 자원간에 동기식 커뮤니케이션을 사용한다.
  • 자원을 사용할 수 없으면 트랜잭션을 커밋할 수 없고 롤백해야 한다. 재시도 횟수를 증가시키고 시스템의 전체적 가용성을 떨어트린다.
  • 비동기식 서비스 상호작용을 지원하려면 서비스 간의 2PC를 지원하면서 동시에 메시지 계층도 지원해야 한다. 이것은 기술적으로 선택의 범위를 제한한다.
  • 중요한 조율 책임을 트랜잭션 관리자에게 넘기는 것은 마이크로 서비스의 핵심 원칙의 하낭니 서비스 자율성을 위반한다.
  • 분산 트랜잭션은 트랜잭션의 격리를 보장하기 위해 자원에 대한 락을 사용한다. 이 것은 경쟁과 데드락의 위험을 증가시키기 때문에 긴 트랜잭션에는 부적합하다.

그렇다면 대신 무엇을 사용해야 할까?

이벤트 기반 커뮤니케이션

비동기 이벤트는 서비스 간의 결합을 느슨하게 하는 데 도움을 주고 저체적인 시스템의 가용성을 높이며 서비스 작성자로 하여금 궁극적 일관성 관점에서 생각하도록 독려한다.

동기식 접근법에서는 주문 서비스가 주문이 마켓에 제출될 떄까지 스텝 순서대로 호출해 다른 서비스의 행동을 조율한다. 어느 스텝하나가 실패하면 주문 서비스는 다른 서비스의 롤백 동작을 실행할 책임이 있다. 이런 유형의 상호작용은 호출 관계가 논리적이고 순차적이어서 추론하기 쉽지만, 감당해야 할 책임 때문에 주문 서비스가 다른 서비스와 단단하게 결합돼 독립성을 저해하고 향후 변경을 어렵게 한다.

이벤트와 자율적 구성

각 서비스는 언제 어떤 작업을 해야 할지 알기 위해 관심 있는 이벤트를 구독한다. 이벤트는 가용성에 대해 낙관적인 접근 방식을 취할 수 있게 해준다. 예를 들어 수수료 서비스가 작동하지 않아도 주문 서비스는 여전히 주문을 생성할 수 있다. 각 서비스는 이벤트에 반응해 처리 과정의 전반적인 결과를 인지하지 않고 독립적으로 행동한다. 결국 이런 설계는 다른 서비스와 겹합력을 떨어트리고 독립성을 증가시켜 독립적으로 변경을 반영하기 쉽게 만든다.

사가(Sagas) 패턴

사가는 조율된 로컬 트랜잭션의 연속이다. 로컬 트랜잭션은 Atomic하지만 사가 전체는 그렇지 않다. 그래서 개발자는 개별 트랜잭션이 실패하더라도 시스템이 궁극적으로 일관성 있는 상태가 되도록 코드를 짜야한다.

보상 동작은 사가에서 이전 작업을 없던 일로 하거나 시스템을 좀 더 일관된 상태로 돌리기 위해 사용한다. 이 설계 방식은 폭넓은 잠재적 시나리오를 고려할 필요가 있기 때문에 비즈니스 로직을 더욱 복잡하게 만들지만, 분산된 서비스 간의 신뢰할 수 있느 상호작용을 구축하기 위한 훌륭한 도구다.

자율적으로 구성된 사가 패턴

각 테스크에서 발생한 이벤트에 대해서 보상을 설계한다. 이렇나 형태의 롤백은 시스템의 일관성을 아주 정확하게 유지하는 것이 아니라 의미상 유지하려고 한다. 롤백 동작을 수행한 시스템은 원래의 완전히 동일한 상태로 돌아갈 수 없을 수 있다. 프로세스에 포함된 모든 동작은 하나 이상의 적절한 보상 동작을 가질 수 있다. 이 방식은 시나리오를 예측하고 코딩하고 테스트하는 모든 것에서 시스템을 더 복잡하게 만든다. 특히 더 많은 서비스가 포함될수록 롤백은 더욱더 복잡해질 수 있다. 고립된 운영이 아닌 실 세계의 환경을 반영하는 서비스를 구축할 때 실패 시나리오를 예측하는 것은 중요한 부분이다. 마이크로 서비스를 설계할 때 더 넓은 애플리케이션이 복원력을 발휘하도록 보상 설계를 고려해야 한다.

  • 장점
    • 자율적 상호작용 스타일은 참여하는 서비스가 서로를 명시적으로 알 필요가 없기 때문에 느슨하게 연결되도록 하는 데 도움이 된다.
  • 단점
    • 규칙을 검증할 때 여러 구분된 서비스를 확인해야 해서 검증이 어려워 진다.
    • 상태 관리를 복잡하게 만든다.
    • 서비스 간의 순환 의존성을 유발한다.
    • 처리 과정이 얼마나 진행됐는지 알기 어렵다.

따라서 비동기 커뮤니케이션 스타일을 선택할 경우 시스템의 실행 흐름을 추적할 수 있는 모니터링과 추적 기능에 투자해야 한다.

조율된 사가 패턴

서비스가 조율자 역할을 하여 여러 서비스에 걸친 사가의 결과를 실행하고 추적하는 프로세스다. 사가의 참여자와 비동기 이벤트 또는 메시지 요청/응답을 통해 상호작용한다. 가장 중요한 것은 프로세스의 각 단계에서 실행의 상태를 추적하는 것이다. 때때로 이 것을 사가 로그라고 한다.

사가도 실패를 한다. 조율된 사가에서 조율자는 실패한 트랜잭션에 영향을 받은 엔티티를 유효한 일관된 상태로 되돌리기 위해 적절한 보상 동작을 시작할 책임이 있다. 기대한 동작이 실패한다면 보상 동작 또는 조율자 자신도 실패할 수 있다. 보상 동작도 의도치 않은 부작용 없이 재시도할 수 있도록 설계해야 한다. 최악의 경우 롤백 중 반복된 실패로 인해 수동 개입이 필요할 수도 있다. 완전한 에러 모니터링은 이런 시나리오도 잡아내야 한다.

  • 장점
    • 사가에서 일련의 로직을 단일 서비스에 집중시키면 한 곳에서 순서를 변경하는 것 뿐만 아니라 사가의 결과와 진행 사항을 추론하기가 쉬워진다. 결국 로직이 조율자로 이동하므로 개별 서비스가 간단해지고 관리해야 할 상태의 복잡도가 줄어든다.
  • 단점
    • 조율자가 너무 많은 로직을 가진다는 위험이 있다.

중첩된(interwoven) 사가 패턴

ACID트랜잭션과 다르게 사가는 격리가 없다. 각 로컬 트랜잭션의 결과는 엔티티에 영향을 주는 다른 트랜잭션이 즉시 볼 수 있다. 이런 가시성은 엔티티가 동시에 여러 사가에 엮일 수 있다는 뜻이다. 그래서 서비스가 인티티의 중간 상태를 예상하고 다루도록 비즈니스 로직을 설계해야 한다. 이때 필요한 중첩의 복잡도는 주로 하위의 비즈니스 로직에 달려있다.

이렇게 중첩된 사가를 다루기 위한 3가지 일반적인 전략이 있다.

  • 회로 차단하기(Short-circuiting)
    • 사가가 진행중일 때 새로운 사가가 시작되는 것을 막는다.
  • 잠그기(Locking)
    • 엔티티의 상태를 변경하고자 하는 다른 사가는 lock을 얻기 위해 대기해야 한다.
    • 이 방식은 여러 사가가 lock을 얻기 위해 서로를 방해하는 데드락에 빠질 수 있다. 시스템이 멈추는 것을 방지하기 위해 데드락 모니터링과 타임아웃을 구현해야 한다.
  • 인터럽트(interruption)
    • 동작이 실행되는 것을 방해한다.
    • 비즈니스 로직의 복잡도를 증가시키지만, 데드락의 위험은 피할 수 있다.

일관성 패턴

  • 보상동작
    • 이전 동작을 없던 일로 하는 동작을 수행한다.
  • 재시도
    • 성공 또는 시간 만료될 때까지 재시도한다.
  • 무시
    • 에러 이벤트가 발생해도 아무것도 하지 않는다.
  • 재시작
    • 원래 상태로 초기화하고 다시 시작한다.
  • 잠정적 동작
    • 잠정적 동작을 수행하고 나중에 확정 또는 취소한다.

이벤트 소싱

엔티티 상태에 대한 이벤트를 게시하는 대신, 그 객체에 발생한 이벤트의 연속으로 전체의 상태를 나타낸다. 특정 시각에 엔티티의 상태를 얻기 위해서 그 시각 이전의 이벤트를 통합한다. 전통적인 방식은 주문츼 최신 상태를 저장하지만 이벤트 소싱에서는 주문의 상태를 변경하도록 한 모든 이벤트를 저장한다. 이런 이벤트를 종합해 현재 상태를 나타낼 수 있다.

분산된 환경에서의 질의

또한 분산된 데이터 소유권은 데이터 조회를 더욱더 어렵게 한다. 조인같은 데이터베이스 수준 또는 근접한 수준에서 데이터를 집계하는 것이 불가능하기 때문이다.

데이터 복제본 저장하기

서비스가 이벤트를 통해 다른 서비스로부터 받은 데이터를 저장하거나 캐시하도록 선택할 수 있다. 표준 데이터를 여러 장소에 유지하면 이 데이터를 갱신하기 위한 비동기식 이벤트가 지연되거나 실패 또는 여러 번 전달될 수 있으므로 궁극적인 일관성 문제와 조회한 복제 데이터가 최신이 아닌 상황에 대비해야 한다. 최신 데이터를 보장하지 않으면서 성공적인 결과를 반환하는 가용성과 최신 상태를 반환하거나 실패하는 일관성 사이에서 선택해야 한다.

질의와 명령 분리하기

시스템에서 일기와 쓰기를 명시적으로 분리한다. 관심사를 합리적으로 분리할 수 있게 해준다.

CQRS

  • 명령(command)측은 생성, 갱신, 삭제의 시스템 갱신을 수행한다. in-band나 RabbitMQ또는 Kafka와 같은 별개의 이벤트 버스로 이벤트를 발행한다.
  • 이벤트 핸들러는 적절한 query또는 read모델을 구축하기 위해 이벤트를 소비한다.
  • 분리된 데이터 저장소가 시스템의 각 측면을 지원할 수 있다.

이벤트가 질의 모델을 갱신하기 때문에 누군가 그 데이터를 조회하면 만료된 뷰를 보게된다. 이를 위해 낙관적 갱신이나 폴링, 게시-구독 3가지 전략을 적용할 수 있다.

  • 낙관적 갱신
    • 명령이 기대한 결과에 근거해 UI를 낙관적으로 갱신할 수 있다. 명령이 시래하면 UI의 상태를 되돌릴 수 있다. 예를 들면 인스타그램의 좋아요 버튼이 있다.
  • 폴링
    • UI는 기대한 변경이 발생될 때까지 쿼리 API를 폴링할 수 있다. command할 떄 클라이언트는 타임스탬프같은 버전을 설정한다. 이후 쿼리에서 새로운 모델로 갱신됐음을 나타내는 것으로 지정된 버전 번호와 같거나 그 이상의 버전이 나올 때까지 폴링을 계속할 것이다.
  • 게시-구독(Publish-subscribe)
    • 폴링을 하는 대신 UI는 웹소켓 채널 등으로 질의 모델의 이벤트를 구독한다.

Sources

  • 마이크로 서비스 인 액션 - 모건 부르스, 파울로 페레이라