본문 바로가기
MSA

[MSA][Saga] 사가 예시

by Real Iron 2023. 12. 22.

사가 예시

Saga 패턴은 분산 애플리케이션의 일관성을 설정하고 여러 마이크로서비스 간의 트랜잭션을 조정하여 데이터 일관성을 유지하는 데 도움이 되는 오류 관리 패턴입니다. 이 프로젝트는 오케스트레이션 기반 사가 구현을 보여줍니다.

관련 저장소(10K+ LOC):

다음 구성 요소를 포함하는 올인원 docker-compose 배포가 제공됩니다.

  • Traefik - 외부 트래픽 라우팅 및 내부 grpc 로드 밸런싱을 담당하는 에지 프록시입니다.
  • 계정 서비스 - 로그인, 가입, 인증, 토큰 관리를 처리하는 서비스입니다.
  • 구매 서비스 - 구매를 생성하고 각 사가 단계의 결과를 스트리밍하는 서비스입니다.
  • 거래 서비스
    • 상품 서비스 - 상품을 생성하고 확인하는 서비스입니다. 제품 재고를 업데이트합니다.
    • 주문 서비스 - 주문을 생성하고 조회하는 서비스입니다.
    • 결제 서비스 - 결제를 생성하고 조회하는 서비스입니다.
    • 오케스트레이터 서비스 - 상태 비 저장 사가 오케스트레이터.
  • 로컬 데이터베이스
    • 계정 데이터베이스(MySQL 8.0)
    • 제품 데이터베이스(MySQL 8.0)
    • 결제 데이터베이스(MySQL 8.0)
    • 주문 데이터베이스(MySQL 8.0)
  • 6노드 Redis 클러스터
    • 계정, 제품, 주문 및 결제를 위한 메모리 내 캐시로 사용됩니다.
    • 캐시 침투 방지를 위한 블룸/뻐꾸기 필터( Redis Bloom 사용 )
      • 가능한 문제: 데이터베이스를 쿼리하기 전에 항상 Bloom/Cuckoo 필터를 확인하므로 Redis와 MySQL 간의 데이터 일관성에 대해 크게 답변합니다. 그러나 필터 업데이트가 실패하거나 Redis에서 필터를 제거하면 캐시와 스토리지 간에 데이터 상태가 일관되지 않아 가양성 쿼리가 발생합니다.
      • 해결 방법: 블룸 필터 쿼리를 다른 서비스로 분리하고 메시지 브로커(예: Kafka)를 사용하여 최소 한 번 전달(TODO)을 보장하기 위해 모든 데이터 수정 이벤트를 처리합니다.
    • 캐시 사태를 방지하기 위한 분산 잠금 장치
    • 로컬 캐시 무효화를 위한 게시/구독으로 사용됩니다.
    • 실시간 구매 결과를 얻기 위한 스트리밍 플랫폼입니다.
  • 가시성
    • Prometheus - 모든 서비스에서 측정항목을 가져옵니다.
    • Jaeger - 서비스 경계에 걸쳐 추적 범위를 보존하고 쿼리합니다.
  • NATS 스트리밍 - saga 명령 및 이벤트에 대한 메시지 브로커입니다.

다음 다이어그램은 아키텍처에 대한 간략한 개요를 보여줍니다.

이 다이어그램에는 캐시 데이터 흐름, 블룸 필터 및 로컬 데이터베이스가 생략되어 있습니다.

용법

docker-compose v1을 통해 모든 서비스를 로컬로 실행하려면 다음을 실행하세요.

./run.sh run
 

그러면 모든 서비스와 Docker 컨테이너의 복제본이 부트랩됩니다.

모든 서비스를 중지하려면 다음을 실행합니다.

./run.sh stop
 

계정 서비스

먼저, 새로운 사용자를 등록해야 합니다:

curl -X POST localhost/api/account/auth/signup \
    --data '{"password":"abcd5432","firstname":"ming","lastname":"hsu","email":"ming@ming.com","address":"taipei","phone_number":"1234567"}'
 

사용자 계정 로그인:

curl -X POST localhost/api/account/auth/login \
    --data '{"email":"ming@ming.com","password":"abcd5432"}'
 

그러면 새 토큰 쌍(새로 고침 토큰 + 액세스 토큰)이 반환됩니다. Authorization인증을 통해 해당 API의 헤더 에 액세스 토큰을 제공해야 합니다 .

새로 고침 토큰으로 새로 고쳐서 새 토큰 쌍을 얻을 수 있습니다.

curl -X POST localhost/api/account/auth/refresh \
    --data '{"refresh_token":"<refresh_token>"}'
 

사용자 개인 정보 얻기:

curl localhost/api/account/info/person -H "Authorization: bearer <access_token>"
 

사용자 개인 정보 업데이트:

curl -X PUT localhost/api/account/info/person -H "Authorization: bearer <access_token>" \
    --data '{"firstname":"newfirst","lastname":"newlast","email":"ming3@ming.com"}'
 

사용자 배송 정보 가져오기:

curl localhost/api/account/info/shipping -H "Authorization: bearer <access_token>"
 

사용자 배송 정보 업데이트:

curl -X PUT localhost/api/account/info/shipping -H "Authorization: bearer <access_token>" \
    --data '{"address":"japan","phone_number":"54321"}'
 

제품 서비스

다음으로 몇 가지 새로운 제품을 만들어 보겠습니다.

curl -X POST localhost/api/product \
     --data '{"name": "product1","description":"first product","brand_name":"mingbrand","price":100,"inventory":1000}'
curl -X POST localhost/api/product \
     --data '{"name": "product2","description":"second product","brand_name":"mingbrand","price":100,"inventory":10}'
 

API는 생성된 제품의 ID를 반환합니다.

페이지 매김이 있는 모든 제품을 나열합니다.

curl "localhost/api/products?offset=0&size=100"
 

ID, 이름, 가격, 현재 재고를 포함한 제품 카탈로그 목록이 반환됩니다.

제품 세부정보 확인:

curl "localhost/api/product/<product_id>"
 

그러면 쿼리된 제품의 이름, 설명, 브랜드, 가격 및 캐시된 재고가 반환됩니다.

구매 서비스

여기에 핵심 부분이 있습니다. 우리는 새로운 구매 이벤트를 사가 오케스트레이터에 보내고 분산 트랜잭션을 트리거하는 새로운 구매를 생성할 것입니다. 성공하면 새 구매의 ID가 반환됩니다.

curl -X POST localhost/api/purchase -H "Authorization: bearer <access_token>" \
    --data '{"purchase_items":[{"product_id":<product_id>,"amount":1}],"payment":{"currency_code":"NT"}}'
 

구매를 생성한 후 구독하여 실시간 거래 결과를/api/purchase/result 받을 수 있습니다 . 구매 서비스는 서버 전송 이벤트(SSE)를 사용하여 결과를 푸시합니다 . 다음 코드 예제에서는 Javascript를 사용하여 서버에서 보낸 이벤트를 구독하는 방법을 보여줍니다. 이 라이브러리를 사용하여 헤더 와 함께 SSE 요청을 보냅니다 .Authorization

var script = document.createElement('script');script.src = "https://unpkg.com/event-source-polyfill@1.0.9/src/eventsource.js";document.getElementsByTagName('head')[0].appendChild(script);
var es = new EventSourcePolyfill('http://localhost/api/purchase/result', {
  headers: {
    'Authorization': 'bearer <access_token>'
  },
});
var listener = function (event) {
  var data = JSON.stringify(event.data);
  console.log(data);
};
es.addEventListener("data", listener);
 

구독이 성공하면 다음과 같은 실시간 결과를 받게 됩니다.

주문 서비스

다음으로 주문이 성공적으로 생성되었는지 확인할 수 있습니다.

curl "localhost/api/order/<payment_id>"  -H "Authorization: bearer <access_token>"
 

결제 서비스

마지막으로 결제가 성공적으로 생성되었는지 확인할 수 있습니다.

curl "localhost/api/payment/<payment_id>"  -H "Authorization: bearer <access_token>"
 

관찰 가능성

Prometheus 측정항목을 노출하고 추적 범위를 보내도록 모든 서비스를 구성할 수 있습니다. 기본적으로 모든 서비스에는 prometheus 지표 엔드포인트가 port 에 노출되어 있습니다 8080. JAEGER_URL분산 추적의 경우 환경 변수를 Jaeger 컬렉션 엔드포인트 로 설정하여 간단히 활성화할 수 있습니다 .

모든 서비스가 활성 상태인지 확인하려면 Prometheus( )를 방문하세요 http://localhost:9090/targets.

Jaeger 웹 UI를 방문하세요 http://localhost:16686. Traefik에서 시작하여 API 호출 체인의 모든 추적 범위를 확인할 수 있습니다. 예를 들어 다음 그림은 를 쿼리하는 요청을 보여줍니다 /api/order/<order_id>. auth.AuthService.Auth주문 서비스는 요청을 받으면 계정 서비스에서 제공하는 gRPC 인증 API인 를 호출하여 먼저 요청을 인증하는 것을 볼 수 있습니다 . 인증이 성공하면 주문 서비스는 계속해서 요청을 처리합니다. 전체 주문을 받기 위해 주문 서비스에서는 다른 gRPC 호출을 통해 구매한 제품의 세부정보를 제품 서비스에 요청합니다 product.ProductService.GetProducts.

좀 더 복잡한 예를 살펴보겠습니다. 이 그림은 새로운 구매를 생성한 후 거래 서비스가 서로 상호 작용하는 방식을 보여줍니다. 인증 프로세스는 이전 예와 유사합니다. CreatePurchaseCmd구매 서비스는 요청을 성공적으로 인증한 후 메시지 브로커에 이벤트를 게시합니다 . 그런 다음 Orchestrator 서비스는 이벤트를 수신하고 saga 트랜잭션을 시작합니다. 다음 다이어그램은 스트리밍 결과 및 Redis 작업의 추적을 포함하여 단일 구매의 모든 관련 추적을 보여줍니다.

각 트랜잭션 서비스는 이벤트를 게시하기 전에 이벤트에 현재 범위 컨텍스트를 추가합니다. 구독자가 새 이벤트를 수신하면 이벤트 페이로드에서 범위 컨텍스트를 추출합니다. 이렇게 추출된 범위는 현재 범위의 상위 범위가 됩니다. 이렇게 하면 모든 트랜잭션에 걸쳐 전체 게시/구독 호출 체인을 생성할 수 있습니다.

또한 Jaeger는 범위에 대한 서비스 토폴로지를 생성합니다. 다음 그림은 클라이언트가 새 구매를 생성할 때의 토폴로지를 보여줍니다.