개요
한 명의 사용자를 지원하는 시스템에서 시작하여, 최종적으로 몇 백만의 사용자를 지원하는 시스템을 설계해 나아가보자.
단일 서버
한 명의 사용자를 지원하는 시스템을 생각해보면, 우리는 모든 컴포넌트를 단 한 대의 서버에서 실행하는 시스템이면 충분할 것이다.
위 그림에서 사용자의 요청이 처리되는 순서를 부여하면 다음과 같다.
1.
사용자는 도메인 이름(api.mysite.com)을 통해 접속하고,
2.
DNS 서버에서 IP주소로 변환된다.
3.
이렇게 조회한 IP주소를 통해 웹 서버에 HTTP 요청을 보낸다.
4.
웹 서버에서 HTML이나 JSON 형태의 응답을 반환한다.
데이터베이스
사용자가 늘면 그만큼 데이터도 많아지기 때문에, 데이터 저장을 데이터 계층으로 분리하여 웹 서버와 데이터베이스 서버를 각각 독립적으로 확장해 나갈 수 있게 만들어야한다.
이러한 데이터베이스에는 관계형 데이터베이스와 비관계형 데이터베이스가 있다.
관계형 데이터베이스는 관계형 데이터베이스 관리 시스템(RDBMS, Relational Database Mangement System)이라고도 불리며, 자료를 테이블과 열(컬럼)으로 표현하여 행(로우)으로 저장한다. 관계형 데이터베이스로는 MySQL이나 오라클, PostgreSQL 등이 있다.
비관계형 데이터베이스는 NoSQL이라고도 불리는데, 대표적으로는 CouchDB와 Neo4j, Redis, Amazon DynamoDB 등이 있다. NoSQL은 다시 4가지로 분류되는데, key-value 저장소, 그래프 저장소, 컬럼 저장소, 문서 저장소가 있다.
우리는 이런 다양한 데이터베이스 중, 각 특성을 잘 파악해 우리의 시스템에 필요한 데이터베이스를 선택해야한다.
수직적 규모 확장 vs 수평적 규모 확장
수직적 규모 확장은 소위 스케일 업(scale up)이라고도 불리는데, 서버를 기존보다 더 고사양 자원(CPU, 메모리, 디스크 용량 등)으로 바꾸는 행위를 말한다. 반면 스케일 아웃(scale out)이라 불리는 수평적 규모 확장은 비슷한 사양의 자원을 가진 서버를 병렬적으로 늘리는 행위를 말한다.
서버에 유입되는 트래픽의 양이 적을 때는 수직적 확장이 좋은 선택이고 그 단순함이 큰 장점이지만, 다음의 몇 가지 단점이 있다.
•
스케일 업에는 한계가 있다. CPU나 메모리는 무한한게 아니며, 기술적 한계로 인해 무한하게 증가시킬 방법은 없다.
•
수직적 규모 확장에는 자동복구(failover) 방안이나 다중화(redundancy) 방안을 제시할 수 없다. 서버에 장애가 발생하면 서비스 전체가 중단된다.
이러한 단점들 때문에 대규모 애플리케이션을 지원한다면 수평적 규모 확장 방법이 더 적절하다.
로드 밸런서
너무 많은 사용자의 요청이 하나의 웹 서버에 몰리면, 서버는 한계 상황에 도달하게 되고 응답 속도가 느려지거나 서버 접속이 불가능해질 수 있다. 이런 문제를 해결하기 위해 로드 밸런서(load balancer)를 도입한다.
로드 밸런서는 부하 분산 집합(load balancing set)에 속한 웹 서버들에게 트래픽 부하를 골고루 분산하는 역할을 한다.
위 그림에서 클라이언트(사용자)가 로드밸런서의 공개 IP 주소로 접속하면, 로드밸런서가 공개 IP 주소가 아닌 사설 IP 주소를 가진 웹 서버에 요청을 분산한다. 이처럼 공개 IP 주소로 받은 요청을 네트워크에 속한 서버 사이의 통신에만 사용될 수 있는 사설 IP 주소에 전달하는 보안 역할도 맡는다.
또한 이와 같이 분산 환경을 구축하고나면 우리의 시스템은 다음과 같이 동작하게 된다.
•
서버 1이 다운되면 모든 트래픽은 서버 2로 전송된다. 때문에 웹 서버 전체가 다운되는 일이 방지된다.
•
트래픽이 한순간에 가파르게 증가하면 두 대의 서버로 트래픽을 감당하기 어려운 시점이 올 수 있다. 이런 경우 서버 3을 추가하고 로드 밸런서에 등록해주기만 하면 간단하게 해결된다.
이처럼 장애를 자동복구(failover)하지 못하는 문제를 해소할 수 있고 웹 서버의 가용성(availability)은 향상된다.
데이터베이스 다중화
데이터 계층 역시 수평적 확장을 통해 장애의 자동복구와 다중화를 지원할 수 있다.
데이터베이스 다중화는 일반적으로 데이터 원본을 저장하는 주(master) 서버와 복제하여 저장하는 부(slave) 서버 형태로 저장한다. 쓰기 연산은 마스터에서만 지원하고, 복제 데이터베이스는 데이터의 사본만 전달받아 읽기 연산을 지원한다.
대부분의 데이터베이스는 쓰기 연산보다 읽기 연산의 비중이 높기 때문에, 위 그림처럼 통상적으로 부 데이터베이스 수가 주 데이터베이스 수보다 많다.
데이터베이스 다중화를 하면 다음과 같은 이점을 얻을 수 있다.
•
성능 : 읽기 연산과 쓰기 연산으로 분산되어 처리하기 때문에 병렬 처리되는 질의(query)의 수가 늘어나 성능이 좋아진다
•
안정성 : 데이터베이스 서버의 일부가 유실이나 파괴되더라도 데이터가 보존될 수 있다.
•
가용성 : 데이터를 여러 서버에 복제해둠으로써, 하나의 데이터베이스에 장애가 생겨도 다른 데이터베이스를 통해 서비스를 유지할 수 있다.
데이터베이스를 다중화하면 데이터베이스 일부가 장애가 발생하는 경우에 대응이 좋아진다.
부 서버에 장애가 발생한 경우라면, 읽기 연산을 주 서버로 옮겨 장애 서버를 대체한다. 주 서버에 장애가 발생한다면, 부 서버 중 하나를 주 서버로 올리고 복구 스크립트(recovery script)를 수행해 보관된 데이터를 최신화 한다.
우리가 설계 해왔던 내용에 데이터베이스 다중화까지 적용한다면 위와 같이 될 것이다.
캐시
캐시는 비싼 연산의 결과나 자주 참조되는 데이터를 메모리에 두고, 이후의 동일한 요청에 보다 빨리 처리할 수 있도록 하는 저장소를 말한다. 우리가 여태 만들었던 설계에서는 웹 페이지를 새로고침 할 때마다 표시할 데이터를 가져오기 위해 데이터베이스 호출이 발생한다. 데이터베이스 호출은 애플리케이션의 성능에 영향을 미치는데, 캐시를 통해 이런 문제를 완화해 애플리케이션의 성능 문제를 해결할 수 있다.
캐시 계층
캐시 계층(cache tier)은 캐시를 통해 데이터를 잠시 보관하는 계층을 말한다. 캐시 계층을 두어 데이터베이스의 부하를 줄이거나, 캐시의 규모를 독립적으로 확장시키는 것도 가능해진다.
위 그림은 캐시 우선 읽기 전략(read-through caching strategy)에 대한 흐름을 나타낸다. 이외에도 다양한 캐시 전략이 있는데, 캐시의 데이터 종류나 크기, 액세스 패턴에 맞는 캐시 전략을 사용할 수 있다.
캐시 사용 시 유의점
캐시를 사용할 때는 여러 사항들을 고려할 필요가 있다.
•
데이터 갱신과 참조가 얼마나 빈번하게 일어나는가
•
어떤 데이터를 캐시로 둘 것 인가(데이터가 휘발되어도 상관 없는가)
•
캐시의 만료는 어떻게 설정할 것인가
•
데이터 일관성은 어떻게 보장할 것인가
•
장애에는 어떻게 대처할 것인가(단일 장애 포인트 대처 방안)
•
캐시의 전체 용량은 얼마나 크게 잡을 것인가
•
데이터 만료 정책은 어떤 알고리즘을 사용할 것인가
콘텐츠 전송 네트워크(CDN)
CDN은 이미지나 비디오, CSS, Javascript와 같은 정적 콘텐츠를 전송하는데 사용되는, 지리적으로 분산된 서버의 네트워크이다. 요청 경로(request path)와 질의 문자열(query string), 쿠키(cookie), 요청 헤더(request header) 등의 정보에 기반하여 HTML 페이지를 캐싱한다.
CDN의 동작 방식은 사용자가 웹사이트를 방문하면 그 사용자와 가장 가까운 CDN 서버가 정적 콘텐츠를 전달하도록 하여 물리적인 네트워크 시간을 줄이는 것이다.
CDN은 위와 같은 흐름으로 동작하는데, 일종의 캐시 서버로 동작하게 된다.
CDN 사용 시 고려해야 할 사항
CDN을 사용하기 위해서는 몇 가지 고려해야할 사항이 있다.
•
비용 : CDN은 보통 thrid-party provider에 의해 운영되고, 데이터 전송량에 따라 비용을 낸다.
•
만료 기간 : 실시간성(시의성, time-sensitive)이 중요한 콘텐츠의 경우 너무 길면 옛날의 데이터를 보여주게되고, 너무 짧으면 원본 서버에 너무 자주 접속하게 된다.
•
장애 대처 방안 : CDN 서버가 죽었을 경우에 대한 애플리케이션 동작을 고려해야한다. CDN이 응답하지 않으면 서버에서 직접 가져오도록 클라이언트에서 설정해주어야한다.
•
콘텐츠 무효화(invalidation) : 만료되지 않은 콘텐츠더라도 애플리케이션의 상황에 따라 무효화가 필요할 수 있다.
일반적인 콘텐츠 무효화 방법
•
CDN 서비스 사업자가 제공하는 API를 사용
•
콘텐츠의 다른 버전을 서비스하도록 오브젝트 버저닝(object versioning)
캐시와 CDN을 적용한다면 우리의 애플리케이션은 다음과 같은 설계를 가질 것이다.
무상태(stateless) 웹 계층
웹 계층을 수평적으로 확장을 하기 위해서는 서버 내에 저장되던 사용자 세션 데이터 같은 상태 정보를 제거해야한다. 관계형 데이터베이스나 NoSQL을 통해 세션 정보를 완전히 제거한 상태의 웹 계층을 무상태 웹 계층이라 부른다.
상태 정보 의존적인 아키텍처
상태 정보를 보관하는 서버는 클라이언트의 정보를 저장해두었다가 다른 요청에서 해당 정보를 사용한다.
하지만 위의 상황 같이 데이터를 저장한 상황이라면, 사용자 A는 다음 요청이 반드시 서버 1에 전달되어야하고 사용자 B는 서버 2, 사용자 C는 서버 3에 전달되어야만 올바르게 처리될 것이다.
같은 클라이언트로부터의 요청을 반드시 같은 서버에서 처리해야하는 이런 상황은 수평적 확장에서의 큰 문제점이다. 로드 밸런서의 고정 세션(sticky session) 기능을 사용할 수 있지만, 이 역시 로드 밸런서에 부담을 주고 장애 상황에서의 처리도 복잡해진다.
무상태 아키텍처
무상태 아키텍처를 적용하여 이러한 문제를 해결할 수 있다.
서버에 구애 받지 않고 별도의 공유 저장소에 세션을 저장하기 때문에, HTTP 요청은 어떤 웹 서버에 전달되던지 상관 없어지게 된다. 이런 구조는 단순하고 안정적이며, 규모 확장이 쉽다.
우리의 애플리케이션 시스템을 무상태 설계로 변경하면 다음과 같다.
데이터 센터
우리의 애플리케이션이 글로벌 서비스로 빠르게 성장한다면, 전 세계 어디서나 쾌적하게 사용할 수 있도록 하기 위해서 데이터 센터(data center)가 필수적이다.
데이터 센터를 통해 각 요청은 사용자와 가장 가까운 데이터 센터로 안내되는데, 이러한 절차를 지리적 라우팅(geoDNS-routing)이라 부른다. 지리적 라우팅에서 DNS는 도메인 이름을 사용자의 x%는 US-East로, (100-x)%의 사용자는 US-West IP 주소로 바꾸어준다.
만약 데이터 센터 중 하나에서 장애가 발생한다면, 요청은 장애가 없는 다른 데이터 센터로 전송되어 처리된다.
이처럼 데이터 센터를 설계하기 위해서는 몇 가지 기술적 문제를 해결해야한다.
•
트래픽 우회 : 올바른 데이터 센터로 트래픽을 보내는 효과적인 방법을 찾아야 한다.
•
데이터 동기화 : 데이터 센터마다 별도의 데이터베이스를 사용할 수 있기 때문에 데이터를 동기화 할 필요가 있다.
•
테스트와 배포 : 애플리케이션이 각각의 데이터 센터에서 정상 동작을 하는지 확인하기 위해 여러 위치에서 테스트 해볼 필요가 있다.
메세지 큐
시스템을 더 큰 규모로 확장하기 위해서는 시스템의 컴포넌트를 분리하여 각각 독립적으로 확장될 수 있어야 한다. 분산 시스템에서 이러한 문제를 해결하는 중요 전략은 메세지 큐(message queue)를 사용하는 것이다.
메세지 큐는 큐에 보관된 메세지를 소비자가 꺼낼 때까지 안전하게 보관하는 것(무손실, durability)을 보장하는 컴포넌트이다. 메세지를 보내고 받는 버퍼 역할을 하며, 요청이 비동기적으로 처리된다.
메세지 큐의 기본 아키텍처는 생산자 혹은 발행자(producer/publisher)라고 불리는 입력 서비스가 메세지를 발행(publish)하여 메세지 큐에 넣는다. 소비자 혹은 구독자(consumer/subsciber)라 불리는 서비스는 메세지 큐에서 발행된 메세지를 받아 처리한다.
생산자는 소비자 프로세스가 다운되어도 메세지를 발행할 수 있고, 소비자는 생산자가 가용하지 않더라도 메세지를 수신할 수 있다. 이처럼 메세지 큐를 사용하면 서버 간 혹은 서비스 간 결합이 느슨해지기 때문에, 규모 확장성이 보장되어야하는 안정적인 애플리케이션을 구성하기에 좋다.
이미지를 보정하거나 처리하는 작업은 일반적으로 시간이 오래 걸리기 때문에 비동기로 처리하면 편리한데, 메세지 큐를 통해 시스템을 구축하여 각 서비스를 독립적으로 확장할 수 있게 만들 수 있다.
로그, 메트릭 그리고 자동화
애플리케이션이나 사업의 규모가 커지게 되면 로그나 메트릭(metric), 자동화(automation)은 필수적으로 구축되어야한다.
•
로그 : 에러 로그를 모니터링하여, 시스템의 오류와 문제를 보다 쉽게 찾아낼 수 있도록 한다. 서버 단위로 모니터링 할수도 있지만, 로그를 모아주는 도구를 활용하면 더 편리하게 처리할 수 있다.
•
메트릭 : 메트릭을 잘 수집하면, 사업 현황에 도움이 되거나 시스템의 현황을 쉽게 파악할 수 있다.
유용한 메트릭들
•
CPU, 메모리, 디스크 I/O 등 호스트 단위 메트릭
•
데이터베이스 성능, 캐시 성능 등 종합(aggregated) 메트릭
•
DAU, 수익(revenue), 재방문(retention) 등 핵심 비즈니스 메트릭
•
자동화 : 시스템이 크고 복잡해지면 지속적 통합이나 빌드, 테스트, 배포 등의 절차를 자동화하여 개발 생산성을 크게 향상시킬 수 잇다.
우리의 시스템에 메세지 큐와 로그, 메트릭, 자동화를 반영하면 다음과 같다.
데이터베이스의 규모 확장
저장할 데이터가 많아지면 데이터베이스에 대한 부하도 증가하는데, 데이터베이스 역시 규모 확장이 필요해진다. 데이터베이스도 수직적 규모 확장과 수평적 규모 확장을 할 수 있다.
수직적 규모 확장
데이터베이스를 수직적 규모 확장으로 더 고성능의 자원(CPU, RAM, 디스크 용량)으로 증설하는 방법이다.
하지만 데이터베이스 역시 서버 하드웨어의 한계가 있기 때문에 무한히 수직적으로 증설할 수 없고, 단일 장애 포인트가 될 수 있으며, 고성능 서버를 사용할수록 비용이 많이 든다.
수평적 규모 확장
데이터베이스의 수평적 규모 확장 또한 더 많은 서버를 추가해 성능을 향상시키는데, 데이터베이스를 샤드(shard)라 부르는 작은 단위로 분할하는 방법으로 이를 샤딩(sharding)이라고 부른다.
샤딩에서 핵심은 각 샤드에 보관되는 데이터 사이에 중복이 없다는 것이다. 때문에 데이터를 어느 샤드에 보관할 지에 대한 샤딩 전략을 결정해야한다.
샤딩 전략을 구현할 때 가장 중요한 것은 샤딩 키(sharding key)를 어떻게 정하느냐 하는 것이다. 샤딩 키는 파티션 키(patition key)라고도 불리는데, 위는 사용자 ID에 따라 나머지 연산의 결과로 샤딩 방식을 결정하는 방식이다. 적절한 샤딩을 통해 데이터베이스의 처리 효율을 높일 수 있다.
이러한 샤딩을 시스템에 도입하면, 몇가지 문제를 고려할 필요가 있다.
•
데이터 재샤딩(resharding) : 데이터가 너무 많아져 샤드를 추가하거나 샤드 간의 데이터 분포가 고르지 못할 때 재샤딩을 통해 데이터를 재배치해야한다.
•
유명인사(celebrity) 문제 : 핫스팍 키(hotspot key) 문제라고도 불리며, 특정 샤드에 질의가 집중되어 과부하가 걸리는 상황을 말한다. 이런 문제가 발생하면 요청이 고루 분산되도록 재샤딩을 하거나 샤딩 전략을 바꾸어야 한다.
•
조인과 비정규화(join & de-normaliztion) : 하나의 데이터베이스를 여러 샤드로 쪼개고나면 여러 샤드에 걸친 데이터를 조인하기 어려워진다. 이런 조회가 많아지면 데이터베이스 비정규화를 통해 해결하기도 한다.
우리의 시스템에 데이터베이스 샤딩을 적용한 아키텍처는 다음과 같다.