-
분산 시스템에서 데이터를 전달하는 효율적인 방법세미나, 영상 요약정리 2023. 5. 3. 00:01
https://forward.nhn.com/2022/sessions/10
NHN Dooray 협업서비스개발그린팀의 김병부님의 발표를 요약해보고자 합니다.
대상
- 분산 시스템에서 메시지 전달에 관심이 있으신 개발자.
- 메시지를 정확하게 전달하고 수신하는데 관심이 있으신 개발자.
- Spring 초급 이상 개발자
분산 시스템이란?
목표를 달성하기 위해 여러 개의 컴퓨터 리소스를 사용하는 시스템
간단한 예시로 마이크로 서비스 아키텍처 애플리케이션
네트워크를 사용하는 것이 특징이다
데이터를 전달하는 방법
Remote API
- 서버-클라이언트 구조
- 사용자의 요청에 즉각 응답하는 API에서 주로 사용하는 방식
- 간단
MessageQueue
- Publisher-Consumer 구조
- 배치 작업, 비동기 작업에서 주로 사용
- 복잡
효율적으로 데이터를 전달하려면?
네트워크는 시스템을 연결하는 유일한 수단
그런데 네트워크는 신뢰할 수 없다..
- 패킷 손실
- 네트워크 지연
- 네트워크 다운
항상 데이터는 유실될 수 있다.
데이터 전달을 보장하려면?
- At-most once delivery (최대 한번만 전달)
- Consumer가 받던말던 Producer는 한 번만 전송하면 메시지가 유실될 수 있다.
- 간단, 대용량 메시지 처리가능
- At-least-once delivery(최소 한번 전달)
- Consumer가 잘 수신했다는 메시지를 보낼때 까지 보내준다
- 중복 메시지를 위한 멱등성을 보장해야 한다.
- Exactly-once-delivery(정확히 한번 전달)
- 누락과 중복이 없다!
- 가장 어려운 개발 난이도
- MessageQueue 기능에 의존한 개발
- MessageQueue 추가로 인한 시스템 복잡도 증가
RDB를 사용하는 애플리케이션에서 전달 방법
서비스별 데이터베이스 패턴
@Transactional fun createData(data Data){ saveData(data) eventHandler.propagate(CreateEvent.of(data)) }
1. 우선 데이터를 저장합니다.
2. 이후 RestAPI를 호출하거나 Event를 발행합니다.
RDB를 사용하니까 @Transaction 어노테이션을 붙여줍니다.
AOP를 사용하여 proxy 객체를 생성합니다.
만약 rollback이 된다면 어떻게 될까요?
가장 중요한 RDB에 저장된 데이터를 롤백됩니다.
최종적으로 REST-API만 호출됩니다.
어떻게 방지할 수 있을까?
@TransactionalEventListener를 사용하여 트랜잭션이 commit 하도록 한다
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) fun propagate(event: CreateEvent){ //이벤트 발생 로직 }
이제 DB 트랜잭션이 항상 성공하고나서 Event를 발행하거나 REST API를 호출할 수 있습니다.
REST API가 실패하면?
DB만 저장되고 REST API에서 실패할 수 있습니다.
maxAttemp옵션으로 최대시도를 줄 수 있고 딜레이를 줄 수 있습니다.
Retry를 시도할 수 있으나 Retry도 실패할 수 있습니다..
네트워크는 언제 돌아올지 우리는 알 수 없습니다.
실패 없는 트랜잭션 처리와 이벤트 전달이라면?
Transactionl Outbox Pattern
- RDB를 Message Queue로 사용
- 이벤트와 데이터 저장이 둘다둘 다 성공하거나 둘 다 롤백된다
- Polling Publisher 패턴을 활용하여 전달한다
Polling Publisher 패턴
- 스케줄러가 이벤트를 주기적으로 RDB에서 polling 해와서 메일 API에 publishing
이를 위해서 테이블을 설계해야 한다
- EventId PK BIGINT (이벤트의 순서를 보장)
- create_at (이벤트 발생 시간)
- status (ready / done)
- payload (메시지의 내용)
코드로 본다면?
@Transactional fun createData(data Data){ saveData(data) saveEvent(CreateEvent.of(data)) }
이제 데이터가 저장되고 event가 저장저장되거나 둘 다 rollback 되거나 합니다.
@Scheduled(cron = "0/5 * * * * *") @Transactional fun publish(){ //현재 시점보다 전에 적재된 event들중 Ready 상태인 event들 으로 restTemplate, message queue 호출 //이후 done 상태로 바꾼다. }
이제 restTemplate 에서 Exception이 발생한다면? 전부다 rollback을 하기 위해 Transactionl을 선언합니다.
예를 들어 1~10번 이벤트를 발행하던 중 7번 이벤트에서 문제가 발생하였습니다.
ㅌ그러면 1~6번까지의 이벤트가 계속 발행될 수 있습니다.
Consumer에서 멱등성만 잘 처리되어 있다면 문제는 없습니다.
장점
REST-API 환경에서 At-least-once를 구현할 수 있다.
단점
Polling, Publisher 과정으로 의한 지연 처리가 발생(5초마다 스케줄러가 돈다)
데이터베이스 부하
데이터베이스 비례한 처리속도
RabbitMQ를 사용한 전달 방법
- RabbitMQ는 AMQP를 구현한 메시지 브로커입니다.
- pub/sub
- ack를 통한 메시지 응답 처리 메커니즘
- 프로듀서 - 큐 - 컨슈머 구조
RabbitMQ가 Routing에 실패하는 경우 NACK를, 성공하면 ACK를 발송합니다.
이를 Producer Confirm이라 합니다.
Consumer Ack
Consumer가 Ack를 날리면 Queue에 있는 데이터가 사라지게 됩니다.
우리의 코드가 버그가 있어 Coumsumer에서 NACK만 날리게 되면?
Queue가 가득 차게 될 것이고 밤에 잠을 이루지 못하게 된다..
이를 위해 Dead Letter를 설정한다
- 큐의 메시지가 오래된 경우
- 큐가 가득 찬 경우
Kafka를 사용한 전달 방법
Producer Confirm
- success callback 또는 fail callback을 받을 수 있다.
Consumer Ack
- AcknowledgingMessageListener를 활용
- Enable Auto Commit Config는 false로 설정
마무리
- Event Driven Architecture의 기본은 데이터 전달
- 최소 At Least Once (최소 한번) 설정
- Producer Confirm, Consumer Ack 고려
'세미나, 영상 요약정리' 카테고리의 다른 글
API 설계 우선 방식과 OpenAPI Specification (1) 2023.05.09 로그인에 사용하는 OAuth: 과거, 현재 그리고 미래 (0) 2023.05.08 ㄷㄷㄷ: Domain Driven Design과 적용 사례공유 (1) 2023.04.23 천만 사용자를 위한 AWS 클라우드 기반 웹 서비스 확장하기1 (0) 2023.03.30 k8s와 gitlab(+ArgoCO)을 이용한 CI/CD 모험기 - 손주호 (0) 2023.01.29