채널톡 실시간 채팅 서버 개선 여정(2)

Nats.io로 Redis 대체하기

Hammer • Backend Engineer

  • 테크 인사이트

앞서 '채널톡 실시간 채팅 서버 개선 여정 : 1편'에서는 채널톡의 실시간 메세지 처리를 위해 사용하고 있는 socket.io 서버의 구조와, 서버의 병목이 발생한 지점에 대해 진단 했습니다. 저희가 해결하고 있는 문제를 다시 한번 짚어보자면 다음과 같습니다.

문제: 채널톡 소켓 서비스의 단일 redis pubsub이 서비스의 병목을 초래하고 있음

1편에서는 redis pubsub을 사용하는 어뎁터인 “socket.io-redis-adapter의 비효율성을 개선해보자” 는 방향으로 접근해 보았으나, 결과적으로 socket.io 를 사용하는 인스턴스의 부하는 줄여주지만, redis의 부하는 오히려 늘어나게 되는 문제가 있었습니다.

2편에서는 단일 인스턴스로 병목이 되고 있는 redis pubsub 을 “scale out이 가능한 messaging system으로 대체해보자” 는 방향으로 접근해 보고자 합니다.

1. Pubsub messaging system

Enterprise 레벨의 요구사항에 따라, messaging system은 더 낮은 지연시간 동안, 더 많은 지점에, 더 많은 메세지를 전달할 수 있도록 개선되어야 했습니다. Publisher Subscriber (pubsub) pattern은 messaging system의 scale out을 용이하게 해주어 결과적으로 더 좋은 퍼포먼스를 낼 수 있는 구조로 많이 활용되고 있습니다. 다양한 Pubsub messaging system들 중에서도, 저희는 다음 조건을 만족하는 서비스를 찾고자 했습니다.

  • 메세지 처리량이 많아져도 pubsub messaging system이 병목이 되지 않도록 scale out이 가능한 구조

  • 메세지 처리에서 latency는 중요한 factor임, 최소한 redis pubsub 만큼의 latency는 나왔으면 좋겠음

  • 하루에도 traffic 변화량이 자주 급변함, auto-scaling 지원이 용이한 서비스이면 좋겠음

순서 보장, at-least-once delivery와 같은 조건들은 필요하지 않기 때문에, kafka 와 같은 클라이언트가 서버에서 polling 해오는 방식보다는 서버에서 클라이언트에게 발송하는 방식으로 어느정도 error tolerance를 허용하는 대신 빠른 latency를 얻을 수 있는 서비스가 더 적합하다고 판단했습니다. 이 조건에 맞춰 저희는 크게 (1) redis cluster, (2) nats.io 의 2가지 서비스를 비교 대상으로 선정 했습니다.

1-A. Redis Cluster

메세지 처리량이 점점 많아지게 되면, redis pubsub을 하나의 machine으로 수행할 경우 병목이 발생할 수 밖에 없습니다. 이를 위해 redis는 cluster mode에서 pubsub 기능도 지원하고 있습니다. 다만 redis pubsub의 경우 아래와 같은 문제로 scalable한 구조로 구성되어 있지 않습니다 [1]

Q. Do all nodes subscribe to traffic on other nodes to get notified of publish events that their clients might have interest in?

A. PubSub in Redis Cluster is very simple. Every PUBLISH to any Redis Cluster node gets copied to every other Redis Cluster node on the Redis Cluster inter-node cluster bus. So, you can SUBSCRIBE to a channel on any Redis Cluster node then PUBLISH to the same channel on any other Redis Cluster node and the PUBLISH will reach every client attached to the cluster. There currently isn’t any fancy logic to route message only to nodes with actual live subscribers, but it could be added eventually. Also see the tiny “Publish/Subscribe” section of http://redis.io/topics/cluster-spec

요약하자면, redis cluster의 한 노드에서 publish 가 일어나면, 다른 모든 노드들이 publish를 받게 되고, 결과적으로 모든 node들은 전체 cluster의 messaging throughput 만큼 부하를 받게 되는 것입니다. 이는 일반적으로 clustering을 통해 각 노드들이 받는 부하를 줄이고자 하는 목적에 반하는 다소 naive 한 설계에 가깝습니다.

Redis 커뮤니티에서도 이와 관련된 문제를 인지하고 있었고, 이는 redis v7 에서 shared pubsub implementation으로 추가될 예정입니다 [2](22.01 에 unstable한 버전으로 릴리즈됨). 다만 redis cluster의 새 feature는 22년 3월 기준 현재도 redis v7 이 stable release가 되지 않은 상태입니다. Messaging system을 고려하는 시점에는 해당 기능 차용이 어려울 것으로 판단해 redis cluster는 선택하지 않았습니다.

1-B. Nats.io

NATS 메세징은 network location과 관계없이 여러 application과 service를 묶어줄 수 있는 abstraction layer를 제공해줄 수 있는 pub/sub messaging 서비스입니다. 분산 시스템을 위해 설계된 기술, 분산 시스템에서 공통 패턴을 구동하는 메시지의 주소 지정, 검색 및 교환을 담당합니다. 마이크로 서비스의 구조를 표방하며, scale out 에 용이한 구조로 enterprise level의 messaging system의 부하를 최소화합니다.

NATS는 기본적으로 subscriber가 메세지가 전송되는 순간에 subject에 대해 듣고 있지 않거나 active 하지 않은 경우 메세지가 전송되지 않는 at most once delivery 를 제공합니다. 밴치마크 자료를 기반으로 했을 때, nats.io 는 기존의 pubsub messaging queue에 비해 크게 상회하는 throughput 수치를 보입니다[3].

또한 redis pubsub와는 다르게, nats 는 subscribe 하고 있는 subject를 CS-trie 자료 구조로 들고 있어, 매번 메세지에 대해 redis pubsub 처럼 모든 subscribe를 순회할 필요가 없습니다. 결과적으로 N개의 client, M개의 subscriber 에 대해 O(N+M)의 시간복잡도가 아닌 O(log M) 의 복잡도를 갖게 됩니다.

결과적으로 아직 redis adapter의 scability 문제가 해결되지 않은 점과, NATS의 performance가 기존 redis에 비해 훨씬 상회하는 점, 충분한 maintain이 유지될 것이라는 점을 근거로 저희는 nats.io 를 기반 messaging system으로 선택했습니다.

2. nats.io 기반의 socket.io adapter 구현 & 벤치마크

socket.io 공식 문서와 기반이 되는 adapter interface를 살펴보면 (https://github.com/socketio/socket.io-adapter), 구현해야 하는 기능은 다음 method 뿐이었습니다. 또한, 저희는 adapter의 부수적인 기능은 사용하고 있지 않아 기술을 검증하는 시점에는 다른 redis adapter의 method는 구현할 필요가 없었죠.

TypeScript

크게 구현에 많은 cost가 들지 않을 것이라 판단했고, 2주 동안 업무의 30% 정도 시간을 들여 adapter 구현을 완료하고, 동작 체크를 완료 했습니다.

메세지의 broadcast 하는 로직은 (1) 에서 시도한 방식과 같이 -* 로 같은 subject를 구독하는 것이 아닌, 각 room 별로 별도의 subject를 구독하는 방식으로 구현했습니다.

완성된 adapter를 기반으로 퍼포먼스도 측정해 보았는데요, nats messaging system의 퍼포먼스를 최대로 사용했을 때, 메세지 throughput을 최대 얼만큼 얻을 수 있는지를 측정했습니다. 아래 이미지와 같이 2개의 NATS 노드를 배치하고, 각 노드에 socket.io-nats-adapter 를 사용하는 nodeJS EC2 인스턴스를 각각 4개씩 연결해 모두 같은 socket.io room에 메세지를 publish 했습니다.

결과는 성공적 이었습니다, 최대로 nats 인스턴스를 CPU를 utilize 했을 때 각 인스턴스에서 46~55 k/s 의 메세지 throughput 을 측정할 수 있었습니다. 같은 방식으로 redis 와 8개의 instance를 연결해 수치를 비교해 보았는데, redis의 경우 초당 8k/s 정도가 최대 였습니다. nats.io 인스턴스 기반으로 구성했을 때 5.5~7배의 throughput 향상을 확인한 것이죠.

3. 추가 검증에서 발견한 문제점

“scale out이 가능한 messaging system으로 대체해보자” 는 방향으로 선정한 nats.io 는 scale out이 가능한 구조와 높은 메세지 throughput 수치로 저희가 해결하려는 문제를 완전히 해결해주는 듯 했습니다. 그러나 부하 테스트를 설계하면서 다른 고민이 생겼습니다.

위 그래프는 현재 채널톡에 신규로 발생하는 socket 생성 요청을 분 단위로 표시한 그래프입니다. 평균적으로 8~9만개의 socket이 새로 생겨나게 되는데요, 초 당 평균 1500개 정도의 socket 이 신규로 생긴다는 뜻입니다. 피크에는 12만~15만, 초당 2000개 이상의 socket이 신규로 생겨날 수 있습니다. 이렇게 신규로 socket 요청들이 발생하게 되면, messaging system에 초당 너무 많은 subject (redis pubsub의 channel) 들이 생겨나거나 삭제될 것입니다.

nats.io 의 각 인스턴스가 같은 subject 으로 이미 연결된 후에는 메세지 전송에 따른 overhead만 부담하게 되지만, 처음 subject 을 주고 받는 시점에는 각 nats 노드 들이 어떤 topic을 subscribe 하고 있는지 모르기 때문에 모든 nats 노드에게 pubsub을 전파해주어야 할 것입니다. 저희는 이 동작이 모든 node에 overhead를 발생시킬 것이라 보고, 이를 검증해보고자 했습니다.

추가 검증의 경우 docker 기반으로 측정 하였습니다, 2개의 client 를 띄워두고 NATS cluster 에 publish 하고 subscribe 하는 subject를 증가시켜보며 memory, cpu, network IO 등이 증가하는지를 측정했습니다.

여러 개의 nats 인스턴스를 docker 로 띄워두고, client가 초당 1k 개의 subject를 publish 하고 subscribe 하도록 했습니다. 그리고 2k, 3k로 늘려가며 memory, network IO, CPU 수치를 비교했습니다.

  • 아무것도 하고 있지 않을 때

  • 1k publish 할 때

  • 2k publish 할 때

  • 3k publish 할 때

memory util이 급격하게 올라가는 것을 확인할 수 있으며, 이외에도 테스트 중 일시적으로 subscribe 대상이 OOM 으로 connection을 유실하게 되면 일시적으로 memory, CPU util이 튀는 것도 확인할 수 있었습니다.

nats.io 의 경우에도 새로 pubsub 하는 subject 개수가 증가함에 따라 memory / CPU rate가 배수로 증가하는 것을 확인할 수 있습니다. 결과적으로는 cluster에서 pubsub 하는 갯수가 늘어나게 되면 부하를 가져오게 되고, cluster에서 처리하는 socket 이 많아질수록 nats를 scale up 해줘야 하는 상황이 될 것입니다.

4. 결론

pubsub의 메세징 처리량은 nats.io 를 통해 효율적으로 해결할 수 있었지만, 하나의 cluster에 대해 pubsub하는 topic의 갯수가 증가함에 따라 messaging system이 부하를 받게 됩니다.

Scale out되는 cluster의 경우 신규 생성/삭제 되는 socket을 cluster의 node 들은 다른 node가 어떤 topic을 듣고 있는지 모르기에 전파를 해줘야 하기 때문에 cluster에 접속하는 서버의 갯수가 많아질수록 nats.io 인스턴스를 scale up 해줘야 합니다.

결과적으로 단일 cluster 내에서 처리할 수 있는 메세지와 소켓 개수는 scale up이 되지 않는 이상 한계에 도달합니다. 더 scalable한 구조를 가져가기 위해, 저희는 cluster를 여러 개의 cluster로 나누어 소켓 서비스 인프라를 파티셔닝 하기로 했습니다.

소켓 서비스 cluster를 나누게 되면, 한 cluster에 속해 있는 node는 해당 cluster 내의 redis만 subscribe 할 수 있게 됩니다. 기존의 메인 서비스 로직에 영향을 받지 않고 소켓 서비스를 분할하기 위해, 저희는 채널(채널톡 서비스의 기업 workspace 단위) 을 기준으로 cluster를 분할 했습니다. 채널톡 서비스는 채널 간 소켓을 보내는 로직이 없기 때문에 (다른 기업 채널에 직접 메세지를 보낼 일은 없겠죠!) 채널 단위로 파티셔닝 했을 때 메인 로직이 영향을 받지 않게 됩니다.

이런 파티셔닝 로직과 관련해서는 3부에서 이어서 설명 드리도록 하겠습니다 😊

5. 별개의 성과

문제를 완벽히 해결하지는 못했지만, nats.io를 활용한 adapter가 개선한 부분도 있었습니다.

  • socket의 생성 / 삭제가 잦지 않은 환경에서는 socket.io-redis 에 비해 7~8배 월등한 성능

  • 파티셔닝의 경우 도메인 레벨로 잘 파티션을 나눌 수 있는 경우에만 적용이 가능함,

    nats.io

결과적으로 nats.io 자체를 도입하는 것이 저희에게도 도전적이고 오랜 검증이 들어갔고, 비슷한 문제를 겪는 다른 단체 / 개인도 같은 고민을 하고 있을 것이라 생각합니다. 저희가 그동안 채널톡 서비스를 운영하면서 오픈 소스에 많은 도움을 받은 만큼, 저희도 오픈소스에 역으로 작게나마 도움을 제공하고 싶은 마음을 갖고 있었고 지금까지 작업한 nats.io adapter는 외부에 공개해도 좋을 것 같다는 결론을 내렸습니다.

이후 검증 당시에는 구현하지 않았던 adapter의 전체 spec을 구현해 2월 까지 작업을 마무리 하였고, 3월 초 최종적으로 오픈소스를 socket.io 공식 adapter에 등재하였습니다.

https://github.com/distrue/socket.io-nats-adapter

여전히 코드에는 반영해야 할 것들이 많이 남아 있습니다. 이 부분들은 꾸준히 오픈소스를 maintain 하며 이어갈 예정이며, 금년 내에는 v1 릴리즈를 할 수 있기를 바랍니다 😇

References


마지막으로, 채널톡 백엔드 내의 코어팀의 white paper 중 일부를 공유하며 회사 홍보로 마칩니다.

채널톡 백엔드팀은 '가져다 쓰는 것' 으로 부족할때 새로운 무언가를 만드는 팀 입니다.

현대 개발에서 '잘 가져다 쓰는' 능력은 중요한 능력입니다. 적절히 맞는 프레임워크/라이브러리를 재빠르게 가져다 쓰는 것은 전반적인 개발 속도를 높이는 중요한 무기입니다. 저희팀도 많은 오픈 소스 등에 도움을 받았습니다.

하지만 세상에 존재하는 것이 저희에게 충분히 맞지 않거나 저희가 원하는 기대치에 만족되지 않으면 어떻게 할까요? 대부분 거기서 멈추고 다른 대안을 찾습니다. 일반적인 팀은 그렇습니다. 그것이 안전한 길이니까요. 하지만 세상을 바꾸는 놀라운 기술들은 항상 모험을 했습니다.

우리의 코어팀은 이런 정신으로 기술을 레벨업 하는 팀 입니다. 필요한 걸 만들어 쓰는 팀 입니다. 물론, 잘 고민 해야겠지요, 필요 없는걸 굳이 만들지는 맙시다. 이 둘을 구분하는것이 핵심입니다. 모두 성공 하겠냐고요? 아니요 대부분 실패할 것입니다. 필요 없는걸 만들기도 할것이고, 만들다 실패하기도 할 것입니다. 당연한 것입니다. 그럼에도 불구하고 도전하는 팀이 바로 코어 팀입니다. 왜냐면 이런 도전 없이는 기술을 레벨업 할 수 없으니까요.

이 도전적인 과제를 함께 해결하고 싶다면, 채널톡 백엔드 팀에 지원하세요!

We Make a Future Classic Product

채널팀과 함께 성장하고 싶은 분을 기다립니다

사이트에 무료로 채널톡을 붙이세요.

써보면서 이해하는게 가장 빠릅니다

회사 이메일을 입력해주세요