수백만 사용자를 지원하는 시스템을 설계하는 것은 도전적인 과제이다!
또한, 지속적인 계량과 끝없는 개선이 요구된다.
이번 장에서는 한 명의 사용자 → 몇 백만 사용자까지 지원하는 시스템을 설계해보자.
단일 서버
모든 컴포넌트가 단 한대의 서버에서 실행되는 간단한 시스템부터 설계해보자.

- 웹 앱, 데이터베이스, 캐시 등이 전부 서버 한 대에서 실행된다.
위 시스템 구성을 이해하기 위해서는 사용자 요청 처리 흐름을 알아야 한다!

-
사용자는 도메인 이름(api.mysite.com
)을 이용해서 웹 사이트에 접속한다.
- 접속을 위해서는 도메인 이름을 DNS에 질의해서 IP 주소로 변환하는 과정이 필요하다.
-
DNS 조회 결과로 IP 주소가 반환된다.
-
해당 IP 주소로 HTTP 요청이 전달된다.
-
요청을 받은 웹 서버는 HTML 페이지나 JSON 형태의 응답을 반환한다.
그렇다면 실제 요청이 어디서부터 올까?
- 웹 애플리케이션
- 비즈니스 로직, 데이터 저장 등을 처리하기 위해서는 서버 구현용 언어(자바, 파이썬 등)를 사용하고, 프레젠테이션용으로는 클라이언트 구현용 언어(HTML, 자바스크립트)를 사용한다.
- 모바일 웹
- 모바일 앱과 웹 서버 간 통신을 위해서는 HTTP 프로토콜을 사용한다.
- 응답 데이터의 포맷으로는 보통 JSON이 쓰인다.
데이터베이스
사용자가 늘면 서버 하나로는 충분하지 않아서 여러 서버를 두어야 한다.
- 웹/모바일 트래픽 처리 용도
- 데이터베이스용

- 웹/모바일 트래픽 처리 서버(웹 계층) ↔ 데이터베이스 서버(데이터 계층)을 분리하면, 독립적으로 확장해 나갈 수 있게 된다.
어떤 데이터베이스를 사용할 것인가?
전통적인 관계형 데이터베이스와 비-관계형 데이터베이스
-
관계형 데이터베이스
- 관계형 데이터베이스 관리 시스템(RDBMS)
- MySQL, 오라클, PostgreSQL
- 자료를 테이블과 열, 컬럼으로 표현
- SQL을 사용해 여러 테이블에 있는 데이터를 관계에 따라 조인해서 합칠 수 있다!
-
비-관계형 데이터베이스
- NoSQL
- CouchDB, Neo4j, Amazon DynamoDB
- 네 종류로 나뉨 → 키-값 저장소, 그래프 저장소, 칼럼 저장소, 문서 저장소
- 조인 연산을 지원하지 않는다.
비-관계형 데이터베이스가 바람직한 경우
- 아주 낮은 응답 지연시간(latency)가 요구될 경우
- 다루는 데이터가 비정형이라 관계형 데이터가 아닐 경우
- 데이터를 직렬화하거나, 역직렬화할 수 있기만 하면 되는 경우
- 아주 많은 양의 데이터를 저장할 필요가 있는 경우
수직적 규모 확장 vs 수평적 규모 확장
수직적 규모 확장(scale up)
→ 서버에 고사양 자원(더 좋은 CPU, RAM 등)을 추가하는 행위
수평적 규모 확장(scale out)
→ 더 많은 서버를 추가하여 성능을 개선하는 행위
수직적 규모 확장의 장단점
-
장점
- 서버로 유입되는 트래픽의 양이 적을 때 유용하다.
- 단순하다.
-
단점
- 한 대의 서버에 CPU나 메모리를 무한대로 증설할 수 없으므로, 한계가 있다.
- 장애에 대한 자동복구 방안이나 다중화 방안을 제시하지 않는다.
- 서버에 장애가 발생하면 웹 사이트는 완전히 중단된다.
→ 따라서, 대규모 애플리케이션을 지원하는 데는 수평적 규모 확장법이 보다 적절하다!
위의 설계에서 사용자는 웹 서버에 바로 연결된다.
- 웹 서버가 다운되면, 사용자는 웹 사이트에 접속할 수 없다.
- 너무 많은 사용자가 접속하면 응답 속도가 느려지거나, 서버 접속이 불가능해질 수도 있다.
→ 부하 분산기, 또는 로드 밸런서(load balancer)를 도입하자!
로드 밸런서
로드 밸런서는 부하 분산 집합(load balancing set)에 속한 웹 서버들에게 트래픽을 고르게 분산시킨다.

부하 분산 집합에 웹 서버를 추가하고 나면, 장애를 자동복구하지 못하는 문제는 해소되고, 웹 계층의 가용성은 향상된다!
→ 이제 웹 계층은 괜찮아보이는데, 데이터 계층은 여전히 하나의 서버이다!
데이터베이스 다중화
많은 데이터베이스 관리 시스템이 다중화를 지원한다.
- 보통은 서버 사이에
master-slave
관계를 설정하고, 데이터 원본은 master
, 사본은 slave
에 저장한다.
- 쓰기 연산은 마스터에서만 지원한다.
slave
데이터베이스는 master
로부터 사본을 전달받으며, 읽기 연산만을 지원한다.
- 따라서, 데이터베이스를 변경하는 DML들은 주 데이터베이스로만 전달되어야 한다!

- 대부분의 애플리케이션은 읽기 연산의 비중이 쓰기 연산보다 훨씬 높다.
- 따라서, 통상 부 데이터베이스의 수가 주 데이터베이스보다 많다.
데이터베이스를 다중화할 때의 이득
-
더 나은 성능
- 모든 데이터 변경 연산은 주 데이터베이스, 읽기 연산은 부 데이터베이스 서버들로 분산된다.
- 병렬로 처리될 수 있는 쿼리의 수가 늘어나므로, 성능이 좋아진다.
-
안정성
- 자연 재해 등의 이유로 데이터베이스 서버 가운데 일부가 파괴되어도 데이터는 보존된다.
- 데이터를 지역적으로 떨어진 여러 장소에 다중화시켜 놓을 수 있기 때문!
-
가용성
- 데이터를 여러 지역에 복제해 둠으로써, 하나의 데이터베이스 서버에 장애가 발생하더라도 다른 서버에 있는 데이터를 가져와서 서비스할 수 있다.
데이터베이스 다중화가 된 상태에서 데이터베이스 서버 하나가 다운된다면?
-
부 서버가 한 대뿐인데 다운된 경우
- 읽기 연산은 한시적으로 모두 주 데이터베이스로 전달된다.
- 즉시 새로운 부 데이터베이스 서버가 장애 서버를 대체한다.
-
부 서버가 여러 대인데 다운된 경우
- 읽기 연산은 나머지 부 데이터베이스 서버들로 분산된다.
- 새로운 부 데이터베이스 서버가 장애 서버를 대체한다.
-
주 데이터베이스 서버가 다운된 경우
- 한 대의 부 데이터베이스만 있는 경우, 부 데이터베이스 서버가 새로운 주 서버가 된다.
- 프로덕션 환경에서는 더 복잡하다!
- 부 서버에 보관된 데이터가 최신 상태가 아닐 수가 있다.
- 없는 데이터는 복구 스크립트를 돌려서 추가해야 한다.
로드밸런서와 데이터베이스 다중화를 고려한 설계안

- 사용자는 DNS로부터 로드밸런서의 공개 IP 주소를 받는다.
- 사용자는 해당 IP 주소를 사용해서 로드밸런서에 접속한다.
- HTTP 요청은 서버 1이나, 서버 2로 전달된다.
- 웹 서버는 사용자의 데이터를 부 데이터베이스 서버에서 읽는다.
- 웹 서버는 데이터 변경 연산은 주 데이터베이스로 전달한다.
→ 이제는 캐시를 붙이고, 정적 콘텐츠를 CDN으로 옮겨서 응답시간을 개선해보자!
캐시
캐시란?
→ 값비싼 연산 결과 또는 자주 참조되는 데이터를 메모리 안에 두고, 뒤이은 요청이 보다 빨리 처리될 수 있도록 하는 저장소
- 웹 페이지를 새로고침 할 때마다 표시할 데이터를 가져오기 위해 한 번 이상의 데이터베이스 호출이 발생한다.
- 애플리케이션의 성능은 데이터베이스를 얼마나 자주 호출하느냐에 크게 좌우된다.
- 캐시는 그런 문제를 완화할 수 있다!
캐시 계층
캐시 계층이란?
- 데이터가 잠시 보관되는 곳으로, 데이터베이스보다 훨씬 빠르다.
- 별도의 캐시 계층을 두면 성능이 개선될 뿐 아니라, 데이터베이스의 부하를 줄일 수 있다.
- 또한, 캐시 계층의 규모를 독립적으로 확장시키는 것도 가능해진다.
읽기 주도형 캐시 전략

- 요청을 받은 웹 서버는 캐시에 응답이 저장되어 있는지를 본다.
- 만일 저장되어 있다면, 해당 데이터를 클라이언트에 반환한다.
- 없는 경우에는 데이터베이스를 통해 데이터를 찾아 캐시에 저장하고, 클라이언트에 반환한다.
- 캐시 서버를 이용하는 방법은 간단하다!
- 대부분의 캐시 서버들이 API를 제공한다.
- ex)
memcached API
SECONDS = 1
cache.get('myKey', 'hi there', 3600 * SECONDS)
cache.get('myKey')
캐시 사용 시 유의할 점
캐시를 사용할 때는 여러 사항들을 고려해야 한다.
-
캐시는 어떤 상황에 바람직한가?
- 데이터 갱신은 자주 일어나지 않지만, 참조는 빈번하게 일어나는 경우
-
어떤 데이터를 캐시에 두어야 하는가?
- 캐시는 데이터를 휘발성 메모리에 두므로, 영속적으로 보관할 데이터를 캐시에 두는 것은 바람직하지 않다.
- 중요한 데이터는 지속적 저장소에 두어야 한다.
-
캐시에 보관된 데이터는 어떻게 만료되는가?
- 만료된 데이터는 캐시에서 삭제되어야 하는데, 만료 정책이 없으면 데이터는 캐시에 계속 남게 된다.
- 만료 기한이 너무 짧으면 데이터베이스를 너무 자주 읽게 되고, 너무 길다면 원본과 차이가 날 가능성이 높아진다.
-
일관성은 어떻게 유지되는가?
- 일관성 : 데이터 저장소의 원본과 캐시 내의 사본이 같은지 여부
- 저장소의 원본을 갱신하는 연산과 캐시를 갱신하는 연산이 단일 트랜잭션으로 처리되지 않는 경우, 깨질 수 있다.
-
장애에는 어떻게 대처할 것인가?
- 캐시 서버를 한 대만 두는 경우 해당 서버는 단일 장애 지점(SPOF)이 되어버릴 가능성이 있다.
- 단일 장애 지점 : 어떤 특정 지점에서의 장애가 전체 시스템의 동작을 중단시켜버릴 수 있는 경우

-
캐시 메모리는 얼마나 크게 잡을 것인가?
- 캐시 메모리가 너무 작으면 데이터가 자주 캐시에서 밀려나버려, 캐시의 성능이 떨어지게 된다.
- 캐시 메모리를 과할당하면 캐시에 보관될 데이터가 갑자기 늘어났을 때 생길 문제도 방지할 수 있다.
-
데이터 방출 정책은 무엇인가?
- 캐시가 꽉 차버리면 추가로 데이터를 넣어야 할 경우, 기존 데이터를 내보내야 한다.
- LRU, LFU, FIFO와 같은 정책이 주로 쓰인다.
콘텐츠 전송 네트워크(CDN)
CDN이란?
→ 정적 콘텐츠를 전송하는 데 쓰이는, 지리적으로 분산된 서버의 네트워크
- 이미지, 비디오, CSS, JavaScript 파일 등을 캐시할 수 있다.
CDN의 동작 흐름
- 어떤 사용자가 웹 사이트를 방문하면, 그 사용자에게 가장 가까운 CDN 서버가 정적 콘텐츠를 전달하게 된다.
- 사용자가 CDN 서버로부터 멀수록, 웹 사이트는 천천히 로드된다.

CDN의 동작 과정

- 사용자 A가 이미지 URL을 통해
image.png
에 접근한다.
- CDN 서버의 캐시에 해당 이미지가 없는 경우, 서버는 원본 서버(웹 서버 or S3)에 요청하여 파일을 가져온다.
- 원본 서버가 파일을 CDN 서버에 반환한다.
- 응답 헤더에는 TTL(해당 파일이 얼마나 오래 캐시될 수 있는지) 값이 들어있다.
- CDN 서버는 파일을 캐시하고 사용자 A에게 반환한다.
- 사용자 B가 같은 이미지에 대한 요청을 CDN 서버에 전송한다.
- 만료되지 않은 이미지에 대한 요청은 캐시를 통해 처리된다.
CDN 사용 시 고려해야 할 사항
-
비용
- CDN은 보통 제3 사업자에 의해 운영되고, 데이터 전송 양에 따라 요금을 내게 된다.
- 자주 사용되지 않는 콘텐츠를 캐싱하는 것은 이득이 크지 않다.
-
적절한 만료 시한 설정
- 만료 시한은 너무 길지도, 짧지도 않아야 한다.
- 너무 길면 콘텐츠의 신선도는 떨어지고, 너무 짧으면 원본 서버에 빈번히 접속하게 된다.
-
CDN 장애에 대한 대처 방안
- CDN 자체가 죽었을 경우 웹 사이트/애플리케이션이 어떻게 동작해야 하는지 고려해야 한다.
-
콘텐츠 무효화 방법
- 아직 만료되지 않은 콘텐츠라도, CDN에서 제거할 수 있다.
- CDN 서비스 사업자가 제공하는 API를 이용해서 콘텐츠 무효화
- 콘텐츠의 다른 버전을 서비스하도록 오브젝트 버저닝을 이용 →
image.png?v=2
CDN과 캐시가 추가된 설계안

- 정적 콘텐츠는 더 이상 웹 서버를 통하지 않고, CDN을 통해 서비스하여 성능을 보장한다.
- 캐시가 데이터베이스 부하를 줄여준다.
무상태(stateless) 웹 계층
이제, 웹 계층을 수평적으로 확장하는 방법을 고민해봐야 한다.
- 상태 정보(사용자 세션 데이터)를 웹 계층에서 제거해야 한다.
- 바람직한 전략은, 상태 정보를 RDBMS나 NoSQL 같은 지속성 저장소에 보관하고, 필요할 때 가져오는 것이다.
→ 무상태 웹 계층!
상태 정보 의존적인 아키텍처
상태 정보를 보관하는 서버는 클라이언트 정보, 즉 상태를 유지하여 요청들 사이에 공유되도록 한다.

- 같은 클라이언트로부터의 요청은 항상 같은 서버로 전송되어야 한다.
- ex) 사용자 A의 상태 정보 → 서버 1, 사용자 B의 HTTP 요청 → 서버 2
- 대부분의 로드 밸런서가 이때문에 고정 세션이라는 기능을 제공하지만, 부담을 준다!
- 게다가 로드 밸런서 뒷단에 서버를 추가하거나 제거하기도 어려워진다.
무상태 아키텍처

이 구조에서 사용자로부터 HTTP 요청은 어떤 웹서버로도 전달될 수 있다!
- 웹 서버는 상태 정보가 필요할 경우 공유 저장소로부터 데이터를 가져온다.
- 따라서, 상태 정보는 웹 서버로부터 물리적으로 분리되어 있다.
- 단순하고, 안정적이며, 규모 확장이 쉽다.
무상태 웹 계층을 갖도록 만든 변경된 설계안

- 세션 데이터를 웹 계층에서 분리하고, 지속성 데이터에 저장한다.
- 자동 규모 확장(auto scaling) : 트래픽 양에 따라 웹 서버를 자동으로 추가하거나 삭제하는 기능
→ 가용성을 높이고, 전 세계 어디서도 쾌적하게 사용할 수 있게 하려면 여러 데이터 센터를 지원하는 것은 필수다!
데이터 센터
두 개의 데이터 센터를 이용하는 사례

- 지리적 라우팅 → 장애가 없는 상황에서 사용자는 가장 가까운 데이터 센터로 안내 된다.
- 사용자의 위치에 따라 도메인 이름을 어떤 IP 주소로 변환할지 결정할 수 있도록 해주는 DNS 서비스
데이터 센터 중, 하나에 심각한 장애가 발생하면 모든 트래픽은 장애가 없는 데이터 센터로 전송된다.

다중 데이터센터 아키텍처를 만들기 위해 해결해야 할 기술적 난제들
-
트래픽 우회
- 올바른 데이터 센터로 트래픽을 보내는 효과적인 방법을 찾아야 한다.
- GeoDNS는 사용자에게서 가장 가까운 데이터센터로 트래픽을 보낼 수 있게 해준다.
-
데이터 동기화
- 데이터 센터마다 별도의 데이터베이스를 사용한다면, 장애가 자동으로 복구되어 다른 데이터베이스로 우회된다고 해도 해당 데이터 센터에서는 찾는 데이터가 없을 수 있다.
- 데이터를 여러 데이터 센터에 걸쳐 다중화해야 한다.
-
테스트와 배포
- 웹 사이트 또는 애플리케이션을 여러 위치에서 테스트해보는 것이 중요하다.
- 자동화된 배포 도구는 모든 데이터 센터에 동일한 서비스가 설치되도록 하는 데 중요한 역할을 한다.
→ 이젠 메시지 큐를 이용해서 시스템의 컴포넌트를 분리하고, 각기 독립적으로 확장될 수 있도록 해보자!
메시지 큐
메시지 큐란?
→ 메시지의 무손실을 보장하는, 비동기 통신을 지원하는 컴포넌트
- 무손실 : 메시지 큐에 일단 보관된 메시지는 소비자가 꺼낼 때까지 안전히 보관된다는 특성
- 메시지의 버퍼 역할을 하며, 비동기적으로 전송한다.
메시지 큐의 기본 아키텍처

- 생산자, 또는 발행자라고 불리는 입력 서비스가 메시지를 만들어 메시지 큐에 발행(publish)한다.
- 큐에는 보통 소비자, 또는 구독자라고 불리는 서비스나 서버가 연결되어 있다.
- 메시지를 받아 그에 맞는 동작을 수행하는 역할을 한다.
메시지 큐의 장점
- 서비스, 또는 서버 간 결합이 느슨해져서 규모 확장성이 보장되어야 하는 안정적 애플리케이션을 구성하기 좋다.
- 생산자는 소비자 프로세스가 다운되어 있어도, 메시지를 발행할 수 있고, 소비자는 생산자 서비스가 가용한 상태가 아니더라도 메시지를 수신할 수 있다.
사용 예시

로그, 메트릭 그리고 자동화
웹 사이트와 함께 사업 규모가 커지고 나면, 로그나 메트릭, 자동화 같은 도구에 필수적으로 투자해야 한다.
-
로그
- 에러 로그를 모니터링하는 것은 중요하다.
- 시스템의 오류와 문제들을 보다 쉽게 찾아낼 수 있다.
- 서버 단위로 모니터링할 수도 있지만, 로그를 단일 서비스로 모아주는 도구를 활용하면 편리하다.
-
메트릭
- 메트릭을 잘 수집하면 사업 현황에 관한 유용한 정보를 얻을 수도 있고, 시스템의 현재 상태를 손쉽게 파악할 수도 있다.
- 호스트 단위 메트릭 : CPU, 메모리, 디스크 I/O에 관한 메트릭
- 종합 메트릭 : 데이터베이스 계층의 성능, 캐시 계층의 성능
- 핵심 비즈니스 메트릭 : 일별 능동 사용자, 수익, 재방문
-
자동화
- 시스템이 크고 복잡해지면 생산성을 높이기 위해 자동화 도구를 활용해야 한다.
- 지속적 통합(CI), 빌드, 테스트, 배포 등의 절차를 자동화해서 개발 생산성을 크게 향상시킬 수 있다.
메시지 큐, 로그, 메트릭, 자동화 등을 반영하여 수정한 설계안

- 메시지 큐는 각 컴포넌트가 보다 느슨히 결합될 수 있도록 하고, 결함에 대한 내성을 높인다.
- 로그, 모니터링, 메트릭, 자동화 등을 지원하기 위한 장치를 추가
데이터베이스의 규모 확장
저장할 데이터가 많아지면 데이터베이스에 대한 부하도 증가하고, 데이터베이스를 증설할 방법을 찾아야 한다.
수직적 확장
기존 서버에 더 많은, 또는 고성능의 자원을 증설하는 방법이다.
- 단점
- 데이터베이스 서버 하드웨어에는 한계가 있으므로, CPU, RAM 등을 무한 증설할 수 없다.
- SPOF로 인한 위험성이 크다.
- 비용이 많이 든다.
수평적 확장
샤딩(sharding) → 데이터베이스의 수평적 확장
- 더 많은 서버를 추가함으로써, 성능을 향상시킨다.

샤딩(Sharding)

- 대규모 데이터베이스를 샤드라고 부르는 작은 단위로 분할하는 기술
- 모든 샤드는 같은 스키마를 쓰지만, 샤드에 보관되는 데이터 사이에는 중복이 없다.
각 샤드 노드에 사용자 데이터가 보관되는 모습

샤딩 전략을 구현할 때 고려해야 할 가장 중요한 것
- 샤딩 키를 어떻게 정하느냐! 이다.
- 샤딩 키는 파티션 키라고도 부르는데, 데이터가 어떻게 분산될지 정하는 하나 이상의 컬럼으로 구성된다.
- 샤딩 키를 통해 올바른 데이터베이스에 질의를 보내서 데이터 조회나 변경을 처리하므로, 효율을 높일 수 있다.
- 데이터를 고르게 분할할 수 있도록 하는 것이 가장 중요하다!
샤딩을 도입할 때의 문제점
-
데이터의 재 샤딩(resharding)
- 데이터가 너무 많아져서 하나의 샤드로는 더 이상 감당하기 어려울 때
- 샤드 간 데이터 분포가 균등하기 못하여, 어떤 샤드에 할당된 공간 소모가 다른 샤드에 비해 빨리 진행될 때
-
유명인사 문제(핫스팟 키 문제)
- 특정 샤드에 질의가 집중되어 서버에 과부하가 걸리는 문제
-
조인과 비정규화
- 하나의 데이터베이스를 여러 샤드 서버로 쪼개면, 데이터를 조인하기가 힘들어진다.
- 데이터베이스를 비정규화하여 하나의 테이블에서 질의가 수행될 수 있도록 해야 한다.
데이터베이스 샤딩을 적용한 설계안

백만 사용자, 그리고 그 이상
시스템의 규모를 확장하는 것은 지속적이고, 반복적인 과정이다.
- 위에서 다룬 내용을 반복하다 보면 원하는 규모의 시스템을 달성할 수 있다.
- 하지만, 수백만 사용자 이상을 지원하려면 새로운 전략을 도입해야 한다.
시스템 규모 확장을 위해 살펴본 기법들
웹 계층은 무상태 계층으로
모든 계층에 다중화 도입
가능한 한 많은 데이터를 캐시할 것
여러 데이터 센터를 지원할 것
정적 콘텐츠는 CDN을 통해 서비스할 것
데이터 계층은 샤딩을 통해 그 규모를 확장할 것
각 계층은 독립적 서비스로 분할할 것
시스템을 지속적으로 모니터링하고, 자동화 도구들을 활용할 것