Whatsapp(왓츠앱) 시스템 디자인
Key Requirements
왓츠앱은 전 세계의 수많은 유저들이 빈번하게 접근하는 확장성이 뛰어난 시스템이기 때문에 신뢰성있는 시스템 디자인이 필수적이다. 그러므로 왓츠앱의 주요 요구사항을 식별하는 것은 굉장히 중요하다. 왓츠앱의 기본 요구사항 중 일부는 다음과 같다.
- 1:1 대화를 지원해야 한다.
- 마지막으로 본 시간과 메시지 확인을 표시하는 기능(전송, 도착, 읽음 표시)
- 종단 간 암호화된 미디어 전송
Capacity Estimation
우리의 목표는 대용량 트래픽을 수용할 수 있는 플랫폼을 설계하는 것이다. 이것을 위해 메시지와 유저의 수, 피크 타임의 트래픽, 데이터 저장소등 다양한 요소들을 고려해야 한다. 아래와 같은 상황으로 가정해보자.
- 하루에 100억건의 메시지가 10억명의 유저들을 통해 전송된다.
- 피크타임 초당 70만명의 활성 유저가 존재한다.
- 피크타임 초당 4천만건의 메시지가 전송된다.
- 평균적으로 각 메시지는 160개의 문자열로 구성되어있고 하루에 1.6TB의 용량을 차지한다.
- 대략 10년동안 서비스 될 예정이고 6PB(10B * 1.6B * 365)정도의 저장공간이 필요하다.
- 어플리케이션은 수많은 마이크로 서비스로 이루어져 있고 각각 특정 작업들을 수행한다. 메시지 전송의 레이턴시는 20ms이며 각 서버는 100개의 커넥션을 동시에 처리할 수 있다고 가정해보자. 이러한 상황에서 채팅 서비스를 지원하려면 8000(40M * 20ms / 100)개의 서버가 필요하다.
High Level Design
왓츠앱에는 2가지 핵심 서비스가 존재하는데 바로 chat service와 transient service이다. Chat service는 사용자가 보낸 온라인 메시지와 관련된 모든 트래픽을 관리한다. 들어오는 메시지 처리, 다른 유저에게 메시지 보내기, 메시지 상태 관리하는 기능(전송되었는지, 읽었는지 등)등이 포함된다.
반면에 Transient service는 오프라인 상태의 유저들에 대한 트래픽을 담당한다. 여기에는 수신자가 오프라인 상태일 때 전송된 메시지를 저장하고 다시 온라인 상태가 되면 메시지를 전달해주는 기능등을 포함한다. 또한 읽음 표시 등의 메시지 상태에 대한 관리를 포함하여 오프라인 유저들의 수많은 기능들도 같이 수행한다. 정리하자면 아래와 같다.
- 유저가 메시지를 보내면 chat service는 유저가 수신자가 온라인 상태인지 확인한다. 온라인이면 메시지를 즉시 전송한다.
- 수신자가 오프라인 상태라면 transient service에게 메시지를 전달한다. transient service는 수신자가 온라인 상태로 돌아오기 전 까지 분산된 저장소에 메시지를 보관한다.
- 수신자가 온라인 상태로 돌아오면 transient service는 저장소에 보관하던 메시지를 chat service에게 전달하고 이는 수신자에게 전송된다.
High Level API Design
메시징 시스템은 메시지를 전송하고 확인하는 주요 2가지 API가 있다.
Sendmessage(fromUser, toUser, clientMetaData, message)
Sendmessage API는 다른 유저에게 메시지를 보낼 때 사용된다. 인자의 역할은 아래와 같다.
- fromUser: 발신자
- toUser: 수신자
- clientMetaData: 디바이스 정보나 플랫폼 등 클라이언트 부가 정보
- message: 실제 전송되는 메시지
Conversation(userId, offset, messageCount, TimeStamp)
Conversation API는 왓츠앱을 열 때 나타나는 스레드에 대화를 표시한다. 또한 한번에 너무 많은 메시지를 가져오는 것을 방지하기 위해 한명의 유저가 API를 호출할 때 개수 제한이 있다. offset과 messageCount 인자는 얼마나 많은 메시지를 가져올 지 조정할 때 사용된다.
- userId: 유저 고유 식별자
- offset: 이전 메시지를 가져올 때 사용
- messageCount: 표시될 메시지 개수
- TimeStamp: 메시지가 업데이트 된 최근 날짜
How features like last seen, single tick, and double tick work?
acknowledgement service는 확인 응답을 지속적으로 생산 및 검사하며 위 기능들을 구현한다. 그리고 응답을 받음에 따라 어떻게 처리할 지 결정한다.
Single Tick
유저 A의 메시지가 유저 B에게 도달하면 서버는 메시지가 전송되었음을 확인하기 위해 ack 시그널을 보낸다.
Double Tick
서버가 유저 B에게 메시지를 보낸 후 유저 B는 서버에게 메시지 수신을 응답하기 위해 ack를 보낸다. 그 이후 서버는 유저 A에게 ack를 보낸다.
Blue Tick
유저 B가 메시지를 읽으면 메시지 확인 검사를 위해 서버에게 ack를 보낸다. 그 이후 서버는 유저 A에게 또다른 ack 메시지를 보낸다.
Last Seen Feature
이 기능은 5초에 한번 씩 서버로 메시지를 보내는 하트비트 메커니즘에 의존한다. 서버는 이러한 메시지를 통해 마지막으로 메시지를 확인한 상태 테이블을 관리한다.
Design of Key Features
One-to-One Communication
엘리스가 밥에게 메시지를 전송하고 싶어한다고 가정해보자. 메시지는 엘리스가 접속되어있는 챗 서버로 전송된다. 엘리스는 챗 서버로부터 메시지를 전송했는지에 대한 ack를 수신한다. 그리고 챗 서버는 데이터 저장소에 밥이 연결된 채팅 서버에 대한 정보를 가져오도록 요청한다. 엘리스의 챗 서버는 밥의 챗 서버로 메시지를 전송하고 푸시 메커니즘을 통해 밥에게 전달된다. 이후 밥은 엘리스의 챗 서버에게 메시지가 수신되었다는 ack를 전송한다. 만약 밥이 메시지를 다시 한번 읽으면 이때에도 마찬가지로 새로운 ack 메시지가 엘리스의 챗 서버에게 전송된다.
User Activity Status
아래 그림은 서버와 클라이언트의 연결 메커니즘에 대해 보여준다. 웹 소켓을 통해 서버와 클라이언트 간 양방향 커넥션이 맺어져있다. 그리고 이 커넥션을 통해 유저의 활동 상태를 모니터링하기 위해 하트비트가 전송된다.
End-to-End Encryption
종단 간 암호화는 오직 통신하는 사용자만 메시지를 읽을 수 있도록 보장하는 기능이다. 이는 통신에 참여하는 모든 유저가 공유하는 공개 키를 사용하여 동작한다.
예를 들어 엘리스와 밥이 채널에서 서로 통신하고 있다고 가정해보자. 엘리스와 밥 모두 각자의 공개 키를 가지고 있다. 또한 각자 공유되지 않은 비밀 키 또한 가지고 있다.엘리스가 밥에게 메시지를 보내고 싶을 때 밥의 공개 키로 메시지를 암호화한다. 이 메시지는 오직 밥의 비밀 키로만 복호화할 수 있다. 비슷하게, 엘리스는 오직 밥에게로 부터 수신된 메시지만 복호화할 수 있다. 이러한 방식에서는 오직 엘리스와 밥만이 서로의 메시지를 읽을 수 있으며 서버는 단지 중개자 역할만을 수행한다.
Bottlenecks
모든 시스템은 오류에 취약하기 때문에 이러한 상황을 대처할 수 있는 방안을 마련해두는 것은 굉장히 중요하다. 대용량 트래픽을 다루기 위해서는 발생할 수 있는 수많은 상황들에 대해 내결함성을 가질 수 있도록 해야한다. 우리 서비스에서는 chat/transient 서버가 가장 중요한 요소이며 운영중에 발생할 수 있는 모든 문제를 해결하는것이 필요하다.
- Chat Server Failure: Chat서버는 핵심 요소 중 하나이며 온라인 상태의 유저에게 메시지를 전송하는 역할을 한다. 그리고 유저가 커넥션을 유지하기 때문에 만약 장애가 발생하면 전체 시스템에 전파된다. 따라서 TCP 커넥션을 다른 서버에게 전달하거나 유저가 커넥션을 유실했을 때 자동으로 새로운 커넥션을 수립하도록 하는 2가지 방법을 사용할 수 있다.
- Transient Storage Failure: transient 저장소 또한 장애가 전체 서비스로 전파되기 쉽다. 만약 해당 요소에 장애가 발생하면 오프라인 유저들의 메시지들이 유실된다. 이것을 막기 위해 각 유저들의 메시지를 임시 저장소에 복제시키는 방법을 사용할 수 있다. 유저가 온라인으로 돌아오면 복제본이 그들의 메시지를 다루기 위해 사용될 수 있다. 그리고 원본 서버의 장애가 해결되면 임시 저장소와 원본 메시지가 병합되어 고유한 새로운 저장소가 생성된다.
Optimizations
Latency
원활한 유저 경험을 위해 메신저 서비스는 실시간으로 동작해야 한다. 이는 최소한의 레이턴시를 가져야한다는 뜻인데, 자주 접근되는 데이터를 캐싱 처리하여 해결할 수 있다. 레디스와 같은 분산 캐시를 통해 유저의 활동 상태와 최근 채팅등을 메모리에 캐싱할 수 있다. 이것은 시스템의 퍼포먼스를 향상시키는데 크게 도움을 준다.
Availability
유저에게 최대한의 가용성을 중요하는것은 굉장히 중요하다. 내결함성을 가지기 위해 우리는 여러개의 transient 메시지 복제본을 가져갈 수 있다. 만약 어떠한 메시지라도 유실되면 손쉽게 복제본에서 다시 조회할 수 있다.