1장에서는 사용자가 1명을 지원하는 시스템으로부터 최종적으로는 몇백만의 사용자를 지원하는 시스템으로 설계가 이어진다.
1. 단일 서버
웹 앱, 데이터베이스, 캐시 등 모든 컴포넌트가 한 대의 서버에서 실행된다.
2. 데이터베이스 분리
사용자가 늘면 한 개의 서버로는 처리가 힘들어 웹/모바일의 트래픽 처리 서버와 데이터베이스 서버를 분리하여야 한다.
데이터베이스 고르기
데이터베이스는 크게 관계형 데이터베이스(Relational Database Management System, RDBMS)와 비관계형 데이터베이스(Not Only SQL, NoSQL)이 존재한다.
RDBMS
- RDBMS는 R의 의미인 관계(Relation)의 의미대로 관계형 데이터 모델을 기초로 두고 모든 데이터를 테이블 형태로 표현하는 데이터베이스이다.
-
RDBMS에는 핵심적인 2가지 특징이 있다.
- 데이터는 정해진 데이터 스키마에 따라 테이블에 저장된다.
- 데이터는 관계를 통해 여러 테이블에 분산된다.
- 테이블은 명확하게 정의된 구조가 있어서 스키마 구조에 맞는 데이터만 추가 할 수 있다.
- 관계형 데이터베이스에서 관계는 외래키를 통해 관계를 표현한다.
- MySQL, 오라클, PostgreSQL 등이 있다.
NoSQL
- RDBMS와 다르게 스키마도 없고 관계도 없는 DB이다.
- Join이라는 개념이 존재하지 않는다.
-
다양한 저장 형태가 존재한다.
- Key-Value Database
- Document Database
- Wide Column Database
- Graph Database
- MongoDB, CouchDB, Cassandra, HBase, Amazon DynomoDB 등이 있다.
일반적으로 대부분의 개발자들한테는 대부분 RDMBS가 최선이다. 하지만 구축하는 시스템에 따라 다른 저장소도 고려해봐야한다. 아래의 경우는 NoSQL이 바람직한 선택일 수 있다.
- 아주 낮은 능답 지연시간(latency)이 요구될 경우
- 다루는 데이터가 비정형(unstructured)이라 관계형 데이터가 아닐 경우
- 데이터(JSON, YMAL, XML 등)를 직렬화하거나 역직렬화 할 수 있기만 하면 될 경우
- 아주 많은 양의 데이터를 저장할 필요가 있을 경우 (NoSQL은 Scale-out을 쉽게 할 수 있다는 장점이 있다.)
수직적 규모 확장(Scale up) vs 수평적 규모 확장(Scale out)
- 수직적 규모 확장은 서버에 CPU, 메모리와 같은 고사양 자원을 추가하는 행위를 말한다.
- 수평적 규모 확장은 더 많은 서버를 추가하여 성능 개선하는 행위를 말한다.
서버로 유입되는 트래픽 양이 적을 때는 단순한 방법인 수직적 확장이 좋으나 아래의 단점이 존재한다.
- 한 대의 서버에 CPU나 메모리를 무한정 증설할 방법은 없으며 비용도 기하급수적으로 증가하게 된다.
- 수직적 규모 확장법은 수평적 규모확장과 다르게 장애에 대한 자동복구 방안이나 다중화 방안을 제시하지 않는다.
→ 이러한 문제들이 있어 대규모 애플리케이션을 지원하는 데는 수평적 규모 확장법이 더 적절하다!
3. 로드 밸런서 도입
한 개의 서버를 운영하여 사용자가 웹 서버에 바로 연결되는 구조일 경우, 웹 서버가 다운되거나 너무 많은 사용자가 접속해 서버가 한계에 도달하면 서비스의 응답 속도가 느려지거나 서비스를 이용할 수 없게 된다. 이러한 문제는 로드밸런서(Load Balancer)를 도입하며 해결할 수 있다.
로드밸런서는 부하분산 집합(load balancing set)에 속한 웹 서버들에게 트래픽을 고르게 분산하는 역할을 한다.
사용자는 서비스를 이용하기 위해 로드밸런서의 공개 IP(Public IP)로 접속하면 로드밸런서는 서버간 통신에 사설 IP(Private IP)를 이용하며 요청을 처리하게 된다.
로드밸런서에 연결된 웹 서버를 여러개를 두면 장애를 복구 문제는 해소되고 트래픽의 분산으로 서비스의 가용성 또한 향상된다.
4. 데이터베이스 다중화
로드밸런서의 도입으로 웹 계층에서의 장애의 자동 복구 능력과 가용성이 향상되었다. 데이터베이스 또한 다중화를 통해 문제를 해결할 수 있다.
데이터베이스의 다중화는 보통 서버 사이에 주(Master)-부(Slave)관계를 설정하고 원본은 Master서버에, 사본은 Slave 서버에 저장하는 방식을 운영한다.
이때 쓰기에 대한 요청은 Master에서만 처리하고 Slave 데이터베이스는 Master로부터 전달받은 사본을 통해 Read 연산을 지원한다. 이러한 구조는 이유는 대부분의 애플리케이션은 읽기 연산의 비중이 쓰기 연산보다 높다는 특성을 이용해 Master보다 Slave를 더 만들기 위해 고안되었다.
- 더 나은 성능: Master-Slave 모델에서 모든 데이터의 변경 요청은 Master에서 처리하고 조회는 N대의 Slave에 분산되어 병렬로 처리되게 되어 성능이 좋아진다.
- 안정성(Reliability), 가용성(Availability): 서버의 일부가 동작하지 않더라도 데이터는 보존될 것이다. 데이터를 지역적으로 멀리 떨어진 장소에 다중화를 시켜두면 자연재해로부터도 보존하고 서비스를 지속적으로 제공할 수 있다.
Master-Slave에서 장애가 발생했을 때의 동작은 아래와 같다.
-
Slave 데이터베이스에 장애가 발생할 경우
- Slave 데이터베이스가 1개일 경우 : 새로운 Slave 데이터베이스가 만들어지기 전까지 읽기 연산이 모두 Master로 전달되게 된다.
- Slave 데이터베이스가 여러개일 경우 : 읽기 연산은 나머지 Slave들로 분산되어 처리될 것이다.
-
Master 데이터베이스에 장애가 발생할 경우
- Slave 데이터베이스가 1개일 경우 : Slave가 Master로 승격하고 모든 연산들이 해당 서버에서 수행된다. 그리고 새로운 Slave 서버가 추가되게 된다.
- Slave 데이터베이스가 여러개일 경우 : Slave 중 한 대가 Master로 승격하여 동작한다.
이때 승격한 Slave에 저장된 데이터가 최신 상태가 아닐 수 있다. 이러한 존재하지 않는 데이터는 복구 스크립트(Recovery Script)를 돌려서 추가해야 한다. 다중 마스터(Multi-Masters)나 원형 다중화(Circular Replication)방식을 도입하면 이런 상황에 도움이 될 수 있으나 구성이 훨씬 복잡하다.
5. 캐시, CDN
응답시간(Latency)은 캐시(Cache)를 붙이고정적 콘텐츠를 콘텐츠 전송 네트워크(Content Delivery Network, CDN)으로 옮기면 개선할 수 있다.
5.1. 캐시
캐시는 값비싼 연산 결과 또는 자주 참조되는 데이터를 메모리 안에 두고, 뒤이은 요청이 보다 빨리 처리될 수 있도록 하는 저장소다.
캐시 계층(Cache Tier)은 데이터가 잠시 보관되는 계층으로 데이터베이스보다 훨씬 빠르다. 주로 애플리케이션의 성능은 데이터베이스를 얼마나 자주 호출하느냐에 따라 크게 좌우되는데, 캐시 계층은 이런 문제를 완화해주며 성능 개선의 효과까지 얻을 수 있다. 이러한 캐시 계층은 독립적으로 확장할 수도 있다.
캐시의 사용(동작) 전략으로는 대표적으로 읽기 주도형 캐시 전략이 있다. 이 외에도 많은 전략이 존재하는데 캐시할 데이터의 종류, 크기, 액세스 패턴에 맞는 전략을 택하여 사용하면 된다.
읽기 주도형 캐시 전략(read-through caching strategy)
요청을 받은 웹 서버는 캐시를 확인한다.
- 만약 값이 있다면 해당 데이터를 반환한다.
- 값이 없다면 데이터베이스의 조회를 통해 캐시에 저장한 후 데이터를 반환한다.
캐시 사용 시 유의할 점
- 상황 : 데이터 갱신은 자주 읽어나지 않지만 참조는 빈번하게 일어날 경우 도입을 고려해볼만하다.
- 저장 데이터 : 캐시는 데이터를 휘발성 메모리에 두기에 영속적으로 보관할 데이터를 저장하는 것은 좋지 않다.
- 데이터의 만료 : 만료 정책이 없다면 데이터가 계속 남게 되어 정책을 만드는 것이 좋다. 만료 기간은 너무 길면 원본과 차이가 날 수 있고, 짧으면 데이터베이스의 접근이 많아질 수 있어 적절한 값을 설정해야 한다.
- 일관성 유지 : 데이터베이스의 원본과 캐시의 업데이트를 하나의 트랜잭션으로 처리하지 않는 경우, 일관성을 유지하는 것은 어려운 문제이다. → Scaling Memcache at Facbook 논문을 참고하는 것을 추천한다.
- 장애 대응 : 캐시 서버를 한 대만 둘 경우 SPOF가 될 수 있다. 이를 위해서는 여러 지역에 걸쳐 캐시 서버를 분산시켜야 한다.
- 메모리의 크기 : 메모리가 너무 작으면 데이터가 캐시에서 자주 밀려나 성능이 떨어지게 된다. 이를 막기 위해서는 캐시 메모리를 과할당(Overprovision)하는 방법이 있다.
- 데이터 방출(eviction) 정책 : LRU, LFU, FIFO와 같은 여러 정책 중 경우에 맞는 정책을 도입하면 된다.
5.2. CDN
CDN은 정적 콘텐츠를 전송하는데 쓰이는, 지리적으로 분산된 서버의 네트워크로 이미지, 비디오, CSS, JavaScript 파일 등을 캐시할 수 있다.
CDN은 request path, query string, cookie, request header 등의 정보에 기반하여 HTML 페이지를 캐싱하게 된다.
동작은 사용자가 웹 사이트에 방문하면, 그 사용자로부터 가까운 CDN서버가 정적 콘텐츠를 전달하게 된다. 만약 해당 CDN 서버로부터 원하는 데이터가 없다면 원본(origin) 서버로부터 데이터를 가져와 CDN 서버에 저장한 후, 다시 반환하는 방식으로 동작하게 된다.
원본 서버가 CDN 서버에 데이터를 응답할 때는 Header에 데이터를 캐싱할 수 기간(TTL, Time-To-Live)를 넘겨주어 CDN은 TTL이 끝날 때까지 데이터를 캐시하게 된다.
대표적인 CDN 서비스 사업자로는 클라우드프론트(Cloudfront)와 아카마이(Akamai) CDN이 있다.
CDN 사용 시 고려해야 할 사항
- 비용 : CDN은 제 3의 서비스 사업자로부터 운영되어 데이터 전송양에 따라 요금을 지불하게 되어 자주 사용하지 않는 데이터는 캐싱하지 않는 것을 고려하는 것이 좋다.
- 적절한 만료 시한 설정 : 시간의 변화에 예민한 콘텐츠의 경우 만료 시점을 잘 정해야 한다.
- CDN 장애에 대한 대처 방안 : CDN이 죽었을 때, 서버가 어떻게 동작할지에 대한 예외 처리가 필요하다.
-
콘텐츠 무효화 방법 : 변경되기 이전의 콘텐츠를 반환하지 않고 무조건 새로운 콘텐츠를 반환하려 할 경우, 만료되지 않은 콘텐츠를 무효화 하는 방법을 통해 변경 이전의 콘텐츠를 CDN에서 제거해야 한다.
- CDN 서비스 사업자가 제공하는 API를 통해 콘텐츠 무효화
- 콘텐츠의 다른 버전을 서비스하도록 오브젝트 버저닝(Object Versioning)을 이용하여 새로운 콘텐츠를 읽어오도록 한다. (e.g.
image.png?v=2
)
6. Scale out을 위한 무상태(Stateles) 웹 계층
수평적 확장을 위해서는 사용자 세선 데이터와 같은 상태정보를 웹 계층에서 제거해야 한다.
상태 정보를 저장하는 Stateful한 서버는 같은 클라이언트의 요청은 같은 서버로 전송되어야 하여 로드밸런서를 통한 고정 세션(Sticky session) 기능을 통해 처리할 수 있다. 하지만 이는 로드밸런서의 부담을 주고 웹 계층의 서버를 증설하거나 제거하기도 까다로워진다는 문제와 장애를 처리하기 복잡하다는 문제들이 존재한다.
Stateless한 서버를 만드는 바람직한 전략은 상태 정보를 RDBMS나 NoSQL 또는 Memcached/Redis와 같은 캐시 시스템과 같은 공유 저장소(Shared storage)에 물리적으로 분리하여 보관하고 필요할 때 가져오는 것이다. 이러한 구조를 형성하면 단순하고, 안정적이며, 규모 확장이 쉽다는 장점이 있다.
7. 데이터 센터(Data Center)
전 세계적으로 사용하는 서비스를 운영할 경우, 가용성을 높이고 전 세계 어디서든 쾌적하게 사용할 수 있도록 하기 위해 여러 데이터 센터를 지원해야 한다.
여러 데이터 센터를 운영할 경우, 사용자는 DNS 서비스에 의해 가장 가까운 데이터 센터로부터 요청/응답을 받게 되는데 이를 지리적 라우팅(geoDNS-routing geo-routing)이라고 한다. 여기서 만약 데이터 센터 중 하나에 심각한 장애가 발생하면 모든 트래픽은 장애가 없는 데이터 센터로 전송된다.
다중 데이터 센터 아키텍처를 만들려면 몇 가지 기술적 난제를 해결해야 한다.
- 트래픽 우회 : 올바른 데이터 센터로 트래픽을 보내는 효과적인 방법을 찾아야 한다.
- 데이터 동기화 : 데이터센터마다 별도의 데이터베이스를 운영하고 있다면, 트래픽이 다른 데이터 센터로 우회될 경우, 찾고 있던 데이터가 없을 수 있다. 이런 상황을 예방하는 보편적인 전략은 데이터를 여러 데이터 센터에 걸쳐 다중화하는 것이다. → 넷플릭스의 다중화 방법에 대해 찾아보면 좋다. (Active-Active for Multi-Region Resiliency)
- 테스트와 배포 : 여러 데이터 센터를 사용하도록 시스템이 구성된 상황이면 여러 위치에서 테스트를 해보는 것이 중요하다. CI/CD의 경우 모든 데이터센터에 동일한 서비스가 설치되도록 하는데 중요한 역할을 한다.
8. 시스템 컴포넌트의 분리를 위한 메시지 큐
메시지 큐는 메시지의 무손실(durability)를 보장하며 비동기 통신을 지원하는 컴포넌트이다.
메시지 큐는 메시지의 버퍼 역할을 하며 메시지를 비동기적으로 전송한다.
동작은 생성자(Publisher/Producer)가 메시지를 만들어 발행(Publish)하면 큐에는 보통 소비자(Subscriber/Consumer)가 메시지를 받아 그에 맞는 동작을 수행하는 구조로 동작한다.
메시지 큐를 이용하면 서비스 또는 서버 간 결합이 느슨해져, 규모 확장성이 보장되어야 하는 안정적 애플리케이션을 구성하기 좋다.
- 생산자와 소비자는 서로간 서버 상태에 상관 없이 메시지의 발행과 소모가 가능하다.
- 큐의 대기열에 쌓이는 메시지 양에 따라, 메시지를 처리하는 Worker프로세스(Consumer)의 양을 늘리거나 줄이며 생산자와 소비자 서비스 규모를 독립적으로 관리할 수 있다.
9. 로그, 메트릭 그리고 자동화
대규모 서비스의 경우, 아래의 도구들에 필수적으로 투자해야 한다.
- 로그 : 에러 로그는 시스템의 오류와 문제들을 보다 쉽게 찾아낼 수 있도록 도와주기에 에러 로그를 모니터링하는 것은 중요하다. 에러 로그는 서버 단위로 모니터링 할 수도 있지만 단일 서비스로 모아주는 도구를 활용하면 더욱 편하게 검색하고 조회할 수 있다.
-
메트릭 : 메트릭을 잘 수집하면 사업 현황에 관한 유용한 정보를 얻을 수 있고, 시스템의 현재 상태를 손쉽게 파악할 수도 있다.
- 호스트 단위 메트릭 : CPU, 메모리, 디스크 I/O에 관한 메트릭이 해당된다.
- 종합(aggregated) 메트릭 : 데이터베이스 계층의 성능, 캐시 계층의 성능 같은 것이 해당된다.
- 핵심 비즈니스 메트릭 : 일별 능동 사용자(daily active user), 수익(revenue), 재방문(retention) 같은 것이 해당된다.
- 자동화 : CI/CD 환경을 구축하면 생산성을 크게 향상시킬 수 있다.
10. 데이터베이스 규모의 확장
저장할 데이터가 많아지면 데이터베이스에 대한 부하도 증가하여 증설할 방법을 찾아야 한다. 데이터베이스 규모의 확장도 서버와 같이 수직적 확장과 수평적 확장이 존재한다.
수직적 확장은 앞에서 다뤘듯이 서버의 자원을 고성능의 자원으로 증설하는 방법이다. 하지만 이러한 방법은 SPOF의 위험성, 확장에 대한 한계, 많은 비용 등의 문제들이 존재한다.
데이터베이스의 수평적 확장은 샤딩(shading)이라고 부른다.
샤딩(sharding)
샤딩은 대규모 데이터베이스를 샤드(shard)라고 부르는 작은 단위로 분할하는 기술을 말한다. 모든 샤드는 같은 스키마를 사용하지만 샤드에 보관되는 데이터 사이에는 중복이 없다.
샤딩 전략을 구현할 때 고려해야 할 가장 중요한 것은 샤딩 키(sharding key)를 정하는 방법이다. 샤딩 키는 파티션 키(Partition key)라고도 부르는데 데이터가 어떻게 분산될지 정하는 하나 이상의 컬럼으로 구성된다. 샤딩 키를 통해 올바른 데이터베이스(샤드)에 쿼리를 날려 처리를 하게 된다.
샤딩 키를 정할 때는 데이터를 고르게 분할할 수 있도록 하는 것이 가장 중요하다.
샤딩을 도입하면 시스템이 복잡해지고 아래와 같은 새로운 문제들도 생겨난다.
-
데이터의 재 샤딩(resharding) : 샤드 소진(shard exhaustion)이라고 불리는 아래와 같은 현상이 발생하면 샤드 키를 계산하는 함수를 변경하고 데이터를 재배치해야 한다. → 안정 해시(consistent hashing)기법을 활용하면 이 문제를 해결할 수 있다.
- 데이터가 너무 많아서 하나의 샤드로 감당하기 힘들 때
- 샤드간 데이터 분포가 균등하지 못하여 어떤 샤드에 할당된 공간 소모가 다른 샤드에 비해 빨리 진행될 때
- 유명인사(celebrity) 문제 / 핫스팟 키(hotspot key) 문제 : 저스틴 비버, 레이디 가가와 같은 유명인사 데이터들이 같은 샤드에 위치한다면 조회와 같은 질의들이 특정 샤드에 집중되어 과부하가 걸릴 수 있다. 이러한 문제를 해결하려면 유명인사들을 각각 하나의 샤드를 할당해줄 수도 있고 더 잘게 쪼갤 수도 있다.
- 조인과 비정규화 (Join and de-normalization) : 데이터베이스를 여러 샤드 서버로 쪼개면, 여러 샤드에 걸친 데이터를 조인하기 힘들어진다. 이를 해결하기 위한 방법으로는 데이터베이스를 비정규화하여 하나의 테이블에서 질의가 수행될 수 있도록 하는 방법이 있다.
백만 사용자, 그리고 그 이상
시스템의 규모를 확장하는 것은 지속적이고 반복적인 과정이다.
수백만 사용자 이상을 지원하려면 시스템을 최적화하고 더 작은 단위의 서비스로 분할하는 등의 새로운 전략을 도입하고 지속적으로 시스템을 가다듬어야 할 것이다.