개발자의 오르막
대규모 채팅 아키텍처는 C10K 문제를 어떻게 해결할까? 본문
Overview
대용량 채팅은 어떤 것을 의미할까요? 우리가 흔히 쓰는 카카오톡, 라인, 당근과 같이 개인과 개인 사이의 채팅이 아닌, 특정 목적을 위해 모인 불특정 다수가 함께 채팅하는 것을 의미합니다. 유튜브, OTT 의 인기와 더불어 유튜브 Live, 트위치, 라인의 Live, 아프리카 TV 와 같이 실시간 라이브방송으로 고객들과 소통하는 쌍방향 커뮤니케이션 서비스가 흥행하고 있습니다.
실시간 라이브 방송으로 인한 대규모 채팅, 수천 또는 수백만 명의 동시접속자가 이루어질 수 있는 환경 이기도 합니다.
때문에 우리는 흔히 C10K 문제라고 하는 1만개의 클라이언트를 동시에 처리할 수 있는 네트워크 I/O 모델 설계 방법 에 대해 고민을 안 할 수 가 없습니다. 특히 라이브 방송을 쇼핑몰로 진행할 때에는 고객사의 매출에 직접적인 영향을 미칠 수 있어, 예민한 도메인이기도 합니다.
C10K (Concurrently handling 10,000 connections) 의 대표적 해결방법
C10K 문제의 대표적인 해결방법은 4가지 정도로 예시를 들 수 있습니다.
- 이벤트 드리븐 아키텍처 (Event-driven architecture) : 이벤트를 발생시키고, 이를 비동기적으로 처리함으로써 성능을 개선합니다.
- 멀티스레딩(Multithreading) : 하나의 프로세스 내에서 여러 개의 쓰레드를 생성하여, 동시에 여러 연결을 처리할 수 있도록 합니다.
- 멀티 프로세싱(Multiprocessing) : 하나의 서버를 여러 개의 프로세스로 분할하여, 각각의 프로세스가 병렬적으로 연결을 처리하도록 합니다.
- 비동기 I/O (Asynchronous I/O) : 입출력 작업을 블로킹하지 않고, 비동기적으로 처리하도록 합니다.
크게 4가지의 대표적인 방법이 있지만, 우리가 주목해야할 점은 동기/비동기와 단일/멀티 라는 단어입니다.
- 하나의 요청이 작업을 모두 끝낼 때까지 다른 작업들이 모두 대기되는 상태(블로킹) 인 동기적 방식에서 하나의 요청의 작업이 끝나지 않더라도 다른 작업이 실행될 수 있는 비동기적 방식으로 작업을 수행하는 것
- 하나의 서버에서 여러 개의 스레드로 분할하여 작업을 각각의 프로세스가 병렬적으로 수행할 수 있는 것
이 두가지 주안점을 두고, 채팅 아키텍처에서는 어떻게 이 C10K 문제를 해결했는지, 당근마켓 아키텍처와 Line 의 Live Chatting 아키텍처를 비교해가며 알아보도록 하겠습니다.
당근마켓과 라인 Live 의 아키텍처를 통해 알아보기
먼저 당근마켓과 라인 Live 의 아키텍처를 궁금해하실 것 같아 먼저 두 아키텍처를 제시해보도록 하겠습니다.
당근마켓의 Chatting-Server 아키텍처
당근마켓의 Chatting-Server 아키텍처입니다.
2021년 9월 기준 1,700만명에 달하는 회원 수에 육박하는 당근마켓은 개인과 개인 간의 채팅 서비스를 주로 하고 있습니다.
위의 아키텍처로 개선 전 당근마켓은 REST API 를 통해 메시지를 풀링하는 방식이었습니다. 또한 기존에는 FCM 에 의존하고 있어, Push 메시지가 밀리면 유저가 이미 접속하고 있더라도 채팅 메시지를 받지 못하는 구조였습니다.
이를 당근마켓은 HTTP 프로토콜 기반인 REST API 방식에서 WebSocket 네트워크 프로토콜로 변경하여 채팅 메시지의 송수신 작업과 FCM 으로 알림을 보내는 작업을 비동기로 방식으로 변경하는데 성공하였습니다.
Question!!
- REST API 를 통해 메시지를 폴링하는 방식
- FCM 에 의존하고 있어 Push 메시지가 밀리면 유저가 채팅 메시지를 받지 못하는 구조
- WebSocket 프로토콜로 변경하면 어떻게 FCM 의 상태와 무관하게 비동기적으로 메시지를 처리할 수 있는지
동기방식인 HTTP 프로토콜 vs 비동기 방식인 WebSocket 프로토콜
위의 당근마켓의 개선안을 이해하려면, 우리는 먼저 HTTP 와 WebSocket 의 동작방식에 대해 알 필요가 있습니다.
HTTP 는 클라이언트와 서버 사이에 요청(Request)과 응답(Response) 의 상호작용을 기본적으로 사용하는 프로토콜입니다.
우리가 흔히 들어본 HTTP 의 3-way handshake 과정이 적용되고 있는 것입니다.
간략하게 3-way handshake 의 동작과정을 설명한다면
- 클라이언트가 서버에게 SYN 패킷을 보냅니다.
- 서버는 클라이언트로부터 SYN 패킷을 받으면, 클라이언트로 SYN-ACK 패킷을 보냅니다.
- 클라이언트는 서버로부터 SYN-ACK 을 받으면, ACK 패킷을 보내서 서버와의 연결을 확립합니다.
으로 정의될 수 있습니다.
Client 에서 Requset 를 Server 에게 보내고, Server 는 Request 를 정상적으로 받았다는 수신 Response 를 보내며, 이를 Client 가 다시 정상적으로 받았을 때 Connection 을 확립하는 것으로 얘기할 수 있습니다. 맺어진 Connection 을 통해 데이터를 주고 받는 것입니다.
HTTP 는 클라이언트가 요청을 보내면, 서버가 응답을 보내는 식으로 이루어지기 때문에 주로 동기(Synchronous) 방식으로 작동하게 됩니다.
다시 돌아와서, 그럼 REST API 를 통해 메시지를 폴링한다는 방식은 무엇일까요?
Polling 방식
Polling 방식은 클라이언트가 사용 가능한 메시지가 있는지 주기적으로 서버에게 묻는 기술입니다.
고객에게 새로운 채팅 메시지가 왔는지 계속해서 서버에게 물어보는 방식이죠. 대부분의 대답이 No 이기 때문에 우리는 전달 받을 채팅 메시지가 없는데도 불구하고 계속해서 네트워크 비용을 소모해야 하는 상황이 발생하게 됩니다.
따라서 REST API 를 통해 메시지를 폴링한다는 의미는, 새로운 메시지가 왔는지 FCM (Firebase Cloud Messaging) 을 통해 계속해서 조회한다는 것을 의미합니다.
그러나 FCM 은 Push 알림으로도 많이 사용되기 때문에 Push 작업이 밀리게 되면, 채팅 메시지를 조회를 요청했던 Request 는 하염없이 Response 를 기다리는 현상이 발생하게 된 것입니다.
그럼 WebSocket 은 HTTP와 무엇이 다르길래 비동기방식으로 처리 가능했을까요?
WebSocket
웹소켓 (WebSocket) 은 서버가 클라이언트에게 비동기 메시지를 보낼 때 널리 사용하는 기술입니다.
최초 클라이언트와 서버가 웹소켓을 연결할 때에는 HTTP 연결로 이루어지지만, 연결이 이루어지면 연결된 Connection 은 연결을 계속 유지하며 양방향적인 성격을 가지고 있습니다.
즉, 데이터를 주고 받을 수 있는 네트워크 통신망을 연결한 셈이죠.
클라이언트는 서버 간에 WebSocket 연결이 설정되면, 이벤트 리스너를 등록하여 서버로부터 메시지를 비동기적으로 수신합니다. 이벤트 리스너는 해당 이벤트가 발생했을 때 실행될 콜백함수를 등록하여 이벤트에 대한 처리를 하는 역할을 합니다.
이로인해 클라이언트는 메시지가 오기 전까지 계속해서 대기를 하지 않아도 되기 때문에, 채팅 메시지에 대한 작업은 비동기적으로 수행될 수 있었습니다.
또한 WebSocket 커넥션은 websocket 요청이 들어올 때마다 하위 쓰레드로 커넥션을 관리하기 때문에, 각 client 별 websocket 에 대한 connection 을 맺을 수 있어 동시에 여러 연결을 처리할 수 있었습니다.
Line Live 의 Chatting-Server 아키텍처
Line Live 의 Chatting-Server 아키텍처에서 중점적으로 다뤄보고 싶은 내용은 바로 Redis 의 Pub/Sub 구조입니다.
사용자 A가 채팅 메시지를 보내면, 채팅 메시지를 받는 사용자들에게 메시지가 즉시 전달되는 것이 아니라 Redis 서버의 내부 처리 큐에 등록하여 순차적으로 처리되는 구조입니다.
우리는 채팅 메시지가 수신자 모두에게 전달되기까지 기다릴 필요 없이 Redis 내부 큐에 등록되기만 하면 작업이 완료되는 것입니다.
Redis 의 Pub/Sub 구조를 통해 Chatting-Server 간 메시지 전송도 비동기적으로 처리하여 대량의 메시지를 처리하는 높은 성능을 확보하게 되었습니다.
이러한 Redis 의 Pub/Sub 구조의 장단점은 다음과 같습니다.
장점
- 높은 성능
- Redis Pub/Sub 은 대량의 메시지를 처리할 수 있는 높은 성능을 가지고 있습니다.
- 메시지 처리가 비동기 방식으로 이루어지기 때문에 대규모 메시지 전송에도 빠르고, 안정적으로 처리할 수 있습니다.
- 확장성
- Redis 의 Pub/Sub 은 여러 개의 서버로 확장하여 클러스터링을 구성할 수 있습니다.
- 대규모 서비스에서도 높은 확장성을 보장할 수 있습니다.
- 실시간성
- Redis Pub/Sub 은 메시지를 즉시 처리하여 실시간성을 보장할 수 있습니다.
단점
- 지속성
- Redis Pub/Sub 은 메시지 전송 후 삭제되기 때문에 메시지의 지속성을 보장하지 않습니다.
- 최대 1회 전송 (at-most-once) 패턴, 메시지가 유실될 수 있으나 최대 1회만 전송한다.
- 대규모 요청에 따른 성능 저하
- Redis Pub/Sub 은 Push 방식으로 Subscribe 에게 메시지를 밀어주기 때문에 Subscribe 가 많을 수록 성능이 저하될 수 있습니다.
Line Live Chatting-Server 는 Redis Pub/Sub 구조로 인해 메시지 유실 우려에 대해서는 MySQL 에 주기적으로 마이그레이션 하는 방법을 통해 극복을 하였습니다. 현재 네이버 Line Live Chatting Server 는 100대 이상의 인스턴스에서 동작하며 성능과 안정성을 증명해내고 있습니다.
Pub/Sub 구조라면 Kafka ??
대규모 채팅 데이터 처리라고 하면 빼놓을 수 없는 키워드 중 하나가 바로 Kafka 입니다.
일단, Kafka 에 대해 간략하게 설명한다면 Kafka 는 Source Application 과 Target Application 의 결합도를 낮추기 위한 메시지큐라고 볼 수 있습니다.
서버 간 데이터 통신을 메시지큐로 처리하여 비동기방식으로 이루어낸 것을 말합니다.
Kafka 의 구성요소는 다음과 같습니다.
- Kafka Broker : Kafka Broker 는 카프카가 설치되어 있는 서버 단위를 말합니다.
- Topic : 카프카에서 데이터가 들어갈 수 있는 공간으로 큐의 역할을 합니다.
- Partition : Partition 은 Topic 하나 당 Partition 을 여러개 보유할 수 있으며, 데이터 대기열을 물리적으로 분산된 것을 말합니다.
- Producer : 메시지를 발행하는 주체입니다.
- Consumer : 메시지를 구독하는 주체입니다.
먼저 Kafka 를 사용한다면 chatting-server architecture 가 어떤식으로 구성되는지 아래 예시를 통해 들어보겠습니다.
- REST API 로 채팅방이 개설 요청을 하게 되면 Chatting-Server 는 Kafka 에 채팅방에 대한 Topic 을 생성합니다.
- Client 는 Chatting-Server 와 WebSocket 방식으로 커넥션을 가져갑니다.
- Client 는 WebSocket 으로 접속할 때 Request 로 던지는 채팅방_아이디를 기준으로 해당 Topic 을 Subscribe 합니다.
- 메시지가 들어올 때마다 채팅방 Topic 을 Subsribe 하는 서버는 Client 에게 메시지를 전달합니다.
우선 동작하는 건 알겠는데, Kafka 가 채팅 메시지 처리를 하는 것에 있어 장점이 있을까요?
Kafka 는 아래 3가지 장점을 가지고 있습니다.
- High throughput message capacity
- Kafka 는 고 가용성으로, Producer (Websocket을 받는 Chatting-Server) 가 메시지를 발행하는 순간 Partition 에 메시지를 보관합니다.
- 분산처리가 가능하기 때문에 짧은 시간 내에 엄청나게 많은 데이터를 Consumer 에게 전달할 수 있습니다.
- 짧은 시간 내에 많은 데이터를 Consumer 에게 전달할 수 있습니다.
- Scalability 와 Fault tolerant
- 카프카는 확장성이 뛰어나므로, 브로커 중 몇대가 죽더라도 Replica 로 복제된 데이터로 안전하게 보관할 수 있습니다.
- 가령, Websocket 으로 연결된 채팅 서버 중 1대가 죽거나 Restart 가 된다 하더라도, 해당 채팅메시지는 Kafka 의 Topic 에 보관되어 있기 때문에 재시작 후 다시 채팅 메시지를 받을 수 있습니다.
- 기존, 연결이 끊어졌을 때 채팅메시지를 못 받았던 부분을 kafka 를 이용한다면 client 에서 다시 Reconnect 하는 것만으로 이전 채팅 메시지들을 다시 받을 수 있습니다.
- Undeleted log
- 컨슈머의 그룹 아이디만 다르게 하면 동일한 데이터도 각각 다르게 처리 가능합니다.
- 동일한 데이터를 채팅서버 메시지 전달용, 통계용, 등등 다양한 데이터파이프 라인으로 활용할 수 있습니다.
지금까지 대규모 채팅 아키텍처에서는 C10K 문제를 어떻게 해결할까에 대한 주제로 글을 적어봤습니다. 아직 부족한 게 많아 내용이 부실하거나 잘못된 부분이 있으면 가감없이 알려주시면 감사하겠습니다.
Reference
'Architecture' 카테고리의 다른 글
[Udemy] Apache Kafka 시리즈 - 초보자를 위한 아파치 카프카 강의 v3 (0) | 2024.05.08 |
---|---|
코드스멜을 피하는 방법 Sonarqube (0) | 2023.07.26 |
분산트랜스코딩으로 알아보는 Boss-Worker 아키텍처 (0) | 2023.02.19 |