- 지원해야 할 기능
- 파일 업로드/다운로드, 파일 동기화, 알림
- 모바일앱, 웹 앱 둘 다 지원
- 파일 암호화 지원
- 파일 크기 10GB 제한
- 가입 사용자: 오천만명
- 일간 능동 사용자(DAU) 기준: 천만명
- 파일 추가(drag-and-drop)
- 파일 다운로드
- 여러 단말에 파일 동기화
- 파일 갱신 이력 조회
- 파일 공유
- 파일이 편집/삭제/공유되었을 때 알림
- 지원하지 않을 기능: 구글 문서 편집 및 협업 기능
- 비 기능적 요구사항
- 안정성: 저장소 시스템이므로 데이터 손실이 발생하면 안됨
- 빠른 동기화 속도
- 네트워크 대역폭: 특히 모바일 데이터 플랜을 사용하는 경우라면 좋아하지 않을 것
- 규모 확장성: 많은 양의 트래픽 처리 가능
- 높은 가용성: 장애가 발생하더라도 시스템은 계속 사용 가능해야 함
- 모든 사용자에게 10GB의 무료 저장공간 할당
- 매일 각 사용자가 평균 2개의 파일을 업로드 (각 파일의 평균 크기 500KB)
- 읽기:쓰기 비율은 1:1
- 필요한 저장공간 총량 = 5천만 사용자 X 10GB = 500페타바이트
- 업로드 API QPS(Query Per Second) = 1천만 사용자 X 2회 업로드 / 24시간 / 3600초 = 약 240
- 최대 QPS = QPS X 2 = 480
-
아래와 같은 구성의 서버 한 대로 시작
- 파일을 올리고 다운로드 하는 과정을 처리할 웹 서버
- 사용자 데이터, 로그인 정보, 파일 정보 등의 메타데이터를 보관할 데이터베이스
- 파일을 저장할 저장소 시스템. 파일 저장을 위해 1TB 공간 사용
-
준비하기
- 아파치 웹 서버 설치
- MySQL 데이터베이스 설치
- 업로드되는 파일을 저장할 drive 디렉터리 생성
- 각 네임스페이스 안에는 특정 사용자가 올린 파일이 보관됨
- 각 파일들은 원래 파일과 같은 이름을 가짐
- 각 파일과 폴더는 그 상대 경로를 네임스페이스 이름과 결합하면 유일하게 식별 가능
-
파일 업로드 API (두 가지 종류의 업로드를 지원)
- 단순 업로드: 파일 크기가 작을 때 사용
- 이어 올리기: 파일 사이즈가 크고 네트워크 문제로 업로드가 중단될 가능성이 높다고 생각될 때 사용
- 예) https://api.example.com/files/upload?uploadType=resumable
- 인자 - uploadType=resumable, data: 업로드할 로컬 파일
- 이어 올리기의 세 단계 절차
- 이어 올리기 URL 을 받기 위한 최초 요청 전송
- 데이터를 업로드하고 업로드 상태 모니터링
- 업로드 장애가 발생하면 장애 발생 시점부터 업로드를 재시작
- 예) https://api.example.com/files/upload?uploadType=resumable
-
파일 다운로드 API
-
예) https://api.example.com/files/download
- 인자 - path: 다운로드할 파일의 경로
{ "path": "/recipes/soup/best_soup.txt" }
-
-
파일 갱신 히스토리 제공 API
-
예) https://api.example.com/files/list_revisions
- 인자 - path: 갱신 히스토리를 가져올 파일의 경로, limit: 히스토리 길이의 최대치
{ "path": "/recipes/soup/best_soup.txt", "limit": 20 }
-
- 모든 API는 사용자 인증을 필요로 하며, HTTPS 프로토콜을 사용해야 함
- SSL(Secure Socket Layer)는 클라이언트와 백엔드 서버가 주고받는 데이터를 보호하기 위해 사용
-
용량이 부족한 경우 가장 먼저 떠오르는 해결책은 데이터를 샤딩하여 어러 서버에 나누어 저장하는 것
- 서버에 장애가 생기면 데이터를 잃게 될 가능성이 높음
-
넷플릭스나 에어비엔비는 아마존 S3(Simple Storage Service, 업계 최고 수준의 규모 확장성, 가용성, 보안, 성능을 제공하는 객체 저장소 서비스)를 사용
- S3는 다중화를 지원. 같은 지역 혹은 여러 지역에 걸쳐 다중화 가능
- AWS 서비스 지역(region)은 아마존 AWS가 데이터 센터를 운영하는 지리적 영역
- 여러 지역에 걸쳐 다중화하면 데이터 손실을 막고 가용성을 최대한 보장할 수 있음
- S3 버킷(bucket)은 파일 시스템 폴더와 비슷함
-
S3에 파일을 넣은 이후 개선할 부분을 좀 더 찾아보기
- 로드밸런서: 네트워크 트래픽 분산 및 장애 발생 시 장애가 발생한 서버 우회
- 웹 서버: 로드밸런서를 추가하면 더 많은 웹 서버를 추가하여 트래픽 폭증에도 쉽게 데응 가능
- 메타데이터 데이터베이스: 데이터베이스를 파일 저장 서버에서 분리하여 SPOF(Single Point of Failure) 회피, 다중화 및 샤딩 정책을 적용하여 가용성과 규모 확장성 요구사항에 대응
- 파일 저장소: S3를 파일 저장소로 사용하고, 가용성과 데이터 무손실을 보장하기 위해 두 개 이상의 지역에 데이터 다중화
-
두 명 이상의 사용자가 같은 파일이나 폴더를 동시에 업데이트 하려고 하는 경우 먼저 처리되는 변경은 성공으로, 나중에 처리되는 변경은 충돌이 발생한 것으로 표시
- 사용자 단말: 사용자가 이용하는 웹브라우저나 모바일 앱 등의 클라이언트
- 블록 저장소 서버(block server), 블록 수준 저장소 서버(block-level-storage)
- 파일 블록을 클라우드 저장소에 업로드하는 서버
- 클라우드 환경에서 데이터 파일을 저장하는 기술
- 파일을 여러개의 블록으로 나눠 저장하며, 각 블록에는 고유한 해시값이 할당됨.
- 해시값은 메타데이터 데이터베이스에 저장됨
- 각 블록은 독립적인 객체로 취급되며 클라우드 저장소 시스템에 보관됨
- 파일을 재구성하려면 블록들을 원래 순서대로 합쳐야 함
- 여기서는 한 블록을 4MB로 정함 (드롭박스 사례 참고)
- 클라우드 저장소: 파일은 블록 단위로 나눠져 클라우드 저장소에 보관됨
- 아카이빙 저장소: 오랫동안 사용되지 않은 비활성 데이터를 저장하기 위한 컴퓨터 시스템
- 로드밸런서: 요청을 모든 API 서버에 고르게 분산하는 역할
- API 서버: 파일 업로드 외에 거의 모든 것을 담당하는 서버
- 사용자 인증, 사용자 프로파일 관리, 파일 메타데이터 갱신 등
- 메타데이터 데이터베이스: 사용자, 파일, 블록, 버전 등의 메타디에터 정보를 관리
- 실제 파일은 클라우드에 보관하며, 이 데이터베이스에는 오직 메타데이터만 저장
- 메타데이터 캐시: 성능을 높이기 위해 자주 쓰이는 메타데이터를 캐시하기 위한 용도
- 알림 서비스: 특정 이벤트가 발생했음을 클라이언트에게 알리는데 쓰이는 발생/구독 프로토콜 기반 시스템
- 클라이언트에게 파일 추가/편집/삭제되었음을 알려 파일의 최신 상태를 확인하도록 함
- 오프라인 사용자 백업 큐: 클라이언트가 접속 중이 아니어서 파일의 최신 상태를 확인할 수 없을 때 사용
-
큰 파일들은 업데이트가 일어날 때마다 전체 파일을 서버로 보내면 네트워크 대역폭을 많이 잡아먹음
- 델타 동기화: 파일이 수정되면 전체 파일 대신 수정이 일어난 블록만 동기화
- 압축: 블록 단위로 압축해 두면 크기를 많이 줄일 수 있음
- 압축 알고리즘은 파일 유형에 따라 정함
- 텍스트 파일: gzip or bzip
- 이미지, 비디오: 다른 압축 알고리즘 사용
- 압축 알고리즘은 파일 유형에 따라 정함
-
새 파일이 추가되었을 때
- 주어진 파일들을 작은 블록들로 분할
- 각 블록들을 압축
- 클라우드 저장소로 보내기 전에 암호화
- 클라우드 저장소로 저장
-
델타 동기화 전략
- 검정색으로 표시된 블록 2와 5는 수정된 블록
- 갱신만 부분만 동기화해야하므로 이 두 블록만 클라우드 저장소에 업로드
- 강한 일관성(strong consistency) 모델을 기본으로 지원해야 함
- 같은 파일이 단말이나 사용자에 따라 다르게 보이는 것은 허용할 수 없음
- 메타데이터 캐시와 데이터베이스 계층에도 같은 원칙이 적용되어야 함
- 메모리 캐시는 보통 최종 일관성(eventual consistency) 모델을 지원
- 강한 일관성 모델을 달성하기 위해 보장해야할 사항
- 캐시에 보관된 사본과 데이터베이스에 있는 원본이 일치
- 데이터베이스에 보관된 원본에 변경이 발생하면 캐시에 있는 사본을 무효화
- 관계형 데이터베이스는 ACID(Atomicity, Consistency, Isolation, Durability)를 보장하므로 강한 일관성을 보장
- NoSQL 데이터베이스는 이를 기본으로 지원하지 않으므로, 동기화 로직 안에 프로그램해 넣어야 함
- 본 설계안에서는 ACID를 기본 지원하는 관계형 데이터베이스를 채택하여 높은 일관성 요구사항에 대응
(중요한 것만 간추린 아주 단순화된 형태의 스키마임에 유의)
- user: 이름, 이메일, 프로파일 등 사용자에 관계된 기본적인 정보들
- device: 단말 정보 보관. push_id는 모바일 푸시 알림을 보내고 받기 위한 것. 한 사용자는 여러대의 단말을 가질 수 있음
- namespace: 사용자의 루트 디렉토리 정보가 보관됨
- file: 파일의 최신 정보가 보관됨
- file_version: 파일의 갱신 이력이 보관되는 테이블. 이 테이블에 보관되는 레코드는 갱신 이력이 훼손되는 것을 막기 위해 전부 읽기 전용
- block: 파일 블록에 대한 정보를 보관하는 테이블. 특정 버전의 파일은 파일 블록을 올바른 순서로 조합하기만 하면 복원해 낼 수 있음
- 두 개 요청이 병렬적으로 전송된 상황 (두 요청 모두 클라이언트 1이 전송)
- 첫 번째 요청: 파일 메타데이터 추가
- 두 번째 요청: 파일을 클라우드 저장소로 업로드하기 위한 것
- 파일 메타데이터 추가
- 클라이언트 1이 새 파일의 메타데이터를 추가하기 위한 요청 전송
- 새 파일의 메타데이터를 데이터베이스에 저장하고 업로드 상태를 대기중으로 변경
- 새 파일이 추가되었음을 알림 서비스에 통지
- 알림 서비스는 관련된 클라이언트(클라이언트 2)에게 파일이 업로드되고 있음을 알림
- 파일을 클라우드 저장소에 업로드
- 클라이언트 1이 파일을 블록 저장소 서버에 업로드
- 블록 저장소 서버는 파일을 블록 단위로 쪼갠 다음 압축하고 암호화한 다음에 클라우드 저장소에 전송
- 업로드가 끝나면 클라우드 스토리지는 완료 콜백을 호출, 이 콜백 호출은 API 서버로 전송됨
- 메타데이터 DB에 기록된 해당 파일의 상태를 완료로 변경
- 알림 서비스에 파일 업로드가 끝났음을 통지
- 알림 서비스는 관련된 클라이언트(클라이언트 2)에게 파일 업로드가 끝났음을 통지
- 파일을 수정하는 경우에도 흐름은 비슷함
- 파일 다운로드는 파일이 새로 추가되거나 편집되면 자동으로 시작됨
- 클라이언트는 다른 클라이언트가 파일을 편집하거나 추가했다는 사실을 감지하는 방법
- 클라이언트 A가 접속 중이고 다른 클라이언트가 파일을 변경하면 알림 서비스가 클라이언트 A에게 변경이 발생했으니 새 버전을 끌어가야 한다고 알림
- 클라이언트 A가 접속 중이 아니면 데이터는 캐시에 보관되고, 해당 클라이언트가 접속 중으로 바뀌면 그때 해당 클라이언트는 새 버전을 가져감
- 클라이언트는 다른 클라이언트가 파일을 편집하거나 추가했다는 사실을 감지하는 방법
- 파일이 변경되었음을 감지한 클라이언트의 상황
- 우선 API 서버를 통해 메타데이터를 가져가야 함
- 블록들을 다운받아 파일을 재구성해야 함
- 파일 다운로드
- 알림 서비스가 클라이언트 2에게 누군가 파일을 변경했음을 알림
- 알림을 확인한 클라이언트 2는 새로운 메타데이터를 요청
- API 서버는 메타데이터 데이터베이스에게 새 메타데이터 요청
- API 서버에게 새 메타데이터가 반환됨
- 클라이언트 2 에게 새 메타데이터가 반환됨
- 클라이언트 2 는 새 메타데이터를 받는 즉시 블록 다운로드 요청 전송
- 블록 저장소 서버는 클라우드 저장소에서 블록 다운로드
- 클라우드 저장소는 블록 서버에 요청된 블록 반환
- 블록 저장소 서버는 클라이언트에게 요청된 블록 반환. 클라이언트 2는 전송된 블록을 사용하여 파일 재구성
- 알림 서비스의 목적은 파일의 일관성을 유지하기 위해 존재
- 클라이언트는 로컬에서 파일이 수정되었음을 감지하는 순간 다른 클라이언트에게 그 사실을 알려서 충돌 가능성을 줄임
- 알림 서비스는 단순히 이벤트 데이터를 클라이언트들로 보내는 서비스
- 알림 서비스 방식
-
롱 폴링: 드롭박스가 채택한 방식 ✅
- 각 클라이언트는 알림 서버와 롱 폴링용 연결을 유지하다가 특정 파일에 대한 변경을 감지하면 연결을 끊음
- 클라이언트는 반드시 메타데이터 서버와 연결해 파일의 최신 내역을 다운로드 해야 함
- 해당 다운로드 작업이 끝났거나 연결 타임아웃 시간에 도달한 경우 새 요청을 보내어 롱 폴링 연결을 복원하고 유지해야 함
-
웹 소켓: 클라이언트와 서버 사이에 지속적인 통신 채널을 제공하여 양방향 통신이 가능
- 채팅서비스와 달리 본 시스템은 알림 서비스와 양방향 통신이 필요하지 않음
- 서버는 파일이 변경된 사실을 클라이언트에게 알려줘야 하지만 반대 방향의 통신이 요구되지 않음
- 웹 소켓은 실시간 양방향 통신이 요구되는 채팅 같은 시스템에 적합함
- 알림을 보낼 일이 그렇게 자주 발생하지 않음
- 알림을 보내야 할 경우에도 단시간에 많은 양의 데이터를 보낼 일이 없음
-
- 파일 갱신 이력을 보존하고 안정성을 보장하기 위해서는 파일의 여러 버전을 여러 데이터센터에 보관해야 함
- 모든 버전을 자주 백업하게 되면 저장용량이 너무 빨리 소진됨
- 저장소 공간을 절약하기 위한 세 가지 방법
- 중복 제거(de-dupe)
- 중복된 파일 블록을 계정 차원에서 제거하는 방법
- 두 블록이 같은 블록인지는 해시 값을 비교하여 판단
- 지능적 백업 전략 도입
- 한도 설정: 보관해야 하는 파일 버전 개수에 상한을 둠. 상한에 도달하면 오래된 버전은 버림
- 중요한 버전만 보관: 중요한 것만 골라내서 보관
- 아카이빙 저장소
- 자주 쓰이지 않는 데이터를 저장하는 저장소
- 몇달 혹은 수년간 이용되지 않은 데이터가 이에 해당
- 아마존 S3 글래시어(glacier) 같은 아카이빙 저장소 이용료는 S3보다 훨씬 저렴
- 중복 제거(de-dupe)
- 로드밸런서 장애
- 로드밸런서에 장애가 발생하면 secondary 로드밸런서가 활성화되어 트래픽을 이어받아야 함
- 로드밸런서끼리는 보통 heartbeat 신호를 주기적으로 보내서 상태를 모니터링함
- 일정 시간동안 박동 신호에 응답하지 않은 로드밸런서는 장애가 발생한 것으로 간주함
- 블록 저장소 서버 장애
- 블록 저장소 서버에 장애가 발생한 경우 다른 서버가 미완료 상태 또는 대기 상태인 작업을 이어받아야 함
- 클라우드 저장소 장애
- S3 버킷은 여러 지역에 다중화할 수 있으므로, 한 지역에서 장애가 발생하였다면 다른 지역에서 파일을 가져옴
- API 서버 장애
- API 서버들은 무상태 서버
- 로드밸런서는 API 서버에 장애가 발생하면 트래픽을 해당 서버로 보내지 않음으로써 장애 서버를 격리
- 메타데이터 캐시 장애
- 메타데이터 캐시 서버도 다중화가 필요
- 한 노드에 장애가 생겨도 다른 노드에서 데이터를 가져올 수 있음
- 장애가 발생한 서버는 새 서버로 교체
- 메타데이터 데이터베이스 장애
- 주 데이터베이스 서버 장애: 부 데이터베이스 서버 가운데 하나를 주 데이터베이스 서버로 바꾸고, 부 데이터베이스 서버를 새로 하나 추가함
- 부 데이터베이스 서버 장애: 다른 부 데이터베이스 서버가 읽기 연산을 처리하도록 하고 장애 서버는 교체
- 알림 서비스 장애
- 접속 중인 모든 사용자는 알림 서버와 롱 폴링 연결을 하나씩 유지
- 알림 서비스는 많은 사용자와의 연결을 유지하고 관리해야 함
- 드롭박스에서는 알림 서비스 서버 한가 관리하는 연결의 수는 1백만 개가 넘음
- 한 대 서버에 장애가 발생하면 백만명 이상의 사용자가 롱 폴링 연결을 다시 만들어야 함
- 한 대 서버로 백만 개 이상의 접속 유지는 가능하지만, 동시에 백만 개 접속을 시작하는 것은 불가능
- 롱 폴링 연결을 복구하는 것은 상대적으로 느릴 수 있음
- 오프라인 사용자 백업 큐 장애
- 큐 역시 다중화
- 큐에 장애가 발생하면 구독 중인 클라이언트들은 백업 큐로 구독 관계를 재설정해야 함
- 구글 드라이브 시스템 설계
- 높은 수준의 일관성
- 낮은 네트워크 지연
- 빠른 동기화 요구
- 크게 두 가지 부분으로 분리
- 파일의 메타데이터를 관리
- 파일 동기화를 처리하는 부분
- 알림 서비스는 롱 폴링을 사용하여 클라이언트로 하여금 파일의 상태를 최신으로 유지하도록 함
- 정답은 없음, 회사마다 요구하는 제약조건에 따라 설계를 진행해야 함
- 설계하는 과정에서 내린 결정과 선택한 기술들에 어떤 생각이 있었는지 잘 설명하는 것이 좋음
- 시간이 남는다면 다른 선택지가 있었는지 논의해 보기
- 블록 저장소 서버를 거치지 않고 파일을 클라우드 저장소에 직접 업로드 한다면?
- 장점: 파일 전송을 클라우드 저장소로 직접 하므로 업로드 시간이 빨라짐
- 단점
- 분할, 압축, 암호화 로직을 클라이언트에 두어야 하므로 플랫폼별로 따로 구현해야 함 (이 설계안에서는 이 모두를 블록 저장소 서버라는 곳에 모아 둠)
- 클라이언트가 해킹 당할 수 있으므로 암호화 로직을 클라이언트 안에 두는 것은 적절하지 않음
- 접속 상태를 관리하는 로직을 별도 서비스로 옮기기
- 관련 로직을 알림 서비스에서 분리해 내면 다른 서비스에서도 쉽게 활용할 수 있음
- 블록 저장소 서버를 거치지 않고 파일을 클라우드 저장소에 직접 업로드 한다면?