SW마에스트로에서 본 프로젝트를 MSA로 설계, 개발하면서 마이크로서비스 간 통신을 어떻게 가져가야할지 고민을 하였다.
프로젝트를 MSA로 설계, 개발하면 마이크로서비스 간 통신은 필연적으로 발생하며 이 과정에서 많은 문제점이 발생할 수 있다.
크게 두가지를 고민하였다.
먼저 분산 트랜잭션 처리 고민이다.
MSA는 polyglot persistence
라는 특징을 가지고 있으며 데이터베이스가 1개가 아닌 N개(마이크로서비스 개수), 즉 각각의 데이터는 각각의 마이크로서비스에서만 접근 가능하도록 책임을 가지고 캡슐화 되어있기 때문이다.
예를 들어 주문에 대한 정보는 주문 마이크로서비스가 책임을 가지지만 재고에 대한 정보는 재고 마이크로서비스가 책임을 가지고 있으므로 사용자가 물건을 주문하면 주문 마이크로서비스와 재고 마이크로서비스 간 통신은 필연적으로 발생하도록 설계할 수 밖에 없다.
그래서 가장 먼저 고민하는 문제는 마이크로서비스 간 통신 시 분산 트랜잭션을 어떻게 처리할 것인가이다. MSA가 아닌 모놀리식으로 설계, 개발할 경우 이에 대한 고민을 할 필요가 없다. 그냥 단일 트랜잭션으로 처리하여 부분 갱신이 일어나지 않도록 하면 된다. 주문을 하려고 봤더니 재고가 부족하면 롤백처리가 대표적이다.
하지만 MSA의 경우 단일 트랜잭션 처리가 불가능하다. 물리적으로 데이터베이스가 분리되어 있기 때문이다. 즉 모놀리식의 경우 트랜잭션에 대한 책임을 디비가 가지면 되지만 MSA는 비즈니스 로직으로 커밋, 롤백 등 트랜잭션에 대한 책임을 가지도록 구현해야 한다.
주문 마이크로서비스에서 주문에 대한 트랜잭션을 처리하고 재고 마이크로서비스로 통신을 하여 재고 차감을 시도하는데 재고가 부족하다면 보상 트랜잭션을 발행하여 방금 했던 주문을 다시 취소하는 것이 대표적이다.
분산 트랜잭션 처리는 Saga Pattern
, Two Phase Commit
TCC
등으로 구현 가능하다.
이렇게 MSA는 데이터베이스가 여러 개로 구성함으로써 분산 트랜잭션에 대한 처리가 어렵다는 단점이 있지만 장애 격리 측면에서 해당 서비스의 장애를 격리하고 다른 서비스는 사용자에게 서비스를 제공할 수 있다는 장점이 있다. 즉 주문은 안되지만 재고를 보여주는 기능은 정상동작 하도록 하는 것이다. 모놀리식으로 구성한다면 디비가 하나이기 때문에 디비 장애 시 모든 서비스를 사용자에게 제공하지 못했을 것이다.
우리는 분산 트랜잭션 관점에서도 고민이지만 네트워크 관점에서도 마이크로서비스 간 통신에 대한 고민을 해야한다.
예를 들어 주문 마이크로서비스에서 재고 마이크로서비스로의 통신이 무조건 성공한다는 보장이 없다. 모종의 이유로, 혹은 오류로 인해, 혹은 해당 서버에 부하가 많이 쌓여 레이턴시가 매우 길어지는 등등의 사유로 인해 통신이 이루어지지 않을 수 있다. 이게 문제점인게 이 장애가 전파된다는 문제점이 있다.
예를 들어 클라이언트가 주문 요청을 보냈는데 -> 주문 마이크로서비스에서 재고 확인을 위해 재고 마이크로서비스로 통신 -> 재고 마이크로서비스로 요청을 보냈으나 여기서 타임아웃이 뜨면 주문 마이크로서비스는 재고 마이크로서비스로부터의 응답시간을 기다리는 동안 그 장애가 클라이언트에게까지 도달한다.
이때 우리는 통신을 재시도를 할 수도 있고 혹은 해당 요청을 실패로 보고 보상 트랜잭션을 발행하는 방법도 있다.
이에 대한 대표적인 디자인 패턴으로 circuit breaker
가 있다. 뒤에서 알아보자.
마이크로서비스 간 통신은 크게 두가지 방법이 있다. 1. 동기 통신 2. 비동기 통신이다.
각 패턴은 장단점이 있어 로직에 따라 필요한 패턴을 적용한다.
동기 통신은 대표적으로 http
, grpc
를 이용해 구현한다.
동기 통신은 A -> B로 통신 시 B에 대한 응답을 기다리고 그동안 A는 블로킹된다.
블로킹,, 블로킹이 핵심이다. 일단 위에서 언급한 장애전파
가 클라이언트에게까지 도달할 수 있다. 또한 동기 통신을 사용하면 사용할수록 마이크로서비스 간 결합도는 증가한다.
이때 사용할 수 있는 디자인 패턴인
circuit breaker
에 대해 다음 시간에 알아보자
비동기 통신은 A -> B로 통신 시 B에 대한 응답을 기다리지 않는다. 해당 요청을 비동기적으로 처리한다.
비동기 통신을 하고자 한다면 중간 매개체가 필요하다. 주로 메시지 브로커
역할을 수행하는 Apache Kafka
나 aws SQS
, RabbitMQ
등의 솔루션을 사용할 수 있다.
결합 감소
동기 통신은 소비자에 대한 정보를 알아야하고 그 과정에서 마이크로서비스 간 결합도가 증가한다. 하지만 비동기 통신은 메시지 발신자가 소비자에 대해 알 필요가 없어 마이크로서비스 간 결합도가 감소한다.
오류 격리
소비자가 실패하더라도 발신자는 여전히 메시지를 보낼 수 있다. 소비자가 복구되면 메시를 수신하여 처리한다. 서비스는 언제든 사용할 수 없게 되거나 새 버전으로 교체될 수 있다. 비동기 메시징은 이 과정에서 일시적 가동 중지 시간을 처리할 수 있다. 반면에 동기 통신은 이 사이에 무조건 실패한다.
응답
업스트림 서비스는 다운스트림 서비스를 기다리지 않으면 더 빨리 응답할 수 있다. 서비스 종속성 체인(서비스 A가 B를 호출하고 B가 C를 호출하는)이 있는 경우 동기 호출에 대기하는 시간만큼 응답시간이 늘어날 수 있다.
부하 평준화
메시지 수신자는 자체 속도에 맞게 메시지를 처리할 수 있다.
메시징 인프라와 결합
특정 메시징 인프라를 사용하면 해당 인프라와 밀접하게 결합될 수 있다. 그렇게 되면 나중에 다른 메시징 인프라로 전환하는 것이 어렵다.
대기 시간
메시지 큐에 메시지가 쌓이면 쌓일수록 앞선 메시지를 처리하느라 늦게 들어온 메시지는 엔드투엔드 대기 시간이 그만큼 길어질 수 있다.
비용
처리량이 높으면 메시징 인프라에 대한 금전적인 비용이 늘어날 수 있다.
복잡성
비동기 메시징 처리는 구현이 매우 어렵다. 메시지를 중복 처리될 수 있는 여지에 대해 어떻게 문제점을 해결할 것인가도 고민이다. 또한 비동기 메시징을 사용하여 요청-응답 의미 체계를 구현하는 것도 어렵다. 응답을 보내려면 다른 큐가 필요하고 요청과 응답 메시지를 상호 연결하는 방법이 필요하다.
비동기 통신을 활용하여 전반적인 MSA 시스템을 이벤트 주도 아키텍처
로 설계할 수 있다.
이벤트 주도 아키텍처는 - 확장성이 뛰어난 애플리케이션을 생성하는데 널리 사용되는 분산 비동기 아키텍처로 이벤트 발생시 해당 이벤트 로그를 보관하고 이를 기반으로 동작하며 비동기 통신을 통해 시스템 내 통합(integration)을 수행한다.
이벤트 주도 아키텍처는 MSA의 단점을 보완하고자 등장하였다.
다음 시간에 자세히 알아보겠다!
https://learn.microsoft.com/ko-kr/azure/architecture/microservices/design/interservice-communication