Post

Redis 메시지 처리: Pub/Sub, List 기반 메시징 큐

메시징 큐와 이벤트 스트림 비교, Pub/Sub 시스템 구현, List를 활용한 메시징 큐 구축 방법

Redis 메시지 처리: Pub/Sub, List 기반 메시징 큐

1. 메시징 큐와 이벤트 스트림

  • 서비스 간 통신이 불가능한 상황이 바로 장애로 이어지지 않게, 당장 메시지를 처리하지 못하더라도 보낸 메시지를 어딘가에 쌓아 둔 뒤 나중에 처리할 수 있는 채널을 만들어 주는 것이 메시지 브로커의 핵심 역할
  • 메시징 큐와 이벤트 스트림의 차이
    • 방향성
      • 메시징 큐의 생산자는 소비자의 큐로 데이터를 직접 푸시
      • 2개의 서비스에 같은 메시지를 보내야 할 때 메시징 큐를 이용하면 생산자는 2개의 각각 다른 메시징 큐에 각각 데이터를 푸시해야 한다
      • 스트림을 이용한다면 생산자는 스트림의 특정 저장소에 하나의 메시지를 보낼 수 있고, 메시지를 읽어가고자 하는 소비자들은 스트림에서 같은 메시지를 풀해갈 수 있기 때문에 메시지를 복제해서 저장하지 않아도 된다
    • 영속성
      • 메시징 큐에서는 소비자가 데이터를 읽어갈 때 큐에서 데이터를 삭제
      • 이벤트 스트림에서 구독자가 읽어간 데이터는 바로 삭제되지 않고, 저장소 설정에 따라 특정 기간 동안 저장될 수 있다
      • 메시징 큐에서 소비자는 새롭게 추가된 이휴의 이벤트만 확인할 수 있다
      • 스트림 방식에서는 메시지를 생산할 때 구독자를 지정하지 않고, 스트림에 쌓인 데이터는 일정 기간 동안 지워지지 않기 때문에 추가된 서비스도 스트림에 남아 있는 이전 데이터의 히스토리를 볼 수 있다
    • 메시징 큐는 일대일 상황에서 한 서비스가 다른 서비스에게 동작을 지시할 때 유용하게 사용될 수 있으며, 스트림은 다대다 상황에서 유리하다

1-1. 레디스를 메시지 브로커로 사용하기

  • 레디스의 pub/sub을 사용해 간단한 메시지 브로커를 구현할 수 있다
  • 발행자가 특정한 채널에 데이터를 전송하면 이 채널을 듣고 있는 모든 소비자는 데이터를 바로 소비할 수 있다
  • 레디스의 pub/sub에서 모든 데이터는 한 번 채널 전체에 전파된 뒤 삭제되는 일회성의 특징을 가지며, 메시지가 잘 전달됐는지 등의 정보는 보장하지 않는다
  • 완벽하게 메시지가 전달돼야 하는 상황에는 적합하지 않을 수 있지만 fire-and-forget 패턴이 필요한 간단한 알림 서비스에서는 유용하게 사용할 수 있다
  • fire-and-forget 패턴
    • fire-and-forget 패턴은 비동기 프로그래밍에서 사용되는 디자인 패턴
    • 어떤 작업을 실행하고 그 결과에 대한 응답을 기다리지 않고 바로 다음 코드를 실행
    • 이 패턴은 주로 성능 향상이나 비동기 작업을 수행할 때 사용되며, 작업의 완료나 결과에 대한 처리가 필요하지 않을 때 유용하게 사용된다
    • fire-and-forget 패턴을 사용할 때는 결과 확인이나 오류 처리를 고려하지 않고 작업을 진행하므로, 신뢰성이 필요한 경우에는 사용하지 않아야 한다
  • list를 메시징 큐로 사용하기
    • list의 데이터는 푸시와 팝이 가능하며 애플리케이션은 list에 데이터가 있는지 매번 확인할 필요 없이 대기하다가 list에 새로운 데이터가 들어오면 읽어갈 수 있는 블로킹 기능을 사용할 수 있다
  • stream을 스트림 플랫폼으로 사용하기
    • 레디스의 stream은 카프카에서 영감을 받아 만들어진 자료 구조로, 데이터는 계속해서 추가되는 방식으로 저장된다(append-only)
    • 소비자와 소비자 그룹이라는 개념을 이용하면 카프카에서와 비슷하게 데이터의 분산 처리를 구현할 수 있다
    • stream에 저장되는 메시지를 실시간으로 리스닝하여 소비할 수도 있으며, 저장돼 있는 데이터를 시간대별로 검색하는 것도 가능하다



2. 레디스의 pub/sub

  • 레디스는 pub/sub 기능을 제공하며 레디스 노드에 접근하는 모든 클라이언트는 발행자와 구독자가 될 수 있다
  • 발행자는 특정 채널에 메시지를 보낼 수 있으며, 구독자는 특정 채널을 리스닝하다가 메시지를 읽어갈 수 있다
  • 레디스에서 pub/sub은 매우 가볍기 때문에 최소한의 메시지 전달 기능만 제공한다
    • 발행자는 메시지를 채널로 보낼 수 있을 뿐, 어떤 구독자가 메시지를 읽어가는지, 정상적으로 모든 구독자에게 메시지가 전달됐는지 확인할 수 없다
    • 구독자 또한 메시지를 받을 수 있지만 해당 메시지가 언제 어떤 발행자에 의해 생성됐는지 등의 메타데이터는 알 수 없다
  • 한 번 전파된 데이터는 레디스에 저장되지 않으며, 단순히 메시지의 통로 역할만 한다
  • 만약 특정 구독자에게 장애가 생겨 메시지를 받지 못했다 하더라도 그 사실을 알 수 없기 때문에 정합성이 중요한 데이터를 전달하기에는 적합하지 않을 수 있다

2-1. 메시지 publish하기

1
PUBLISH hello world
  • PUBLISH 커맨드를 이용해 데이터를 전파할 수 있다
  • 위 커맨드를 수행하면 hello라는 채널을 수신하고 있는 모든 서버들에 world라는 메시지가 전파된다
  • 메시지가 전파된 후에는 메시지를 수신한 구독자의 수가 반환된다

2-2. 메시지 구독하기

1
SUBSCRIBE event1 event2
  • SUBSCRIBE 커맨드를 이용해 특정 채널을 구독할 수 있다
  • 클라이언트가 위 커맨드를 수행하면 event1과 event2 채널을 동시에 구독하기 시작
  • 클라이언트가 구독자로 동작할 때에는 새로운 채널을 구독할 수 있지만 pub/sub과 관련되지 않은 다른 커맨드를 수행할 수는 없다
  • 구독자가 수행할 수 있는 커맨드는 SUBSCRIBE, SSUBSCRIBE, SUNSUBSCRIBE, PSUBSCRIBE, UNSUBSCRIBE, PUNSUBSCRIBE, PING, RESET, QUIT
  • PSUBSCRIBE 커맨드를 사용하면 일치하는 패턴에 해당하는 채널을 한 번에 구독할 수 있으며, 이때 레디스는 glob-style 패턴을 지원한다

2-3. 클러스터 구조에서의 pub/sub

  • 레디스 클러스터에서 pub/sub을 사용할 때, 메시지를 발행하면 해당 메시지는 클러스터에 속한 모든 노드에 자동으로 전달된다
  • 따라서 레디스 클러스터의 아무 노드에 연결해 SUBSCRIBE 커맨드를 사용하면 데이터를 수신할 수 있다

2-4. sharded pub/sub

  • pub/sub을 사용할 때 메시지가 모든 노드에 복제되는 비효율을 해결하기 위해 레디스 7.0에서는 sharded pub/sub 기능이 도입됐다
  • sharded pub/sub 환경에서 각 채널은 슬롯에 매핑된다. 클러스터에서 키가 슬롯에 할당되는 것과 동일한 방식으로 채널이 할당되며, 같은 슬롯을 가지고 있는 노드 간에만 pub/sub 메시지를 전파한다
    1
    
    SPUBLISH apple a
    
    • SPUBLISH 커맨드로 발행된 메시지는 모든 노드에 전파되지 않으며 노드의 복제본에만 전달된다
    • apple 채널은 apple 키 값을 할당받을 수 있는 슬롯을 포함한 마스터 노드에 연결될 수 있도록 리다이렉트된다
  • sharded pub/sub을 이용한다면 클러스터 구조에서 pub/sub되는 메시지는 모든 노드로 전파되지 않기 때문에 불필요한 복제를 줄여 자원을 절약할 수 있다는 장점이 있다



3. 레디스의 list를 메시징 큐로 사용하기

  • list에는 큐의 tail과 head에서 데이터를 넣고 뺄 수 있는 LPUSH, LPOP, RPUSH, RPOP 커맨드가 존재하기 때문에 애플리케이션 특성에 맞는 메시징 큐를 직접 구현할 수 있다는 장점이 있다

3-1. list의 EX 기능

  • 트위터는 각 유저의 타임라인 캐시 데이터를 레디스에서 list 자료 구조로 관리한다
  • 타임라인 캐시에 데이터를 저장할 때 RPUSH 커맨드가 아닌 RPUSHX 커맨드를 사용한다
    • RPUSHX는 데이터를 저장하고자 하는 list가 이미 존재할 때만 아이템을 추가하는 커맨드다
    • 이 커맨드를 이용하면 이미 캐시된(이미 키가 존재하는) 타임라인에만 데이터를 추가할 수 있다. 자주 들어오지 않는 유저에 대해서는 타임라인 캐시 데이터를 굳이 관리할 필요가 없기 때문이다.
  • 사용자의 캐시가 이미 존재하는지의 유무를 애플리케이션에서 확인하는 과정 없이, 모든 로직을 레디스에서 제어할 수 있기 때문에 불필요한 확인 과정을 줄여 성능을 향상시킬 수 있다

3-2. list의 블로킹 기능

  • 레디스를 이벤트 큐로 사용할 경우 블로킹 기능 또한 유용하게 사용할 수 있다
  • BRPOP과 BLPOP은 각각 RPOP과 LPOP에 블로킹을 추가한 커맨드다
    1
    
    BRPOP queue:a 5
    
    • 위 커맨드는 queue:a에 데이터가 입력될 때까지 최대 5초 동안 대기하고, 5초가 경과하면 nil을 반환하라는 의미다
  • 클라이언트가 BLPOP을 사용해 데이터를 요청했을 때 list에 데이터가 있으면 즉시 반환한다
  • 만약 데이터가 없을 경우에는 list에 데이터가 들어올 때까지 기다린 후에 들어온 값을 반환하거나, 클라이언트가 설정한 타임아웃 시간만큼 대기한 후 nil 값을 반환한다
  • 타임아웃 값을 0으로 설정하면 데이터가 리스트에 들어올 때까지 제한 없이 기다리라는 의미로 쓰인다
  • 하나의 리스트에 대해 여러 클라이언트가 동시에 블로킹될 수 있으며, 리스트에 데이터가 입력되면 가장 먼저 요청을 보낸 클라이언트가 데이터를 가져간다
  • BRPOP은 RPOP과 다르게 2개의 데이터를 반환한다. 첫 번째는 pop된 리스트의 키 값을 반환하고, 두 번째에 반환된 데이터의 값을 반환한다. 이렇게 설계된 이유는 동시에 여러 개의 리스트에서 대기할 수 있게 하기 위해서다
    1
    
    BRPOP queue:a queue:b queue:c timeout 1000
    
    • 위 커맨드에서 BRPOP은 1000초 동안 queue:a queue:b queue:c 중 어느 하나라도 데이터가 들어올 때까지 기다린 뒤, 그중 하나의 리스트에 데이터가 들어오면 해당 값을 읽어온다

3-3. list를 이용한 원형 큐

  • 특정 아이템을 계속해서 반복 접근해야 하는 클라이언트, 혹은 여러 개의 클라이언트가 병렬적으로 같은 아이템에 접근해야 하는 클라이언트에서는 원형 큐를 이용해 아이템을 처리할 수 있다
  • RPOPLPUSH 커맨드를 사용하면 간편하게 원형 큐를 사용할 수 있다
This post is licensed under CC BY 4.0 by the author.