분산 시스템에서 데이터를 전달하는 효율적인 방법

유재희·2023년 2월 4일
0

Conference

목록 보기
9/9

https://www.youtube.com/watch?v=uk5fRLUsBfk

위의 발표를 보고 정리한 포스트

분산 시스템이란?


데이터를 전달하는 방법

  • Remote API
    - REST API, gRPC

  • MessageQueue


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로 전파한다고 가정

일반적인 코드

  • Create 요청이 들어오면
  1. task를 생성후 Repository에 저장할것이다.
  2. 다른 컴포넌트에게 전파하기 위해 나의 event,메시지 혹은 REST-API콜 전달
  3. 마지막에 @Transactional을 붙일것이다. 왜냐? RDB를 사용하니까 데이터를 안전하게 저장하고 싶기 때문에

@Transactional??

  • Proxy 객체가 한 번 감싸고 있다.
  • 그렇다면 실질적인 순서는?
  1. 데이터를 저장하고
  2. 이벤트를 전달하고
  3. 트랜잭션에 의해 커밋, 혹은 롤백이 된다.

실행 순서

  • 다른 컴포넌트에게 이벤트 전달

  • 그후 트랜잭션이 실행됨
  • 만약 commit이 정상적으로 되지 않는다면???
  • 영속되지 않은 데이터로 REST-API만 전달된 상황
  • 원본데이터는 존재하지 않음

해결책

  • SpringFramework에서 제공하는 대표적인 두 가지 방법
  1. @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으로 업데이트
  • 저장

이과정에서 예외가 발생한다면 !!

  • @Transactional로 롤백처리

장단점

  • 실시간성이 필요한 데이터는 해당 패턴 사용 불가
  • 대용량 이벤트를 발행해야한다, 트랜잭션 하나에 많은 이벤트가 발생한다면 이 패턴은 사용하기 부적합하다.

RabbitMQ를 사용한 전달방법

  • 응답처리 메커니즘 처리시 -> ACK, 실패시 -> NACK

  • Exchange에 의해 적절한 큐로 라우팅됨

  • exchange -> queue 라우팅 과정중 네트워크에서 실패할 가능성이 있다.
  • 이를 위한 해결책 두 가지 과정

Producer Confirm

  • exchange에서 응답 패킷을 보내는것이 Producer Confirm

  • 스프린에서 제공하는 CorrelationData 클래스 제공

  • rabbitTemplate send() 메서드에 이 객체를 사용하는 코드

  • 콜백하는 코드 setConfirtmCallback 메서드에 correclationData 파라미터
  • 파라미터에 대해
  • correlationData -> 메시지를 발행할때 생성한 객체
  • ack -> boolean 성공 실패 여부
  • cause -> 실패 원인을 확인하기 위함 (String)

callback이기 때문에 실시간으로 즉각 대응하기 어렵지만 어떤 메세지가 실패했는지 파악 가능하다.

설정 코드

Consumer Acknowledge

    1. Channerl 객체 사용

  • @RabbitListener를 사용한 방법
  • 일반적으로 이벤트 리스너의 receiveMessage를 구현할때 message하나만 받지만 해당 패턴에선 channel을 붙여준다.
  • SpringFramework가 자동 적용해주지만 수동 전송도 가능하다.

    1. MessageListener를 직접 구현하는 경우
  • ChannerAware라는 Prefix가 있는 객체를 사용하면 구현이 가능하다.

버그로 인해 컨슈머가 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()로 콜백

profile
몰라요

0개의 댓글