이번 포스팅에서는 NHN 유튜브의 분산 시스템에서 데이터를 전달하는 효율적인 방법 강의를 보고 정리한 내용이다.
포스팅에서 다룰 내용
- 데이터 전달 보장 방법론
- RDB를 사용하는 애플리케이션에서 전달 방법
- RabbitMQ를 사용한 전달 방법
- Kafka를 사용하는 애플리케이션의 전달 방법
분산 시스템이란?
- 목표를 달성하기 위해 여러 개의 컴퓨터 리소스를 사용하는 시스템
- 시스템은 두 개 이상의 컴포넌트로 구성되어 있다.
- 엔터프라이즈 애플리케이션
- 마이크로 서비스 아키텍처 애플리케이션
- 모놀리식 아키텍처 애플리케이션 + 검색엔진
- 네트워크를 사용하여 컴포넌트 간의 기능을 통합
- 분산 시스템은 네트워크를 사용한다는 것이 특징
데이터를 전달하는 방법
- Remote API
- MessageQueue
1. Remote API를 사용한 데이터 전달
- 서버-클라이언트 구조
- 서버는 API를 제공
- 클라이언트는 서버가 제공하는 API를 사용해 데이터를 처리
- 서버에서 데이터를 처리할 수도 있고 클라이언트가 서버의 데이터를 가져와서 처리할 수도 있음(양쪽에서 가능)
- 서버에 데이터를 조작(C/U/D)
- 서버의 데이터를 조회
- 사용자 요청에 즉각 응답하는 API에서 주로 사용하는 방식
- 예를 들어, 게이트웨이의 경우 사용자 요청을 받아 여러 컴포넌트들의 데이터를 받아
즉각적으로 응답한다.
- 예를 들어, 게이트웨이의 경우 사용자 요청을 받아 여러 컴포넌트들의 데이터를 받아
- 비교적 간단한 개발
2. MessageQueue를 사용한 데이터 전파
- Publisher - Consumer 구조
- 일반적으로, Publisher가 데이터를 만들어서 MessageQueue에 던지면
- Conumser는 MessageQueue에서 데이터를 받아와 처리한다.
- 만약, Conumser와 연결된 다른 컴포넌트들이 있다면 REST API를 호출할 수도 있고,
직접 DB에 처리할 수도 있다.
- 만약, Conumser와 연결된 다른 컴포넌트들이 있다면 REST API를 호출할 수도 있고,
- 일반적으로 배치 작업, 비동기 작업에서 주로 사용
- 비교적 복잡한 개발
효율적인 방법은?
분산 시스템에서 컴포넌트들은 네트워크로 연결
- 네트워크는 시스템을 연결하는 유일한 수단
- 하지만, 네트워크는 신뢰할 수 없는 매체(Media)
- 패킷 손실(Packet Loss)
- 네트워크 지연(Latency)
- 네트워크 다운(Network down)
- 항상 데이터 유실에 대비
데이터 전달 보장 방법론
REST API를 사용하거나 MessageQueue를 사용하던간에
`Endpoint`부터 `Endpoint`까지 데이터를 전달하는 추상화된 방법론을 의미
At-most-once delivery(최대 한번 전달 - 정확도가 가장 낮음)
- Producer는 메시지를 최대 한번만 전송(Consumer가 메시지를 받던 말건 전송)
- Fire and forget
- Consumer는 메세지를 최대 한번만 수신
- 네트워크 유실
- Producer 애플리케이션에서 발생한 예외
- Consumer 애플리케이션에서 발생한 예외
- 장점
- 간단한 구조
- 간단한 개발
- 단점
- 메시지 유실
At-least-once delivery(최대 한번 전달 - 정확도가 조금 더 높음)
- Producer는 메시지를 최소 한번 이상 발송(최소 한번 이상 Consumer에게 전달)
- 발송 메시지 상태 관리
- Consumer는 메시지를 최소 한번 이상 수신
- ACK 유실로 인한 재 발송
- 장점
- Producer는 메시지 발송 보장
- 효과 대비 쉬운 개발
- 단점
- 멱등성(idempotent)을 보장하는 Consumer
Exactly-least-once delivery(최대 한번 전달 - 정확도가 가장 더 높음)
- 메시지는 정확하게 한번만 전송한다.
- 장점
- 누락과 중복이 없음
- 단점
- 가장 어려운 개발 난이도
- Producer, Consumer에서 모든 상태 관리
- MessageQueue 기능에 의존한 개발
- MessageQueue 추가로 인한 시스템 복잡도 증가
여러분의 애플리케이션은 데이터를 어떻게 전달할까?
최소 한번은 전달 할까?
우리가 Spring의 Getting Started 코드를 그대로 복붙해서 사용한다면 최소 한번 전달을 지키는 것은 어렵다.
RDB를 사용하는 애플리케이션에서 전달 방법
RDB를 사용하는 애플리케이션에는 최소 한번 전달을 어떻게 구현할까?
1. 서비스별 데이터베이스 패턴(Database per Service Pattern)
- 마이크로 서비스 아키텍처 패턴
- 모던 애플리케이션의 일반적인 형태
아주 흔하게 볼 수 있는 패턴이다. 마이크로 서비스 아키텍처의 가장 일반적인 패턴이라고 할 수 있다.
(필자의 캡스톤 프로젝트에서 사용했던 패턴인 것 같다.)
즉, 서비스마다 독립된 데이터 저장소를 가지고 있고,
각 컴포넌트마다 자기 데이터베이스의 데이터를 처리를 한다.
위 패턴에서 어떤 컴포넌트에서 다른 컴포넌트로 데이터를 전파를 해야될 때를 가정해보자.
- DB 트랜잭션을 시작해 데이터를 정상적으로 DB에 저장해야 된다.
- 그 후, 다른 컴포넌트에게 REST API를 사용해서 전파한다.
아래는 코드다.
- taskRepository의 save 메소드를 사용하여 일단 저장을 한다.
- 그 후, `eventHandler`의 `propagate()` 메소드를 사용해 다른 컴포넌트에 나의 이벤트나 메시지나
혹은 REST API 콜을 하는 것을 전달한다. - 마지막으로, `@Transactional` 이라는 어노테이션을 붙일 것이다. 왜냐? RDB를 사용하고 안전하게 저장해야 하니까
@Transactional
- 스프링 프레임워크에서 제공하는 어노테이션
- AOP를 사용하여 Proxy 객체 생성
- Target 객체에 추가적인 코드 삽입
- `transaction.aspectj.AnnotationTransactionAspect.java`
- 실행 순서
- 데이터 저장
- 이벤트 전달
- 트랜잭션 커밋/롤백
근데 위 패턴은 아래 상황에 문제가 생길 수 있다.
1. REST API를 사용해서 다른 컴포넌트에 데이터를 전달
2. DB 트랜잭션을 시작
위 과정이 DB에 깔끔하게 커밋되면 아주 좋은 상황인데 현실은 그렇지 않다.
모종의 이유로 예외가 생기고 데드락이 발생하거나 SQL이 문제가 생길 수 있다.
3. 이슈 발생 시 롤백 진행
4. REST API만 전달
이러면 어떻게 될까?
가장 중요한 데이터는 `RDB`에 저장되지 않은 상태로, 원본 데이터가 없는 상태로 이벤트만 발생된다.
그럼 이건 어떻게 방지가 가능할까?
트랜잭션 Commit 이벤트를 사용하면 된다.
트랜잭션 Commit 이벤트를 사용하는 방법
- @TransactionalEventListener(Spring Framework에서 제공하는 Event를 이용하는 것)
- TransactionSynchronizationManager, TransactionSynchronization(콜백 메서드를 호출하는 것)
@TransactionalEventListener 사용 코드
아까 말한 `EventHandler`의 `propagate()` 메소드.
즉, REST API 혹은 MessageQueue를 사용해서 다른 컴포넌트에 데이터를 전달해야 하는 경우에는
`@TransactionalEventListener`라는 어노테이션을 `propagate`라는 메소드에 심어준다.
이러면 해결된 걸까?
그렇지 않다.
위처럼 다른 컴포넌트로의 REST API 작업이 실패할 수도 있다.
왜냐? 네트워크는 신뢰할 수 없기 때문이다.
즉, 아래 사진과 같이 DB에만 데이터가 저장되고 전파는 못하게 되는 상황이 연출될 것이다.
그렇기 때문에 방지하고자 아래 코드처럼 `@Retryable`을 달아보자.
이러면 끝일까?
그렇지 않다.
Retry도 실패할 수 있다.
그래서 아래와 같이 설정할 수도 있다.
최대 시도 횟수는 3회이며 재시도마다 `BackOff`를 100 밀리세컨드로 준다는 것이다.
하지만, 위 방법조차 네트워크는 믿을 수 없기에 계속 실패할 수 있다.
언제 돌아올지 모른다.
처리해야 하는 데이터와 이벤트가 굉장히 중요하다고 한다면
즉, 우리가 애플리케이션들에서 원하는 기능에 트랜잭션 처리와 이벤트 전달에 실패가 없으려면
아래와 같은 패턴을 이용해야 한다.
마이크로 서비스 아키텍처 패턴
- Transactional Outbox Pattern
- RDB를 Message Queue로 사용
- OLTP에 Event Message를 포함하는 패턴
- Polling Publisher Pattern
- RDB Message Queue Polling & Publishing
위 두 가지를 섞어서 사용해야 한다.
`Transactional Outbox Pattern`은 `RDB`를 MessageQueue처럼 쓰는거다.
즉, 이벤트나 Message가 발행되면 RDB에 원 트랜잭션으로 같이 저장을 하는 것이다.(원 트랜잭션에)
이렇게 되면 둘다 한번에 저장하므로 저장이되거나 롤백이 되거나 할 것이다.
그러면 어떻게 Publish를 하느냐
Daemon이나 스케줄러를 하나 띄워서 DB에 저장된 이벤트를 주기적으로 폴링을 하고 발행을 해야되는 친구를 추가한다.
이때, 이벤트 저장을 위해 중요한 필드는 아래 4종류가 될 것이다.
- event_id는 PK이며 이벤트 순서 보장하기 위함이다. 가장 빠른 PK를 활용한 인덱스를 활용하는 것이다.
- create_at는 이벤트 발생 시간을 적어줘야 한다. 그래야 Consumer가 이벤트를 받을 때 오래된 이벤트는 거르는 것이 가능하다.
- 이때, Datetime를 사용할텐데 정확도를 적어주는게 좋다.
밀리세컨드, 나노세컨드로 정확도를 설정한다면 같이 적어주는게 좋다.
- 이때, Datetime를 사용할텐데 정확도를 적어주는게 좋다.
- status 이벤트의 상태를 저장하는 친구이다.
- payload는 메시지를 저장해야 하는 payload이다.
아래는 코드이다.
한 트랜잭션에 이벤트와 데이터가 저장이 될 것이다.
Polling Publisher 패턴을 적용한 코드
`@Scheduled`를 사용해 5초마다 한번씩 실행한다.
위 코드에서 `@Transactional`이라는 어노테이션을 붙인 이유는 여러 이유로 인해
버그가 났을 때, 롤백을 하기 위함이다.
이러면 10개를 처리할 때 하나에 문제가 생기면 그걸 계속 퍼다 나를 것이고
이때, Consumer에서 멱등성 있게 코드를 잘 작성했다면 정상적인 처리가 될 것이다.
하지만, 위의 모든 내용은 아쉽게 장단점이 존재한다.
장점
- REST API 환경에서 At-least-once를 구현할 수 있다.
단점
- Polling, Publisher 과정으로 의한 지연 처리
- 코드처럼 5초마다 Polling 하기 때문에 최대 5초뒤에 적용될 수 있다.
- 또한, Polling Pubhser 패턴에서 버그가 있다라고 한다면 무제한으로 늘어날 수 있다.
- 그렇기에 실시간성이 필요한 데이터의 경우 위의 패턴을 사용할 수 없다.
- 데이터 베이스 부하
- 데이터 베이스 비례한 처리속도
'Infra > DevOps' 카테고리의 다른 글
분산 시스템에서 데이터를 전달하는 효율적인 방법 - 2 (0) | 2024.09.20 |
---|---|
[Ansible] Ansible 설치 가이드 (0) | 2024.07.29 |
[Docker] Docker 설치 가이드 (0) | 2024.07.29 |
[Jenkins] Jenkins 설치 가이드 (0) | 2024.07.29 |
[DevOps] Configuration Drift (0) | 2024.07.21 |