본문 바로가기

Server

[아키텍처] 사용자 수에 따라 시스템 설계하기

한 명의 사용자를 지원하는 시스템에서 시작하여 몇 백만 사용자를 지원하는 시스템을 설계하자.

단일 서버와 데이터베이스

모든 컴포넌트가 단 한 대의 서버에서 실행되는 간단한 시스템을 설계하면 아래 그림과 같다. 웹 앱, 데이터베이스 캐시 등이 전부 서버 한 대에서 실행된다.

 

단일 서버

 

사용자가 늘면 서버 하나로는 충분하지 않아서 여러 서버를 두어야 한다. 웹과 모바일 트래픽을 처리하는 서버(웹 계층)와 데이터베이스 서버(데이터 계층)을 분리하면 각각을 독립적으로 확장해 나갈 수 있다.

 

데이터베이스 추가

 

관계형 데이터베이스는 자료를 테이블과 열, 칼럼으로 표현한다. SQL을 사용하면 여러 테이블에 있는 데이터를 그 관계에 따라 join하여 합칠 수 있다. 반대로 비 관계형 데이터베이스인 NoSQL은 일반적으로 join 연산은 지원하지 않는다.

 

아주 낮은 응답 지연시간이 요구되거나, 다루는 데이터가 비정형이거나, 데이터(JSON, YAML, XML 등)를 직렬화하거나 역직렬화 할 수 있기만 하면 되거나, 아주 많은 양의 데이터를 저장해야 할 경우 NoSQL를 고려해야 한다.

스케일 업과 스케일 아웃

스케일 업(scale up)이라고 부르는 수직적 규모 확장(vertical scaling) 프로세스는 서버에 고사양 자원을 추가하는 행위를 말한다. 반면 스케일 아웃(scale out)이라고 부르는 수평적 규모 확장 프로세스는 더 많은 서버를 추가하여 성능을 개선하는 행위를 말한다.

 

서버로 유입되는 트래픽의 양이 적을 때는 수직적 확장이 좋은 선택이다. 이 방법의 가장 큰 장점은 단순함이다. 하지만 이 방법은 여러 가지 문제가 있다.

 

수직적 규모 확장에는 한계가 있다. 한 대의 서버에 CPU와 메모리를 무한대로 증설할 수 없다. 그리고 자동복구(failover) 방안이나 다중화(redundancy) 방안을 제시하지 않는다. 서버에 장애 발생하면 웹 사이트와 앱은 완전히 중단된다. 이러한 단점 때문에 대규모 애플리케이션을 지원하는 데는 수평적 규모 확장법이 적당하다.

로드밸런서

 

앞선 설계에서 사용자는 웹 서버에 바로 연결한다. 웹 서버가 다운되거나 너무 많은 사용자가 접속하여 웹 서버가 한계 상황에 도달하는 경우 응답 속도가 느려지거나 서버 접속이 불가능해진다. 이런 문제는 부하 분산기 또는 로드밸런서를 도입한다.

 

로드밸런서는 부하 분산 집합(load balancing set)에 속한 웹 서버들에게 트래픽 부하를 고르게 분산하는 역할을 한다. 웹 서버는 클라이언트의 접속을 직접 처리하지 않고 더 나은 보안을 위해 서버 간 통신에는 사설 IP 주소가 이용된다. 사설 IP 주소는 같은 네트워크에 속한 서버 사이의 통신에만 쓰일 수 있는 주소로 인터넷을 통해 접속할 수 없다. 사용자는 로드밸런서의 공개 IP 주소로 접속하고 로드밸런서는 웹 서버와 통신하기 위해 사설 주소를 이용한다. 

 

로드 밸런서 추가

 

로드밸런서에 또 하나의 웹 서버를 추가하면 장애를 자동복구하지 못하는 문제를 해소하고 웹 계층의 가용성이 향상된다. 이제 웹 계층은 괜찮아 보이지만, 아직 데이터 계층은 안정적이지 못하다.

데이터베이스 다중화

서버 사이에 주(master)와 부(slave) 관계를 설정하고 데이터 원본은 주 서버에 사본은 부 서버에 저장한다. 쓰기 연산(write operation)은 마스터에서만 지원하고 부 테이터베이스는 주 데이터베이스로부터 그 사본을 전달받으며, 읽기 연산(read operation)만 지원한다. 대부분 애플리케이션은 읽기 연산의 비중이 쓰기 연산보다 훨씬 높다. 따라서 부 데이터베이스 수가 주 데이터베이스 수가 더 많다.

 

장점을 살펴보면 다음과 같다. 읽기 연산이 병렬로 처리될 수 있으므로 성능이 좋아진다. 데이터를 지역적으로 떨어진 여러 장소에 다중화 시키므로 데이터베이스 서버 가운데 일부가 파괴되어도 데이터는 보존된다. 그리고 하나의 데이터베이스 서버에 장애가 발생하면 다른 서버에 데이터를 가져와 계속 서비스한다.

 

주 서버가 다운될 경우, 한 대의 부 서버가 새로운 주 서버가 될 것이며 모든 데이터베이스 연산은 일시적으로 새로운 주 서버에서 수행된다. 그리고 부 서버에 보관된 데이터가 최신 상태가 아닐 수 있다. 없는 데이터는 복구 스크립트(recovery script)를 돌려 추가한다.

캐시

캐시는 값비싼 연산 결과 또는 자주 참조되는 데이터를 메모리 안에 두고 뒤이은 요청이 보다 빨리 처리될 수 있도록 하는 저장소다. 애플리케이션의 성능은 데이터베이스를 얼마나 자주 호출하느냐에 크게 좌우되는데, 캐시는 그런 문제를 완화한다.

 

다양한 캐시 전략이 있는데 캐시할 데이터의 종류, 크기, 엑세스 패턴에 맞는 캐시 전략을 선택하면 된다. 그리고 캐시를 사용할 때는 여러 고려 사항이 있다.

 

어떤 데이터를 캐싱해야 할까? 데이터 갱신은 자주 일어나지 않지만 참조는 빈번하게 일어날 경우 캐시를 고려해볼 만하다. 그리고 영속적으로 보관할 데이터를 캐시에 두는 것은 바람직하지 않다.

 

만료(expire) 기간은 어떻게 설정할까? 만료 기간이 너무 짧으면, 데이터베이스를 너무 자주 읽게 되고 너무 길면, 원본가 차이가 난다.

 

일관성(consistency)는 어떻게 유지할까? 데이터 저장소의 원본과 캐시 내의 사본이 같을 경우 일관적이다. 원본을 갱신하는 연산과 캐시를 갱신하는 연산이 단일 트랜잭션으로 처리되지 않는 경우 일관성이 깨진다.

 

장애는 어떻게 대처할까? 캐시 서버를 한 대만 두는 경우 단일 장애 지점(Single Point of Failure, SPOF)가 되어버릴 가능성이 있다. 이를 피하려면 여러 지역에 걸쳐 캐시 서버를 분산시켜야 한다.

 

캐시 메모리의 크기는 어느 정도가 적당한가? 메모리가 너무 작으면 데이터가 너무 자주 캐시에서 밀려나(eviction) 캐시의 성능이 떨어진다. 따라서 캐시 메모리를 과할당(overprovision)한다.

 

데이터 방출(eviction) 정책은 무엇인가? 캐시가 꽉 차버리면 추가로 캐시에 데이터를 넣어야 할 경우 기존 데이터를 내보내야 한다. 캐시 데이터 방출 정책으로 LRU(Least Recently Used), 마지막으로 사용된 시점이 가장 오래된 데이터를 내보내는 정책이 가장 널리 쓰인다. LFU(Least Frequently Used, 사용된 빈도가 가장 낮은 데이터를 내보냄), FIFO(First In First Out, 가장  먼저 캐시에 들어온 데이터를 가장 먼저 내보내는 정책)도 있다.

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

CDN은 정적 콘텐츠를 전송하는 데 쓰이는, 지리적으로 분산된 서버의 네트워크이다. 이미지, 비디오, CSS, javascript 파일 등을 캐시한다. 사용자가 웹 사이트를 방문하면, 그 사용자에게 가장 가까운 CDN 서버가 정적 콘텐츠를 전달하게 된다. 

 

CDN을 사용할 때 비용, 적절한 만료 시한 설정(TTL), CDN 장애에 대한 대처 방안, 콘텐츠 무효화(invalidation) 방법을 고려해야 한다.

 

데이터베이스 다중화, 캐시, CDN

무상태(stateless) 웹 계층

웹 계층을 수평적으로 확장할 때 사용자 세션 데이터와 같은 상태 정보를 웹 계층에서 제거해야 한다. 상태 정보를 보관하는 서버는 같은 클라이언트로부터의 요청은 항상 같은 서버로 전송되어야 한다. 로드밸런서는 이를 지원하기 위해 고정 세션(sticky session)이라는 기능을 제공하고 있다. 이는 로드밸런서에 부담을 준다.

 

반면 무상태 아키텍처는 상태 정보가 웹 서버로부터 물리적으로 분리되어 있기 때문에 HTTP 요청을 어떤 웹 서버로도 전달될 수 있다. 이런 구조는 단순하고 안정적이며 규모 확장이 쉽다.

 

무상태 아키텍처에서 공유 저장소(shared storage)로 NoSQL을 사용하면 규모 확장이 간편해진다. 그리고 트래픽 양에 따라 웹 서버를 자동으로 추가하거나 삭제할 수 있는 자동 규모 확장(autoscaling)이 가능하다.

데이터 센터

가용성을 높이고 전 세계 어디서도 쾌적하게 사용할 수 있도록 하기 위해서는 여러 데이터 센터(data center)를 지원하는 것이 필수다.

다중 데이터 센터 아키텍처를 만들려면 여러 기술적 난제를 해결해야 한다.

 

  • 트래픽 우회. GeoDNS는 사용자에게서 가장 가까운 데이터 센터로 트래픽을 보내야 한다.
  • 데이터 동기화(synchronization). 데이터를 여러 데이터 센터에 걸쳐 다중화한다.
  • 테스트와 배포(deployment). 다중 데이터 센터 아키텍처에서는 웹 사이트 또는 애플리케이션을 여러 위치에서 테스트해보는 것이 중요하다. 그리고 자동화된 배포 도구는 모든 데이터 센터에 동일한 서비스가 설치되도록 하는 데 중요한 역할을 한다.

메시지 큐

시스템을 더 큰 규모로 확장하기 위해서는 시스템의 컴포넌트를 분리하여 각각 독립적으로 확장될 수 있도록 해야한다. 분산 시스템이 이러한 문제를 풀기 위해 메시지 큐(message queue)를 사용한다.

 

메시지 큐는 메시지의 무손실(durablity)을 보장하는 비동기 통신(asynchronous communication)을 지원한다. 메시지의 버퍼 역할을 하며 비동기적으로 전송한다. 메시지 큐를 이용하면 서비스 또는 서버 간 결합이 느슨해져, 생산자와 소비자 서비스의 규모는 각기 독립적으로 확장될 수 있어 안정적 애플리케이션을 구성하기 좋다.

로그, 메트릭 그리고 자동화

로그

시스템의 오류와 문제들을 보다 쉽게 찾아낼 수 있도록 하기 위해 에러 로그를 모니터링하는 것은 중요하다. 로그를 단일 서비스로 모아주는 도구를 활용하면 더 편리하다.

메트릭

메트릭을 잘 수집하면 사업 현황에 관한 유용한 정보를 얻을 수도 있고 시스템의 현재 상태를 손쉽게 파악할 수 있다. 호스트 단위 메트릭, 종합(aggregated) 메트릭, 핵심 비즈니스 메트릭은 메트릭 가운데 유용하다.

자동화

빌드, 테스트, 배포 등의 절차를 자동화하여 개발 생산성을 크게 향상시킬 수 있다.

데이터베이스 규모의 확장

수직적 확장

수직적 접근법에는 몇 가지 심각한 약점이 있다. 사용자가 계속 늘어나면 한 대 서버로는 결국 감당하기 어렵다. 그리고 SPOF로 인한 위험성과 비용이 많이 든다는 약점이 있다.

수평적 확장

데이터베이스의 수평적 확장은 샤딩(sharding)이라고도 부른다. 샤딩은 대규모 데이터베이스를 샤드(shard)라고 부르는 작은 단위로 분할한다. 모든 샤드는 같은 스키마를 쓰지만 샤드에 보관되는 데이터 사이에는 중복이 없다.

 

샤딩 키는 파티션 키(partition key)라고 부르는데, 데이터가 어떻게 분산될지 정하는 하나 이상의 칼럼으로 구성된다. 샤딩 키를 정할 때는 데이터를 고르게 분할할 수 있도록 하는 게 중요하다.

 

샤딩을 도입하면 시스템이 복잡해지고 풀어야 할 문제가 발생한다.

  • 데이터의 재 샤딩(resharding)
    데이터가 많아져 하나의 샤드로는 더 이상 감당하기 어렵거나 샤드 간 데이터 분포가 균등하지 못하여 다른 샤드에 비해 빨리 진행될 때(샤드 소진 shard exhaustion) 샤드 키를 계산하는 함수를 변경하고 데이터를 재배치해야 한다.
  • 유명인사(celebrity) 문제
    핫스팟 키(hotspot key) 문제라고도 불리며 특정 샤드에 질의가 집중되어 서버에 과부하가 걸리는 문제이다.
  • 조인과 비정규화
    여러 샤드에 걸친 데이터를 조인하기가 힘들어 데이터베이스를 비정규화하여 하나의 테이블에서 질의가 수행되도록 한다. 

 

  • 참고
  • 가상 면접 사례로 배우는 대규모 시스템 설계 기초