- 모듈이 서로 느슨하고 적절하게 연결시킨 구조 선호 ➡️ 메시지 브로커 필요
- 모듈 간의 통신에서는 되도록 비동기 통신(async) 사용 권장
- 메시지 브로커의 종류
1. 메시징 큐와 이벤트 스트림
메시징 큐
- 생산자(producer): 데이터를 생성
- 소비자(consumer): 데이터를 수신
이벤트 스트림
- 발행자(publisher): 데이터 생성
- 수신자(subscriber): 데이터 조회
메시징 큐 vs. 이벤트 스트림
- 방향성
- 메시징 쿠의 생산자는 소비자의 큐로 데이터를 직접 푸시하기 때문에 2개의 서비스에 같은 메시지를 보낼 때 2번 푸시해야 함
- 이벤트 스트림에서 발행자는 스트림의 특정 저장소에 하나의 메시지를 보낼 수 있고, 메시지를 읽어가고자 하는 수신자들은 스트림에서 같은 메시지를 풀(pull)해 갈 수 있기 때문에 메시지를 복제해서 저장하지 않아도 됨
- 데이터의 영속성
- 메시징 큐에서는 소비자가 데이터를 읽어갈 때 큐에서 데이터 삭제
- 이벤트 스트림에서 구독자가 읽어간 데이터는 바로 삭제되지 않고, 저장소의 설정에 따라 특정 기간 동안 저장됨
메시징 큐는 일대일 상황에서 한 서비스가 다른 서비스에게 동작을 지시할 때 유용
스트림은 다대다 상황에서 유리함
레디스를 메시지 브로커로 사용하기
- 레디스의 pub/sub 기능을 이용해 메시지 브로커 구현
- 레디스 pub/sub
- 모든 데이터는 한 번 채널 전체에 전파된 뒤 삭제(일회성)
- 메시지가 잘 전달됐는지 등의 정보 보장 X(fire-and-forget 패턴에 사용됨)
- c.f) fire-and-forget 패턴: 비동기 프로그래밍에서 사용되는 디자인 패턴으로, 어떤 작업을 실행하고 그 결과에 대한 응답을 기다리지 않고 바로 다음 코드를 실행하는 것
- 레디스의 list와 stream을 이용해 각각 메시징 큐와 이벤트 스트림으로 사용하기 알맞음
2. 레디스의 pub/sub
- 레디스의 pub/sub은 매우 가볍기 때문에 최소한의 메시지 전달 기능만 제공
- 발행자는 메시지를 채널로 보낼 수 있을 뿐, 어떤 구독자가 메시지를 읽어가는지, 정상적으로 모든 구독자에게 메시지가 전달됐는지 확인할 수 없음
- 구독자는 메시지를 받을 수 있지만 해당 메시지가 언제 어떤 발행자에 의해 생성되었는지 등 메타데이터는 알 수 없음
- 한 번 전파된 데이터는 레디스에 저장 X ➡️ 정합성이 중요한 데이터를 전달하기에는 적합하지 않음
명령어 | 설명 |
---|
PUBLISH | 데이터 전파(발행자) |
SUBSCRIBE | 특정 채널 구독(구독자) |
클러스터 구조에서의 pub/sub
- 클러스터: 레디스가 자체적으로 제공하는 데이터 분산 형태의 구조
- 메시지를 발행하면 해당 메시지는 클러스터에 속한 모든 노드에 자동으로 전달
- 클러스터는 주로 대규모 서비스에서 데이터를 분산해서 저장하고 처리하기 위해 도입
- 레디스 클러스터 내에서 pub/sub을 사용할 때 메시지가 모든 레디스 노드에 복제되는 방식은 클러스터 환경의 핵심 목표와는 부합하지 않음
➡️ 불필요한 리소스 사용, 네트워크 부하
sharded pub/sub
- 각 채널은 슬롯에 매핑
- 클러스터에서 키가 슬롯에 할당되는 것과 동일한 방식으로 채널 할당, 같은 슬롯을 가지고 있는 노드 간에만 pub/sub 메시지 전파
- 클러스터 구조에서 pub/sub 되는 메시지는 모든 노드로 전파되지 않기 때문에 불필요한 복제를 줄여 자원 절약 가능
3. 레디스의 list를 메시징 큐로 사용하기
list의 EX 기능
명령어 | 설명 |
---|
RPUSHX | 데이터를 저장하고자 하는 list가 이미 존재할 때에만 아이템 추가 |
- SNS 타임라인의 경우 이미 캐시된(이미 키가 존재하는) 타임라인에만 데이터를 추가할 수 있음(자주 사용하지 않는 사람의 타임라인 캐시 데이터를 관리할 필요가 없음)
- 사용자의 캐시가 이미 존재하는지 유무를 애플리케이션에서 확인하는 불필요한 확인 과정이 없어 성능 향상 가능
list의 블로킹 기능
- 이벤트 기반(event-driven) 구조: 이벤트 루프를 돌며 신규로 처리할 이벤트가 있는지 체크, 새로운 이벤트가 없을 경우 정해진 시간(polling interval) 동안 대기한 뒤 다시 이벤트 큐에 데이터가 있는지 확인하는 과정 반복(polling)
- 단점: 폴링 프로세스가 진행되는 동안 애플리케이션과 큐의 리소스가 불필요하게 소모, 폴링 인터벌 동안 대기한 뒤 다시 확인하는 과정을 거치기 때문에 이벤트를 즉시 처리할 수 없음
- BRPOP, BLPOP: list에 데이터가 있으면 즉시 반환, 만약 데이터가 없을 경우 기다려서 들어온 값을 반환 or 클라이언트가 설정한 타임아웃 시간 만큼 대기한 후 nil 반환
list를 이용한 원형 큐
- 특정 아이템을 반복 접근해야 하는 클라이언트, 혹은 여러 개의 클라이언트가 병렬적으로 같은 아이템에 접근 해야 하는 클라이언트의 경우 원형 큐(circular queue)를 이용해 아이템 처리
- RPOPPUSH
4. Stream
레디스의 Stream과 아파치 카프카
- Stream: 대용량, 대규모의 메시징 데이터를 빠르게 처리할 수 있도록 설계됨, 데이터를 계속해서 추가하는 방식으로 저장되는(append-only) 자료 구조
- stream 활용
- 백엔드 개발자들은 대량의 데이터를 효율적으로 처리하는 플랫폼으로 활용
- 데이터 엔지니어들은 여러 생산자가 생성한 데이터를 다양한 소비자가 처리할 수 있게 지원하는 데이터 저장소 및 중간 큐잉 시스템으로 활용
스트림이란?
- 연속적인 데이터의 흐름, 일정한 데이터 조각의 연속
데이터의 저장
메시지의 저장과 식별
- 카프카
- 토픽: 각각의 분리된 스트림, 같은 데이터를 관리하는 하나의 그룹
- 각 메시지는 0부터 시작해 증가하는 시퀀스 넘버로 식별
- 시퀀스 넘저는 토픽 내의 파티션 안에서만 유니크하게 증가하기 때문에 토픽이 1개 이상의 파티션을 갖는다면 메시지는 하나의 토픽 내에서 유니크하게 식별되지 않음
- 레디스 stream
- 각 메시지는 시간과 관련된 유니크한 ID를 가지며, 이 값은 중복되지 않음
스트림 생성과 데이터 입력
- 카프카
- 각 스트림은 토픽으로 관리됨
- 생성자는 데이터를 토픽에 푸시, 소비자는 토픽에서 데이터 읽음
- 토픽 생성 후 프로듀서를 이용해 메시지 보냄
- 레디스 stream
- 따로 stream을 생성하는 과정 필요 X
- XADD 커맨드 이용
- 데이터는 hash 자료 구조처럼 필드-값 쌍으로 저장되므로 각 메시지마다 유동적인 데이터 저장 가능
데이터의 조회
- 카프카
- 소비자는 특정 토픽을 실시간으로 리스닝하며, 새롭게 토픽에 저장되는 메시지를 받을 수 있음
- 레디스 stream
- 실시간으로 처리되는 데이터 리스닝(XREAD)
- ID를 이용해 필요한 데이터 검색(XRANGE, XREVRANGE)
소비자와 소비자 그룹
- 팬아웃(fan-out): 같은 데이터를 여러 소비자에게 전달하는 것
- 같은 데이터를 여러 소비자가 나눠서 가져가기 위해서는?
- 같은 역할을 하는 여러 개의 소비자를 이용해 메시지를 병렬 처리함으로써 서비스의 처리 성능을 높일 수 있음
- 레디스 stream
- 데이터가 저장될 때마다 고유한 ID(시간)를 부여받아 순서대로 저장됨
- 소비자에게 데이터가 전달될 때 순서 항상 보장(시간순)
- 카프카
- 유니크 키는 파티션 내에서만 보장되기 때문에 소비자가 여러 파티션에서 토픽을 읽어갈 때에는 데이터의 순서를 보장할 수 없음
- 데이터의 정렬이 보장되지 않기 때문에 메시지 순서 보장을 위해 소비자 그룹 사용
소비자 그룹
- 카프카
- 소비자 그룹에 여러 소비자 추가 가능
- 소비자는 토픽 내의 파티션과 일대일로 연결됨
- 파티션을 이용해 소비자의 부하 분산 관리
- 레디스 stream
- 레디스 stream은 메시지가 전달되는 순서가 보장되기 때문에 카프카의 소비자 그룹과는 약간 다름
- 소비자 그룹 내의 한 소비자는 다른 소비자가 아직 읽지 않은 데이터만을 읽어감
- XGROUP: 소비자 그룹 생성
- XREADGROUP: 소비자 그룹 이용해 데이터 읽음, 마스터에서만 호출 가능
- stream의 상태를 나타내는 개념으로 간주
- stream과 소비자 그룹은 독립적으로 동작 가능
- 하나의 소비자 그룹에서 여러 개의 stream 리스닝 가능
- 파티션이라는 분할 없이도 소비자 그룹이라는 개념을 이용해 여러 소비자에게 데이터 분산 가능
ACK와 보류 리스트
- 레디스 stream
- 각 소비자별로 읽어간 메시지에 대한 리스트를 새로 생성하며, 마지막으로 읽어간 데이터의 ID로 last_delivered_id 값 업데이트(중복 전달 방지)
- 보류 리스트를 이용해 소비자가 처리한 데이터 파악
- 데이터가 처리됐다는 뜻의 ACK를 보내면 보류 리스트에서 해당 메시지 삭제
- 카프카
- 파티션별 오프셋 관리
- __consumer_offsets: 소비자가 지정된 토픽의 특정 파티션의 메시지를 읽으면 소비자 그룹, 토픽, 파티션 내용이 통합되어 저장됨
- 오프셋은 소비자가 다음으로 읽어야 할 위치(마지막으로 읽은 위치 X)
레디스 stream의 메시지 보증 전략
- at most once: 메시지를 최소 한 번 보내는 것, 메시지를 받자마자 실제 처리하기 전에 먼저 ACK 보냄
- at least once: 받은 메시지를 모두 처리한 뒤 ACK, 실제로 메시지가 처리됐지만 ACK를 전송하기 전에 소비자가 종료되는 상황 발생 가능
- exactly once: 모든 메시지가 무조건 한 번씩 전송되는 것 보장, 이미 처리된 메시지인지 아닌지를 확인하는 과정 필요
메시지의 재할당
- XCLAIM: 메시지의 소유권을 다른 소비자에게 할당, 최소 대기 시간 지정
- 메시지가 보류 상태로 머무른 시간이 최소 대기 시간을 초과한 경우에만 소유권을 변경할 수 있도록 해서 같은 메시지가 2개의 다른 소비자에게 중복으로 할당되는 것 방지
메시지의 자동 재할당
- XAUTOCLAIM
- 소비자가 직접 보류했던 메시지 중 하나를 자동으로 가져와서 처리
- 할당 대기 중인 다음 메시지의 ID를 반환하는 방식으로 동작하기 때문에 반복적 호출 가능
- 지정한 소비자 그룹에서 최소 대기 시간을 만족하는 보류 중인 메시지가 있다면 지정한 소비자에게 소유권을 재할당하는 방식으로 동작
메시지의 수동 재할당
- stream 내 각 메시지는 counter 값 가짐
- counter는 XREADGROUP을 이용해 소비자에게 할당하거나 XCLAIM을 이용해 재할당할 경우 1씩 증가
- counter가 특정 값에 도달하면 이 메시지를 특수한 다른 stream으로 보내, 관리자가 추후에 처리할 수 있도록 함 ➡️ dead letter
stream 상태 확인
- XINFO
- XINFO consumer: 특정 소비자 그룹에 속한 소비자의 정보
- XINFO GROUPS: stream에 속한 전체 소비자 그룹 list
- XINFO STREAM: stream 자체의 정보