마이크로서비스 패턴(Microservices Patterns) by Chris Richardson 책의 3.3장 비동기 메시징 패턴 응용 통신을 개괄적으로 정리한 내용입니다.
메시징은 서비스가 메시지를 서로 비동기적으로 주고 받는 통신 방식이다.
메시지 구성 요소
- 메시지
- header
- 송신된 데이터에 대한 metadata에 해당하는 key-value로 구성
- messageId(송신자 또는 메시징 인프라에서 생성)
- 응답이 출력될 메시지 채널을 가리키는 반환 주소(option)
- body
- 실제로 송신할 텍스트 또는 이진 포맷의 데이터
- 종류
- 문서(document)
- 데이터만 포함한 제네릭한 메시지(e.g. 커맨드에 대한 응답)
- 메시지를 어떻게 해석할지는 수신자가 결정
- 커맨드(command)
- RPC 요청과 동등한 메시지
- 호출 작업과 전달할 매개변수가 지정되어 있음
- 이벤트(event)
- 송신자에게 어떤 사건이 발생했음을 알리는 메시지
- 이벤트는 대부분 Order, Customer 같은 도메인 객체의 상태 변화를 나타내는 도메인 이벤트
- 문서(document)
- header
- 채널
- 송신자
- 송신자의 비즈니스 로직은 하부 통신 메커니즘을 캡슐화한 송신 포트 인터페이스 호출
- 메시지 송신자 어댑터 클래스로 구현
- 이 클래스는 메시징 인프라를 추상화한 메시지 채널을 통해 수신자에게 메시지 전달
- 수신자 메시지 핸들러(handler, 처리기) 어댑터 클래스
- 메시지 처리하기 위해 호출
- 컨슈머 비즈니스 로직으로 구현된 수신 포트 인터페이스 호출
- 송신자가 채널에 보낼 수 있는 메시지와 수신자가 채널에서 받을 수 있는 메시지 갯수는 무제한
- 종류
- point-to-point 채널
- 채널을 읽는 컨슈머 딱 하나만 지정하여 메시지 전달
- 일대일 상호 작용 스타일의 서비스가 이 채널 사용(e.g. command messgage)
- publish-subscribe 채널
- 같은 채널을 바라보는 모든 컨슈머에 메시지 전달
- 일대다 상호 작용 스타일 서비스가 사용(e.g. 이벤트 메시지)
- point-to-point 채널
- 송신자
메시징 상호 작용 스타일 구현
- 요청/응답 and 비동기 요청/응답
- messageId <-> correlationId
- 단방향 알림(one-way notification)
- 서비스는 응답 미반환
- 발행/구독
- 서비스가 도메인 이벤트 발행
- 해당 도메인 클래스의 이름을 딴 발행/구독 채널을 소유
- (주문 서비스는 Order 이벤트를 Order 채널에 발행)
- 서비스는 자신이 관심있는 도메인 객체의 이벤트 채널을 구독
- 발행/비동기 응답
- 발행/구독과 요청/응답의 엘리먼트를 조합한 고수준의 상호 작용 스타일
- 클라이언트
- 응답 채널 헤더가 명시된 메시지를 발행/구독 채널에 발행
- consumer
- correlationId가 포함된 응답 메시지를 지정된 응답 채널에 씀
메시징 기반 서비스의 API 명세 작성
- 구성 요소
- 메시지 채널명
- 각 채널 통해 교환되는 메시지 타입과 포맷
- 메시지 포맷은 JSON, XML, 프로토콜 버퍼 등 표준 포맷으로 기술
- 비동기 작업 문서화
- 요청/비동기 응답 스타일 API
- 서비스의 커맨드 메시지 채널
- 서비스가 받는 커맨드 메시지의 타입과 포맷
- 서비스가 반환하는 응답 메시지의 타입과 포맷
- 단방향 알림 스타일 API
- 서비스의 커맨드 메시지 채널
- 서비스가 받는 커맨드 메시지의 타입과 포맷
- 요청/비동기 응답 스타일 API
- 발행 이벤트 문서화
- 발행/구독 스타일로도 이벤트 발행 가능
- 구성요소
- 이벤트 채널
- 서비스가 채널에 발행하는 이벤트 메시지의 타입과 포맷
메시징 기술
- brokerless 메시징
- 서비스 간 메시지를 직접 교환
- ZeroMQ 등
- TCP, 유닉스형 domain socket, multicast 등 다양한 전송 기술 지원
- 장점
- 네트워크 트래픽이 가볍고 지연 시간이 짧음
- 메시지 브로커가 성능 병목점이나 SPOF(Single Point Of Failure)가 될 일이 없음
- 메시지 브로커를 설정/관리할 필요가 없으므로 운영 복잡도가 낮음
- 단점
- 서비스가 서로의 위치를 알아야 하므로 서비스 디스커버리 매커니즘 중 하나를 사용해야 함
- 메시지 교환 시 송신자/수신자 모두 실행중이어야 하므로 가용성 떨어짐
- 전달 보장(guarantted delivery) 같은 메커니즘을 구현하기가 더 어려움
- 메시지 브로커
- 정의
- 서비스가 서로 통신할 수 있게 해주는 인프라 서비스
- 모든 메시지가 지나가는 중간 지점
- 장점
- 송신자가 컨슈머의 네트워크 위치를 몰라도 됨
- 컨슈머가 메시지를 처리할 수 있을 때까지 메시지 브로커에 메시지를 버퍼링 가능
- 제품
- ActiveMQ
- RabbitMQ
- Apache Kafka
- AWS Kinesis, AWS SQS 등 클라우드 기반 메시징 서비스도 있음
- 제품 선택 시 검토 사항
- 프로그래밍 언어 지원 여부
- 메시징 표준 지원 여부
- AMQP, STOMP 등 표준 프로토콜을 지원하는 제품인가, 아니면 자체 표준만 지원하는가?
- 메시지 순서
- 전달 보장
- 영속화
- 내구성
- 컨슈머가 메시지 브로커에 다시 접속할 경우, 접속이 중단된 시간에 전달된 메시지 받을 수 있는지
- 확장성
- 지연 시간
- 경쟁사 컨슈머
- 정의
메시지 브로커로 메시지 채널 구현
- 제품
- ActiveMQ(JMS 메시지 브로커) -> queue, topic
- RabbitMQ(AMQP 기반 메시지 브로커) -> exchange, queue
- Apache Kafka -> topic
- AWS Kinesis -> stream
- AWS SQS -> queue
- 장점
- 느슨한 결합
- 메시지 버퍼링
- 유연한 통신
- 명시적 IPC
- 단점
- 성능 병목 가능성
- 단일 장애점 가능성
- 운영 복잡도 증가
메시징 처리 고려 사항
- 수신자 경합과 메시지 순서 유지
- Apache Kafka, AWS Kinesis
- sharded 채널 이용
- 솔루션 구성
- 샤딩된 채널은 복수의 샤드로 구성되며, 각 샤드는 채널처럼 작동
- 송신자는 메시지 헤더에 샤드 키(보통 무작위 문자열 or byte)를 지정. 메시지 브로커는 메시지를 샤드 키별로 샤드/파티션에 배정
- 메시징 브로커는 여러 수신자 인스턴스를 묶어 마치 동일한 논리 수신자처럼 취급(apache kafka에서의 consumer group). 메시지 브로커는 각 샤드를 하나의 수신자에 배정하고 수신자가 시동/종료하면 샤드를 재배정
- Apache Kafka, AWS Kinesis
- 중복 메시지 처리
- 클라이언트, 네트워크, 메시지 브로커 자신이 실패할 경우 같은 메시지를 여러 번 전달할 수도 있음
- 방법1(멱등한(idempotent) 메시지 핸들러 작성)
- 멱등하면 좋지만, 그런 서비스는 많이 없음
- 메시지를 추적하고 중복을 솎아냄
- 컨슈머가 messageId 이용해 메시지 처리 여부를 추적하면서 중복 메시지 솎아내면 간단히 해결
- 컨슈머는 메시지를 처리할 때 비즈니스 엔티티를 생성/수정하는 트랜잭션의 일부로 messageId를 DB 테이블에 기록
- 전용 테이블에 messageId가 포함된 row를 삽입하고, 중복된 메시지라면 insert 쿼리가 실패
- 전용 테이블 대신 일반 애플리케이션 테이블에 messageId 기록
- (한 DB 트랜잭션으로 두 테이블을 업데이트하는 일이 불가능한, 트랜잭션 모델이 제한적인 NoSQL DB를 쓸 때 유용)
트랜잭셔널 메시징
- RDB
- DB 테이블을 메시지 큐로 활용
- transactional outbox pattern
- NoSQL
- DB에는 레코드로 적재된 비즈니스 엔티티를 발행할 메시지 목록을 가리키는 속성이 존재
- 서비스가 DB entity를 업데이트할 때 이 목록에 메시지를 덧붙임
- 단일 DB 작업이므로 원자적이지만, 문제는 이벤트를 가진 비즈니스 엔티티를 효과적으로 찾아 발행해야 함
메시지를 DB에서 메시지 브로커로 옮기는 방법 2가지
- 이벤트 발행(polling 발행기 패턴)
- message relay로 테이블을 polling해서 미발행 메시지를 조회하는 것
- select * from outbox order by ... asc 쿼리를 주기적으로 실행
- 조회 메시지를 하나씩 각자의 목적지 채널로 보내서 메시지 브로커에 발행
- 나중에 outbox 테이블에서 메시지 삭제
- 장점
- 규모가 작을 경우 쓸 수 있는 단순한 방법
- 단점
- DB를 자주 폴링하면 비용 유발
- outbox 테이블 쿼리 대신 비즈니스 엔티티를 쿼리해야 하면, 효율적일 수도 불가능할 수도 있음
- 이벤트 발행(transaction log tailing)
- message relay로 DB 트랜잭션 로그(commit log)를 tailing 하는 방법
- 커밋된 업데이트는 각 DB의 트랜잭션 로그 항목으로 남는데, transaction log miner로 트랜잭션 로그를 읽어 변경분을 하나씩 메시지로 메시지 브로커에 발행
- 사례
- Debezium
- DB 변경분을 아파치 카프카 메시지 브로커에 발행하는 오픈소스 프로젝트
- LinkedIn Databus
- 오라클 트랜잭션 로그를 마이닝해서 변경분을 이벤트로 발행하는 오픈소스 프로젝트
- DynamoDB streams
- 최근 24시간 동안 테이블 아이템에 적용된 변경분을 시간 순으로 정렬된 데이터 가지고, 애플리케이션은 스트림에서 변경분을 읽어 이벤트로 발행
- Eventuate Tram
- MySQL binlog 프로토콜, Postgres WAL 폴링을 응용해서 outbox 테이블의 변경분을 읽어 아파치 카프카로 발행
- Debezium
메시징 라이브러리/프레임워크
- 서비스가 메시지 주고 받으려면 라이브러리 필요
- 메시지 브로커에도 클라이언트 라이브러리가 있지만 직접 사용하면 다음의 문제 발생
- 메시지 브로커 API에 메시지를 발행하는 비즈니스 로직이 클라이언트 라이브러리와 결합됨
- 메시지 브로커의 클라이언트 라이브러리는 대부분 저수준이고 메시지 주고받는 코드가 꽤 긴 편
- 메시지 브로커의 클라이언트 라이브러리는 기본적인 메시지 소통 수단일 뿐, 고수준의 상호 작용 스타일은 지원하지 않음
- 저수준 세부사항을 감추고 고수준의 상호 작용 스타일을 직접 지원하는 고수준 라이브러리/프레임워크가 필요
비동기 메시징으로 가용성 개선
동기 통신
- 단점
- REST는 대중적인 IPC지만, 동기 프로토콜이라는 치명적 문제점
- 모든 서비스가 가동 중이어야 함(가용성)
동기 상호 작용 제거
- 비동기 상호 작용 스타일
- 클라이언트가 요청하는 채널의 A서비스가 B,C,D 등 다른 서비스와 메시지를 비동기 방식으로 교환
- 최종적으로 클라이언트에 응답 메시지 전송
- 메시지가 소비되는 시점까지 메시지 브로커가 메시지를 버퍼링하기 때문에 매우 탄력적
- 서비스에 동기 API가 있는 경우 데이터를 복제하면 가용성 높일 수 있음
- 데이터 복제
- 서비스 요청 처리에 필요한 데이터의 레플리카를 유지하는 방법
- 데이터 레플리카는 데이터를 소유한 서비스가 발행하는 이벤트를 구독해서 최신 데이터 유지 가능
- 예를 들어 주문 서비스는 소비자/음식점 데이터의 레플리카를 갖고 있으므로 자기 완비형
- 개선사항
- 엄청난 양의 데이터를 그대로 복제하는 것은 실용적이지 않음. 다른 서비스가 소유한 데이터를 업데이트 하는 문제도 데이터 복제만으로 해결되지 않음
- 다른 서비스와의 상호 작용을 지연시켜서 개선(응답 반환 후 마무리)
- 로컬에서 가용한 데이터만 갖고 요청을 검증
- 메시지를 outbox 테이블에 삽입하는 식으로 DB를 업데이트
- 클라이언트에 응답을 반환
- 내용
- 서비스는 요청 처리 중에 다른 서비스와 동기적 상호작용하지 않고, 다른 서비스에 메시지를 비동기 전송함
- 예시
- 클라이언트가 주문 요청
- 주문 서비스는 주문을 PENDING 상태로 생성
- 주문 서비스는 주문 ID가 포함된 응답을 클라이언트에 반환
- 주문 서비스는 ValidateConsumerInfo 메시지를 소비자 서비스에 전송
- 주문 서비스는 ValidateOrderDetails 메시지를 음식점 서비스에 전송
- 소비자 서비스는 ValidateConsumerInfo 메시지를 받고 주문 가능한 소비자인지 확인 후, ConsumerValidated 메시지를 주문 서비스에 보냄
- 음식점 서비스는 ValidateOrderDetails 메시지를 받고 올바른 메뉴 항목인지 음식점에서 주문배달지로 배달이 가능한지 확인 후, OrderDetailsValidated 메시지를 주문 서비스에 전송
- 주문 서비스는 ConsumerValidated 및 OrderDetailsValidated를 받고 주문 상태를 VALIDATED로 변경
- 클라이언트에 상태 반환