https://www.youtube.com/watch?v=uk5fRLUsBfk
위의 발표를 보고 정리한 포스트
분산 시스템이란?
데이터를 전달하는 방법
Remote API
- 데이터 처리는 서버측에서 일어날 수도 (C, U, D) 혹은 조회한 클라이언트에서 처리할 수도 있다. (R)
- 주로 사용자 요청에 즉각 응답하는 방식에 Remote API를 많이 사용한다.
MessageQueue 방식
- Publisher가 MessageQueue에 데이터를 공급하면 Consumer가 데이터를 처리하는 구조
- Publisher - MessageQueue - Consumer 구조 비교적 복잡
(Server - Client)보다 하나 더 늘어난 컴포넌트
효율적인 방법?
- 개발자는 데이터를 전달함에 있어 네트워크 사용은 필수고, 네트워크는 신뢰할 수 없는 매체이기때문에 항상 데이터 유실에 대비해야한다.
- TCP/IP는 데이터 전달을 보장하는 프로토콜 아닌가? 왜 믿을 수 없을까 관련글
- https://kukuta.tistory.com/219
- 어떤 방식을 사용하던간에 (REST-API, MessageQueue) end to end까지 효율적으로 전달하는 추상화된 방식을 설명할것.
1. At-most once delivery(최대 한 번 전달)
- 데이터가 유실 되든말든 Prod는 메시지 큐에 단 한 번 전송 (Fire and Forget)
- Consumer는 데이터를 못 받을 수 있음 네트워크 상에서 패킷이 유실 될 수 있다.
- Prod 어플리케이션에서 메시지를 전송 중 예외가 발생할 수도 혹은 Client 앱에서 데이터 처리를 정상적으로 하지 못할 수도 있다.
2. At-least-once delivery
- at-most once delivery 다음으로 정확도가 더 높은 방식
- Prod는 Consumer에게서 수신 확인 응답 패킷(ACK)을 받을때까지 데이터 전송
- 장점에 써있는 바와 같이 발송을 보장하고 Prod측에서 상태관리만 하면 되기 때문에 비교적 쉬운 개발
- 멱등성 (여러번 실행해도 같은 결과를 나타내는 것), 같은 메시지를 여러번 받더라도 Consumer는 항상 같은 결과를 반환해야한다.
3. Exactly-once-delivery
우리들의 어플리케이션은 데이터를 효율적으로 전달하고 있는가?
- 신뢰성있는 어플리케이션은 최소 한 번의 전송은 해야하며 메시지를 안전하게 처리해야 한다.
- SpringBoot를 사용하고 있다는 가정하에, 기본 설정, 혹은 복붙한 설정(코드)을 사용하고 있다면 최소 한 번 전달의 원칙을 지키지 않고 있을 가능성이 놓다.
어떻게 최소 한번 데이터를 전송하는 신뢰성 있는 어플리케이션을 개발 할 수 있는가?
1. RDB를 사용하는 어플리케이션에서 전달 방법
- 서비스별 데이터베이스 패턴
- 각 서비스는 자신만의 독립된 데이터 저장소를 가지고 있고 각 컴포넌트마다 자신의 데이터 저장소의 데이터를 처리한다.
어떤 컴포넌트에서 다른 컴포넌트로 데이터 전파을 해야한다면
- 제일 처음 DB트랜잭션을 시작해서 데이터를 정상적으로 저장
- 그 후 다른 컴포넌트에게 REST-API로 전파한다고 가정
일반적인 코드
- task를 생성후 Repository에 저장할것이다.
- 다른 컴포넌트에게 전파하기 위해 나의 event,메시지 혹은 REST-API콜 전달
- 마지막에 @Transactional을 붙일것이다. 왜냐? RDB를 사용하니까 데이터를 안전하게 저장하고 싶기 때문에
@Transactional??
- Proxy 객체가 한 번 감싸고 있다.
- 그렇다면 실질적인 순서는?
- 데이터를 저장하고
- 이벤트를 전달하고
- 트랜잭션에 의해 커밋, 혹은 롤백이 된다.
실행 순서
- 그후 트랜잭션이 실행됨
- 만약 commit이 정상적으로 되지 않는다면???
- 영속되지 않은 데이터로 REST-API만 전달된 상황
- 원본데이터는 존재하지 않음
해결책
- SpringFramework에서 제공하는 대표적인 두 가지 방법
- @TransactionalEventlListener의 경우
- Eventhandler의 propagate메서드
- REST-API를 이용하거나 MeesageQueue를 이용해 다른 컴포넌트에게 데이터를 전달을 하는 경우 위 어노테이션을 propagate메서드에 붙여준다.
- 우리가 원했던 트랜잭션 커밋 후 데이터 전파 실행
- 그러나 다 해결되지 않는다.
- 커밋은 완료됐지만 전달이 실패한다면? 네트워크는 신뢰할 수 없는 매체
- 데이터 저장만하고 데이터 전파는 실패
- @Retryable을 붙여주면 실패했을때 재시도를 할 수 있다.
- 그런데.. 이것 마저 실패한다면??
- maxAttempts와 backoff를 설정한다.
- 최대 실행 횟수, 지연시간
- 이것마저 실패한다면??
마이크로 서비스 아키텍쳐 패턴
- 우리가 원하는것이 트랜잭션 커밋과 더불어 보장되는 데이터 전파라면.
- Transactional Outbox Pattern과 Polling Publisher Pattern을 섞어 쓴다.
- RDB를 MessageQueue처럼 사용하는 방식
Transactional Outbox Pattern
- 하나의 트랜잭션에 이벤트, 메세지가 발행될때 RDB에 같이 저장한다면 둘 다 성공 혹은 둘 다 실패할것이다.
- 어떻게 구현할 것인가?
Polling Publisher Pattern을 활용해야한다.
- RDB에 저장되어 있는 데이터들을 주기적으로 Polling하고 발행하는 역할을 추가한다.
RDB에서 MessageQueue처럼 사용할 테이블을 설계해야한다.
화자의 주요 필드 4가지
- event_id : PK로 빠른 인덱스와 이벤트 순서 보장
- created_at : 이벤트 발생시간으로 Consumer에서 처리할 데이터 구분
- status : 완료 여부 상태
- payload : 메시지 저장
By Code
변경된 코드
- taskRepository와 eventRepository에 One트랜잭션에 데이터, 이벤트저장
Polling Publisher Pattern 코드
- @Scheduled 애노테이션 4초마다 실행
- Ready상태인 이벤트들을 조회
- restTemplate로 이벤트 전송
- 전송한 이벤트들을 done으로 업데이트
- 저장
이과정에서 예외가 발생한다면 !!
장단점
- 실시간성이 필요한 데이터는 해당 패턴 사용 불가
- 대용량 이벤트를 발행해야한다, 트랜잭션 하나에 많은 이벤트가 발생한다면 이 패턴은 사용하기 부적합하다.
RabbitMQ를 사용한 전달방법
- 응답처리 메커니즘 처리시 -> ACK, 실패시 -> NACK
- exchange -> queue 라우팅 과정중 네트워크에서 실패할 가능성이 있다.
- 이를 위한 해결책 두 가지 과정
Producer Confirm
- exchange에서 응답 패킷을 보내는것이 Producer Confirm
- 스프린에서 제공하는 CorrelationData 클래스 제공
- rabbitTemplate send() 메서드에 이 객체를 사용하는 코드
- 콜백하는 코드 setConfirtmCallback 메서드에 correclationData 파라미터
- 파라미터에 대해
- correlationData -> 메시지를 발행할때 생성한 객체
- ack -> boolean 성공 실패 여부
- cause -> 실패 원인을 확인하기 위함 (String)
callback이기 때문에 실시간으로 즉각 대응하기 어렵지만 어떤 메세지가 실패했는지 파악 가능하다.
설정 코드
Consumer Acknowledge
- @RabbitListener를 사용한 방법
- 일반적으로 이벤트 리스너의 receiveMessage를 구현할때 message하나만 받지만 해당 패턴에선 channel을 붙여준다.
- SpringFramework가 자동 적용해주지만 수동 전송도 가능하다.
버그로 인해 컨슈머가 NACK만 날릴 경우.
- Consumer Ack을 구현할때의 주의점
- Consumer는 NACK만 전송하고 큐에 메세지는 계속 쌓이는 경우가 발생할 수 있다.
- Dead Letter 큐를 활용해 해결가능
- Dead Letter설정
- 큐에서 정상적으로 처리하지 못한 메세지를 DeadLetter Exchange로 넘기고
- 라우팅으로 Dead Letter Queue에 넘겨준다.
- requeue시 basic reject, basic nack로 메세지를 처리하는 경우
- 큐내의 메세지가 TTL이 초과할경우 (오래됐을 경우)
- 큐가 가득찼을 경우
*requeue를 true로 설정하면 Dead Letter로 가지 않고 기존의 메세지 큐에 다시 들어간다.
- Dead Letter 적용 코드
- Builder사용
- maxAttempts 최대 실행 횟수
- backOff 간격 설정
- recover Requeue하지 말고 DeadLetter로 넣어라
- 실패시 처리 코드
- 수동으로라도 데이터를 다시 전달 할 수있다.
Kafka를 이용한 방법
마찬가지로 Producher Confierm, Consumer Ack 방식
- kafkaTemplate.send 메서드를 활용해서 메세지 발행
- 응답은 ListenableFutuer
- 두 가지 콜백을 등록할 수 있음
- success, fail CallBack
- success -> reulst ->
- fail -> throwable
- onMessage메서드
- Acknowledgment 파라미터 확인
- acknowledgment.acknowledge()로 콜백