마이크로서비스패턴[3] — 3장
3장 프로세스 간 통신
1장 모놀리식 지옥에서 벗어나라
2장 분해 전략
3장 프로세스 간 통신
4장 트랜잭션 관리: 사가
5장 비즈니스 로직 설계
6장 비즈니스 로직 개발: 이벤트 소싱
7장 마이크로서비스 쿼리 구현
8장 외부 API 패턴
9장 마이크로서비스 테스트 1부
10장 마이크로서비스 테스트 2부
11장 프로덕션 레디 서비스 개발
12장 마이크로서비스 배포
13장 마이크로서비스로 리팩터링
3. 프로세스 간 통신
이 장에서는 마이크로서비스간에 어떤 패턴들로 통신을 하는지 알아볼 수 있다.
3.1. 상호 작용 스타일
*일대일 / 일대다: 각 클라이언트의 요청을 한 서비스가 처리하는지, 여러 서비스가 협동하여 처리하는지로 구분.
*동기 / 비동기
일대일 상호작용의 종류
요청/응답: 클라이언트는 요청을 하고 응답을 대기.서비스간의 강한 결합
비동기 요청/응답: 클라이언트는 서비스에 요청,서비스는 비동기로 응답
단방향 알림: 클라이언트는 서비스에 요청만 하고, 서비스는 응답을 보내지 않음일대다 상호작용의 종류
발행/구독: 클라이언트는 알림 메시지를 발행하고, 구독중인 서비스가 메시지를 소비
발행/비동기 응답: 클라이언트는 요청 메시지를 발행하고, 주어진 시간동안 서비스가 응답하기를 기다림
3.2. 마이크로서비스 API 정의
인터페이스 명세를 작성한 후 클라이언트 개발자와 의논하며 API를 정의하는 방식으로 ‘선 설계 후 개발' 을 하면 클라이언트 니즈에 좀 더 부합한 서비스를 구축할 수 있다.
3.3. 동기 RPI 패턴 응용 통신
RPI(Remote Procedure Invocation)는 클라이언트가 서비스에 요청을 보내면 서비스가 처리 후 응답을 회신하는 IPC다. 프로토콜의 종류는 다양하지만 책에서는 REST와 gRPC의 사례를 위주로 설명한다.
3.3.1. 동기 RPI 패턴:REST
REST에 대한 설명은 다른 글에도 엄청 많으니 적지는 않겠지만,
리처드슨의 REST 성숙도모델은 한번 고민할 필요가 있다고 생각한다.
REST API의 가이드는 이미 나와있지만 가이드를 어디까지 지켜야하는지는 확실하게 표준이 나와 있지 않고, 단순히 REST를 쓴다고 하니 쓰는줄로 아는 경우도 많이 보였다. 표준이 없는 상황이지만 REST 성숙도모델을 기준으로 레벨3의 HATEOAS까지 적용하는것이 더 나은 방식으로 웹 생태계를 발전시켜나가는게 아닐까하는 생각이 든다.
REST가 정답은 아니다. REST는 리소스기준으로 호출하기 때문에 요청 한번으로 연관된 모든 객체를 가져오기에는 적합하지 않다. 연관된 리소스를 함께 조회하는 방식으로 문제를 해결할 수도 있지만, 효율이 떨어지거나 구현 시간이 많이 소요된다는 문제도 있어서 GraphQL이나 Netflix Falcor같은 대체 기술이 이러한 문제를 해결해주기도 한다.
비즈니스객체에 수행할 작업을 HTTP동사에 매핑하는것도 고민거리중에 하나인데, /orders/{orderId}/cancel 과 같이 url끝에 하위 리소스를 정의해 해결하는 방법도 있다. 하지만 REST답지 않은 스타일 때문에 gRPC같은 대체기술로 문제를 해결하기도 한다.
3.3.2. 동기 RPI 패턴:gRPC
gRPC 또한 다른 글들을 통해 사용되는 많은 사례들을 확인할 수 있다. 여기서 알아둘 점은 REST처럼 동기 통신하는 매커니즘이기 때문에 부분 실패를 해결해야한다는 문제점을 인지해야한다.
3.3.3. 부분 실패 처리: circuit breaker
분산 시스템은 다른 서비스를 동기 호출할 때 마다 실패할 가능성이 항상 존재한다. 서비스는 기술적 오류나 과부하로 인해 응답이 늦어지는 경우가 발생할 수 있는데, 클라이언트는 응답 도중에 블로킹되고 동기 호출된 서비스의 문제가 클라이언트까지 올라감으로써 전체 시스템이 중단될 위험이 있다.
부분실패가 전체 시스템에 영향을 주지 않도록 설계를 해야 할 필요성이 있는데, 보통 2가지의 방법으로 분리하여 관리한다.
cf. Fault Tolerance in a High Volume, Distributed System — Netflix
- 견고한 RPI 프록시 설계
- 응답 대기에 대해 타임아웃을 걸어둔다.
- 미처리 요청 개수 제한 : 클라이언트가 서비스에 요청 가능한 미처리 요청의 최대 개수 설정
- circuit breaker 패턴 : 서비스의 에러율이 지정된 임계치를 초과한 이후의 요청은 바로실패 처리 - Fail over(실패 서비스 대응)
- 캐시된 데이터 표시
- 서비스 디스커버리에 의존
3.3.4. 서비스 디스커버리
이전에는 어플리케이션의 서비스 인스턴스의 주소는 대부분 정적이었다. 하지만 마이크로서비스 기반의 어플리케이션들은 인스턴스마다 네트워크 위치들이 동적으로 배정되거나, 자동 확장, 실패등 변화할 요인이 많기 때문에 이를 관리할 필요성이 있다.
서비스 디스커버리는 클라이언트가 서비스를 호출하면 서비스 인스턴스의 네트워크 위치를 DB화하는 서비스 레지스트리이다.
서비스 인스턴스가 시작/종료 할 때 마다 서비스 레지스트리는 업데이트되며, 클라이언트가 서비스를 요출하면 서비스 디스커버리가 가용한 서비스 인스턴스 목록중에서 한 서비스로 요청을 라우팅한다.
3.4. 비동기 메시징 패턴 응용 통신
메시징은 서비스가 메시지를 서로 비동기적으로 주고받는 통신 방식이다. 비동기통신은 클라이언트가 서비스에 메시지를 보내 요청을 하게 되면, 요청받는 서비스 인스턴스가 응답가능할 경우 메시지를 클라이언트에 보내는 형식으로 통신을 한다. 중요한 부분은 비동기 통신을 하기 때문에 클라이언트가 응답을 기다리며 블로킹을 하지 않는다는 것이다. 또한 클라이언트도 응답을 바로 받지 못한다는 전제하에 작성한다.
3.4.1. 메시지채널
메시지는 채널을 통해 교환된다. 송신자의 비즈니스 로직은 송신 포트 인터페이스를 호출하고, 인터페이스는 메시지 채널을 통해 수신자에게 메시지를 전달한다. 수신자의 메시지 핸들러 어댑터 클래스는 메시지를 처리하기 위해 호출되고, 이 클래스는 컨슈머 비즈니스 로직으로 구현된 수신 포트 인터페이스를 호출한다.
채널은 point-to-point(일대일 상호작용)과 publish-subscribe(발행-구독) 2가지의 종류가 있다.
- 상호작용스타일
- 요청/응답, 비동기 요청/응답
- 단방향 알림
- 발행/구독
- 발행/비동기 응답
3.4.2. 메시지 브로커
메시지 브로커는 서비스가 서로 통신할 수 있게 해주는 인프라 서비스이다.
메시지 브로커는 모든 메시지가 지나가는 중간 지점이며, 메시지 브로커의 가장 큰 장점은 송신자가 컨슈머의 네트워크 위치를 몰라도 된다는 것이다. 또한 컨슈머가 메시지를 처리할 수 있을 때 메시지 브로커에 메시지를 버퍼링할 수도 있다.
- 장점
- 채널에 메시지를 보내는 식으로 요청, 서비스 인스턴스의 정보가 필요하지 않음
- 처리 가능한 시점까지 메시지를 버퍼링. 큐에 지속적으로 메시지를 쌓기 때문에 연계된 서비스가 느려지거나 불능 상태가 되더라도 컨슈머는 계속 이벤트를 쌓을 수 있음 - 단점
- 병목현상의 가능성(유연한 확장으로 보완)
- SPOF 가능성. 하지만 요즘 브로커는 고가용성이 보장되도록 설계되어있음
- 운영복잡도가 높음. 설치, 구성, 운영이 필요
3.4.3. 수신자 경합과 메시지 순서 유지
메시지 순서를 유지한 채 scale-out 을 하기 위해 아파치 카프카나 AWS 키네시스등은 샤딩된 채널을 이용하여 문제를 해결한다.
- 샤딩된 채널은 복수의 샤드로 구성, 각 샤드는 채널처럼 작동
- 송신자는 메시지 헤더에 샤드키를 지정하고, 메시지 브로커는 메시지를 샤드 키별로 샤드/파티션에 배정
- 메시징 브로커는 수신자 인스턴스들을 묶어 하나의 동일한 논리 수신자로 취급(컨슈머 그룹). 메시지 브로커는 각 샤드를 하나의 수신자에 배정하고, 수신자가 시동/종료하면 샤드를 재배정
3.4.4. 중복 메시지 처리
메시지 브로커는 ‘적어도 한 번 전달'을 보장하는 대신에, 브로커 자신이 실패할 경우 같은 메시지를 여러번 전달할 수도 있다. 프로세스가 뒤죽박죽이 되는 경우도 발생할 수 있으므로 중복 메시지처리를 고민해야 할 필요가 있다.
- 멱등한 메시지 핸들러 작성
반복적으로 호출해도 문제가 없는 경우는 문제가 발생하지 않지만, 이러한 경우는 적지않다. - 메시지 추적과 중복 메시지 솎아내기
메시지 핸들러가 중복 메시지를 걸러내서 멱등하게 동작하도록 해야 한다.컨슈머가 메시지ID를 이용해서 메시지 처리 여부를 추척하면서 솎아내는 경우 해결할 수 있다.(소비하는 메시지ID값을 DB 테이블에 저장)
3.4.5. 트랜잭셔널 메시징
서비스가 메시지를 주고받으려면 라이브러리가 필요한데, 메시지브로커 자체는 고수준의 상호작용 스타일을 지원하지 않는다.(이벤트소싱, 트랜잭셔널관리) 그렇기 때문에 책에서는 저자가 만들었다는 eventuate tram 이라는 프레임워크로 사례를 들고 있다.
3.5. 비동기 메시징으로 가용성 개선
요청을 처리하는 과정에서 타 서비스와 동기 통신을 하면 그만큼 가용성이 떨어지기 때문에 가능하면 서비스가 비동기 메시징을 이용하여 통신하도록 설계하는 것이 좋다.
3.5.1. 동기 상호 작용 제거
- 비동기 상호 작용 스타일
클라이언트와 서비스는 메시지 브로커를 통해 서로 비동기통신을 하며, 블로킹과정이 발생하지 않는다. 이런 아키텍처는 메시지가 소비되는 시점까지 메시지 브로커가 메시지를 버퍼링하기 때문에 매우 탄력적이다.
- 데이터 복제
서비스에 동기API가 있는 경우, 데이터를 복제하여 가용성을 높일 수 있다. 데이터를 소유한 서비스가 발행하는 이벤트를 구독해서 최신 데이터를 유지하는 방법이다. 물론 대용량의 레플리카를 만드는 것을 비효율적이므로 경우에 맞게 사용하는것이 좋다.
이번 장에서는 어플리케이션이 외부 혹은 다른 서비스들로부터 요청을 받고처리하는과정에 대해 설명하였다. 핵심은 복잡하게 얽혀있는 시스템들간에 직접적인 통신은 지양하고, 메시지 브로커를 통해 비동기통신을 지향한다는 점이다.글로써 이해하는 방법도 좋지만 관련서비스들, 예를들어 Hystrix나 MQTT, Kafka등을 찾아가며 이해하는것이 용이하다고 생각한다.