개요

『가상 면접 사례로 배우는 대규모 시스템 설계 기초』 책은 대규모 트래픽을 받아 본 경험이 없는 필자에게 많은 인사이트를 얻게 해주었다. 필자와 같은 상황이라면 읽어보는 것을 추천한다! (특정 기술에 대한 부가 설명 링크도 제공하고 있으니 꼭 읽어보는 것을 추천!)

알렉스 쉬, 『가상 면접 사례로 배우는 대규모 시스템 설계 기초』 (인사이트, 2021) 1장 내용을 읽고 종합적으로 정리하였습니다.

목표

한 명의 사용자를 지원하는 시스템에서 시작하여, 최종적으로는 몇백만 사용자를 지원하는 시스템을 설계해 보자.

단일 서버 구성

먼저 한 대의 서버에서 실행되는 간단한 시스템부터 설계해 보고, 응답시간(latency)을 개선하기 위한 방식들을 순차적으로 적용해 보자.

요청 처리 흐름 과정 🌊

  1. 도메인 이름(www.platinouss.com)을 DNS에 질의한다.
  2. DNS 조회 결과로 IP 주소(172.16.254.1)가 반환된다. 이 주소는 웹 서버의 주소이다.
  3. 해당 주소(웹 서버)로 HTTP 요청이 전달된다.
  4. 요청을 받은 웹 서버는 데이터베이스에 접근해 요청을 처리하거나, 필요한 데이터를 반환받는다.
  5. 서버는 최종적으로 웹 페이지나 JSON 형태의 응답을 반환한다.

어떤 DB를 사용해야 할까❓

관계형 데이터베이스(RDB)와 비-관계형 데이터베이스(NoSQL)에서 고를 수 있다.

NoSQL이 바람직한 선택일 때는 언제일까?

  • 아주 낮은 응답 지연시간이 요구될 경우
  • 다루는 데이터가 비정형이라 관계형 데이터가 아닐 경우
  • 데이터(JSON 등)를 직렬화(Serialize) 하거나, 역직렬화 할 수 있기만 하면 될 경우
  • 아주 많은 양의 데이터를 저장할 필요가 있을 경우

Scale-Up 🆚 Scale-Out

Scale-Up은 수직적 규모 확장으로 서버에 고사양 자원(더 좋은 CPU, 더 많은 RAM 등)을 추가하는 행위를 말한다. 반면 Scale-Out은 수평적 규모 확장으로 더 많은 서버를 추가하여 성능을 개선하는 행위를 말한다.

그럼 각 확장 방법의 장단점에는 어떤 것이 있을까?

Scale-Up은 단순함이라는 장점이 있다. 따라서 서버로 유입되는 트래픽의 양이 적을 때는 좋은 선택이 될 수 있지만 한계점이 존재한다.

먼저, 한 대의 서버에 CPU나 메모리를 무한대로 증설할 방법이 없기 때문에 한계가 있다. 또한 장애에 대한 자동복구(failover) 방안이나 다중화(redundancy) 방안을 제시하지 않아 서버에 장애가 발생하면, 한 대의 서버로 구동되던 웹 사이트(앱)은 완전히 중단되게 된다.

따라서 대규모 애플리케이션을 지원하는 데는 Scale-Out이 적절하다.

응답시간 개선 방식

로드 밸런서(Load Balancer)

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

사용자는 로드 밸런서의 공개 IP 주소(Public IP)로 접속하므로 웹 서버는 클라이언트의 접속을 직접 처리하지 않는다. 또한 보안을 위해, 서버 간 통신에는 사설 IP 주소(Private IP)가 이용된다.


만약, 서버 1이 다운되면 모든 트래픽은 서버 2로 전송된다. 따라서 웹 사이트(앱) 전체가 다운되는 일이 방지된다.

또한 부하를 나누기 위해 새로운 서버를 추가할 수도 있다. 웹 사이트로 유입되는 트래픽이 급격하게 증가하면, 두 대의 서버로 트래픽을 감당할 수 없는 시점이 오게되는데, 이때 웹 서버 계층에 더 많은 서버를 추가하면 로드 밸런서가 자동적으로 트래픽을 분산할 것이다.

따라서, 부하 분산 집합에 새로운 웹 서버를 추가하면 장애를 자동복구(failover)할 수 있으며, 웹 계층의 가용성(availability)이 향상된다.

데이터베이스 다중화

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


보통 쓰기 연산(write operation)은 주 데이터베이스에서만 지원하고, 부 데이터베이스는 주 데이터베이스로부터 사본을 전달받으며, 읽기 연산(read operation)만을 지원한다.

부 데이터베이스의 수가 주 데이터베이스의 수보다 많은 이유는, 대부분의 애플리케이션은 읽기 연산의 비중이 쓰기 연산보다 훨씬 높기 때문이다.

데이터베이스를 다중화 하면, 다음과 같은 장점을 가진다.

  • 데이터 변경 연산은 주 데이터베이스 서버로만 전달되고, 읽기 연산은 부 데이터베이스 서버로만 전달되어 query가 병렬로 처리된다.
  • 데이터를 여러 장소에 다중화시켜 놓음으로써, 일부 데이터베이스 서버가 파괴되어도 데이터가 보존된다.
  • 데이터를 여러 지역에 복제함으로써, 하나의 데이터베이스 서버에 장애가 발생하더라도 다른 서버의 데이터로 계속 서비스할 수 있다.

그렇지만 해당 방식에도 문제점이 존재한다. 만약 주 데이터베이스와 부 데이터베이스가 각 한 대씩 있으며, 주 데이터베이스 서버가 다운된다고 가정하면, 부 데이터베이스 서버가 새로운 주 서버가 되며, 모든 데이터베이스 연산은 일시적으로 새로운 주 서버에서 진행될 것이다. 그리고 새로운 부 서버가 추가될 것인데, 이때 부 서버에 보관된 데이터가 최신 상태가 아닐 수 있다는 점이다.

해당 문제점은 복구 스크립트(recovery script)를 통한 데이터 추가와 다중 마스터(multi-masters), 원형 다중화(circular replication)를 통해 대응할 수 있다.

이렇게, 로드 밸런서와 데이터베이스 다중화를 적용한 설계는 다음과 같다.

캐시(Cache)

캐시는 값비싼 연산 결과 또는 자주 참조되는 데이터를 메모리 안에 두고, 해당 데이터를 참조하는 요청을 보다 빠르게 처리될 수 있도록 하는 저장소를 뜻한다.
애플리케이션의 성능은 데이터베이스를 얼마나 자주 호출하느냐에 크게 좌우되는데, 캐시는 이러한 문제를 완화하는 역할을 한다.

캐시 계층(cache tier)은 데이터가 잠시 보관되는 곳으로 데이터베이스보다 훨씬 빠르다. 성능이 개선될 뿐만 아니라, 데이터베이스의 부하를 줄일 수 있고, 캐시 계층의 규모를 독립적으로 확장시키는 것도 가능하다.


위와 같은 캐시 전략은 읽기 주도형 캐시 전략(read-though caching strategy)이다. 요청을 받은 웹 서버는 캐시에 응답이 저장되어 있는지를 확인한다. 만일 저장되어 있다면 해당 데이터를 클라이언트에 반환한다. 만약 없다면, 데이터베이스 질의를 통해 데이터를 찾아 캐시에 저장한 뒤 클라이언트에 반환한다.

그 밖의 다양한 캐시 전략이 있는데, 캐시할 데이터 종류, 크기, 액세스 패턴에 맞는 캐시전략을 사용하면 된다.

그럼 캐시 사용 시 유의할 점에는 어떤 것이 있을까?

  • 캐시는 어떤 상황에 바람직한가?
    • 데이터 갱신이 자주 일어나지 않지만, 참조는 빈번하게 일어날 경우에 고려한다.
  • 어떤 데이터를 캐시에 두어야 하는가?
    • 영속적으로 보관하지 않을 데이터를 캐시에 둔다.
  • 캐시 데이터의 만료는 어떻게 설정하는게 좋은가?
    • 만료 기간이 너무 짧을 경우 데이터베이스의 접근이 많아지고, 만료 기간이 너무 길 경우 원본가의 차이가 날 가능성이 높아진다.
  • 일관성(consistency)는 어떻게 유지되는가?
    • 저장소의 원본을 갱신하는 연산과 캐시를 갱신하는 연산이 단일 트랜잭션으로 처리되지 않는 경우, 캐시와 저장소의 일관성을 유지하는 것이 어려운 문제가 된다.
  • 장애에는 어떻게 대처할 것인가?
    • 캐시 서버를 한 대만 두는 경우, 해당 서버는 단일 장애 지점(SPOF)이 될 가능성이 있다. 따라서 여러 지역에 걸쳐 캐시 서버를 분산시켜야 한다.
  • 캐시 메모리는 어느정도로 잡을 것인가?
    • 캐시 메모리가 너무 작을 경우, 데이터가 자주 밀려나(eviction) 캐시의 성능이 떨어진다. 캐시 메모리를 과할당(overprovision)하여, 캐시에 보관될 데이터가 갑자기 늘어났을 때 생기는 문제도 방지할 수 있다.
  • 데이터 방출(eviction) 정책을 어떻게 설정할 것인가?
    • 캐시가 꽉 차 추가로 캐시에 데이터를 넣어야 할 경우 기존 데이터를 내보내는 것을 캐시 데이터 방출 정책이라고 하는데, 경우에 맞는 정책을 적용할 필요가 있다. 가장 널리 쓰이는 것은 LRU(Least Recently Used)이다.

콘텐츠 전송 네트워크(CDN)

CDN은 정적 콘텐츠를 전송하는 데 쓰이는, 지리적으로 분산된 서버의 네트워크로 이미지, 비디오, CSS, JS 파일 등을 캐시할 수 있다.

동적 콘텐츠 캐싱은 요청 경로(request path), 질의 문자열(query string), 쿠키(cookie), 요청 헤더(request header) 등의 정보에 기반하여 HTML 페이지를 캐시하는 것이다.

CDN이 동작하는 과정은 다음과 같다.


어떤 사용자가 웹 사이트를 방문하면, 그 사용자에게 가장 가까운 CDN 서버가 정적 콘텐츠를 전달하게 된다. 또한 만료되지 않은 이미지에 대한 요청은 CDN 서버의 캐시를 통해 처리된다.

CDN 사용 시 고려해야 할 사항에는 다음과 같은 것들이 있다.

  • 자주 사용되지 않는 콘텐츠를 캐싱하는 것은 이득이 크지 않으므로, CDN에서 사용하지 않는다.
  • CDN 장애에 대한 대처 방안
    • 일시적으로 CDN이 동작하지 않을 때, 해당 문제를 감지하여 원본 서버로부터 직접 콘텐츠를 가져오도록 구성하는 것이 필요하다.
  • 아직 만료되지 않은 콘텐츠라 하더라도 콘텐츠 무효화 방안이 필요하다.
    • CDN 서비스 사업자가 제공하는 API, 콘텐츠의 다른 버전을 서비스하도록 오브젝트 버저닝(object versioning)을 이용한다.

무상태(stateless) 웹 계층

웹 계층을 Scale-Out(수평적 확장) 하려면, 상태 정보(사용자 세션 데이터 등)를 웹 계층에서 제거해야 한다. 바람직한 전략은 상태 정보를 관계형 데이터베이스나 NoSQL 같은 지속성 저장소에 저장하고, 필요할 때 가져오도록 한다. 이렇게 구성된 웹 계층을 무상태(stateless) 웹 계층이라 한다.

만약, 상태 의존적인 아키텍처일 경우 같은 클라이언트로부터의 요청은 항상 같은 서버로 전송되어야 한다는 문제가 발생한다. 다른 서버로 요청될 경우, 해당 사용자에 관한 데이터가 보관되어 있지 않기 때문에 인증에 실패한다.


대부분의 로드 밸런서가 특정 세션의 요청을 처음 처리한 서버로만 전송하는 고정 세션(sticky session) 기능을 제공하고 있지만, 해당 기능은 로드 밸런서에 부담을 준다. 또한 로드 밸런서 뒷단에 서버를 추가하거나 제거가 까다롭고, 서버의 장애를 처리하기 복잡해진다.

따라서 상태 정보를 웹 서버로부터 물리적으로 분리시킨 무상태(stateless) 아키텍처를 사용할 필요가 있다. 다음 그림은 무상태(stateless) 아키텍처를 보여준다.


해당 아키텍처는 사용자의 HTTP 요청을 어떤 웹 서버로부터 전달해도 문제없다. 그리고 웹 서버는 상태 정보가 필요할 경우 공유 저장소로부터 데이터를 가져온다. 이 아키텍처는 단순하고, 안정적이며 규모 확장이 쉽다는 장점을 가진다.

데이터 센터

가용성을 높이고 전 세계 어디서도 쾌적하게 사용할 수 있도록 하기 위해서는 여러 데이터 센터(data center)를 지원하는 것이 필수이다.

다음은 두개의 데이터 센터를 이용하는 사례이다.


장애가 없는 상황에서 사용자는 가장 가까운 데이터 센터로 안내되는데, 이 절차를 지리적 라우팅(geoDNS-routing)이라고 부른다.

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

다중 데이터 센터 아키텍처를 만들려면 다음 문제를 해결해야 한다.

  • 트래픽 우회
    • 올바른 데이터 센터로 트래픽을 보내는 효과적인 방법을 찾아야 한다. GeoDNS는 사용자에게 가장 가까운 데이터 센터로 트래픽을 보낼 수 있도록 해준다.
  • 데이터 동기화
    • 데이터 센터마다 별도의 데이터베이스를 사용하고 있는 상황이라면, 장애가 자동으로 복구되어(failover) 트래픽이 다른 데이터베이스로 우회된다 해도, 해당 데이터 센터에는 찾는 데이터가 없을 수가 있다. 이러한 상황을 맞는 보편적인 전략으로는 데이터를 여러 데이터 센터에 걸쳐 다중화 하는 것이다.
  • 테스트와 배포(deployment)
    • 여러 데이터 센터를 사용하도록 구성되어 있다면, 여러 위치에서 테스트해보는 것이 중요하다. 자동화된 배포 도구는 모든 데이터 센터에 동일한 서비스가 설치되도록 하는 데 중요한 역할을 한다.

메시지 큐

메시지 큐는 메시지의 무손실(durability)을 보장하는 비동기(asynchronous) 통신을 지원하는 컴포넌트이다. 메시지의 버퍼 역할을 하며, 비동기적으로 전송한다.


메시지 큐의 기본 아키텍처는, 생산자 또는 발행자(producer/publisher)라고 불리는 입력 서비스가 메시지를 만들어 메시지 큐에 발행(publish)한다. 큐에는 보통 소비자 혹은 구독자(consumer/subscriber)라 불리는 서비스 혹은 서버가 연결되어 있는데, 메시지를 받아 그에 맞는 동작을 수행하는 역할을 한다.

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

생산자는 소비자 프로세스가 다운되어 있어도 메시지를 발행할 수 있고, 소비자는 생산자 서비스가 가용한 상태가 아니더라도 메시지를 수신할 수 있다.

사용 예로는, 시간이 오래 걸릴 수 있는 작업을 비동기적으로 처리하도록 메시지 큐에 넣고, 작업을 처리할 수 있는 프로세스들이 해당 작업을 메시지 큐에서 꺼내어 비동기적으로 처리한다. 따라서 생산자와 소비자 서비스 규모는 각기 독립적으로 확장될 수 있다.

로그, 메트릭 그리고 자동화

  • 로그
    • 에러 로그 모니터링은 중요한 요소. 로그를 단일 서비스로 모아주는 도구를 활용하면 편리하게 검색하고 조회할 수 있다.
  • 메트릭
    • 호스트 단위 메트릭 : CPU, 메모리, 디스크 I/O에 관한 메트릭
    • 종합(aggregated) 메트릭 : 데이터베이스 계층의 성능, 캐시 계층의 성능
    • 핵심 비즈니스 메트릭 : 일별 능동 사용자(daily active user), 수익, 재방문
  • 자동화
    • 시스템이 크고 복잡해지면 생산성을 높이기 위해 자동화 도구를 활용해야 한다.
    • 지속적 통합(continuous integration)을 도와주는 도구를 활용하면, 개발자가 만드는 코드가 어떤 검증 절차를 자동으로 거치도록 할 수 있어서 문제를 쉽게 감지할 수 있다. 이 외에도 빌드, 테스트, 배포 등의 절차를 자동화 할 수 있어 개발 생산성을 향상시킬 수 있다.

데이터베이스의 규모 확장

데이터베이스 확장에는 수직적 규모 확장법수평적 규모 확장법이 존재한다.

  • 수직적 규모 확장법 서버의 Scale-Up과 동일하게, 고성능의 자원(CPU, RAM, 디스크 등)을 추가하는 방식이며, 문제점도 동일하다.

  • 수평적 규모 확장법 데이터베이스의 수평적 확장은 샤딩(sharding)이라고 부르며, 더 많은 데이터베이스 서버를 추가함으로써 성능을 향상할 수 있다.

샤딩은 대규모 데이터베이스를 샤드(shard)라고 부르는 작은 단위로 분할하는 기술을 말한다. 모든 샤드는 같은 스키마를 사용하지만 샤드에 보관되는 데이터 사이에는 중복 데이터가 존재하지 않는다.

샤딩 전략을 구현할 때 가장 중요한 점은, 샤딩 키(sharding key)를 어떻게 정하는지이다. 샤딩 키는 파티션 키(partition key)라고도 부르는데, 데이터가 어떻게 분산될지 정하는 하나 이상의 column을 뜻한다.
당연한 말이지만, 샤딩 키를 정할 때는 데이터를 고르게 분할할 수 있도록 하는 것이 가장 중요하다!

샤딩에서 고려해야 할 문제점은 다음과 같다.

  • 데이터의 재샤딩(resharding)
    • 데이터가 너무 많아져서 하나의 샤드로 감당하기 어려울 때, 샤드 간 데이터 분포가 균등하지 못할 때 샤드 소진(shard exhaustion) 현상이 발생하게 되는데, 샤드 키를 계산하는 로직을 변경하고 데이터를 재배치해야 한다. 안정 해시(consistent hashing) 기법을 활용하면 해당 문제를 해결할 수 있다.
  • 유명인사(celebrity) 문제
    • 특정 샤드에 질의가 집중되어 서버에 과부화가 걸리는 문제이다. 해당 문제를 해결하려면, 유명인사 각가에 샤드 하나씩을 할당하는 방법 등이 존재한다.
  • 조인과 비정규화(join & denormalization)
    • 여러 샤드에 걸친 데이터를 조인하기 어렵다. 이를 해결하는 방법은 데이터베이스를 비정규화하여 하나의 테이블에서 질의가 수행될 수 있도록 한다.

최종 설계

정리

시스템 규모 확장을 위한 기법들을 학습하면서, 핵심은 모든 계층에 다중화를 도입시켜야 한다는 점이었다.

그렇지만, 데이터 동기화. 즉, 데이터 일관성(consistency)을 유지시켜야 한다는 점이 기술적으로 어려워보였다. 추후에 해당 부분을 중점으로 학습하고 포스팅해봐야겠다!

댓글남기기