포스팅에 사용된 그림은 책에서 제공하는 그림들 입니다.
구글 드라이브, 드롭박스, 마이크로소프트의 원드라이브, 애플의 아이클라우드 등 클라우드 저장소 서비스는 높은 인기를 누리게 된 클라우드 서비스인데, 그 가운데 구글 드라이브 서비스를 설계해 보자.
구글 드라이브는 파ㄹ 저장 및 동기화 서비스로, 문서, 사진, 비디오, 기타 파일을 클라우드에 보관할 수 있고 이 파일들은 컴퓨터, 스마트폰, 태블릿 등 어떤 단말에서도 이용 할 수 있다. 그리고 보관된 파일은 다른사람과 손쉽게 공유할 수 있다.
요구사항을 도출해보자.
모든 것을 담은 단일 서버로 부터 출발해 점진적으로 천만명 사용자 지원이 가능한 시스템으로 발전시켜보자.
우선 아래와 같은 단일 서버로 시작해보자
웹 서버는 아파치, 데이터베이스는 MYSQL로 설치하고, 업로드되는 파일을 저장할 drive/라는 디렉터리를 준비하자. drive/ 디렉터리 안에는 네임스페이스라 불리는 하위 디렉터리들을 둔다. 각 네임스페이스 안에는 특정 사용자가 올린 파일이 보관된다. 이 파링들은 원래 파일과 같은 이름을 갖는다. 각 파일과 폴더는 그 상대 경로를 네임스페이스 이름과 결합하면 유일하게 식별해 낼 수 있다.
아래 그림은 drive/ 디렉터리에 실제 파일이 보관된 그림이다.
이 시스템은 어떤 API들을 제공해야 할까? 기본적으로 세 가지 API가 필요한데, 파일 업로드 API, 다운로드 API, 파일 갱신 히스토리 제공 API다
이 시스템은 두 가지 종류의 업로드를 지원한다.
이어 올리기 API의 예
https://api.example.com/files/upload?uploadType=resumable
인자
이어 올리기는 다음 세 단계 절차로 이루어진다.
API의 예
https://api.example.com/files/download
인자
예
{
“path”: “/recipes/soup/best_soup.txt”
}
API의 예
https://api.exmaple.com/files/list_revisions
인자
예
{
“path”: “/recipes/soup/best_soup.txt”,
“limit”: 20
}
3개 나열한 API는 모든 사용자 인증을 필요로 하고 HTTPS 프로토콜을 사용해야 한다. SSL를 지원하는 프로토콜을 이용하는 이유는 클라이언트와 백엔드 서버가 주고받는 데이터를 보호하기 위한 것이다.
업로드되는 파일이 많아지다 보면 결국에는 파일 시스템은 가득 차게 된다.
위 그림의 파일 시스템은 딱 10MB의 여유공간밖에는 남지 않은 상태다. 이렇게 되면 사용자는 더 이상 파일을 올릴 수 없게 되므로, 긴급히 문제를 해결해야 한다. 가장 먼저 떠오르는 해결책은 데이터를 샤딩하여 여러 서버에 나누어 저장하는 것이다. 아래그림은 user_id를 기준으로 샤딩한 예다
위 그림처럼 데이터베이스를 샤딩 해두어도 서버에 장애가 생기면 데이터를 잃게 되지 않을까 여전히 걱정할 것이다. 이의 해결책은 아마존S3를 사용하는것이 가장 적합할 것이다. 아마존 S3는 업계 최고 수준의 규모 확장성, 가용성, 보안, 성능을 제공하는 객체 저장소 서비스다.
S3는 다중화를 지원하는데, 같은 지역 안에서 다중화를 할 수도 있고 여러 지역에 걸쳐 다중화를 할 수도 있다. AWS 서비스 지역은 아마존 AWS가 데이터 센터를 운영하는 지리적 영역이다. 아래 그림에 나오듯이, 데이터를 다중화 할 때는 같은 지역 안에서만 할 수도 있고 여러 지역에 걸쳐 할 수도 있다. 여러 지역에 걸쳐 다중화하면 데이터 손실을 막고 가용성을 최대한 보장할 수 있으므로 그렇게 하기로 하고, S3 버킷은 마치 파일 시스템의 폴더와도 같은 것이다.
파일을 S3에 넣고 나니 이제 데이터 손실 걱정은 사라졌다. 미래에 비슷한 문제가 벌어지는 것을 막기 위해, 개선할 부분들을 살펴보자.
이 모든 부분을 개선하고 나면 웹 서버, 메타데이터 데이터베이스, 파일 저장소가 한 대 서버에서 여러 서버로 잘 분리 되었을 것이다. 아래 그림은 수정한 설계안이다.
구글 드라이브 같은 대형 시스템의 경우 때때로 동기화 충돌이 발생할 수 있다. 두 명 이상의 사용자가 같은 파일이나 폴더를 동시에 업데이트하려고 하는 경우다. 이런 충돌은 어떻게 해소할 수 있을까? 먼저 처리되는 변경은 성공한 것으로 보고, 나중에 처리되는 변경은 충돌이 발생한 것으로 표시하면 해소할 수 있을 것이다.
위 그림에서 사용자 1과 2는 같은 파일을 동시에 갱신하려 한다. 하지만 이 시스템은 사용자 1의 파일을 먼저 처리했다. 따라서 사용자 1의 파일 갱신 시도는 정상적으로 처리되었지만 사용자 2에 대해서는 동기화 충돌 오류가 발생할 것이다. 이 오류는 어떻게 해결할까? 오류가 발생한 시점에 이 시스템에는 같은 파일의 두 가지 버전이 존재하게 된다. 즉, 사용자 2가 가지고 있는 로컬 사본과 서버에 있는 최신 버전이다. 이 상태에서 사용자는 두 파일을 하나로 합칠지 아니면 둘 중 하나를 다른 파일로 대체할지를 결정하면 된다.
여러 사용자가 같은 문서를 편집할 때 발생할 수 있는 동기화 문제를 해결하는 것은 흥미로운 과제다.
아래그림은 이번 면접 문제에 대한 개략적 설계안이다. 지금부터 각각의 컴포넌트에 대해 조금 더 상세히 알아보자.
사용자 단말: 사용자가 이용하는 웹브라우저나 모바일 앱 등의 클라이언트.
블록 저장소 서버: 파일 블록을 클라우드 저장소에 업로드하는 서버다. 블록 저장소는 블록 수준 저장소라고도 하며, 클라우드 환경에서 데이터 파일을 저장하는 기술이다. 이 저장소는 파일을 여러개의 블록으로 나눠 저장하며, 각 블록에는 고유한 해시값이 할당된다. 이 해시값은 메타데이터 데이터베이스에 저장된다. 각 블록은 독립적인 객체로 취급되며 클라우드 저장소 시스템에 보관된다. 파일을 재구성하려면 블록들을 원래 순서대로 합쳐야 한다. 예시한 설계안의 경우 한 블록은 드롭박스의 사례를 참고하여 4MB로 정했다.
블록 저장소 서버, 메타데이터 데이터베이스, 업로드 절차, 다운로드 절차, 알림 서비스, 파일 저장소 공간 및 장애 처리 흐름에 대해 좀 더 자세히 알아보자.
정기적으로 갱신되는 큰 파일들은 업데이트가 일어날 때마다 전체 파일을 서버로 보내면 네트워크 대역폭을 많이 잡아먹는다. 이를 최적화 할때 사용할 수 있는 두 가지 방법을 알아보자
이 시스템에서 블록 저장소 서버는 파일 업로드에 관계된 힘든 일을 처리하는 컴포넌트다. 클라이언트가 보낸 파일을 블록 단위로 나누고, 각 블록에 압축 알고리즘을 적용해야 하고, 암호화까지 해야한다. 아울러 전체 파일을 저장소 시스템으로 보내는 대신 수정된 블록만 전송해야 한다.
새 파일이 추가되었을 때 블록 저장소 서버가 어떻게 동작하는지는 아래 그림에 나와 있다.
아래 그림은 델타 동기화 전략이 어떻게 동작하는지를 보여준다. 검정색 블록은 수정된 블록이다. 갱신된 부분만 동기화해야 하므로 이 두 블록만 클라우드 저장소에 업로드하면 된다.
블록 저장소 서버에 델타 동기화 전략과 압축 알고리즘을 도입하였으므로, 네트워크 대역폭 사용량을 감소할 수 있다.
이 시스템은 강한 일관성 모델을 기본으로 지원해야 한다. 같은 파일이 단말에 따라 다르게 보이는 것은 허용할 수 없다는 뜻이다. 메타데이터 캐시와 데이터베이스 계층에도 같은 원칙이 적용되어야 한다.
메모리 캐시는 보통 최종 일관성 모델을 지원한다. 따라서 강한 일관성을 달성하려면 다음 사항을 보장해야 한다.
관계형 데이터베이스는 ACID를 보장하므로 강한 일관성을 보장하기 쉽다. 하지만 NoSQL 데이터베이스는 이를 기본으로 지원하지 않으므로, 동기화 로직 안에 프로그램해 넣어야 한다.
아래그림은 이 데이터베이스의 스키마 설계안이다. 중요한 것만 간추린, 아주 단순화된 형태의 스키마임에 유의하자.
user: user 테이블에는 이름, 이메일, 프로파일 사진 등 사용자에 관계된 기본적 정보들이 보관된다
device: device 테이블에는 단말 정보가 보관된다. push_id는 모바일 푸시 알림을 보내고 받기 위한 것이다. 한 사용자가 여러 대의 단말을 가질 수 있음에 유의해야 한다
namespace: namespace 테이블에는 사용자의 루트 디렉터리 정보가 보관된다.
file: file 테이블에는 파일의 최신 정보가 보관된다.
file_version: 파일의 갱신 이력이 보관되는 테이블. 이 테이블에 보관되는 레코드는 전부 읽기 전용이다. 갱신 이력이 훼손되는 것을 막기 위한 조치
block: 파일 블록에 대한 정보를 보관하는 테이블. 특정 버전의 파일은 파일 블록을 올바른 순서로 조합하기만 하면 복원 가능
사용자가 파일을 업로드하면 무슨 일이 벌어지는지 상세하게 알아보자.
첫 번째 요청은 파일 메타데이터를 추가하는 것이고, 두 번째 요청은 파일을 클라우드 저장소로 업로드하기 위한 요청이다. 이 두 요청은 전부 클라이언트 1이 보낸 것이다.
파일을 수정하는 경우에도 흐름은 비슷하다.
파일 다운로드는 팡리이 새로 추가되거나 편집되면 자동으로 시작된다. 그렇다면 클라이언트는 다른 클라이언트가 파일을 편집하거나 추가했다는 사실을 어떻게 감지할까? 두 가지 방법이 있다.
어떤 파일이 변경되었음을 감지한 클라이언트는 우선 API 서버를 통해 메타데이터를 새로 가져가야 하고, 그 다음에 블록들을 다운받아 파일을 재구성해야 한다. 아래 그림은 자세한 흐름을 보여준다. 지면 한계상 가장 중요한 컴포넌트들만 그렸음을 유의하자.
파일의 일관성을 유지하기 위해, 클라이언트는 로컬에서 파일이 수정되었음을 감지하는 순간 다른 클라이언트에게 그 사실을 알려서 충돌 가능성을 줄여야 한다. 알림 서비스는 그 목적으로 이용된다. 단순하게 보자면 알림 서비스는 이벤트 데이터를 클라이언트로 내는 서비스다. 따라서 다음 두가지 정도의 선택지가 있다.
둘 다 좋은 방안이지만 본 설계안의 경우에는 롱 폴링을 사용할 것인데 이유는 다음과 같다.
롱 폴링 방안을 쓰게 되면 각 클라이언트는 알림 서버와 롱 폴링용 연결을 유지하다가 특정 파일에 대한 변경을 감지하면 해당 연결을 끊는다. 이때 클라이언트는 반드시 메타데이터 서버와 연결해 파일의 최신 내역을 다운로드 해야한다. 해당 다운로드 작업이 끝났거나 연결 타임아웃 시간에 도달한 경우에는 즉시 새 요청을 보내어 롱 폴링 연결을 복원하고 유지해야 한다.
파일 갱신 이력을 보존하고 안정성을 보장하기 위해서는 파일 여러 버전을 여러 데이터센터에 보관할 필요가 있다. 그런 상황에서 모든 버전을 자주 백업하게 되면 저장용량이 너무 빨리 소진될 가능성이 있다. 이런 문제를 피하고 비용을 절감하기 위해서는 보통 아래 세 가지 방법을 사용한다.
장애는 대규모 시스템이라면 피할 수 없는 것이다. 설계 시 이 점을 반드시 고려해야 한다.
높은 수준의 일관성, 낮은 네트워크 지연, 그리고 빠른 동기화가 요구되는 구글 드라이브 시스템을 설계해 보았다
이런 유용한 정보를 나눠주셔서 감사합니다.