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

개선을 위해 소켓 서버 인프라를 어떤식으로 구성했는가

Hammer • Backend Engineer

  • 테크 인사이트

개요

이번 포스트에서는 최종적으로 저희가 개선을 위해, 소켓 서버 인프라를 어떤식으로 구성했는지를 이야기 해보려합니다. 앞선 두 편의 포스트에선 실험을 진행했던 socket.io-redis-adpater 개선 및 pub/sub messaging system 도입 등을 이야기했고, 그 한계 역시 짚어보았습니다.

결론적으로, 2편의 말미에선 레디스 인스턴스 하나로 묶이는 소켓 인프라를 클러스터 단위로 분할했다고 언급했는데요, 오늘은 변경된 소켓 서버 인프라에 대한 개괄과 선택한 이유, 그로 인한 결과를 알아보겠습니다.

문제점

우선, 지난번 문제점에 대해서 다시 짚어보죠. 소켓 서버 인프라에서 Scalable 한 구조가 반드시 필요했습니다. socket.IO 서버 인스턴스는 쉽게 늘릴 수 있지만, 이를 중계하는 Redis 머신은 불가능했습니다. 하나의 레디스 인스턴스가 중간에서 pub/sub 을 이용해 여러 socket.IO 서버를 통신시켜주는 것은 부담이 컸습니다.

물론, 레디스 인스턴스에 대해서 Scale Up 할 수 있는 옵션은 그 당시에도 충분했지만, Scale in/out 에 대해서 대책이 필요했죠. 채널톡은 현재도 꾸준히 트래픽이 증가하고 있으니까요.

그림 1. 파티셔닝 전 소켓 서버 구조

파티셔닝

파티셔닝은 다수의 컴퓨터 파워와 CPU 코어, 네트워크 대역폭 등을 효율적으로 활용해 성능을 확장할 수 있습니다. 저희는 소켓 인프라 역시 이에 대상이 될 수 있다고 생각했죠. 특히, socket.IO 에서 제공하는 namespaceroom 이 논리적인 그룹으로 묶여 필요한 메세지만을 수신하는 형태이기에[1] 파티셔닝을 통한 논리적인 분할 역시 가능했습니다.

파티셔닝된 소켓 서버 인프라는 어떤 형태일까요? 결과적으론 아래 그림 2와 같지만, 이를 이해하기 위해선 부연설명이 필요합니다. 추가적인 채널톡의 인프라를 소개하겠습니다.

그림 2. 파티셔닝된 소켓 서버 구조

ch-gateway

그림 중앙에 보이는 서비스인 ch-gateway 는 내부에서 Healthcheck 와 함께 Service Discovery 를 제공하는 프로젝트입니다. 이전부터 채널톡 API 서버들은 이를 이용해 내부 인프라에서 사용하는 MSA 를 적절히 호출할 수 있었죠.

여기서 ch-gateway 에게 질의 시 가야할 서비스를 분할하는 기준(파티션 키)은 채널입니다. 채널이란, 채널톡 서비스를 제공하는 기업의 workspace 단위입니다. 특정 기업 AB 가 굳이 서로 정보를 공유할 필요 없으며 공유하면 안 되죠. 실제 메세지, 등록된 사용자 정보 등은 채널 단위로 나뉠 수 있는 정보이기에 ch-gateway 는 서비스 디스커버리 역할을 효과적으로 제공하고 있습니다.

자, 그렇다면 그림을 상세히 들여다 봅시다. 채널 단위로 분할된 소켓 서버 인프라라고 해서 크게 달라진 것은 없이 각 소켓 클러스터 내부에서는 하나의 Redis 머신이 여러 socket.IO 인스턴스와 연결되어있는 것은 똑같습니다. 특정 채널 기준으로 API 호출은 ch-gateway 가 적절한 인스턴스를 찾아 제공하고, 이로 인해 단일 Redis 머신으로 인한 부하를 줄일 수 있게되었죠.

Metrics

아래는 현재 약 3개월 동안 분산된 Redis 머신 중 하나의 CPUUtilizationEngineCPUUtilization 당연하게도 파티셔닝을 통한 부하 분산 덕분에 현저히 낮아졌고, 예측 가능한 수치로 관리되고 있습니다.

그림 5, EngineCPUUtilization(좌) CPUUtilization(우)

남은 도전

socket.io

파티셔닝은 위와 같이 안전한 메트릭을 제공하도록 했지만, 아직 개선 과제가 존재합니다.

위 그림에서도 알 수 있듯이 socket.io 서버 인스턴스는 여러대가 Redis 를 이용해 정보를 주고받는 형태입니다. 수평적 확장이 가능함에도 소켓 인스턴스가 늘어나면 Redis 입장에선 subsriber 가 늘어 레디스 부하가 여전히 늘어날 수 있는 상황입니다. [2]

이를 관리하기 위해서는 하나의 소켓 서버가 견딜 수 있는 부하를 늘리는 것이 효과적인데요, 내부 부하 테스트 및 튜닝을 진행한 결과 Node.js 런타임에서는 한계가 있어 다른 런타임에서 소켓 인스턴스를 돌리는 것도 좋은 옵션으로 보고 있습니다.

이미 채널톡 내에서는 golang, java 등을 여러 런타임을 고려 중인데요, 이를 소개하는 글로도 찾아뵐 수 있을 것 같습니다.

Redis ShardedPubSub

추가로, Redis 7.x 에서는 이전에 저희가 필요했던 ShardedPubSub[3] 을 지원하기 시작했습니다. 현재 이 글을 작성하는 날짜 기준으론 AWS ElastiCacheRedis 7.x 버전을 지원하진 않아 기다릴 필요가 있지만, 클러스터 구성도 충분히 고려 가능합니다.

끝으로

채널톡은 계속해서 성장하고 있고 증가하는 트래픽을 적절히 관리하기 위해 실험을 진행합니다. 상황에 맞는 인프라를 선택하기에 급급하지 않으며 확장성과 고장감내, 서비스 분할 등 여러 방면에서 고민합니다.

이번 소켓 서버 관련 작업은 어떤 팀에게는 당연하거나, 더 쉬운 방식으로 풀 수 있는 문제일 수 있습니다. 하지만, 팀 내에서는 적절한 trade off 였고, 저희 채널팀은 매번 이러한 상황에서 추후 확장성까지 도모합니다. 모든 엔지니어링 문제는 상황에 맞는 선택의 연속이기에 저희의 이러한 여정이 인프라 문제 해결에 도움이될 수 있기를 바랍니다. 감사합니다.

References

[1] https://socket.io/docs/v4/namespaces/

[2] https://channel.io/ko/blog/real-time-chat-server-1-redis-pub-sub

[3] https://redis.io/docs/manual/pubsub/#sharded-pubsub

We Make a Future Classic Product

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

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

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

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