단일서버
모든 컴포넌트가 한 대의 서버에서 실행. (웹, 앱, 데이터베이스, 캐시 등)
데이터베이스 서버 추가
단일서버에서 사용자가 늘면 웹/모바일 트래픽 처리용 서버와 데이터베이스 서버로 분리한다.
이때 데이터베이스를 관계형 데이터베이스와 비관계형 데이터베이스 중 무엇을 사용할 것인지 요구사항에 따라 선택해야한다.
일반적으로 낮은 응답 지연 시간, 비정형 데이터, 데이터를 직렬화하거나 역직렬화할 수 있기만 하면 되고, 아주 많은 양의 데이터를 저장해야한다면 비관계형 데이터베이스를 선택한다.
수직적 확장 vs 수평적 확장
수직적 확장(scale up) : 서버에 고사양 자원을 추가
수평적 확장(scale out) : 더 많은 서버를 추가
수직적 확장의 한계:
1. 한 대의 서버에 CPU, 메모리를 무한대로 증설할 수는 없음
2. 장애에 대한 자동복구(failover) 방안이나 다중화(redundancy) 방안을 제시하지 않음.
-> 서버가 중단되면 서비스가 중단됨
이러한 단점으로 인해 대규모 어플리케이션에서는 수평적 확장법이 적절함.
수평적 확장을 적용할 때는 부하 분산기, 로드 밸런서를 도입한다.
로드밸런서
로드밸런서는 부하 분산 집합에 속한 웹 서버들에게 트래픽 부하를 고르게 분산하는 역할을 한다.
이때 보안을 위해 서버 간 통신에는 사설 IP 주소가 이용된다. 사설 IP주소는 같은 네트워크에 속한 서버 사이의 통신에만 쓰일 수 있는 IP 주소로 인터넷을 통해서는 접속할 수 없다. 로드밸런서는 웹 서버와 통신하기 위해 이 사설 주소를 사용한다.
만약 서버1이 다운되면 모든 트래픽은 서버1로 전송된다.
트래픽이 증가하면 웹 서버 계층에 더 많은 서버를 추가한다.
그런데 현재는 데이터베이스에는 장애의 자동복구나 다중화를 지원하는 구성은 아니다.
데이터베이스 다중화
쓰기연산(insert, update, delete)은 마스터 데이터베이스에서 하고, 슬레이브는 마스터에게 그 사본을 전달받으며 읽기 연산(select)만 지원한다.
일반적으로 대부분의 트래픽은 읽기이므로 슬레이브의 수를 마스터보다 많게 한다.
데이터베이스를 다중화하면 성능, 안정성, 가용성 향상을 기대할 수 있다.
1. 슬레이브가 한 대 뿐인데 다운된 경우라면, 읽기 연산은 한시적으로 마스터가 한다. 슬레이브가 여러 대라면 하나의 슬레이브가 다운되어도 다른 슬레이브들이 대체할 것이다.
2. 마스터가 다운되면 슬레이브 중 하나가 새로운 마스터가 된다. 그런데 문제는 슬레이브에 저장된 데이터가 최신 상태가 아닐 수 있다는 것이다. 이때는 복구 스크립트를 돌려서 추가해야한다. (이때 다중 마스터, 원형 다중화 방식을 도입하면 이런 상황을 대처할 수 있다.)
로드밸런서, 데이터베이스 다중화가 적용된 아키텍처는 아래와 같다.
사용자 DNS로부터 로드밸런서의 공개 IP 받음 ->
사용자가 IP로 로드밸런서에 요청 ->
HTTP 요청은 서버1,2,3 중 하나로 전달 ->
웹 서버는 사용자의 데이터를 슬레이브에서 read ->
웹 서버는 데이터 변경 연산은 마스터로 전달
이제 캐시, CDN을 적용하여 응답 시간을 개선해보자.
캐시
캐시는 값비싼 연산 결과 또는 자주 참조되는 데이터를 메모리 안에 두고, 뒤이은 요청이 빨리 처리될 수 있도록 하는 저장소.
캐시 계층
캐시 계층은 데이터가 잠시 보관되는 곳으로 데이터베이스보다 훨씬 빠르다. 별도의 캐시 계층을 두면 성능 개선과 데이터베이스 부하 감소의 효과를 누리고 캐시 계층의 규모를 독립적으로 확장시키는 것도 가능.
read through caching 전략(look aside) : 요청을 받으면 웹 서버는 응답이 캐시에 저장되어 있는지 확인하고 만일 저장되어 있다면 해당 데이터를 클라이언트에게 반환하고 없다면 데이터베이스에서 읽어와 저장한 뒤 반환한다.
이 외에도 다양한 캐시 전략이 있는데, 캐시할 데이터의 종류, 크기, 액세스 패턴에 맞는 캐시 전략을 선택하면 된다.
캐시 사용시 유의점
• 캐시는 update는 잘 안하고 read를 자주하는 경우에 사용하는 것이 좋다.
• 캐시는 휘발성이므로 영속적으로 저장할 데이터를 캐시에 담아두는 것은 좋지 않다.
• 캐시에 보관된 데이터의 만료시간을 정하는 정책을 마련하는 것이 좋다. 너무 짧으면 데이터베이스를 자주 읽게 되고, 너무 길면 원본과 차이가 있을 수 있다.
• 일관성(데이터 저장소의 원본과 캐시 내의 사본이 일치하는가)을 보장해야한다. 저장소의 원본을 갱신하는 연산과 캐시를 갱신하는 연산이 다른 경우 일관성이 깨질 수 있다. (Scaling Memcache at Facebook - 좋은 글이 있어 링크를 첨부한다.)
• 캐시 서버를 한대만 둘 경우 캐시 서버가 단일 장애 지점이 될 수 있다.(단일 장애 지점:특정 지점에서의 장애가 시스템의 동작을 중단시킬 수 있는 경우). 이와 같은 상황을 만들지 않으려면 캐시 서버 역시 여러 지역에 분산시켜야한다.
• 캐시 메모리의 크기를 너무 작게 잡으면 액세스 패턴에 따라 데이터가 너무 자주 캐시에서 밀려나(eviction) 캐시의 성능이 떨어진다. 이를 막기 위해서는 캐시 메모리 과할당(overprovision)을 하는 것. 이렇게 하면 캐시에 보관될 데이터가 갑자기 늘어났을 때 생길 문제를 방지할 수 있다.
• 데이터 방출 정책(eviction)을 잡아야한다. 가장 많이 쓰이는 것은 LRU(Least Recently Used)이다. 즉 마지막으로 사용된 시점이 가장 오래된 데이터를 내보내는 것이다. LFU(Least Frequently Used)는 사용된 빈도가 가장 낮은 데이터를 내보내는 것이고, FIFO는 가장 먼저 캐시에 들어온 데이터를 가장 먼저 내보내는 것이다.
콘텐츠 전송 네트워크 (CDN)
CDN은 정적 콘텐츠를 전송하는데 쓰이는, 지리적으로 분산된 서버의 네트워크이다. 즉 이미지, 비디오, CSS, Javascript 파일 등을 캐시할 수 있다. (동적 컨텐츠는 request path, query string, cookie, request header 등의 정보에 기반하여 HTML 페이지를 캐시하는 것.)
CDN 동작과정은 다음과 같다.
사용자가 웹사이트 방문 요청 -> 사용자와 지리적으로 가장 가까운 CDN 서버가 정적 콘텐츠를 전달 -> 근데 만약 정적 콘텐츠가 없다면 서버는 원본 서버에 콘텐츠 데이터 요청 -> 정적 콘텐츠는 지정된 TTL동안 CDN 서버에 캐시된다.
CDN 사용시 고려해야할 점
비용 : 보통 CDN은 제 3사업자에 의해 제공되며, 데이터 전송 양에 따라 비용이 청구된다. 자주 사용하지 않는 데이터를 캐싱하면 필요 이상의 비용이 청구될 수 있다.
적절한 만료 시한 설정 : 시의성이 중요한(time-sensitive) 콘텐츠의 경우 만료 시점을 잘 설정해야한다. 너무 길지도 않고 너무 짧지도 않게 설정해야함. 너무 길면 콘텐츠의 신선도가 떨어지고, 너무 짧으면 원본 서버에 빈번히 접속해야한다.
CDN 장애에 대한 대처 방안 : 예로 만약 CDN 서버가 죽었을 때 클라이언트가 CDN이 아닌 원본 서버로부터 데이터를 요청하는 로직 추가를 고민해봐야한다.
콘텐츠 무효화(invalidation) : 아직 만료되지 않은 콘텐츠라 하더라도 아래 방법 중 하나를 쓰면 CDN에서 제거 가능
- CDN 서비스 사업자가 제공하는 API를 이용하여 콘텐츠 무효화
- 콘텐츠의 다른 버전을 서비스하도록 오브젝트 버저닝(object versioning) 이용. 콘텐츠의 새로운 버전을 지정하기 위해서는 URL 마지막에 버전 번호를 인자로 준다. ex) image.png?v=2
CDN과 캐시가 추가되면 아키텍처는 아래와 같다.
이로써 정적 콘텐츠는 더이상 웹 서버를 통해 서비스하지 않아도 되므로 성능이 향상된다.
캐시가 데이터베이스의 부하를 줄여준다.
무상태(stateless) 웹계층
상태 정보 의존적 아키텍처 :
웹 계층을 수평적으로 확장하기 위해서는 상태 정보(사용자 세션 데이터)를 웹 계층에서 제거해야한다. 일반적으로는 상태 정보를 NoSQL이나 RDBMS 같은 지속성 저장소에 보관하고, 필요할 때 가져오도록 하는 것이다. 이러한 웹 계층을 무상태 웹 계층이라고 한다.
상태 정보를 가지는 서버의 문제점은 특정 클라이언트가 서버1, 2, 3 중 서버1에 접속했을 때, 해당 연결이 클라이언트에 대한 정보를 유지하고 있으므로(세션) 클라이언트는 계속해서 서버1과만 요청과 응답을 받아야 한다는 것이다. 서버2, 3은 클라이언트에 대한 정보가 없으므로 클라이언트의 인증을 다시 요구할 것이다.
이와 같은 문제를 해결하기 위해서는 로드밸런서에 고정 세션이라는 기능을 추가해야하는데, 이는 로드밸런서에게 부담을 준다.
위와 같은 아키텍처는 사용자로부터 HTTP 요청은 어떤 웹 서버로도 전달될 수 있다. 웹 서버는 상태 정보가 필요할 경우 공유 저장소(shared storage)로부터 데이터를 가져온다. 즉 상태 정보는 웹 서버로부터 물리적으로 분리되어 있다. 이러한 구조는 단순하고 안정적이며, 확장이 쉽다. 이때 사용하는 공유 저장소는 RDBMS, NoSQL, Redis 중 무엇이든 가능하다. 아래 그림에서 NoSQL을 사용한 이유는 확장이 간편해서이다.
데이터 센터
아래 그림은 두개의 데이터 센터를 사용하는 경우인데, 사용자는 가장 가까운 데이터 센터로 안내된다. 이러한 절차를 지리적 라우팅(geoDNS-routing)이라고 부른다. geoDNS는 사용자의 위치에 따라 도메인 이름을 어떤 IP 주소로 변환할지 결정할 수 있도록 해주는 DNS 서비스이다.
만약 데이터 센터 중 하나에 심각한 장애가 발생하면 모든 트래픽은 장애가 없는 데이터 센터로 전송된다. 물론 다중 데이터 센터 아키텍처를 구성하기 위해서는 몇가지 기술적 난제를 해결해야한다.
트래픽 우회 : 올바른 데이터 센터로 트래픽을 보내는 효과적인 방법을 찾아야 한다.GeoDNS는 사용자에게 가장 가까운 데이터센터로 트래픽을 보낼 수 있도록 한다.
데이터 동기화 : 데이터 센터마다 별도의 데이터베이스를 사용한다면, 장애가 자동으로 복구(failover)되어 트래픽이 다른 데이터베이스로 우회된다 하더라도 해당 데이터센터에는 찾는 데이터가 없을 수 있다. 이런 상황을 막는 일반적인 방법은 데이터를 여러 데이터 센터에 걸쳐 다중화한 것이다. (넷플릭스가 데이터를 다중화한 방법)
테스트와 배포 : 여러 데이터 센터를 사용하도록 시스템이 구성된 상황이라면 웹 사이트 또는 애플리케이션을 여러 위치에서 테스트해보는 것이 중요하다. 하여 자동화된 배포 도구가 모든 데이터 센터에 동일한 서비스가 설치되도록 하는 데 중요한 역할을 한다.
시스템을 더 큰 규모로 확장하기 위해서는 시스템의 컴포넌트를 분리하여, 각기 독립적으로 확장될 수 있도록 하여야 한다. 메시지 큐는 많은 실제 분산 시스템이 이 문제를 풀기 위해 채용하는 핵심적인 전략이다.
메시지 큐
메시지 큐는 메시지의 무손실(durability, 즉 메시지 큐에 담긴 데이터를 소비자가 꺼낼 때까지 안전히 보관되는 특성)을 보장하는, 비동기 통신을 지원하는 컴포넌트이다. 메시지의 버퍼 역할을 하며, 비동기적으로 전송한다.
메시지 큐의 기본 아키텍처는 매우 간단하다.
생산자(producer/publisher)라고 불리는 입력 서비스가 메시지를 만들어 메시지 큐에 발행(publish)한다. 큐에는 보통 소비자 혹은 구독자(consumer/subscriber)라 불리는 서비스 혹은 서버가 연결되어 있는데, 메시지를 받아 그에 맞는 동작을 수행한다.
메시지 큐를 이용하면 서비스 또는 서버 간 결합이 느슨해져서, 규모 확장성이 보장되어야 하는 안정적 어플리케이션을 구성하기 좋다.
생산자는 소비자 서버가 다운되어 있어도 메시지를 발행할 수 있고, 소비자는 생산자 서비스가 가용한 상태가 아니여도 메시지를 수신할 수 있다.
사용 예로는 이미지 cropping, sharpening, blurring 등을 지원하는 사진 보정 어플리케이션을 만든다고 해보자. 이러한 보정은 시간이 오래 걸릴 수 있는 프로세스이므로 비동기적으로 처리한다면 편리하다.
아래 그림에서 웹서버는 사진 보정 작업을 메시지 큐에 넣는다. 사진 보정 작업 프로세스들은 작업(job)들을 메시지 큐에서 꺼내어 비동기적으로 완료한다. 큐의 크기가 커지면 더 많은 작업 프로세스를 추가해야 처리 시간을 줄일 수 있다. 반대로 큐가 거의 항상 비어있는 상태라면, 작업 프로세스의 수를 줄이면 된다.
로그, 메트릭 그리고 자동화
서비스의 규모가 커지면 로그,메트릭,자동화 같은 도구들이 필수적이다.
로그
에러 로그를 모니터링하는 것은 중요하다. 시스템의 오류, 문제들을 쉽게 찾아낼 수 있기 때문. 로그를 단일 서비스로 모아주는 도구를 활용하면 더 편리하게 검색하고 조회할 수 있다.
메트릭
시스템의 현재 상태를 손쉽게 파악할 수 있다. 메트릭 중 특히 유용한 것들은 아래와 같다.
호스트 단위 메트릭 : CPU, 메모리, 디스크I/O에 관한 메트릭
종합 메트릭 : 데이터베이스 계층의 성능, 캐시 계층 성능 같은 것
핵심 비즈니스 메트릭 : DAU, 수익, 재방문(retention) 같은 것
1. 메시지 큐는 각 컴포넌트가 보다 느슨히 결합될 수 있도록 하고, 결함에 대한 내성을 높인다.
2. 로그, 모니터링, 메트릭, 자동화 등을 지원하기 위한 장치들을 추가하였다.
데이터베이스의 규모 확장
저장할 데이터가 많아지면 데이터베이스에 대한 부하도 증가한다. 데이터데이스의 규모를 확장하는 데에는 두 가지 접근법이 있다. 하나는 수직적 규모 확장법, 다른 하나는 수평적 규모 확장법이다.
수직적 규모 확장(scale up)
기존 서버에 더 많은, 혹은 고성능의 자원(CPU, RAM, 디스크)을 증설하는 방법이다. (실제 아마존은 24TB의 RAM을 갖춘 서버도 제공한다.) 예로 2013년 스택오버플로우는 한 해 동안 방문한 천만 명의 사용자 전부를 단 한 대의 마스터 데이터베이스로 처리하였다. 하지만 이러한 수직적 확장에는 몇가지 심각한 문제가 있다.
1. 하드웨어는 한계가 있으므로 CPU, RAM 등을 무한 증설할 수는 없다. 사용자가 늘어나면 한 대 서버로는 결국 감당하기 어렵게 될 것이다.
2. SPOF(single point of failure)로 인한 위험성이 크다.
3. 비용이 많이 든다.
수평적 확장(sharding)
데이터베이스의 수평적 확장은 샤딩이라고도 불린다. 더 많은 서버를 추가함으로써 성능을 향상시킬 수 있도록 한다.
샤딩은 대규모 데이터베이스를 샤드라고 불리는 작은 단위로 분할하는 기술이다. 모든 샤드는 같은 스키마를 쓰지만 샤드에 보관되는 데이터 사이에는 중복이 없다. 아래 그림은 샤드로 분할된 데이터베이스의 예시다. 사용자 데이터를 어느 샤드에 넣을지는 사용자ID에 따라 정한다. %4를 해시 함수로 사용하여 데이터가 보관되는 샤드를 정한다. 결과가 0이면 0번 샤드에, 1이면 1번 샤드에 보관한다.
샤딩 전략을 구현할 때 가장 중요한 것은 샤딩 키를 어떻게 정하느냐이다. 파티션 키라고도 불리는 샤딩 키는 데이터가 어떻게 분산될지 정하는 하나 이상의 칼럼으로 구성된다. 위 그림의 경우 샤딩 키는 user_id이다. 샤딩 키를 통해 올바른 데이터베이스에 질의를 보내어 데이터 조회나 변경을 처리하므로 효율을 높일 수 있다. 샤딩 키를 정할 때는 데이터를 고르게 분할 할 수 있도록 하는 게 가장 중요하다.
샤딩은 데이터베이스 규모 확장을 실현하는 훌륭한 기술이지만 완벽하진 않다. 샤딩을 도입하면 해결해야할 문제가 생긴다.
데이터의 재샤딩 (resharding) : 재 샤딩은 데이터가 너무 많아져서 하나의 샤드로는 더 이상 감당하기 힘들거나, 샤드 간 데이터 분포가 균등하지 못하여 어떤 샤드에 할당된 공간 소모가 다른 샤드에 비해 빨리 진행될 때(샤드 소진 - shard exhaustion) 필요하다. 이런 현상이 발생하면 샤드 키를 계산하는 함수를 변경하고, 데이터를 재배치해야한다. (5장의 안정해시 기법)
유명인사 문제 : 핫스팟 키 문제라고도 불리는데, 특정 샤드에 질의가 집중되어 서버에 과부하가 걸리는 문제이다. 예로 티모시, 두아 리파같은 유명인사들의 데이터가 전부 같은 샤드에 저장되는 데이터베이스가 있다고 해보자. 이 데이터로 SNS 어플리케이션을 구축하면 결국 해당 샤드에는 read 연산에 의해 과부하가 발생할 것이다. 이런 문제가 발생하면 유명인사의 데이터를 각각의 하나의 샤드로 퍼트려놔야 할 수 있다.
조인과 비정규화 문제 : 일단 하나의 데이터베이스를 여러 샤드 서버로 쪼개고 나면, 여러 샤드에 걸친 데이터를 조인하기가 힘들어진다. 이를 해결하기 위해서는 데이터베이스를 비정규화하여 하나의 테이블에서 질의가 수행될 수 있도록 하는 것이다.
비정규화(=반정규화) : 일반적으로 RDBMS는 정규화를 한다. 정규화를 하면 데이터의 중복을 최소화할 수 있다는 장점이 있지만, join과 같은 연산으로 인해 읽기의 시간이 늘어나는 단점이 있다. 이에 반해 비정규화는 읽는 시간을 최적화하도록 된 데이터베이스이다. 비정규화를 중복을 일정 부분 허용하여 join의 연산을 줄이므로 읽기 작업의 시간을 줄일 수 있다. - 참고:https://owlyr.tistory.com/20
아래 그림은 데이터베이스 샤딩을 적용한 아키텍쳐이다.
아래 아키텍쳐는 관계형 데이터베이스가 요구되지 않는 기능들은 NoSQL로 이전하였다.
지금까지 살펴본 기법들을 정리하면 다음과 같다.
1. 웹 계층을 무상태 계층으로
2. 모든 계층에 다중화 도입
3. 가능한 한 많은 데이터를 캐시
4. 여러 데이터 센터를 지원
5. 정적 콘텐츠는 CDN을 통해 서비스
6. 데이터 계층은 샤딩을 통해 규모를 확장
7. 각 계층은 독립적 서비스로 분할할 것
8. 시스템을 지속적으로 모니터링하고, 자동화 도구들을 활용
모든 내용은 가상 면접 사례로 배우는 대규모 시스템 설계 기초를 참고하였습니다.
'대규모 시스템 설계 기초1' 카테고리의 다른 글
처리율 제한 장치의 설계 (0) | 2024.03.11 |
---|---|
개략적인 규모 추정 (0) | 2024.03.09 |