Post

가상 면접 사례로 배우는 대규모 시스템 설계 기초 1장 - 사용자 수에 따른 규모 확장성

가상 면접 사례로 배우는 대규모 시스템 설계 기초 책 정리 글입니다.

개요

원준이라는 친구가 나 공부 열심히하라고 책을 선물해줬다.

읽어보도록 했다.


1장은 한 명의 사용자를 지원하는 시스템에서 시작한다.

✏️ 단일 서버

웹브라우저, 웹 서버, DNS가 있을때 사용자의 요청 (웹 브라우저에서 요청을 보냄)이 어떻게 처리되는지를 알아야한다.

naver에 접속한다고 가정하자.

  1. www.naver.com를 주소창에 입력
  2. DNS서버에 www.naver.com의 IP를 질의
  3. DNS서버에서 www.naver.com에 맞는 IP를 반환.
  4. 반환받은 IP에 HTTP 요청.
  5. 요청을 받은 웹 서버는 요청을 처리.

✏️ 데이터베이스

여기서 만약 사용자가 늘면 트래픽 처리 서버(웹 계층)과 데이터베이스 서버(데이터 계층)을 분리하여 확장시킬 수 있다.

어떤 데이터베이스를 사용할지는 크게 관계형 데이터베이스(relational database)와 비-관계형 데이터베이스 사이에서 고를 수 있다.

관계형 데이터베이스

  • RDBMS(Relational Database Management System)으로도 불린다.
  • MySQL, Oracle, PostgreSQL 등이 있다.
  • 조인 연산이 가능.

비-관계형 데이터베이스

  • NoSQL로도 불린다.
  • CouchDB, Neo4j, Cassandra, HBase, Amazon DynamoDB 등이 있다.
  • 이것들은 4개로 또 분리될 수 있다.
    • 키-값 저장소(key-value store)
    • 그래프 저장소(graph store)
    • 칼럼 저장소(column store)
    • 문서 저장소(document sotre)
  • 일반적으로 조인 연산이 불가능.

관계형 데이터베이스는 꾸준히 사랑받았기에 쓰이는것도 있다. 비-관계형 데이터베이스는 다음과 같은 상황에서 고려하기 좋다.

  • 아주 낮은 응답 지연시간(latency)가 요구됨.
  • 다루는 데이터가 비정형이라 관계형 데이터가 아님.
  • 데이터(JSON, YAML, XML, …)를 직렬화 또는 역직렬화 하기만하면 된다.
  • 아주 많은 양의 데이터를 저장할 필요가 있다.

NoSQL이 빠른 이유는 키-값인 경우 데이터를 읽는데에 최적화된 자료구조를 사용하기 때문이다. NoSQL 키-값 저장소는 접근 속도가 O(1)인 HashMap을 일반적인 RDB는 O(N)인 배열을 사용한다.

✏️ 수평적 규모 확장 vs 수직적 규모 확장

스케일 업(scale up)이라고 불리는 수직적 확장, 스케일 아웃(scale out)이라고 불리는 수평적 확장이 있다.

스케일 업 방식은 서버에 CPU를 더 놓거나 RAM을 많이 확보하는 등 하드웨어 등을 추가하여 하나의 서버에서 가능한한 할 수 있는 것들을 하는 방식이라고 생각하고
스케일 아웃 방식은 서버를 증설하여 성능을 개선하는 방식이다.

스케일 업

  • 장점
    • 확장이 단순하다.
  • 단점
    • CPU나 메모리를 무한대로 늘릴 순 없다.
    • 장애에 대한 자동복구(failover) 방안이나 다중화(redundancy) 방안을 제시하지 않는다.
    • 서버 장애 발생시 웹,앱 등 서비스가 완전히 중단된다.

단점이 너무 치명적이라 대규모 시스템에서는 스케일 아웃방식을 사용하는것이 좋다.

서버 장애가 발생시에는 사용자가 웹 사이트에 접속할 수 없고, 서버 장애가 발생하는 원인 중에는 과도한 트래픽양이 있다.

이를 해결하기 위해서 부하 분산기 또는 로드밸런스(load balancer)를 도입하는게 최선이다.

로드밸런서

로드밸런서는 부하 분산 집합(load balancing set)에 속한 웹 서버들에게 트래픽 부하를 고르게 분산하는 역할을 한다.

image

위에서 설명한대로 사용자는 하나의 서버 IP로 들어오면 서버 시스템 설계 내부에 있는 로드밸런서가 Private IP 즉, 사설IP라고도 불리는 각 웹 서버의 IP에 적절한 알고리즘으로 트래픽을 할당해준다.

사설IP는 같은 네트워크에 속한 서버 사이의 통신에서만 쓰이는 IP 주소다.

이렇게 로드밸런서를 추가하면 자동복구 문제도 해소되고 웹 계층의 사용성도 증가한다.

왜냐하면 서버1이 죽으면 복구되는동안 서버2로 트래픽을 주면 되고, 트래픽이 과도하게 증가한다 싶으면 서버를 증설하고 로드밸런서를 통해 우아하게 대처가 가능하기 때문이다.

웹 서버는 이렇게 증설하면 되고 데이터 계층인 데이터베이스는 어떤식으로 확장이 가능할까

데이터베이스 다중화

보통 주(master)-부(slave) 관계를 설정하고 데이터 원본은 주 서버에, 사본은 부 서버에 저장하는 방식을 사용한다.

쓰기 연산 같이 읽기 연산보다 상대적으로 오래걸리고 많은데서 관리하면 일관성 문제를 일으킬 수 있기에 주 서버에서만 지원한다. 부 데이터베이스 서버는 주 데이터베이스 서버로부터 사본을 받고, 읽기 연산만 지원한다.

image

  • 장점
    • 더 나은 성능: 읽기 연산과 쓰기 연산이 분산처리 되므로 효율적이고, 읽기 같은 경우는 병렬로 처리될 수 있으므로 성능이 좋아진다.
    • 안정성: 여러군데에 데이터 사본을 저장해놨기에 데이터베이스 서버 일부가 파고되어도 데이터는 다른데에 보존되어 있을 것이다.
    • 가용성: 여러군데 복사를 해놨기에 하나의 데이터베이스 서버에 장애가 발생해도 다른 서버에 있는 데이터를 가져와 계속 서비스를 진행할 수 있다.

그래서 최종적으로 웹 서버에는 로드밸런서를 둬서 부하를 분산시키고, 데이터베이스는 주, 부 데이터베이스 서버를 나눠 부하를 분산시킨다.


✏️ 캐시

캐시를 알아둬야하는 이유는 응답시간(latency)를 개선하기 위함이다.

캐시는 값비싼 연산 결과나 자주 참조되는 데이터를 메모리 안에 두고 요청에 대한 처리를 빨리하도록 도와준다.

만약 새로고침을 한다하면 데이터를 새로 가져와야할텐데 그때마다 데이터베이스를 호출하는것은 성능에 문제가 생길 수 있다. 캐시는 이런 문제를 완화시켜줄 수 있다.

캐시 계층(Cache tier)

캐시 계층은 데이터가 잠시 보관되는 곳이다. 데이터베이스보다 훨씬 빠르고 데이터베이스의 부하도 줄일 수 있다. 캐시 계층을 독립적으로 확장도 가능하다.

image

위와 같이 캐시 서버는 동작하고 이러한 방식을 읽기 주도형 캐시 전략(read-through caching strategy)라고 부른다. 캐시할 데이터 종류, 크기, 액세스 패턴 등에 따라 전략이 달라지므로 맞는걸 선택하면 된다.

캐시 서버는 일반적으로 널리 쓰이는 프로그래밍 언어로 API를 제공하기에 사용하기 쉽다.

// memcached라는 캐시 시스템 C언어를 사용
SECONDS = 1
cache.set('cache', 'hi cache', 3600 * SECONDS)
cache.get('cache')

캐시 사용시 주의점

  • 갱신은 자주 일어나지 않지만 참조는 빈번이 일어난다면 고려할만 하다.
  • 캐시는 휘발성이므로 영속적으로 보관할 데이터는 캐시에 두는것이 바람직하지 않다.
  • 캐시는 언제 어떻게 만료(expire)되는가?에 대한 정책을 수립할 필요가 있다. 만료시간을 너무 짧게, 너무 길게 가져가지 않도록 주의해야한다.
  • 데이터베이스와 일관성을 어떻게 유지할 것인가? 에 대한 생각도 해봐야한다. 찾아보니 이는 논문도 나올정도로 많은 연구가 진행되고 있는 것 같다.
  • 장애에는 어떻게 대처할 지도 생각해봐야한다. 캐시 서버를 하나로 두면 해당 서버는 단일 장애 지점이 되어버릴 가능성이 있다. 때문에 여러 지역에 걸쳐 캐시 서버를 분산 시켜야 한다.

단일 장애 지점(Single Point of Failure, SPOF)는 어떤 특정 지점에서의 장애가 전체 시스템의 동작을 중단시켜버릴 수 있는 경우 그 지점을 단일 장애 지점이라고 부른다.

  • 캐시 메모리는 얼마나 크게 잡을 것인가? 캐시 메모리가 작다면 데이터가 밀려나면서 자주 갱신되기에 성능이 저하된다. 크게 잡는편이 좋다고 한다.
  • 데이터 방출(eviction) 정책도 수립해야한다. 캐시가 꽉 차면 기존 데이터를 내보내고 새로 데이터를 받아야하는데, 자주 쓰이는 알고리즘은 LRU(Least Recently Used)방식이다. 외에 LFU, FIFO 등이 있다.

✏️ CDN(컨텐츠 전송 네트워크)

CDN은 정적 콘텐츠를 전송하는데 쓰이는 지리적으로 분산된 서버 네트워크다

이미지, 비디오, CSS, javascript파일 등을 캐시할 수 있다.

동작방식은 다음과 같이 간단하다.

  • 사용자가 웹 사이트에 방문
  • 지리적으로 가까운 CDN 서버가 정적 콘텐츠를 사용자에게 전달.

지리적으로 분산된 네트워크라서 거리가 멀면 응답이 느린건 당연하다.

image

거리가 멀수록 요청전달이 느려진다.

image

CDN은 이런식으로 캐싱하여 전달된다.

동작방식은 어렵지 않아서 설명하지 않아도 되지만 추가적으로 알아야할 것들이 있다.

사용자는 이미지 URL을 통해 image.png에 접근하는데, 이 URL 도메인은 CDN 사업자가 제공한 것이다.

1
https://mysite.cloudfront.net/logo.png

와 같은 이미지 URL 예시가 있다.

또한 3번에서 이미지를 저장하고 있는 원본 서버에서 파일을 CDN서버에 반환할 때, 파일이 얼마나 오래 캐시 될 수 있는지를 설명하는 TTL(Time-To-Live) 값이 들어 있다. 이 값을 통해 CDN은 해당 이미지가 캐시에 얼마나 존재할 수 있는지를 정할 수 있다.

CDN 사용 시 고려해야할 사항

  • 비용: CDN은 보통 제 3 사업자에 의해 운영되고, 데이터 전송 양에 따라 요금을 내기 때문에 자주 참조되지 않는 콘텐츠를 캐싱해두는건 비용적으로 손실이 있다.
  • 적절한 만료 시한 설정: 캐시와 마찬가지로 만료시간을 잘 설정해야 성능을 개선할 수 있다.
  • CDN 장애에 대한 대처 방안: CDN이 응답하지 않는 경우 어떻게 해야할 지 생각해야한다. 원본 서버에서 직접 콘텐츠를 가져오도록 클라이언트를 구성하는 것도 방법 중 하나다.
  • 콘텐츠 무효화(invalidation) 방법: 아직 만료되지 않은 콘텐츠라 하더라도 밑의 방법을 통해 CDN에서 제거할 수 있다.
    • CDN 서비스 사업자가 제공하는 API를 이용하여 무효화
    • 콘텐츠의 다른 버전을 서비스하도록 오브젝트 버저닝(object versioning)이용. 새로운 버전을 지어하기 위해 URL 마지막에 버전 번호를 인자로 주면 된다. ex) image.png?v=2

웹서버, 데이터베이스 서버, 로드밸런서, 데이터베이스 다중화(Master-Slave), 캐시 서버, CDN을 추가하면 다음과 같은 시스템 설계가 될 것이다.

image

✏️ 무상태(stateless) 웹 계층

상태 아키텍처

image

웹 계층을 수평적으로 확장하기 위해서는 상태 정보(사용자 세션 데이터 등)을 웹 서버에서 제거해야한다. 만약 3개의 서버(A서버, B서버, C서버)가 있고, 3명의 사용자(A유저, B유저, C유저)가 있다고 치자.

서버 A에는 A유저의 인증정보가, 서버 B에는 B유저의 인증정보가, 서버 C에는 C유저의 인증정보가 있다고 하면 유저가 요청 보낼때마다 해당 서버에 요청이 전달되어야할 것이다. 즉, 사용자에 따라 항상 같은 서버에 요청을 보내야한다. 대부분 로드밸런서는 고정 세션(sticky session)이라는 기능을 제공하여 위의 상황을 지원하는데 로드밸런스 입장에서는 부담이다. 게다가 추가적으로 서버를 늘리거나 제거할 때도 예기치 않은 장애를 일으킬 수 있고 로드밸런서 설정을 건드려줘야하기에 까다로워진다.

무상태 아키텍처

image

그렇기에 사용자의 데이터들을 따로 모아놓는 저장소를 두는게 좋다. 이 저장소는 공유 저장소(shared storage)라고 불려진다.

위처럼 구성하면 요청에 대해 물리적으로 분리해줄 필요는 없기에 부담없이 웹 서버를 수평적으로 확장할 수 있고 이처럼 단순하고 안정적이라 규모확장에도 이점이 있다.

보통 공유 저장소는 관계형 데이터베이스일 수도 있고, Redis와 같은 캐시 시스템일수도 있고, NoSQL일수도 있다.

만약에 서비스가 성장하여 전 세계에 배포되고 서비스를 제공해야 한다면 전 세계에서 빠르게 편하게 사용할 수 있도록 데이터 센터(data center)를 지원하는 것이 필수다.


✏️ 데이터 센터

두 개의 데이터 센터가 있다고 하면 다음 그림처럼 될 것이다.

image

별 일이 없는 경우 사용자의 요청은 가장 가까운 데이터 센터로 요청이 갈텐데, 이를 지리적 라우팅(geoDNS-routing)이라고 한다. 사용자의 위치에 따라 어떤 IP 주소를 반환할지 결정하는 역할을 한다.

책에서는 x%를 지정하면 x%는 왼쪽 데이터센터로, (100-x)%는 오른쪽 데이터센터로 요청을 보내는 예제를 보여줬다.

만약 데이터 센터 하나가 장애가 발생하여 다른 하나의 데이터 센터로 트래픽이 몰린다면 어떻게 될까?

이 상황을 방지하기 위해 데이터센터를 만들때 고려해야할 사항들이 있다.

  • 트래픽우회: 올바른 데이터센터로 요청을 보내는 방법을 생각해야한다.
  • 데이터동기화(synchronization): 데이터센터마다 별도의 데이터베이스를 사용하고 있다면 데이터일관성의 문제가 발생할 수 있으므로 데이터를 각 데이터센터에 다중화처리를 하는것이다. 그 방법 중 하나는 넷플릭스가 사용하고 있다고 한다.
  • 테스트와 배포(deployment): 여러 데이터센터를 사용하도록 구축되었다면 다양한 환경에서 테스트해보고 배포해야한다. 자동화 배포를 통해 데이터센터가 동일한 서비스를 제공하도록 하자.

더 큰 규모로 확장하기 위해서는 시스템의 컴포넌트들을 분리하여 각기 독립적으로 확장 될 수 있도록 도와줄 필요가 있는데 이를 도와주는 것이 메시지 큐(Message Queue)이다.


✏️ 메시지 큐

메시지 큐는 메시지의 무손실을 보장하는, 비동기 통신을 지원하는 컴포넌트이다.

메시지의 버퍼 역할을 하며 비동기적으로 전송한다.

image

생산자(producer/publisher)가 메시지를 만들어 메시지 큐에 발행하면 소비자(consuemer/subscriber)가 메시지를 받아 메시지에 맞는 동작을 수행한다.

메시지 큐를 사용하면 서비스 또는 서버 간의 결합이 느슨해져서, 규모 확장성이 보장되어야하는 어플리케이션에 안정감을 줄 수 있다.

메시지 큐가 있기에 생산자는 소비자 프로세스가 다운되어도 메시지를 발행할 수 있고, 소비자 또한 생산자가 다운되어 있어도 메시지큐에서 메시지를 받아 볼 수 있다.

만약에 사진 보정과 같이 작업이 오래걸리는 일을 수행해야한다면

웹 서버는 메시지큐에 해당 작업을 보내고, 소비자의 워커(worker)프로세스들은 이 메시지를 받아 비동기적으로 작업을 수행하면 된다.

이렇게 하면 소비자와 생산자가 독립적으로 규모를 확장시킬 수 있다. 메시지 큐가 커지면 소비자에 더 많은 프로세스를 추가하여 처리시간을 줄일 수 있고, 메시지 큐가 거의 비어있다면 프로세스를 줄여 비용절감에 효과를 줄 수도 있다.


✏️ 로그, 메트릭, 자동화

소규모 웹 사이트라면 로그, 메트릭(metric), 자동화(automation)을 하면 좋지만 필수적인 요소는 아니여서 관심이 없었다. 하지만 웹사이트 규모가 커지면서 이런 도구에 필수적으로 투자하는 환경이 조성되었다.

  • 로그: 에러 로그를 모니터링 하는건 따로 적지 않아도 중요하다. 서버 단위로 모니터링 하는것보다 로그를 단일 서버로 모아주는 도구를 활용하면 좋다.
  • 메트릭: 시스템의 현재 상황을 손쉽게 파악할 수 있도록 도와준다.
    • 호스트 단위 메트릭: CPU, 메모리, 디스크I/O에 관한 메트릭
    • 종합 메트릭: DB계층의 성능, 캐시 계층의 성능
    • 핵심 비즈니스 메트릭: 일별 사용자, 수익, 재방문 같은 것들.
  • 자동화: CI(continuous integration) 지속적 통합을 도와주는 도구를 활용하여 개발자의 코드가 어떤 검증 절차를 자동으로 거치도록 할 수 있다. 이를 통해 문제를 감지할 수도 있다. 더 나아가 빌드, 테스트, 배포 자동화를 하여 개발 생산성을 증대시킬 수 있다.

✏️ 데이터베이스의 규모 확장

위에서 스케일 업의 장단점에 대해 설명하였고, 책에서는 스케일 아웃 = 수평적 확장 = 샤딩으로 설명하고 있다.

샤딩에 대해서 정리할 것이다.

샤딩

샤딩은 대규모 데이터베이스를 샤드라는 작은 단위로 분할하는 기술을 일컫는다.

모든 샤드들은 같은 스키마를 사용하지만 데이터 중복은 없다.

image

위 방식은 user_id를 나눈 나머지 값에 따라 데이터를 보관하는 샤드를 정한다.

그렇기에 샤드0에는 user_id가 0,4,8,12 …인 값들이 저장될 것이고, 샤드1에는 user_id가 1,5,9,13, …인 값들이 저장될 것이다.

이처럼 샤드를 구성할때에는 user_id처럼 샤드를 결정짓는 샤딩 키(sharding key)를 정하는 것이 중요하다. 샤딩 키는 파티션 키(partition key)로도 부른다. 데이터를 어떻게 분산시킬 지 정하는 하나 이상의 컬럼으로 구성된다. 데이터를 고르게 분산할 수 있는 샤딩 키를 정하는 것이 중요하다.

샤딩은 완벽하지는 않다. 샤딩을 도입하면서 새로 고려해야할 문제들이 생기기 때문이다.

  • 데이터의 재샤딩: 데이터가 너무 많아져서 하나의 샤드로 감당이 안되거나 샤드 간 데이터 불균형으로 인해 어떤 한 샤드의 공간이 빠르게 소진될 때 발생하는 문제이다. 이를 샤드 소진(shard exhaustion)로도 불리며 샤드 키를 계산하는 함수를 변경하여 데이터를 재배치 해야한다. 안정 해시(consistent hashing)을 통해 해결할 수 있다.
  • 유명인사(celebrity) 문제: 만약 한 샤드에 봉준호, BTS, 손흥민이 있다고 치면 해당 샤드에 read가 많이 걸려 부하가 발생할 수 있다. 위 유명인사들을 각각 샤드로 쪼개 해결할 수는 있다.
  • 조인과 비정규화: 샤드는 데이터가 여러군데에 분산되어 있기에 조인이 힘들어진다. 하나의 방법은 데이터베이스를 비정규화하여 하나의 테이블에서 하나의 질의가 수행되도록 하는 것이다.

🤔 정리

1장 내용은 스토리형식으로 시스템 규모가 확장될때마다 고려해야할 사항에 대해 설명했다.

  • 데이터베이스를 설정할때 고려해야할 사항(관계형 DB ? NoSQL ?)
  • 스케일 업의 단점이 있기에 스케일 아웃을 선호하는데 이를 해결하기 위해선 웹 계층에서는 로드밸런서로 트래픽을 관리, 데이터 계층에서는 Master-Slave방식으로 트래픽을 관리하였다.
  • 더나아가 자주 참조되는건 캐시 서버로 둬서 빠른 응답속도를 제공하였지만, 데이터 일관성 부분은 숙제로 남아있다.
  • CDN을 통해서 정적 리소스들을 사용자들에게 빠르게 전달할 수 있게 되었다.
  • 좀 더 유연한 요청처리를 위해 무상태로 웹을 구성해야하는데, 이를 도와주기 위해 공유저장소를 두었다.
  • 위의 구성들을 사용자들에게 빠른 응답을 제공하기 위해 데이터센터를 두었고, 각각 데이터 센터에서도 데이터 일관성을 고려하기 시작하였다.
  • 메시지 큐를 둬서 소비자와 생산자가 각각 독립적으로 확장할 수 있게 돕고, 비동기적 처리로 인해 일의 효율성도 증대 되었다.
  • 로그, 메트릭을 통해 시스템을 헬스체크를 할 수있고 모니터링 할 수 있게 되었다.
  • 자동화를 통해 검증된 로직을 통해 테스트, 빌드, 배포까지 할 수 있게 도와주게 되었다.
  • 데이터베이스를 샤드라는 작은 단위로 쪼개 효율적으로 데이터베이스를 관리할 수 있게 되었지만 그에 따른 고려해야할 사항이 많다는걸 알게되었다.
This post is licensed under CC BY 4.0 by the author.