Search

가상 면접 사례로 배우는 대규모 시스템 설계 기초 1

1장. 사용자 수에 따른 규모 확장성

웹 계층의 확장

수직적 규모 확장 vs 수평적 규모 확장
소위 스케일 업이라고도 하는 수직적 규모 확장 프로세스는 서버에 고사양 자원(CPU, RAM)을 추가하는 행위를 말한다.
반면 스케일 아웃이라고도 하는 수평적 규모 확장 프로세스는 더 많은 서버를 추가하여 성능을 개선하는 행위를 말한다.
서버로 유입되는 트래픽의 양이 적을 때는 수직적 확장이 좋은 선택이며, 이 방법의 가장 큰 장점은 단순함이다.
그러나 불행하게도 이 방법에는 몇 가지 심각한 단점이 있다.
수직적 규모 확장에는 한계가 있으며, 한 대의 서버에 CPU나 메모리를 무한대로 증설할 방법은 없다.
수직적 규모 확장법은 장애에 대한 자동복구(failover) 방안이나 다중화(re-dundancy) 방안을 제시하지 않으며, 서버에 장애가 발생하면 웹사이트/앱은 완전히 중단된다.
이런 단점 때문에 대규모 어플리케이션을 지원하는 데는 수평적 규모 확장법이 보다 적절하다.
로드밸런서
사용자는 웹/모바일 트래픽 처리용 웹 서버에 바로 연결되며, 너무 많은 사용자가 접속하여 웹 서버가 한계 상황에 도달하게 되면 응답 속도가 느려지거나 서버 접속이 불가능해질 수도 있다.
로드밸런서는 부하 분산 집합에 속한 웹 서버들에게 트래픽 부하를 고르게 분산하는 역할을 한다.
또한 보안을 위해 사용자는 로드밸런서의 공개 IP 주소로 접속 하고 서버 간 통신에는 사설 IP 주소로 접속하며, 웹 서버는 클라이언트의 접속을 직접 처리하지 않는다.
부하 분산 집합에 또 하나의 웹 서버를 추가하고 나면 장애를 자동복구하지 못하는 문제는 해소되며, 웹 계층의 가용성은 향상된다.
예를 들면 다음과 같은 상황들이 있다.
서버 1이 다운되면 모든 트래픽은 서버 2로 전송되므로 사이트 전체가 다운되는 일이 방지되고, 부하를 나누기 위해 새로운 서버를 추가할 수도 있다.
웹사이트로 유입되는 트래픽이 가파르게 증가하다 감당할 수 없는 시점이 오면, 웹 서버 계층에 더 많은 서버를 추가하여 로드밸런서가 자동적으로 트래픽을 분산하게끔 할 수도 있다.
무상태(stateless) 웹 계층
웹 계층을 수평적으로 확장하려면 상태 정보(사용자 세션 데이터와 같은)를 웹 계층에서 제거하여야 한다.
상태 의존적인 웹 계층에서 동일한 클라이언트로부터의 요청은 항상 같은 서버로만 전송되어야 하며 다른 서버로 전송된다면 인증에 실패할 것이다.
대부분의 로드밸런서가 이를 지원하기 위해 고정 세션이라는 기능을 제공하지만, 로드밸런서에게 부담을 주고 뒷단에 서버를 추가하거나 제거하기도 까다로울 뿐더러 장애를 처리하기도 복잡해진다.
무상태 웹 계층에서 사용자로부터의 HTTP 요청은 어떠한 수평적 웹 서버로도 전달될 수 있으며, 웹 서버는 상태 정보가 필요할 경우 공유 저장소로부터 데이터를 가져온다.
이 공유 저장소는 관계형 데이터베이스일 수도 있고, Redis 같은 캐시 시스템일 수도 있으며, NoSQL일 수도 있다.
이러한 구조는 상태 정보가 웹 서버로부터 물리적으로 분리되었기 때문에 트래픽 양에 따라 웹 서버를 자동으로 추가하거나 삭제하는 오토스케일링을 통한 규모 확장이 쉽게 하고, 단순하고, 안정적이다.

데이터 계층의 확장

데이터베이스 선택
사용자가 늘면 서버 하나로는 충분하지 않아서 웹/모바일 트래픽 처리 용도와 데이터베이스용으로 여러 서버를 두어야 한다.
웹/모바일 트래픽 처리 서버(웹 계층)와 데이터베이스 서버(데이터 계층)를 분리하면 그 각각을 독립적으로 확장해 나갈 수 있게 된다.
데이터베이스는 전통적인 관계형 데이터베이스와 비-관계형 데이터베이스 사이에서 고를 수 있다.
관계형 데이터베이스
대표적으로 MySQL, Oracle, PostreSQL 등이 있다.
자료를 테이블과 열, 컬럼으로 표현하며 SQL을 사용하여 여러 테이블에 있는 데이터를 그 관계에 따라 JOIN하여 합칠 수 있다.
비-관계형 데이터베이스
대표적으로 CouchDB, Neo4j, Cassandra, DynamoDB 등이 있다.
다시 네 부류로 나누면 키-값 저장소, 그래프 저장소, 컬럼 저장소, 문서 저장소로 나눌 수 있다.
이런 비-관계형 데이터베이스는 일반적으로 JOIN 연산은 지원하지 않는다.
대부분의 개발자에게는 관계형 데이터베이스가 최선일 것이지만 아래와 같은 경우에는 비-관계형 데이터베이스가 바람직한 선택일 수 있다.
1.
아주 낮은 응답 지연시간(latency)이 요구됨
2.
다루는 데이터가 비정형이라 관계형 데이터가 아님
3.
데이터(JSON, YAML, XML등)를 직렬화하거나 역직렬화 할 수 있기만 하면 됨
4.
아주 많은 양의 데이터를 저장할 필요가 있음
데이터베이스 다중화
많은 RDBMS가 다중화를 지원하며 보통은 서버 사이에 master-slave 관계를 설정하고 데이터 원본은 master 서버에, 사본은 slave 서버에 저장하는 방식이다.
쓰기 연산은 master에서만 지원하고, slave는 master로부터 그 사본을 전달받아 읽기 연산만을 지원한다.
데이터베이스를 변경하는 명령어들, 가령 insert, update, delete 등은 master로만 전달되어야 한다.
대부분의 어플리케이션은 읽기 연산의 비중이 쓰기 연산보다 훨씬 높기 때문에 통상 slave의 수가 master의 수보다 많다.
이렇게 데이터베이스를 다중화하면 다음과 같은 이득이 있다.
더 나은 성능
master-slave 다중화 모델에서 모든 데이터 변경 연산은 master 서버로만 전달되는 반면 읽기 연산은 slave 서버들로 분산되기 때문에 병렬로 처리될 수 있는 쿼리의 수가 늘어나므로 성능이 좋아진다.
안정성
데이터를 지역적으로 떨어진 여러 장소에 다중화시켜 놓을 수 있기 때문에 자연 재해 등의 이유로 데이터베이스 서버 가운데 일부가 파괴되어도 데이터는 보존될 것이다.
가용성
데이터를 여러 지역에 복제해 둠으로써, 하나의 데이터베이스 서버에 장애가 발생하더라도 다른 서버에 있는 데이터를 가져와 계속 서비스할 수 있게 된다.
예를 들면 다음과 같은 상황들이 있다.
slave 서버가 한 대 뿐인데 다운된 경우라면, 읽기 연산은 한시적으로 모두 master 서버로 전달되고 새로운 slave 서버가 장애 서버를 대체할 것이다. slave 서버가 여러 대인 경우라면, 읽기 연산은 나머지 slave 서버들로 분산되고 새로운 slave 서버가 장애 서버를 대체할 것이다.
한 대의 slave 서버만 있는 경우 master 서버가 다운되면 해당 slave 서버가 새로운 master 서버가 될 것이다. 모든 데이터베이스 연산은 일시적으로 새로운 master 서버상에서 수행하며 새로운 slave 서버가 추가될 것이다. slave 서버에 보관된 데이터가 최신 상태가 아닐 수 있기 때문에 없는 데이터는 복구 스크립트를 돌려서 추가해야 한다. 또한 다중 master나 원형 다중화 방식을 도입하면 이런 상황에 대처하는데 도움이 될 수도 있다.
데이터베이스의 규모 확장
저장할 데이터가 많아지면 데이터베이스에 대한 부하도 증가하며 증설할 방법을 찾아야 한다.
수직적 확장
스케일 업이라고도 부르는 수직적 규모 확장법은 기존 서버에 더 많은, 또는 고성능의 자원을 증설하는 방법이다.
하지만 하드웨어의 물리적 한계나 비용 증가의 이유로 CPU, RAM 등을 무한 증설할 수는 없다.
또한 단일 장애 지점(SPOF)로 인한 위험성이 크다.
수평적 확장
데이터베이스의 수평적 확장은 샤딩(sharding)이라고도 부르는데, 더 많은 서버를 추가함으로써 성능을 향상시킬 수 있도록 한다.
샤딩은 대규모 데이터베이스를 샤드(shard)라고 부르는 작은 단위로 분할하는 기술을 일컫는다.
모든 샤드는 같은 스키마를 쓰지만 샤드에 보관되는 데이터 사이에는 중복이 없다.
샤딩 전략을 구현할 때 고려해야 할 가장 중요한 것은 샤딩 키를 어떻게 정하느냐 하는 것이다.
샤딩 키는 파티션 키라고도 부르는데, 데이터가 어떻게 고르게 분산될지 정하는 하나 이상의 컬럼으로 구성된다.
하지만 수평적 확장인 샤딩을 도입하면 시스템이 복잡해지고 풀어야 할 새로운 문제도 생긴다.
데이터의 재 샤딩(resharding)
재샤딩은 데이터가 너무 많아져서 하나의 샤드로는 더 이상 감당하기 어려울 때, 샤드 간 데이터 분포가 균등하지 못하여 어떤 샤드에 할당된 공간 소모가 다른 샤드에 비해 빨리 진행될 때 필요하다.
샤드 소진이라고도 부르는 이런 현상이 발생하면 샤드 키를 계산하는 함수를 안정 해시 기법을 활용해 변경하고 데이터를 재배치하여야 한다.
핫스팟 키 문제
특정 샤드에 쿼리가 집중되어 서버에 과부하가 걸리는 문제다.
이 문제를 풀려면 쿼리가 집중되는 각 키 각각에 샤드 하나씩을 할당해야 할 수도 있고, 심지어는 더 잘게 쪼개야 할 수도 있다.
조인과 비정규화
하나의 데이터베이스를 여러 샤드 서버로 쪼개고 나면, 여러 샤드에 걸친 데이터를 조인하기가 힘들어진다.
이를 해결하는 한 가지 방법은 데이터베이스를 비정규화하여 하나의 테이블에서 질의가 수행될 수 있도록 하는 것이다.

응답시간 개선

캐시
웹 페이지를 새로고침 할 때마다 표시할 데이터를 가져오기 위해 한 번 이상의 데이터베이스 호출이 발생하며, 어플리케이션의 성능은 데이터베이스를 얼마나 자주 호출하느냐에 크게 좌우된다.
캐시 계층은 데이터가 잠시 보관되는 곳으로 데이터베이스보다 훨씬 빠르기 때문에 그런 문제를 완화할 수 있다.
별도의 캐시 계층을 두면 성능이 개선될 뿐 아니라 데이터베이스의 부하를 줄일 수 있고, 캐시 계층의 규모를 독립적으로 확장시키는 것도 가능해진다.
예를 들어 요청을 받은 웹 서버는 캐시에 응답이 저장되어 있는지를 보고, 저장되어 있다면 해당 데이터를 반환하고 저장되어 있지 않다면 데이터베이스 쿼리를 통해 데이터를 찾아 캐시에 저장한 뒤 클라이언트에 반환하는 읽기 주도형 캐시 전략이 있다.
캐시를 사용할 때는 아래 사항들을 고려해야 한다.
캐시는 어떤 상황에 바람직한가?
데이터 갱신은 자주 일어나지 않지만 조회는 빈번하게 일어난다면 고려해볼 만하다.
어떤 데이터를 캐시에 두어야 하는가?
캐시는 데이터를 휘발성 메모리에 두므로, 영속적으로 보관할 데이터를 캐시에 두는 것은 바람직하지 않다.
예를 들어, 캐시 서버가 재시작되면 캐시 내의 모든 데이터는 사라지기 때문에 중요 데이터는 여전히 영속성 저장소에 두어야 한다.
캐시에 보관된 데이터는 어떻게 만료(expire)되는가?
만료 정책이 없다면 캐시에 계속 남게 되므로 만료된 데이터는 캐시에서 삭제되어야 한다.
만료 기한이 너무 짧으면 데이터베이스를 너무 자주 읽기 때문에 곤란하다.
만료 기한이 너무 길면 원본과 차이가 날 가능성이 높아진다.
데이터 방출(eviction) 정책은 무엇인가?
캐시가 꽉 차버리면 추가로 캐시에 데이터를 넣어야 할 경우 기존 데이터를 내보내야 한다.
가장 널리 쓰이는 것은 마지막으로 사용된 시점이 가장 오래된 데이터를 내보내는 LRU(Least Recently Used) 정책이다.
다른 정책으로는 사용된 빈도가 가장 낮은 데이터를 내보내는 LFU(Least Frequently Used) 정책이나 가장 먼저 캐시에 들어온 데이터를 가장 먼저 내보내는 FIFO(First-In First-Out) 정책이 있다.
일관성은 어떻게 유지되는가?
일관성은 데이터 저장소의 원본과 캐시 내의 사본이 같은지 여부다.
저장소의 원본을 갱신하는 연산과 캐시를 갱신하는 연산이 단일 트랜잭션으로 처리되지 않는 경우 이 일관성은 깨질 수 있다.
캐시 메모리는 얼마나 크게 잡을 것인가?
캐시 메모리가 너무 작으면 액세스 패턴에 따라서는 데이터가 너무 자주 캐시에서 밀려나버려(eviction) 캐시의 성능이 떨어지게 된다.
이를 막을 한 가지 방법은 캐시 메모리를 과할당(overprovision)하는 것으로 캐시에 보관될 데이터가 갑자기 늘어났을 때 생길 문제도 방지할 수 있게 된다.
장애에는 어떻게 대처할 것인가?
캐시 서버를 한 대만 두는 경우 해당 서버는 단일 장애 지점(SPOF)이 되어버릴 가능성이 있다.
단일 장애 지점이란 어떤 특정 지점에서의 장애가 전체 시스템의 동작을 중단시켜버릴 수 있는 경우를 말한다.
결과적으로, SPOF를 피하려면 여러 지역에 걸쳐 캐시 서버를 분산시켜야 한다.
컨텐츠 전송 네트워크(CDN)
CDN은 정적 컨텐츠를 전송하는데 쓰이는 지리적으로 분산된 서버의 네트워크이다.
request path, query string, cookie, request header 등의 정보에 기반하여 이미지, 비디오, HTML, CSS, JavaScript 같은 정적 컨텐츠를 캐시한다.
사용자 A가 CDN 서비스 사업자가 제공하는 URL을 통해서 이미지에 접근한다고 가정해보자.
CDN 서버의 캐시에 해당 이미지가 없는 경우 서버는 원본 서버에 요청하며, 원본 서버는 해당 파일이 얼마나 오래 캐시될 수 있는지 설명하는 TTL 값을 HTTP 헤더에 추가하여 CDN 서버에 이미지를 반환한다.
CDN 서버는 원본 서버로부터 받은 파일을 TTL에 명시된 시간만큼 캐시하고 사용자 A에게 반환한다.
이후 사용자 B가 동일한 URL을 통해서 이미지에 접근한다면 이 요청은 캐시를 통해 처리될 것이다.
CDN 사용 시 고려해야 할 사항은 다음과 같다.
비용
CDN은 보통 제3 사업자에 의해 운영되며 CDN으로 들어오고 나가는 데이터 전송 양에 따라 요금을 내게 된다.
자주 사용하지 않는 컨텐츠를 캐싱하는 것은 이득이 크지 않으므로 CDN에서 빼는 것을 고려하도록 하자.
적절한 만료 시한 설정
시의성이 중요한(time-sensitive) 컨텐츠의 경우 만료 시점을 잘 정해야 한다.
너무 길지도 않고 짧지도 않아야 하는데, 너무 길면 컨텐츠의 신선도는 떨어질 것이고 너무 짧으면 원본 서버에 빈번히 접속하게 되어서 좋지 않다.
CDN 장애에 대한 대처 방안
CDN 자체가 죽었을 경우 웹사이트/어플리케이션이 어떻게 동작해야 하는지 고려해야 한다.
해당 문제를 감지하여 원본 서버로부터 직접 컨텐츠를 가져오도록 클라이언트를 구성하는 것이 필요할 수도 있다.
컨텐츠 무효화(invalidation)
CDN 서비스 사업자가 제공하는 API를 이용하거나 컨텐츠의 다른 버전을 서비스하도록 오브젝트 버저닝을 하면 아직 만료되지 않은 컨텐츠라 하더라도 CDN에서 제거할 수 있다.

분산 시스템

데이터 센터
만약 데이터 센터 중 하나에 심각한 장애가 발생하면 모든 트래픽은 장애가 없는 데이터 센터로 전송된다.
이런 다중 데이터 센터 아키텍처를 만드려면 아래와 같은 기술적 난제를 해결해야 한다.
트래픽 우회
올바른 데이터 센터로 트래픽을 보내는 효과적인 방법을 찾아야 한다.
지리적 라우팅 DNS 서비스는 사용자에게서 가장 가까운 데이터 센터로 트래픽을 보낼 수 있도록 해 준다.
데이터 동기화
데이터 센터마다 별도의 데이터베이스를 사용하고 있는 상황이라면, 장애가 발생해 트래픽이 다른 데이터베이스로 우회된다 해도 해당 데이터 센터에는 찾는 데이터가 없을 수 있다.
이런 상황을 막는 보편적 전략은 데이터를 여러 데이터센터에 걸쳐 다중화하는 것이다.
테스트와 배포
여러 데이터 센터를 사용하도록 시스템이 구성된 상황이라면 웹 사이트 또는 어플리케이션을 여러 위치에서 테스트해 보는 것이 중요하다.
한편, 자동화된 배포 도구는 모든 데이터 센터에 동일한 서비스가 설치되도록 하는데 중요한 역할을 한다.
시스템을 더 큰 규모로 확장하기 위해서는 시스템의 컴포넌트를 분리하여, 각기 독립적으로 확장될 수 있도록 하여야 한다.
메세지 큐
메세지 큐는 메세지의 무손실을 보장하며 비동기 통신을 지원하는 컴포넌트로 많은 분산 시스템에서의 기술적 난제를 풀기 위해 채용하고 있는 핵심적 전략 가운데 하나다.
생산자 또는 발행자라고 불리는 입력 서비스가 메세지를 만들어 메세지 큐에 발행한다.
큐에는 보통 소비자 혹은 구독자라 불리는 서비스 혹은 서버가 연결되어 있는데, 메세지를 받아 그에 맞는 동작을 수행하는 역할을 한다.
생산자는 소비자 프로세스가 다운되어 있어도 메세지를 발행할 수 있고, 소비자는 생산자 서비스가 가용한 상태가 아니더라도 메세지를 수신할 수 있다.
이로 인해 서비스 또는 서버 간 결합이 느슨해져서, 규모 확장성이 보장되어야 하는 안정적 어플리케이션을 구성하기 좋다.
만약 큐의 크기가 커진다면 작업 프로세스를 추가하여 처리 시간을 줄일 수 있다.
로그, 메트릭 그리고 자동화
웹 사이트와 함께 사업 규모가 커진다면 로그, 메트릭, 자동화 같은 도구에 필수적으로 투자해야 한다.
로그
시스템의 오류와 문제들을 보다 쉽게 찾아낼 수 있도록 하기 위해 에러 로그를 모니터링하는 것은 중요하다.
에러 로그는 서버 단위로 모니터링 할 수도 있지만, 로그를 단일 서비스로 모아주는 도구를 활용하면 더 편리하게 검색하고 조회할 수 있다.
메트릭
메트릭을 잘 수집하면 사업 현황에 관한 유용한 정보를 얻을 수도 있고, 시스템의 현재 상태를 손쉽게 파악할 수도 있다.
자동화
시스템이 크고 복잡해지면 생산성을 높이기 위해 자동화 도구를 활용해야 한다.
지속적 통합을 도와주는 도구를 활용하면 개발자가 만드는 코드가 어떤 검증 절차를 자동으로 거치도록 할 수 있어서 문제를 쉽게 감지할 수 있다.
이 외에도 빌드, 테스트, 배포 등의 절차를 자동화할 수 있어서 개발 생산성을 크게 향상시킬 수 있다.

2장. 개략적인 규모 추정

개략적 규모 추정을 효과적으로 해 내려면 규모 확장성을 표현하는 데 필요한 기본기에 능숙해야 한다.
특히, 2의 제곱수나 응답지연 값, 그리고 가용성에 관계된 수치들을 기본적으로 잘 이해하고 있어야 한다.

2의 제곱수

제대로 된 계산 결과를 얻으려면 데이터 볼륨의 단위를 2의 제곱수로 표현하면 어떻게 되는지를 우선 알아야 한다.
최소 단위는 1바이트이고, 8비트로 구성되며 ASCII 문자 하나가 차지하는 메모리 크기는 1바이트이다.

응답지연 값

통상적인 컴퓨터에서 구현된 연산들의 처리 속도를 시각화한 수치이다.
이 수치들을 분석하면 다음과 같은 결론이 나온다.
메모리는 빠르지만 디스크는 아직도 느리다.
디스크 탐색은 가능한 한 피하라.
단순한 압축 알고리즘은 빠르다.
데이터를 인터넷으로 전송하기 전에 가능하면 압축하라.
데이터 센터는 보통 여러 지역에 분산되어 있고, 센터들 간에 데이터를 주고받는 데는 시간이 걸린다.

가용성에 관계된 수치들

고가용성은 시스템이 오랜 시간 동안 지속적으로 중단 없이 운영될 수 있는 능력을 지칭하는 용어다.
고가용성을 표현하는 값은 퍼센트로 표현하는데, 100%는 시스템이 단 한 번도 중단된 적이 없었음을 의미한다.
SLA(service Level Agreement)는 서비스 사업자가와 고객 사이에 맺어진 합의를 의미하며, 이 합의에는 서비스 사업자가 제공하는 서비스의 가용시간이 공식적으로 기술되어 있다.
가용시간은 관습적으로 숫자 9를 사용해 표시하며, 9가 많을수록 좋다고 보면 된다.

개략적인 규모 추정과 관계된 면접에서 가장 중요한 것은 문제를 풀어 나가는 절차다.
올바른 절차를 밟느냐가 결과를 내는 것보다 중요하며, 면접자가 보고 싶어 하는 것은 여러분의 문제 해결 능력일 것이다.
근사치를 활용한 계산
면접장에서 99987/9.1과 같은 복잡한 계산을 하는 것은 어려운 일이다.
그러니 100000/10과 같은 적절한 근사치를 활용하여 시간을 절약하자.
가정들은 적어두라
나중에 살펴볼 수 있도록
단위를 붙이라
여러분 스스로도 헷갈리게 될 수 있으므로 단위를 붙이는 습관을 들여두면 모호함을 방지할 수 있다.
많이 출제되는 개략적 규모 추정 문제
QPS(Query Per Second), 최대 QPS, 저장소 요구량, 캐시 요구량, 서버 수 등을 추정하는 것이다.

3장. 시스템 설계 면접 공략법

시스템 설계 면접은 두 명의 동료가 모호한 문제를 풀기 위해 협력하여 그 해결책을 찾아내는 과정에 대한 시뮬레이션이며, 정해진 결말도 없고 정답도 없다.
이 면접은 여러분의 설계 기술을 시연하는 자리이고, 설계 과정에서 내린 결정들에 대한 방어 능력을 보이는 자리이며, 면접관의 피드백을 건설적인 방식으로 처리할 자질이 있음을 보이는 자리인 것이다.

효과적인 면접을 위한 4단계 접근법

1단계. 문제 이해 및 설계 범위 확정
요구사항을 완전히 이해하지 않고 답을 내놓는 행위는 아주 엄청난 부정적 신호다.
엔지니어가 가져야 할 가장 중요한 기술 중 하나는 올바른 질문을 하는 것, 적절한 가정을 하는 것, 그리고 시스템 구축에 필요한 정보를 모으는 것이다.
2단계. 개략적인 설계안 제시 및 동의 구하기
설계안에 대한 최초 청사진을 제시하고 면접관을 마치 팀원인 것처럼 대하며 의견을 구하라.
화이트보드나 종이에 클라이언트(모바일/웹), API, 웹 서버, 데이터저장소, 캐시와 같은 핵심 컴포넌트를 포함하는 다이어그램을 그려라.
이 최초 설계안이 시스템 규모에 관계된 제약사항들을 만족하는지를 개략적으로 계산해보며 소리 내어 설명하라.
아울러, 이런 개략적 추정이 필요한지는 면접관에게 미리 물어보도록 하자.
3단계. 상세 설계
이제 면접관과 해야할 일은 설계 대상 컴포넌트 사이의 우선순위를 정하는 것이다.
면접관은 여러분이 집중 했으면 하는 영역을 알려 주기도 하고, 병목 구간이나 자원 요구량 추정치와 같은 시스템의 성능 특성에 대한 질문을 던질 수도 있다.
대부분의 경우 면접관은 여러분이 특정 시스템 컴포넌트들의 세부사항을 깊이 있게 설명하는 것을 보길 원한다.
면접 시에는 시간 관리에도 특별히 주의를 기울여야 하며, 사소한 세부사항을 설명하느라 정작 규모 확장 가능한 시스템을 설계할 능력이 있다는 것을 보일 기회를 놓쳐버리게 될 수도 있다.
4단계. 마무리
이 마지막 단계에서 면접관은 설계 결과물에 관련된 몇 가지 후속 질문을 던질 수도 있고, 여러분 스스로 추가 논의를 진행하도록 할 수도 있다.
면접관이 시스템 병목구간, 혹은 좀 더 개선 가능한 지점을 찾아내라 주문할 수 있다.
개선할 점은 언제나 있게 마련이고, 이런 질문은 여러분의 비판적 사고 능력을 보이고, 마지막으로 좋은 인상을 남길 기회다.
또한 여러분이 만든 설계를 한번 다시 요약해주는 것도 도움이 될 수 있다.
여러 해결책을 제시한 경우에는 특히 중요하며, 긴 면접 세션이 끝난 뒤에 면접관의 기억을 환기시켜주는 효과가 있다.
만약 오류가 발생하면 무슨 일이 생기는지(서버 오류, 네트워크 장애 등) 따져보면 흥미로울 것이다.
이와 더불어 로그와 메트릭은 어떻게 수집하고 모니터링할 것인지나 시스템은 어떻게 배포해 나갈 것인지와 같이 운영 이슈도 논의할 가치가 충분하다.
이외에도 미래에 닥칠 더 많은 사용자를 감당하기 위한 규모 확장 요구에 어떻게 대처할 것인지도 흥미로운 주제다.
시간이 좀 남았다면, 필요하지만 다루지 못했던 세부적 개선사항들을 제안할 수 있다.

면접 세션에서 해야 할 것과 하지 말아야 할 것 요약

해야 할 것
스스로 내린 가정이 옳다 믿고 진행하지 말고 질문을 통해 확인하라
문제의 요구사항을 이해하라
정답이나 최선의 답안 같은 것은 없기 때문에 요구사항을 정확하게 이해했는지 다시 확인하라
면접관이 여러분의 사고 흐름을 이해할 수 있도록 하기 위해 면접관과 소통하라
가능하다면 여러 해법을 함께 제시하라
개략적 설계에 면접관이 동의하면, 가장 중요한 컴포넌트부터 각 컴포넌트의 세부사항을 설명하기 시작하라
좋은 면접관은 여러분과 같이 팀원처럼 협력하기 때문에 면접관의 아이디어를 이끌어 내라
포기하지마라
하지 말아야 할 것
전형적인 면접 문제들에도 대비하지 않은 상태에서 면접장에 가지 마라
요구사항이나 가정들을 분명히 하지 않은 상태에서 설계를 제시하지 마라
처음부터 특정 컴포넌트의 세부사항을 너무 깊이 설명하지 마라.
진행 중에 막혔다면 힌트를 청하기를 주저하지 마라
설계안을 내놓는 순간 면접이 끝난다고 생각하지 마라

사례

뉴스 피드 시스템을 설계하라는 요구를 받았다 해 보자.
지원자: 모바일 앱과 웹 앱 가운데 어느 쪽을 지원해야 하나요? 아니면 둘 다 일까요?
면접관: 둘 다 지원해야 합니다.
지원자: 가장 중요한 기능은 무엇인가요?
면접관: 새로운 포스트를 올리고, 다른 친구의 뉴스 피드를 볼 수 있도록 하는 기능입니다.
지원자: 이 뉴스 피드는 시간 역순으로 정렬되어야 하나요? 아니면 특별한 정렬 기준이 있습니까? 제가 특별한 정렬 기준이 있느냐고 묻는 이유는 피드에 올라갈 포스트마다 다른 가중치가 부여되어야 하는지 알고 싶어서 인데요. 가령 가까운 친구의 포스트가 사용자 그룹에 올라가는 포스트보다 더 중요하다거나.
면접관: 문제를 단순하게 만들기 위해, 일단 시간 역순으로 정렬된다고 가정합시다.
지원자: 한 사용자는 최대 몇 명의 사용자와 친구를 맺을 수 있나요?
면접관: 5000명입니다.
지원자: 사이트로 오는 트래픽 규모는 어느 정도입니까?
면접관: DAU는 천만 명입니다.
지원자: 피드에 이미지나 비디오도 올라올 수 있나요? 아니면 포스트는 그저 텍스트입니까?
면접관: 이미지나 비디오 같은 미디어 파일도 포스트 할 수 있어야 합니다.
개략적으로 보자면 이 설계는 두 가지 플로우로 나눠 생각해 볼 수 있다.
피드 발행
사용자가 포스트를 올리면 관련된 데이터가 캐시/데이터베이스에 기록되고, 해당 사용자의 친구 뉴스 피드에 뜨게 된다.
피드 생성
어떤 사용자의 뉴스 피드는 해당 사용자 친구들의 포스트를 시간 역순으로 정렬하여 만든다.
면접관이 이 설계에 만족하고 있다고 해보자.
이제 두 가지 플로우에 대해 보다 깊이 탐구해야 한다.

4장. 처리율 제한 장치의 설계

네트워크 시스템에서 처리율 제한 장치(rate limiter)는 클라이언트 또는 서비스가 보내는 트래픽의 처리율을 제어하기 위한 장치다.
API 요청 횟수가 제한 장치에 정의된 임계치(threshold)를 넘어서면 추가로 도달한 모든 호출은 처리가 중단된다.
다음은 몇 가지 사례다.
사용자는 초당 2회 이상 새 글을 올릴 수 없다.
같은 IP 주소로는 하루에 10개 이상의 계정을 생성할 수 없다.
같은 디바이스로는 주당 5회 이상 리워드를 요청할 수 없다.
API에 처리율 제한 장치를 두면 좋은 점은 다음과 같다.
DoS 공격에 의한 자원 고갈을 방지할 수 있다.
비용을 절감한다.
서버 과부하를 막는다.

처리율 제한 알고리즘

토큰 버킷
토큰 버킷은 지정된 용량을 갖는 컨테이너다.
토큰 공급기(refiller)는 매초 2개의 토큰을 추가하고, 버킷이 가득 차면 추가로 공급된 토큰은 버려진다. (overflow)
각 요청은 충분한 토큰이 있는 경우 하나의 토큰을 사용하고, 충분한 토큰이 없는 경우 해당 요청은 버려진다.(dropped)
버킷의 갯수는 공급 제한 규칙에 따라 달라지며 제어하는 대상마다 버킷을 할당한다.
장점
구현이 쉽다.
메모리 사용 측면에서도 효율적이다.
짧은 시간에 집중되는 트래픽도 처리 가능하다.
단점
버킷 크기와 토큰 공급률이라는 두 개의 인자를 적절하게 튜닝하는 것이 까다롭다.
누출 버킷
토큰 버킷과 비슷하지만 요청 처리율이 고정되어 있다는 점이 다르다.
보통 FIFO 큐로 구현되며, 요청이 도착하면 큐가 비어있는 경우에만 추가하고 가득 찬 경우에는 버린다.
그리고 지정된 시간마다 큐에서 요청을 꺼내어 처리한다.
장점
큐의 크기가 제한되어 있어 메모리 사용량 측면에서 효율적이다.
고정된 처리율을 갖고 있기 때문에 안정적 출력이 필요한 경우에 적합하다.
단점
단시간에 많은 트래픽이 몰리는 경우 큐에는 오래된 요청들이 쌓이게 되고, 그 요청들을 제때 처리 못하면 최신 요청들은 버려지게 된다.
버킷 크기와 처리율이라는 두 개의 인자를 적절하게 튜닝하는 것이 까다롭다.
고정 윈도 카운터
타임라인을 고정된 간격의 윈도우로 나누고, 각 윈도우마다 카운터를 붙인다.
요청이 접수될 때마다 해당 타임라인의 카운터 값은 1씩 증가한다.
이 카운터의 값이 사전에 설정된 임계치에 도달하면 새로운 요청은 새 윈도우가 열릴 때까지 버려진다.
장점
메모리 효율이 좋다.
이해하기 쉽다.
윈도우가 닫히는 시점에 카운터를 초기화하는 방식은 특정한 트래픽 패턴을 처리하기에 적합하다.
단점
윈도우 경계 부근에서 일시적으로 많은 트래픽이 몰려드는 경우, 기대했던 시스템의 처리 한도보다 많은 양의 요청을 처리하게 된다.
이동 윈도 로그
고정 윈도 카운터의 문제인 경계 부근에 트래픽이 집중되는 경우 시스템에 설정된 한도보다 많은 요청을 처리한다는 것을 해결한다.
요청의 타임스탬프를 Redis의 정렬 집합 같은 캐시에 보관하여 추적한다.
새 요청이 오면 해당 윈도우에 만료된 타임스탬프 로그는 제거하고 새 요청의 타임스탬프를 로그에 추가한다.
로그의 크기가 허용치보다 같거나 작으면 요청을 시스템에 전달하고 그렇지 않은 경우에는 처리를 거부한다.
장점
처리율 제한 메커니즘이 아주 정교하다.
어느 순간의 윈도우를 보더라도, 허용되는 요청의 개수는 시스템의 처리율 한도를 넘지 않는다.
단점
거부된 요청의 타임스탬프도 보관하기 때문에 다량의 메모리를 사용한다.
이동 윈도 카운터
고정 윈도 카운터와 이동 윈도 로깅을 결합한 것이다.
처리율 제한 장치의 한도가 분당 7개 요청으로 설정되어 있고 이전 1분 동안 5개의 요청과 현재 1분 동안 3개의 요청이 왔다고 해보자.
‘현재 1분간 요청 수 + 직전 1분간 요청 수 * 이동 윈도와 직전 1분이 겹치는 비율’ 공식에 따르면 3+5*70%=6.5개다.
처리율 제한 한도가 분당 7개 요청이라고 했으므로 현재 1분 동안 3개의 요청은 시스템으로 전달되지만 그 직후에는 한도에 도달하여 더 이상의 요청은 받을 수 없을 것이다.
장점
이전 시간대의 평균 처리율에 따라 현재 윈도의 상태를 계산하므로 짧은 시간에 몰리는 트래픽에도 잘 대응한다.
메모리 효율이 좋다.
단점
직전 시간대에 도착한 요청이 균등하게 분포되어 있다고 가정한 상태에서 추정치를 계산하기 때문에 다소 느슨하다.

사례

1단계. 문제 이해 및 설계 범위 확정
지원자: 어떤 종류의 처리율 제한 장치를 설계해야 하나요? 클라이언트 측 제한 장치입니까, 아니면 서버 측 제한 장치입니까?
면접관: 좋은 질문이에요. 서버측 API를 위한 장치를 설계한다고 가정합시다.
지원자: 어떤 기준을 사용해서 API 호출을 제어해야 할까요? IP 주소를 사용해야 하나요? 아니면 사용자 ID? 아니면 생각하는 다른 어떤 기준이 있습니까?
면접관: 다양한 형태의 제어 규칙을 정의할 수 있도록 하는, 유연한 시스템이어야 합니다.
지원자: 시스템 규모는 어느 정도여야 할까요? 스타트업 정도 회사를 위한 시스템입니까 아니면 사용자가 많은 큰 기업을 위한 제품입니까?
면접관: 설계할 시스템은 대규모 요청을 처리할 수 있어야 합니다.
지원자: 시스템이 분산 환경에서 동작해야 하나요?
면접관: 그렇습니다.
지원자: 이 처리율 제한 장치는 독립된 서비스입니까 아니면 어플리케이션 코드에 포함될 수도 있습니까?
면접관: 그 결정은 본인이 내려주시면 되겠습니다.
지원자: 사용자의 요청이 처리율 제한 장치에 의해 걸러진 경우 사용자에게 그 사실을 알려야 하나요?
면접관: 그렇습니다.
2단계. 개략적 설계안 제시 및 동의 구하기
기본적인 클라이언트-서버 통신 모델을 사용하면 이 장치를 클라이언트 측에 둘 수도 있고, 서버 측에 둘 수도 있을 것이다.
클라이언트 측에 둔다면 모든 클라이언트의 구현을 통제하는 것이 어려울 수 있고, 요청이 쉽게 위변조될 수 있기 때문에 안정적으로 둘 수 있는 장소가 못 된다.
서버 측에 둔다면 API 서버 내부에 두거나 미들웨어를 만들어 해당 미들웨어로 하여금 API 서버로 가는 요청을 통제하도록 할 수 있다.
널리 사용하는 마이크로서비스의 경우 API 게이트웨이가 처리율 제한, SSL 종단, 사용자 인증, IP 허용 목록 관리 등을 지원한다.
하지만 처리율 제한 장치를 어디에 두는지에 대해 정답은 없으며 현재 기술 스택이나 엔지니어링 인력, 우선순위, 목표에 따라 달라질 수 있다.
일반적으로 적용될 수 있는 몇 가지 지침은 다음과 같다.
프로그래밍 언어, 캐시 서비스 등 현재 사용하고 있는 기술 스택을 점검하라.
현재 사용하는 프로그래밍 언어가 서버측 구현을 지원하기 충분할 정도로 효율이 높은지 확인하라
여러분의 사업 필요에 맞는 처리율 제한 알고리즘을 찾아라.
서버측에서 모든 것을 구현하기로 했다면, 알고리즘을 자유롭게 선택할 수 있다.
하지만 제3 사업자가 제공하는 게이트웨이를 사용하기로 했다면 선택지는 제한될 수도 있다.
여러분의 현재 설계를 확인하라.
여러분의 설계가 마이크로서비스에 기반하고 있고, 사용자 인증이나 IP 허용 목록 관리 등을 처리하기 위해 API 게이트웨이를 이미 설계에 포함시켰다면 처리율 제한 기능 또한 게이트웨이에 포함시켜야 할 수도 있다.
여러분 회사의 현재 인력을 파악하라.
처리율 제한 서비스를 직접 만드는 데는 시간이 들며, 충분한 인력이 없다면 상용 API 게이트웨이를 쓰는 것이 바람직한 방법일 것이다.
3단계. 상세 설계
개략적 설계를 통해서는 아래와 같은 구체적인 설계나 성능 최적화 방안, 모니터링 방안들은 알 수가 없다.
처리율 제한 규칙
클라이언트가 분당 5회 이상 로그인 할 수 없도록 제한하는 등의 규칙들은 보통 설정 파일 형태로 미들웨어의 디스크에 저장된다.
작업 프로세스는 수시로 규칙을 디스크에서 읽어 캐시에 저장한다.
처리율 한도 초과 트래픽의 처리
클라이언트는 자기 요청이 처리율 제한에 걸리고 있는지를 HTTP 응답 헤더의 X-Ratelimit 이 붙은 헤더들을 통해 알 수 있다.
어떤 요청이 한도 제한에 걸리면 API는 HTTP 429 응답을 보내거나, 경우에 따라서 한도 제한에 걸린 요청을 나중에 처리하기 위해 큐에 보관할 수도 있다.
분산 환경에서의 처리율 제한 장치의 구현
여러 대의 서버와 병렬 스레드를 지원하도록 시스템을 확장할 땐 경쟁 조건과 동기화의 문제를 풀어야 한다.
경쟁 조건은 두 개의 요청을 각 스레드가 병렬로 처리했다면 요청 수를 누적하기 위한 counter의 값이 병렬로 읽어져 올바르지 않은 값이 저장되는 문제이다.
이 문제를 해결하기 위한 널리 알려진 해결책은 락이지만 성능을 떨어뜨리기 때문에 루아 스크립트나 Redis의 정렬 집합을 고려해야 한다.
동기화 이슈는 무상태 웹 계층을 사용할 때 여러 대의 서버가 동일한 클라이언트의 정보를 서로 알지 못하기 때문에 처리율 제한이 올바르게 수행되지 않는 문제이다.
이 문제를 해결하기 위해 고정 세션을 사용한다면 항상 같은 처리율 제한 장치로 보낼 수 있지만 확장 가능하지도 않고 유연하지도 않다.
더 나은 해결책은 Redis와 같은 중앙 집중형 데이터 저장소를 사용하는 것이다.
성능 최적화
여러 데이터 센터를 지원하는 경우 멀리 떨어진 사용자에겐 응답 지연이 증가할 수 밖에 없다.
대부분의 클라우드 서비스 사업자는 세계 곳곳에 엣지 서버를 두고 사용자의 트래픽을 가장 가까운 엣지 서버로 전달하여 응답 지연을 줄인다.
또한 처리율 제한 장치 간에 데이터를 동기화할 때 최종 일관성 모델을 사용하는 것을 고려해야 한다.
모니터링
처리율 제한 장치를 설치한 이후 채택한 알고리즘이나 규칙이 효과적인지 확인하기 위해 모니터링할 필요가 있다.
예를 들어 처리율 제한 규칙이 너무 빡빡하게 설정되었다면 많은 유효 요청이 처리되지 못하고 버려질 것이다.
또한 이벤트 때문에 트래픽이 급증할 때 처리율 제한 장치가 비효율적으로 동작한다면, 토큰 버킷 알고리즘 같은 적절한 알고리즘으로 바꾸는 것을 고려해야 한다.
4단계. 마무리
시간이 허락한다면 다음과 같은 부분을 언급해보면 도움이 될 것이다.
경성(hard) 또는 연성(soft) 처리율 제한
경성 처리율 제한은 요청의 개수는 임계치를 절대 넘어걸 수 없다는 것이다.
연성 처리율 제한은 요청 개수는 잠시 동안은 임계치를 넘어설 수 있다는 것이다.
다양한 계층에서의 처리율 제한
앞에선 OSI 7 계층 중 어플리케이션 계층에서의 처리율 제한에 대해서만 살펴보았다.
하지만 IPTables를 사용하면 IP 주소를 사용한 네트워크 계층에서의 처리율 제한 적용이 가능하다.
처리율 제한을 회피하기 위한 클라이언트를 설계하는 방법
클라이언트 측 캐시를 사용하여 API 호출 횟수를 줄인다.
처리율 제한의 임계치를 이해하고, 짧은 시간 동안 너무 많은 메세지를 보내지 않도록 한다.
예외나 에러를 처리하는 코드를 도입하여 클라이언트가 예외적 상황으로부터 우아하게 복구될 수 있도록 한다.
재시도 로직을 구현할 때는 충분한 백오프 시간을 둔다.

5장. 안정 해시 설계

수평적 규모 확장성을 달성하기 위해서는 요청 또는 데이터를 서버에 균등하게 나누는 것이 중요하다.

해시 키 재배치 문제

N개의 캐시 서버가 있을 때, 이 서버들에 부하를 균등하게 나누는 보편적인 방법은 해시 함수를 사용하는 것이다.
serverIndex = hash(key) % 서버수
이 방법은 서버 풀의 크기가 고정되어 있을 때, 그리고 데이터 분포가 균등할 때는 잘 동작한다.
하지만 서버가 추가되거나 기존 서버가 삭제되면 키에 대한 해시 값은 변하지 않지만 나머지 연산을 적용하여 계산한 서버 인덱스 값이 달라져서 문제가 생긴다.
1번 서버가 죽으면 대부분 캐시 클라이언트가 데이터가 없는 엉뚱한 서버에 접속하게 된다는 뜻이다.
그 결과로 대규모 캐시 미스가 발생하게 될 것이다.

안정 해시

안정 해시는 해시 테이블 크기가 조정될 때 평균적으로 오직 k/n개의 키만 재배치하는 해시 기술이다.
여기서 k는 키의 개수이고, n은 슬롯의 개수다.
이와는 달리 대부분의 전통적 해시 테이블은 슬롯의 수가 바뀌면 거의 대부분 키를 재배치한다.
해시 공간과 해시 링
SHA-1의 해시 공간 범위인 0부터 2^160 - 1까지라고 알려져있다.
해시 함수 f로는 SHA-1을 사용하면, 그 함수의 출력 값 범위는 x0은 0, xn은 2^160 - 1이며, 나머지 x1부터 xn-1까지는 그 사이의 값을 갖게 될 것이다.
이 해시 공간의 양쪽을 구부려 접으면 해시 링을 만들 수 있다.
해시 서버와 해시 키
해시 함수 f를 사용하면 서버 IP나 이름을 이 링 위의 어떤 위치에 대응시킬 수 있다.
캐시할 키 또한 해시 링 위의 어느 지점에 배치할 수 있다.
서버 조회, 추가, 제거
어떤 키가 저장되는 서버는, 해당 키의 위치로부터 시계 방향으로 링을 탐색해 나가면서 만나는 첫 번째 서버다.
서버를 추가하더라도 시계 방향으로 해시 키를 사용했던 기존 1개의 서버만 키가 변경되며 다른 키는 재배치되지 않는다.
서버 제거도 마찬가지로 기존 조회 규칙에 따라 가운데 일부의 키만 재배치된다.
키를 재배치할 땐 새로 추가되거나 제거된 노드로부터 반시계 방향에 있는 첫 번째 서버 사이의 키들을 새로 추가되거나 제거된 노드로 재배치하여야 한다.
기본 구현법의 두 가지 문제
서버가 추가되거나 삭제되는 상황을 감안하면 파티션의 크기를 균등하게 유지하는게 불가능하다.
파티션은 인접한 서버 사이의 해시 공간이다.
어떤 서버는 굉장히 작은 해시 공간을 할당 받고, 어떤 서버는 굉장히 큰 해시 공간을 할당 받는 상황이 가능하다는 것이다.
키의 균등 분포를 달성하기 어렵다.
서버를 추가하거나 삭제하게되면 링 위에 균등하게 분포되지 않을 수 있고 한 서버에 키가 몰릴 수 있다.
해결책: 가상 노드
가상 노드는 실제 노드 또는 서버를 가리키는 노드로서, 하나의 서버는 링 위에 여러 개의 가상 노드를 가질 수 있다.
따라서 각 서버는 하나가 아닌 여러 개의 파티션을 관리해야 한다.
키의 위치로부터 시계방향으로 링을 탐색하다 만나는 최초의 가상 노드가 해당 키가 저장될 서버가 된다.
가상 노드의 개수를 늘리면 키의 분포는 점점 더 균등해지지만 가상 노드 데이터를 저장할 공간은 더 많이 필요하게 될 것이다.
그러니 시스템 요구사항에 맞도록 가상 노드 개수를 적절히 조정해야 할 것이다.

6장. 키-값 저장소 설계

키-값 저장소

키-값 저장소는 비관계형 데이터베이스로 고유 식별자인 키와 해당 키로만 접근할 수 있는 값의 키-값 쌍이 존재한다.
키는 일반 텍스트 혹은 해시 값으로 짧을 수록 좋고, 값은 무엇이 오든 상관없다.
쓰기 요청
1.
쓰기 요청이 커밋 로그 파일에 기록된다.
2.
데이터가 메모리 캐시에 기록된다.
3.
메모리 캐시가 가득차거나 사전에 정의된 어떤 임계치에 도달하면 데이터는 디스크에 있는 SSTable에 기록된다. SSTable은 Sorted-String Table의 약어로 <키, 값>의 순서쌍을 정렬된 리스트 형태로 관리하는 테이블이다.
읽기 요청
1.
데이터가 메모리에 있는지 확인하고 있다면 결과를 반환한다.
2.
데이터가 메모리에 없다면 블룸 필터를 검사한다.
3.
블룸 필터를 통해 어떤 SSTable에 키가 보관되어 있는지 알아낸다.
4.
SSTable에서 데이터를 가져온다.
5.
해당 데이터를 클라이언트에 반환한다.

분산 키-값 저장소

가장 직관적으로 빠른 속도를 보장하기 위해선 키-값 쌍 전부를 메모리에 해시 테이블로 저장하면 되지만, 모든 데이터를 메모리 안에 두는 것이 불가능할 수도 있다.
이 문제를 해결하기 위해선 데이터 압축이나 자주 사용하는 데이터만 메모리에 두고 나머지는 디스크에 저장하는 방법이 있다.
그러나 이렇게 개선하더라도 한 대 서버로 부족한 때가 곧 찾아올 것이다.
분산 키-값 저장소는 분산 해시 테이블이라고도 불리며 키-값 쌍을 여러 서버에 분산시키는 것이다.
분산 시스템을 설계할 때는 데이터 일관성, 가용성, 파티션 감내성의 세 가지 요구사항을 동시에 만족하기는 불가능하다는 CAP 정리를 이해하고 있어야 한다.
데이터 일관성
분산 시스템에 접속하는 모든 클라이언트는 어떤 노드에 접속했느냐에 관계없이 언제나 같은 데이터를 보게 되어야 한다.
가용성
분산 시스템에 접속하는 클라이언트는 일부 노드에 장애가 발생하더라도 항상 응답을 받을 수 있어야 한다.
파티션 감내성
파티션은 두 노드 사이에 통신 장애가 발생하였음을 의미한다.
파티션 감내성은 네트워크에 파티션이 생기더라도 시스템은 계속 동작하여야 한다는 것을 뜻한다.
이들 가운데 어떤 두 가지를 충족하려면 나머지 하나는 반드시 희생되어야 한다.
그러므로 시스템의 성향이나 요구사항에 맞도록 CAP 정리를 적용하여 시스템을 설계해야 한다.

시스템 컴포넌트

데이터 파티션
대규모 어플리케이션의 경우 전체 데이터를 한 대 서버에 욱여넣는 것은 불가능하며, 데이터를 작은 파티션들로 분할한 다음 여러 대 서버에 저장해야 한다.
데이터를 파티션 단위로 나눌 때 다음 두 가지 문제를 중요하게 따져봐야 한다.
데이터를 여러 서버에 고르게 분산할 수 있는가
노드가 추가되거나 삭제될 때 데이터의 이동을 최소화할 수 있는가
안정 해시는 이 문제를 푸는 적합한 기술이며, 이를 통해 데이터를 파티션하면 안정 해시의 이점을 모두 누릴 수 있다.
데이터 다중화
높은 가용성과 안정성을 확보하기 위해서는 데이터를 N개 서버의 비동기적으로 다중화할 필요가 있다.
어떤 키를 해시 링 위에 배치한 후, 시계 방향으로 순회하며 만나는 N개의 서버에 데이터 사본을 보관하는 것이다.
그런데 가상 노드를 사용한다면 위와 같이 선택한 N개의 노드가 대응될 실제 물리 서버의 개수가 N보다 작아질 수 있다.
이 문제를 피하려면 노드를 선택할 때 같은 물리 서버를 중복 선택하지 않도록 해야 한다.
또한 같은 데이터 센터에 속할 경우 정전, 네트워크 이슈, 자연재해 등의 문제를 동시에 겪을 가능성이 있기 때문에 서로 다른 데이터 센터에 위치하고 고속 네트워크로 연결하는 것이 좋다.
데이터 일관성
여러 노드에 다중화된 데이터는 적절히 동기화가 되어야 한다.
정족수 합의 프로토콜을 사용하면 읽기/쓰기 연산 모두에 일관성을 보장할 수 있다.
N = 사본 개수
W = 쓰기 연산에 대한 정족수. (서버로부터 쓰기 연산이 성공한 응답 수로 쓰기 연산이 성공한 것으로 간주하기 위한 수)
R = 읽기 연산에 대한 정족수. (서버로부터 읽기 연산이 성공한 응답 수로 읽기 연산이 성공한 것으로 간주하기 위한 수)
중재자는 클라이언트와 노드 사이에서 프록시 역할을 하며, 각 값을 정하는 것은 응답 지연과 데이터 일관성 사이의 타협점을 찾는 과정이다.
R = 1, W = N : 빠른 읽기 연산에 최적화된 시스템
W = 1, R = N : 빠른 쓰기 연산에 최적화된 시스템
W + R > N : 강한 일관성이 보장됨 (보통 N = 3, W = R = 2)
W + R ≤ N : 강한 일관성이 보장되지 않음
일관성 모델은 키-값 저장소를 설게할 때 고려해야 할 또 하나의 중요한 요소이다.
강한 일관성
모든 사본에 현재 쓰기 연산의 결과가 반영될 때까지 해당 데이터에 대한 읽기/쓰기를 금지하여 모든 읽기 연산은 가장 최근에 갱신된 결과를 반환한다.
새로운 요청의 처리가 중단되기 때문에 고가용성 시스템에는 적합하지 않다.
약한 일관성
읽기 연산은 가장 최근에 갱신된 결과를 반환하지 못할 수 있다.
최종 일관성
약한 일관성의 한 형태로 갱신 결과가 결국에는 모든 사본에 동기화되는 모델이다.
쓰기 연산이 병렬적으로 발생하면 시스템에 저장된 값의 일관성이 깨질 수 있으며, 이 문제는 클라이언트가 해결해야 한다.
다이나모 또는 카산드라 같은 저장소가 채택하고 있는 모델이다.
데이터를 다중화하면 가용성은 높아지지만 사본 간 일관성이 깨질 가능성이 높아지며, 버저닝과 벡터 시계는 그 문제를 해결하기 위한 기술이다.
버저닝
데이터를 변경할 때마다 해당 데이터의 새로운 버전을 만들며, 각 버전의 데이터는 변경 불가능하다.
하지만 버저닝 연산이 동시에 이뤄진다면 마지막 두 버전 사이의 충돌은 해소하기 어렵다.
벡터 시계
버저닝 시스템에서 충돌을 발견하고 자동으로 해결해내는 시스템이다.
데이터에 [서버, 버전]의 순서쌍을 매달아 어떤 버전이 선행 버전인지, 후행 버전인지, 그리고 다른 버전과 충돌이 있는지 파별하는데 쓰인다.
D([S1, v1], …, [Sn, vn])의 데이터가 있다고 했을 때 D[Si, vi]가 이미 존재한다면 v1를 증가시키고 그렇지 않으면 새 항목 [Si, 1]을 만든다.
어떤 버전 X와 Y 사이에 충돌이 있는지 보려면 Y의 벡터 시계 구성요소 중 일부가 X의 벡터 시계 동일 서버 구성요소보다 작은 값을 갖는 것이 있는지 보면 된다. (모든 구성요소가 작은 값을 갖는 경우엔 이전 버전이다.)
그러나 충돌 감지 및 해소 로직이 클라이언트에 들어가야 하므로 클라이언트 구현이 복잡해질 수 있다.
또한 [서버, 버전] 순서쌍 개수가 굉장히 빨리 늘어나는 것을 방지하기 위해 임계치를 설정한다면 버전 간 선후 관계가 정확하게 결정될 수 없기 때문에 충돌 해소 과정의 효율성이 낮아지게 된다.

장애 감지

분산 시스템에선 보통 두 대 이상의 서버가 똑같이 서버 A의 장애를 보고해야 해당 서버에 실제로 장애가 발생했다고 간주한다.
모든 노드 사이에 멀티캐스팅 채널을 구축하는 것이 서버 장애를 감지하는 가장 손쉬운 방법이지만, 서버가 많을 때는 비효율적이다.
가십 프로토콜 같은 분산형 장애 감지 솔루션을 채택하는 편이 효율적이다.
가십 프로토콜 동작 원리
각 노드는 각 멤버 ID와 그 박동 카운터 쌍의 목록을 가진 멤버십 목록을 유지한다.
각 노드는 주기적으로 자신의 박동 카운터를 증가시킨다.
각 노드는 무작위로 선정된 노드들에게 주기적으로 자기 박동 카운터 목록을 보낸다.
박동 카운터 목록을 받은 노드는 멤버십 목록을 최신 값으로 갱신한다.
어떤 멤버의 박동 카운터 값이 지정된 시간 동안 갱신되지 않으면 해당 멤버는 장애 상태인 것으로 간주한다.

장애 처리

일시적 장애 처리
가십 프로토콜로 장애를 감지한 시스템은 가용성을 보장하기 위해 적절한 접근법을 통해 조치를 취한다.
엄격한 정족수 접근법
읽기와 쓰기 연산을 금지한다.
느슨한 정족수 접근법
정족수 요구사항을 강제하는 대신, 장애 상태인 서버를 제외한 쓰기 연산을 수행할 W개의 건강한 서버와 읽기 연산을 수행할 R개의 건강한 서버를 해시 링에서 고른다.
장애 상태인 서버로 가는 요청은 다른 서버가 잠시 맡아 처리하고, 그동안 발생한 변경사항은 해당 서버가 복구되었을 때 일괄 반영하여 데이터 일관성을 보존한다.
이를 위해 임시로 쓰기 연산을 처리한 서버에는 그에 관한 단서를 남겨두며, 이를 단서 후 임시 위탁 기법이라 부른다.
영구 장애 처리
영구적인 노드의 장애 상태는 반-엔트로피 프로토콜을 구현하여 사본들을 동기화하는 방식으로 처리할 수 있다.
반-엔트로피 프로토콜은 사본들을 비교하여 최신 버전으로 갱신하는 과정을 포함하며, 사본 간의 일관성이 망가진 상태를 탐지하고 전송 데이터의 양을 줄이기 위해서 머클 트리를 사용한다.
머클 트리는 키 공간을 버킷으로 나눈 뒤 각 버킷에 포함된 각 키에 균등 분포 해시 함수를 통해 해시 값을 계산하여 노드를 만드는 과정을 상향식으로 진행하여 이진 트리를 만드는 것이다.
두 서버의 머클 트리의 루드 노드 해시 값이 일치한다면 같은 데이터를 갖는 것이다.
이렇게 아래쪽으로 탐색해 나가다 다른 데이터를 갖는 버킷을 찾으면 해당 버킷만 동기화하면 된다.
머클 트리를 사용하면 동기화해야 하는 데이터의 양은 실제로 존재하는 차이의 크기에 비례할 뿐, 두 서버에 보관된 데이터의 총량과는 무관해진다.

7장. 분산 시스템을 위한 유일 ID 생성기 설계

유일 ID 생성기를 설계하려 할 때 auto_increment 속성이 설정된 관계형 데이터베이스의 기본 키를 쓰면 되지 않을까라고 생각하게 된다.
하지만 데이터베이스 서버 한 대로는 분산 환경일 때의 요청을 감당할 수 없을뿐더러, 여러 데이터베이스 서버를 쓰는 경우에는 지연 시간을 낮추기가 무척 힘들 것이다.

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

지원자: ID는 어떤 특성을 갖나요?
면접관: ID는 유일해야 하고, 정렬 가능해야 합니다.
지원자: 새로운 레코드에 붙일 ID는 항상 1만큼 큰 값이어야 하나요?
면접관: ID의 값은 시간이 흐름에 따라 커질 테지만 언제나 1씩 증가한다고 할 수는 없습니다. 다만 확실한 것은, 아침에 만든 ID보다는 저녁에 만든 ID가 큰 값을 갖는다는 점입니다.
지원자: ID는 숫자로만 구성되나요?
면접관: 그렇습니다.
지원자: 시스템 규모는 어느 정도입니까?
면접관: 초당 10,000 ID를 생성할 수 있어야 합니다.
만족해야 할 요구사항은 아래와 같다.
ID는 유일해야 한다.
ID는 숫자로만 구성되어야 한다.
ID는 64비트로 표현될 수 있는 값이어야 한다.
ID는 발급 날짜에 따라 정렬 가능해야 한다.
초당 10,000개의 ID를 만들 수 있어야 한다.

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

다중 마스터 복제
이 접근법은 데이터베이스의 auto_increment 기능을 활용한다.
다만 다음 ID의 값을 구할 때 1만큼 증가시켜 얻는 것이 아니라, 현재 사용 중인 데이터베이스 서버의 수인 k만큼 증가시킨다.
이렇게 하면 규모 확장성 문제를 어느 정도 해결할 수 있는데, 데이터베이스 수를 늘리면 초당 생산 가능 ID 수도 늘릴 수 있기 때문이다.
하지만 이 방법은 데이터 센터를 여러 개로 늘리기 어려우며, ID의 유일성은 보장되지만 시간 흐름에 맞춰 커지도록 보장하지 못하고, 서버를 추가하거나 삭제할 때도 잘 동작하도록 만들기 어렵다.
UUID
UUID는 컴퓨터 시스템에 저장되는 정보를 유일하게 식별하기 위한 128비트짜리 수다.
중복 UUID가 1개 생길 확률을 50%로 끌어 올리려면 초당 10억 개의 UUID를 100년동안 계속해서 만들어야 한다.
UUID는 공통적인 형태이기 때문에 서버 간 조율 없이 독립적으로 생성 가능하다.
장점은 단순하며, 동기화 이슈도 없고, 규모 확장에도 쉽다.
단점은 128비트로 길고, 시간순으로 정렬할 수 없고, 숫자가 아닌 형태라는 것이다.
티켓 서버
이 아이디어의 핵심은 auto_increment 기능을 갖춘 티켓 데이터베이스 서버를 중앙 집중형으로 하나만 사용하는 것이다.
장점은 구현하기가 쉽고, 작은 규모의 어플리케이션에 적합하며, 유일성이 보장되는 숫자로만 된 ID를 쉽게 만들 수 있다.
단점은 티켓 서버가 SPOF가 되며, 이 이슈를 피하기 위해 여러 대의 티켓 서버를 준비하면 데이터 동기화의 문제가 생기게 된다.
트위터 스노플레이크 접근법
트위터는 스노플레이크라고 부르는 독창적인 ID 생성 기법을 사용한다.
사인 비트
1비트를 할당하며, 나중을 위해 음수와 양수를 구별할 수 있게끔 한다.
타임스탬프
41비트를 할당하며, epoch 시각 이후로 몇 밀리초가 경과했는지를 나타내는 값이다.
데이터센터 ID
5비트를 할당하며, 2^5 = 32개의 데이터센터를 지원할 수 있다.
서버 ID
5비트를 할당하며, 데이터센터당 32개의 서버를 사용할 수 있다.
일련번호
12비트를 할당하며, 각 서버는 ID를 생성할 때마다 이 일련번호를 1만큼 증가시키고 1밀리초가 경과할 때마다 0으로 다시 초기화한다.

3단계. 상세 설계

개략적인 설계를 통해 분산 시스템에서 사용할 유일성 보장 ID 생성기로 트위터 스노플레이크 접근법을 선택하였다.
데이터센터 ID와 서버 ID는 시스템이 시작될 때 결정되며, 시스템 운영 중 변경된다면 ID 충돌이 발생할 수 있기 때문에 신중해야 한다.
타임스탬프는 41비트로 최댓값인 약 69년동안만 정상 동작하기 때문에 69년이 지나면 기epoch 시각을 바꾸거나 ID 체계를 다른 것으로 이전하여야 한다.
일련번호는 서버가 같은 밀리초 동안 하나 이상의 ID를 만들어 낸 경우에만 0보다 큰 값을 갖게 되며 12비트인 4096개의 값을 가질 수 있다.

4단계. 마무리

설계를 진행하고 시간이 조금 남았다면 면접관과 다음을 추가로 논의할 수도 있을 것이다.
시계 동기화
설계에선 ID 생성 서버들이 전부 같은 시계를 사용한다고 가정하였지만, 하나의 서버가 여러 코어에서 실행되거나 물리적으로 독립된 여러 장비에서 실행될 경우 유효하지 않을 수 있다.
NTP(Network Time Protocol)은 이 문제를 해결하는 가장 보편적인 수단이다.
각 절(section)의 길이 최적화
동시성이 낮고 수명이 긴 어플리케이션이라면 일련번호 절의 길이를 줄이고 타임스탬프 절의 길이를 늘리는 것이 효과적일 수도 있을 것이다.
고가용성
ID 생성기는 필수 불가결 컴포넌트이므로 아주 높은 가용성을 제공해야 할 것이다.

8장. URL 단축기 설계

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

지원자: URL 단축기가 어떻게 동작해야 하는지 예제를 보여주실 수 있을까요?
면접관: https://www.systeminterview.com/q=chatsystem&c=loggedin&v=v3&l=long이 입력으로 주어졌다고 해 봅시다. 이 서비스는 https://tinyurl.com/y7ke-ocwj와 같은 단축 URL을 결과로 제공해야 합니다. 이 URL에 접속하면 원래 URL로 갈 수도 있어야 하죠.
지원자: 트래픽 규모는 어느 정도일까요?
면접관: 매잉ㄹ 1억 개의 단축 URL을 만들어 낼 수 있어야 합니다.
지원자: 단축 URL의 길이는 어느 정도여야 하나요?
면접관: 짧으면 짧을수록 좋습니다.
지원자: 단축 URL에 포함될 문자에 제한이 있습니까?
면접관: 단축 URL에는 숫자(0부터 9까지)와 영문자(a부터 z, A부터 Z까지)만 사용할 수 있습니다.
지원자: 단축된 URL을 시스템에서 지우거나 갱신할 수 있습니까?
면접관: 시스템을 단순화하기 위해 삭제나 갱신은 할 수 없다고 가정합시다.
이 시스템의 기본적 기능은 아래와 같다.
주어진 긴 URL을 훨씬 짧게 줄인다.
축약된 URL로 HTTP 요청이 오면 원래 URL로 안내
높은 가용성과 규모 확장성, 그리고 장애 감내성이 요구됨
개략적 추정
쓰기 연산 : 매일 1억 개의 단축 URL 생성
초당 쓰기 연산 : 1억 / 24 / 3600 = 1160회
초당 읽기 연산 : 읽기 연산과 쓰기 연산 비율은 10:1이라고 하면, 1160 * 10 = 11,600회
보관 레코드 수 : 10년간 운영한다고 가정하면, 1억 * 365 * 10 = 3650억개
저장 용량 : 축약 전 URL의 평균 길이를 100바이트라고 하면, 3650억 * 100 = 36.5TB

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

URL 단축기는 기본적으로 두 개의 엔드포인트를 필요로 한다.
URL 단축용 엔드포인트
/api/v1/data/shorten의 형태로 단축할 URL을 인자로 실어서 POST 요청을 보내고 단축된 URL을 반환받는다.
URL 리다이렉션용 엔드포인트
/api/v1/shortUrl의 형태로 단축 URL에 대해서 HTTP GET 요청이 오면 원래 URL을 반환한다.
단축 URL을 받은 서버는 원래 URL로 바꾸어서 301 또는 302 응답의 Location 헤더에 넣어서 반환한다.
301 Permanently Moved
이 응답은 해당 URL에 대한 HTP 요청의 처리 책임이 영구적으로 Location 헤더에 반환된 URL로 이전되었다는 응답이다.
브라우저는 이 응답을 캐시하며 추후 같은 단축 URL에 요청을 보낼 필요가 있을 때 캐시된 원래 URL로 요청을 보내게 된다.
서버 부하를 줄이기 위해선 이 응답을 사용하는 것이 좋다.
302 Found
이 응답은 주어진 URL로의 요청이 일시적으로 Location 헤더가 지정하는 URL에 의해 처리되어야 한다는 응답이다.
브라우저의 요청은 언제나 단축 URL 서버에 먼저 보내진 후 원래 URL로 리다이렉션되어야 한다.
클릭 발생률이나 발생 위치를 추적하는 트래픽 분석이 중요할 때는 이 응답을 사용하는 것이 좋다.
단축 URL이 www.tinyurl.com/{hashValue}와 같은 형태라고 하면 중요한 것은 긴 URL을 이 해시 값으로 대응시킬 해시 함수를 찾는 것이며 해시 함수는 다음 요구사항을 만족해야 한다.
입력으로 주어지는 긴 URL이 다른 값이면 해시 값도 달라야 한다.
계산된 해시 값은 원래 입력으로 주어졌던 긴 URL로 복원될 수 있어야 한다.

3단계. 상세 설계

데이터 모델
개략적인 설계를 할 땐 모든 것을 해시 테이블에 두었지만 메모리는 유한한 데다 비싸기 때문에 실제 시스템에서 쓰기에는 곤란하다.
더 나은 방법은 <단축 URL, 원래 URL>의 순서쌍을 관계형 데이터베이스에 저장하는 것이다.
해시 함수
해시 함수는 원래 URL을 단축 URL로 변환하는데 사용된다.
해시 값은 [0-9, a-z, A-Z]의 문자들로 구성되며, 사용할 수 있는 문자의 개수는 10 + 26 + 26 = 62개다.
설계 요구사항을 만족하는 해시 값의 길이를 정하기 위해서는 62^n ≥ 3650억을 만족하는 n의 최솟값을 찾아야 한다.
해시 후 충돌 해소
원래 URL을 줄인 해시 값이 사전에 정한 길이보다 길다면 앞에서부터 원하는 길이만큼 잘라서 사용해야 한다.
하지만 이렇게 하면 해시 결과가 서로 충돌할 확률이 높아지기 때문에 충돌이 해소될 때까지 사전에 정한 문자열을 원래 URL에 덧붙인 후 다시 해시한다.
이 방법을 쓰면 충돌은 해소할 수 있지만 한 번 이상 데이터베이스 질의를 해야 하므로 오버헤드가 크다.
데이터베이스 대신 블룸 필터를 사용하면 성능을 높일 수 있다.
base-62 변환
진법(base) 변환은 URL 단축기를 구현할 때 흔히 사용되는 접근법이며, 수의 표현 방식이 다른 두 시스템이 같은 수를 공유하여야 하는 경우에 유용하다.
62진법을 쓰는 이유는 설계 요구사항에서 해시 값으로 사용할 수 있는 문자의 개수가 62개이기 때문이다.
URL 단축기 상세 설계
URL 단축기는 시스템의 핵심 컴포넌트이므로, 처리 흐름이 논리적으로는 단순해야 하고 기능적으로는 언제나 동작하는 상태로 유지되어야 한다.
처리 흐름은 다음과 같다.
1.
입력으로 긴 URL을 받는다.
2.
데이터베이스에 해당 URL이 있는지 검사한다.
3.
데이터베이스에 있다면 해당 URL에 대한 단축 URL을 만든 적이 있는 것이므로, 데이터베이스에서 해당 단축 URL을 가져와서 클라이언트에게 반환한다.
4.
데이터베이스에 없는 경우에는 해당 URL은 새로 접수된 것이므로, 유일한 ID를 생성하고 데이터베이스의 기본 키로 사용한다.
5.
62진법 변환을 적용, ID를 단축 URL로 만든다.
6.
ID, 단축 URL, 원래 URL로 새 데이터베이스 레코드를 만든 후 단축 URL을 클라이언트에 전달한다.
예제는 다음과 같다.
입력된 URL이 https://en.wikipedia.org/wiki/Systems_design 이라고 하자.
이 URL에 대해 ID 생성기가 반환한 ID는 2009215674938이다.
이 ID를 62진수로 변환하면 zn9edcu를 얻는다.
ID는 2009215674938, shortURL은 zn9edcu, longURL은 https://en.wikipedia.org/wiki/Systems_design의 새로운 데이터베이스 레코드를 만든다.
URL 리다이렉션 상세 설계
쓰기보다 읽기를 더 자주 하는 시스템이라 <단축 URL, 원래 URL>의 쌍을 캐시에 저장하여 성능을 높였다.
로드밸런서의 동작 흐름은 다음과 같이 요약할 수 있다.
1.
사용자가 단축 URL을 클릭한다.
2.
로드밸런서가 해당 클릭으로 발생한 요청을 웹 서버에 전달한다.
3.
단축 URL이 이미 캐시에 있는 경우에는 원래 URL을 바로 꺼내서 클라이언트에게 전달한다.
4.
캐시에 해당 단축 URL이 없는 경우에는 데이터베이스에서 꺼낸다. 데이터베이스에 없다면 아마 사용자가 잘못된 단축 URL을 입력한 경우일 것이다.
5.
데이터베이스에서 꺼낸 URL을 캐시에 넣은 후 사용자에게 반환한다.

4단계. 마무리

설계를 마친 후에 시간이 남는다면 다음과 같은 것을 면접관과 이야기할 수 있을 것이다.
처리율 제한 장치
지금까지 설계한 시스템은 엄청난 양의 URL 단축 요청이 밀려들 경우 무력화될 수 있다는 잠재적 결함을 갖고 있다.
처리율 제한 장치를 두면 IP 주소를 비롯한 필터링 규칙들을 이용해 요청을 걸러낼 수 있을 것이다.
웹 서버의 규모 확장
이 설계에 포함된 웹 계층은 무상태 계층이므로 웹 서버를 자유로이 증설하거나 삭제할 수 있다.
데이터 분석 솔루션
URL 단축기에 데이터 분석 솔루션을 통합해 두면 어떤 링크를 얼마나 많은 사용자가 클릭했는지, 언제 주로 클릭했는지 등 중요한 정보를 알아낼 수 있을 것이다.
가용성, 데이터 일관성, 안정성
대규모 시스템이 성공적으로 운영되기 위해서는 반드시 갖추어야 할 속성들이다.

9장. 웹 크롤러 설계

웹 크롤러는 검색 엔진에서 널리 쓰이는 기술로 몇 개 웹 페이지에서 시작하여 그 링크를 따라 나가면서 웹에 새로 올라오거나 갱신된 컨텐츠를 찾아내어 수집하는 것이 주된 목적이다.
검색 엔진 인덱싱
가장 보편적인 용례로 웹 페이지를 모아 검색 엔진을 위한 로컬 인덱스를 만든다.
Googlebot은 구글 검색 엔진이 사용하는 웹 크롤러다.
웹 아카이빙
나중에 사용할 목적으로 장기보관하기 위해 웹에서 정보를 모으는 절차를 말한다.
많은 국립 도서관이 크롤러를 돌려 웹 사이트를 아카이빙하고 있다.
웹 마이닝
인터넷에서 유용한 지식을 도출해 낸다.
유명 금융 기업들은 크롤러를 사용해 주주 총회 자료나 연차 보고서를 다운받아 기업의 핵심 사업 방향을 알아내기도 한다.
웹 모니터링
인터넷에서 저작권이나 상표권이 침해되는 사례를 모니터링한다.
디지마크사는 웹 크롤러를 사용해 해적판 저작물을 찾아내서 보고한다.
우선 우리가 설계할 웹 크롤러가 감당해야 하는 데이터의 규모와 기능들을 알아내야만 한다.

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

지원자: 이 크롤러의 주된 용도는 무엇인가요? 검색 엔진 인덱스 생성용인가요? 아니면 데이터 마이닝? 아니면 그 외에 다른 용도가 있나요?
면접관: 검색 엔진 인덱싱에 쓰일 것입니다.
지원자: 매달 얼마나 많은 웹 페이지를 수집해야 하나요?
면접관: 10억 개의 웹 페이지를 수집해야 합니다.
지원자: 새로 만들어진 웹 페이지나 수정된 웹 페이지도 고려해야 하나요?
면접관: 그렇습니다.
지원자: 수집한 웹 페이지는 저장해야 합니까?
면접관: 네. 5년간 저장해 두어야 합니다.
지원자: 중복된 컨텐츠는 어떻게 해야 하나요?
면접관: 중복된 컨텐츠를 갖는 페이지는 무시해도 됩니다.
면접관과 크롤러 기능 요구사항을 명확히 하는 한편, 좋은 웹 크롤러가 만족시켜야 할 다음과 같은 속성에 주의를 기울이는 것도 바람직하다.
규모 확장성
웹은 거대하기 때문에 병행성을 활용하면 보다 효과적으로 웹 크롤링을 할 수 있을 것이다.
안정성
웹은 잘못 작성된 HTML, 아무 반응이 없는 서버, 장애, 악성 코드가 붙어 있는 링크 등의 함정으로 가득하다.
크롤러는 이런 비정상적 입력이나 환경에 잘 대응할 수 있어야 한다.
예절
크롤러는 수집 대상 웹 사이트에 짧은 시간 동안 너무 많은 요청을 보내서는 안 된다.
확장성
새로운 형태의 컨텐츠를 지원하기가 쉬워야 한다.
예를 들어, 이미지 파일도 크롤링하기 위해 전체 시스템을 새로 설계해야 한다면 곤란할 것이다.
개략적 추정
매달 10억 개의 웹 페이지를 다운로드한다.
QPS = 10억 / 30일 / 24시간 / 3600초 = 대략 초당 400페이지
최대 QPS = 2 * QPS = 800
웹 페이지의 크기 평균은 500k라고 가정하면 10억 페이지 * 500k = 월 500TB의 저장 용량
5년간 보관한다고 가정하면 500TB * 12개월 * 5년 = 30PB의 저장 용량

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

시작 URL 집합
시작 URL 집합은 웹 크롤러가 크롤링을 시작하는 출발점이다.
전체 웹을 크롤링해야 하는 경우에는 크롤러가 가능한 한 많은 링크를 탐색할 수 있도록 하는 URL을 고르는 것이 바람직하다.
일반적으로는 전체 URL 공간을 작은 부분집합으로 나누는 전략을 쓴다.
시작 URL로 무엇을 쓸 것이냐는 질문에 정답은 없기 때문에 의도가 무엇인지만 정확히 전달하도록 하자.
미수집 URL 저장소
현대적 웹 크롤러는 크롤링 상태를 다운로드할 URL과 다운로드된 URL의 두 가지로 나눠 관리한다.
이 중 다운로드할 URL을 저장 관리하는 컴포넌트가 미수집 URL 저장소이며 FIFO 큐를 사용한다.
HTML 다운로더
미수집 URL 저장소가 제공한 다운로드할 페이지의 URL을 통해 인터넷에서 웹 페이지를 다운로드한다.
도메인 이름 변환기
웹 페이지를 다운받으려면 URL을 IP 주소로 변환하는 절차가 필요하며, HTML 다운로더는 도메인 이름 변환기를 사용하여 URL에 대응되는 IP 주소를 알아낸다.
컨텐츠 파서
웹 페이지를 다운로드하면 이상한 웹 페이지는 문제를 일으킬 수 있는데다 저장 공간만 낭비하게 되어 파싱과 검증 절차를 거쳐야 한다.
크롤링 서버 안에 컨텐츠 파서를 구현하면 크롤링 과정이 느려지게 될 수 있으므로 독립된 컴포넌트로 만든다.
중복 컨텐츠인가?
적절한 자료 구조를 도입하여 데이터 중복을 줄이고 데이터 처리에 소요되는 시간을 줄여야 한다.
두 HTML 문서를 비교하는 가장 간단한 방법은 문자열로 보고 비교하는 것이지만, 비교 대상 문서의 수가 많다면 느리고 비효율적이기 때문에 해시 값을 통해 비교한다.
URL 추출기
URL 추출기는 HTML 페이지를 파싱하여 링크들을 골라내는 역할을 한다.
상대 경로를 사용하는 경우 도메인 이름을 붙여 절대 경로로 변환한다.
URL 필터
URL 필터는 특정한 컨텐츠 타입이나 파일 확장자를 갖는 URL, 접속 시 오류가 발생하는 URL, 접근 제외 목록에 포함된 URL 등을 크롤링 대상에서 배제하는 역할을 한다.
이미 방문한 URL?
이미 방문한 적 있는 URL인지 추적하면 같은 URL을 여러 번 처리하는 일을 방지할 수 있으므로 서버 부하를 줄이고 시스템이 무한 루프에 빠지는 일을 방지할 수 있다.
해당 자료 구조로는 블룸 필터나 해시 테이블이 널리 쓰인다.
URL 저장소
이미 방문한 URL을 보관한다.

3단계. 상세 설계

DFS vs BFS
웹은 페이지가 노드이며 URL은 간선인 방향이 있는 그래프와 같으며, 크롤링 프로세스는 이 그래프를 간선을 따라 탐색하는 과정이다.
DFS, BFS는 그래프 탐색에 널리 사용되는 두 가지 알고리즘이다.
DFS는 그래프 크기가 클 경우 어느 정도로 깊숙이 가게 될지 가늠하기 어렵기 때문에 좋은 선택이 아닐 가능성이 높으므로 보통 BFS를 사용한다.
하지만 한 페이지에서 나오는 링크의 상당수는 같은 서버의 다른 페이지를 참조하는 링크이기 때문에 한 서버에 수많은 요청으로 과부하에 걸리게 될 것이다.
또한 표준적 BFS 알고리즘은 우선순위를 두지 않기 때문에 페이지 순위, 사용자 트래픽의 양, 업데이트 빈도 등 여러 가지 척도에 따라 처리 우선순위를 구별할 수 없다.
예의 바른 크롤러
예의 바른 크롤러를 만드려면 동일 웹 사이트에 대해서는 한 번에 한 페이지만 요청해야 하며, 같은 웹 사이트의 페이지를 다운받는 태스크를 시간차를 두고 실행하도록 하면 될 것이다.
이 요구사항을 만족하려면 웹사이트의 호스트명과 다운로드를 수행하는 작업 스레드 사이의 관계를 유지하면 된다.
큐 라우터를 통해 같은 호스트에 속한 URL은 언제나 같은 큐로 가도록 보장하며, 큐 라우터는 매핑 테이블을 통해 호스트 이름과 큐 사이의 관계를 보관하고 찾는다.
또한 각 작업 스레드마다 별도의 FIFO 큐를 갖게 하고, 큐 선택기를 통해 큐들을 순회하면서 꺼낸 URL을 해당 큐를 갖고 있는 작업 스레드에 전달하며, 작업 스레드는 일정한 지연시간을 두고 순차적으로 처리한다.
우선순위를 구분하는 크롤러
유용성에 따라 URL의 우선순위를 나눌 때는 페이지랭크, 트래픽의 양, 갱신 빈도 등 다양한 척도를 사용할 수 있다.
큐 라우터를 통해 URL이 큐로 가기 전 순위결정장치를 거쳐 우선순위를 계산하도록 하며, 큐 선택기가 URL을 꺼낼 때 우선순위가 높은 URL을 더 많이 선택하도록 한다.
이를 통해 우선순위 결정 과정을 처리하는 전면 큐, 크롤러가 예의 바르게 동작하도록 보증하는 후면 큐로 나눌 수 있다.
신선도를 유지하는 크롤러
웹 페이지는 수시로 추가되고, 삭제되고, 변경되기 때문에 데이터의 신선함을 유지하기 위해서는 이미 다운로드한 페이지라고 해도 주기적으로 재수집할 필요가 있다.
그러나 모든 URL을 재수집하는 것은 많은 시간과 자원이 필요하므로, 웹 페이지의 변경 이력을 활용하거나 우선순위를 활용하여 중요 페이지를 자주 재수집하도록 할 수 있다.
미수집 URL 저장소를 위한 저장장치
처리해야 하는 URL의 수가 수억 개에 달하기 때문에 그 모두를 메모리에 보관하는 것은 안정성이나 규모 확장성 측면에서 바람직하지 않다.
전부 디스크에 저장하는 것도 느리기 때문에 성능 병목지점이 될 수 있어 좋은 방법이 아니다.
따라서 절충안은 대부분의 URL을 디스크에 두지만 IO 비용을 줄이기 위해 메모리 버퍼에 큐를 두는 것이다.
버퍼에 있는 데이터는 주기적으로 디스크에 기록할 것이다.
HTML 다운로더의 성능 최적화
분산 크롤링
성능을 높이기 위해 크롤링 작업을 여러 서버의 여러 스레드를 통해 분산하는 방법이다.
URL 공간을 작은 단위로 분할하여 각 서버가 그 중 일부를 담당하도록 한다.
도메인 이름 변환 결과 캐시
도메인 이름 변환기가 DNS 요청을 보내고 결과를 받는 작업의 동기적 특성 때문에 크롤러 성능 병목 중 하나가 될 수 있다.
따라서 DNS 조회 결과로 얻어진 도메인 이름과 IP 주소 사이의 관계를 캐시에 보관해 놓고 크론 잡을 통해 주기적으로 갱신하도록 하면 성능을 효과적으로 높일 수 있다.
지역성
크롤링 작업을 수행하는 서버를 지역별로 분산한다면 대상 서버와 지역적으로 가까울 때 페이지 다운로드 시간이 줄어들 수 있다.
이런 지역성을 활용하는 전략은 크롤링 서버, 캐시, 큐, 저장소 등 대부분의 컴포넌트에 적용 가능하다.
짧은 타임아웃
응답이 느리거나 아예 응답하지 않는 웹 서버가 존재하며, 대기 시간이 길어지면 좋지 않기 때문에 최대 얼마나 기다릴지 미리 정해둔다.
이 시간 동안 서버가 응답하지 않으면 크롤러는 해당 페이지 다운로드를 중단하고 다음 페이지로 넘어간다.
HTML 다운로더의 안정성
안정 해시
다운로더 서버들의 부하를 분산할 때 적용 가능한 기술로, 다운로더 서버를 쉽게 추가하고 삭제할 수 있다.
크롤링 상태 및 수집 데이터 저장
장애가 발생한 경우에 쉽게 복구할 수 있도록 크롤링 상태와 수집된 데이터를 지속적 저장장치에 기록해 두는 것이 바람직하다.
저장된 데이터를 로딩하고 나면 중단되었던 크롤링을 쉽게 재시작할 수 있다.
예외 처리
예외가 발생해도 전체 시스템이 중단되는 일 없이 그 작업을 우아하게 이어나갈 수 있어야 한다.
데이터 검증
시스템 오류를 방지하기 위한 중요 수단 가운데 하나다.
크롤러의 확장성
새로운 모듈을 끼워 넣음으로써 새로운 형태의 컨텐츠를 지원할 수 있도록 신경 써야 한다.
문제 있는 컨텐츠 감지 및 회피
중복 컨텐츠
HTML에 대한 해시나 체크섬을 사용하면 중복 컨텐츠를 쉽게 탐지할 수 있다.
거미 덫
무한히 깊은 디렉터리 구조를 포함하는 링크가 있다면 크롤러는 무한 루프에 빠질 수 있다.
URL의 최대 길이를 제한하면 회피할 수 있지만, 모든 종류의 덫을 피할 수는 없기 때문에 사람이 수작업으로 덫을 확인하고 URL 필터를 통해 크롤러 탐색 대상에서 제외시켜야 한다.
데이터 노이즈
광고나 스크립트 코드, 스팸 URL 같이 가치가 없는 컨텐츠는 제외해야 한다.

4단계. 마무리

시간이 허락한다면 면접관과 다음과 같은 것을 추가로 논의해보면 좋을 것이다.
서버 측 렌더링
많은 웹사이트가 자바스크립트, Ajax 등의 기술을 통해 링크를 즉석에서 만들어 내기 때문에 동적으로 생성되는 링크는 발견할 수 없을 것이다.
이 문제는 페이지를 파싱하기 전에 서버 측 렌더링을 적용하면 해결할 수 있다.
원치 않는 페이지 필터링
저장 공간 등 크롤링에 소요되는 자원은 유한하기 때문에 스팸 방지 컴포넌트를 두어 품질이 조악하거나 스팸성인 페이지를 걸러내도록 해 두면 좋다.
데이터베이스 다중화 및 샤딩
다중화나 샤딩 같은 기법을 적용하면 데이터 계층의 가용성, 규모 확장성, 안정성이 향상된다.
수평적 규모 확장성
대규모의 크롤링을 하기 위해서는 다운로더 서버가 수천대 필요할 수 있기 때문에 수평적 규모 확장성을 달성하기 위해 서버를 무상태로 만들어야 한다.
가용성, 일관성, 안정성
성공적인 대형 시스템을 만들기 위해 필수적으로 고려해야 하는 개념들이다.
데이터 분석 솔루션
시스템을 세밀히 조정하기 위해서는 데이터를 수집하고 분석하는 것이 중요하다.

10장. 알림 시스템 설계

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

지원자: 이 시스템은 어떤 종류의 알림을 지원해야 하나요?
면접관: 푸시 알림, SMS 메시지, 그리고 이메일입니다.
지원자: 실시간 시스템이어야 하나요?
면접관: 연성 실시간 시스템이라고 가정합니다. 알림은 가능한 한 빨리 전달되어야 하지만 시스템에 높은 부하가 걸렸을 때 약간의 지연은 무방합니다.
지원자: 어떤 종류의 단말을 지원해야 하나요?
면접관: IOS 단말, 안드로이드 단말, 그리고 랩톱/데스크톱을 지원해야 합니다.
지원자: 사용자에게 보낼 알림은 누가 만들 수 있나요?
면접관: 클라이언트 어플리케이션 프로그램이 만들 수도 있구요. 서버 측에서 스케줄링 할 수도 있습니다.
지원자: 사용자가 알림을 받지 않도록 설정할 수도 있어야 하나요?
면접관: 네 해당 설정을 마친 사용자는 더 이상 알림을 받지 않습니다.
지원자: 하루에 몇 건의 알림을 보낼 수 있어야 하나요?
면접관: 천만 건의 모바일 푸시 알림, 백만 건의 SMS 메시지, 5백만 건의 이메일을 보낼 수 있어야 합니다.

2단계. 개략적 설계인 제시 및 동의 구하기

알림 유형별 지원 방안
IOS나 안드로이드에서 푸시 알림을 보내기 위해서는 세 가지 컴포넌트가 필요하다.
알림 제공자(provider)
알림 요청을 만들어 푸시 알림 서비스로 보내는 주체다.
알림 요청을 만드려면 알림 요청을 보내는데 필요한 고유 식별자인 단말 토큰과 알림 내용을 담은 JSON 딕셔너리인 페이로드가 필요하다.
APNS 혹은 FCM
애플이 제공하는 원격 서비스인 APNS나 구글이 제공하는 FCM으로 푸시 알림을 단말 장치로 보내는 역할을 담당한다.
단말 장치
푸시 알림을 수신하는 사용자 단말이다.
SMS 메시지나 이메일은 보통 제3 사업자의 서비스를 많이 이용하며 이용 요금을 낸다.
연락처 정보 수집 절차
알림을 보내려면 모바일 단말 토큰, 전화번호, 이메일 주소 등의 정보가 필요하다.
사용자가 우리 앱을 설치하거나 처음으로 계정을 등록하면 API 서버는 해당 사용자의 정보를 수집하여 데이터베이스에 저장한다.
데이터베이스의 개략적인 설계안에는 한 사용자가 여러 단말을 가질 수 있고, 알림은 모든 단말에 전송되어야 한다는 점을 고려하였다.
알림 전송 및 수신 절차
개략적 설계안의 초안으로 각 시스템 컴포넌트를 설계하면 다음과 같다.
1부터 N까지의 서비스
서비스는 각각 마이크로서비스일 수도 있고, 크론잡일 수도 있고, 분산 시스템 컴포넌트일 수도 있다.
알림 시스템
알림 시스템은 알림 전송/수신 처리의 핵심이다.
이 시스템은 서비스 1~N개의 알림 전송을 위한 API를 제공해야 하고, 제3자 서비스에 전달할 알림 페이로드를 만들어낼 수 있어야 한다.
제3자 서비스
사용자에게 알림을 실제로 전달하는 역할을 하며 사용 시 유의할 것은 확장성이다.
쉽게 새로운 서비스를 통합하거나 기존 서비스를 제거할 수 있어야 한다.
또한 FCM은 중국에선 사용할 수 없기 때문에 JPush, PushY 같은 서비스를 사용해야 한다는 것도 고려해야 한다.
하지만 이 설계에는 몇 가지 문제가 있다.
SPOF
알림 서비스에 서버가 하나 밖에 없다면 그 서버의 장애가 전체 서비스의 장애로 이어진다.
규모 확장성
한대 서비스로 푸시 알림에 관계된 모든 것을 처리하면 데이터베이스나 캐시 등 중요 컴포넌트의 규모를 개별적으로 늘릴 방법이 없다.
성능 병목
알림을 처리하고 보내는 것은 자원을 많이 필요로 하는 작업일 수 있다.
따라서 모든 것을 한 서버로 처리하면 사용자 트래픽이 많이 몰리는 시간에는 시스템이 과부하 상태에 빠질 수 있다.
이러한 문제점들을 개선한 개략적 설계는 다음과 같다.
1부터 N까지의 서비스
알림 시스템 서버의 API를 통해 알림을 보낼 서비스들이다.
알림 서버
사내 서비스 또는 인증된 클라이언트만 이용 가능한 알림 전송 API를 제공한다.
이메일 주소, 전화번호 등에 대한 기본적인 알림 검증을 수행한다.
알림에 포함시킬 데이터를 데이터베이스 또는 캐시에 질의를 통해 가져온다.
완성된 알림 데이터를 메시지 큐에 넣는다.
캐시
사용자 정보, 단말 정보, 알림 템플릿 등을 캐시한다.
데이터베이스
사용자, 알림, 설정 등 다양한 정보를 저장한다.
메시지 큐
시스템 컴포넌트 간 의존성을 제거하기 위해 사용한다.
다량의 알림이 전송되어야 하는 경우를 대비한 버퍼 역할도 한다.
알림 종류별로 별도 메시지 큐를 사용해서 일부 제3자 서비스의 장애가 발생하더라도 다른 종류의 알림은 정상적으로 동작한다.
작업 서버
메시지 큐에서 전송할 알림을 꺼내서 제3자 서비스로 전달하는 역할을 담당하는 서버다.
제3자 서비스
사용자에게 알림을 실제로 전달하는 역할을 한다.
단말 장치
푸시 알림을 수신하는 사용자 단말이다.

3단계. 상세 설계

안정성
분산 환경에서 운영될 알림 시스템을 설계할 때는 안정성을 확보하기 위한 사항 몇 가지를 반드시 고려해야 한다.
데이터 손실 방지
알림 시스템은 알림 데이터를 데이터베이스에 보관하고 재시도 메커니즘을 구현해야 한다.
알림 중복 전송 방지
분산 시스템의 특성상 가끔은 같은 알림이 중복되어 전송되기도 한다.
그 빈도를 줄이려면 중복을 탐지하는 메커니즘을 도입하고 오류를 신중하게 처리해야 한다.
알림에 이벤트 ID를 사용하여 검사하는 간단한 중복 방지 로직이 있다.
추가로 필요한 컴포넌트 및 고려사항
알림 템플릿
알림 메시지 대부분의 형식은 비슷하기 때문에 인자나 스타일, 추적 링크를 조정하여 사전에 지정한 형식에 맞는 알람을 만들어내야 한다.
템플릿을 사용하면 전송될 알림들의 형식을 일관성 있게 유지할 수 있고, 오류 가능성뿐 아니라 알림 작성에 드는 시간도 줄일 수 있다.
알림 설정
많은 웹사이트와 앱에서는 사용자가 알림 설정을 상세히 조정할 수 있도록 하고 있다.
이 정보는 알림 설정 테이블에 보관되며 특정 종류의 알림을 보내기 전에 반드시 해당 사용자가 해당 알림을 켜 두었는지 확인해야 한다.
전송률 제한
알림을 너무 많이 보내기 시작하면 사용자가 알림 기능을 아예 꺼 버릴 수도 있기 때문에 한 사용자가 받을 수 있는 알림의 빈도를 제한해야 한다.
재시도 방법
제3자 서비스가 알림 전송에 실패하면 해당 알림을 재시도 전용 큐에 넣고, 같은 문제가 계속해서 발생하면 개발자에게 통지한다.
푸시 알림과 보안
IOS와 안드로이드 앱의 알림 전송 API는 appKey와 appSecret을 사용하여 보안을 유지한다.
따라서 인증된 혹은 승인된 클라이언트만 해당 API를 사용하여 알림을 보낼 수 있다.
큐 모니터링
알림 시스템을 모니터링할 때 중요한 메트릭 하나는 큐에 쌓인 알림의 개수이다.
이 수가 너무 크면 작업 서버들이 이벤트를 빠르게 처리하고 있지 못하다는 뜻이므로 작업 서버를 증설하는게 바람직할 것이다.
이벤트 추적
알림 확인율, 클릭율, 실제 앱 사용으로 이어지는 비율 같은 메트릭은 사용자를 이해하는데 중요하다.
따라서 보통 알림 시스템을 만들면 데이터 분석 서비스와도 통합해야만 한다.
수정된 개략적 설계안
알림 서버에 인증과 전송률 제한 기능이 추가되었다.
전송 실패에 대응하기 위해 실패한 알림을 다시 큐에 넣고 지정한 횟수만큼 재시도하는 기능이 추가되었다.
전송 템플릿을 사용하여 알림 생성 과정을 단순화하고 알림 내용의 일관성을 유지한다.
모니터링과 추적 시스템을 추가하여 시스템 상태를 확인하고 추후시스템을 개선하기 쉽도록 하였다.

4단계. 마무리

알림 시스템을 만들며 각 컴포넌트의 구현 방법과 최적화 기법에 대해서 알아보았으며, 특히 아래 주제에 집중하였다.
안정성
메시지 전송 실패율을 낮추기 위해 안정적인 재시도 메커니즘을 도입하였다.
보안
인증된 클라이언트만이 알림을 보낼 수 있도록 appKey, appSecret 등의 메커니즘을 이용하였다.
이벤트 추적 및 모니터링
알림이 만들어진 후 성공적으로 전송되기까지의 과정을 추적하고 시스템 상태를 모니터링하기 위해 알림 전송의 각 단계마다 이벤트를 추적하고 모니터링할 수 있는 시스템을 통합하였다.
사용자 설정
사용자가 알림 수신 설정을 조정할 수 있도록 하였다.
따라서 알림을 보내기 전 반드시 해당 설정을 확인하도록 시스템 설계를 변경하였다.
전송률 제한
사용자에게 알림을 보내는 빈도를 제한할 수 있도록 하였다.

11장. 뉴스 피드 시스템 설계

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

지원자: 모바일 앱을 위한 시스템인가요? 아니면 웹? 둘 다 지원해야 합니까?
면접관: 둘 다 지원해야 합니다.
지원자: 중요한 기능으로는 어떤 것이 있을까요?
면접관: 사용자는 뉴스 피드 페이지에 새로운 스토리를 올릴 수 있어야 하고, 친구들이 올리는 스토리를 볼 수도 있어야 합니다.
지원자: 뉴스 피드에는 어떤 순서로 스토리가 표시되어야 하나요? 최신 포스트가 위에 오도록 해야 하나요? 아니면 토픽 점수 같은 다른 기준이 있습니까? 예를 들어, 가까운 친구의 포스트는 좀 더 위에 배치되어야 한다는가 하는.
면접관: 그냥 단순히 시간 흐름 역순으로 표시된다고 가정합시다.
지원자: 한 명의 사용자는 최대 몇 명의 친구를 가질 수 있습니까?
면접관: 5000명입니다.
지원자: 트래픽 규모는 어느 정도입니까?
면접관: 매일 천만 명이 방문한다고 가정합시다.
지원자: 피드에 이미지나 비디오 스토리도 올라올 수 있습니까?
면접관: 스토리에는 이미지나 비디오 등의 미디어 파일이 포함될 수 있습니다.

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

피드 발행
사용자가 스토리를 포스팅하면 해당 데이터를 캐시와 데이터베이스에 기록한다.
새 포스팅은 친구의 뉴스 피드에도 전송된다.
사용자
모바일 앱이나 브라우저에서 새 포스팅을 올리는 주체다.
로드밸런서
트래픽을 웹 서버들로 분산한다.
웹 서버
HTTP 요청을 내부 서비스로 중계하는 역할을 담당한다.
포스팅 저장 서비스
새 포스팅을 데이터베이스와 캐시에 저장한다.
포스팅 전송 서비스
새 포스팅을 친구의 뉴스 피드에 푸시한다.
뉴스 피드 데이터는 캐시에 보관하여 빠르게 읽어갈 수 있도록 한다.
알림 서비스
친구들에게 새 포스팅이 올라왔음을 알리거나, 푸시 알림을 보내는 역할을 담당한다.
뉴스 피드 생성
지면 관계상 뉴스 피드는 모든 친구의 포스팅을 시간 흐름 역순으로 모아서 만든다고 가정한다.
사용자
뉴스 피드를 읽는 주체다.
로드밸런서
트래픽을 웹 서버들로 분산한다.
웹 서버
트래픽을 뉴스 피드 서비스로 보낸다.
뉴스 피드 서비스
캐시에서 뉴스 피드를 가져오는 서비스다.
뉴스 피드 캐시
뉴스 피드를 렌더링할 때 필요한 피드 ID를 보관한다.

3단계. 상세 설계

피드 발행 흐름 상세 설계
웹 서버
웹 서버는 클라이언트와 통신할 뿐 아니라 인증이나 처리율 제한 등의 기능도 수행한다.
올바른 인증 토큰을 Authorization 헤더에 넣고 API를 호출하는 사용자만 포스팅할 수 있어야 한다.
또한, 스팸을 막고 유해한 컨텐츠가 자주 올라오는 것을 방지하기 위해서 특정 기간 동안 한 사용자가 올릴 수 있는 포스팅의 수에 제한을 두어야 한다.
포스팅 전송(팬아웃) 서비스
포스팅 전송, 즉 팬아웃은 어떤 사용자의 새 포스팅을 그 사용자와 친구 관계에 있는 모든 사용자에게 전달하는 과정으로 쓰기 시점에 팬아웃(Push 모델)과 읽기 시점에 팬아웃(Pull 모델)이 있다.
쓰기 시점에 팬아웃하는 모델
새로운 포스팅을 기록하는 시점에 해당 사용자의 캐시에 해당 포스팅을 기록하여 뉴스 피드를 갱신하게 된다.
장점
뉴스 피드가 실시간으로 갱신되며 친구 목록에 있는 사용자에게 즉시 전송된다.
새 포스팅이 기록되는 순간에 뉴스 피드가 이미 갱신되므로 뉴스 피드를 읽는 데 드는 시간이 짧아진다.
단점
친구가 많은 사용자의 경우 친구 목록을 가져오고 그 목록에 있는 사용자 모두의 뉴스 피드를 갱신하는 데 많은 시간이 소요될 수 있다. (핫키 문제)
서비스를 자주 이용하지 않는 사용자의 피드까지 갱신해야 하므로 컴퓨팅 자원이 낭비된다.
읽기 시점에 팬아웃하는 모델
요청에 기반하여 피드를 읽어야 하는 시점에 뉴스 피드를 갱신한다.
장점
비활성화된 사용자 또는 서비스에 거의 로그인하지 않는 사용자의 경우에는 이 모델이 유리하며, 로그인하기까지는 어떤 컴퓨팅 자원도 소모되지 않는다.
데이터를 친구 각각에 푸시하는 작업이 필요 없으므로 핫키 문제도 생기지 않는다.
단점
뉴스 피드를 읽는 데 많은 시간이 소요될 수 있다.
이 두 가지 방법을 결합하여 대부분 사용자에 대해서는 뉴스 피드를 빠르게 가져올 수 있도록 푸시 모델을 사용한다.
친구나 팔로워가 아주 많은 상요자의 경우에는 팔로워로 하여금 해당 사용자의 포스팅을 필요할 때 가져가도록 하는 풀 모델을 사용하여 시스템 과부하를 방지할 것이다.
아울러 안정 해시를 통해 요청과 데이터를 보다 고르게 분산하여 핫키 문제를 줄여볼 것이다.
1.
그래프 데이터베이스에서 친구 ID 목록을 가져온다. 그래프 데이터베이스는 친구 관계나 친구 추천을 관리하기 적합하다.
2.
사용자 정보 캐시에서 친구들의 정보를 가져온다. 그런 후에 사용자 설정에 따라 친구 가운데 일부를 걸러낸다.
3.
친구 목록과 새 스토리의 포스팅 ID를 메시지 큐에 넣는다.
4.
팬아웃 작업 서버가 메시지 큐에서 데이터를 꺼내어 뉴스 피드 데이터를 뉴스 피드 캐시에 넣는다. 뉴스 피드 캐시는 <포스팅 ID, 사용자 ID>의 순서쌍을 보관하는 매핑 테이블이라고 볼 수 있다. 메모리 요구량이 지나치게 늘어날 수 있기 때문에 ID만 보관하며, 캐시의 크기에 조정 가능한 제한을 둔다. 대부분의 사용자가 보려 하는 것은 수 천 개의 스토리 전부가 아닌 최신 스토리이므로 캐시 미스가 일어날 확률은 낮다.
피드 읽기 흐름 상세 설계
1.
사용자가 뉴스 피드를 읽으려는 요청을 보낸다.
2.
로드밸런서가 요청을 웹 서버 가운데 하나로 보낸다.
3.
웹 서버는 피드를 가져오기 위해 뉴스 피드 서비스를 호출한다.
4.
뉴스 피드 서비스는 뉴스 피드 캐시에서 포스팅 ID 목록을 가져온다.
5.
뉴스 피드에 표시할 사용자 이름, 사용자 사진, 포스팅 컨텐츠, 이미지 등을 사용자 캐시와 포스팅 캐시에서 가져와 완전한 뉴스 피드를 만든다.
6.
생성된 뉴스 피드를 JSON 형태로 클라이언트에게 보내며, 클라이언트는 해당 피드를 렌더링한다.
캐시 구조
캐시는 뉴스 피드 시스템의 핵심 컴포넌트며 다섯 계층으로 나눈다.
뉴스 피드
뉴스 피드의 ID를 보관한다.
컨텐츠
포스팅 데이터를 보관한다.
인기 컨텐츠는 따로 보관한다.
소셜 그래프
사용자 간 관계 정보를 보관한다.
행동
포스팅에 대한 사용자의 행위에 관한 정보를 보관한다.
포스팅에 대한 좋아요, 답글 등등이 이에 해당한다.
횟수
좋아요 횟수, 응답 수, 팔로워 수, 팔로잉 수 등의 정보를 보관한다.

4단계. 마무리

데이터베이스 규모 확장
수직적 규모 확장 vs 수평적 규모 확장
SQL vs NoSQL
master-slave 다중화
레플리카에 대한 읽기 연산
일관성 모델
데이터베이스 샤딩
이 외에도 논의해 보면 좋을 만한 주제로는 다음과 같은 것들이 있다.
웹 계층을 무상태로 운영하기
가능한 한 많은 데이터를 캐시할 방법
여러 데이터 센터를 지원할 방법
메시지 큐를 사용하여 컴포넌트 사이의 결합도 낮추기
핵심 메트릭에 대한 모니터링 (피크 QPS, 뉴스 피드 새로고침 지연 시간 등)

12장. 채팅 시스템 설계

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

지원자: 어떤 앱을 설계해야 하나요? 1:1 채팅 앱입니까 아니면 그룹 채팅 앱입니까?
면접관: 둘 다 지원할 수 있어야 합니다.
지원자: 모바일 앱인가요 아니면 웹 앱인가요?
면접관: 둘 다입니다.
지원자: 처리해야 하는 트래픽 규모는 어느 정도입니까?
면접관: 일별 능동 사용자 수 기준으로 5천만 명을 처리할 수 있어야 합니다.
지원자: 그룹 채팅의 경우에 인원 제한이 있습니까?
면접관: 최대 100명까지 참가할 수 있습니다.
지원자: 중요 기능으로는 어떤 것이 있을까요? 가령, 첨부파일도 지원할 수 있어야 하나요?
면접관: 1:1 채팅, 그룹 채팅, 사용자 접속상태 표시를 지원해야 합니다. 텍스트 메시지만 주고받을 수 있습니다.
지원자: 메시지 길이에 제한이 있나요?
면접관: 네. 100000자 이하여야 합니다.
지원자: 종단 간 암호화를 지원해야 하나요?
면접관: 현재로서는 필요 없습니다만 시간이 허락하면 논의해볼 수 있겠습니다.
지원자: 채팅 이력은 얼마나 오래 보관해야 할까요?
면접관: 영원히요.
이 앱은 다음과 같은 기능을 가져야 한다.
응답지연이 낮은 일대일 채팅 기능
최대 100명까지 참여할 수 있는 그룹 채팅 기능
사용자의 접속상태 표시 기능
다양한 단말 지원. 하나의 계정으로 여러 단말에 동시 접속 지원
푸시 알림

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

채팅 시스템의 경우 클라이언트는 모바일 앱이거나 웹 어플리케이션이고, 클라이언트는 서로 직접 통신하지 않고 채팅 서비스와 통신한다.
채팅 시스템은 아래 기능을 제공해야 한다.
클라이언트들로부터 메시지 수신
메시지 수신자 결정 및 전달
수신자가 접속 상태가 아닌 경우에는 접속할 때까지 해당 메시지 보관
메시지 송신 클라이언트가 채팅 서비스에 메시지를 보낼 때 HTTP 프로토콜을 사용하는 것은 괜찮은 선택이다.
HTTP의 keep-alive 헤더를 사용하면 클라이언트와 서버 사이의 연결을 끊지 않고 계속 유지할 수 있으며, TCP 접속 과정의 핸드셰이크 횟수를 줄일 수 있어서 효율적이다.
하지만 채팅 서비스가 메시지 수신 클라이언트에게 임의 시점에 메시지를 보내는데 HTTP 프로토콜은 사용하기가 어렵다.
서버가 연결을 만드는 것처럼 동작할 수 있도록 하기 위해 폴링, 롱 폴링, 웹소켓 등의 기술을 사용해야 한다.
폴링
폴링은 클라이언트가 주기적으로 서버에게 새 메시지가 있느냐고 물어보는 방법이다.
폴링 비용은 폴링을 자주하면 할 수록 올라가며, 답해줄 메시지가 없는 경우에는 서버 자원이 불필요하게 낭비된다.
롱 폴링
폴링의 비효율성을 개선하기 위해 나온 방법이 롱 폴링이다.
롱 폴링의 경우 클라이언트는 새 메시지가 반환되거나 타임아웃 될 때까지 연결을 유지한다.
클라이언트는 새 메시지를 받으면 기존 연결을 종료하고 서버에 새로운 요청을 보내어 모든 절차를 다시 시작한다.
하지만 이 방법에는 다음과 같은 단점이 있다.
HTTP 서버들은 보통 무상태 서버이기 때문에 메시지를 보내는 클라이언트와 수신하는 클라이언트가 같은 채팅 서버에 접속하게 되지 않을 수도 있다.
로드밸런싱을 위해 라운드 로빈 알고리즘을 사용하는 경우, 메시지를 받은 서버는 해당 메시지를 수신할 클라이언트와 롱 폴링 연결을 가지고 있지 않은 서버일 수 있는 것이다.
서버 입장에서는 클라이언트가 연결을 해제했는지 아닌지 알 좋은 방법이 없다.
메시지를 많이 받지 않는 클라이언트도 타임아웃이 일어날 때마다 주기적으로 서버에 다시 접속하기 때문에 여전히 비효율적이다.
웹소켓
웹소켓은 서버가 클라이언트에게 비동기 메시지를 보낼 때 널리 사용하는 기술이다.
웹소켓 연결은 클라이언트가 시작하며, 한번 맺어진 연결은 지속적이며 양방향이다.
이 연결은 처음에는 HTTP 연결이지만 특정 핸드셰이크 절차를 거쳐 웹소켓 연결로 업그레이드된다.
이 지속적인 연결이 만들어지고 나면 서버는 클라이언트에게 비동기적으로 메시지를 전송할 수 있다.
웹소켓은 80이나 443처럼 기본 포트번호를 그대로 쓰기 때문에 방화벽이 있는 환경에서도 잘 동작한다.
웹소켓을 사용하면 메시지를 보낼 때나 받을 때 동일한 프로토콜을 사용할 수 있으므로 설계뿐 아니라 구현도 단순하고 직관적이다.
유의할 것은 웹소켓 연결은 지속적으로 유지되어야 하기 때문에 서버 측에서 연결 관리를 효율적으로 해야 한다는 것이다.
개략적 설계안
클라이언트와 서버 사이의 주 통신 프로토콜로 웹소켓을 사용하기로 결정했지만 다른 부분에서는 굳이 웹소켓을 쓸 필요가 없다.
채팅 시스템은 무상태 서비스, 상태유지 서비스, 제3자 서비스 연동의 세 부분으로 나누어 살펴볼 수 있다.
무상태 서비스
무상태 서비스는 로그인, 회원가입 등을 처리하는 서비스다.
무상태 서비스는 로드밸런서 뒤에 위치하며, 로드밸런서는 요청을 그 경로에 맞는 서비스로 정확하게 전달한다.
이 서비스는 모놀리틱 서비스일 수도 있고 마이크로서비스일 수도 있다.
자세히 살펴봐야 하는 것은 서비스 탐색 서비스이며, 클라이언트가 접속할 채팅 서버의 DNS 호스트명을 클라이언트에게 알려주는 역할을 한다.
상태 유지 서비스
각 클라이언트가 채팅 서버와 독립적인 네트워크 연결을 유지해야 하기 때문에 유일하게 상태 유지가 필요한 서비스다.
클라이언트는 보통 서버가 살아 있는 한 다른 서버로 연결을 변경하지 않는다.
서비스 탐색 서비스는 채팅 서비스와 긴밀히 협력하여 특정 서버에 부하가 몰리지 않도록 한다.
제3자 서비스 연동
채팅 앱에서 가장 중요한 제3자 서비스는 푸시 알림이다.
새 메시지를 받았다면 앱이 실행 중이지 않더라도 알림을 받아야 한다.
규모 확장성
동시 접속자가 1M라고 가정하고 접속당 10K의 서버 메모리가 필요하다고 본다면, 10GB 메모리만 있으면 모든 연결을 다 처리할 수 있다.
하지만 그 정도 규모의 트래픽을 서버 한 대로 처리한다면 SPOF 같은 문제가 있기 때문에 면접에서 좋은 점수를 따기는 어려울 것이다.
서버만 한 대 같은 설계안에서 출발하여 점차 다듬어 나가는 것은 괜찮다.
유의할 것은 실시간으로 메시지를 주고 받기 위해 클라이언트는 채팅 서버와 웹소켓 연결을 끊지 않고 유지한다는 것이다.
채팅 서버는 클라이언트 사이에 메시지를 중계하는 역할을 담당한다.
접속상태 서버는 사용자의 접속 여부를 관리한다.
API 서버는 로그인, 회원가입, 프로파일 변경 등 그 외 나머지 전부를 처리한다.
알림 서버는 푸시 알림을 보낸다.
키-값 저장소에는 채팅 이력을 보관하며, 시스템에 접속한 사용자는 이전 채팅 이력을 전부 보게 될 것이다.
저장소
관계형 데이터베이스를 쓸 것인가? 아니면 NoSQL을 채택할 것인가?
이 질문에 대한 올바른 답을 하기 위해 중요하게 따져야 할 것은, 데이터의 유형과 읽기/쓰기 연산의 패턴이다.
사용자 프로파일, 설정, 친구 목록처럼 일반적인 데이터는 안정성을 보장하는 관계형 데이터베이스에 보관한다.
다중화와 샤딩은 이런 데이터의 가용성과 규모확장성을 보증하기 위해 보편적으로 사용되는 기술이다.
채팅 이력과 같은 채팅 시스템에 고유한 데이터를 어떻게 보관할지 결정하려면 읽기/쓰기 연산 패턴을 이해해야 한다.
채팅 이력 데이터의 양은 엄청나다.
이 데이터 가운데 빈번하게 사용되는 것은 주로 최근에 주고받은 메시지다.
사용자는 대체로 최근에 주고받은 메시지 데이터만 보게 되는 것이 사실이나, 검색 기능을 이용하거나, 특정 사용자가 언급된 메시지를 보거나, 특정 메시지로 점프하거나 하여 무작위적인 데이터 접근을 하게 되는 일도 있다.
1:1 채팅 앱의 경우 읽기:쓰기 비율은 대략 1:1 정도다.
이 설계안에서 키-값 저장소를 추천하는 이유는 다음과 같다.
키-값 저장소는 수평적 규모확장이 쉽다.
키-값 저장소는 데이터 접근 지연시간이 낮다.
관계형 데이터베이스는 데이터 가운데 롱 테일에 해당하는 부분을 잘 처리하지 못하는 경향이 있다. (인덱스가 커지면 데이터에 대한 무작위적 접근을 처리하는 비용이 늘어난다.)
이미 많은 안정적인 채팅 시스템이 키-값 저장소를 채택하고 있다.
데이터 모델
1:1 채팅을 위한 메시지 테이블
기본 키는 message_id로 메시지 순서를 쉽게 정할 수 있도록 하는 역할도 담당한다.
created_at은 서로 다른 두 메시지가 동시에 만들어질 수 있기 때문에 메시지 순서를 정할 수 없다.
그룹 채팅을 위한 메시지 테이블
channel_id, message_id의 복합키를 기본 키로 사용한다.
channel_id는 파티션 키로도 사용하여 그룹 채팅에 적용될 모든 질의를 특정 채널을 대상으로 한다.
메시지 ID
message_id는 메시지들의 순서를 표현할 수 있어야 하기 때문에 다음과 같은 속성을 만족해야 한다.
message_id의 값은 고유해야 한다.
ID 값은 정렬 가능해야 하며 시간 순서와 일치해야 한다.
RDBMS라면 auto_increment가 대안이 될 수 있겠지만 NoSQL은 해당 기능을 제공하지 않는다.
그렇기 떄문에 스노플레이크 같은 전역적 64-bit 순서 번호 생성기를 사용할 수 있다.
하지만 메시지 사이의 순서는 같은 채널, 혹은 같은 1:1. 채팅 세션 안에서만 유지되면 되기 때문에 지역적 순서 번호 생성기를 사용할 수 있다.

3단계. 상세 설계

서비스 탐색
서비스 탐색 기능의 주된 역할은 클라이언트에게 가장 적합한 채팅 서버를 추천하는 것이다.
이때 사용되는 기준으로는 클라이언트의 위치, 서버의 용량 등이 있다.
오픈 소스 솔루션으로는 아파치 주키퍼가 있으며, 사용 가능한 모든 채팅 서버를 등록시킨 후 클라이언트가 접속을 시도하면 사전에 정한 기준에 따라 최적의 채팅 서버를 골라준다.
1.
사용자 A가 시스템에 로그인을 시도한다.
2.
로드밸런서가 로그인 요청을 API 서버들 가운데 하나로 보낸다.
3.
API 서버가 사용자 인증을 처리하고 나면 서비스 탐색 기능이 동작하여 해당 사용자를 서비스할 최적의 채팅 서버를 찾는다.
4.
사용자 A는 찾아진 채팅 서버와 웹소켓 연결을 맺는다.
메시지 흐름
1:1 채팅 메시지 처리 흐름
1.
사용자 A가 채팅 서버 1로 메시지 전송
2.
채팅 서버 1은 ID 생성기를 사용해 해당 메시지의 ID 결정
3.
채팅 서버 1은 해당 메시지를 메시지 동기화 큐로 전송
4.
메시지가 키-값 저장소에 보관됨
5.
사용자 B가 접속 중인 경우 채팅 서버 2로 전송되고, 접속 중이 아니라면 푸시 알림 메시지를 푸시 알림 서버로 보냄
6.
사용자 B와 채팅 서버 2 사이에 웹소켓 연결을 이용하여 채팅 서버 2는 메시지를 사용자 B에게 전송
여러 단말 사이의 메시지 동기화
사용자 A는 전화기와 랩톱 두 대의 단말을 사용하고 있다.
각 단말은 cur_max_message_id라는 변수를 유지하는데, 해당 단말에서 관측된 가장 최신 메시지의 ID를 추적하는 용도다.
아래 두 조건을 만족하는 메시지는 새 메시지로 간주한다.
수신자 ID가 현재 로그인한 사용자 ID와 같다.
키-값 저장소에 보관된 메시지로서, 그 ID가 cur_max_message_id보다 크다.
소규모 그룹 채팅에서의 메시지 흐름
사용자 A가 보낸 메시지가 사용자 B, C의 메시지 동기화 큐에 복사된다.
소규모 그룹의 경우 메시지를 수신자별로 복사해서 큐에 넣는 작업의 비용이 문제가 되지 않고, 새로운 메시지를 확인할 때 자기 큐만 보면 되기 때문에 동기화 플로우가 단순하다.
하지만 많은 사용자를 지원해야 하는 경우라면 똑같은 메시지를 모든 사용자의 큐에 복사하는게 바람직하지 않다.
한 수신자는 여러 사용자로부터 오는 메시지를 수신할 수 있어야 하기 때문에 메시지 동기화 큐는 여러 사용자로부터 오는 메시지를 받을 수 있어야 한다.
접속상태 표시
접속상태 서버를 통해 사용자의 상태를 관리하며, 클라이언트와 웹소켓으로 통신하는 실시간 서비스의 일부이다.
사용자 로그인
클라이언트와 실시간 서비스 사이에 웹소켓 연결이 맺어지고 나면 접속상태 서버는 A의 상태와 last_active_at 타임스탬프 값을 키-값 저장소에 보관한다.
이 절차가 끝나고 나면 해당 사용자는 접속 중인 것으로 표시될 것이다.
로그아웃
사용자 로그아웃은 키-값 저장소에 보관된 사용자 상태가 online에서 offline으로 바뀌게 된다.
이 절차가 끝나고 나면 UI 상에서 사용자의 상태는 접속 중이 아닌 것으로 표시될 것이다.
접속 장애
사용자의 인터넷 연결이 끊어지면 클라이언트와 서버 사이에 맺어진 웹소켓 같은 지속성 연결도 끊어진다.
이런 장애에 대응하는 간단한 방법은 사용자를 오프라인 상태로 표시하고 연결이 복구되면 온라인 상태로 변경하는 것이다.
하지만 짧은 시간 동안 인터넷 연결이 끊어졌다 복구되는 일은 흔하기 때문에 바람직하지 않은 방법이다.
이 문제를 해결하기 위해 박동 검사를 사용하여 온라인 상태의 클라이언트로부터 주기적으로 박동 이벤트를 접속 상태 서버로 보내도록 한다.
마지막 이벤트를 받은지 x초 이내에 또 다른 박동 이벤트 메시지를 받으면 해당 사용자의 접속 상태를 계속 온라인으로 유지하고, 그렇지 않은 경우에만 오프라인으로 바꾼다.
상태 정보의 전송
상태 정보 서버는 pub/sub 모델을 사용한다.
그룹 크기가 작은 경우엔 각 친구 관계마다 채널을 하나씩 두고 상태정보 변화를 통지하는 방법이 효과적이다.
하지만 그룹 크기가 커지면 비용이나 시간이 많이 들게 되므로, 사용자가 그룹 채팅에 입장하는 순간에만 상태 정보를 읽거나, 친구 리스트에 있는 사용자의 접속 상태를 수동으로 갱신하도록 유도해야 한다.

4단계. 마무리

채팅 앱을 확장하여 사진이나 비디오 등의 미디어를 지원하도록 하는 방법
압축 방식, 클라우드 저장소, 썸네일 생성 등
종단 간 암호화
메시지 발신인과 수신자 이외에는 아무도 메시지 내용을 볼 수 없다.
캐시
클라이언트에 이미 읽은 메시지를 캐시해 두면 서버와 주고받는 데이터 양을 줄일 수 있다.
로딩 속도 개선
슬랙은 사용자의 데이터, 채널 등을 지역적으로 분산하는 네트워크를 구축하여 앱 로딩 속도를 개선하였다.
오류 처리
채팅 서버 하나가 죽으면 서비스 탐색 기능이 동작하여 클라이언트에게 새로운 서버를 배정하고 다시 접속할 수 있도록 해야 한다.
안정적인 메시지 전송을 위해 재시도나 큐를 사용한다.

13장. 검색어 자동완성 시스템

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

지원자: 사용자가 입력하는 단어는 자동완성될 검색어의 첫 부분이어야 하나요? 아니면 중간 부분이 될 수도 있습니까?
면접관: 첫 부분으로 한정하겠습니다.
지원자: 몇 개의 자동완성 검색어가 표시되어야 합니까?
면접관: 5개입니다.
지원자: 자동완성 검색어 5개를 고르는 기준은 무엇입니까?
면접관: 질의 빈도에 따라 정해지는 검색어 인기 순위를 기준으로 삼겠습니다.
지원자: 맞춤법 검사 기능도 제공해야 합니까?
면접관: 아뇨. 맞춤법 검사나 자동수정은 지원하지 않습니다.
지원자: 질의는 영어입니까?
면접관: 네. 하지만 시간이 허락한다면 다국어 지원을 생각해도 좋습니다.
지원자: 대문자나 특수 문자 처리도 해야 합니까?
면접관: 아뇨. 모든 질의는 영어 소문자로 이루어진다고 가정하겠습니다.
지원자: 얼마나 많은 사용자를 지원해야 합니까?
면접관: 일간 능동 사용자 기준으로 천만 명입니다.
요구사항
빠른 응답 속도
사용자가 검색어를 입력함에 따라 자동완성 검색어도 충분히 빨리 표시되어야 한다.
페이스북 검색어 자동완성 시스템에 관한 문서를 보면 시스템 응답속도는 100밀리초 이내여야 한다.
그렇지 않으면 시스템 이용이 불편해진다.
연관성
자동완성되어 출력되는 검색어는 사용자가 입력한 단어와 연관된 것이어야 한다.
정렬
시스템의 계산 결과는 인기도 등의 순위 모델에 의해 정렬되어 있어야 한다.
규모 확장성
시스템은 많은 트래픽을 감당할 수 있도록 확장 가능해야 한다.
고가용성
시스템의 일부에 장애가 발생하거나, 느려지거나, 예상치 못한 네트워크 문제가 생겨도 시스템은 계속 사용 가능해야 한다.
개략적 규모 추정
일간 능동 사용자는 천만 명으로 가정한다.
평균적으로 한 사용자는 매일 10건의 검색을 수행한다고 가정한다.
질의할 때마다 평균적으로 20바이트의 데이터를 입력한다고 가정한다.
문자 인코딩 방법으로는 ASCII를 사용한다고 가정할 것이므로, 1문자 = 1바이트이다.
질의문은 평균적으로 4개 단어로 이루어진다고 가정할 것이며, 각 단어는 평균적으로 다섯 글자로 구성된다고 가정할 것이다.
따라서 질의당 평균 4 * 5 = 20바이트이다.
검색창에 글자를 입력할 때마다 클라이언트는 검색어 자동완성 백엔드에 요청을 보낸다.
평균적으로 1회 검색당 20건의 요청이 백엔드로 전달된다.
대략 (10,000,000 사용자 * 10 질의 * 20자) / 24시간 / 3600초) = 초당 24,000건의 질의(QPS)가 발생할 것이다.
최대 QPS = QPS * 2 = 대략 48,000
질의 가운데 20% 정도는 신규 검색어라고 가정할 것이다.
따라서 10,000,000 사용자 * 10 질의 * 20자 * 20% = 대략 0.4GB 정도의 신규 데이터가 시스템에 추가된다.

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

데이터 수집 서비스
사용자가 입력한 질의를 실시간으로 수집하는 시스템이다.
데이터가 많은 어플리케이션에 실시간 시스템은 그다지 바람직하지 않지만 출발점으로는 괜찮을 것이다.
질의문과 사용빈도를 저장하는 빈도 테이블이 있다고 가정한다.
질의 서비스
주어진 질의에 다섯 개의 인기 검색어를 정렬해 내놓는 서비스이다.
데이터 수집 서비스의 빈도 테이블을 사용하여 가장 많이 사용된 5개 검색어를 구한다.
SELECT * FROM frequency_table WHERE query LIKE 'prefix%' ORDER BY frequency DESC LIMIT 5
SQL
복사
데이터 양이 적을 때는 나쁘지 않은 설계안이지만 데이터가 아주 많아지면 데이터베이스가 병목이 될 수 있다.

3단계. 상세 설계

트라이 자료구조
관계형 데이터베이스를 이용해 가장 인기 있었던 다섯 개 질의문을 골라내는 방법은 효율적이지 않다.
이 문제는 트라이 자료구조를 사용해 해결할 것이며 시스템의 핵심적인 부분이 될 것이다.
트라이 자료구조의 핵심 아이디어
트라이는 트리 형태의 자료구조다.
이 트리의 루트 노드는 빈 문자열을 나타낸다.
각 노드는 글자 하나를 저장하며, 26개의 자식 노드(해당 글자 다음에 등장할 수 있는 모든 글자의 개수)를 가질 수 있다.
각 트리 노드는 하나의 단어, 또는 접두어 문자열을 나타낸다.
트라이 자료구조의 용어
p : 접두어(prefix)의 길이
n : 트라이 안에 있는 노드 개수
c : 주어진 노드의 자식 노드 개수
가장 많이 사용된 질의어 k개를 찾는 방법
해당 접두어를 표현하는 노드를 찾는다. 시간 복잡도는 O(p)이다.
해당 노드부터 시작하는 하위 트리를 탐색하여 모든 유효 노드를 찾는다. 유효한 검색 문자열을 구성하는 노드가 유효 노드로 시간 복잡도는 O(c)이다.
유효 노드들을 정렬하여 가장 인기 있는 검색어 k개를 찾는다. 시간 복잡도는 O(c log c)이다.
k = 2이고, 사용자가 검색창에 ‘be’를 입력한 경우
1.
접두어 노드 ‘be’를 찾는다.
2.
해당 노드부터 시작되는 하위 트리를 탐색하여 모든 유효 노드를 찾는다. [beer:10], [best:35], [bet:29] 가 유효 노드다.
3.
유효 노드를 정렬하여 2개만 골라낸다. [best:35], [bet:29]가 접두어(검색어) ‘be’에 대해 검색된 2개의 인기 검색어다.
이 알고리즘의 시간 복잡도는 위의 각 단계에 소요된 시간의 합인 O(p) + O(c) + O(c log c)이다.
최악의 경우에는 k개 결과를 얻으려고 전체 트라이를 다 검색해야 하는 일이 생길 수 있다.
이 문제를 해결하려면 다음과 같은 방법이 있다.
접두어의 최대 길이를 제한
사용자가 검색창에 긴 검색어를 입력하는 일은 거의 없기 때문에 p값은 작은 정수값이라고 가정해도 안전하다.
검색어의 최대 길이를 제한할 수 있다면 접두어 노드를 찾는 단계의 시간 복잡도는 O(p)에서 O(1)로 바뀔 것이다.
각 노드에 인기 검색어를 캐시
각 노드에 k개의 인기 검색어를 저장해 두면 전체 트라이를 검색하는 일을 방지할 수 있다.
각 노드의 질의어를 저장할 공간이 많이 필요하게 된다는 단점이 있지만 빠른 응답속도가 아주 중요할 때는 이 정도 저장공간을 희생할 만한 가치는 있다.
두 가지 최적화 기법을 적용하면 시간 복잡도가 아래와 같이 달라진다.
접두어 노드를 찾는 시간 복잡도는 O(1)로 바뀐다.
최고 인기 검색어 5개를 찾는 질의의 시간 복잡도도 O(1)로 바뀐다.
즉, 최고 인기 검색어 k개를 찾는 전체 알고리즘의 복잡도도 O(1)로 바뀌게 된다.
데이터 수집 서비스
사용자가 검색창에 뭔가 타이핑을 할 때마다 실시간으로 데이터를 수정한다면 아래와 같은 문제가 있다.
매일 수천만 건의 질의가 입력될 텐데 그때마다 트라이를 갱신하면 질의 서비스는 심각하게 느려질 것이다.
일단 트라이가 만들어지고 나면 인기 검색어는 그다지 자주 바뀌지 않을 것이기 때문에 트라이는 그렇게 자주 갱신할 필요가 없다.
데이터 분석 서비스 로그
데이터 분석 서비스 로그에는 검색창에 입력된 질의에 관한 원본 데이터가 보관된다.
새로운 데이터가 추가될 뿐 수정은 이루어지지 않으며 로그 데이터에는 인덱스를 걸지 않는다.
로그 취합 서버
데이터 분석 서비스로부터 나오는 로그는 보통 그 양이 엄청나고 데이터 형식도 제각각인 경우가 많아서 데이터를 잘 취합하여 쉽게 소비할 수 있도록 해야 한다.
실시간 어플리케이션의 경우 결과를 빨리 보여주는 것이 중요하므로 데이터 취합 주기를 보다 짧게 가져갈 필요가 있을 수 있다.
한편 대부분의 경우에는 일주일에 한 번 정도로 로그를 취합해도 충분하며, 데이터 취합의 실시간성이 얼마나 중요한지에 따라 달라진다.
취합된 데이터
매주 취합된 데이터는 query, time, frequency 필드를 가지며 질의어, 해당 주가 시작한 시간, 빈도수를 나타낸다.
작업 서버
작업 서버는 주기적으로 비동기적 작업을 실행하는 서버 집합이다.
트라이 자료구조를 만들고 트라이 데이터베이스에 저장하는 역할을 담당한다.
트라이 캐시
트라이 캐시는 분산 캐시 시스템으로 트라이 데이터를 메모리에 유지하여 읽기 연산 성능을 높이는 구실을 한다.
매주 트라이 데이터베이스의 스냅샷을 떠서 갱신한다.
트라이 데이터베이스
트라이 데이터베이스는 지속성 저장소로 문서 저장소나 키-값 저장소를 사용할 수 있다.
문서 저장소
새 트라이를 매주 만들 것이므로, 주기적으로 트라이를 직렬화하여 데이터베이스에 저장할 수 있다.
몽고디비 같은 문서 저장소를 활용하면 이런 데이터를 편리하게 저장할 수 있다.
키-값 저장소
트라이에 보관된 모든 접두어를 해시 테이블 키로 변환하고, 각 트라이 노드에 보관된 모든 데이터를 해시 테이블 값으로 변환하면 해시 테이블 형태로 변환이 가능하다.
질의 서비스
1.
검색 질의가 로드밸런서로 전송된다.
2.
로드밸런서는 해당 질의를 API 서버로 보낸다.
3.
API 서버는 트라이 캐시에서 데이터를 가져와 해당 요청에 대한 자동완성 검색어 제안 응답을 구성한다.
4.
데이터가 트라이 캐시에 없는 경우에는 데이터를 데이터베이스에서 가져와 캐시에 채운다. 캐시 미스는 캐시 서버의 메모리가 부족하거나 캐시 서버에 장애가 있어도 발생할 수 있다.
질의 서비스는 번개처럼 빨라야하기 때문에 다음과 같은 최적화 방안도 생각할 수 있다.
AJAX 요청
웹 어플리케이션의 경우 브라우저는 보통 AJAX 요청을 보내어 자동완성된 검색어 목록을 가져온다.
이 방법의 장점은 요청을 보내고 받기 위해 페이지를 새로고침 할 필요가 없다는 것이다.
브라우저 캐싱
대부분 어플리케이션의 경우 자동완성 검색어 제안 결과는 짧은 시간 안에 자주 바뀌지 않는다.
따라서 제안된 검색어들을 브라우저 캐시에 넣어두면 후속 질의의 결과는 해당 캐시에서 바로 가져갈 수 있다.
구글 검색 엔진이 이런 캐시 메커니즘을 사용한다.
데이터 샘플링
대규모 시스템의 경우, 모든 질의 결과를 로깅하도록 해 놓으면 CPU 자원과 저장공간을 엄청나게 소진하게 된다.
데이터 샘플링 기법을 통해 N개 요청 가운데 1개만 로깅하도록 하면 자원 소모를 줄일 수 있다.
트라이 연산
트라이 생성
작업 서버가 담당하며, 데이터 분석 서비스의 로그나 데이터베이스로부터 취합된 데이터를 이용한다.
트라이 갱신
두 가지 방법이 있다.
첫번째론 매주 한 번 갱신을 위해 새로운 트라이를 만들고 기존 트라이를 대체하는 방법이다.
두번째론 트라이의 각 노드를 개별적으로 갱신하는 방법이다.
두번째 방법은 노드를 갱신할 때 상위 노드들도 전부 갱신해야 하기 때문에 성능상 좋지 않아 트라이가 작을 때 고려해볼만하다.
검색어 삭제
여러 가지로 위험한 질의어를 자동완성 결과에서 제거해야 한다.
이를 위한 좋은 방법은 트라이 캐시 앞에 필터 계층을 두고 부적절한 질의어가 반환되지 않도록 하는 것이다.
필터 계층을 두면 필터 규칙에 따라 검색 결과를 자유롭게 변경할 수 있다는 장점이 있다.
만약 데이터베이스에서 해당 검색어를 물리적으로 삭제하려면 다음번 업데이트 사이클에 비동기적으로 진행할 수 있다.
저장소 규모 확장
영어만 지원하면 되기 때문에 첫 글자를 기준으로 샤딩하는 방법을 생각해 볼 수 있다.
하지만 ‘c’로 시작하는 단어가 ‘x’로 시작하는 단어보다 많다는 것을 생각하면 데이터를 각 서버에 균등하게 배분하기는 불가능하다.
이 문제를 해결하기 위해 검색어 대응 샤드 관리자를 두어 어떤 검색어가 어느 저장소 서버에 저장되는지에 대한 정보를 관리한다.
그리고 ‘s’로 시작하는 검색어의 양이 ‘u’-’z’로 시작하는 검색어를 전부 합친 것과 비슷한지와 같은 과거 질의 데이터의 패턴을 분석하여 샤딩을 진행한다.

4단계. 마무리

면접관: 다국어 지원이 가능하도록 시스템을 확장하려면 어떻게 해야 할까요?
비영어권 국가에서 사용하는 언어를 지원하려면 트라이에 유니코드 데이터를 저장해야 한다.
면접관: 국가별로 인기 검색어 순위가 다르다면 어떻게 해야 하나요?
국가별로 다른 트라이를 사용하도록 한다.
또한 트라이를 CDN에 저장하여 응답속도를 높이는 방법도 고려한다.
면접관: 실시간으로 변하는 검색어의 추이를 반영하려면 어떻게 해야 하나요?
이 설계는 작업 서버가 매주 한 번씩만 돌며 때맞춰 실행하더라도 트라이를 구성하는데 많은 시간이 들기 때문에 부적합하다.
하지만 아래와 같은 아이디어들이 있다.
샤딩을 통해 작업 대상 데이터를 더 잘개 쪼개어 트라이를 자주 재구성한다.
최근 검색어에 더 높은 가중치를 주도록 한다.

14장. 유튜브 설계

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

지원자: 어떤 기능이 가장 중요한가요?
면접관: 비디오를 올리는 기능과 시청하는 기능입니다.
지원자: 어떤 클라이언트를 지원해야 하나요?
면접관: 모바일 앱, 웹 브라우저, 그리고 스마트 TV입니다.
지원자: 일간 능동 사용자 수는 몇 명입니까?
면접관: 5백만입니다.
지원자: 사용자가 이 제품에 평균적으로 소비하는 시간은 얼마인가요?
면접관: 30분입니다.
지원자: 다국어 지원이 필요한가요?
면접관: 네. 어떤 언어로도 이용 가능해야 합니다.
지원자: 어떤 비디오 해상도를 지원해야할까요?
면접관: 현존하는 비디오 종류와 해상도를 대부분 지원해야 합니다.
지원자: 암호화가 필요할까요?
면접관: 네.
지원자: 비디오 파일 크기에 제한이 있습니까?
면접관: 작은 비디오, 혹은 중간 크기 비디오에 초점을 맞추도록 합시다. 비디오 크기는 최대 1GB로 제한합니다.
지원자: 아마존이나 구글, 마이크로소프트가 제공하는 클라우드 서비스를 활용해도 될까요?
면접관: 좋은 질문입니다. 모든 걸 바닥부터 쌓아 올리는 것은 대부분 회사에게는 비현실적인 일이죠. 활용할 수 있다면 하는 것이 바람직할 것입니다.
요구사항
빠른 비디오 업로드
원활한 비디오 재생
재생 품질 선택 기능
낮은 인프라 비용
높은 가용성과 규모 확장성, 그리고 안정성
지원 클라이언트는 모바일 앱, 웹 브라우저, 그리고 스마트 TV
개략적 규모 추정
일간 능동 사용자 수는 5백만
한 사용자는 하루에 평균 5개의 비디오를 시청
10%의 사용자가 하루에 1비디오 업로드
비디오 평균 크기는 300MB
비디오 저장을 위해 매일 새로 요구되는 저장 용량 = 5백만 * 10% * 300MB = 150TB
CDN 비용
클라우드 CDN을 통해 비디오를 서비스할 경우 CDN에서 나가는 데이터의 양에 따라 과금한다.
아마존의 클라우드프론트를 CDN 솔루션으로 사용할 경우, 100% 트래픽이 미국에서 발생한다고 가정하면 1GB당 $0.02의 요금이 발생한다.
따라서 매일 발생하는 요금은 5백만 * 5비디오 * 0.3GB * $0.02 = $150,000 이다.

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

개략적으로 보면 이 시스템은 세 개 컴포넌트로 구성된다.
단말
컴퓨터, 모바일 폰, 스마트 TV를 통해서 유튜브를 시청할 수 있다.
CDN
비디오는 CDN에 저장한다.
재생 버튼을 누르면 CDN으로부터 스트리밍이 이루어진다.
API 서버
비디오 스트리밍을 제외한 모든 요청은 API 서버가 처리한다.
피드 추천, 비디오 업로드 URL 생성, 메타데이터 데이터베이스와 캐시 갱신, 사용자 가입 등등이 API 서버가 처리하는 작업이다.
비디오 업로드 절차
사용자
컴퓨터나 모바일 폰, 혹은 스마트 TV를 통해 유튜브를 시청하는 이용자다.
로드밸런서
API 서버 각각으로 고르게 요청을 분산하는 역할을 담당한다.
API 서버
비디오 스트리밍을 제외한 다른 모든 요청을 처리한다.
메타데이터 데이터베이스
비디오의 메타데이터를 보관한다.
샤딩과 다중화를 적용하여 성능 및 가용성 요구사항을 충족한다.
메타데이터 캐시
성능을 높이기 위해 비디오 메타데이터와 사용자 객체는 캐시한다.
원본 저장소
원본 비디오를 보관할 대형 이진 파일 저장소(BLOB) 시스템이다.
BLOB 저장소는 이진 데이터를 하나의 개체로 보관하는 데이터베이스 관리 시스템이다.
트랜스코딩 서버
비디오 트랜스코딩은 비디오 인코딩이라 부르기도 하는 절차로, 비디오의 포맷을 변환하는 절차다.
단말이나 대역폭 요구사항에 맞는 최적의 비디오 스트림을 제공하기 위해 필요하다.
트랜스코딩 비디오 저장소
트랜스코딩이 완료된 비디오를 저장하는 BLOB 저장소다.
CDN
비디오를 캐시하는 역할을 담당한다.
사용자가 재생 버튼을 누르면 비디오 스트리밍은 CDN을 통해 이루어진다.
트랜스코딩 완료 큐
비디오 트랜스코딩 완료 이벤트들을 보관할 메시지 큐다.
트랜스코딩 완료 핸들러
트랜스코딩 완료 큐에서 이벤트 데이터를 꺼내어 메타데이터 캐시와 데이터베이스를 갱신할 작업 서버들이다.
비디오 업로드 절차는 다음 두 프로세스가 병렬적으로 수행된다.
프로세스 a: 비디오 업로드
1.
비디오를 원본 저장소에 업로드한다.
2.
트랜스코딩 서버는 원본 저장소에서 해당 비디오를 가져와 트랜스코딩을 시작한다.
3.
트랜스코딩이 완료되면 아래 두 절차가 병렬적으로 수행된다.
a.
완료된 비디오를 트랜스코딩 비디오 저장소로 업로드한다.
b.
트랜스코딩 완료 이벤트를 트랜스코딩 완료 큐에 넣는다.
4.
완료 핸들러가 메타데이터 데이터베이스와 캐시를 갱신한다.
5.
API 서버가 단말에게 비디오 업로드가 끝나서 스트리밍 준비가 되었음을 알린다.
프로세스 b: 메타데이터 갱신
원본 저장소에 파일이 업로드되는 동안, 단말은 병렬적으로 비디오 메타데이터 갱신 요청을 API 서버에 보낸다.
이 요청에 포함된 메타데이터에는 파일 이름, 크기, 포맷 등의 정보가 들어 있다.
API 서버는 이 정보로 메타데이터 캐시와 데이터베이스를 업데이트한다.
비디오 스트리밍 절차
비디오 재생버튼을 누르면 여러분의 장치가 원격지의 비디오로부터 지속적으로 비디오 스트림을 전송 받아 영상을 재생한다.
스트리밍 프로토콜은 비디오 스트리밍을 위해 데이터를 전송할 떄 쓰이는 표준화된 통신방법이다.
프로토콜마다 지원하는 비디오 인코딩이 다르고 플레이어도 다르기 때문에 비디오 스트리밍 서비스를 설계할 때는 서비스의 용례에 맞는 프로토콜을 잘 골라야 한다.
비디오는 사용자의 단말에서 가장 가까운 CDN 서버에서 스트리밍되며 전송지연은 아주 낮다.

3단계. 상세 설계

비디오 트랜스코딩
비디오를 녹화하면 단말은 해당 비디오를 특정 포맷으로 저장한다.
이 비디오가 다른 단말에서도 순조롭게 재생되려면 다른 단말과 호환되는 비트레이트와 포맷으로 저장되어야 한다.
비트레이트는 비디오를 구성하는 비트가 얼마나 빨리 처리되어야 하는지를 나타내는 단위다.
비트레이트가 높은 비디오는 일반적으로 고화질 비디오이며, 높은 성능의 컴퓨팅 파워가 필요하고 인터넷 회선 속도도 빨라야 한다.
비디오 트랜스코딩의 중요성
가공되지 않은 비디오는 저장 공간을 많이 차지하며, 초당 60프레임으로 녹화된 HD 비디오는 수백 GB의 저장공간을 차지하게 될 수 있다.
상당수의 단말과 브라우저는 특정 종류의 비디오 포맷만 지원한다. 따라서 호환성 문제를 해결하려면 하나의 비디오를 여러 포맷으로 인코딩해 두는 것이 바람직하다.
사용자에게 끊김 없는 고화질 비디오 재생을 보장하려면, 네트워크 대역폭이 충분하지 않은 사용자에게는 저화질 비디오를, 대역폭이 충분한 사용자에게는 고화질 비디오를 보내는 것이 바람직하다.
모바일 단말의 경우 네트워크 상황이 수시로 달라질 수 있으며, 비디오가 끊김 없이 재생되도록 하기 위해서는 비디오 화질을 자동으로 변경하거나 수동으로 변경할 수 있도록 하는 것이 바람직하다.
인코딩 포맷
컨테이너
비디오 파일, 오디오, 메타데이터를 담는 바구니 같은 것이다.
컨테이너 포멧은 .avi, .mov, .mp4 같은 파일 확장자를 보면 알 수 있다.
코덱
비디오 화질은 보존하면서 파일 크기를 줄일 목적으로 고안된 압축 및 압축 해제 알고리즘이다.
가장 많이 사용되는 비디오 코덱으로는 H.264, VP9, HEVC가 있다.
유향 비순환 그래프(DAG) 모델
비디오를 트랜스코딩하는 것은 컴퓨팅 자원을 많이 소모할 뿐 아니라 시간도 많이 드는 작업이다.
게다가 콘텐츠 창작자는 각자 자기만의 비디오 프로세싱 요구사항을 갖고 있다.
예를 들어, 어떤 사람은 비디오 위에 워터마크를 표시하고 싶어 할 것이고, 어떤 사람은 썸네일 이미지를 손수 만들어 쓰고 싶어 할 것이고, 어떤 사람은 고화질 비디오를 선호할 것이고, 어떤 사람은 저화질 비디오도 충분하다고 생각할 것이다.
이처럼 각기 다른 유형의 비디오 프로세싱 파이프라인을 지원하는 한편 처리 과정의 병렬성을 높이기 위해서는 적절한 수준의 추상화를 도입하여 클라이언트 프로그래머가 실행할 작업을 손수 정의할 수 있도록 해야 한다.
원본 비디오는 비디오, 오디오, 메타데이터의 세 부분으로 나누어 처리된다.
비디오에 적용되는 작업은 다음과 같다.
검사
좋은 품질의 비디오인지, 손상은 없는지 확인하는 작업이다.
비디오 인코딩
비디오를 다양한 해상도, 코덱, 비트레이트 조합으로 인코딩하는 작업이다.
결과물은 360p.mp4, 480p.mp4, 720p.mp4, 1080p.mp4, 4k.mp4 등이 있다.
썸네일
사용자가 업로드한 이미지나 비디오에서 자동 추출된 이미지로 썸네일을 만드는 작업이다.
워터마크
비디오에 대한 식별정보를 이미지 위에 오버레이 형태로 띄워 표시하는 작업이다.
비디오 트랜스코딩 아키텍처
이 아키텍처는 다섯 개의 주요 컴포넌트로 구성되며, 인코딩된 비디오가 만들어진다.
전처리기
1.
비디오 분할
비디오 스트림을 GOP(Group of Pictures)라고 불리는 단위로 쪼갠다.
GOP는 특정 순서로 배열된 프레임 그룹이다.
하나의 GOP는 독립적으로 재생 가능하며, 길이는 보통 몇 초 정도다.
어떤 종류의 오래된 단말이나 브라우저는 GOP 단위의 비디오 분할을 지원하지 않는다.
그런 단말의 경우에는 전처리기가 비디오 분할을 대신한다.
2.
DAG 생성
클라이언트 프로그래머가 작성한 설정 파일에 따라 DAG를 만들어낸다.
task { name 'download-input' type 'Download' input { url config.url } output { it -> context.inputVideo = it.file } next 'transcode' } task { name 'transcode' type 'Transcode' input { input context.inputVideo config config.transConfig } output { it -> context.file = it.outputVideo } }
Plain Text
복사
3.
데이터 캐시
전처리기는 분할된 비디오의 캐시이기도 하다.
안정성을 높이기 위해 전처리기는 GOP와 메타데이터를 임시 저장소에 보관한다.
비디오 인코딩이 실패하면 시스템은 이렇게 보관된 데이터를 활용해 인코딩을 재개한다.
DAG 스케줄러
DAG 그래프를 몇 개 단계로 분할한 다음에 그 각각을 자원 관리자의 작업 큐에 집어넣는다.
첫 단계에서는 비디오, 오디오, 메타데이터를 분리한다.
두 번째 단계에서는 해당 비디오 파일을 인코딩하고, 썸네일을 추출하며, 오디오 파일 또한 인코딩한다.
자원 관리자
1.
작업 스케줄러는 실행할 작업이 보관되어 있는 우선순위 큐인 작업 큐에서 가장 높은 우선순위의 작업을 꺼낸다.
2.
작업 스케줄러는 해당 작업을 실행하기 적합한 작업 서버를 작업 서버의 가용 상태 정보가 보관되어 있는 우선순위 큐인 작업 서버 큐를 통해 고른다.
3.
작업 스케줄러는 해당 작업 서버에게 작업 실행을 지시한다.
4.
작업 스케줄러는 해당 작업이 어떤 서버에게 할당되었는지에 관한 정보를 현재 실행 중인 작업 및 작업 서버 정보가 보관되어 있는 큐인 실행 큐에 넣는다.
5.
작업 스케줄러는 작업이 완료되면 해당 작업을 실행 큐에서 제거한다.
작업 실행 서버
작업 서버는 DAG에 정의된 작업을 수행한다.
작업 종류에 따라 작업 서버도 구분하여 관리한다.
임시 저장소
어떤 저장소 시스템을 선택할 것이냐는 저장할 데이터의 유형, 크기, 이용 빈도, 데이터 유효기간 등에 따라 달라진다.
예를 들어, 메타데이터는 작업 서버가 빈번히 참조하는 정보이고 크기가 작기 때문에 메모리에 캐시 해 두면 좋을 것이다.
그리고 비디오/오디오 데이터는 BLOB 저장소에 두는 것이 바람직하다.
임시 저장소에 보관한 데이터는 비디오 프로세싱이 완료되면 삭제한다.
인코딩된 비디오
인코딩된 비디오는 인코딩 파이프라인의 최종 결과물로 title_720p.mp4 와 같은 이름을 갖는다.
시스템 최적화
속도 최적화: 비디오 병렬 업로드
비디오 전부를 한 번의 업로드로 올리는 것은 비효율적이다.
하나의 비디오는 작은 GOP들로 분할할 수 있고 병렬적으로 업로드하면 일부가 실패해도 빠르게 업로드를 재개할 수 있다.
따라서 비디오를 GOP 경계에 맞춰 분할하는 작업을 단말이 수행하면 업로드 속도를 높일 수 있다.
속도 최적화: 업로드 센터를 사용자 근거리에 지정
CDN을 업로드 센터로 사용하여 여러 곳에 두고 사용자 근거리의 업로드 센터로 보내도록 하면 업로드 속도를 개선할 수 있다.
속도 최적화: 모든 절차를 병렬화
낮은 응답 지연을 달성하려면 느슨하게 결합된 시스템을 만들어서 병렬성을 높이는 것이 방법이 될 수 있다.
어떤 단계의 결과물이 이전 단계의 결과물을 입력으로 사용하면 의존성이 생기고 병렬성을 높이기 어렵다.
결합도를 낮추기 위해 메시지 큐를 도입한다.
안전성 최적화: 미리 사인된 업로드 URL
허가 받은 사용자만이 올바른 장소에 비디오를 업로드할 수 있도록 하기 위해, 미리 사인된 업로드 URL을 이용한다.
1.
클라이언트는 HTTP 서버에 POST 요청을 하여 미리 사인된 URL을 받는다. 해당 URL이 가리키는 객체에 대한 접근 권한이 이미 주어져 있는 상태다.
2.
API 서버는 미리 사인된 URL을 돌려준다.
3.
클라이언트는 해당 URL이 가리키는위치에 비디오를 업로드한다.
안전성 최적화: 비디오 보호
비디오 제작자의 저작권을 보호하기 위해 세 가지 선택지를 생각해볼 수 있다.
디지털 저작권 관리 시스템 도입
가장 널리 사용되는 시스템으로는 애플의 페어플레이, 구글의 와이드바인, 마이크로소프트의 플레이레디가 있다.
AES 암호화
비디오를 암호화하고 접근 권한을 설정하는 방식이다.
암호화된 비디오는 재생 시에만 복호화하고 허락된 사용자만 암호화된 비디오를 시청할 수 있다.
워터마크
비디오 위에 소유자 정보를 포함하는 이미지 오버레이를 올리는 것이다.
비용 최적화
CDN은 비싸기 때문에 비용을 낮출 수 있는 방법을 고민해야 한다.
인기 있는 비디오는 빈번히 재생되는 반면, 나머지는 거의 보는 사람이 없다.
이에 착안하여 몇 가지 최적화를 시도해 볼 수 있다.
인기 비디오는 CDN을 통해 재생하되 다른 비디오는 비디오 서버를 통해 재생한다.
인기가 별로 없는 비디오는 인코딩 할 필요가 없을 수도 있고, 짧은 비디오라면 필요할 때 인코딩하여 재생할 수 있다.
어떤 비디오는 특정 지역에서만 인기가 높기 때문에 이런 비디오는 다른 지역에 옮길 필요가 없다.
CDN을 직접 구축하고 인터넷 서비스 제공자 ISP와 제휴한다면 초대형 프로젝트지만 사용자 경험을 향상시키고 인터넷 사용 비용을 낮출 수 있을 것이다.
오류 처리
시스템 오류에는 두 가지 종류가 있다.
회복 가능 오류
특정 비디오 세그먼트를 트랜스코딩하다 실패했다든가 하는 오류는 회복 가능한 오류에 속한다.
일반적으로 보자면 이런 오류는 몇 번 재시도하면 해결된다.
하지만 계속해서 실패하고 복구가 어렵다 판단되면 클라이언트에게 적절한 오류 코드를 반환해야 한다.
회복 불가능 오류
비디오 포맷이 잘못되었다거나 하는 회복 불가능한 오류가 발견되면 시스템은 해당 비디오에 대한 작업을 중단하고 클라이언트에게 적절한 오류 코드를 반환해야 한다.
시스템 컴포넌트 각각에 발생할 수 있는 오류에 대한 전형적 해결 방법은 아래와 같다.
업로드 오류
몇 회 재시도 한다.
비디오 분할 오류
낡은 버전의 클라이언트가 GOP 경계에 따라 비디오를 분할하지 못하는 경우라면 전체 비디오를 서버로 전송하고 서버가 해당 비디오 분할을 처리하도록 한다.
트랜스코딩 오류
재시도한다.
전처리 오류
DAG 그래프를 재생성한다.
DAG 스케줄러 오류
작업을 다시 스케줄링한다.
자원 관리자 큐에 장애 발생
레플리카를 이용한다.
작업 서버 장애
다른 서버에서 해당 작업을 재시도한다.
API 서버 장애
API 서버는 무상태 서버이므로 신규 요청은 다른 API 서버로 우회될 것이다.
메타데이터 캐시 서버 장애
데이터는 다중화되어 있으므로 다른 노드에서 데이터를 여전히 가져올 수 있을 것이다.
장애가 난 캐시 서버는 새로운 것으로 교체한다.
메타데이터 데이터베이스 서버 장애
주 서버가 죽었다면 부 서버 가운데 하나를 주 서버로 교체한다.
부 서버가 죽었다면 다른 부 서버를 통해 읽기 연산을 처리하고 죽은 서버는 새것으로 교체한다.

4단계. 마무리

API 계층의 규모 확장성 확보 방안
API 서버는 무상태 서버이므로 수평적 규모 확장이 가능하다는 사실을 언급하면 좋을 것이다.
데이터베이스 계층의 규모 확장성 확보 방안
데이터베이스의 다중화와 샤딩 방법에 대해 이야기하자.
라이브 스트리밍
라이브 스트리밍은 비디오를 실시간으로 녹화하고 방소하는 절차를 말한다.
라이브 스트리밍 시스템과 비-라이브 스트리밍 시스템 간에는 비디오 업로드, 인코딩, 스트리밍이 필요하다는 점이 비슷하다.
하지만 중요한 차이는 다음과 같다.
라이브 스트리밍의 경우 응답지연이 좀 더 낮아야 하기 때문에 스트리밍 프로토콜 선정에 유의해야 한다.
라이브 스트리밍의 경우 작은 단위의 데이터를 실시간으로 빨리 처리해야 하기 때문에 병렬화 필요성이 떨어진다.
라이브 스트리밍의 경우 너무 많은 시간이 걸리는 오류 처리 방법은 사용하기 어렵다.
비디오 삭제
삭제할 비디오는 업로드 과정에서 식별해 낼 수도 있지만, 사용자의 신고 절차를 통해 판별할 수도 있다.

15장. 구글 드라이브 설계

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

지원자: 가장 중요하게 지원해야 할 기능들은 무엇인가요?
면접관: 파일 업로드/다운로드, 파일 동기화, 그리고 알림입니다.
지원자: 모바일 앱이나 웹 앱 가운데 하나만 지원하면 되나요, 아니면 둘 다 지원해야 합니까?
면접관: 둘 다 지원해야 합니다.
지원자: 파일을 암호화해야 할까요?
면접관: 네.
지원자: 파일 크기에 제한이 있습니까?
면접관: 10GB 제한이 있습니다.
지원자: 사용자는 얼마나 됩니까?
면접관: 일간 능동 사용자 기준으로 천만 명입니다.
기능적 요구사항
파일 추가
가장 쉬운 방법은 파일을 구글 드라이브 안으로 drag-and-drop 하는 것이다.
파일 다운로드
여러 단말에 파일 동기화
한 단말에서 파일을 추가하면 다른 단말에도 자동으로 동기화되어야 한다.
파일 갱신 이력 조회
파일 공유
파일이 편집되거나 삭제되거나 새롭게 공유되었을 때 알림 표시
비기능적 요구사항
안정성
저장소 시스템에서 안정성은 아주 중요하며, 데이터 손실은 발생하면 안 된다.
빠른 동기화 속도
파일 동기화에 시간이 너무 많이 걸리면 사용자는 인내심을 잃고 해당 제품을 더 이상 사용하지 않게 될 것이다.
네트워크 대역폭
특히 모바일을 사용한다면 네트워크 대역폭을 불필요하게 많이 소모한다면 안 된다.
규모 확장성
아주 많은 양의 트래픽도 처리 가능해야 한다.
높은 가용성
일부 서버에 장애가 발생하거나, 느려지거나, 네트워크 일부가 끊겨도 시스템은 계속 사용 가능해야 한다.
개략적 추정
가입 사용자는 오천만 명이고 천만 명의 DAU 사용자가 있다고 가정
모든 사용자에게 10GB의 무료 저장공간 할당
매일 각 사용자가 평균 2개의 파일을 업로드한다고 가정하고, 각 파일의 평균 크기는 500KB라고 가정
읽기:쓰기 비율은 1:1
필요한 저장공간 총량 = 5천만 사용자 * 10GB = 500PB
업로드 API QPS = 1천만 사용자 * 2회 업로드 / 24시간 / 3600초 = 약 240
최대 QPS = QPS * 2 = 480

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

모든 것을 담은 한 대 서버에서 출발해 점진적으로 천만 사용자 지원이 가능한 시스템으로 발전시켜보자.
파일을 올리고 다운로드 하는 과정을 처리할 웹 서버
사용자 데이터, 로그인 정보, 파일 정보 등의 메타데이터를 보관할 데이터베이스
파일을 저장하기 위해 1TB의 공간을 사용하는 저장소 시스템
아파치 웹 서버를 설치하고, MySQL 데이터베이스를 설치하고, 업로드 파일을 저장할 drive/ 디렉터리를 준비한다.
디렉터리 안에는 네임스페이스라 불리는 하위 디렉터리들을 두고, 네임스페이스 안에는 특정 사용자가 올린 파일이 보관된다.
이 파일들은 원래 파일과 같은 이름을 갖고, 각 파일과 폴더는 네임스페이스 이름과 결합하면 유일하게 식별해 낼 수 있다.
API
파일 업로드 API
파일 크기가 작을 땐 단순 업로드를 지원한다.
파일 사이즈가 크거나, 네트워크 문제로 업로드가 중단될 가능성이 높다면 이어 올리기를 지원한다.
이어 올리기는 최초 요청 전송 후 업로드 상태를 모니터링하며, 장애가 발생하면 발생시점부터 업로드를 재시작한다.
파일 다운로드 API
다운로드할 파일의 경로를 인자로 받는다.
파일 갱신 히스토리 API
갱신 히스토리를 가져올 파일의 경로와 히스토리 길이의 최대치를 인자로 받는다.
이 API 들은 사용자 인증을 필요로 하고 데이터를 보호하기 위해 HTTPS 프로토콜을 사용해야 한다.
한 대 서버의 제약 극복
파일 저장소
파일 시스템이 가득 차게 되면 더 이상 파일을 올릴 수 없게 되므로 긴급히 문제를 해결해야 한다.
가장 먼저 떠오르는 해결책은 user_id를 기준으로 데이터를 샤딩하여 여러 서버에 나누어 저장하는 것이다.
많은 자료를 검토한 결과 S3 서비스에 대해 알게 되었고, 같은 지역 혹은 여러 지역에 걸쳐 다중화를 지원한다는 것을 알았다.
S3를 파일 저장소로 사용하고 가용성과 데이터 무손실을 보장하기 위해 두 개 이상의 지역에 데이터를 다중화한다.
로드밸런서
네트워크 트래픽을 분산하기 위해 로드밸런서를 사용한다.
로드밸런서는 트래픽을 고르게 분산할 뿐만 아니라, 특정 웹 서버에 장애가 발생하면 자동으로 해당 서버를 우회해준다.
웹 서버
로드밸런서를 추가하고 나면 더 많은 웹 서버를 손쉽게 추가할 수 있으며, 트래픽이 폭증해도 쉽게 대응이 가능하다.
메타데이터 데이터베이스
데이터베이스를 파일 저장 서버에서 분리하여 SPOF를 회피한다.
아울러 다중화 및 샤딩 정책을 적용하여 가용성과 규모 확장성 요구사항에 대응한다.
동기화 충돌
두 명 이상의 사용자가 같은 파일이나 폴더를 동시에 업데이트하려고 하는 경우 동기화 충돌이 발생할 수 있다.
이 경우 시스템이 먼저 처리하는 변경을 성공한 것으로 보고, 나중에 처리하는 변경을 충돌이 발생한 것으로 표시한다.
충돌이 발생한 사용자는 로컬 사본과 서버의 최신 버전 파일을 하나로 합칠지 아니면 둘 중 하나를 다른 파일로 대체할지 결정해야 한다.
개략적 설계안
사용자 단말
사용자가 이용하는 웹브라우저나 모바일 앱 등의 클라이언트
블록 저장소 서버
파일 블록을 클라우드 저장소에 업로드하는 서버다.
블록 저장소는 블록 수준 저장소라고도 하며, 클라우드 환경에서 데이터 파일을 저장하는 기술이다.
이 저장소는 파일을 여러 개의 블록으로 나눠 저장하며, 각 블록에는 고유한 해시값이 할당된다.
이 해시값은 메타데이터 데이터베이스에 저장되며, 파일을 재구성하려면 블록들을 원래 순서대로 합쳐야 한다.
클라우드 저장소
파일은 최대 4MB의 블록 단위로 나눠져 클라우드 저장소에 보관된다.
아카이빙 저장소
오랫동안 사용되지 않은 비활성 데이터를 저장하기 위한 컴퓨터 시스템이다.
로드밸런서
요청을 모든 API 서버에 고르게 분산하는 구실을 한다.
API 서버
파일 업로드 외에 거의 모든 것을 담당하는 서버다.
사용자 인증, 사용자 프로파일 관리, 파일 메타데이터 갱신 등에 사용된다.
메타데이터 데이터베이스
사용자, 파일, 블록, 버전 등의 메타데이터 정보를 관리한다.
실제 파일은 클라우드에 보관하며, 이 데이터베이스에는 오직 메타데이터만 둔다는 것을 명심하자.
메타데이터 캐시
성능을 높이기 위해 자주 쓰이는 메타데이터는 캐시한다.
알림 서비스
특정 이벤트가 발생했음을 클라이언트에게 알리는데 쓰이는 발생/구독 프로토콜 기반 시스템이다.
클라이언트에게 파일이 추가되었거나, 편집되었거나, 삭제되었음을 알려, 파일의 최신 상태를 확인하도록 하는데 쓰인다.
오프라인 사용자 백업 큐
클라이언트가 접속 중이 아니라서 파일의 최신 상태를 확인할 수 없을 때는 해당 정보를 이 큐에 두어 나중에 클라이언트가 접속했을 때 동기화될 수 있도록 한다.

3단계. 상세 설계

블록 저장소 서버
정기적으로 갱신되는 큰 파일들은 업데이트가 일어날 때마다 전체 파일을 서버로 보내면 네트워크 대역폭을 많이 잡아먹게 된다.
블록 저장소 서버에 아래 방법을 도입하면 네트워크 대역폭 사용량을 절감할 수 있다.
델타 동기화
파일이 수정되면 전체 파일 대신 수정이 일어난 블록만 동기화하는 것이다.
압축
블록 단위로 압축해 두면 데이터 크기를 많이 줄일 수 있다.
이때 압축 알고리즘은 파일 유형에 따라 정한다.
예를 들어, 텍스트 파일을 압축할 때는 gzip이나 bzip2를 쓰고, 이미지나 비디오를 압축할 때는 다른 압축 알고리즘을 쓴다.
블록 저장소 서버는 클라이언트가 보낸 파일을 블록 단위로 나눠야 하고, 각 블록에 압축 알고리즘을 적용해야 하고, 암호화까지 해야 한다.
또한 전체 파일을 저장소 시스템으로 보내는 대신 수정된 블록만 전송해야 한다.
높은 일관성 요구사항
이 시스템은 강한 일관성 모델을 기본으로 지원해야 한다.
같은 파일이 단말이나 사용자에 따라 다르게 보이는 것을 허용할 수 없다는 뜻이다.
메타데이터 캐시와 데이터베이스 계층에도 같은 원칙이 적용되어야 한다.
메모리 캐시는 보통 최종 일관성 모델을 지원하며 다음과 같은 사항을 보장해야 한다.
캐시에 보관된 사본과 데이터베이스에 있는 원본이 일치한다.
데이터베이스에 보관된 원본에 변경이 발생하면 캐시에 있는 사본을 무효화한다.
관계형 데이터베이스는 ACID를 보장하므로 강한 일관성을 보장하기 쉽다.
하지만 NoSQL 데이터베이스는 이를 기본으로 지원하지 않으므로, 동기화 로직 안에 프로그램해 넣어야 한다.
메타데이터 데이터베이스
user
이름, 이메일, 프로파일 사진 등 사용자에 관계된 기본적 정보들이 보관된다.
device
단말 정보가 보관되며, push_id는 모바일 푸시 알림을 보내고 받기 위한 것이다.
한 사용자가 여러 대의 단말을 가질 수 있음에 유의하자.
namespace
사용자의 루트 디렉터리 정보가 보관된다.
file
파일의 최신 정보가 보관된다.
file_version
파일의 갱신 이력이 보관되는 테이블이다.
이 테이블에 보관되는 레코드는 전부 읽기 전용이다.
이는 갱신 이력이 훼손되는 것을 막기 위한 조치다.
block
파일 블록에 대한 정보를 보관하는 테이블이다.
특정 버전의 파일은 파일 블록을 올바른 순서로 조합하기만 하면 복원해 낼 수 있다.
업로드 절차
두 개 요청이 병렬적으로 전송된 상황을 보여준다.
첫 번째 요청은 파일 메타데이터를 추가하기 위한 것이고, 두 번째 요청은 파일을 클라우드 저장소로 업로드하기 위한 것이다.
이 두 요청은 전부 클라이언트 1이 보낸 것이다.
파일 메타데이터 추가
1.
새 파일의 메타데이터를 추가하기 위한 요청 전송
2.
새 파일의 메타데이터를 데이터베이스에 저장하고 업로드 상태를 대기 중으로 변경
3.
새 파일이 추가되었음을 알림 서비스에 통지
4.
알림 서비스는 관련된 클라이언트 2에게 파일이 업로드되고 있음을 알림
파일을 클라우드 저장소에 업로드
1.
파일을 블록 저장소 서버에 업로드
2.
블록 저장소 서버는 파일을 블록 단위로 쪼갠 다음 압축하고 암호화 한 다음에 클라우드 저장소에 전송
3.
업로드가 끝나면 클라우드 스토리지는 완료 콜백을 API 서버로 호출.
4.
메타데이터 DB에 기록된 해당 파일의 상태를 완료로 변경
5.
알림 서비스에 파일 업로드가 끝났음을 통지
6.
알림 서비스는 관련된 클라이언트 2에게 파일 업로드가 끝났음을 알림
다운로드 절차
파일 다운로드는 파일이 새로 추가되거나 편집되면 자동으로 시작된다.
클라이언트 A가 접속 중인 경우
다른 클라이언트가 파일을 변경하면 알림 서비스가 클라이언트 A에게 변경이 발생했으니 새 버전을 끌어가야 한다고 알린다.
클라이언트 A가 네트워크에 연결된 상태가 아닐 경우
데이터는 캐시에 보관되고, 해당 클라이언트의 상태가 접속 중으로 바뀌면 그때 해당 클라이언트는 새 버전을 가져갈 것이다.
어떤 파일이 변경되었음을 감지한 클라이언트는 우선 API 서버를 통해 메타데이터를 새로 가져가야 하고, 그 다음에 블록들을 다운받아 파일을 재구성해야 한다.
1.
알림 서비스가 클라이언트 2에게 누군가 파일을 변경했음을 알림
2.
알림을 확인한 클라이언트 2는 새로운 메타데이터를 요청
3.
API 서버는 메타데이터 데이터베이스에게 새 메타데이터 요청
4.
API 서버에게 새 메타데이터가 반환됨
5.
클라이언트 2에게 새 메타데이터가 반환됨
6.
클라이언트 2는 새 메타데이터를 받는 즉시 블록 다운로드 요청 전송
7.
블록 저장소 서버는 클라우드 저장소에서 블록 다운로드
8.
클라우드 저장소는 블록 저장소 서버에 요청된 블록 반환
9.
블록 저장소 서버는 클라이언트에게 요청된 블록을 반환
10.
클라이언트 2는 전송된 블록을 사용하여 파일 재구성
알림 서비스
알림 서비스는 파일의 일관성을 유지하기 위해 파일이 수정되었음을 감지하는 순간 다른 클라이언트에 그 사실을 알려 충돌 가능성을 줄인다.
단순하게 보면 알림 서비스는 이벤트 데이터를 클라이언트들로 보내는 서비스다.
롱 폴링과 웹소켓의 선택지가 있지만 롱 폴링을 사용하는 이유는 다음과 같다.
채팅 서비스와는 달리, 본 시스템의 경우에는 알림 서비스와 양방향 통신이 필요하지 않다. 서버는 파일이 변경된 사실을 클라이언트에게 알려주어야 하지만 반대 방향의 통신은 요구되지 않는다.
웹소켓은 실시간 양방향 통신이 요구되는 채팅 같은 응용에 적합하다. 알림을 보낼 일은 그렇게 자주 발생하지 않으며, 알림을 보내야 하는 경우에도 단시간에 많은 양의 데이터를 보낼 일은 없다.
롱 폴링 방안을 쓰게 되면 각 클라이언트는 알림 서버와 롱 폴링용 연결을 유지하다가 특정 파일에 대한 변경을 감지하면 해당 연결을 끊는다.
이때 클라이언트는 반드시 메타데이터 서버와 연결해 파일의 최신 내역을 다운로드 해야 한다.
해당 다운로드 작업이 끝났거나 연결 타임아웃 시간에 도달한 경우에는 즉시 새 요청을 보내어 롱 폴링 연결을 복원하고 유지해야 한다.
저장소 공간 절약
파일 갱신 이력을 보존하고 안정성을 보장하기 위해서는 파일의 여러 버전을 여러 데이터센터에 보관할 필요가 있다.
그런 상황에서 모든 버전을 자주 백업하게 되면 저장용량이 너무 빨리 소진될 가능성이 있다.
보통 아래 세 가지 방법을 사용한다.
중복 제거
중복된 파일 블록을 계정 차원에서 제거하는 방법이다.
두 블록이 같은 블록인지는 해시 값을 비교하여 판단한다.
지능적 백업 전략 도입
한도 설정
보관해야 하는 파일 버전 개수에 상한을 두고, 상한에 도달하면 제일 오래된 버전을 버린다.
중요한 버전만 보관
자주 바뀌는 파일에 대해 불필요한 버전과 사본이 만들어지는 것을 피하려면 그 가운데 중요한 것만 골라내야 한다.
아카이빙 저장소
자주 쓰이지 않는 데이터는 아카이빙 저장소로 옮기며, 아카이빙 저자소의 이용료는 훨씬 저렴하다.
장애 처리
로드밸런서 장애
로드밸런서에 장애가 발생한 경우 부 로드밸런서가 활성화되어 트래픽을 이어받아야 한다.
로드밸런서끼리는 보통 박동 신호를 주기적으로 보내서 상태를 모니터링한다.
일정 시간동안 박동 신호에 응답하지 않은 로드밸런서는 장애가 발생한 것으로 간주한다.
블록 저장소 서버 장애
블록 저장소 서버에 장애가 발생하였다면 다른 서버가 미완료 상태 또는 대기 상태인 작업을 이어받아야 한다.
클라우드 저장소 장애
S3 버킷은 여러 지역에 다중화할 수 있으므로, 한 지역에서 장애가 발생하였다면 다른 지역에서 파일을 가져오면 된다.
API 서버 장애
API 서버들은 무상태 서버다.
따라서 로드밸런서는 API 서버에 장애가 발생하면 트래픽을 해당 서버로 보내지 않음으로써 장애 서버를 격리할 것이다.
메타데이터 캐시 장애
메타데이터 캐시 서버도 다중화한다.
따라서 한 노드에 장애가 생겨도 다른 노드에서 데이터를 가져올 수 있다.
장애가 발생한 서버는 새 서버로 교체하면 된다.
메타데이터 데이터베이스장애
주 데이터베이스 서버 장애
부 데이터베이스 서버 가운데 하나를 주 데이터베이스 서버로 바꾸고, 부 데이터베이스 서버를 새로 하나 추가한다.
부 데이터베이스 서버 장애
다른 부 데이터베이스 서버가 읽기 연산을 처리하도록 하고 그동안 장애 서버는 새 것으로 교체한다.
알림 서비스 장애
접속 중인 모든 사용자는 알림 서버와 롱 폴링 연결을 하나씩 유지한다.
따라서 알림 서비스는 많은 사용자와의 연결을 유지하고 관리해야 한다.
따라서 한 대 서버에 장애가 발생하면 여러 명의 사용자가 롱 폴링 연결을 다시 만들어야 한다.
주의할 것은 동시에 백만 개 접속을 ‘시작’하는 것은 불가능하므로, 롱 폴링 연결을 복구하는 것은 상대적으로 느릴 수 있다.
오프라인 사용자 백업 큐 장애
이 큐 또한 다중화 해 두어야 한다.
큐에 장애가 발생하면 구독 중인 클라이언트들은 백업 큐로 구독 관계를 재설정해야 할 것이다.

4단계. 마무리

높은 수준의 일관성, 낮은 네트워크 지연, 그리고 빠른 동기화가 요구된다는 점이 설계 과정을 흥미진진하게 만들었다.
설계안은 크게 파일의 메타데이터를 관리하는 부분과, 파일 동기화를 처리하는 부분으로 구성된다.
알림 서비스는 이 두 부분과 병존하는 또 하나의 중요한 컴포넌트로 롱 폴링을 사용하여 클라이언트로 하여금 파일의 상태를 최신으로 유지할 수 있도록 한다.
정답은 없으며, 회사마다 요구하는 제약조건이 달라질테니 그에 맞게 설계를 진행해야 한다.
그 과정에서 내린 결정들과 선택한 기술들 이면에 어떤 생각이 있었는지 면접관에게 설명할 수 있도록 잘 기억해 두도록 하자.
설계안의 어떤 다른 선택지가 있었는지 논의해보면 좋을 것이다.
블록 저장소 서버를 거치지 않고 파일을 클라우드 저장소에 직접 업로드한다면?
S3의 PresignedURL과 같은 방법으로 클라우드 저장소로 직접 업로드하면 업로드 시간이 빨라질 수 있다.
하지만 분할, 압축, 암호화 로직을 클라이언트에 두어야 하므로 플랫폼별로 따로 구현해야 한다.
뿐만 아니라 클라이언트가 해킹 당할 가능성이 있으므로 암호화 로직은 클라이언트에 두지 않는 것이 적절하다.
이 설계안에서는 이 모두를 블록 저장소 서버라는 곳에 모아뒀다.
접속상태 관리 로직을 별도 서비스로 옮긴다면?
접속상태를 관리하는 로직을 다른 서비스에서도 사용해야 할 필요성이 있다면, 알림 서비스로부터 별도 서비스로 분리해내는 것이 좋을 것이다.