채팅 시스템 설계

haaaalin·2023년 8월 28일
0
post-thumbnail

설계 범위

  • 1:1 채팅, 그룹 채팅 지원
  • 모바일 앱, 웹 앱 지원
  • 일별 능동 사용자 수(DAU: Daily Active User) 기준, 5천만명 트래픽
  • 그룹 채팅 인원 제한 100명
  • 중요기능
    • 1:1 채팅
    • 그룹 채팅
    • 사용자 접속상태 표시 지원
    • 텍스트 메시지만 지원
    • 하나의 계정으로 여러 단말 동시 접속 지원
    • 푸시 알림
  • 메시지 길이 제한: 100,000자 이하
  • 종단 간 암호화 → 시간이 허락하면 논의 가능
  • 채팅 이력 보관 기간: 제한 X

설계안

  • 클라이언트는 서로 직접 통신 X
  • 채팅을 시작하려는 클라이언트는 네트워크 통신 프로토콜을 이용해 채팅 서비스에 접속 필요

사용할 통신 프로토콜을 정하는 문제가 중요

HTTP는 안될까?

오랜 세월 검증되었고, 현재 웹에서 가장 널리 사용되는 프로토콜.

keep-alive 헤더를 사용해 TCP접속 과정에서 발생하는 핸드셰이크 횟수를 줄이고, 클라이언트와 서버 사이의 연결을 끊지 않을 수 있어 채팅 서비스를 구현할 수 있지만 ..

HTTP는 클라이언트가 연결을 만들기 때문에 서버에서 클라이언트로 임의 시점에 메시지를 보내는 데는 쉽게 사용할 수 없다

폴링

클라이언트가 주기적으로 서버에 새 메시지가 있는지 물어보는 방법이다.

답해줄 메시지가 없는 경우에는 서버 자원이 불필요하게 낭비된다는 단점

롱 폴링

폴링의 여러 가지 비효율적인 점을 고려한 기법이 롱 폴링이다.

  • 새 메시지가 반환되거나, 타임아웃 될 때까지 연결 유지
  • 새 메시지를 받으면 기존 연결 종료 후, 서버에 새로운 요청을 보낸다.

약점

  • 메시지 전송 클라이언트와 수신 클라이언트가 같은 채팅 서버에 접속하지 않을 수도 있다. → HTTP 서버들은 보통 무상태(stateless) 서버, 로드밸런싱을 사용한다면, 메시지를 받은 서버는 해당 메시지를 수신할 클라이언트와 연결이 되어 있지 않을 수 있다.
  • 서버 입장에서 클라이언트 연결 해제 유무를 알 길이 없다.
  • 여전히 비효율적 → 메시지를 받지 않아도, 타임아웃이 일어날 때마다 주기적으로 서버에 다시 접속

웹소켓

서버가 클라이언트에게 비동기 메시지를 보낼 때 많이 사용하는 방법

  • 연결은 클라이언트가 시작
  • 연결 특징: 항구적, 양방향
  • 연결 후에 서버는 클라이언트에게 비동기적으로 메시지 전송 가능
  • 80 또는 443 HTTP 또는 HTTPS 프로토콜이 사용하는 기본 포트번호를 그대로 사용하기 때문에 방확벽이 있는 환경에서도 잘 동작

개략적 설계안

채팅 시스템의 구성

  • 무상태 서비스
  • 상태유지 서비스
  • 제3자 서비스 연동

채팅이 아닌 다른 부분에서는 굳이 웹소켓을 사용할 필요는 없다.

무상태 서비스

로그인, 회원가입, 사용자 프로필 표시 등을 처리하는 전통적인 요청/응답 서비스

무상태 서비스는 로드밸런서 뒤에 위치

이 서비스들 가운데 상당수가 완제품으로 나와 있어서 쉽게 가져다 쓸 수 있다.

“서비스 탐색” 서비스?

클라이언트가 접속할 채팅 서버의 DNS 호스트명을 클라이언트에게 알려주는 역할

특정 서버에 부하가 몰리지 않도록 도움

상태 유지 서비스

유일하게 상태 유지가 필요한 서비스 = 채팅 서비스

제3자 서비스 연동

실행 중이지 않더라도, 새 메시지를 받았다면 알림을 받아야 한다.

따라서 푸시 알림 서비스 통합은 아주 중요

규모 확장성

이번에 다루는 채팅 시스템의 경우, 동시 접속자가 1M, 접속당 10K의 서버 메모리가 필요하다면, 10GB 메모리만 있어도 모든 연결을 다 처리 가능

하지만, SPOF(Single-Point-Of-Failure)를 고려해야 한다.

  • 채팅 서버: 클라이언트 사이에서 메시지 중계 역할
  • 접속 상태 서버: 사용자의 접속 여부 관리
  • API 서버: 로그인, 회원가입, 프로필 변경 등 그 외 나머지 전부 처리
  • 알림 서버: 푸시 알림 전송
  • 키-값 저장소: 채팅 이력 보관

저장소

데이터 베이스를 정할 때 가장 중요한 것!

  • 데이터의 유형
  • 읽기/쓰기 연산의 패턴

채팅 시스템이 다루는 데이터의 유형

  • 사용자 프로필, 설정, 친구 목록과 같은 일반 데이터 → 안정성을 보장하는 관계형 DB에 보관
  • 채팅 이력 → ?

채팅 이력의 읽기/쓰기 연산 분석

  • 채팅 이력 데이터의 양
    • ex) 페이스북 메신저, 왓츠앱 → 매일 600억개의 메시지 처리
  • 가장 빈번하게 사용되는 데이터
    • 주로 최근에 주고 받은 메시지. 오래된 메시지는 잘 보지 않는다.
  • 검색 기능이나, 특정 사용자가 언급된 메시지 등 무작위적인 데이터 접근 가능성
  • 1:1 채팅앱의 경우 읽기/쓰기 비율은 약 1:1

⇒ 결론적으론, 키-값 저장소 추천

  • 수평적 규모 확장 용이
  • 데이터 접근 지연시간이 낮다
  • 관계형 DB는 long tail에 해당하는 부분 처리 어려움 → 인덱스가 커지면, 데이터에 대한 무작위적 접근 처리 비용 증가
  • 이미 선례가 많다. ex) 페이스북 메신저: HBase, 디스코드: 카산드라

데이터 모델

1:1 채팅 메시지 테이블 - message

그룹 채팅 메시지 테이블 - group_message

  • channel_id: 파티션 키로도 사용할 예정 (모든 질의는 특정 채널을 대상으로 할 것이기 때문)

message ID

message_id는 메시지들의 순서도 표현할 수 있어야 한다. 따라서 아래 두 가지를 만족 필요

  • message_id 값은 고유해야 한다
  • ID 값은 정렬 가능해야 하고, 시간 순서와 일치해야 한다.

아래와 같은 방법이 있다.

  • RDBMS는 해결 가능, NoSQL은 auto_increment 기능을 제공하지 않는다.
  • 스노플레이크 같은 전역적 64-bit 순서 번호 생성기 이용
  • 지역적 순서 번호 생성기
    • ID의 유일성은 같은 그룹 안에서만 보증하면 충분
    • 메시지 사이의 순서는 같은 채널, 같은 1:1 채팅 세션 안에서만 유지되면 충분하기 때문에 지역적으로 생성 가능

상세 설계

서비스 탐색

클라이언트에게 가장 적합한 채팅 서버 추천하는 역할

채팅 서버 추천 기준

  • 클라이언트의 위치
  • 서버의 용량

서비스 탐색을 구현하는 데 널리 쓰이는 오픈소스로는 아파치 주키퍼 등이 존재

  • 사용 가능한 모든 채팅 서버를 등록
  • 클라이언트 접속 시도시, 사전에 정한 기준에 따라 최적의 채팅 서버 선택

아래는 주키퍼로 구현한 서비스 탐색 기능이다.

  1. 사용자 A가 로그인 시도
  2. 로그인 요청 API 서버들 가운데 하나로 전송
  3. API 서버가 사용자 인증 처리 후, 서비스 탐색 기능 동작 → 해당 사용자에게 최적의 채팅 서버 탐색 후, 채팅 서버 2 반환
  4. 사용자 A는 채팅 서버 2와 웹소켓 연결

메시지 흐름

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

  1. 사용자 A가 채팅 서버 1로 메시지 전송

  2. 채팅 서버 1은 ID 생성기를 사용해 해당 메시지 ID 새엇ㅇ

  3. 채팅서버 1은 해당 메시지를 메시지 동기화 큐로 전송

  4. 메시지가 키-값 저장소에 저장

  5. (a) 사용자 B가 접속 중인 경우 → 사용자 B가 접속 중인 채팅 서버로 전송

    (b) 사용자 B가 접속 중이 아닌 경우 → 푸시 알림 메시지 푸시 알림 서버로 전송

  6. 채팅 서버 2는 메시지를 사용자 B에게 전송

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

  • 사용자 A가 휴대폰과 랩톱에서 채팅 앱에 로그인한 결과로, 채팅 서버 1과 연결되어 있는 상태
  • 각 단말은 cur_max_message_id (해당 단말에서 관측된 가장 최신 메시지 ID)라는 변수 유지

아래 조건 만족 시, 새 메시지로 간주

  • 수신자 ID가 현재 로그인한 사용자 ID와 같다
  • 키-값 저장소에 보관된 메시지로서, 그 ID가 cur_max_message_id보다 크다

cur_max_message_id는 단말마다 별도로 유지, 관리하면 되는 값 → 새 메시지를 가져오는 동기화 작업도 쉽게 구현 가능

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

해당 그룹에 3명의 사용자가 있다고 가정.

  • 사용자 A가 보낸 메시지가 사용자 B와 C의 메시지 동기화 큐에 복사
    • 해당 큐를 각각 할당된 메시지 수신함으로 이해해도 무방

소규모 그룹 채팅에 적합한 이유

  • 새로운 메시지를 확인하기 위해, 자기 큐만 확인하므로, 메시지 동기화 flow가 단순
  • 그룹이 크지 않으면 메시지를 수신자별로 복사해서 큐에 넣는 작업의 비용 문제 X

접속상태 표시

접속상태 서버 또한, 클라이언트와 웹소켓으로 통신하는 실시간 서비스의 일부

사용자 로그인

클라이언트와 실시간 서비스 사이에 웹소켓 연결 후,

접속상태 서버는 A의 상태와 last_active_at 타임스탬프 값을 키-값 저장소에 보관 → 접속 중 표시

로그아웃

키-값 저장소에 보관된 사용자 상태가 online → offline으로 변경

접속 장애

인터넷 연결이 끊어진다면, 클라이언트와 서버 사이의 웹소켓 연결 또한 끊어진다.

사용자를 오프라인 상태로 변경하고, 복구되면 온라인 상태로 변경?
→ 짧은 시간 동안 인터넷 연결이 끊어졌다 복구되는 흔하기 때문에, 지나친 작업이고, 사용자 경험 측면에서도 바람직 X

박동 검사

  • 온라인 상태의 클라이언트는 접속 상태 서버로 주기적으로 박동 이벤트 전송
  • n초 이내에 다른 박동 이벤트 메시지를 받으면, 해당 사용자의 접속 상태를 온라인으로 유지
  • 그렇지 않을 경우 오프라인으로 변경

상태 정보의 전송

사용자 A와 친구인 사용자들은 사용자의 상태 변화를 어떻게 알까?

상태정보 서버는 발행-구독 모델 사용 → 각 친구관계마다 채널을 하나씩 사용

그룹 크기가 작을 때는 효과적이지만, 그룹 크기가 더 커지면, 접속상태 변화를 알리는 비용이나 시간이 많이 들게 되므로 좋지 않다.

만약, 그룹 하나에 100,000 사용자가 있다면, 상태변화 1건당 100,000개의 이벤트 메시지 발생

그렇다면 그룹의 크기가 클 때는 어떻게 해야할까?

  • 해당 사용자가 그룹 채팅에 입장하는 순간에만 상태 정보 읽기
  • 친구 리스트에 있는 사용자의 접속 상태 갱신을 수동으로

마무리

  • 채팅 앱 확장 시 사진이나 비디오 등의 미디어 지원 방법
    • 미디어 파일의 크기 고려 → 압축 방식, 클라우드 저장소, 섬네일 생성 등
  • 종단 간 암호화
    • 메시지 발신인과 수신자 이외에는 아무도 메시지 내용을 볼 수 없도록
  • 캐시
    • 클라이언트에게 이미 읽은 메시지를 캐시해 두기
  • 로딩 속도 개선
    • 사용자의 데이터, 채널 등을 지역적으로 분산하는 네트워크 구축 → 앱 로딩 속도 개선
  • 오류 처리
    • 채팅 서버 오류: 채팅 서버 하나에 수십만 사용자 접속 상황, 서버가 만약 죽는다면, 서비스 탐색 기능이 동작하여 클라이언트에게 새로운 서버를 배정, 다시 접속할 수 있도록 해야한다.
    • 메시지 재전송: 재시도, 큐는 메시지의 안정적 전송을 보장하기 위해 흔히 사용되는 기법
profile
한 걸음 한 걸음 쌓아가자😎

0개의 댓글