대규모 시스템 설계 (1)

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

I. 단일 시스템부터 차근차근

대체로 대규모 시스템이란 하나의 컴퓨터(서버)보다 여러 대를 연결해 수백만 명의 사용자를 지원하는 분산 시스템을 일컫는다. 세상에는 아무리 큰 시스템도 결국 한 대의 서버에서 시작하고, 여기서 차근차근 서버를 늘려 나가면서 확장해 나가는 구조로 이뤄지기 마련이다.

대개 모놀리식 - MSA의 관계와 헷갈리는 경우가 많은데, 모놀리식과 MSA는 소프트웨어를 어떻게 설계하고 구성하는지에 대한 ‘아키텍처 관점’이고, 단일 및 대규모 시스템은 시스템이 처리하는 트래픽과 데이터의 크게 따른 ‘인프라/규모 관점’의 차이라고 보면 된다. 극단적으로 생각해보면 모놀리식으로 대규모 시스템을 운용할 수도 있는 것이다. (프로그램 한 덩이를 똑같이 복사해 수십, 수백 대 컴퓨터에 띄우는 개념이랄까…)

우선 여기 사용자가 도메인에 접속해 요청-응답 받는 간단한 구조가 있다고 가정해 보자.

  1. 사용자가 도메인 이름을 이용해 웹사이트에 접속한다. (naver.com)
  • ※ 이 때 사용자는 보통 두 가지 단말(웹 앱, 모바일 앱)로부터 요청을 보낸다.
  1. 입력된 도메인 이름은 DNS(도메인 네임 서비스)에 질의해 IP 주소로 변환된다. (15.125.22.21)
  2. 조회 결과로 IP 주소가 반환된다.
  3. 해당 IP 주소로 HTTP(Hypertext Transfer Protocol) 요청이 전달된다.
  4. 요청을 받은 웹 서버는 HTML 페이지나 JSON 형태의 응답을 반환한다.

여기에 사용자가 늘어서 웹/모바일 트래픽을 처리하는 서버(웹 계층)와 데이터베이스 서버(데이터 계층)를 각각 두자고 가정해보자.

1. 데이터베이스 추가

1-1. 관계형 데이터베이스(RDBMS)

  • 자료를 테이블, 열, 칼럼으로 표현.
  • MySQL, Oracle, PostgreSQL 등이 있다.
  • 여러 테이블에 있는 데이터를 관계에 따라 조인(join)하여 사용.

1-2. 비관계형 데이터베이스(NoSQL)

  • 다루는 데이터가 비정형. 관계형이 아니라서 ‘No’ SQL.
  • CouchDB, Neo4j, Cassandra, HBase, Amazon DynamoDB 등이 있다.
  • 저장 방식에 따라 굉장히 많은 갈래로 나눠진다.
    1. key-value store
    2. graph store
    3. column store
    4. document store
  • 일반적으로 조인 연산은 지원하지 않는다.
  • 데이터(JSON, YAML, XML 등)를 직렬화(serialize)하거나 역직렬화(deserialize)할 수 있기만 하면 됨.
  • 아주 많은 양의 데이터를 저장할 필요가 있다.
  • 규모 확장이 간편하다.

1-3. 수직적 규모 확장 vs 수평적 규모 확장

  • 수직적 규모 확장 (= Scale Up): 고사양 자원을 추가하는 행위.
    • 서버에 유입되는 트래픽 양이 급증하는게 아닌 이상 이 방법이 좋다. 심지어 단순하다.
    • 다만 단점도 명확함. 장애 자동복구(failover)나 다중화(redundancy) 방안도 없고, 증설도 한계가 있음.
  • 수평적 규모 확장 (= Scale Out): 더 많은 서버를 추가하는 행위. 양으로 때려박기.

2. 로드밸런서

2-1. 역할

너무 많은 사용자가 접속 → 웹 서버가 한계에 도달 → 응답 속도 느려짐 → 심할 경우 서버 다운 → 패가망신

이러한 상황을 막기 위해 ‘부하 분산 집합’에 속한 웹 서버의 트래픽 부하를 고르게 분산하는 역할을 한다. (웹 서버가 추가될 때마다 집합에 넣어주기만 하면 된다)

2-2. 작동 방식

  • 기존: 사용자가 직접 웹 서버에 접속함
  • 로드밸런서 부착: 사용자는 로드 밸런서의 공개 IP 주소로 접속 → 로드밸런서는 웹 서버와 사설 주소로 통신하므로(사설 IP 주소는 같은 네트워크에 속한 서버 사이 통신에서만 쓰일 수 있다. 인터넷을 통해서는 접속 불가) 자연스레 보안도 강화된다

2-3. 문제 발생시

  • 기존: 싹싹 빌어야 함
  • 로드밸런서 부착: 서버 1이 다운 되어도 모든 트래픽이 서버 2로 전송됨. 트래픽이 급증해도 웹 서버 계층에 서버를 더 추가하기만 하면 됨. 그러면 로드밸런스가 알아서 분산해 줌.

물론 데이터 계층도 신경을 써줘야 한다. 웹 서버만 주구장창 늘어난다고 DB가 멀쩡히 버틸거라 생각하면 안 된다. 이 경우 ‘데이터베이스 다중화’ 기술이 존재한다.

3. 데이터베이스 다중화

요즘은 많은 DBMS가 다중화를 지원한다. 보통 서버 사이에 주 서버(master)-부 서버(slave) 관계를 설정하고, 원본은 주 서버에 사본은 부 서버에 저장하는 방식. DB 변경 명령어(insert, delete, update)와 쓰기 연산은 주 서버로만 보내야 한다. 부 서버는 읽기 연산만 지원. (대부분 읽기 연산 비중이 훨씬 높고, 부 서버 수가 당연히 많아야 한다)

또한 부 서버가 많으면 많아질수록 병렬 처리 가능한 질의(query) 수가 늘어나고, 일부 DB가 손상-파괴되어도 안전하게 보존이 가능하다. 가용성은 덤.

근데 만약 주 서버가 다운되면? 이 경우에는 부 서버 중 하나가 새로운 주 서버가 되고, 다시 부 서버가 추가되는 구조다.

다만 프로덕션(production) 환경에서는 조금 문제가 복잡해진다. 만약 부 서버에 보관된 데이터가 최신이 아니라면, 없는 데이터는 복구 스크립트(recovery script)를 돌려 추가해야 하기 때문. 다중 마스터(multi-masters)나 원형 다중화(circular replication)을 도입하면 도움은 되겠지만 완벽하게 해결은 안 됨. 이건 지금 다룰 내용은 아님.

지금까지 배운 내용을 맨 처음 예시에 추가해 보면, 작동 방식은 이렇게 바뀐다.

  1. 사용자가 도메인 이름을 이용해 웹사이트에 접속한다. (naver.com)
  • ※ 이 때 사용자는 보통 두 가지 단말(웹 앱, 모바일 앱)로부터 요청을 보낸다.
  1. 입력된 도메인 이름은 DNS(도메인 네임 서비스)에 질의해 IP 주소로 변환된다. (15.125.22.21)
  2. 조회 결과로 로드밸런서의 공개 IP 주소가 반환된다.
  3. 해당 IP 주소로 로드밸런서에 접속한다.
  4. HTTP(Hypertext Transfer Protocol) 요청이 서버 1이나 서버 2에 전달된다.
  5. 웹 서버가 사용자의 데이터를 주 DB 서버에 쓰고, 부 DB 서버에서 읽는다.

4. 캐시

캐시 : 자주 참조되는 데이터를 메모리에 둬 DB보다 빨리 처리될 수 있도록 하는 저장소.

응답 시간은 ① 캐시(cache)를 붙이고 ② 정적 콘텐츠를 콘텐츠 전송 네트워크(CDN; Content Delivery Network)로 옮겨 개선이 가능하다. 캐시 계층을 독립적으로 만들면 나중에 확장도 가능하다.

4-1. 캐시 우선 읽기 전략

  1. 웹 서버가 요청을 받는다.
  2. 캐시에 응답이 저장되어 있는지 보고, 만약 있다면 해당 데이터를 반환한다.
  3. 없다면 DB에 질의를 보내 데이터를 찾아 캐시에 저장한 뒤 클라이언트에 반환한다

※ 이 외에도 데이터 종류, 크기, 액세스 패턴에 맞게 여러 전략을 취할 수 있다.

4-2. 유의점

  • 캐시 서버를 왜 쓰는지? → 대부분의 캐시 서버가 프로그래밍 언어로 API를 제공하기 때문
  • 언제 쓰면 좋은지? → 데이터 갱신은 자주 일어나지 않고 참조가 빈번하게 일어날 경우
  • 어떤 데이터를 두면 좋은지? → 휘발적인 데이터 (영속 보관은 바람직하지 않음)
  • 캐시 데이터는 어떻게 만료되는지? → 만료 정책을 적당히 설정
    • 너무 짧으면 DB를 자주 읽게 되고, 너무 길면 원본과 차이가 남
  • 일관성(원본과 사본이 같은지)은 어떻게 유지하는지? → 페이스북 논문(Scaling Memcahe at Facebook)을 참고할 것
  • 장애는 어떻게 대처할지? → 캐시 서버를 한 대만 두면 SPOF(단일 장애점)이 될 가능성 존재. 따라서 여러 지역에 걸쳐 캐시 서버를 분산시켜야 함.
  • 캐시 메모리는 얼마나 크게 잡을지? → 캐시 메모리는 과할당(overprovision)하는 게 나쁘지 않음.
    • 캐시 메모리가 너무 작으면 (액세스 패턴에 따라) 데이터가 캐시에서 밀려나거나(eviction) 갑자기 데이터가 들이닥칠 때 문제가 발생.
      • 데이터 방출 정책(eviction): 캐시가 꽉 찼을 때 데이터가 들어오면 기존 데이터를 내보내는 정책. 가장 널리 쓰이는 건 LRU(Least Recently Used; 마지막 사용 시점이 가장 오래된 데이터부터 내보냄)나 FIFO(First In First Out; 들어온 순서대로 내보냄).

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

정적 콘텐츠를 전송할 때 쓰는 분산 서버 네트워크. 이미지, 비디오, CSS, JavaScript 파일 등을 캐시. (동적 콘텐츠 전송 방법도 있는데 지금은 안 다룰 것)

5-1. 작동 방식

  1. 사용자가 웹사이트 방문
  2. 가장 가까운 CDN 서버가 사용자에게 정적 콘텐츠 전달
  3. 만약 데이터가 CDN에 없으면 원본(origin) 서버에서 가져옴
  4. 원본 서버가 파일을 CDN 서버에 반환 및 저장 (응답의 HTTP 헤더에는 해당 파일이 얼마나 오래 캐시될 수 있는지 적어둔 TTL(Time-To-Live) 값이 동봉됨)
  5. CDN 서버가 사용자에게 콘텐츠 전달

5-2. 특징

  • 보통 제3사업자에 의해 운영.
  • 데이터 전송 양에 따라 요금을 낸다. (자주 사용 안하는 콘텐츠는 캐싱에서 제외할 것)

캐시까지 맨 처음 예시에 추가해 보면, 작동 방식은 이렇게 바뀐다.

  1. 사용자가 도메인 이름을 이용해 웹사이트에 접속한다. (naver.com)
  • ※ 이 때 사용자는 보통 두 가지 단말(웹 앱, 모바일 앱)로부터 요청을 보낸다.
  1. 입력된 도메인 이름은 DNS(도메인 네임 서비스)에 질의해 IP 주소로 변환된다. (15.125.22.21)
  2. 조회 결과로 로드밸런서의 공개 IP 주소가 반환된다.
  3. 해당 IP 주소로 로드밸런서에 접속한다.
  4. HTTP(Hypertext Transfer Protocol) 요청이 서버 1이나 서버 2에 전달된다.
  5. 정적 콘텐츠는 더 이상 웹 서버를 통해 서비스하지 않고, CDN을 통해 제공한다. (DB 부하 ↓)
  6. 나머지 정보는 웹 서버가 사용자의 데이터를 주 DB 서버에 쓰고, 부 DB 서버에서 읽는다.

6. 무상태(stateless) 웹 계층

웹 계층을 수평적으로 확장하기 위해서는 사용자 세션 데이터 같은 ‘상태 정보’를 제거해야 한다. 바람직한 전략은 상태 정보는 지속성 저장소(RDBMS, NoSQL)에 보관하고 필요 시에 가져오는 것.

상태 정보 의존적인 아키텍처는 같은 클라이언트 요청은 항상 같은 서버로만 전송되어야 함. 이를 지원하려면 로드밸런서는 고정 세션(sticky session) 기능을 제공해야 하는데 상당히 부담된다고. 서버 추가나 제거도 어렵다.

무상태 아키텍처는 사용자의 HTTP 요청이 어떤 웹 서버로도 전달 가능하다. 웹 서버는 상태 정보가 필요하면 공유 저장소(shared storage)로부터 데이터를 가져오고, 둘은 물리적으로 분리되어 있다. 즉, 세션 데이터를 웹 계층에서 분리해 지속성 데이터 보관소에 저장하도록 만들 수가 있는 것.

7. 데이터 센터

사용자는 지리적 라우팅(geo-routing)에 따라 가까운 데이터 센터로 안내된다. 지리적 라우팅의 geoDNS는 사용자 위치에 따라 도메인 이름을 어떤 IP 주소로 변환할지 결정해주는 DNS 서비스다. 만약 데이터 센터 중 한 곳에 장애가 발생하면 모든 트래픽은 장애가 없는 데이터 센터로 전송된다.

데이터 센터마다 별도 DB를 사용한다면, 장애가 자동 복구(failover)되어 트래픽이 다른 DB로 우회한다 해도 해당 데이터 센터에 찾는 데이터가 없을 수 있다. (이를 막기 위해 대체로 데이터를 여러 데이터 센터에 걸쳐 다중화한다. 넷플릭스가 이 방식을 취한다고)

8. 메시지 큐

시스템을 더 큰 규모로 확장하려면 컴포넌트를 분리해 독립적으로 확장할 수 있게 해야 하는데, 이 때 메시지 큐가 핵심 전략 중 하나. 메시지 큐는 ‘메시지의 무손실을 보장하는 비동기 통신 지원 컴포넌트’. 메시지 버허 역할을 하며 비동기적으로 전송.

8-1. 메시지 큐의 기본 아키텍처

생산자(producer)와 발행자(publisher)라 불리는 입력 서비스가 메시지 큐에 발행 → 소비자(consumer) 혹은 구독자(subscriber)라 불리는 서비스 혹은 서버가 연결되어 있음 → 메시지를 받아 그에 맞는 동작을 수행

8-2. 장점

메시지 큐 이용시 서비스, 서버 간 결합이 느슨해짐 → 규모 확장성이 보장되어야 하는 안정적 애플리케이션 구성이 좋아짐. (ex: 생산자는 소비자 프로세스가 다운되어 있어도 메시지 발행 가능. 소비자는 생산자 서비스가 가용한 상태가 아니어도 메시지 수신 가능)

9. 로그, 메트릭, 자동화

소규모 웹 사이트를 만들 때는 로그, 메트릭, 자동화 같은 걸 하면 좋다. (필수는 아님) 다만 규모가 커지면 반드시 필수

  • 에러 로그 모니터링: 로그를 단일 서비스로 모아주는 도구 활용시 더 편리하게 검색-조회 가능
  • 메트릭 수집: 사업 현황에 유용한 정보를 얻을 수 있음. 현 상태 파악도 좋고.
    • 호스트 단위 메트릭: GPU, 메모리, 디스크 I/O에 관한 메트릭이 여기 해당
    • 종합 메트릭: DB 계층 성능, 캐시 계층 성능
    • 핵심 비즈니스 메트릭: 일별 능동 사용자, 수익, 재방문 등
  • 자동화: 시스템이 크고 복잡해지면 필수. 코드 검증, 빌드, 테스트, 배포 등.

10. DB의 규모 확장

아까 ‘데이터베이스 추가’ 단에서 배운 스케일 업과 스케일 아웃을 본격적으로 한다고 가정해보자.

10-1. 스케일 업

아마존 AWS의 RDS는 24TB RAM을 갖춘 서버도 상품으로 제공 중. 스택오버플로는 2013년 한 해 동안 방문한 천만 명의 사용자 전부를 단 한 대의 마스터 DB로 처리했다. 다만 여기에는 심각한 약점 존재. 서버 하드웨어 성능에는 한계가 있어 무한 증설이 어렵고, SPOF 위험성이 크다. 그리고 무엇보다 비용 문제가 심각.

10-2. 스케일 아웃

DB의 수평적 확장은 샤딩이라고도 부름. DB를 샤드라고 부르는 작은 단위로 분할하는 기술.

  • 이 때 샤드는 같은 스키마를 쓰지만, 샤드에 보관되는 데이터 사이에는 중복이 없다.

어쨌든 이 전략에 가장 중요한 건 샤딩 키. 파티션 키라고도 부르는데, 데이터가 어떻게 분산될지 정하는 하나 이상의 칼럼. 아래 사례는 샤딩 키 user_id를 4로 나눈 나머지를 통해 데이터 보관 샤드를 정하는 방식.

  • 다만 샤딩도 조심은 해야 함. 데이터 분포가 균등하게 안 이뤄질 수도 있고, 데이터가 너무 많아지면 하나의 샤드로는 감당하기 어려워질수도. 안정 해시(consistence hashing) 기법을 활용해서 이 문제는 해결 가능하다고 한다.
  • 유명인사 문제: 특정 샤드에 질의가 집중되어 서버에 과부하가 걸리는 문제. 운좋게 케이티 페리, 레이디 가가, 저스틴 비버가 같은 샤드에 저장됐다고 가정하면, 이 샤드에 read 연산이 엄청 걸리면서 과부하가 생기는 현산.
  • 조인과 비정규화: 하나의 DB를 여러 샤드 서버로 쪼개면 데이터 조인이 어려워짐. 비정규화를 해서 하나의 테이블로 합쳐야 할지도.

11. 결론

지금까지 알아본, ‘단일 시스템에서 대규모 시스템으로 확장하는 스킬’은 다음과 같다.

  1. 웹 계층은 무상태 계층으로
  2. 모든 계층에 다중화 도입
  3. 가능한 한 많은 데이터를 캐시
  4. 여러 데이터 센터 지원
  5. 정적 콘텐츠는 CDN을 통해 서비스
  6. 데이터 계층은 샤딩을 통해 규모 확장
  7. 각 계층은 독립적 서비스로 분할
  8. 시스템을 지속적으로 모니터링, 자동화 도구 활용

II. 개략적 규모 측정

개략적 규모 측정은 보편적으로 통용되는 성능 수치상 사고 실험을 행하여 추정치를 계산하는 행위로서, 어떤 설계가 요구사항에 부합한지 보기 위한 측정을 말한다. 실제로 많은 면접장에서 상황을 주고, 계산해서 개략적 규모를 측정해보라고 말할 수도 있다.

1. 규모 확정성 표현시 필요한 기본기

1-1. 2의 제곱수

  • 2의 10제곱의 근사치 = 1KB
  • 2의 20제곱의 근사치 = 1MB
  • 2의 30제곱의 근사치 = 1GB
  • 2의 40제곱의 근사치 = 1TB
  • 2의 50제곱의 근사치 = 1PB

1-2. 응답지연(latency)

컴퓨터 연산들의 응답지연값. 이건 하나하나 적기가 너무 많아서 굳이 따로 안 적겠다. 결론적으로는 다음과 같다.

  • 메모리는 빠르다
  • 디스크는 느리다. 디스크 탐색은 가능한 피할 것
  • 단순한 압축 알고리즘은 빠르다
  • 데이터를 인터넷으로 전송하기 전에 가능하면 압축하라
  • 데이터 센터는 보통 여러 지역에 분사되어 있는데, 센터 간 데이터를 주고받는 데는 시간이 걸린다

1-3. 가용성 수치

가용시간이라고 보면 된다. AWS, GCP, Azure 같은 사업자는 99% 이상의 SLA(Service Level Agreement)를 제공한다. 관습적으로 숫자 9를 사용해 표시. 9가 많으면 많을수록 좋다.

1-4. 예를 들어

트위터의 월간 능동 사용자(monthly active user)는 3억(300million) 명이다. 50%의 사용자가 트위터를 매일 사용한다. 평균적으로 각 사용자는 매일 2건의 트윗을 올린다. 미디어를 포함하는 트윗은 10% 정도다. 데이터는 5년간 보관된다.

일간 능동 사용자는 3억 * 50% = 1.5억 QPS = 1.5억 * 2트윗 / 24시간 / 3600초 = 약 3,500 최대 QPS = 2 * QPS = 약 7,000

평균 트윗 크기는 tweet_id에 64바이트, 텍스트에 140바이트, 미디어에 1MB 미디어 저장소 요구량 = 1.5억 * 2 * 10% * 1MB = 30TB/일

즉, 5년간 미디어를 보관하기 위한 저장소 요구량은 30TB * 265 * 5 = 약 55PB


III. 시스템 설계 면접 공략

훌륭한 설계 면접은 정해진 결말도, 정답도 없다. 다만 팁이 될 만한 부분은 분명 있다.

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

시스템 설계 면접을 볼 때는 요구사항을 확실히 이해하고 답을 내놓아야 한다. 면접은 퀴즈 쇼가 아니고, 정답 따위는 없다는 걸 상기하자. 깊이 생각하고 질문해 요구사항과 가정을 분명히 하라. 다음과 같은 질문은 도움이 된다.

  • 구체적으로 어떤 기능을 만들어야 하는지?
  • 제품 사용자 수는 얼마나 되는지?
  • 회사 규모는 얼마나 빨리 커지리라 예상하는지?
  • 회사가 주로 사용하는 기술 스택은 무엇인지?
  • 설계를 단순화하기 위해 활용할 수 있는 기존 서비스로는 어떤 것이 있는지?

2. 개략적 설계안 제시

  • 설계안에 대한 최초 청사진 제시
  • 핵심 컴포넌트를 포함하는 다이어그램을 그려라 (클라이언트, API, 웹 서버, 데이터 저장소, 캐시, CDN, 메시지 큐)
  • 최초 설계안이 제약사항을 만족하는지 개략적으로 계산

3. 해야 할 것

  • 질문을 통해 확인하라(clarification), 스스로 내린 가정이 옳다 믿고 진행하지 말라.
  • 문제의 요구사항을 이해하라.
  • 정답이나 최선의 답안 같은 것은 없다는 점을 명심하라. 스타트업을 위한 설계안과 수백만 사용자를 지원해야 하는 중견 기업을 위한 설계안이 같을리 없다. 요구사항을 정확하게 이해했는지 다시 확인하라.
  • 면접관이 여러분의 사고 흐름을 이해할 수 있도록 하라. 면접관과 소통하라. 가능하다면 여러 해법을 함께 제시하라.
  • 개략적 설계에 면접관이 동의하면, 각 컴포넌트의 세부사항을 설명하기 시작하라. 가장 중요한 컴포넌트부터 진행하라.
  • 면접관의 아이디어를 이끌어 내라. 좋은 면접관은 여러분과 같은 팀원처럼 협력한다.
  • 포기하지 말라.

4. 하지 말아야 할 것

  • 전형적인 면접 문제들에도 대비하지 않은 상태에서 면접장에 가지 말라.
  • 요구사항이나 가정들을 분명히 하지 않은 상태에서 설계를 제시하지 말라.
  • 처음부터 특정 컴포넌트의 세부사항을 너무 깊이 설명하지 말라. 개략적 설계를 마친 뒤에 세부사항으로 나아가라.
  • 진행 중에 막혔다면, 힌트를 청하기를 주저하지 말라.
  • 다시 말하지만, 소통을 주저하지 말라. 침묵 속에 설계를 진행하지 말라.
  • 설계안을 내놓는 순간 면접이 끝난다고 생각하지 말라. 면접관이 끝났다고 말하기 전까지는 끝난 것이 아니다. 의견을 일찍, 그리고 자주 구하라.