Redis Pub/Sub Performance

sojw·2024년 3월 14일
0

Redis의 Pub/Sub 특징

  • 메시지가 큐에 저장되지 않음.
  • kafka의 컨슈머 그룹같은 분산처리 개념이 없음.
  • 메시지 발행시 push 방식으로 subscriber들에게 전송
  • subscriber가 늘어날수록 성능이 저하

Redis의 Pub/Sub는 언제 사용할까?

  • 실시간으로 빠르게 전송되어야 하는 메시지
  • 메시지 유실을 감내할 수 있는 케이스(메시지의 한시적으로만 유용한 케이스)
  • 최대 1회 전송(at-most-once)패턴이 적합한 경우
  • Subscriber들이 다양한 채널을 유동적으로 바꾸면서 한시적으로 구독하는 경우

성능: RabbitMQ와 Redis 게시/구독

메시지 처리 방식의 차이는 다양한 시나리오에서 RabbitMQ와 Redis의 성능에 영향을 미칩니다.

속도

Redis는 주로 메모리에서 메시지를 처리하므로 RabbitMQ보다 훨씬 빠릅니다. 하지만 Redis 서버에 장애가 발생하면 읽지 > 않은 메시지가 손실될 위험이 있습니다.

반대로 지속 모드에서 작동하는 경우 RabbitMQ는 다음 메시지를 보내기 전에 각 소비자의 승인을 기다립니다. 또한 > RabbitMQ는 메시지를 디스크에 저장하는 데 시간이 더 걸리므로 평균적인 메시지 교환 속도가 더 느립니다.

Redis는 초당 최대 수천만 개의 메시지를 전송할 수 있는 반면, RabbitMQ는 초당 최대 수만 개의 메시지만 처리할 수 있습니다.

가용성

메시지 브로커 시스템이 노드를 복제할 수 있도록 하는 클러스터링은 RabbitMQ와 Redis에서 서로 다른 방식으로 > 처리됩니다.

RabbitMQ의 경우 관련 데이터와 함수를 포함하는 다중 노드가 클러스터에 복제됩니다. 하지만 메시지 대기열은 피어 관계를 공유하는 이러한 노드 간에 복제되지 않습니다. 이를 위해 개발자는 복제를 지원하는 특수한 메시지 대기열을 사용합니다.

반면, Redis 클러스터는 특정 버전 이상의 Redis에서 도입된 기능입니다. 각 리더 노드의 데이터를 하나 또는 여러 개의 팔로워에 복사합니다. 리더 노드에 장애가 발생하면 팔로워가 대신 고가용성 메시지 전송 기능을 제공합니다.

  • 토스ㅣSLASH 23 - 실시간 시세 데이터 안전하고 빠르게 처리하기

UDP(User Datagram Protocol)
UDP 멀티캐스터는 속도는 빠르지만 라우터 설정과 Kubernetes 배포 설정이 필요하다는 단점이 있습니다.

Kafka
높은 처리량과 뛰어난 안정성을 제공하지만 Redis Pub/Sub 보다 속도가 느리다는 단점이 있습니다.

Redis Pub/Sub
Kafka보다 5배 빠른 속도를 보여줬습니다. 즉, 낮은 지연시간과 사용하기 쉽고 편리한 커맨드 지원을 해줍니다.

pubsub_channels

server.pubsub_channels 의 실제 타입은 Map<ChannelName, Set> 입니다.

특정 채널 이름을 기준으로 클라이언트 리스트를 찾아 이를 순회하며 메세지를 보내는 형식으로 되어 있습니다.

pubsub_patterns

그에 반해 pubsub_patterns 는 LinkedList<Pair<PatternStr, Client>> 인데, 해당 타입에서도 유추할 수 있듯이 패턴 문자열과 클라이언트 쌍을 통해 고유성(Uniqueness)을 보장합니다. 다시 말해, 레디스는 PSUBSCRIBE에 > 대해 특정 패턴 문자열 별로 클라이언트를 묶는 방식을 사용하지 않고 있습니다.

이러한 이유로 PSUBSCRIBE 를 통해 등록된 클라이언트가 많을 경우, 또한 등록된 패턴 문자열이 다양할 경우 레디스는 그만큼의 부하를 그대로 받게 됩니다.

개선된 어댑터로 인해 부하가 더욱 심해진 이유

Time complexity: O(N+M) where N is the number of clients subscribed to the receiving channel and M is the total number of subscribed patterns (by any client).

실제로, PUBLISH 는 위와 같은 레디스의 구조로 인해 O(N + M) 의 시간 복잡도를 가집니다.

개선된 어댑터에서는 특정 Room 으로만 PUBLISH를 하기 위해 변경된 점이 추가적으로 있었는데, 바로 PSUBSCRIBE 를 동적으로 진행하는 부분이었습니다.

  public async addAll(id: SocketId, rooms: Set<Room>) {
    super.addAll(id, rooms)
    rooms.forEach(async (room) => {
      if (!this.subs.has(this.roomTopic(room))) {
        this.subs.add(this.roomTopic(room))
        await this.subClient.pSubscribe(
          this.roomTopic(room),
          (msg, channel) => this.onmessage(null, channel, msg),
          true,
        )
      }
    })
  }

이는 분명히 PUBLISH 의 성능 저하를 야기할 수 있는 코드이기에 결국엔 소켓 서버에는 부하를 줄일지언정 레디스에 부하를 더 키우는 꼴이 되었죠.

0개의 댓글