빌드 날짜: 2026-08-29
참고 도서
| 표제 | 데이터 중심 애플리케이션 설계 |
| 저자 | 마틴 클레프만 |
| ISBN | 979-11-5839-098-3 (93000) |
| 출판사 | 위키북스 |
| 발행일 | 2018.04.12 |
| 표제 | 클라우드 시스템을 관리하는 기술 |
| 저자 | 토머스 리몬첼리, 스트래치 체일럽, 크리스티나 호건 |
| ISBN | 978-89-6848-261-8 [93000] |
| 출판사 | 한빛미디어(주) |
| 발행일 | 2015년 02월 25일 |
| 표제 | 인프라 디자인 패턴 |
| 저자 | 스기하라 타케오 외 4 |
| ISBN | 978-89-94774-88-6 (93000) |
| 출판사 | 비제이퍼블릭 |
| 발행일 | 2015.02.12 |
| 표제 | 클린 아키텍처 |
| 저자 | 로버트 C. 마틴 |
| ISBN | 978-89-6626-247-2 (14000) |
| 출판사 | 인사이트 |
| 발행일 | 2019.08.20 |
| 표제 | 마이크로서비스 도입 이렇게 한다 |
| 저자 | 샘 뉴먼 |
| ISBN | 979-11-89909-25-3 (93000) |
| 출판사 | 책만 |
| 발행일 | 2021.01.20 |
개요
사업 목표
잘 정의된 사업 목표들은 측정 가능하며, 측정치(KPI)를 자동으로 수집하여 현황판(dashboard)으로 만들 수 있어야 한다
이상적인 시스템 구조
- SOA(Service-oriented architecture) : 서비스를 구성하는 각 하위 시스템은 그 자체로도 서비스다
- IaC(Infrastructure as code) : 코드로 작성된 인프라를 이용해 자동으로 프로덕션 환경을 구축한다
SOA best practice
- 모든 API는 동일한 기저 RPC 프로토콜을 사용한다
- 모든 서비스를 동일한 방식으로 모니터링할 수 있다
- 서비스들은 최대한 동일한 기법으로 구현한다; 로드 밸런싱, 코딩 규약 등
- 강결합 시스템을 SOA로 마이그레이션 할 때, 분리하기 쉬운 모듈을 찾기보다는 가장 필요한 부분을 찾아 작업해야 한다
이상적인 릴리스 과정
- 코드를 체크인하면 자동으로 기능 테스트가 실행된다
- 기능 테스트를 통과하면 자동으로 패키지 빌드 -> 테스트 환경 구축 -> 통합 테스트를 진행한다
- 통합 테스트를 통과하면 프로덕션으로 n% 롤아웃한다
- 장애가 검출되지 않으면 점직적으로 100% 롤아웃 전환한다
이상적인 운영
- 덜 자주 발동하는 대응책들에 대해 주기적으로, 자동으로 장애를 일으킨다
- 개발자와 운영자는 별개의 팀이 아니다
운영을 위한 시스템 설계
- 대기열 배출 : 종료 전, 기존 요청 대기열은 모두 처리
- 개별 기능 켜고 끄기 : 임의 시각을 기점으로 기능을 on/off
- 우아한 강등 : 서비스를 제공할 수 없는 경우, 제한된 서비스를 제공하거나, 임시 정적 응답이라도 제공
- 접근 제어, 속도 제한 : 사용자별로 API 접근 허용, 사용 비율 제한을 결정하는 ACL(Access control list)을 관리
- 대형 시스템에서는 개별 모듈 단위로 디버깅 로그를 활성화할 수 있어야 한다
- 시스템 작동 상황 감시
- 보안 및 법규 준수 감사
설계와 아키텍처
- 흔히 아키텍처는 고수준, 설계는 저수준의 구조를 의미하지만 이러한 구분은 무의미하다
둘 사이의 경계는 뚜렷하지 않고, 고수준에서 저수준으로 향하는 의사결정의 연속만 존재한다
- 아키텍처의 목표는 시스템을 만들고 유지보수하는 데 투입되는 인력을 최소화하는 것
좋은 아키텍처는 유스케이스를 그 중심에 두기 때문에, 프레임워크나 도구, 환경에 전혀 구애받지 않고 구조를 기술할 수 있다
설계 수준 구별
- 주로, 기능 요구사항(예. A는 B를 할 수 있다)은 애플리케이션이, 비기능 요구사항(예. B는 3초 내로 완료되어야 한다)은 인프라가 충족한다
- 기능 요구사항은 실무자들이 이미지화하기 쉽지만, 비기능 요구사항은 정확히-충분하게 정의하기 어렵다
일본 IPA에서는 비정기적으로, 비기능 요구사항 정의에 도움이 되는 가이드를 일반에 공개한다 비기능요구 등급 분류
지연 시간
- 지연 시간이 적절한 지 평가하기 위해 p95, p99, p999 등의 분위수를 사용하며, SLA(서비스 수준 계약)의 기준이 된다
- 아마존은 p999를 기준으로 내부 서비스의 응답 시간 요구사항을 기술하는데, 이는 보통 응답 시간이 가장 느린 요청을 경험한 고객들은 많은 구매를 해 많은 데이터를 가진 가장 소중한 고객들이기 때문이다
- 특정 API 응답 지연은 다른 API 엔드포인트로 전파될 수 있다
TCP에서 Head-of-line blocking 문제가 발생하듯, 클라이언트에 뷰를 구성하기 위해 여러 API 응답을 모두 기다려야 하는 상황이 존재하면 특정 1개 API로 인해 전체적으로 느린 경험을 하게 된다
시스템 탄력성을 위해 고려할 점
- 규모가 큰 시스템에서는 발생 확률이 백만분의 1인 장애가 매일 발생한다
따라서 장애가 서비스 중단으로 이어지지 않도록 구성요소를 분리해야 한다
- 같은 공장에서 같은 시기에 만들어진 부품들이 동시에 고장나는 일도 발생할 수 있다
- 로드밸런서 자체도 단일 장애 지점이 될 수 있다
기술 부채
- 기술 부채
개발자가 임시방편을 선택하면 기술은 부채를 지게 된다. 누적된 기술 부채가 너무 커서 제품을 포기해야 하는 상황을 기술 파산이라고 한다
- 기술 부채 요소
코드 부채, 설계 부채, 테스트 부채, 문서 부채
패러다임 개요
- 구조적 프로그래밍 : 제어흐름의 직접적인 전환에 규칙 부과 -- 점프 구문 삭제
- 객체 지향 프로그래밍 : 제어흐름의 간접적인 전환에 규칙 부과 -- 함수 포인터 삭제
다형성을 언어 차원에서 지원함으로써, "고수준 함수1" -> "중간 수준 함수1"의 의존성이 아래와 같이 바뀔 수 있다(의존성 역전; Dependency Inversion)
"고수준 함수1" -> "인터페이스1" <- "중간 수준 함수1"
즉, "고수준 함수1"과 "중간 수준 함수1"은 서로 독립적인 컴포넌트가 되므로, 독립적으로 개발/배포가 가능하다
- 함수형 프로그래밍 : 할당문에 규칙 부과 -- 변수 변경 제거
불변성이 왜 중요한가? 경합, 교착상태, 동시성 문제가 가변 변수로 인해 발생하기 때문!
이벤트 소싱을 이용하면 가변 변수를 줄일 수 있다 -- 최신 상태를 유지하려고 하는 대신, 트랜잭션을 순서대로 저장한다.
=> 상태가 필요해지면, 상태의 시작점으로부터 모든 트랜잭션을 처리한다. 물론 지름길을 택할 수도 있다. 예를 들어 매일 자정에 상태를 계산한 후 저장한다. 그 후 상태가 필요해지면 자정 이후의 트랜잭션만 처리하면 된다
=> 결과적으로 애플리케이션은 CRUD가 아니라 CR만 수행한다
SOLID 설계 원칙
Single Responsibility Principle; 단일 책임 원칙
각 모듈은 변경되어야 할 이유가 단 하나여야 한다
=> 각 모듈은 하나의 액터(사용자 또는 이해관계자)에 대해서만 책임을 져야 한다
=> 그렇지 않다면 모듈을 분리해야 한다
예: 우발적 중복
회계팀에서 사용하는 calculatePay() 메서드와 인사팀에서 사용하는 reportHours() 메서드가 정규 근로 시간을 계산하는 함수 regularHours()를 공유할 때, 인사팀에서 정규 근로 시간 정의를 변경하면 이는 회계팀에도 영향을 미쳐 잘못된 급여가 지불될 가능성이 있다
Open-Closed Principle; 개방-폐쇄 원칙
기존 코드를 수정하기보다, 새로운 코드를 추가하는 방식으로 행위를 변경하도록 설계해야 한다
=> 이를 위해 시스템은 컴포넌트 단위로 분리하고, 저수준 컴포넌트의 변경으로부터 고수준 컴포넌트를 보호할 수 있도록 계층구조가 만들어져야 한다
Liskov Substitution Principle; 리스코프 치환 원칙
상호 대체 가능한 구성요소를 이용해 소프트웨어 시스템을 만들 수 있으려면, 이들 구성요소는 반드시 서로 치환 가능해야 한다
=> 이들 구성요소는 클래스, 인터페이스에 국한되지 않는다. 예를 들어, 특정 REST API 군을 처리하는 서버도 치환 가능한 시스템의 구성요소가 될 수 있다
예. 높이와 너비를 각각 변경할 수 있는 직사각형은, 정사각형의 부모가 될 수 없다
Interface Segregation Principle; 인터페이스 분리 원칙
사용하지 않는 것에 의존하지 않아야 한다 -- 사용하는 것만 인터페이스로 분리하여 넘겨야 한다
Dependency Inversion Principle; 의존성 역전 원칙
고수준 정책을 구현하는 코드는 (자주 변경되는) 저수준 세부사항을 구현하는 코드에 의존해서는 안 된다. 세부사항이 정책에 의존해야 한다
코드 디자인 패턴
- Abstract Factory
서로 관련 있는 여러 객체들에 대한 군집을 생성하는 인터페이스 제공
AbstractFactory : 군집 인스턴스 생성 연산 정의 → 구현 : ConcreteFactory
AbstractProduct : 군집을 구성하는 각 객체 정의 → 구현 : ConcreteProduct - Builder
복잡한 ─ 필드가 많지만, 인스턴스화에 모든 필드가 필요하지는 않은 ─ 객체 생성 로직을 별도 클래스로 분리
- Factory Method(Virtual Constructor)
객체 생성 인터페이스를 정의. 구현을 어떻게 할 지는 각 서브클래스가 결정
- Prototype
견본이 되는 인스턴스를 복사하여 새로운 객체 생성. Prototype은 복제 연산을 가져야 한다
- Singleton
단 하나의 클래스 인스턴스만 존재하며, 이를 공유
- Adapter, Wrapper
클래스의 인터페이스를 기대하는 다른 인터페이스로 변환
- Facade
서브시스템을 사용하기 쉽도록 상위 수준의 인터페이스 정의
- Decorator
동적으로 새로운 기능 추가
- Proxy
다른 객체에 대한 접근을 제어하는 대리자
- Chain of Responsibility
요청을 자신이 처리해야 한다면 처리, 아니라면 후속 처리자에게 전달
- Iterator
내부를 노출하지 않고 원소를 차례대로 접근하는 방법 제공
- Mediator
내부 객체들이 서로를 직접 참조하지 않고, Mediator를 통해 교류
- Observer
감시하는 객체의 상태가 변하면 의존하는 객체들이 통지받도록 한다
- State
객체 내부 상태에 따라 행동을 변경
- Template Method
부모 클래스(abstract)는 공통 메서드를 정의하고, 각 leaf 클래스는 제각각의 구현을 갖는다
- Strategy
동일한 계열의 알고리즘들을 캡슐화하여 상호 교환할 수 있도록 한다
- Scott Meyers
All non-leaf classes should be abstract
코드 리팩터링
- 자주 쓰이지 않는 코드의 성능 향상에 얽매이지 말 것
- 주석을 적는 대신 주석이 설명하는 부분을 메서드로 추출
- Extract Method
코드들을 그룹으로 묶어도 좋겠다고 판단된다면, 목적이 드러나는 직관적인 이름의 메서드로 추출
- Split Temporary Variable
루프 변수나 누적용 임시 변수가 아닌 경우, 각 대입마다 새로운 임시변수를 사용하여 되도록 final 임시변수만 이용
- Remove Assignments to Parameters
매개변수는 되도록 final이 되도록 한다
- Replace Method with Method Object
지역변수로 메서드 추출이 어려운 아주 긴 메서드가 있을 때, 지역변수를 필드로 하는 객체를 만들고 메서드를 잘게 쪼갠다
- Move Method
메서드가 다른 클래스의 기능을 더 많이 이용할 때, 해당 클래스에 비슷한 내용의 메서드를 작성. 기존 메서드는 대리자로 변경하거나 삭제
- Extract Class
클래스가 2가지 이상의 책임을 가진 경우
- Pull Up Field/Method/Constructor Body
두 하위클래스의 같은 필드/메서드/생성자 코드는 상위클래스로. 반대로 특정 서브클래스에서만 이용하는 것은 하위클래스로
- Replace Inheritance with Delegation
상위클래스의 일부만 이용하는 경우, 상위클래스 인스턴스를 필드로 갖고 메서드에서는 호출을 위임. 반대로, 클래스 전반에 걸쳐 위임이 도배되는 경우엔 차라리 상속으로 전환
시스템 규모 확장
- 모든 시스템에는 필연적으로 병목 또는 제약이 있다. 하지만 이는 시스템이 목표를 달성하는 능력에 해가 될 때만 문제점이 된다
너무 빨리 바로잡는다면, 절대 나타나지 않을 문제에 노력을 낭비하는 것일 수 있다
- AKF scaling cube
- x축 > 수평 중복 > 서비스를 복제하여 처리량을 늘린다
트랜잭션이 독립적으로 처리될 수 있다면, 성능 향상은 복제본 개수에 비례한다. CPU, RAM 증설도 이에 포함된다
- y축 > 기능 또는 서비스 분할 > 시스템의 결합성을 낮춘다 or 요청 종류에 따라 핸들러를 분할한다
- 전자의 예 : 단일 컴퓨터로 웹 서버, DB 서버 등을 모두 구동하던 상황에서 각각에 개별 머신을 할당
- 후자의 예 : 중요한 요청에 대해 전용 컴퓨팅 풀로 처리, 검색엔진 요청은 캐시 없는 고지연 머신으로 처리
- z축 > lookup-oriented split > 자료를 segment로 분할하고, 각 세그먼트에 전용 자원을 할당
- x축 > 수평 중복 > 서비스를 복제하여 처리량을 늘린다
마이크로서비스
개요
- 마이크로서비스 아키텍처는 서비스를 모듈의 단위로 한다
- 서비스 간 통신은 API를 통해서만 이루어진다 -- 시간이 지나도 모듈성을 유지하기 쉽다
단순히 단일 서버 번들을 각 서비스 엔드포인트별로 따로 배포하는 것만으로는 마이크로서비스 아키텍처를 충족했다고 할 수 없다
- 서비스마다 DB를 따로 갖는다
- DBMS까지 분해할 필요는 없다 -- 여기서의 DB는 스키마(격리된 테이블 집합)를 의미한다
예. 큼지막한 주문 테이블 하나를 공유하는 대신, 키를 공유하는 각자의 주문 테이블을 갖는다
- 한편, 모놀리스를 분해할 때 DB는 코드 분리가 완료된 후 나중에 해도 된다
- DBMS까지 분해할 필요는 없다 -- 여기서의 DB는 스키마(격리된 테이블 집합)를 의미한다
CQRS(Command and Query Responsibility Segregation)
- 커맨드(삽입/갱신/삭제)와 쿼리(조회)를 처리하는 책임을 분리한다
예를 들어 RDBMS로 커맨드를 처리하고, 일래스틱서치와 같은 검색 전용 DB를 이용할 수 있다
- 커맨드 DB의 변경사항을 쿼리 DB가 구독하여 공용 뷰를 최신화한다
- 여러 서비스들을 개별 API들로 조회하는 비용이 저렴하다면 CQRS는 필요하지 않을 수 있다
마이크로서비스가 적합하지 않은 경우
- 도메인에 익숙하지 않은 경우, 도메인 경계를 잘못 설정할 위험이 있다
이런 경우, 모놀리스로 개발한 후 분해하는 것이 처음부터 마이크로서비스를 시도하는 것보다 수월하다
- 스타트업
- 제품의 시장성이 확인되지 않았다면 도메인 변경이 일어나기 쉽다
- 또한 초기 스타트업이라면 조직 자체가 소규모라 의사소통 비용도 작을 것이다
- 이러한 경우, 마이크로서비스는 제품의 수익서을 높이기 위한 확장을 위한 좋은 선택이 될 수는 있을 것이다
- 고객에게 소프트웨어 패키지를 납품해야 하는 경우
서비스는 적을수록 좋다
- 마이크로서비스는 결코 서비스가 많을수록 좋다는 의견을 지지하지 않는다
- 서비스를 분리하는 경우의 예 : 후보 서비스들 중 분해하기 쉬우면서 분해하는 경우 이익이 큰 것부터 분해한다
- 서비스를 병합하는 경우의 예 : 두 서비스 사이에 지나치게 많은 호출이 존재한다
서비스 분리 과도기에서의 호출 패턴
- (클라이언트) 변경 없이 (모놀리스)의 호출을 모두 (신규 서비스)가 처리
- (클라이언트)의 API 엔드포인트 변경으로 (모놀리스) 호출을 모두 (신규 서비스)가 처리
- (모놀리스)와 (신규 서비스) 모두가 요청을 처리 <- DB 분리가 되지 않은 상태에서, (신규 서비스)가 잘 작동하는지 테스트
- (모놀리스)에서 (신규 서비스)를 호출하여 요청 처리
- (신규 서비스)에서 (모놀리스)를 호출하여 요청 처리
CAP 정리와 PACELC 정리
참고 문헌 : Proving PACELC
CAP 정리 또는 브루어(Brewer)의 정리는 분산 시스템이 다음 3가지를 모두 보장할 수 없다는 것을 명시한다
- 일관성(Consistency)
잘못된 응답을 생성하지 않는다 -- 모든 읽기 요청은 어느 노드에서 처리됐는지와 무관하게 가장 최근 쓰기 결과나 오류를 응답 받는다
- 가용성(Availability)
모든 요청은 응답받는다 -- 노드 자체에 장애가 발생하지 않는 한, 노드는 수신한 요청에 대해 응답을 반환한다
- 파티션 내성(Partition Tolerance)
노드 간 통신이 실패해도 시스템은 계속 운영된다
분산 시스템은 필연적으로 여러 노드들이 서로 통신하며 운영되는데, 광역 네트워크 장애로 인해 시스템이 내부적으로는 통신이 가능하지만 서로 간에는 통신이 불가능한 파티션 현상이 발생하는 경우, 일관성과 가용성을 모두 만족시킬 수는 없다는 의미다.
어떤 시스템을 AP, CP, CA 중 하나로 분류할 수 있다는 오해를 불러 일으키는 점, 파티션이 발생하지 않은 경우에 대한 설명이 부족한 점을 보충하는 PACELC 정리가 이후 등장했다.
- 네트워크 파티션이 발생하지 않은 경우 : 시스템은 지연 시간과 일관성 중 어떤 것을 우선할 것인지 선택해야 한다
예를 들어, 모든 노드에 트랜잭션이 전파된 뒤 응답할 것인지? 일부에만 전파되어도 응답할 것인지?
- 네트워크 파티션이 발생한 경우 : 시스템은 일관성과 가용성 중 어떤 것을 우선할 것인지 선택해야 한다
예를 들어, 파티션 그룹들이 모두 요청을 처리하고 각각 그룹 내부적으로만 트랜잭션을 전파한 뒤 응답할 것인지? 파티션 그룹들 중 하나만 요청을 처리하도록 강제할 것인지?
구체적으로는
- PA/EL : 항상 일관성을 경시한다
- PA/EC : 정상 상태에서는 일관성을, 파티션 상태에서는 가용성을 우선한다
- PC/EL : 정상 상태에서는 지연 시간을, 파티션 상태에서는 일관성을 우선한다
- PC/EC : 항상 일관성을 우선한다
데이터베이스
데이터 모델
- 네트워크 모델 : 데이터를 하나의 큰 트리 -- 계층 구조로 표현하려고 했지만, 다대다 관계의 표현에 적절하지 않음
- 관계형 : 기존 사용 사례 + 다대다 관계를 적절히 처리할 수 있어 널리 채택됨
- NoSQL : 관계형 모델로 효율적으로 처리할 수 없는 문제를 해결하기 위해 등장
- 문서 데이터베이스 : 문서 단위로 데이터(보통 자유 형식)를 저장하고, 문서 간 관계는 거의 없는 사용 사례에 적합
- 그래프 데이터베이스 : 모든 것이 잠재적으로 관계를 가질 수 있는 사용 사례에 적합
한편, 주요 관계형 데이터베이스에 JSON 지원이 추가되기도 하고, 문서 데이터베이스에서 집계 쿼리와 유사한 기능을 제공하기도 한다.
문서 데이터베이스는 스키마리스로 불리기도 하는데, 데이터를 읽는 코드는 어느 정도 구조를 가정하기 때문에 오해의 소지가 있다. 대신 아래와 같이 구분하는 것이 좋다
- 쓰기 스키마(schema-on-write) : Java, C#등이 정적(컴파일 타임) 타입 검사를 수행하듯, 검증된 데이터만 쓰기 허용
- 읽기 스키마(schema-on-read) : 스크립트 언어들의 동적(런타임) 타입과 유사하게, 읽어온 데이터의 구조는 암묵적
컬럼 지향 저장소
데이터베이스의 2가지 주요 사용 사례는 아래와 같다
| 특성 | OLTP(Online transaction processing) | OLAP(Online analytic processing) |
|---|---|---|
| 주요 요청자 | 고객(엔드 유저) | 데이터 분석가(직원) |
| 읽기 패턴 | 질의 당 적은 수의 레코드, 키 기준 | 많은 레코드의 적은 수의 컬럼에 대한 집계 |
| 쓰기 패턴 | 사용자 요청에 따라 낮은 지연으로 임의 위치에 수행 | 대규모 불러오기 또는 이벤트 스트림 |
| 데이터 표현 | 데이터의 최신 상태 | 시간에 따른 이벤트 이력 |
| 데이터셋 크기 | GB ~ TB | TB ~ PB |
비즈니스 의사 결정에 중요한 OLAP 시스템으로 컬럼 지향 저장소가 출시되어 활발히 이용되고 있다
- OLTP 데이터베이스들이 로우 지향 방식으로 데이터를 배치하는 반면, 컬럼 지향 저장소는 각 컬럼들만을 (따로=파티셔닝) 모아 저장한다 -- 대신 로우를 합성해낼 수 있도록 저장 순서를 유지한다
- 칼럼의 고유값의 수는 로우 수에 비해 상당히 작은 -- 값의 중복이 많은 경향이 있어 압축에 유리하다
메시지 큐
설계 전 제약사항
- Pub/Sub 구조를 사용한다 : 메시지 생산과 소비는 특정 토픽에 대하여 이루어진다
- 메시지 소비는 이벤트 풀링 방식으로 진행된다
설계 요소
- 높은 처리 대역폭을 위해 하나의 토픽에 대한 메시지들을 여러 파티션에 분할하여 저장할 필요가 있다
- 생산자가 특별히 지정하지 않은 경우 : 임의 파티션에 전달된다
- 생산자가 파티션을 지정한 경우 : 해당 파티션에 전달된다
- 신규 메시지를 n개 복제본까지 저장이 완료됐을 때 생산자에게 확인 응답을 전달한다 : n은 처리 속도와 안정성 사이의 trade-off
- 내결함성을 위해 각 파티션들은 분산 복제되어야 한다
예. 각 파티션에 대해 1개 마스터, 2개 복제본을 사용하는 노드 배치
노드1 노드2 노드3 노드4 토픽1/파티션1(마스터) 토픽1/파티션1(복제) 토픽1/파티션1(복제) 토픽1/파티션2(마스터) 토픽1/파티션2(복제) 토픽1/파티션2(복제) 예. 토픽이 많이 추가되어 노드를 추가해야 한다면? -- 물론 노드를 하나만 추가해야 된다는 법은 없다
노드1 노드2 노드3 노드4 노드5 토픽1/파티션1(마스터) 토픽1/파티션1(복제) 토픽1/파티션1(복제) 토픽1/파티션2(마스터) 토픽1/파티션2(복제) 토픽1/파티션2(복제) 토픽2/파티션1(마스터) 토픽2/파티션1(복제) 토픽2/파티션1(복제) 토픽2/파티션2(복제) 토픽2/파티션2(마스터) 토픽2/파티션2(복제) 예. 노드가 줄었다면?
- 기존 복제 파티션이 마스터 승격 -- 메시지 큐잉 처리
- 신규 복제 파티션 추가 -- 마스터로부터 열심히 복제 수행
- 관리자의 조정이 아니라, 장애 발생으로 인해 노드가 줄었다면 대체 노드가 신규 복제 파티션을 갖는다
노드1 노드2(삭제) 노드3 노드4 토픽1/파티션1(마스터) 토픽1/파티션1(복제) 토픽1/파티션1(복제 시작) 토픽1/파티션2(복제 시작) 토픽1/파티션2(마스터) 토픽1/파티션2(복제) 예. 처리량을 늘리기 위해 파티션을 추가한다면? -- 신규 메시지만 신규 파티션으로 가도록 해야한다
노드1 노드2 노드3 노드4 토픽1/파티션1(마스터) 토픽1/파티션1(복제) 토픽1/파티션1(복제) 토픽1/파티션2(마스터) 토픽1/파티션2(복제) 토픽1/파티션2(복제) 토픽1/파티션3(복제) 토픽1/파티션3(마스터) 토픽1/파티션3(복제) 예. 불필요한 파티션을 제거한다면?
- 신규 메시지가 삭제 대상 파티션에 가지 않도록
- 모든 메시지가 소비되면 해당 파티션 삭제
- 소비자는 특정 토픽에 대해 구독을 신청하고, 메시지를 가져갈 파티션을 할당받는다
- 적절한 메시지 소비를 위해, 어떤 파티션이 어떤 소비자에게 할당됐는지 관리하는 코디네이터가 있어야 한다
- 코디네이터는 소비자 추가만이 아니라, 소비자 제거(의도적이든 사고든)에도 대응해야 한다
즉, 소비자가 할당받은 파티션은 이후 변경될 수 있다
- 특정 생산자의 메시지를 FIFO로 소비되도록 보장하려면?
- 생산자는 파티션을 지정하여 메시지를 생산한다
- 토픽의 파티션에 대한 소비자를 최대 1개로 제한한다
- 파티션보다 많은 수의 소비자가 존재할 수 없으므로, 높은 처리 대역폭이 필요하다면 미리 파티션을 다수로 나눌 필요가 있다
메시지 전달 보장 레벨
- 최대 한 번 전달 : 생산자는 메시지 생산에 대한 확인 응답을 받지 않고, 소비자가 메시지를 가져가면 무조건 처리됐다고 간주
- 최소 한 번 전달 : 생산자는 메시지 생산에 대한 확인 응답을 받고, 소비자가 메시지 소비 완료했다고 브로커에게 전달해야 처리됐다고 간주
- 생산자는 타임아웃 발생 시 메시지를 다시 생산
- 메시지 소비가 실제로는 완료됐지만 브로커에게 전달되지 않은 경우, 같은 메시지가 다시 소비
- 정확히 한 번 전달 : 많은 오버헤드가 추가로 필요
분산 데이터 시스템
설계 원칙
모든 상황에 맞는 확장 아키텍처는 없다 -- 1KB를 초당 10만건 처리하는 시스템과 2GB를 분당 3건 처리하는 시스템은 데이터 처리량이 같아도 설계는 매우 다르다.
특정 애플리케이션에 적합한 아키텍처는 주요 동작이 무엇인지, 잘 하지 않는 동작이 무엇인지에 대한 가정을 부하 매개변수로 하여 구축한다 -- 하지만 스타트업 초기 단계나 검증되지 않은 제품은 불확실한 가정에 대비하기보다, 유지보수에 초점을 맞추는 것이 좋다
- 운용성 : 운영팀이 시스템을 원활하게 운영할 수 있도록
- 단순성 : 새로운 엔지니어가 시스템을 이해하기 쉽도록
- 발전성 : 엔지니어가 이후 시스템을 변경하기 쉽도록
복제
복제 방식에 따른 구분
동기식 복제
- 쓰기가 모든 팔로워에 반영 > 리더에도 최종 반영 > 클라이언트에 성공 응답
- 임의 한 노드 장애가 전체 시스템을 멈추게 하므로 비현실적
비동기식 복제
- 쓰기는 리더에만 반영되면 클라이언트에 성공 응답을 보내고, 복제는 백그라운드에서 진행
- 리더가 갑자기 중단되면 미처 팔로워에 복제되지 않은 쓰기는 유실된다
- 비동기 팔로워의 읽기는 옛날 정보를 반환할 수 있지만, 시간이 지나면 최종적으로 하나의 값으로 수렴한다
이를 "최종적 일관성"이라 일컫는데, 이 용어는 의도적으로 모호하다 -- 복제 서버가 얼마나 뒤쳐질 수 있는지에 대한 제한이 없다
반동기식 복제
- 두 방식을 같이 사용
예. 같은 리전에 존재하는 팔로워 하나에는 동기식으로 복제, 다른 리전에 존재하는 팔로워에는 비동기식으로 복제
토폴로지에 따른 구분
단일 리더 복제
- 쓰기 요청은 단일 리더 노드에서만 처리
- 단일 리더 복제는 보통 완전히 비동기식으로 구성
다중 리더 복제
- 쓰기 요청은 여러 리더 노드들이 처리
- 같은 행에 대한 동시 쓰기를 어떻게 해결할 지 결정 필요
- 충돌 자체를 회피할 수 있도록 설계
- 충돌을 자동으로 수정할 수 있도록 설계
- 충돌을 사용자에게 알려 직접 해결하도록 설계
리더 장애 복구
- 다른 데이터 저장소와 함께 사용하는 사례에서는 팔로워에 미처 반영되지 않은 데이터를 단순히 폐기하는 경우 큰 문제가 발생할 수 있다
GitHub 예. MySQL 다중 리더 중 하나가 중단됐을때, 리더로 승격된 팔로워의 기본키 자동 증가 카운터가 불일치하여 기본키가 재사용됨
- 토폴로지에서 분리된 노드가 여전히 자신을 리더라고 믿고 있다면(split brain) 큰 문제가 될 수 있다
- 리더가 죽었다고 판단하는 타임아웃을 적절히 설정해야 한다. 너무 짧으면 불필요한 장애 복구가 발생하고, 너무 길면 장애 복구가 늦어진다
자동 복구보다 수동 복구가 나을수도?
리더 없는 복제
- 쓰기 요청을 모든 노드들이 처리. 대표적으로 AWS의 다이나모 DB가 있다
특정 시점에 노드들이 서로 다른 데이터를 가지기 십상이다
- 읽기 복구 : 읽기 시점에 데이터 버전 불일치를 발견하면 이전 버전 데이터를 업데이트
- 안티 엔트로피 처리 : 백그라운드 프로세스에서 지속적으로 동기화 수행
- 클라이언트가 직접 여러 노드에 쓰기를 보내기도, 중간에 코디네이터를 둬 대신 수행하도록 구현하기도 한다
- n개 복제 서버 중 w개 노드에 쓰기가 성공하면 쓰기 확정
w + c개의 노드에 쓰기 요청을 보냈고 w개의 노드에 쓰기가 성공했다면, 최대 c개의 쓰기 실패는 단순히 무시 가능
- n개 복제 서버 중 r개 노드에 읽기가 성공하면 가장 최신 버전의 데이터로 반환
- r개 노드 중 최소한 1개에 최신 버전의 데이터가 존재해야 하므로, w + r > n을 만족해야 하며, 이때의 w, r을 각각 쓰기, 읽기 정족수라 한다
- 읽기와 쓰기가 동시에 발생하는 경우, 어떤 버전의 데이터가 반환될 지는 불분명하다
- 동시 쓰기는 여전히 문제가 된다 동시 쓰기 문제
- 노드 간 복제 지연 시간이 균일하지 않다면, 나중에 발생한 읽기가 이전에 발생한 읽기보다 이전 버전의 데이터를 가져갈 수 있다
선형성과 인과성
- 일반적으로 프로그래밍에서 변수에 어떤 값을 할당한 후 즉시 해당 변수를 읽었을 때, 직전에 쓴 값을 그대로 읽을 것으로 기대하며, 읽기를 몇 번 반복해 수행하든 그 결과는 동일할 것으로 기대한다
분산 시스템에서 위와 같은 성질을 제공한다면 이를 선형성 시스템이라고 한다 -- 그 외는 비선형성 시스템
- 선형성을 보장하는 시스템은 모든 연산에 대해 전역적인 단 하나의 순서를 부여해야하고, 필연적으로 느리다
- 단일 리더 복제 시스템은 리더가 모든 연산에 단조 증가하는 일련번호를 부여하고, 팔로워는 이를 순서대로 적용함으로써 선형성을 만족할 수 있다
- 여러 노드가 쓰기를 수행하는 시스템에서는 합의를 통해 순서를 결정하여 선형성을 만족할 수 있다
- 보통 선형성이 필요한 경우는 인과 관계가 있는 두 쓰기 결과를 읽을 때다
- 두 개의 쓰기 A, B가 서로 다른 시간에 발생했다면 나중에 발생한 쓰기가 이전 쓰기를 참조할 가능성이 있다 -- 인과 관계가 존재할 수 있다
- 두 개의 쓰기가 동시에 발생했다면 두 쓰기 사이에 인과 관계가 존재할 리는 없다
따라서 선형성을 보장하는 대신 좀 더 적은 비용으로 쓰기 연산이 인과성을 보존한다면, 서로 관계가 있는 연산들에 대해서는 순서를 보장할 수 있다
파티션
- 모든 데이터셋을 한 노드가 들고 있는 것은 비효율적일 수 있고, 심지어 OLAP 시스템에서는 불가능할 수도 있다
- 노드 수에 따라 자동으로 파티션을 확장/축소하는 것은 복잡할 수 있다
간단한 해결책으로 파티션을 처음부터 1000개 만들어 고정하는 식으로 구현할 수도 있지만, 파티션 개수에 의한 오버헤드에 유의해야 한다
- 시간을 기준으로 파티셔닝하면, 여전히 특정 파티션이 핫스팟이 될 수 있다
파티션 분배는 시간을 기준으로 하지 않는 대신, 각 파티션 내에서 시간을 기준으로 데이터를 정렬해 저장하는 것이 더 나을 수 있다
분산 트랜잭션과 합의
2단계 커밋(2PC)
- 코디네이터는 쓰기에 참여하는 모든 노드에게 트랜잭션의 내용이 유효한지 묻는다
이 시점에 "네"라고 응답한 노드는 단독으로 롤백을 수행할 수 없다 -- 이후 어떤 장애가 도중에 발생하든 코디네이터로부터 커밋 또는 롤백 결정을 받은 뒤 반영해야 한다. 이러한 장애 상태의 트랜잭션은 의심스러운 또는 불확실한 상태에 있다고 한다
- 모든 노드가 유효하다고 응답했다면, 코디네이터는 모든 노드에게 커밋을 요구한다
코디네이터 장애 복구를 위해, 코디네이터는 최종 요청을 노드들에게 보내기 전에 디스크에 기록해야 한다
XA 트랜잭션
- 이기종 간 2단계 커밋을 구현하는 표준
- PostgreSQL, MySQL 등의 관계형 데이터베이스와 ActiveMQ, MSSQ 등의 메시지 브로커에서 지원된다
- 여러 XA 구현은 코디네이터 장애가 발생하여 트랜잭션이 의심스러운 상태에 있을 때, 더 큰 장애로 확산하는 걸 막기 위해 커밋 여부를 일방적으로 결정할 수 있는 방법을 제공한다
합의
합의 알고리즘은 다음을 만족해야 한다
- 균일한 동의 : 어떤 두 노드도 다르게 결정하지 않는다
- 무결성 : 어떤 노드도 두 번 결정하지 않는다
- 유효성 : 결정된 값은 어떤 노드에서 제안된 것이다
- 종료 : 죽지 않은 모든 노드는 어떤 값을 결정한다
도중에 죽은 노드는 더 이상 합의에 참여할 수 없다 -- 도중에 죽은 노드의 복구를 기다리지 않는다
합의 알고리즘을 구현하는 것은 매우 어려우며, 주키퍼와 같은 성공적인 도구를 이용해 합의, 장애 감지, 분산 잠금, 멤버십 서비스 등을 위탁하는 것이 합리적이다