규모 확장의 단계
서버 한 대에서 시작해 사용자가 늘 때마다 무엇을 덧붙이는지 따라간다. 로드 밸런서, 복제, 캐시, CDN, 무상태 계층, 메시지 큐, 샤딩이 차례로 등장한다.
한 대에서 여러 대로
처음 만든 서비스는 대개 서버 한 대에서 돈다. 웹 서버와 데이터베이스가 같은 기계에 있고, 사용자는 DNS로 그 기계의 IP 주소를 받아 바로 붙는다(DNS는 네트워크 노트). 사용자가 적을 때는 이것이 가장 싸고 단순하다. 사용자가 늘면 어딘가가 먼저 막힌다. CPU일 수도, 디스크일 수도, 데이터베이스 연결 수일 수도 있다. 규모 확장은 막힌 곳을 찾아 그 부분만 늘리거나 떼어 내는 일을 되풀이하는 과정이다.
1. 웹 서버와 DB가 한 대에 있다. 사용자는 DNS로 주소를 얻어 바로 붙는다.
아래 절들은 이 그림의 부품을 하나씩 본다. 순서는 흔히 손대는 차례를 따랐을 뿐 정답이 아니다. 읽기가 많은 서비스는 캐시가 먼저고, 큰 파일을 내려주는 서비스는 CDN이 먼저다. 무엇이 막혔는지는 재 보고 정한다. 재는 법은 마지막 부분의 관측 절에서 다룬다.
수직 확장과 수평 확장
서버를 늘리는 길은 두 가지다. 수직 확장(scale up)은 한 대를 더 큰 기계로 바꾼다. 코드를 고칠 필요가 없어 가장 먼저 쓰는 방법이지만, 살 수 있는 가장 큰 기계가 천장이고 값은 성능보다 빨리 오른다. 무엇보다 그 한 대가 죽으면 서비스 전체가 멈춘다(단일 장애점).
수평 확장(scale out)은 같은 서버를 여러 대 둔다. 대수만큼 처리량이 늘고 한 대가 죽어도 나머지가 받는다. 대신 요청을 나눠 줄 장치(로드 밸런서)가 필요하고, 서버가 저마다 상태를 들고 있으면 안 된다. 웹 서버는 상태를 밖으로 빼기 쉬워 수평 확장이 잘 되고, 데이터베이스는 상태 그 자체라 어렵다. 이 노트의 뒷부분이 대부분 데이터베이스 이야기인 이유다.
여러 대를 둘 때는 한 대가 빠져도 나머지가 피크를 감당하도록 여유를 계산한다(N+1). AWS는 Static stability using Availability Zones에서 한 영역이 통째로 빠져도 남은 영역이 새 서버를 띄우지 않고 버틸 만큼 미리 여유를 두는 설계를 설명한다. 장애가 난 순간에는 새 서버를 띄우는 일 자체가 실패할 수 있기 때문이다.
로드 밸런서
로드 밸런서는 사용자에게 공개 주소 하나만 보여 주고, 들어온 요청을 뒤의 서버들에 나눠 준다. 웹 서버들은 사설 주소만 가져 밖에서 직접 닿지 않는다. 로드 밸런서는 서버마다 주기적으로 상태 확인(health check) 요청을 보내 답이 없는 서버를 목록에서 빼고, 살아나면 다시 넣는다. 그래서 서버를 더하고 빼도, 한 대가 죽어도 사용자는 알아채지 못한다.
AWS의 How Elastic Load Balancing works가 이 동작(상태 확인, 사설 주소로 전달, 여러 영역에 걸친 분산)을 잘 정리해 두었다. 로드 밸런서 자신도 단일 장애점이 되지 않도록 여러 노드로 돌리고, DNS가 그 노드들의 주소를 돌려준다. 요청을 어느 서버에 줄지 고르는 알고리즘과 L4·L7의 차이는 설계의 부품 노트에서 다룬다.
읽기 복제본
웹 계층을 늘리면 다음 병목은 대개 데이터베이스다. 대부분의 서비스는 쓰기보다 읽기가 훨씬 많으므로, 먼저 읽기를 나눈다. 쓰기는 주 DB(primary) 한 대가 모두 받고, 주 DB는 변경 기록을 복제본(replica)들에 보낸다. 읽기는 복제본들이 나눠 받는다. 복제본이 여럿이면 하나가 죽어도 읽기는 계속되고, 주 DB가 죽으면 복제본 하나를 새 주 DB로 올린다.
복제는 보통 비동기라 복제본은 주 DB보다 조금 뒤처진다(복제 지연). 방금 프로필을 고친 사용자가 복제본에서 읽으면 옛 프로필이 보일 수 있다. 그래서 자기가 방금 쓴 데이터는 잠시 주 DB에서 읽는 식의 규칙을 둔다. PostgreSQL에서는 복제본이 읽기 질의를 받는 기능을 Hot Standby라고 부르고, 문서에 복제본에서 읽을 때 생기는 충돌과 지연을 자세히 적어 두었다. 동기·비동기 복제, 장애 조치, 분할 뇌는 데이터베이스의 복제·샤딩·합의 노트에 있다.
캐시
복제본을 늘려도 같은 값을 계속 DB에서 읽는 것은 낭비다. 캐시는 자주 읽는 값을 메모리에 두는 빠른 저장소다(Redis나 Memcached를 많이 쓴다, 키-값 데이터베이스 노트). 메모리 읽기는 디스크를 거치는 DB 질의보다 자릿수가 다르게 빠르다. 가장 흔한 사용법은 캐시 우선 읽기(cache-aside)다. 앱이 먼저 캐시에 묻고, 없으면 DB에서 읽어 캐시에 넣은 뒤 돌려준다.
- TTL(time to live): 캐시에 넣을 때 만료 시간을 붙인다. 지우기를 깜빡해도 TTL이 지나면 새 값을 읽으므로, TTL은 "얼마나 오래된 값까지 참을 수 있나"에 대한 답이다.
- 무효화: 값을 고치면 캐시 항목을 고치기보다 지우는 편이 안전하다. 두 요청이 동시에 캐시를 고치면 늦게 도착한 옛 값이 남을 수 있지만, 지우기는 몇 번 겹쳐도 결과가 같다.
- 메모리가 차면: 캐시는 정해진 메모리만 쓰고, 넘치면 오래 안 쓴 항목(LRU) 같은 기준으로 내보낸다.
- 캐시 서버가 죽으면: 모든 읽기가 한꺼번에 DB로 쏟아진다. 캐시를 여러 대에 나눠 두고, DB가 캐시 없이도 잠깐은 버티는지 확인해 둔다.
캐시에 쓰는 다른 방식(write-through, write-back)과 만료 순간 요청이 몰리는 문제는 설계의 부품 노트에서 이어 간다.
CDN
이미지, 동영상, JS·CSS 파일처럼 모든 사용자에게 같은 파일은 사용자 가까이에 사본을 두면 빠르다. CDN(content delivery network)은 세계 곳곳에 엣지 서버를 두고, 사용자를 가장 가까운 엣지로 보낸다. 엣지에 사본이 있으면 바로 답하고, 없으면 원본 서버(origin)에서 한 번 받아 저장한 뒤 답한다. 원본 서버는 처음 한 번만 요청을 받으니 부하가 크게 준다.
엣지가 사본을 얼마나 들고 있을지는 원본 서버가 응답의 Cache-Control 헤더로 정한다. HTTP 캐시 규칙은 RFC 9111이 정의하고, MDN의 HTTP 캐싱 안내가 읽기 쉽게 풀어 준다. 파일을 고친 뒤 옛 사본이 남는 문제는 보통 파일 이름에 버전이나 내용 해시를 넣어(app.3f9a.js) 아예 다른 주소로 만드는 식으로 피한다. 만료 전에 지우는 기능(invalidation)도 있지만 엣지마다 지우는 데 시간이 걸린다. Amazon CloudFront 문서의 How CloudFront delivers content가 엣지와 원본 사이의 흐름을 보여 준다.
무상태 웹 계층
로그인 정보 같은 세션을 웹 서버 메모리에 두면, 그 사용자의 요청은 늘 같은 서버로 가야 한다(스티키 세션). 그 서버가 죽으면 세션이 사라지고, 사용자가 한 서버에 몰려도 다른 서버로 옮길 수 없다. 서버를 자동으로 늘리고 줄이기도 어렵다.
해법은 상태를 웹 서버 밖으로 빼는 것이다. 세션은 Redis 같은 공유 저장소나 DB에 두고, 웹 서버는 요청마다 거기서 읽는다. 이렇게 아무것도 기억하지 않는 서버를 무상태(stateless)라고 한다. The Twelve-Factor App의 Processes 항목이 이 원칙을 짧게 정리한다. 프로세스는 아무것도 공유하지 않고, 남겨야 할 데이터는 바깥의 저장 서비스에 둔다. 무상태가 되면 로드 밸런서가 아무 서버에나 요청을 줘도 되고, 서버를 부하에 따라 늘리고 줄이는 자동 확장이 쉬워진다.
여러 데이터센터
데이터센터 하나가 통째로 멈추는 일(정전, 네트워크 단절, 자연재해)에도 버티려면 다른 지역에 같은 구성을 하나 더 둔다. 평소에는 DNS가 사용자 위치를 보고 가까운 데이터센터의 주소를 준다. 한 곳이 죽으면 상태 확인에서 이를 알아채고 남은 곳의 주소만 준다(장애 조치, failover).
- DNS의 한계: 클라이언트와 중간 리졸버가 DNS 답을 TTL 동안 들고 있어, 주소를 바꿔도 바로 옮겨 가지 않는다. Google SRE 책의 Load Balancing at the Frontend가 이 한계와, 그래서 DNS 뒤에 한 겹 더 두는 네트워크 로드 밸런서를 설명한다.
- 데이터 동기화: 가장 어려운 부분이다. 데이터센터 사이 왕복은 수십~수백 ms라 보통 비동기로 복제하고, 그러면 장애 순간 아직 건너가지 못한 쓰기를 잃을 수 있다. 어느 쪽이 어떤 데이터의 쓰기를 받을지도 정해야 한다(다중 리더 복제).
- 남은 곳의 여유: 한쪽이 죽으면 남은 쪽이 모든 사용자를 받아야 한다. 평소에 반씩 받는 두 곳이라면 각자 두 배를 감당할 여유가 있어야 한다.
- 배포 도구: 여러 곳의 구성이 어긋나지 않도록 서버 설정과 배포를 자동화하고, 장애 조치를 실제로 연습해 본다.
메시지 큐
사진 크기 바꾸기, 메일 보내기, 동영상 변환처럼 오래 걸리는 일을 요청 안에서 처리하면 사용자가 기다리고 웹 서버가 묶인다. 메시지 큐는 이런 일을 떼어 낸다. 웹 서버(생산자)는 할 일을 큐에 넣고 바로 답하고, 작업 서버(소비자)가 큐에서 꺼내 처리한다. 둘은 서로를 모르고 큐만 알기 때문에 따로 늘리고 줄일 수 있다. 소비자가 잠시 죽어도 일은 큐에 남아 기다린다.
일을 잃지 않으려면 소비자가 처리를 마친 뒤에 확인(ack)을 보내고, 큐는 확인을 받아야 메시지를 지운다. 확인 전에 소비자가 죽으면 메시지는 다른 소비자에게 다시 간다. 그래서 같은 일이 두 번 처리될 수 있고, 작업은 두 번 해도 결과가 같게(멱등) 만든다. RabbitMQ의 Work Queues 튜토리얼이 작업 분배와 확인을 짧은 예제로 보여 준다. 큐 자체를 어떻게 만드는지는 설계 사례: 데이터 파이프라인 노트에서 다룬다.
로그·지표·자동화
서버가 몇 대일 때는 접속해서 로그를 보면 되지만, 수십 대가 되면 어디서 무엇이 잘못됐는지 알 수 없다. 그래서 모든 서버가 로그와 지표를 한곳으로 보내고, 대시보드로 보고, 이상하면 경보가 사람을 부르게 한다. 무엇을 잴지는 Google SRE 책의 Monitoring Distributed Systems 장이 출발점이다. 사용자를 마주하는 시스템이라면 지연 시간, 트래픽, 오류, 포화도 네 가지를 먼저 보라고 권한다.
- 로그: 사건 하나하나의 기록이다. 서버마다 흩어 두지 않고 모아서 검색한다. 요청마다 ID를 붙여 여러 서버를 거친 요청을 한 줄로 이어 본다(분산 추적).
- 지표: 숫자의 시계열이다(초당 요청 수, 지연 시간의 백분위수). 평균은 느린 꼬리를 감추므로 99번째 백분위수(p99)처럼 꼬리를 본다.
- 자동화: 빌드·테스트·배포를 사람 손에서 떼어 내고, 새 버전은 일부 서버에 먼저 내보내 지표를 본 뒤 넓힌다. 서버가 많아질수록 손으로 하는 작업은 실수의 원천이 된다.
샤딩
복제본과 캐시로 읽기는 나눴지만 쓰기는 여전히 주 DB 한 대가 받는다. 쓰기까지 한 대가 감당하지 못하거나 데이터가 한 대에 담기지 않으면, 데이터를 키로 나눠 여러 DB에 둔다. 이를 샤딩(sharding)이라 하고, 나뉜 조각 하나를 샤드라 부른다. 샤드마다 스키마는 같고 담긴 행만 다르다. 각 샤드는 다시 자기 복제본을 가진다.
- 샤딩 키 고르기: 함께 읽는 데이터가 한 샤드에 모이고, 부하가 고르게 퍼지는 키를 고른다. 사용자 ID가 흔한 선택이다.
- 뜨거운 샤드: 유명인 한 명의 글에 요청이 몰리면 그 샤드만 바빠진다. 그런 키만 더 잘게 나누거나 따로 둔다.
- 재분배: 단순한 나머지 방식은 샤드를 하나만 늘려도 대부분의 키가 옮겨 간다. 그래서 일관된 해싱을 쓰거나, 처음부터 칸을 잘게 나눠 두고 칸 단위로 서버를 옮긴다.
- 샤드를 넘는 질의: 여러 샤드에 걸친 조인과 트랜잭션은 어렵거나 느려진다. 그런 질의가 드물도록 키와 데이터 모델을 고른다.
MySQL을 샤딩해 주는 Vitess의 Sharding 문서가 키 범위로 샤드를 정하고 나누는 실제 방식을 보여 준다. 샤딩 방식의 비교와 일관된 해싱의 원리, 재분배 비용은 데이터베이스의 복제·샤딩·합의 노트에 자세히 있다.
- Amazon Builders' Library — AWS
- 가상 면접 사례로 배우는 대규모 시스템 설계 기초 — 인사이트
- Static stability using Availability Zones — AWS Builders' Library
- How Elastic Load Balancing works — AWS
- Hot Standby — PostgreSQL Documentation
- RFC 9111: HTTP Caching — IETF
- HTTP caching — MDN
- How CloudFront delivers content — AWS
- The Twelve-Factor App: Processes — 12factor.net
- Load Balancing at the Frontend — Google SRE Book
- Work Queues tutorial — RabbitMQ
- Sharding — Vitess