블라인드 스터디 : 분산 이메일 시스템 설계
서버개발

블라인드 스터디 : 분산 이메일 시스템 설계

[8장] 분산 이메일 시스템 설계

인터넷의 성장으로 이메일의 양이 늘어나고 활성 사용자도 크게 늘어 이메일 시스템에도 변화가 필요했다

복잡성과 규모 면에서 크게 달라졌으며, 다양한 기능을 갖추게 되었다

 

 

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

해당 시스템에는 10억 명의 사용자가 있으며 인증은 따로 존재하지 않는다 

사람당 하루에 평균 발신 이메일이 10건, 수신은 40건, 메타데이터는 50KB이라고 가정한다

첨부파일을 포함하는 이메일은 전체의 20%라고 가정하며 크기는 500KB이라고 가정한다

 

 

기능 요구사항

- 이메일 수신/발신

- 읽음 여부에 따른 이메일 필터링

- 제목, 발신인, 메일 내용에 따른 검색

- 스팸 및 바이러스 방지 기능

- HTTP를 사용한 연결

- 첨부파일 지원

 

 

비기능 요구사항

- 안정성 : 이메일 데이터는 소실 X

- 가용성 : 이메일과 사용자 데이터를 여러 노드에 자동으로 복제하여 가용성을 보장 및 장애 대응

- 확장성 : 사용자 수가 늘어나도 감당

- 유연성 : 새 컴포넌트를 더하여 쉽게 기능을 추가하고 성능을 개선

 

개략적인 규모 추정

하루에 보내는 이메일의 수는 평균 100억 건으로 QPS는 100,000이지만, 단일 국가라면 훨씬 몰리게 된다

평균적인 업무시간인 9시 ~ 21시까지 이메일을 보내게 된다면 200,000 건 정도로 추정된다

(한국에서는 새벽에 보내는 이메일은 예의가 아니므로..)

 

메타 데이터는 데이터베이스에 저장된다고 가정하며 1년간 메타데이터를 유지하기 위해 730PB가 필요하다

첨부 파일은 1년간 보관하는데 1460PB가 필요하다

 

많은 데이터가 처리되어야 하며 분산 데이터 베이스 솔루션이 필요하다

(보통 이메일 서비스들이 첨부파일을 장기간 보관하지 않으므로 이는 축소될 수 있다고 생각된다)

 

 

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

이메일 프로토콜

SMTP

Simple Mail Transfer Protocol의 준말로, 이메일을 한 서버에서 다른 서버로 보내는 표준 프로토콜이다

 

POP

이메일 클라이언트가 원격 메일 서버에서 이메일을 수신하고 다운로드 하기 위해 사용하는 표준 프로토콜이다

단말로 다운로드된 이메일은 서버에서 삭제되며, 한 대 단말에서만 이메일을 읽을 수 있다

이 프로토콜은 이메일을 일부만 읽을 수 없어 전부 내려 받아야만 가능해 용량이 큰 파일에서는 불편함이 따른다

 

IMAP

Internet Mail Access Protocol의 준말로, 원격 메일 서버에서 이메일을 수신하는데 사용하는 또 다른 표준 프로토콜이다

POP과 달리 클릭하지 않으면 메시지를 다운로드 하지 않으며, 메일 서버에서 지워지지도 않는다

따라서 여러 단말에서 이메일을 읽을 수 있어 개인 이메일 계정에서 가장 널리 사용된다

인터넷 속도가 느려도 잘 동작하는데, 이메일을 실제로 열기 전에는 헤더만 다운로드하기 때문이다

 

HTTPS

기술적으로 보자면 메일 전송 프로토콜은 아니나, 웹 기반 이메일 시스템의 메일함 접속에 이용될 수 있다

 

 

DNS

수신자 도메인의 메일 교환기 레코드 검색에 이용된다

우선순위가 높은 도메인이 먼저 사용되며 송신자 측 메일 서버는 이 메일 서버에 접속하여 메시지를 보내려고 시도한다

연결에 실패하면 그다음 우선 순위가 높은 메일 서버에 연결을 시도한다

 

 

첨부파일

이메일 첨부 파일은 이메일 메시지와 함께 전송되며 일반적으로 base64 인코딩을 사용한다

첨부 파일에는 보통 크기 제한이 있고, 개인/기업에 따라 차등을 두기도 한다

 

 

전통적 메일 서버

분산 메일 서버에 대해 알아보기 전 기존 메일 서버에 대해서 알아보자

보통 서버 한 대로 운용되며, 사용자가 많지 않을 때 잘 동작한다

 

사용자 A : 발신자 - 발신 메일 클라이언트 C, 사용자 B : 수신자 - 발신 메일 클라이언트 D

1. 사용자 A가 특정 클라이언트 C에 로그인하여 이메일을 작성하고 보내기 버튼을 누른다

이메일은 C 메일 서버로 전송되며 사이 통신 프로토콜은 SMTP이다

2. C 메일 서버는 DNS 질의를 통해 수신사(D) SMTP 서버 주소를 찾는다

주소를 조회한 뒤 해당 메일 서버로 이메일을 보낸다. 이 또한 SMTP가 이용된다

3. D 메일 서버는 이메일을 저장하고 수신자인 B가 읽어갈 수 있도록 한다

4. B가 D에 로그인하면 D 클라이언트는 IMAP/POP 서버를 통해 새 이메일을 가져온다

 

저장소

전통적 메일 서버는 이메일을 파일 시스템의 디렉토리에 저장한다

각각의 이메일은 고유한 이름을 가진 별도 파일에 보관하며 각 사용자의 설정 데이터와 메일함은 사용자 디렉터리에 보관한다

이는 수십억 개의 이메일을 검색하고 백업하는 목적으로는 활용하기 곤란하며 디스크 IO가 병목을 겪는다

또한, 가용성과 안정성도 만족하지 않고 디스크 손상이나 서버 장애가 언제든 발생할 수 있다

따라서 더 안정적인 분산 데이터 저장소 계층이 필요하다

 

 

분산 메일 서버

현대적 사용 패턴을 지원하고 확장성과 안정성 문제를 해결한다

 

분산 메일 서버 아키텍처

여러 서버 사이에 데이터를 동기화하는 것은 어려운 작업이고, 수신자 메일 서버에서 이메일이 스팸으로 잘못 분류되지 않도로 하려면 어려운 문제들을 해결하여야 한다

 

기본적으로 웹서버로 사용자가 이용하는 요청/응답 서비스 등의 API 요청을 처리하고, 실시간 서버(상태 유지 서버)를 웹소켓 혹은 롱폴링 등을 이용하며 새로운 이메일 내역을 전달한다 

웹소켓이 더 합리적이지만 브라우저 호환성 문제가 있어 롱 폴링도 고려해야 한다

SockJS와 같이 롱폴링과 웹소켓을 선택적으로 이용하는 것을 사용하면 합리적인 선택이 될 수 있다

 

메타데이터 데이터베이스

이메일 제목, 본문, 발신인, 수신인 목록 등의 메타 데이터를 저장하는 데이터베이스이다

 

첨부 파일 저장소

정적 파일이므로 아마존 S3와 같은 객체 저장소를 사용한다

S3는 이미지나 동영상 등의 대용량 파일을 저장하는데 적합한, 확장에 용이한 저장소 인프라다

시계열 데이터베이스인 카산드라도 로그와 같은 정적 정보를 저장하는데 사용되는데

이는 실질적으로 큰 파일을 지원하지 않으며 첨부 파일을 저장하면 레코드 캐시를 사용하기 어렵다

 

분산 캐시

최근에 수신된 이메일이 자주 읽을 가능성이 더 높으므로 클라이언트로 하여금 메모리에 캐시해 두도록 하면 메일을 표시하는 시간을 많이 줄일 수 있다

레디스와 같은 여러 자료구조를 지원하는 캐시 서버를 구성할 수 있다

 

검색 저장소

검색 저장소는 분산 문서 저장소다

고속 텍스트 검색을 지원하는 역 인덱스를 자료구조로 사용한다

 

 

이메일 전송 절차

웹 서버는 다음 역할을 담당한다

- 기본적인 이메일 검증

- 수신자 이메일 주소 도메인이 송신자 이메일 주소 도메인과 같은 경우 이메일 내용의 스팸 여부와 바이러스 감염 여부를 검사한 뒤 통과된 이메일은 송신자와 수신자의 편지함에 저장된다. 이후 로직이 필요 없어진다

 

메시지 큐는 다음 역할을 담당한다

- 큐에 넣기에 첨부 파일의 크기가 너무 큰 이메일의 경우 객체 저장소에 따로 저장하고, 해당 저장 위치에 대한 참조 정보만 저장한다

- 기본적인 검증에 실패한 메일은 에러 큐에 담는다

 

외부 전송 담당 SMTP 작업 프로세스는 큐에서 메시지를 꺼내어 이메일의 스팸 및 바이러스 감염 여부를 확인한다

검증 절차를 통과한 이메일은 저장소 계층 내의 보낸 편지함에 저장된다

외부 전송 담당 SMTP 작업 프로세스가 수신자의 메일 서버로 메일을 전송한다

 

메시지 큐에 보관되는 모든 메시지에는 이메일을 생성하는 데 필요한 모든 메타 데이터가 포함되어 있으며 비동기적 메일 처리를 가능하게 한다

또한 외부 메일 서버 전송용 SMTP 프로세스를 분리하여 독립적으로 규모를 조정할 수 있다

 

메일이 처리되지 않고 큐에 오랫동안 남아있다면 원인을 분석해야 한다

- 수신자 측 메일 서버에 장애 발생 : 나중에 메일을 다시 전송해야 한다

- 이메일을 보낼 큐의 소비자 수가 불충분 : 더 많은 소비자를 추가하여 처리 시간을 단축해야 한다

 

 

이메일 수신 절차 

이메일이 SMTP 서버의 로드 밸런서에 도착해 분산된 SMTP 서버로 나뉜다

이메일의 첨부 파일이 큐에 들어가기 너무 큰 경우 첨부 파일 저장소(S3)에 보관한다

이메일을 수신 이메일 큐에 넣어 결합도를 낮추며 수신되는 이메일의 양이 폭증하는 경우 버퍼 역할을 수행하도록 한다

 

메일 처리 프로세스는 스팸 메일을 걸러내고, 바이러스를 차단하는 역할을 한 뒤 이메일을 메일 저장소, 캐시, 객체 저장소 등에 보관한다

수신자가 온라인 상태인 경우 이메일을 실시간 서버로 전달하여 바로 웹소켓 등을 통해 전달하며, 오프라인 상태인 경우에는 저장소 계층에 보관하여 온라인 상태가 될 때 RESTful API를 통해 연결한다

 

 

3단계 : 상세 설계

메타 데이터  데이터베이스

이메일의 헤더는 일반적으로 작고, 빈번하게 이용된다

본문의 크기는 다양하지만 사용 빈도는 낮고, 일반적으로 이메일은 한 번만 읽힌다

이메일 가져오기, 읽은 메일 표시, 검색 등의 작업은 사용자 별로 격리 수행되어야 한다

사용자는 보통 최근 메일만 읽으므로 만들어진 지 오래된 데이터는 전체 질의에 일부이다

데이터 손실은 용납되지 않는다

 

 

데이터베이스 선정

RDBMS

이메일 헤더와 본문에 대한 인덱스를 만들어 두면 간단한 검색 질의는 빠르게 처리할 수 있으나, 데이터 크기가 크기 때문에 적합하지 않다

비정형 BLOB에 대해서는 검색 질의 성능이 나오지 않으며 많은 디스크 IO가 발생하기 때문에 적합하지 않다

 

분산 객체 저장소

이메일의 원시 데이터를 S3같은 객체 저장소에 보관하는 것으로 백업 데이터를 보관하기에는 좋지만 이메일의 읽음 표시, 키워드 검색, 이메일 타래 등의 기능을 구현하기에는 적합하지 않다

 

NoSQL

카산드라가 좋은 대안이 될 수도 있지만 사용된 업체가 없으며 성능적 튜닝이 필요하다

 

대체적으로 자체 데이터베이스 시스템을 만들어 사용되지만 이를 설계하는 것은 매우 어렵기 때문에 다음 조건만 충족할 수 있음을 보이자

- 단일 컬럼의 크기는 최대 한 자릿 수 MB이다

- 강력한 데이터 일관성을 보장한다

- 디스크 IO가 최소화 되어야 한다

- 가용성이 아주 높아야 하고 일부 장애를 감내할 수 있어야 한다

- 증분 백업이 쉬워야 한다

 

 

데이터 모델

user_id를 파티션 키로 사용하여 특정한 사용자의 데이터는 항상 같은 샤드에 보관하는 방법이 있다

이 방법은 여러 사용자에게 해당 메일의 내용을 공유할 수 없다는 단점이 있다

파티션 키와 클러스터 키를 가져 모든 노드에 분산되도록 하며 같은 파티션 내에서 정렬되도록 하여야 한다

 

특정 사용자의 모든 메일은 한 파티션 내에 있으므로 처리 순서가 보장된다

분산 서버로 인해 처리 메일의 순서가 보장되지 않는 문제를 해결할 수 있다

 

읽지 않은 메일을 확인하는데 NoSQL을 이용하여 읽은 메일과 읽지 않은 메일을 구분하여 저장한다(비정규화)

 

이메일 타래

이메일 타래란 모든 답장을 최초 메시지에 타래로 엮어 보여주는 기능으로 관련된 모든 메일을 확인할 수 있게 해준다

전통적으로 JMZ같은 알고리즘을 통해 구현되는데 중요하지 않으므로 넘어간다

 

 

일관성 문제

높은 가용성을 위해 레플리케이션에 의존하는 분산 데이터베이스는 데이터 일관성과 가용성 사이에서 타협을 하여야 한다

데이터 정확성이 아주 중요하므로, 모든 메일함은 반드시 하나의 주 사본을 통해 서비스되어야 하며 장애가 발생하면 다른 사본을 통해 주 사본이 복원될 때까지 동기화/갱신 작업을 완료하면 안된다

 

 

검색

이메일 검색은 보통 이메일 제목이나 본문에 특정 키워가 포함되어있는지 찾는다

검색 기능을 제공하려면 이메일이 전송, 수신, 삭제될 때마다 색인 작업을 수행하여 검색 기능이 눌리면 수행된다

쓰기 연산이 읽기 연산보다 많이 사용되기 때문에 하나의 방법으로 Elastic Search가 있다

 

Elastic Search

질의가 대부분 사용자의 이메일 서버에서 실행되므로, user_id를 파티션 키로 사용하여 같은 사용자의 이메일을 같은 노드에 묶어 놓는다

사용자는 검색 버튼을 누른 다음 결과가 수신될 때까지 기다리며 요청은 동기 방식으로 처리되어야 한다

이메일 전송/수신/삭제는 카프카에 의해 비동기적으로 수행되는 것에 반해 검색은 동기적으로 수행되므로 실제로 색인을 수행할 서비스와 결합도를 낮추었다

그러나 이로 인해 주 이메일 저장소와 동기화가 맞춰야 하는 미션이 있다