본문 바로가기

server/system design

8. 메신져 (slack // facebook messenger // whatapp // kakaotalk)

1. 기능 요구 사항

  • 사용자 간의 일대일 채팅 및 그룹 채팅.
  • 사용자가 텍스트, 사진, 비디오 및 기타 파일 전송
  • 채팅 기록
  • 사용자의 온라인/오프라인 상태 표시
  • 알림

2. 추정 및 제약 사항

  • 매일 약 100억 개의 메시지가 전송되는 20억 명의 사용자를 처리
  • 최소 대기 시간으로 실시간 채팅.
  • 메시징 서비스는 고가용성이어야 하지만 이 경우 일관성이 우선순위가 높다.
  • 시스템은 매우 일관성이 있어야 한다.  즉, 모든 사용자는 로그인하는 모든 장치를 통해 동일한 순서로 메시지를 볼 수 있어야 한다.

 

예상 용량

메시지 저장용량 추정

각 메시지에 대해 평균 크기가 140바이트이고 매일 전송되는 약 100억 개의 메시지를 추정하면 일일 메시지에 대해 약 1.4TB의 저장 용량이 필요

100억 메시지 * 140바이트 = 1.4테라바이트

위의 추정치는 메타 데이터가 없고 단일 메시지 사본이 저장되어 있다고 가정한다. 그러나 실제 메시징 응용 프로그램에서는 다른 정보와 함께 메타데이터도 저장된다. 메시지는 확장성과 대기 시간 감소를 위해 여러 캐시에 복제된다

디자인을 단순하게 유지하면서 매일 1.4TB의 메시지 저장 용량을 가정하고 10년 동안 저장한다고 가정하면

10년 * 365일 * 1.4TB ≅ 5페타바이트

 

온라인/오프라인 상태 저장

메시지는 저장되는 유일한 데이터가 아닙니다. 예를 들어 Facebook Messenger의 경우 사용자의 상태도 저장됩니다. 그러나 이들은 데이터베이스에 저장할 필요가 없으며 메모리 캐시에만 저장된다. 10억 명을 온라인 사용자라고 가정하고 각 항목에 1KB가 필요한 경우

10억 온라인 사용자 * 1KB = 분산 캐시의 1 테라바이트 메모리 공간.

 

3. 기본 아키텍처

시스템의 가장 기본적인 아키텍처에 대해 서로 통신하려는 두 명의 사용자 A와 B가 있다고 가정한다. A가 B의 주소를 알고 B가 A의 주소를 알고 있는 이상적인 시나리오에서는 알려진 IP 주소에서 서로 정보를 보낼 수 있다.

클라이언트 A와 B는 더 큰 네트워크의 일부이며 수백만 명의 다른 사용자가 메시지 교환한다. 이제 이러한 경우 사용자가 다른 사용자의 IP 주소를 아는 것은 불가능하다. 따라서 사용자 간의 직접 연결 대신 연결을 관리하기 위해 사이에 서버가 있어야 합니다.

 

여러 서버의 클러스터가 있는 수평 확장 시스템에서 클라이언트 A는 로드 밸런서에 연결하고 로드 밸런서는 그 순간에 클라이언트에게 서비스를 제공할 서버를 결정한다. A가 서버 2를 통해 B에 대한 메시지를 보낸다고 가정해보면 서버 2는 메시지를 데이터베이스에 저장하고 로드 밸런서를 통해 B에도 중계한다.

이제 A가 B와 통신하기를 원하면 B의 IP 주소를 알 필요가 없다. 단순히 서버(이 경우 서버 2)에 연결하고 서버는 B(또는 프로세스 B의 메시지를 처리함). 서버 2는 해당 프로세스를 통해 B에게 메시지를 보낸다

 

알림 전송/전송/읽음

보낸 메시지는 수신자에 의해 전송, 전달 및 읽었는지 여부를 알려준다.

시스템이 메시지를 보낸 사람에게 전송, 배달 및 읽기 알림을 전달하는 방법을 살펴본다.

 

메시지 알림 주기

  1. A는 서버를 통해 B에게 메시지를 보낸다
  2. 연결이 일부 메시징 프로토콜(예: TCP)을 따르기 때문에 서버가 메시지를 수신하면 A에게 승인이 전송된다.
  3. 서버는 데이터를 데이터베이스에 저장하고 사용자 B에 메시지를 보낸다
  4. 사용자 B가 메시지를 받으면 서버에 승인을 보낸다
  5. 서버는 메시지가 B에게 배달되었다는 알림을 A에게 보낸다.
  6. B가 메시지를 보는 즉시 서버에 확인을 보내야 한다.  이 확인 메시지는 보낸 동일한 서버로 보낼 필요는 없다.
  7. 알림은 고유 한 메시지 ID와 독립된 메시지로 취급될 수 있다. 알림은 서버를 통해 A에게 전송됩니다.

 

서버-클라이언트 연결

서버와 클라이언트 간의 통신을 위한 푸시 접근 방식으로 진행하면 다시 두 가지 선택이 가능하다. Long Polling 및 WebSockets

 

클라이언트가 서버에 정보를 요청할 때 서버는 클라이언트에 대한 새 메시지가 있거나 연결 시간이 초과될 때까지 연결을 열린 상태로 유지한다. 서버가 새 메시지로 응답하면 연결이 종료된다. 연결이 끝나면 클라이언트는 즉시 서버에 대한 또 다른 메시지 요청을 시작하고 주기가 반복된다. 이 접근 방식은 풀 접근 방식(폴링)과 관련된 대기 시간과 리소스를 줄인다.

 

웹소켓

서버가 WebSocket 연결을 통해 클라이언트에 메시지를 보낼 수 있다. 클라이언트가 서버와 설정한 HTTP 연결은 '핸드 셰이크'를 통해 WebSocket 연결로 설정될 수 있다. 이것은 서버와 클라이언트 간의 양방향 일관된 연결이며 메시지를 보내고 받는 데 사용할 수 있다.

 

 

 

4. 온라인 사용자에게 메시지 보내기 - 1 : 1 채널

1단계: Alice는 연결된 채팅 서버로 연결되는 메시지를 Bob에게 보낸다.

2단계: Alice는 연결된 채팅 서버(예: Chat_Server_A)로부터 승인을 받고 메시지는 Alice에게 SENT로 표시된다.

3단계: Chat_Server_A는 Bob이 연결된 채팅 서버에 대한 정보를 가져오기 위해 데이터 저장소에 요청한다.

4단계: Chat_Storage는 Bob이 Chat_Server_B에 연결되었다는 정보를 반환한다

5단계: Chat_Server_A는 메시지를 Chat_Server_B로 전달한다.

6단계: 푸시 메커니즘을 사용하여 메시지가 Bob에게 전달된다.

7단계: Bob은 ACK를 Chat_Server_B로 다시 보낸다

8단계: ACK는 Alice가 연결된 Chat_Server_A로 전달된다.

9단계: ACK가 Alice에게 전달되고 DELIVERED로 표시된다.

10단계: Bob이 메시지를 읽을 때 ACK가 Chat_Server_B로 전송된다.

11단계: Chat_Server_B는 Alice가 연결된 서버를 가져오도록 요청한다.

12단계: Chat_Storage는 Alice가 Chat_Server_A에 연결되었다는 정보를 반환한다.

13단계: Chat_Server_B는 읽은 ACK를 Chat_Server_A로 전달한다.

14단계: ACK는 요청을 READ로 Alice에게 전달된다.

 

 

 

사용자가 오프라인이면 메시지는 어떻게 될까?

B가 오프라인 상태가 되어 더 이상 서버에 연결되지 않은 사용자에 대한 메시지가 수신되면 메시징 시스템은 위에서 논의한 해시 테이블에서 B에 대한 항목을 찾지 못한다. 이는 B가 서버와 열린 연결이 없기 때문에 연결 ID도 없기 때문이다. 그렇다면 서버는 B에게 온 메시지를 어디에 저장해야 할까?

이 메시지는 B의 사용자 ID에 대해 데이터베이스에 저장된다. 사용자 B가 서버에 연결하고 B에 대한 열린 연결이 생성되자마자 데이터베이스에서 B의 사용자 ID에 대한 새 메시지가 있는지 확인한다. B를 위해 데이터베이스에서 기다리고 있던 메시지는 B에 대한 연결을 통해 B는 메시지를 수신한다.

 

온라인/오프라인 상태

Facebook 메신저는 사용자의 온라인/오프라인 상태를 보여준다.  WhatsApp에서는 '마지막으로 확인'이 표시된다 약간 다르지만 이 두 기능은 동일한 기본 사항을 사용하여 구현할 수 있다. 포스트 앞부분에서 설명했듯이 사용자의 상태에 대한 정보는 데이터베이스가 아닌 메모리 캐시에 저장된다.

따라서 사용자가 온라인/ 오프라인, 또는 사용자가 '마지막으로 본' 시간을 알기 위해서는 온라인 사용자가 주기적으로 서버에 보내는 heartbeat가 필요합니다. 서버는 메모리 캐시에 저장된 테이블의 하트비트 타임스탬프를 계속 업데이트한다. 모든 활성 사용자는 사용자 ID에 대한 타임스탬프 값이 있는 이 테이블의 항목을 갖게 된다.

온라인 클라이언트가 5초마다 하트비트를 보내도록 시스템이 구성되어 있다고 가정한다.

클라이언트 A가 5초마다 하트비트를 보내 A가 애플리케이션을 사용하고 있음을 서버에 알린다. 메시징 서버는 하트비트를 수신하면 테이블의 시간을 업데이트한다.

A는 테이블에 항목이 있고 타임스탬프가 최근인 경우 온라인 상태가 된다. B라는 다른 사용자가 A의 상태를 알고 싶어 하면 서버는 테이블에서 A의 타임스탬프를 읽고 그가 온라인인지 오프라인인지 확인하고 B에게 표시할 수 있다.

 

일시적인 데이터 저장

FIFO 기반 정책을 사용하여 일시적인 메시지를 저장하고 검색하는 대기열 기반 메커니즘을 구현할 수 있다. 이를 위해 Amazon SQS 또는 Windows Azure Queue Service와 같은 기존 클라우드 기반 기술을 사용할 수 있다. 이 대기열을 사용하여 오프라인 사용자에게 전송된 임시 메시지를 저장할 수 있다. 메시지가 오프라인 사용자에게 전달되면 이러한 임시 메시지에 대한 모든 참조가 시스템에서 제거된다.

 

 

5. 오프라인 사용자에게 이미지 보내기 - 1 : 1 채널

 

1단계: Alice는 Alice가 연결된 서버인 Chat_Server_A로 전달되는 Bob에게 이미지를 보낸다.

2단계: Chat_Server_A는 파일을 파일 서버에 이미지를 업로드한다.

3단계: 파일 서버는 업로드된 파일의 이미지 URL을 Chat_Server_A로 반환한다.

4단계: 이미지 url은 Alice의 장치에서 이미지를 렌더링 하는 데 사용되는 Alice에게 반환, 이미지는 Alice의 쪽에서 SENT로 표시된다.

5단계: Chat_Server_A는 Bob이 연결된 서버를 요청한다

6단계: Chat_Storage는 Bob이 오프라인 상태라는 정보를 반환한다.

7단계: Chat_Server_A는 image-url이 포함된 메시지를 임시 서버로 전달한다.

8단계: 임시 서버는 임시 저장소에 image-url을 포함하는 메시지를 저장한다.

9단계: Bob이 온라인 상태가 되어 Chat_Server_B로 heartbeat를 수행한다.

10단계: Chat_Server_B는 임시 서버에서 Bob에 대한 임시 메시지를 가져온다

11단계: Chat_Server_B는 임시 메시지를 Bob에게 전달한다.

12단계: Bob은 파일 서버에서 이미지를 가져온다. 이 시점에서 이미지가 Bob의 장치로 전달되고 임시 서버의 모든 참조가 임시 서버에서 제거된다.

13단계: Bob의 기기는 Alice의 이미지에 대한 ACK를 Chat_Server_B로 보낸다

14단계: Alice가 연결된 서버에 대한 정보를 가져온다.

15단계: ACK를 Chat_Server_A로 전달

16단계: ACK는 메시지를 DELIVERED로 표시하여 Alice에게 전달된다.

 

 

데이터베이스에서 메시지 저장 및 검색

채팅 서버는 새 메시지를 받을 때마다 데이터베이스에 저장해야 한다. 이를 위해 두 가지 옵션이 있다

  1. 메시지를 저장하기 위해 데이터베이스와 함께 작동할 별도의 스레드를 시작
  2. 메시지를 저장하기 위해 데이터베이스에 비동기 요청을 보낸다.

데이터베이스를 설계하는 동안 다음과 같은 특정 사항을 염두에 두어야 한다

  1. 데이터베이스 연결 풀을 효율적으로 사용하는 방법.
  2. 실패한 요청을 재시도하는 방법은 무엇입니까?
  3. 몇 번의 재시도 후에도 실패한 요청을 어디에 기록합니까?
  4. 모든 문제가 해결되었을 때 이러한 기록된 요청(재시도 후 실패한)을 어떻게 재시도합니까?

어떤 스토리지 시스템을 사용해야 합니까?

매우 높은 비율의 소규모 업데이트를 지원할 수 있고 다양한 레코드를 빠르게 가져올 수 있는 데이터베이스가 필요하다. 데이터베이스에 삽입해야 하는 작은 메시지가 많고 쿼리 하는 동안 사용자가 주로 메시지에 순차적으로 액세스 한다.

사용자가 메시지를 수신/전송할 때마다 데이터베이스에서 행을 읽고 쓸 여유가 없기 때문에 MySQL과 같은 RDBMS 또는 MongoDB와 같은 NoSQL을 사용할 수 없습니다. 이렇게 하면 서비스의 기본 작업이 높은 대기 시간으로 실행될 뿐만 아니라 데이터베이스에 엄청난 부하가 발생한다.

HBase와 같은 광범위한 열 데이터베이스 솔루션으로 두 요구 사항을 모두 쉽게 충족할 수 있다.  HBase는 하나의 키에 대해 여러 값을 여러 열에 저장할 수 있는 열 지향 키-값 NoSQL 데이터베이스다. HBase는 Google의 BigTable을  모델로 한다.. HDFS  (Hadoop Distributed File System) 위에서 실행된다. HBase는 데이터를 그룹화하여 메모리 버퍼에 새 데이터를 저장하고 버퍼가 가득 차면 데이터를 디스크에 덤프 한다. 이러한 저장 방식은 많은 작은 데이터를 빠르게 저장할 뿐만 아니라 키 또는 스캔 범위별로 행을 가져오는 데 도움이 된다. 또한 가변 크기 데이터를 저장하는 효율적인 데이터베이스이다.

클라이언트는 서버에서 데이터를 어떻게 효율적으로 가져와야 할까요?  클라이언트는 서버에서 데이터를 가져오는 동안 페이지를 매겨야 한다. 페이지 크기는 클라이언트마다 다를 수 있다. 예를 들어 휴대전화의 화면이 더 작기 때문에 뷰포트에서 더 적은 수의 메시지/대화가 필요하다.

 

6. 데이터 파티셔닝

많은 데이터(5년 동안 3.6PB)를 저장할 것이기 때문에 여러 데이터베이스 서버에 배포해야 한다.

UserID 기반 파티셔닝:  사용자의 모든 메시지를 동일한 데이터베이스에 보관할 수 있도록 UserID의 해시를 기반으로 파티셔닝 한다고 가정한다. 하나의 DB 샤드가 4TB라면 5년 동안 “3.6PB/4TB ~= 900” 샤드를 갖게 된다. 단순화를 위해 1K 샤드를 유지한다고 가정해 보면 "hash(UserID) % 1000"으로 샤드 번호를 찾은 다음 데이터를 저장/검색합니다. 이 분할 방식은 모든 사용자의 채팅 기록을 가져오는 데 매우 빠르다

처음에는 하나의 물리적 서버에 상주하는 여러 샤드가 있는 더 적은 수의 데이터베이스 서버로 시작할 수 있다. 서버에 여러 데이터베이스 인스턴스를 가질 수 있으므로 단일 서버에 여러 파티션을 쉽게 저장할 수 있다.

메시지의 기록을 무제한으로 저장할 것이기 때문에 더 적은 수의 물리적 서버에 매핑되는 많은 수의 논리적 파티션으로 시작할 수 있으며 스토리지 요구가 증가함에 따라 논리적 파티션을 배포하기 위해 더 많은 물리적 서버를 추가해야 한다.

MessageID 기반 파티셔닝:  사용자의 다른 메시지를 별도의 데이터베이스 샤드에 저장하면 채팅 메시지 범위를 가져오는 것이 매우 느려지므로 이 방식을 채택해서는 안된다.

 

 

7. 사용자 상태 관리

사용자의 온라인/오프라인 상태를 추적하고 상태 변경이 발생할 때마다 모든 관련 사용자에게 알려야 한다. 모든 활성 사용자에 대해 서버에 연결 개체를 유지 관리하고 있으므로 이를 통해 사용자의 현재 상태를 쉽게 파악할 수 있다. 하지만 5억 명의 활성 사용자가 있는 경우 각 상태 변경을 모든 관련 활성 사용자에게 브로드캐스트 해야 하는 경우 많은 리소스가 소모된다. 이에 대해 다음과 같은 최적화를 수행할 수 있다.

  1. 클라이언트가 앱을 시작할 때마다 친구 목록에 있는 모든 사용자의 현재 상태를 가져온다
  2. 사용자가 오프라인 상태가 된 다른 사용자에게 메시지를 보낼 때마다 보낸 사람에게 실패를 보내고 클라이언트의 상태를 업데이트한다.
  3. 사용자가 온라인 상태가 될 때마다 서버는 사용자가 즉시 오프라인 상태가 되지 않는지 확인하기 위해 항상 몇 초의 지연으로 해당 상태를 브로드캐스트 할 수 있다.
  4. 클라이언트는 사용자의 뷰포트에 표시되는 사용자에 대한 상태를 서버에서 가져올 수 있다. 서버가 사용자의 온라인 상태를 브로드캐스트 하는 동안 사용자가 오프라인 상태로 될 수 있기 때문에 이 작업을 자주 수행해서는 안 된다.
  5. 클라이언트가 다른 사용자와 새 채팅을 시작할 때마다 그 시점의 상태를 가져올 수 있다.

 

8. 그룹 채팅

그룹 채팅을 수용할 수 있도록 메시징 시스템의 설계를 확장할 수 있다. 채팅 구성원 중 한 명이 그룹 채팅으로 보낸 메시지는 채팅의 모든 구성원이 수신해야 한다.

각 그룹 채팅은 고유한 개체로 간주되며 GroupChatID가 할당된다. A가 GroupChatID가 1인 그룹에서 메시지를 보낸다고 가정한다. 로드 밸런서는 그룹 1을 처리하는 서버를 조회하고 해당 서버로 메시지를 보낸다. 서버에는 그룹 메시지를 처리하는 서비스가 있으며 그룹 채팅의 일부이기도 한 다른 사용자에 대해 이 서비스를 쿼리 한다. 그룹 서비스는 그림과 같이 각 GroupChatID의 모든 구성원을 저장하는 테이블을 유지 관리한다.

그룹 서비스는 그룹 채팅 1의 일부인 모든 사용자 A, B 및 C를 반환한다. 그런 다음 서버는 A, B 및 C에 대한 메시지를 처리하는 다른 연결로 메시지를 보낸다. A, B, C에 대한 고유한 개방형 연결을 통해 그룹 메시지가 채팅의 각 구성원에게 전달된다.

 

참고 사항

https://www.youtube.com/watch?v=zKPNUMkwOJE&ab_channel=TusharRoy-CodingMadeSimple

https://www.youtube.com/watch?v=uzeJb7ZjoQ4&ab_channel=Exponent

https://www.youtube.com/watch?v=RjQjbJ2UJDg&ab_channel=codeKarle

https://www.youtube.com/watch?v=vvhC64hQZMk&ab_channel=GauravSen

https://www.youtube.com/watch?v=L7LtmfFYjc4&ab_channel=TechDummiesNarendraL

https://medium.com/double-pointer/system-design-interview-facebook-messenger-whatsapp-slack-discord-or-a-similar-applications-47ecbf2f723d

https://systeminterview.com/design-a-chat-system.php

https://learnsystemdesign.blogspot.com/p/design-facebook-messanger.html

https://www.codekarle.com/system-design/Whatsapp-system-design.html

https://techtakshila.com/system-design-interview/chapter-1/

 

'server > system design' 카테고리의 다른 글

쿠폰발급 서비스 구축하기 (실험용)  (0) 2022.08.12
9. netflix  (0) 2022.02.13
확률적 자료구조  (0) 2021.08.12
7. UBER // 쏘카 // lyft // 카카오택시  (0) 2021.08.09
6. tinder  (0) 2021.08.01