ch12 채팅 시스템 설계
채팅 시스템 설계
채팅 앱이라고 했을 때 사람들이 서로 떠올리는 것은 제각각일 수 있다. 면접관이 일대일 채팅이라고 생각하는데 그룹 채팅 앱을 설계해버리면 곤란할 수 있다.
1단계 문제 이해 및 설계 범위 확정
- 응답지연이 낮은 일대일 채팅 기능
- 최대 100명까지 참여할 수 있는 그룹 채팅 기능
- 사용자의 접속상태 표시 기능
- 다양한 단말 지원. 하나의 계정으로 여러 단말에 동시 접속 지원
- 푸시 알림
5천만 DAU를 처리할 수 있도록
2단계 개략적 설계안 제시 및 동의 구하기
해당 채팅 서비스는 아래 기능을 제공해야 한다.
- 클라이언트 메시지 수신
- 메시지 수신자 결정 및 전달
- 수신자가 접속 상태가 아닌경우 접속할 때까지 메시지 보관
채팅 서비스는 어떤 통신 프로토콜을 사용할 것인지가 중요한 문제
메시지 송신 클라이언트(sender) : 클라이언트/서버 애플리케이션에 요청을 보내는 클라이언트
HTTP 프로토콜을 사용
-
채팅 서비스와의 접속에는 keep-alive 헤더를 사용하면 효율적
- 클라이언트와 서버 사이의 연결을 유지할 수 있음
- TCP 접속 과정에서 발생하는 핸드셰이크 횟수를 줄일 수 있음
-
HTTP는 메시지 전송 용도로는 괜찮은 선택
- 페이스북 같은 대중적인 채팅 서비스가 초기에 HTTP를 사용했음
HTTP는 클라이언트가 연결을 만드는 프로토콜 서버에서 임의 시점에 클라이언트에게 메시지를 보내는데 쓰일 수 없다.
그렇다면, 서버가 연결을 만드는 것처럼 동작할 수 있도록 하기 위해 기법들은 어떤 것들이 있을까?
폴링
- 클라이언트가 주기적으로 서버에게 새 메시지가 있는지 물어보는 방법
- 폴링을 자주하면 할수록 비용 증가
- 답해줄 메시지가 없는 경우, 서버 자원이 낭비
이러한 비효율적인 부분들에 의해 롱 폴링이 나옴
롱 폴링
클라이언트는 새 메시지가 반환되거나 타임아웃 될 때까지 연결을 유지 새 메시지를 받으면 연결을 종료 서버에 새로운 요청을 보내어 모든 절차를 다시 시작
약점
- 전송 클라이언트와 수신 클라이언트가 같은 채팅 서버에 접속되지 않을 수도 있다. HTTP 서버들은 무상태서버가 대부분이기에 로드밸런싱을 위해 라운드 로빈 알고리즘을 사용하는 경우, 메시지를 받은 서버는 해당 메시지를 수신할 클라이언트와의 롱 폴링 연결을 가지고 있지 않은 서버일 수 있는 것
- 서버 입장에서는 클라이언트가 연결을 해제했는지 알 방법이 없다.
라운드로빈,,
웹소켓
서버가 클라이언트에게 비동기 메시지를 보낼때 널리 사용되는 기술
- 웹소켓 연결은 클라이언트가 시작
- 한번 맺어진 연결은 항구적, 양방향
- 처음에는 HTTP 연결이지만 특정 핸드셰이크 절차를 거쳐 웹소켓 연결로 업그레이드
- 연결이 만들어지고 나면 서버는 클라이언트에게 비동기적으로 메시지를 전송할 수 있음
- 방화벽이 있는 환경에서도 잘 동작함
- 80, 443(HTTP, HTTPS) 프로토콜이 사용하는 기본 포트번호를 그대로 쓰기 때문
어떻게 웹소켓이 메시지 전송이나 수신에 쓰일 수 있는가?
유의할 점 : 웹소켓 연결은 항구적으로 유지되어야 하기 때문에 서버 측 연결관리를 효율적으로 해야함
개략적 설계안
무상태 서비스
- 로드밸런서가 요청을 전달
- 요청을 경로에 맞는 서비스로 정확하게 전달
상태 유지 서비스
채팅 서비스는 상태유지가 필요한 서비스 각 클라이언트가 채팅 서버와 독립적인 네트워크 연결을 유지
제3자 서비스 연동
- 채팅 앱에서 중요한 제3자 서비스는 푸시알림
- 새 메시지를 받았다면 앱이 실행중이지 않아도 알림을 받아야 해서
- 푸시 알림 서비스와의 통합이 중요
규모 확장성
트래픽 규모가 얼마 되지 않을 때는 방금 설명한 모든 기능을 서버 한 대로 구현할 수 있음
대량의 트래픽을 처리해야 하는 경우에도 모든 사용자 연결을 클라우드 서버 한 대로 처리할 수 있기는 함
서버 한 대로 얼마나 많은 접속을 동시에 허용할 수 있느냐
동시 접속자가 1M이라고 가정할 때, 접속당 10K의 서버 메모리가 필요하다고 본다면 10GB 메모리만 있으면 모든 연결을 다 처리할 수 있을 것이다.
하지만 모든 것을 서버 한 대에 담은 설계안을 내밀면, 면접에서 좋은 점수를 따기 어려울 것
누구도 그정도의 트래픽을 서버 한 대로 처리하려 하지 않을것이기 때문
SPOF도 이유중 하나
- 채팅 서버는 클라이언트 사이에 메시지를 중계
- 접속상태 서버는 사용자의 접속 여부를 관리
- API 서버는 로그인, 회원가입, 프로파일 변경 등 나머지를 처리
- 알림 서버는 푸시 알림
- key-value store는 채팅 이력을 보관
- 이전 채팅 이력
저장소
데이터 계층
- 어떤 데이터베이스를 쓰느냐
- 관계형 데이터베이스? NoSQL?
- 따져야할것: 데이터 유형과 읽기/쓰기 연산의 패턴
채팅 시스템이 다루는 데이터
- 사용자 프로파일, 설정, 친구 목록
- 안정성을 보장하는 관계형 데이터베이스
- 가용성, 규모확장성 -> 다중화 , 샤딩
- 채팅 이력
- 어떻게 보관할지 결정하려면 읽기/쓰기 연산 패턴을 이해해야함
읽기 / 쓰기 연산 패턴
-
채팅 이력 데이터 양은 엄청남 페이스북 메신저, 왓츠앱 은 매일 600억개..
-
자주 사용되는 데이터는 주로 최근에 주고받은 메시지
-
대부분 사용자는 오래된 메시지를 들여다보지 않음
-
검색 기능, 특정 사용자가 언급된 메시지, 특정 메시지로 점프 -> 무작위적인 데이터 접근이 가능해야함
-
1:1 채팅 앱의 경우 읽기:쓰기 비율은 대략 1:1임
키-값 저장소를 선택하게된 이유
- 수평적 규모확장이 쉬움
- 데이터 접근 지연시간이 낮음
- RDB는 롱테일 데이터를 처리를 잘 못함. 인덱스가 커지면 무작위 접근 비용이 늘어남
페이스북 메신저(HBase), 디스코드(Cassandra)
데이터 모델
메시지 데이터를 어떻게 보관할 것인지
1:1 채팅을 위한 메시지 테이블
그룹 채팅을 위한 메시지 테이블
channel_id 와 message_id 는 복합키
channel_id는 파티션 키로도 사용
메시지 ID
message_id를 어떻게 둘까?
- message_id의 값은 고유해야 한다(unique)
- ID값은 정렬 가능해야함, 시간순서와 일치해야함, 새로운 ID는 이전 ID보다 큰 값이어야 한다
RDBMS라면 auto_increment 가 대안 NoSQL은 이러한 기능을 제공하지 않음
스노플레이크와 같은 전역적 64-bit 순서 번호 생성기
지역적 순서 번호 생성기
3단계 상세설계
컴포넌트 가운데 몇 가지를 골라 자세히 들여다 보는 면접 상황이다.
채팅 시스템은
- 서비스 탐색
- 메시지 전달 흐름
- 사용자 접속 상태 표시
서비스 탐색
주키퍼로 서비스 탐색 기능예시
- 사용자 A가 시스템에 로그인을 시도
- 로드밸런서가 로그인 요청을 API 서버들 가운데 하나로 보냄
- API 서버가 사용자 인증을 처리, 서비스 탐색 기능 동작 -> 해당 사용자를 서비스할 최적의 채팅 서버 찾기
- 사용자 A는 채팅 서버 2와 웹소켓 연결
메시지 흐름
1:1 채팅 메시지 처리 흐름
사용자 A가 B에게 보낸 메시지를 어떤 경로로 처리하는지
- 사용자 A가 채팅 서버 1로 메시지 전송
- 채팅 서버 1은 ID 생성기를 사용해 해당 메시지의 ID 결정
- 채팅 서버 1은 해당 메시지를 메시지 동기화 큐로 전송
- 메시지가 키-값 저장소에 보관됨
- (a) 사용자 B가 접속 중인 경우 메시지는 사용자 B가 접속 중인 채팅 서버
- 채팅 서버 2는 메시지를 사용자 B에게 전송, 사용자 B와 채팅 서버 2 사이에는 웹소켓 연결이 있는 상태
여러 단말 사이의 메시지 동기화
여러 개 단말을 사용하는 사람이 많을 수 있다.
사용자 A와 Chat server 1 사이에 웹소켓 연결이 만들어져 있음 랩톤에서 별도 웹소켓이 채팅 서버 1에 연결되어 있음
cur_max_message_id 는 각 단말이 최신 메시지 ID를 추적하는 용도
id를 통해서 키-값 저장소에서 새 메시지를 가져옴 (동기화 작업)
소규모 그룹 채팅에서의 메시지 흐름
큐는 메시지 수신함
소규모 그룹 채팅에 적합
이유
- 새로운 메시지가 왔는지 확인하려면 자기 큐만 보면 되니까 메시지 동기화 플로가 단순함
- 그룹이 크지 않으면 메시지를 수신자별로 복사해서 큐에 넣는 작업의 비용이 문제가 되지 않는다.
궁금! : 큐의 크기를 크게 설정할 수 었는건가? 왜 사용자별로 큐를 두는걸까
큐를 한개 두고 <메시지를 전송한 전송자 , 메시지>
요런식으로 쌍을 묶어서 처리하는 건 어떨까?
ex) 위챗 이라는 곳이 사용하는 접근법, 그룹의 크기를 500명으로 제한하고 있음 많은 사용자를 지원해야 하는 경우라면 똑같은 메시지를 모든 사용자 큐에 복사하는게 바람직 하지 않음
수신자 관점
한 수신자는 여러 사용자로부터 오는 메시지를 수신할 수 있어야 한다.
접속상태 표시
채팅 애플리케이션에서 녹색 점이 의미하는 것
녹색 점을 표현하기 위해 필요한 것은 어떤게 있을까?
사용자의 상태가 바뀌는 시나리오가 몇 가지 있다.
사용자 로그인
웹 소켓 연결이후, A의 상태, last_active_at 타임스탬프 를 키-값 저장소에 보관 이후 해당 사용자가 접속 중인 것으로 표시됨
로그아웃
키-값 저장소에 보관된 사용자 상태가 offline으로 바뀌어야 된다
접속 장애
네트워크 연결이 끊어지면 대응하는 간단한 방법 : 사용자를 오프라인 상태로 표시하고, 복구되면 온라인으로 변경
문제
짧은 시간 동안 끊어졌다 복구되는 일은 흔함. 사용자가 차를 타고 터널을 지나가는 상황을 생각해 보자. 매번 업데이트를 하게 되면 자주 표시기를 변경해야 할 거다. 이건 사용자 경험 측면에서 좋지 않다.
해결
온라인 상태의 클라이언트들을 주기적으로 박동 이벤트를 접속 상태 서버로 보내도록 하고 마지막 이벤트를 받은 x초 이내에 다른 박동 이벤트 메시지를 받으면 해당 사용자의 접속 상태를 계속 온라인으로 유지할 수 있다. 그렇지 않을 경우에만 오프라인으로 바꾸는 것이다. (지연시간? 을 두는 거?)
사용자 A와 친구 관계에 있는 사용자들은 어떻게 상태변화를 알게될까?
상태 정보의 전송
상태 정보 서버 : 발행-구독 모델(publish-subscibe model) 을 사용, 각각 친구관계마다 채널을 하나씩 둠
A-B, A-C, A-D
A-B : 사용자 B가 구독 A-C : 사용자 C가 구독 A-D : 사용자 D가 구독
클라이언트-서버 사이 통신은 실시간 웹소켓으로
그룹크기가 작을때 효과적 ex) 위챗은 500으로 크기 제한
그룹 크기가 커지면 접속 상태 변화를 알리는데 비용과 시간이 많이 들게됨 그룹 하나에 100,000 사용자가 있다고 할때
상태 변화 1건당 100,000개의 이벤트 메시지가 발생
이러한 성능 문제를 해소하기 위한 방법 중 하나는 그룹 채팅에 입장하는 순간에만 상태 정보를 읽게 하거나, 친구 리스트에 있는 사용자의 접속상태를 갱신하고 싶으면 수동으로 하도록 유도하는 것
4단계 마무리
추가적으로 얘기해볼 수 있는 것
- 채팅 앱을 확장하여 사진, 비디어 등의 미디어를 지원하도록 하는 방법
- 압축 방식, 클라우드 저장소, 섬네일 생성
- 종단 간 암호화 : 메시지 발신인과 수신자 이외에 아무도 메시지 내용을 볼 수 없게
- 캐시 : 클라이언트에 이미 읽은 메시지를 캐시해 두면 서버와 주고받는 데이터 양을 줄일 수 있음
- 로딩 속도 개선 : slack은 사용자 데이터, 채널 등을 지역적으로 분산 시켜 로딩 속도를 개선했음
- 오류 처리
- 채팅 서버 오류 : 서버 하나가 죽으면 서비스 탐색 기능(주키퍼)이 동작해 클라이언트에게 새로운 서버를 배정하고 다시 접속 할 수 있도록 해야함
- 메시지 재전송 : 재시도, 큐 는 메시지의 안정적 전송을 보장하기 위해 사용할 수 있음
다음 챕터