////
Search

12장 - 채팅 시스템 설계

생성일
2024/07/20 06:22
태그
Interview

1. 문제 이해 및 설계 범위 확정

앱은 그룹, 1:1 채팅을 지원해야 한다.
모바일과 웹을 지원해야 한다.
DAU는 5천만 명을 동시에 처리할 수 있을 수준이여야 한다.
1:1 채팅, 그룹채팅, 사용자 접속상태를 표시할 수 있어야하고, 텍스트만 지원한다.
메세지는 100,000자 이하로 지원한다.
채팅 이력은 영원히 보관해야 한다.

2. 개략적 설계안 제시 및 동의 구하기

채팅 서비스가 제공해야 하는 기능
클라이언트로부터 메시지 수신
메시지 수신자 결정 및 전달
수신자가 접속 상태가 아닌 경우 접속할 때까지 해당 메시지 보관
어떤 통신 프로토콜을 사용할 것인지에 대해서도 고민해야 한다.
HTTP를 사용할 경우 Keep-Alive 옵션을 사용하여 핸드셰이크 횟수를 줄일 수 있다.
HTTP는 클라이언트가 연결을 만드는 프로토콜이기에 임의 시점에 메세지를 보내는 데는 쉽게 쓰이기 어렵다.
때문에 폴링, 롱 폴링, 웹소켓 등의 기술들이 나왔다.

2.1. 폴링

클라이언트가 서버에 지속적으로 새로운 메세지가 있는지 확인하는 방법이다.
폴링을 자주할 수록 코스트가 올라가고, 서버 자원이 불필요하게 낭비된다.

2.2 롱 폴링

폴링과 비슷한데 서버측에서 새로운 메세지가 올때까지 커넥션을 유지하다가 타임아웃이 발생하면 다시 요청하는 방식이다.
서버 입장에서는 연결 해제를 알 수 있는 좋은 방법이 없다.
커넥션을 계속 유지해야 해서 비용이 증가하고 비효율적이다.

2.3. 웹소켓

웹소켓은 서버가 클라에게 비동기 메세지를 보낼 때 가장 널리 사용되는 기술이다.
웹소켓을 잘 이용하면 메시지를 보낼 때나 받을 때 동일한 프로토콜을 사용할 수 있으므로 설계뿐 아니라 구현도 단순하고 직관적이다.
단, 서버 측에서 연결 관리를 효율적으로 해야한다.

2.4. 개략적 설계안

주 통신 프로토콜로 웹소켓을 사용하겠다.

2.4.1. 무상태 서비스

무상태 서비스는 로드밸러서 뒤에 위치한다.
서비스탐색 (Service Discovery) = 클라이언트가 접속할 채팅 서버의 DNS 호스트명을 클라이언트이게 알려주는 역할을 한다.

2.4.2. 상태 유지 서비스

해당 설계에서 유일하게 상태 유지가 필요한건 채팅 서비스다.
각 클라이언트가 채팅 서버와 독립적인 네트워크 연결을 유지해야 하기 때문
클라는 보통 서버가 살아 있는 한 다른 서버로 연결을 변경하지 않는다.

2.4.3. 제3자 서비스 연동

채팅 앱에서 가장 중요한 서드파티는 푸시 알림이다.
새 메시지를 받았다면 설사 앱이 실행 중이지 않더라도 알림을 받아야 한다.

2.4.4. 규모 확장성

대량의 트래픽을 처리해야 하는 경우 따져봐야 하는건 서버 한 대로 얼마나 많은 접속을 동시처리 가능하냐다.
접속당 10K의 메모리가 필요하면, 동시 접속자 1M는 10GB의 메모리만 있으면 모든 연결을 다 처리할 수 있다.
하지만 한대의 서버만 있을 경우 SPOF문제를 가지게 된다.
서버 한대에서 점차 개선시켜 나가는것도 괜찮은데 면접관에게는 시작이라고 정확하게 전달하도록 하자.
채팅 서버는 클라이언트 사이에 메시지를 중계한다.
접속 상태 서버는 사용자의 접속 여부를 관리한다.
API 서버는 로그인, 회원가입, 프로파일 변경 등 그 외 나머지를 전부 처리한다.
알림 서버는 푸시 알림을 보낸다.
키-값 저장소에는 채팅 이력을 보관한다.

2.4.5. 저장소

데이터의 유형과 읽기/쓰기 연산의 패턴에 따라 저장소 종류는 선택한다.
사용자 프로파일, 설정, 친구 목록과 같은 일반적 데이터는 관계형 데이터베이스에 저장한다.
채팅 이력은 주로 최근에 주고받은 메시지 데이터만 보게되며, 1:1 채팅은 읽기와 쓰기 비율도 1:1 정도다.
본 설계안에서는 키-값 저장소를 추천한다.
키-값 저장소는 수평적 규모확장이 쉽다.
키-값 저장소는 데이터 접근 Latency가 낮다.
관계형 데이터베이스는 롱 테일에 해당하는 부분을 잘 처리하지 못하는 경향이 있다.
인덱스가 커지면 무작위적인 접근 처리 비용이 늘어난다.
페이스북이나 디스코드 같은 서비스에서 키-값 저장소를 채택하고 있다.

2.5. 데이터 모델

메세지 ID
message_id를 만드는 기법은 자세히 논의할 만한 가치가 있는 흥미로운 주제다.
메세지 id는 고유해야 하며 시간 순서로 정렬 가능해야 한다.

3. 상세 설계

3.1. 서비스 탐색

서비스 탐색은 클라이언트에게 가장 적합한 채팅 서버를 추천한다.
기준으로는 클라이언트의 위치와 서버의 용량 등이 있다.
가장 널리 쓰이는 오픈소스 솔루션으로는 아피치 주키퍼 같은 것이 있다.
서비스 탐색 기능의 동작 순서
1.
사용자가 A가 시스템에 로그인을 시도한다.
2.
로드 밸런서가 로그인 요청을 API 서버들 중 하나로 보낸다.
3.
API 서버가 인증을 처리하고 나면, 서비스 탐색 기능이 작동하여 해당 사용자를 서비스할 최적 채팅 서버로 보낸다.
4.
사용자 A는 반환된 채팅 서버와 웹 소켓 연결을 맺는다.

3.2. 메시지 흐름

3.2.1. 1:1 채팅 메시지 처리 흐름

1.
사용자 A가 채팅 서버 1로 메세지 전송
2.
채팅 서버 1은 ID 생성기로 해당 메세지 ID 결정
3.
채팅 서버 1은 해당 메세지를 메세지 동기화 큐로 전송
4.
메세지가 키-값 저장소에 보관됨
5.
사용자 B가 접속 중인 경우 B가 접속 중인 채팅 서버로 전송된다.
a.
접속 중이 아니라면 푸시 알람을 보낸다.
6.
채팅 서버 2는 사용자 B에게 메세지를 전송
a.
사용자 B와 채팅 서버 2사이에는 웹 소켓으로 연결되있기 때문에 이를 이용

3.2.2. 여러 단말 사이의 메시지 동기화

사용자 A는 전화기와 랩톱의 두 대 단말을 이용하고 있다.
각 단말은 cur_max_message_id라는 변수를 유지하는데, 해당 단말에서 관측된 가장 최신 메세지 ID를 추적하는 용도다.
두 조건을 만족한다면 메세지는 새 메세지로 간주한다.
수신자 ID가 현재 로그인한 사용자 ID랑 같다.
키-값 저장소에 보관된 메세지로서, 그 ID가 cur_max_message_id보다 크다.

3.2.3. 소규모 그룹 채팅에서의 메시지 흐름

사용자가 3명인 소규모 그룹이 있다고 상정해보자
A가 메시지를 보내면 B와 C의 메시지 동기화 큐에 복제된다.
각 큐는 메시지 수신함 같은 것으로 생각해도 무관하다.
이 설계안은 소규모 그룹 채팅에 적합하다.
새로운 메세지가 왔는지 확인하려면 본인 큐만 보면 되니까 메세지 동기화가 단순해진다.
그룹이 크지 않다면 메세지를 수신자별로 복사하여 큐에 넣는 작업 비용이 문제가 되지 않는다.
하지만 많은 사용자를 지원해야 하는 경우라면 모든 사용자의 큐에 복사하는게 바람직하지 않다.
메세지 동기화는 여러 사용자로부터 오는 메세지를 받을 수 있어야 한다.

3.3. 접속상태 표시

3.3.1. 사용자 로그인

클라이언트와 실시간 서비스 사이에 웹소켓 연결이 맺어지고 나면 접속상태 서버는 사용자 상태와 last_active_at 타임스탬프 값을 키-값 저장소에 보관하여 이를 토대로 접속 중으로 표시한다.

3.3.2. 로그아웃

키-값 저장소에 보관된 사용자 상태가 online에서 offline으로 바뀌게 된다는 점에 유의하자.

3.3.3. 접속 장애

본 설계안에선 박동 검사를 통해 인터넷 연결 문제를 해결한다.
온라인 상태의 클라이언트로 하여금 주기적으로 박동 이벤트를 접속상태 서버로 보내도록 하고
마지막 이벤트를 받은 지 x초가 지나면 오프라인으로 바꾸는 것이다.

3.3.4. 상태 정보의 전송

상태정보 서버는 발행-구독 모델을 사용해 친구 사용자의 상태가 변경될 경우 이 사실을 연관 채널에 알린다.
이 방은 그룹 크기가 작을 때 효과적이다.
그룹이 커지면 비용과 시간이 많이 들어 좋지않다.
너무 많은 메시지가 발생한다면 채팅 방에 입장하는 순간에만 읽거나, 수동으로 갱신하도록 한다.