본문은 '가상 면접 사례로 배우는 대규모 시스템 설계 기초’를 읽고 정리한 내용을 기반으로 작성된 글입니다.
[단일 서버]
☑️ 단일 서버 구성에서의 사용자 요청 처리 흐름
- 사용자는 도메인 이름(api.site.com)을 통해 웹사이트에 접속함
- 이 접속을 위해서는 도메인 이름을 DNS(Domain Name Service)에 질의하여 IP주로 변환하는 과정 필요
- DNS는 보통 제 3사업자(third party) ↔ 우리 시스템의 일부는 아님
- DNS 조회 결과로 IP주소 반환 (15.123 …)
- 해당 IP주소로 HTTP(HyperText Transfer Protocol) 요청이 전달됨
- 요청을 받은 웹 서버는 HTML페이지나 JSON형태의 응답 반환
[데이터베이스]
사용자가 늘면 서버 하나로는 충분하지 않아서 여러 서버를 두어야 한다.
- 웹/모바일 트래픽 처리 용도
- 데이터베이스용
☑️ 관계형 데이터베이스(RDBMS)
- 여러 테이블의 데이터를 관계에 따라 조인(join)하여 합칠 수 있음
☑️ 비-관계형 데이터베이스(NoSQL)
대부분의 개발자에게는 관계형이 최선일 것이지만,,
- 아주 낮은 응답 지연시간이 요구되는 경우
- 다루는 데이터가 비정형이라 관계형이 아닌 경우
- 데이터를 직렬화, 역직렬화 할 수 있기만 하면 되는 경우
- 아주 많은 양의 데이터를 저장할 필요가 있는 경우
NoSQL이 바람직한 선택일 수 있다.
[수직적 규모 확장 vs 수평적 규모 확장]
‘스케일 업(scale up)’ ↔ 수직적 규모 확장 : 서버에 고사양 자원(CPU, RAM 등)을 추가하는 행위
‘스케일 아웃(scale out)’ ↔ 수평적 규모 확장 : 더 많은 서버를 추가하여 성능을 개선하는 행위
서버로 유입되는 트래픽 양이 적을 때는 수직적 확장이 좋고 단순하다. 그러나
- 수직적 규모 확장은 한계가 있음 (무한대로 CPU나 메모리 증설 불가)
- 수직적 규모 확장은 장애에 대한 자동복구(failover) 방안이나 다중화(re-dundancy) 방안을 제시하지 않음 → 서버 장애가 발생하면 서비스는 완전히 중단된다.
따라서 대규모 애플리케이션을 지원하는 데는 수평적 규모 확장법이 보다 적절하다.
[로드밸런서]
: 부하 분산 집합(load balancing set)에 속한 웹 서버들에게 트래픽 부하를 고르게 분산하는 역할을 한다.
웹 서버가 다운되거나 사용자 증가로 서버가 한계 상황에 도달 → 웹 서버 다운, 접속 불가 문제를 해결하기 위해 도입하는 것이 최선이다.
- 사용자는 로드밸런서의 공개 IP주소로 접근함
- 웹 서버는 클라이언트의 접속을 직접 처리하지 않음
- 서버 간 통신에는 사설 IP주소가 이용됨
- 사설 IP주소 ↔ 같은 네트워크에 속한 서버 사이의 통신에만 쓰이는 주소
- 서버1이 다운되면 → 모든 트래픽은 서버2로 전송됨
- 웹 서버 계층에 더 많은 서버를 추가하기만 하면 로드밸런스가 자동적으로 트래픽을 분산하기 시작한다.
그렇다면 데이터 계층은??
[데이터베이스 다중화]
: 보통은 서버 사이에 주(master)-부(slave) 관계를 설정하고, 데이터 원본은 주 서버, 사본은 부 서버에 저장하는 방식
- 쓰기 연산은 마스터에서만 지원
- 부 데이터베이스는 읽기 연산만 지원
☑️ 데이터베이스 다중화로 얻는 이득
- 성능 : 병렬로 처리될 수 있는 질의(query)수가 늘어남
- 안정성 : 데이터베이스 서버 일부가 파괴되어도 데이터는 보존됨
- 가용성 : 데이터를 여러 지역에 복제해 둠 → 서버 장애가 생겨도 계속 서비스 가능
☑️ 주 데이터베이스 서버가 다운되면?
- 부 데이터베이스가 한 대
- 해당 부 데이터베이스 서버가 새로운 주 서버가 됨
- 새로운 부 데이터서버가 추가됨
사실 프로덕션 환경에서는 부 데이터서버의 데이터가 최신이 아니기 때문에 훨신 복잡함 ↔ 복구 스크립트 (다중 마스터, 원형 다중화 방식 도입)
☑️ 로드밸런서와 데이터베이스 다중화를 고려한 설계안
- 사용자는 DNS로부터 로드밸런서의 공개 IP주소를 받음
- 사용자는 해당 IP주소를 사용해 로드밸런서에 접속
- HTTP 요청은 서버1, 서버2로 전달됨
- 웹 서버는 사용자의 데이터를 부 데이터베이스 서버에서 읽음
- 웹 서버는 데이터 변경 연산 → 주 데이터베이스로 전달함
이제 응답시간을 개선해보자.
[캐시]
응답시간은 캐시(cache)를 붙이고 정적 콘텐츠를 콘텐츠 전송 네트워크(Content Delivery Network, CDN)로 옮기면 개선할 수 있다.
☑️ 캐시
: 값비싼 연산 결과 또는 자주 참조되는 데이터를 메모리 안에 두고, 뒤이은 요청이 보다 빨리 처리될 수 있도록 하는 저장소
애플리케이션의 성능은 데이터베이스를 얼마나 자주 호출하느냐에 크게 좌우됨 ↔ 캐시는 이 문제를 완화 가능
☑️ 캐시 계층
: 데이터가 잠시 보관되는 곳 → 데이터베이스보다 훨씬 빠르다.
별도의 계층을 둠 → 성능 개선, DB부하 감소, 캐시 계층 규모의 독립적 확장 가능
[읽기 주도형 캐시 전략]
- 요청을 받은 웹 서버 → 캐시에 응답이 저장되어 있는지 확인
- 있다면 해당 데이터를 클라에게 반환
- 없다면 데이터베이스 질의를 통해 데이터를 찾아 캐시에 저장한 뒤 클라에게 반환
읽기 주도형 외에도 다양한 전략이 있음
☑️ 캐시 사용 시 유의할 점
Q : 어떤 상황에 바람직한가?
A : 갱신은 자주 일어나지 않지만 참조는 빈번하게 일어나는 상황
Q : 캐시에 보관된 데이터는 어떻게 만료(expire)되는가?
A : 영속적으로 보관할 데이터 제외
Q : 일관성(consistency)은 어떻게 유지되는가?
A : 트랜잭션 관리, 자동 업데이트 메커니즘 등
Q : 장애에는 어떻게 대처할 것인가?
A : 여러 지역에 걸쳐 캐시 서버 분산하기
Q : 캐시 메모리는 얼마나 크게 잡을 것인가?
A : 과할당하는게 좋다
Q : 데이터 방출(eviction) 정책은 무엇인가?
A : LRU(Least Recently Used), LFU(Least Frequently Used), FIFO(First In First Out) 등등
[콘텐츠 전송 네트워크(CDN)]
: 정적 콘텐츠를 전송하는 데 쓰이는, 지리적으로 분산된 서버의 네트워크 ↔ 이미지, 비디오, CSS, JavaSrcipt 파일 등을 캐시
사용자가 웹사이트 방문 → 그 사용자에게 가장 가까운 CDN 서버가 정적 콘텐츠를 전달
☑️ CDN 동작 과정
- 사용자 A가 이미지 URL을 통해 이미지에 접근함 (URL의 도메인은 CDN 서비스 사업자가 제공)
- CDN 서버의 캐시에 해당 이미지가 없는 경우, 원본 서버에 요청하여 파일을 가져옴
- 원본 서버가 파일을 CDN 서버에 반환하고, 응답의 HTTP 헤더에는 TTL값도 같이 반환
- CDN 서버는 파일을 캐시하고 사용자에게 반환함 → 이미지는 TTL에 명시된 시간이 끝날 때까지 캐시됨
- 사용자 B가 CDN 서버에 요청 → 만료되지 않은 이미지에 대한 요청은 캐시를 통해 처리된다.
☑️ CDN 사용 시 고려해야 할 사항
- 비용 : CDN은 보통 제3 사업자(third-party providers)에 의해 운영되기 때문
- 적절한 만료 시한 설정 : 너무 길면 콘텐츠의 신선도가 떨어지고, 너무 짧으면 원본 서버에 빈번히 접속해야 함
- CDN 장애에 대한 대처 방안 : CDN 자체가 죽었을 경우 동작방안
- 콘텐츠 무효화 : 만료되지 않은 콘텐츠라도 제거할 수 있어야 함
☑️ CDN과 캐시가 추가된 설계
[무상태(stateless) 웹 계층]
웹 계층을 수평적으로 확장하려면 상태 정보(사용자 세션 데이터 같은)를 웹 계층에서 제거해야 한다.
: 상태 정보를 관계형 데이터베이스나 NoSQL같은 지속성 저장소에 보관하는 웹 계층
☑️ 상태 정보 의존적인 아키텍처
: 클라이언트 정보(상태)를 유지하여 요청들 사이에 공유되도록 함
문제점 : 같은 클라이언트로부터의 요청은 항상 같은 서버로 전송되어야 함
☑️ 무상태 아키텍처
- 사용자로부터의 HTTP 요청은 어떤 웹 서버로도 전달될 수 있음
- 상태 정보가 필요한 경우 공유 저장소로부터 데이터를 가져옴
- 상태 정보, 웹 서버가 물리적으로 분리되어 있음 → 단순, 안정적, 규모확장이 쉬움
☑️ 무상태 웹 계층을 갖도록 설계 변경
상태 정보가 웹 서버들로부터 제거됨 → 트래픽 양에 따라 웹 서버를 넣거나 빼기만 하면 자동으로 규모 확장 가능
[데이터 센터]
웹사이트가 매우 빨리 성장하여 전 세계 사용자의 이목을 받는 경우,
가용성을 높이고 전 세계 어디서도 쾌적하게 사용할 수 있도록 하기 위해 여러 데이터 센터를 지원하는 것이 필수다.
☑️ 지리적 라우팅
: 사용자를 가장 가까운 데이터 센터로 안내해주는 절차
☑️ 기술적 난제
- 트래픽 우회 : 올바른 데이터 센터로 트래픽을 보내는 효과적 방법 찾기
- 데이터 동기화 : 장애가 생겼을 때 → 트래픽 우회 → 다른 데이터센터에도 찾는 데이터가 있어야 함
- 테스트와 배포 : 여러 위치에서 테스트하고, 모든 데이터 센터에 동일한 서비스가 설치되도록 배포해야 함
[메시지 큐]
: 메시지의 무손실을 보장하는 비동기 통신을 지원하는 컴포넌트
↔ 메시지 큐에 일단 보관된 메시지는 소비자가 꺼낼 때까지 안전히 보관됨을 보장함
→ 서비스 또는 서버 간 결합이 느슨해져서, 규모 확장성에 용이함
☑️ 메시지 큐의 기본 아키텍쳐
- 생산자 또는 발행자(producer/publisher)가 메시지를 만들어 메시지 큐에 발행함
- 소비자 혹은 구독자(consumer/subscriber)가 메시지를 받아 그에 맞는 동작 수행
생산자/발행자 ↔ 입력 서비스
소비자/구독자 ↔ 서비스, 서버
[로그, 메트릭 그리고 자동화]
: 규모가 커지면 필수적으로 투자해야 하는 도구
[데이터베이스의 규모 확장]
[수직적 확장(scale-up)]
☑️ 수직적 접근법의 심각한 약점
- DB 서버의 CPU, RAM등을 무한 증설할 순 없음 → 결국에는 한 대로는 감당하기 어렵게 됨
- SPOF(Single Point Of Failure)로 인한 위험성이 크다.
- 고성능 서버일수록 비용이 많이 든다.
[수평적 확장]
☑️ 샤딩
: DB를 샤드(shard)라고 부르는 작은 단위로 분할하는 기술 ↔ DB의 수평적 확장
- 모든 샤드는 같은 스키마를 쓰지만, 샤드에 보관되는 데이터 사이에는 중복이 없음
- ex) 4개의 샤드 ↔ (user_id % 4)를 해시 함수로 사용하여 데이터가 보관되는 샤드를 정함
☑️ 샤딩에서 고려할 부분
- 샤딩 키를 어떻게 정하느냐 !!
- 데이터가 어떻게 분산될지 정하는 하나 이상의 칼럼(ex. user_id)
- 데이터를 고르게 분할할 수 있도록 하는 게 중요함
- 데이터의 재 샤딩
- 하나의 샤드로는 감당하기 어려울 때
- 데이터 분포가 균등하지 못할 때
- 유명인사 문제
- ㅋㅋㅋ
- 조인과 비정규화
- 하나의 DB를 여러개로 쪼개면, 여러 샤드에 걸친 데이터를 조인하기 힘들다 → DB를 비정규화하여 하나의 테이블에서 질의가 수행되도록 한다.
- ex) 인덱싱 추가, 요약 테이블 추가
☑️ 샤딩 + 여러 도구들을 적용한 아키텍쳐
정리하면,
- 웹 계층은 무상태 계층으로
- 모든 계층에 다중화 도입
- 가능한 한 많은 데이터 캐시
- 여러 데이터 센터 지원
- 정적 콘텐츠는 CDN을 통해 서비스
- 데이터 계층은 샤딩을 통해 규모 확장
- 각 계층은 독립적 서비스로 분할
- 시스템 지속적 모니터링, 자동화 도구 활용
재밌다~