페이스북을 보다가 좋은 글을 발견해서 공부도 하면서 번역을 해 본다.
이번 장에서는 수 많은 동시 접속자와 수억 명의 유저로부터 들어오는 트래픽을 처리할 수 있도록 스크래치를 이용해 확장 가능한 분산 시스템을 구축하는 방법을 간단하게 살펴볼 것이다. 만약에 시스템 디자인 인터뷰를 준비하고 있다면 이 템플릿을 기반으로 천천히 공부해보자. 다음 장에서는 더욱 더 발전된 확장 전략에 대해 살펴볼 것이다.
초기에 우리가 웹 앱과 데이터베이스를 호스트할 수 있는 하나의 서버만 있다고 하자. URL을 이용해서 사이트에 접속하려고 할 때 DNS는 URL의 공용 IP를 리턴하고 클라이언트의 요청은 공용 IP 주소를 이용해서 웹 서버에 도달하게 된다.
이제 수백 명의 사용자를 확보했고 데이터베이스를 확장해야 한다. 데이터베이스를 독립적으로 확장하기 위해서는 웹 앱이 있던 웹 서버로부터 데이터베이스를 분리해야 한다. 그래서 이제 데이터베이스 계층을 따로 마련한다.
웹 서버는 데이터 계층에 읽기/쓰기/수정 요청을 지속적으로 보내고 데이터 계층은 웹 서버에 데이터를 보내고 클라이언트에 데이터가 보이도록 한다.
사용자는 점진적으로 증가하고 수천 명으로 늘었다. 하나의 서버가 그 많은 요청을 처리하는 것이 불가능하게 된다.
이 단계에서는 더 많은 서버를 붙이고 로드 벨런서를 두어서 들어오는 트래픽을 뒷 단의 서버로 분산해야 한다.
더 자세하게 알아보자. 로드 벨런서는 공용 IP를 갖고 있고 여러 개의 웹 서버는 사설 IP를 갖고 있다. DNS는 사이트의 공용 IP를 리턴하고 그 IP는 로드 벨런서로 도달하는 데 사용된다. 로드 벨런서는 웹 서버의 사설 IP를 사용해서 요청을 웹 서버로 분산하게 된다.
사용자가 계속 증가하고 하나의 데이터베이스 서버는 그런 로드를 처리하지 못하고 있다. 이제 읽기 명령은 쓰기/수정 명령과 분리해서 데이터 베이스 서버에 부하를 줄이고 사용자가 더 좋은 서비스를 받고 낮은 레이턴시를 달성해야 한다. 여러 개의 읽기 전용 데이터베이스 서버를 두고 메인 데이터베이스 서버로부터 데이터를 복제해 놓자.
읽기 전용 데이터베이스는 읽기 요청을 처리하는 데 사용되고 메인 데이터베이스는 읽고 수정하는 명령을 처리하는 데 사용된다.
이 구성 환경에서 몇 까지 복잡한 문제가 발생한다.
첫 번째는 데이터 회복 스크립트를 돌리는 것이다. 두 번째는 여러 개의 메인 데이터베이스를 셋업하는 것이다. 마지막은 순환 복제 셋업을 구성하는 것이다.
데이터베이스 콜을 비싸고 종종 느리다. 읽기 레이턴시를 줄이기 위해 캐싱을 구현할 필요가 있고 캐시 티어를 마련할 수 있다. 이 방법은 데이터베이스 로드를 효과적으로 줄일 수 있다.
캐싱을 구현한다면 몇 가지 명심해두자.
캐싱의 만료 정책을 구현해야 한다. 이 정책은 데이터가 캐시에서 지워지도록 한다. 이 기간 동안 데이터베이스에는 접근이 되지 않는다. 종종 이 특정 시간을 TTL이나 Time To Live라고 한다. 이 만료 기간은 너무 짧으면 계속 데이터베이스로부터 데이터를 가져와야 해서 어플리케이션에 영향을 미친다. 그렇다고 너무 길어서도 안 된다. 너무 길면 최신 데이터를 유지할 수 없다.
캐시와 데이터베이스 사이의 일관성을 유지하는 것이 아주 중요하다. 데이터베이스의 아이템은 변경될 수 있거나 언제든지 수정될 수 있다. 그러나 그 변경 사항이 캐시에 반영되지 않을 수 있다. 다음에 아이템이 캐시에 로드되기 전까지 데이터는 변경되지 않는다. 여러 리전에 걸쳐 확장한 상태라면 캐시 데이터와 지속적인 스토리지 사이에 일관성을 유지하는 것은 아주 큰 도전이 될 것이다.
데이터 제거(Data Eviction) : 캐시에서 용량이 찼고 새로운 데이터가 캐시에 입력돼야 한다면 캐시에서 데이터를 지우는 과정이 필요한데 이 과정을 데이터 제거라고 한다. 유명한 데이터 제거 정책은 Least Recently Used(LRU), Least Frequently Used(LFU), First In First Out(FIFO)이다.
사용자가 전세계적으로 여러 지역에 매우 분산되어 있을 때 컨텐츠 전송 네트워크를 사용해서 CSS 파일이나, 이미지, 비디오, 자바스크립트 파일이나 특정한 동적 콘텐츠를 저장하는 것이 좋다. 컨텐츠 전송 네트워크는 전세계적으로 분산되고 리소스는 가장 가까운 컨텐츠 전송 네트워크에서 클라이언트에 패치된다. 컨텐츠 전송 네트워크는 로드/반응 시간을 매우 줄인다.
기억해둬야 할 것은 다음과 같다:
STEP 5는 데이터베이스의 로드를 줄이는 방법이고 STEP 6는 웹 서버의 로드를 줄인다
웹 계층을 수평적으로 늘리려면 웹 계층에 있는 세션 데이터 같은 상태를 옮겨야 한다. 만약 웹 계층을 무상태(stateless)하게 만들지 않으면 상태가 유지될 때 다른 서버에 유저의 요청을 보낼 수 없다. 이 문제에 가장 나이브한 솔루션은 유저의 세션 정보를 특정 서버와 묶는 스티키 세션이다. 결과적으로 해당 세션의 유저로부터 오는 이후의 모든 요청은 특정 서버로 향한다. 이 방법은 많은 웹 서버를 두는 것의 장점을 활용하지 않는다. 따라서 우아한 솔루션이라고 말할 수 없다. 웹 계층을 stateless하게 만드는 것은 자동으로 웹 계층을 늘리고 줄이는 데(독립적으로 웹 계층을 확장하기) 도움을 줄 수 있다.
많은 과정을 거쳐 여기까지 왔다. 이제 웹 계층을 작은 서비스로 나누고 싶다. 핵심은 소결합을 구현해서 밀결합하지 않게 컴포넌트를 만드는 것이다. 만약 하나의 컴포넌트에서 실패하더라도 다른 컴포넌트에 영향을 주지 않고 계속 작동하게 만드는 것이다. 마치 서로 의존하고 있지만 소결합으로 구성되어 있어서 실패가 발생하지 않는 것처럼 보이는 것인다.
메세지 큐는 많은 분산 시스템에 소결합을 적용하는 핵심 전략이다.
메세지 큐를 구현하는 어떤 방법은 자바 메세지 서비스나 JMS, Amazon SQS Queue, Azure Queue 또는 다른 여러가지가 있다.
메세지 큐는 메모리에 저장되고 고가용성을 지원한다.
메세지 큐는 Pub-Sub 구조이다. 생산자 또는 발행자라고 불리는 인풋 서비스는 메세지를 생산하고 메세지 큐에 메세지를 넣는다. 소비자 혹은 구독자라 불리는 서비스나 서버는 큐와 연결되고 처리할 메세지를 구독한다.
메세지 큐의 장점
이제 데이터베이스 샤딩을 이용해서 데이터베이스를 확장할 때가 왔다.
여기서도 명심해야 할 것이 있다.
리샤딩 : 데이터가 계속 증가함에 따라 샤드가 더 이상 데이터를 저장하지 못하거나 특정 데이터 샤드가 비균형적인 데이터 분배가 발생하면 빨리 exhaustion을 겪게 된다. 이때 이미 샤드된 DB를 리샤딩할 필요가 있다.
조인과 비정규화 : 일단 데이터베이스가 여러 서버에 샤딩이 되면, 성능과 복잡성 제약으로 인해 여러 데이터베이스 샤드에 조인을 수행하는 것이 어렵다. 일반적인 해결 방법은 데이터베이스를 비정규화(하나 이상의 테이블에 데이터를 중복해서 배치하는 최적화 기법)를 통해 이전에 필요했던 조인을 하나의 테이블에서 수행할 수 있게 하는 것이다.
이제 비즈니스가 상당히 성장했고, 서로 다른 종류의 메트릭을 수집하는 것이 사이트의 비즈니스적 통찰을 얻기 위해 중요하다. 중요한 메트릭으로는 다음과 같다.
이제 자동화다. 우리의 인프라나 코드 베이스는 점점 커진다. 효율을 향상 시키기 위해 서로 다른 자동화를 활용할 필요가 있다. 지속적 통합은 요즘 아주 중요한 이론이다.
또한 에러를 트래킹하기 위해 적절한 로깅도 필수이다. 에러 로그를 모니터링하는 것은 아주 중요하다.
사이트는 계속 빠르게 성장하고 수 백만의 다국적 유저를 보유하고 있다. 가용성을 향상시키고 더 넓은 지역에 사용자 경험을 증가시키기 위해서 하나 이상의 데이터 센터에 사이트를 배포하는 것이 아주 중요하다.
유저 요청은 가장 가까운 데이터 센터에 geoDNS 기반으로 라우팅 된다. GeoDNS는 유저의 위치를 기반으로 도메인 이름을 IP 주소를 찾아주는 DNS 서비스다.
적절한 관리가 이뤄져서 모든 데이터 센터에 데이터가 적절히 동기화돼야 한다.
위의 단계 1부터 단계 11까지는 아마도 스크래치로부터 시스템을 구축하고 한 명의 유저부터 천 만 유저까지 시스템을 확장하는 데 도움이 될 것이다.
그래서 분산된 확장 가능한 시스템을 구축할 때 염두해 둬야 할 것은 아래와 같다.