티스토리 뷰
크리스 리처드슨의 '마이크로서비스 패턴'에 나오는 44가지 패턴 중 핵심 패턴인 Saga. Event sourcing, API composition, CRQS, External API, Transactional Outbox/Polling publisher/Transaction Log tailing 8가지를 먼저 빠르게 이해해 봅니다.
‘마이크로서비스 패턴' (크리스 리처드슨 지음, 이일웅 번역) 참조
먼저 아래 요약본을 먼저 보시면 좀 더 이해가 쉬울 수 있습니다.
그리고, 2024년 1월 현재 Axon 프레임워크를 이용한 마이크로서비스 패턴 개발 예제를 올리고 있습니다.
마이크로서비스 패턴을 실제 어떻게 개발하는지 알고 싶은 분들은 읽어 보시길 추천 합니다.
https://happycloud-lee.tistory.com/274
마이크로서비스 패턴 개요
전체 mSVC 패턴 도식도
https://microservices.io/patterns/index.html
각 Case별 대표적 패턴과의 관계임
Transaction(트랜잭션)이란?
트랜잭션이란이란 '데이터베이스의 상태를 변화시키는 복수의 연산으로 구성된 하나의 논리적인 작업단위'입니다.
쉽게말해, 트랜잭션이란 어떤 일을 하기위한 작업들의 모음입니다.
동기와 비동기 처리 이해
먼저 동기와 비동기 처리의 차이에 대해 이해하여야 합니다.
동기방식은 요청하고 응답 올때까지 기다리는 방식이고, 비동기방식은 요청하고 딴일하다 나중에 응답신호가 오면 결과를 읽어 처리하는 방식입니다.
동기방식은 blocking과 non-blocking으로 나눌 수 있습니다. blocking은 응답 올때까지 다음으로 진행하지 않고 기다리는 방식이고, non-blocking은 다음으로 진행하고 응답이 오면 해당 처리를 하는 방식입니다.
그래서 non-blocking을 비동기방식과 헷갈려 합니다. jQuery같은 js에서 non-blocking여부를 나타내는 속성 이름을 async라고 해서 더욱 그렇습니다.
아래는 동기 vs 비동기 Blocking과 Non-blocking에 대해서 매우 쉽게 설명한 글입니다.
musma.github.io/2019/04/17/blocking-and-synchronous.html
위 글에 바탕을 두고 개념의 차이를 요약하면 아래와 같습니다.
Blocking방식은 요청하고 응답 올때까지 기다리는 방식이고, Non-Blocking방식은 요청하고 딴일하다 나중에 응답신호가 오면 결과를 읽어 처리하는 방식입니다.
동기방식은 요청자와 제공자사이에 계속 Connection이 맺어져 있어야 하고, 비동기방식은 Connection은 끊어지고 서로간에 이벤트를 통해 통신하는 방식입니다.
비동기방식은 요청자와 제공자 사이에 Message Broker라는 또 다른 서비스가 중계해주지만, 동기 방식은 요청자 어플리케이션에 non-blocking처리를 하는 로직이 있습니다.
최근 흔히 사용하는 REST API는 동기 방식이고 보통 Non-Blocking방식으로 통신합니다.
Frontend와 backend사이는 거의 대부분 동기+non-blocking 방식으로 구현합니다.
비동기방식 패턴 중 제일 대표적인 것이 Publish/Subscribe ( 또는 Producer-Consumer) 패턴입니다.
Publish/Subscribe패턴(줄여서 Pub-펍/Sub-썹 패턴)은 Youtube 채널구독과 동일한 방식입니다.
Publisher인 유튜버들은 구독을 요청할 수 있는 채널을 만들고, 그 채널에 동영상을 계속 publishing합니다.
Subscriber인 사용자가 특정 유튜브채널을 구독 신청하면 새로운 동영상이 업로드 될때 첫화면에 추천동영상으로 받아보거나 모바일앱 알림 등으로 받아 볼 수 있습니다.
즉, 사용자는 구독만 해놓고 있으면 새 동영상 올라올때까지 Youtube에 머물러 있을 필요가 없습니다.
아래 그림을 참조하여 비동기메시지 방식을 좀 더 이해해 보시기 바랍니다.
[출처] https://engineering-skcc.github.io/microservice%20outer%20achitecture/inner-architecture-saga/
이런 비동기 방식을 사용하는 이유는 큰 서비스를 이루는 마이크로서비스 중 하나가 실패해도 전체적인 서비스는 그대로 진행되도록 하기 위함입니다.
예를 들어 쇼핑몰 상품 주문 시 주문서비스, 결제서비스, 배송서비스가 필요한데 동기방식으로 하면 결제나 배송 서비스에 문제가 생기면 상품 주문은 실패하게 됩니다. 그러나 비동기방식을 사용하면 Queue에 처리해야할 메시지가 있기 때문에 일시적 장애가 발생해도 복구가 되면 처리 가능합니다. 따라서 상품 주문은 어떠한 경우에도 정상적으로 처리될 수 있습니다.
일반적으로 Front와 Backend 서비스간에는 동기방식을 사용하고, backend의 서비스들간에는 비동기방식을 사용합니다.
또한 데이터 조회 서비스는 동기방식을 사용하고, 데이터 변경 서비스는 비동기방식을 사용합니다.
비동기 메시징 이슈 및 해결방안은 여기를 참조하십시오.
중복메시지 제거 방안은 아직 이해하기 어려울 수 있습니다. 이 글을 모두 읽고 맨 끝에 가서 다시 읽어 보시기 바랍니다.
Pattern: Saga
Context & Problem
마이크로서비스마다 고유의 DB가 있습니다. 이 분산된 DB간의 정합성(Consistency)을 보장해야 합니다.
예를 들어 주문을 받으면 주문서비스는 새 주문데이터를 생성되고 배달서비스도 배달 요청 데이터를 생성합니다. 그 후 결제서비스에서 실패가 나면 주문과 배달서비스의 데이터를 삭제해서 전체DB의 정합성을 지켜야 합니다.
Forces:
mSVC에서 2PC(Phased Commit)는 불가능합니다.
2PC를 위해선 중지된 마이크로서비스가 있으면 안되는데 mSVC를 적용하는 의미가 없어져 버립니다.
(2PC는 연관된 서비스로부터 commit준비되었다는 응답이 와야 commit하는 방식이기 때문에 중지된 서비스가 있으면 안됩니다.)
Solution
Saga는 마이크로서비스간에 데이터정합성을 보장하면서 비동기 메시징을 할 수 있도록 고안한 패턴입니다.
(Saga는 긴 여정의 모험담을 담은 영웅전설을 뜻합니다.)
분산 트랜잭션의 문제점 복수의 서비스, 데이터베이스, 메시지 브로커에 걸쳐있는 트랜잭션에서 데이터 정합성을 유지하기 위한 전통적인 방법은 분산 트랜잭션을 이용하는 것입니다. 분산 트랜잭션은 통상 트랜잭션 안의 모든 작업이 일괄 커밋되거나 롤백되도록 보장하기 위해 2단계 커밋(two-phase commit, 2PC)을 사용합니다. 그런데 분산 트랜잭션은 몇가지 문제점을 가지고 있습니다. 우선 MongoDB, Cassandra와 같은 NoSQL 데이터베이스들과 RabbitMQ나 Apache Kafka와 같은 최신 메시지 기술들이 분산 트랜잭션을 지원하지 않습니다. 결국 분산 트랜잭션의 사용하려면 몇가지 최신 기술을 포기해야 합니다. 또 다른 문제는 분산 트랜잭션이 가용성을 감소시킨다 점입니다. 분산 트랜잭션을 커밋하기 위해서는 이에 참여하는 서비스들이 모두 가용해야 합니다. 가용성은 트랜잭션에 참여하는 서비스 가용성의 곱에 비례하기 때문에 분산 트랜잭션에 참여하는 서비스가 늘어날 수록 가용성은 감소합니다. (가용성 = 평균가용성^참여서비스 수. 서비스 수가 2개이고 평균 50%의 가용성이라면 전체 가용성은 0.5^2=25%가 됨) 표면적으로는 분산 트랜잭션이 로컬 트랜잭션과 유사하기 때문에 상당한 유용한 것처럼 보이지만, 앞서 언급된 문제들로 인해 최신 애플리케이션에는 적합하지 못합니다. 따라서 마이크로서비스 아키텍처에서는 트랜잭션의 데이터 정합성을 유지하기 위해 느슨한 연결과 비동기 서비스에 기반한 메커니즘인 Saga를 이용해야 합나다. [출처] 세상을 바라보는 또 하나의 눈 |
- Saga는 마이크로서비스내의 Transaction들로 구성되며 순서(Step)별로 Transaction들을 배치합니다. Transaction은처리Transaction과 보상 Transaction이 있습니다.
- 보상처리는 C(reate)는 D(elete)로, U(pdate)는 Another U로, D는 C로 하면 됩니다.
- Saga step별로 각 서비스를 순서대로 배치하고 서비스별로 transaction과 compensation transaction을 정의합니다. 전체서비스의 진행을 rollback할 지 진행할지의 기준이 되는 Transaction인 Pivot transaction을 정의합니다.
- Pivot transaction이 실패했을 때 compensation을 수행해야 하는 transaction들을 Compensatable transaction이라 합니다.
- Pivot transaction이 성공했을 때 계속 retry하여 결국은 처리해야하는 transaction들을 Retriable transaction이라고 합니다.
- 주문->결제->배달의 순서로 진행되는 서비스가 있고 각각 Order()->Pay()->Delivery()라는 transaction이 있다고 합시다. 이때 Pay()를 Pivot transaction으로 정의하면 Order()는 compensatable transaction이고, Delivery()는 retriable transaction이 됩니다. Pay()가 실패하면 Pay()의 보상 transaction(예: rejectOrder())이 실행되어야 합니다. Pay()가 성공하면 Delivery()는 성공할때까지 계속 반복 수행되어야 합니다.
- Compensatable transaction은 보통 Compensation(보상) transaction이 있으나 없을 수도 있습니다. (예: 고객 유효성 체크 transaction과 같은 조회성 transaction은 보상 transactin이 불필요). Pivot과 Retriable transaction은 보상 transaction이 없습니다.
- 아래 예에서 4번 step의 authorizeCreditCard()가 pivot transaction입니다. createOrder(), verifyConsumerDetails(), createTicket()는 compensatable transaction입니다. authorizeCreditCard()가 실패하면 보상transactioin인 rejectTicket()과 rejectOrder()가 수행되어 rollback처리를 합니다. approveTicket()과 approveOrder()는 retriable transaction이고 일시적 장애가 나도 이벤트메시지를 읽어 성공할때까지 재시도합니다.
[출처] 마이크로서비스패턴(크리스리처드슨)
- Choreography(자율연출)-based Saga vs Orchestration(조직화)-based Saga
- Choreography방식: 각 서비스는 타 서비스 전형 신경 쓰지 않고 이벤트에 따라 수행하고 결과를 이벤트로 발행함. “각자 알아서 한다.”
- Orchestration방식: 통제자(Controller, Orchestrator, Coordinator)가 각 서비스를 비동기로 Request/Reply하여 전체 서비스의 흐름을 제어함. “콘트롤타워가 진행한다.”
- 단순한 Saga는 Choreography방식, 복잡한 Saga는 Orchestration방식
- Orchestration방식에서 Orchestrator는 절대 business logic을 담아서는 안됩니다. 전체 프로세스 흐름을 연결하는 역할만 해야 합니다.
- 실생활 예시 : 산불진화
- Choreography: 각 지자체별 소방소, 시/군청등이 각 자 알아서 불꺼야 함. 서로 진행상황을 체크하면서 진행해야 하니까 전체적인 진행상황 파악도 어렵고, 서로간에 의존도도 높아진다. -> 이해하기 어렵고 서비스 간 결합도가 높아짐
- Orchestration: 청와대가 콘트롤타워가 되서 각 지자체 소방소와 시/군청을 지휘하여 진화함. 전체 의사소통도 단순해 지고, 서로 간에 의존도가 낮아진다. -> 단순하고 서비스 간 결합도가 낮아짐
Choreography-based Saga | Orchestration-based Saga |
|
|
[출처] 마이크로서비스패턴(크리스리처드슨)
- Orchestration-based Saga 모델링
State Machine(상태기계)로 모델링합니다. 책에서는 아래와 같이 state, transition, action으로 설명하고 있습니다. .
- State: 상태 (예: 소비자확인, 티켓생성)
- Transition(전이): 상태의 변경 (예: 소비자확인됨, 결제승인실패)
- action: 상태 변경된 후 다음 상태로 전이하기 위한 action (예: 신용카드승인 요청, 주문승인요청)
쉽게 이해하려면 action은 Command(요청), State는 요청에 대한 실행으로 변경된 데이터, Transition은 요청 처리 결과 발생한 Event로 생각하면 됩니다. Command 요청이 오면 이를 처리하는 Command Handler가 요청을 수행하게 되고 그 결과 State(상태)의 변화가 발생하고 그 수행 결과 Event가 생성되게 됩니다.
Event가 발생하면 다음 실행을 위한 Command가 Trigger(촉발)됩니다. 아래는 코끼리를 냉장고에 넣기 위한 State machine 모델링입니다. 정상적인 처리에 대해서만 기술하면 아래와 같습니다.
Command | Command Handler | State | Event |
냉장고 문을 열어라 | 문을 연다 | 문 상태 변경: 열림/안 열림 | 냉장고 문을 열었다. |
코끼리를 넣어라 | 코끼리를 넣는다 | 코끼리 상태 변경: 넣음/못넣음 | 코끼리를 넣었다. |
냉장고 문을 닫아라 | 문을 닫는다 | 문 상태 변경: 닫힘/못 닫힘 | 냉장고 문이 닫혔다. |
성공보고 해라 | 성공보고 | 레포트 생성 | 성공보고를 했다. |
위 State machine을 Saga로 표현하면 아래와 같습니다. (응답채널은 1개 입니다.)
- 마이크로서비스간 DB가 격리(Isolation)되어 있지 않아 생기는 Anomaly(변칙, 비정상)
- RDBMS는 분산 transaction환경에서도 ACID(Atomicity, Consistency, Isolation, Durability)를 통해 문제 없이 처리가 됩니다. (ACID이해하기 참고) 그러나 분산된 Saga transaction 간에는 각 마이크로서비스가 고유의 DB를 갖고 있으므로 DB Isolation을 할 수가 없습니다.
- 그래서 Saga는 ACD만 지원합니다.
- Atomicity: transaction과 보상 transaction으로 Data의 All or nothing 변경 보장
- Consistency: 한 서비스 내의 일관성은 로컬DB가, 여러 서비스간의 일관성은 어플리케이션으로 보장
- Durability: 각 서비스의 로컬DB가 보장
- DB Anomaly 유형
- Lost Updates: 데이터 생성 트랜잭션이 문제가 생겨 오래 걸렸고, 데이터 삭제 트랜잭션이 먼저 끝났다면, 데이터 생성 트랜잭션이 삭제된 데이터를 다시 생성시킬 수 있습니다.
- Dirty Read: A Transaction이 처리가 끝나지 않은 B Transaction의 commit되지 않은 데이터를 읽을 수 있습니다.
- Fuzzy/unrepeatable Read: B Transaction이 1차로 읽은 후 A transaction이 새 Data를 생성하여, B transaction이 2번째 읽을 때는 다른 결과가 나올 수 있습니다. Repeatable Read의 Phantom Read와 동일한 문제임.
- 피자주문 예시
- Lost updates:
- 새 피자주문Saga가 시작되고, 바로 주문취소 Saga가 시작되었다.
- 피자주문 -> 주문취소 순서로 진행되야 하는데 어떠한 이유로 주문취소가 먼저 수행되었다.
- 피자주문Saga에서 다시 주문데이터가 생성되어 고객은 취소한 피자를 받게 된다. - Dirty Read:
- 피자주문Saga1은 3개 주문처리를 시작했고, 바로 이어 피자주문Saga2가 1개로 정정주문을 시작했다.
- 피자주문Saga1의 포인트결재 트랜잭션이 고객포인트를 차감시켰다. 고객포인트가 0이 되었다.
- 피자주문Saga1이 완료가 안된 상태에서 피자주문Saga2의 고객검증 트랜잭션이 고객포인트가 부족하다고 주문을 거절할 수 있다. - Fuzzy/Unrepeatable Read:
- 고객 포인트는 현재 3으로 피자3개 주문이 가능하다.
- 피자주문Saga1은 1개 주문처리를 시작했고, 바로 이어 피자주문 Saga2가 3개로 정정주문을 시작했다.
- 피자주문Saga2의 고객검증 트랜잭션이 먼저 처리되었다. 3점이므로 포인트 검증은 성공한다. 그리고 바로 피자주문Saga1이 포인트결재 트랜잭션이 실행되어 포인트를 2로 만들어 버렸다.
- 피자주문Saga2는 고객 포인트 검증이 성공 했으므로 포인트 결제를 시도한다. 이때 고객 포인트를 읽어 보니 검증시에는 3점이었는데 2점으로 리턴된다.
- 피자주문Saga2가 처음에 포인트 값을 읽었을땐 3점으로 유효하지만, 피자주문Saga1이 포인트 차감 후 다시 읽어보면 포인트값이 달라지기 때문에 Unrepeatable read라고 할 수 있다.
- Lost updates:
- Lost Updates: 데이터 생성 트랜잭션이 문제가 생겨 오래 걸렸고, 데이터 삭제 트랜잭션이 먼저 끝났다면, 데이터 생성 트랜잭션이 삭제된 데이터를 다시 생성시킬 수 있습니다.
- DB Anomaly 해결안
- Symantic Lock: 데이터 생성 시 상태를 나타내는 필드를 만들고 진행 상태에 따라 이 값을 변경합니다. 다른 트랜잭션은 이 상태값을 이용하여 적절한 처리를 합니다 (예: Query시 상태값이 완료인것만 읽어 처리)
- Commutative(교환, 대체) Updates: A->B로 실행되던 B->A로 실행되던 결과가 동일한 transaction이 있다면 Saga transaction 순서 배치할 때 A와 B는 붙여서 배치함
- Pessimistic(비관적) view: 중요한 트랜잭션은 위험 최소화를 위해 처리순서를 바꾸는 방법임. 보통 Repeatable transaction으로 지정하여 항상 데이터가 갱신되도록 함.
- Reread value: 데이터 업데이트 전에 다른 트랜잭션에 의해 변경되지 않았는지 최초 읽은 데이터와 현재 상태의 데이터가 같은지 검증 후 업데이트
- Version File: 중복/오류처리를 방지하기 위해 트랜잭션 요청과 처리를 기록하는 방법. 트랜잭션은 이 기록을 참고하여 처리여부를 결정함.
- By Value: 요청의 중요도(value)에 따라 Saga를 쓸지 분산트랜잭션을 사용할 지 결정하는 방법. 중요도가 높고 위험한 요청은 분산트랜잭션을 사용합니다.
- Saga Frameworks지원하는 툴
- Axon, Eventuate ES, Eventuate Tram, LRA
Resulting Context:
Pattern: Event sourcing
Context & Problem
마이크로서비스는 본인 DB변경과 다른 MS를 위한 메시지 발행을 반드시 해야 합니다.
따라서 두 처리가 항상 정상 수행되기 위한 방법이 필요합니다.
Forces: 2PC is not an option
Solution
- RDBMS라면 Transational Outbox/Transaction Log tailing패턴이 답이 될 수 있습니다.
Transactional outbox는 메시지 발행을 위한 테이블입니다. 이 패턴은 뒤쪽에 나옵니다.
간단히 얘기하면 DB update와 Outbox table writing을 동일한 transaction으로 만들면 됩니다.
동일 transaction이므로 transaction내 처리가 성공하면 모든 DB의 데이터가 commit될테고, 실패하면 모두 rollback되므로 문제가 없습니다. RDBMS는 Atomicity(원자성)을 보장하기 때문에 가능합니다. (ACID에 대한 이해 참고)
하지만 NoSQL은 Atomicity를 보장하지 않기 때문에 불가능합니다. 마이크로서비스중 어느 하나라도NoSQL을 사용한다면 이 패턴은 사용할 수가 없습니다.
- 업데이트할 Table이 2개(이벤트 발행을 위한 Transactional Outbox Table과 서비스를 위한 Table)라서 문제니 하나를 없애면 되지 않을까 ? 뭘 없애지 ? 없애지 말고 하나로 합쳐보자!
그래서 Event store라는 테이블로 합칩니다. 이 table에 db update와 관련한 event(CUD event)를 저장합니다.
그리고 서비스 Table은 Query(조회) 목적으로만 관리하고 이 Query테이블은 Event Store의 Event를 시간 순서대로 다시 계산(Event Reply)하여 나온 최종 상태만 업데이트 합니다.
이렇게 하려면 요청된 Command를 수행하여 Event Store에 Event를 등록하고, 기존 Event들을 Reply하여 최종 상태를 계산하는 논리적(메모리상) 객체가 필요합니다.
이 논리적 객체를 Aggregate라고 부릅니다.
다시말해, Aggregate는 Command를 처리하고 Event를 등록하며 Event Replay하여 최종 상태를 계산하는 논리적인 DB객체입니다.
event store table에 CUD의 event가 모두 있으므로 Aggregate는 각 event record를 순서대로 적용(replay라고 함)하여 Aggregate의 최종상태를 계산 할 수 있습니다.
예를 들어 피자주문 Aggregate가 있고, A가 3개 주문 -> 2개 수정 -> 1개 수정 주문 했다고 한다면, event store 에는 3개의 record가 생깁니다. 피자주문 Aggregate의 최종 상태를 알려면 이 3개의 Event를 시간 순서대로 Replay하면 됩니다.
즉 최종 주문숫자는 1개가 됩니다.
Command가 수신되면 Aggregate의 Command Handler가 수행되고 그 결과 Event를 event store에 물리적으로 writing합니다. 그리고 그 Event를 메시지브로커로 발송하면 됩니다. - Persistent DB와 Event sourcing의 차이
- 물리적인 DB대신 논리적인 Aggregate로 처리
- 기존 Persistent DB는 데이터의 최종 상태만을 저장하는 반면 Aggregate는 최종상태를 이벤트들을 replay하여 계산합니다.
- Event sourcing에서도 Query(조회) 목적으로 Event Reply한 최종 결과를 물리적 DB에 CUD합니다.
- Event soucing의 특징
- Aggregate와 Event Store는 1:1입니다.
- 이벤트 저장소는 오직 insert만 필요합니다. 기존 event record를 수정하거나 삭제는 불가합니다.
- 성능향상을 위해 snapshot을 이용합니다. : 시간 또는 이벤트갯수를 기준으로 별도의 snapshot저장소에 그 시점의 최종상태를 저장하고, 적용할 이벤트 목록을 구할 때 shapshot데이터 이후 이벤트만 처리합니다.
- 멱등성(Idempotent)을 보장하여 Immutable(불변)합니다. => 몇번을 다시 실행해도 결과는 항상 똑같다는 의미입니다.
- CQRS패턴 반드시 필요: 조회전용DB를 구성하여 SQL을 통해 쉽게 Query할 수 있도록 합니다. 이게 CQRS패턴입니다. Aggregate 최종 상태 구성을 위한 event replay 시 조회전용DB도 CUD합니다.
- Saga & Event sourcing
- Saga내 서비스가 ACID를 지원하지 않는 NoSQL DB를 사용하는 경우는 Event sourcing패턴을 적용하는 것이 좋음. 반대로 전부 RDBMS만 사용한다면 Event sourcing대신에 Transactional Outbox, Transaction Log tailing, Polling publisher패턴을 사용할 수 있습니다.
- choreography-based Saga와 Event sourcing은 환상의 짝꿍 BUT 현실에 부적합
Aggregate 생성/수정과 메시지발행이 원자적(All or nothing)으로 처리되기 때문입니다.
그러나, 1) choreography-based Saga는 단순한 프로세스에만 적합하고, 2) Aggregate의 변화가 없어도 무조건 메시지를 발행해야 하고, 3) Aggregate 자체가 없는 Saga 참여자가 있을때도 있어 현실에서 사용하긴 부적합니다. - orchestrated-based Saga와 Event soucing은 문제많은 짝꿍 BUT 최고의 솔루션
원자적으로 처리하기 힘든 경우가 많아 ACID를 지원하는 RDB를 사용하는것이 편합니다. NoSQL사용은 해결할 이슈가 많습니다.
- Aggregate 생성/수정과 Saga orchestrator의 생성을 원자적으로 처리해야 함
- Saga orchestrator는 응답 소비, 본인 상태 업데이트, 커맨드메시지 발송을 원자적으로 처리해야 함
- Saga 참여자: 메시지 소비, 중복메시지 제거, 애그리커드 생성/수정, 응답메시지 발송을 원자적으로 처리해야 함
- 예시
- 새 주문이 요청되면 기존 이벤트저장소의 모든 이벤트를 다시 실행하여 최종 주문데이터를 구성합니다
- 새 주문 이벤트의 비즈니스로직을 수행한 후 이벤트저장소에 추가합니다
- 다른 서비스를 위해 메시지브로커에 메시지를 발송하고, Query전용 DB를 업데이트 합니다.
Result Context
- 연관 서비스의 멱등성(Idempotent) 보장
이벤트저장소에 등록된 이벤트(예: 새로운 주문 요청)는 다른 서비스(예: 결제, 음식점, 배달 등)의 수행을 위해 메시지가 발행되어야 합니다. 그래서 이벤트저장소 처리기는 이벤트저장소에 이벤트를 저장하고 반드시 메시지브로커의 응답채널에 메시지를 발행 합니다.
그럼 그 응답채널을 구독한 메시지컨슈머(다른 서비스)가 자신의 일을 하게됩니다.
그런데 여기서 메시지브로커의 응답채널에 동일한 메시지가 중복해서 발송될 수 있습니다.
이의 해결책은 메시지브로커가 중복메시지를 제거하면 됩니다. event store가 RDB일때와 NoSQL일때 다릅니다.- Event store가 RDB일때
- 메시지발행 후 메시지발행여부테이블에서 해당 event를 찾아 발행여부flag를 변경합니다. 이 작업은 메시지발행과 같은 transaction으로 하여 Atomicity(원자성-db writing은 All or nothing)성질에 따라 수행을 보장합니다.
- 메시지브로커는 메시지발행여부 테이블을 보고 중복메시지를 걸러냅니다.
- Event Store가 NoSQL일때
- 별도의 테이블이 아닌 event store의 해당 event record의 발행여부flag를 변경합니다.
- 메시지브로커는 event store의 발행여부 flag를 보고 중복메시지를 걸러냅니다.
- Event store가 RDB일때
- 스키마 변화 처리
- Aggregate의 스키마가 변한 경우 이벤트저장소에서 이벤트 record를 읽은 후 업캐스터 컴포넌트를 이용해 최신스키마로 변환하고 사용하면 됨. 쉽게 말하면 프로그램적으로 최신 스키마로 변환하면 된다는 의미입니다.
참고
- Implementing Event sourcing & CQRS ( 심천보, 쿠팡 )
- Event sourcing & CQRS sample source ( 심천보 )
- 백근영.log: axon을 이용한 event sourcing, CQRS 구현하기
Pattern: API Composition
Case:
마이크로서비스는 자신만의 DB를 갖고 있고 격리되어 있다. 다른 마이크로서비스의 DB와 SQL로 Join이 불가능하다.
Solution:
- 각 서비스의 API를 호출하여 그 결과를 조합하는 API조합기를 만듭니다.
- API조합기는 Client단에 만들 수 있습니다. ajax()로 화면 partial refresh를 하는 방법이 대표적인 예입니다.
예를들면, 주문현황 화면은 주문서비스(음식점명 리턴), 주방서비스(조리상태 리턴), 배달서비스(배달상태, 배달예정시간 리턴), 회계서비스(결제상태 리턴)를 ajax의 non-blocking방식으로 호출하여 결과가 리턴되면 화면의 해당 영역만 바꾸는 방식을 사용합니다. - API조합기는 서버단에 만들 수도 있습니다. API Gateway나 BFF(Backend For Frontend)로 구현합니다.
이 패턴은 서버단의 API조합기가 각 서비스의 API를 호출하여 받은 결과를 조합하여 Client에 반환합니다.
위 Client단에 위치할 때는 Client와 Server간에 트래픽이 4번 이었는데, Server단에 위치하면 1번으로 줄어듭니다. - API조합기를 Client에 둘것인가? Server에 둘것인가 ?
- Client(웹브라우저, 모바일단말기)가 서버와 같은 네트워크 Zone (예: 인트라넷) => Client단
- Client가 서버와 다른 네트워크 zone(예: 모바일단말기같이 방화벽 외부)이거나 Client-server간 네트워크가 느림
- 취합로직이 복잡하면 BFF 어플리케이션 개발
- 비즈니스로직 없이 기계적 취합만 하면 되면 API Gateway - BFF는 마이크로서비스별 1개가 아니라 Client type(웹, 모바일, 3rd party 등)별, 최상위 또는 차상위 domain별 등 적절한 기준별로 만드십시오.
Resulting context:
장단점
- 네트워크 트래픽 증가: SQL문 하나만 끝났는데, 서비스가 분할 되어 있으니 여러번 호출할 수밖에 없음
- 서비스 응답 실패 처리 필요: 서비스가 중지되거나 네트워크 문제로 응답이 실패한 경우 처리 => 캐시데이터 반환, 실패된 서비스 빼고 반환
- 데이터 일관성 결여: 서비스간의 데이터 정합성이 일시적으로 안 맞을 수 있기 때문에 잘못된 정보가 응답될 수 있음
Pattern: CQRS(Command Query Responsibility Segregation)
Context & Problem:
마이크로서비스는 자신만의 DB를 갖고 있고 격리되어 있다. 다른 마이크로서비스의 DB와 SQL로 Join이 불가능하다.
단건은 API Composition패턴으로 가능하지만, 다건(목록 리턴)은 성능이 너무 떨어진다.
또한 데이터를 저장한 DB가 Query에 부적합 할때도 많고(예: RDB는 복합필터링이나 검색엔 취약), 비즈니스적으로 더 중요한 데이터의 생성/수정에 개발역량을 집중(예: 주문이력 제공보다 신규주문 받는게 더 중요)하고 싶어합니다.
Solution:
- Command와 Query를 분리합니다. 다시말해, 데이터의 CUD용 DB와 R용 DB를 분리하여 처리합니다.
- 타 서비스 DB와 내 서비스 로컬복제DB를 1:1로 만드는것이 아닙니다. 타 서비스 DB의 데이터를 조합하여 내 서비스에 필요한 View DB를 만드는것입니다. (예: 주문,주방,배달,회계서비스 Data를 조합한 주문이력View DB를 만듦)
- 타DB 변경에 대한 이벤트메시지를 구독하여 변경 시마다 메시지큐로 변경 메시지를 받아 View DB를 업데이트합니다.
- Query최적화를 위해 로컬복제본의 스키마 또는 DB종류는 원본과 다르게 만들수도 있습니다.
- 가능한 API Composition패턴을 사용하고, 꼭 필요한 경우에만 CQRS패턴을 사용하십시오.
Resulting contenxt
- 장점: 비즈니스 중요도가 높은 서비스에 집중, 분산DB의 Join문제 해결, Query최적화 View DB사용 가능
- 단점: 아키텍처 복잡도 증가, 복제 지연으로 부정확 가능성
CQRS View 설계 고려사항
CQRS View 아키텍처는 아래와 같습니다.
구독한 채널에 메시지가 오면 Event handler는 Data access를 통해 View DB를 업데이트합니다.
[출처] 마이크로서비스 패턴(크리스리처드슨 지음)
View 설계의 고려사항도 각 컴포넌트별로 아래와 같습니다.
- Event handler, Data access: 안전한 db update(동일 record를 동시 업데이트 방지, 멱등성 보장), 복제 시차 완화
- View DB: DB유형선정 및 스키마 설계, View DB 재구성
각 고려사항에 대한 보다 자세한 설명입니다.
- DB유형선정 및 스키마 설계 : key-value, 문서조회용, 검색용, SQL용에 따라 적합한 DB를 선정합니다.
key-value: redis | 문서조회용: mongodb, DynamoDB | 텍스트검색용: Elastic search | SQL용: RDB
스키마설계는 요구사항에 따라 다릅니다.
- 안전한 db update
- 동일 record 업데이트 방지: locking
- 멱등성보장: 중복메시지 제거로 해결. 중복처리 방지는 이 글 하단의 '메시지컨슈머의 중복 처리 방지 방안' 참조
- 복제시차완화: 복제 시차로 일시적으로 데이터가 틀려지는 경우는 어느정도 감수할 수 밖에 없습니다.
- View DB 재구성: 기존 View DB 스키마 변경, 새 View table의 추가 등으로 view DB를 재구성할 필요가 반드시 생김
- 메시지브로커는 이벤트를 영구 보관 안하므로, 과거 이벤트들을 아카이빙하고 아카이빙이벤트+현재이벤트를 reply하여 재구성
- snapshot을 이용하여 reply를 빠르게 할 수 있습니다.
Pattern: 외부 API패턴 - API Gatway & BFF
Context & Problem
모놀리식어플리케이션이 마이크로서비스로 전환화면서 클라이언트(웹브라우저, 모바일앱, 3rd party앱)가 호출해야할 서버상의 서비스가 훨씬 많아졌습니다. 클라이언트가 필요한 서비스를 찾아서 데이터를 요청하고 여러 서비스에서 받은 데이터를 조합하는게 점점 더 힘들어집니다. 또한, 서버상의 마이크로서비스의 API가 변하면 클라이언트는 소스를 수정해야 합니다.
요약하면, 마이크로서비스화 되면서 클라이언트는 서비스탐색, 데이터조합, Tight coupling 이슈가 생깁니다.
Solution
- Client와 마이크로서비스 간에 API Gateway를 배치합니다.
- API Gateway는 마이크로서비스 캡슐화, 서비스탐색 및 라우팅(Reverse proxying), API조합(응답 데이터조합)을 합니다.
- 인증(Authentication) 및 인가(Authorization), 캐싱, 사용량제어, 메트릭수집, 요청로깅과 같은 엣지(edge-주변)기능도 제공합니다.
- API Gateway 아키텍처 및 ownership : Client별 API Layer + Edge기능을 제공하는 Common Lay로 구성
Ownership은 전담팀모델, Netflix모델, BFF모델이 있습니다.
전담팀모델은 API Gateway 전담팀이 모든 API를 관리하는 방식입니다.
Netflix모델은 API Layer는 각 Client team에서 담당하고, API Gateway팀(별도구성 필요)은 Common layer와 운영을 담당하는것입니다.
주의) Client유형별로 Team을 만드는것이 아닙니다. 각 서비스의 Client팀들이 API Layer에서 본인 서비스의 API만 관리하는 것입니다.
Netflix 모델
[출처] 마이크로서비스패턴 (크리슨리처드슨 지음)
BFF모델은 Client 유형별로 API Gateway를 구성하는 방식입니다. Netflix도 현재 BFF모델로 이전중입니다.
주의) Client유형별로 Team을 만드는것이 아닙니다. 각 서비스의 Client팀들이 API Layer에서 본인 서비스의 API만 관리하는 것입니다.
BFF 모델
[출처] 마이크로서비스패턴 (크리스리처드슨 지음)
- API Gateway 제품
각 제품의 단점만 설명합니다.
아래는 상용제품입니다.
- AWS API Gateway: API조합 지원 안함
- ALB (AWS Load Balancer): API조합, 인증 로직 없음
- IBM API Connect:
아래는 오픈 소스입니다.
- Kong: nginx HTTP서버 기반. API조합 지원 안함
- Traefik(트래픽): 고 언어 기반 API Gateway. API조합 지원 안함
- API Gateway 개발 프레임워크
직접 API Gateway를 개발할때 사용할 수 있는 개발 프레임워크입니다.
- Zuul: 넷플릭스 Zuul, 스프링클라우드 Zuul. 메소드기반 라우팅 지원하지 않음(예: GET /orders를 A서비스 라우팅은 되나, POST /orders를 B서비스로 라우팅하는건 안됨)
- 스프링클라우드 게이트웨이: 크리스리처드슨은 이 프레임워크를 추천함
- 페이스북 GraphQL: 웹클라이언트가 서버로부터 데이터를 효율적으로 가져오기 위한 Query Language입니다. API조합이 가능하여 한번의 API호출로 Client별로 맞는 데이터를 가져올 수 있습니다. ( GraphQL 개념잡기 참조 )
Resulting Context
이슈: 성능, 콜백지옥, 부분실패
- 성능: Client와 API Gateway간에는 REST API 동기방식을 사용하는데, blocking과 non-blocking 중 어떤것이 나은지를 기능과 성능 관점에서 고려해야 합니다.
- 콜백지옥: API Gateway와 backend서비스간에 non-blocking방식(병렬로 호출하고 callback으로 리턴 받아 데이터 조합)이 좋은데 callback지옥(중첩된 callback으로 소스가 복잡해짐)에 빠지지 않기 위해 리액티브 프로그래밍을 해야 합니다. 리액티브 프로그래밍을 지원하는 툴은 아래와 같습니다.
- node.js의 promise, Mono, RxJava, 스칼라 Future, Java8 CompletableFutures
- 좀 더 개발적인 정보가 필요하면 RxJava로 리액티브프로그래밍 이해를 참조하세요.
- 부분실패: backend 서비스의 일시적 장애 시 API Gateway가 무한정 기다리지 않기 위해 Circuit break와 같은 처리가 필요합니다.
Pattern: Transactional Outbox, Transaction Log tailing, Polling Publisher
Context & Problem
마이크로서비스는 본인 DB변경과 다른 MS를 위한 이벤트 메시지 발행을 반드시 해야 합니다.
두 처리가 항상 정상 수행되기 위한 방법이 필요합니다.
Solution
- 여러 transaction을 하나로 처리할 수 있는 RDBMS에만 적용 가능합니다.
- DB변경과 outbox테이블(이벤트를 Message Broker로 내보낼때 사용되는 table)기록을 한개의 transaction으로 한다. -> Transactional outbox 패턴
- outbox table을 주기적으로 polling하여 Message broker로 보낸다 -> Polling publisher패턴
- outbox table의 변경 이벤트가 발생할 때 Message broker로 보낸다. -> Transaction Log tailing 패턴.
outbox table에 기록되면 transaction log에도 자동 기록되고, transaction log miner가 이 transaction log를 읽어 Message broker로 보내는 방식입니다. - 지원하는툴: Debezium, LinkedIn Databus, DynamoDB Streams, Eventuate Tram
메시지 중복 처리 방지 방안
이제 여기를 읽어 보시면 중복처리 방안에 대해 이해가 되실 겁니다.
모든 Microservice패턴에 대한 마인드맵이 있어 공유합니다.
[출처] Microservices Patterns - Conceptual Map by Gabriel Moral
이상으로 마이크로서비스의 주요패턴 설명을 마치겠습니다.
수고 하셨습니다.
'Micro Service > mSVC&MSA' 카테고리의 다른 글
Case별 마이크로서비스 패턴, 메시지 구조 (0) | 2020.09.01 |
---|---|
ACID 이해하기 (0) | 2020.07.29 |
비동기메시징 이슈 및 해결방안 (0) | 2020.07.28 |
마이크로서비스 핵심만 빠르게 이해하기 (0) | 2020.07.28 |
DDD 핵심만 빠르게 이해하기 (14) | 2019.12.22 |
- Total
- Today
- Yesterday
- CQRS
- 스핀프로젝트
- 육각형인간
- AXON
- 분초사회
- 마이크로서비스
- 요즘남편 없던아빠
- 호모프롬프트
- 디토소비
- API Composition
- SAGA
- 애자일
- 마이크로서비스 패턴
- 버라이어티가격
- Event Sourcing
- 스포티파이
- micro service
- 리퀴드폴리탄
- 돌봄경제
- spotify
- 도파밍
- agile
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |