결제 플랫폼은 일반적으로 고객에게 전자 지갑 서비스를 제공하여 고객으로 하여금 지갑에 돈을 넣어 두고 필요할 때 사용할 수 있도록 한다.
예를 들어, 은행 카드에서 전자 지갑에 돈을 이체해 두면 전자상거래 사이트에서 제품을 구매할 때 그 지갑의 돈을 사용하여 결제하는 옵션을 선택할 수 있다.
1단계: 설계 범위
1. 기능 요구사항
- 전자 지급 간 이체
- 1,000,000TPS
- 99.99%의 안전성
- 트랜잭션
- 재현성
2. 개략적인 규모 추정
- TPS를 거론한다는 것은 배후에 트랜잭션 기반 데이터베이스를 사용한다는 뜻이다.
- 오늘날 데이터센터 노드에 실행되는 관계형 데이터베이스는 초당 수천 건의 트랜잭션을 지원할 수 있다.
- 이체 명령을 실행하려면 두 번의 연산이 필요하기 때문에 1백만 건의 TPS를 처리하기 위해서는 2백만 TPS를 지원해야 하고, 결국 2000개 노드가 필요하다는 뜻이다.
2단계: 개략적 설계
1.API 설계
API | 기능 |
POST /v1/wallet/balance_transfer | 한 지갑에서 다른 지갑으로 자금 이체 |
요청 인자
- amount 필드의 자료형이 double이 아니라 string이다.
- 그러나 실제로는 float이나 double을 택하는 경우 많은데, 대부분의 프로그래밍 언어와 데이터베이스가 지원하기 때문이다.
필드 | 설명 | 자료형 |
from_account | 자금을 인출할 계좌 | string |
to_account | 자금을 이체할 계좌 | string |
amount | 이체할 금액 | string |
currency | 통화 단위 | string(ISO 4217) |
transaction_id | 중복 제거에 사용할 ID | uuid |
응답 몬문 사례
{
"status" : "success",
"transaction_id": "0123423-41234-23r11-123123-123124"
}
2. 인메모리 샤딩
지갑 애플리케이션은 모든 사용자 계정의 잔액을 유지한다.
- <사용자, 잔액> 관계를 나타내기에 좋은 자료 구조는 해시 테이블이라고 불리는 맵(map) 또는 키-값 저장소다.
인메모리 저장소로 인기 있는 선택지 하나는 레디스이다.
- 하나의 레디스로는 벅차기 때문에 클러스터를 구성하고 사용자 계정을 모든 노드에 균등하게 분산시켜야 한다.
- 파티셔닝 또는 샤딩이라고 한다.
키-값 데이터를 n개 파티션에 고르게 분배하려면 키의 해시 값을 계산하고 이를 파티션의 수 n으로 나누는 것이 한 가지 방법이다.
- 모든 레디스 노드의 파티션 수 및 주소는 한군데 저장한다.
- 높은 가용성을 보장하는 설정 정보 전문 저장소 주키퍼(ZooKeeper)를 용도로 쓰면 좋다.
마지막 구성 요소는 이체 명령 처리를 담당하는 서비스로, 지갑 서비스라고 부른다.
- 이체 명령의 수신
- 이체 명령의 유효성 검증
- 명령이 유효한 것으로 확인되면 이체에 관게된 두 계정의 잔액 갱신
=>> 두 계정은 서로 다른 레디스 노드에 있을 수 있다.
- A, B, C라는 세 클라이언트가 있으며, 이들의 게정 잔액 정보는 세 개 레디스 노드에 균등하게 분산되어 있다.
- 이체 요청을 처리하는 지갑 서비스 노드는 두 개 있다.
- 클라이언트 A에서 클라이언트 B로 1달러를 이체하라는 명령을 받으면 두 개의 레디스 노드에 두 개의 명령이 전달된다.
두 개의 레디스 노드를 업데이트하는데, 두 연산이 모두 성공하리라는 보장이 없다.
=> 두 업데이트 연산은 하나의 원자적 트랜잭션으로 실행되어야 한다.
3. 분산 트랜잭션
3.1. 데이터베이스 샤딩
서로 다른 두 개 저장소 노드를 갱신하는 연산을 원자적으로 수행하려면 어떻게 해야 할까?
- 레디스 노드를 트랜잭션을 지원하는 관계형 데이터베이스 노드로 교체한다.
=> 이체 명령이 서로 다른 두 데이터베이스 서버에 있는 계정 두 개를 업데이트해야 할 가능성이 높은데, 두 작업이 정확히 동시에 처리된다는 보장이 없다.
첫 번재 계정의 잔액을 갱신한 직후에 두 번째 계정의 잔액도 반드시 갱신되도록 하려며 어떻게 해야할까?
3.2. 분산 트랜잭션: 2단계 커밋(2PC)
- 분산 트랜잭션은 프로세스를 원자적인 하나의 트랜잭션으로 묶는 방안이다.
- 구현법으로는 저수준 방안과 고수준 방안 두 가지가 있다.
저수준 방안은 데이터베이스 자체에 의존하는 것으로 가장 일반적으로 사용되는 알고리즘은 2단계 커밋(2PC)이다.
- 해당 방인이 저수준 방안인 이유는, 준비 단계를 실행하려면 데이터베이스 트랜잭션 실행 방식을 변경해야 하기 때문이다.
- 2PC의 가장 큰 문제점은 다른 노드의 메세지를 기다리는 동안 락이 오랫동안 잠긴 상태로 남을 수 있어서 성능이 좋지 않다는 것이다.
- 또 다른 문제는 조정자가 SPOF, 단일 장애 지점이 될 수 있다.
3.3. 분산 트랜잭션: TC/C
TC/C는 두 단계로 구성된 보상 트랜잭션이다.
- 조정자는 모든 데이터베이스에 트랜잭션에 필요한 자원 예약을 요청한다.
- 조정자는 모든 데이터베이스로부터 회신을 받는다.
- 모두 "예"라고 응답하면 조정자는 모든 데이터베이스에 작업 확인을 요청하는데, 이것이 바로 '시도-확정' 절차다.
- 어느 하나라도 '아니요'라고 응답하면 조정자는 모든 데이터베이스에 작업 취소를 요청하며, 이것이 바로 '시도-취소' 절차이다.
2PC의 두 단계는 한 트랜잭션이지만 TC/C에서는 각 단계가 별도 트랜잭션이라는 점을 유의한다.
1) TC/C 사례
단계 | 실행연산 | A | C |
1 | 시도 | 잔액 변경: -$1 | 아무것도 하지 않음 |
2 | 확인 | 아무것도 하지 않음 | 잔액 변경: +$1 |
취소 | 잔액 변경: +$1 | 아무것도 하지 않음 |
첫 번째 단계(시도): 시도 단계에서는 조정자 역할을 하는 지갑 서비스가 두 개의 트랜잭션 명령을 두 데이터베이스로 전송한다.
- 조정자는 계정 A가 포함된 데이터베이스에 A의 잔액을 1달러로 감소시키는 트랜잭션을 시작한다.
- 조정자는 계정 C가 포함된 데이터베이스에는 아무 작업도 하지 않는다. 조정자가 데이터베이스에 NOP 명령을 보낸다고 가정하면, 아무 작업도 수행하지 않으며 항상 성공했다는 응답을 보낸다.
두 번째 단계(확정): 두 데이터베이스가 모두 예라고 응답하면 지갑 서비스는 확정 단계를 시작한다.
- 계정 C는 아직 1달러를 받지 못했기 때문에 확인 단계에서 지갑서비스는 계정 C의 잔액에 1달러를 추가해야 한다.
두 번째 단계(취소): 첫 번째 시도가 실패하면 분산 트랜잭션을 취소하고 관련된 자원을 반환해야 한다.
- 시도 단계의 트랜잭션에서 계정 A의 잔액은 이미 바뀌었고 트랜잭션은 종료되었다.
- 종료된 트랜잭션의 효과를 되돌리려면 지갑 서비스는 또 다른 트랜잭션을 시작하여 계정 A에 1달러를 다시 추가해야 한다.
2) 2PC와 TC/C의 비교
- 2PC에서는 두 번째 단계는 미완성된 트랜잭션을 중단하거나 커밋하여 끝낸다.
(즉, 미완성된 트랜잭션을 중단하거나 커밋하여 끝낸다.) - TC/C의 두 번재 단계는 오류가 발생했을 때 이전 트랜잭션 결과를 상쇄하는 새로운 트랜잭션을 실행한다.
(즉, 오류가 발생하면 이전 트랜잭션 결과를 상쇄하는 새로운 트랜잭션을 실행한다.)
첫 번째 단계 | 두 번째 단계: 성공 | 두 번째 단계: 실패 | |
2PC | 로컬 트랜잭션은 아직 완료되지 않은 상태 | 모든 로컬 트랜잭션을 커밋 | 모든 로컬 트랜잭션을 취소 |
TC/C | 모든 로컬 트랜잭션이 커밋되거나 취소된 상태로 종료 | 필요한 경우 새 로컬 트랜잭션 실행 | 이미 커밋된 트랜잭션의 실행 결과를 되돌림 |
- TC/C는 보상 기반 분산 트랜잭션이라고 부른다.
- 실행 취소 절차를 비즈니스 로직으로 구현하므로 고수준 해법이다.
- 장점
- 특정 데이터베이스에 구애받지 않는다는 것이다.
- 트랜잭션을 지원하는 데이터베이스이기만 하면 TC/C는 작동한다.
- 단점
- 애플리케이션 계층의 비즈니스 로직에서 세부 사항을 관리하고 분산 트랜잭션의 복잡성을 처리해야 한다.
3) 단계별 상태 테이블
TC/C 실행 도중에 지갑 서비스가 다시 시작되면 어떻게 되나?
- 과거 모든 작업 기록이 사라질 수 있으며, 어떻게 복구해야 할지 알 수 없게 된다.
- 해결책으로 각 단계 상태 정보를 트랜잭션 데이터베이스에 저장하면 된다.
- 상태 정보는 다음을 포함해야 한다.
- 분산 트랜잭션의 ID와 내용
- 각 데이터베이스에 대한 시도 단계의 상태 (not sent yet, has been sent, response received)
- 두 번째 단계의 이름. Confirm, Cancel 중 하나.
- 두 번째 단계의 상태
(단계 상태 테이블은 데이터베이스에 저장해둔다.) - 순서가 어긋났음을 나타내는 플래그
4) 불균형 상태
시도 단계가 끝나고 나면 1달러가 사라진다.
- 시작전: A + C = 1달러
- 시도: A + C = 0달러
( 잔액 총합은 동일해야 한다는 회계 원칙을 위반한다.) - 확정: A + C = 1달러
트랜잭션 보증은 TC/C 방안에서도 여전히 유효하다.
- TC/C는 여러 개의 독립적인 로컬 트랜잭션으로 구성된다.
- TC/C는의 실행 주체는 애플리케이션이며, 애플리케이션은 이런 독립적 로컬 트랜잭션이 만드는 중간 결과를 볼 수 있다.
- 반면, 데이터베이스 트랜젹신이나 2PC 같은 분산 트랜잭션의 경우 실행 주체는 데이터베이스이며 애플리케이션은 그 중간 실행 결과를 알 수없다.
분산 트랜잭션 실행 도중에는 항상 데이터 불일치가 발생한다.
데이터베이스와 같은 하위 시스템에서 불일치를 수정하는 경우에는 그 사실을 알 필요는 없지만, 그렇지 않다면 직접 처리해야한다.
5) 유효한 연산 순선
표. 시도 단계에서 가능한 일
선택지 | 계정 A | 계정 C |
선택 1 | + 1달러 | NOP |
선택 2 | NOP | + 1달러 |
선택 3 | - 1달러 | + 1달러 |
- 두 번째 선택지와 세 번재 선택지의 경우 유효하지 않은 방법이다.
- 선택 1만이 올바른 방법이다.
6) 잘못된 순서로 실행된 경우
예시)
- 계정 A에 대한 작업이 실패한다.
- 지갑 서비스에 실패를 반환한 다음 취소 단계로 진입하여 계정 A와 계정 C 모두에 취소명령을 전송한다.
- 계정 C를 관리하는 데이터베이스에 네트워크 문제가 있어서 시도 명령 전에 취소 명령부터 받게 되었다.
(취소할 것이 없는 상태이다.)
시도 절차를 미처 받지 못한 경우에도 취소 절차를 실행할 수 있다.
- 취소 명령이 먼저 도착하면 데이터베이스에 아직 상응하는 시도 명령을 못보았음을 나타내는 플래그를 참으로 설정하여 저장해 둔다.
- 시도 명령이 도착하면 항상 먼저 도착한 취소 명령이 있었는지 확인한다. 있었으면 바로 실패를 반환한다.
단계별 상태 테이블에서 순서가 어긋난 경우를 처리하기 위한 플래그를 마련했던 것은 이러한 이유이다.
3.4. 분산 트랜잭션: 사가
선형적 명령 수행
- 사가(Saga)는 유명한 분산 트랜잭션 솔루션 가운데 하나로 마이크로서비스 아키텍처에서는 사실상 표준이다.
사가 개념
- 모든 연산은 순서대로 정렬된다. 각 연산은 자기 데이터베이스에 독립 트랜잭션으로 실행된다.
- 연산은 첫 번재부터 마지막까지 순서대로 실행된다. 한 연산이 완료되면 다음 연산이 개시된다.
- 연산이 실패하면 전체 프로세스는 실패한 연산부터 맨 처음 연산까지 역순으로 보상 트랜잭션을 통해 롤백된다.
(n개의 연산을 실행하는 분산 트랜잭션은, 보상 트랜잭션을 위한 n개 연산까지 총 2n개의 연산을 준비해야한다.)
연산 실행 순서는 어떻게 조율할까?
- 분산 조율: 마이크로서비스 아키텍처에서 사가 분산 트랜잭션에 관련된 모든 서비스가 다른 서비스의 이벤트를 구독하여 작업을 수행하는 방식. 완전히 탈 중앙화된 조율 방식이다.
- 중앙 집중형 조율: 하나의 조정자가 모든 서비스가 올바른 순서로 작업을 실행하도록 조율한다.
TC/C vs 사가
- 지연 시간 요구사항이 없거나 앞서 살펴본 송금 사례처럼 서비스 수가 매우 적다면 아무것이나 사용하면 된다. 마이크로서비스 아키텍처에서 흔히 하는 대로 하고 싶다면 사가를 선택하면 된다.
- 지연 시간에 민감하고 많은 서비스/운영이 관계된 시스템이라면 TC/C가 더 낫다.
TC/C | 사가 | |
보상 트랜잭션 실행 | 취소 단계에서 | 롤백 단계에서 |
중앙 조정 | 예 | 예(중앙 집중형 조율 모드에서만) |
작업 실행 순서 | 임의 | 선형 |
병렬 실행 가능성 | 예 | 아니요(선형적 실행) |
일시적으로 일관되지 않은 상태 허용 | 예 | 예 |
구현 계층: 애플리케이션 또는 데이터베이스 | 애플리케이션 | 애플리케이션 |
4. 이벤트 소싱
4.1. 배경
실제로 전자 지갑 서비스 제공 업체도 감사를 받을 수 있다.
외부 감사는 까다로운 질문들을 한다.
- 특정 시점의 계정 잔액을 알 수 있나요?
- 과거 및 현재 계정 잔액이 정확한지 어떻게 알 수 있나요?
- 코드 변경 후에도 시스템 로직이 올바른지는 어떻게 검증하나요?
해당 질문에 체계적으로 답할 수 있는 철학 중 하나는 도메인 주도 설계에서 개발된 기법인 이벤트 소싱이다.
4.2. 정의
이벤트 소싱에는 네 가지 중요한 용어가 있다.
- 명령
- 명령은 외부에서 전달된, 의도가 명확한 요청이다.
- 예를 들어 고객 A에서 C로 1달러를 이체하라는 요청은 명령이다.
- 이벤트 소싱에서 순서는 아주 중요하다. 따라서 명령은 일반적으로 FIFO 큐에 저장된다.
- 이벤트
- 작업 이행 전에는 반드시 명령의 유효성 검사를 해야한다.
- 검사를 통과한 명령은 반드시 이행되어야 한다.
- 명령 이행 결과를 이벤트라고 부른다.
- 명령과 이벤트에는 두 가지 중요한 차이점이 있다.
- 이벤트는 검증된 사실로, 실행이 끝난 상태다. 이벤트에 대해 이야기 할때는 과거 시제를 사용한다.
- 명령에는 무작위성이나 I/O가 포함될 수 있지만 이벤트는 결정론적이다. 이벤트는 과거에 실제로 있었던 일이다.
- 이벤트 생성 프로세스에는 두 가지 중요한 특징
- 하나의 명령으로 여러 이벤트가 만들어질 수 있다.
- 이벤트 생성과정에는 무작위성이 개입될 수 있어서, 같은 명령에 항상 동일한 이벤트들이 만들어진다는 보장이 없다.
- 이벤트 순서는 명령 순서를 따라야 하기 때문에 FIFO 큐에 저장한다.
- 상태
- 이벤트가 적용될 때 변경되는 내용이다.
- 지갑 시스템에서 상태는 모든 클라이언트 계정의 잔액으로, 맵을 자료 구조로 사용하여 표현할 수 있다.
- 키는 계정 이름 또는 ID이고 값은 계정 잔액이다.
- 상태 기계
- 이벤트 소싱 프로세스를 구동한다.
- 크게 두 가지 기능이 있다.
- 명령의 유효성을 검사하고 이벤트를 생성한다.
- 이벤틀르 적용하여 상태를 갱신한다.
- 이벤트 소싱을 위한 상태 기계는 결정론적으로 동작해야 한다. 따라서 무작위성을 내포할 수 없다.
- 아래 그림은 명령을 이벤트로 변환하고 이벤트를 적용하는 두 가지 기능을 지원해야 하므로, 명령 유효성 검사를 위한 상태 기계 하나와 이벤트 적용을 위한 상태기계 하나를 두었다.
지갑 서비스 예시
- 지갑 서비스의 경우 명령은 이체 요청이다.
- 명령은 FIFO 큐에 기록하며 큐로는 카프카를 널리 사용한다.
계정 잔액은 관계형 데이터베이스에 있다고 가정한다.
상태 기계는 명령을 큐에 들어간 순서대로 확인하다.
재현성
이벤트 소싱이 다른 아키텍처에 비해 갖는 가장 중요한 장점은 재현성이다.
앞서 언급한 분산 트랜잭션 방안의 경우 지갑 서비스는 갱신한 계정 잔액을 데이터베이스에 저장한다.
- 계정 잔액이 변경된 이유는 알기가 어렵다.
- 한 번 업데이트가 이루어지고 나면 과거 잔액이 얼마였는지 알 수 없다.
- 데이터베이스는 특정 시점의 잔액이 얼마인지만 보여 준다.
하지만, 이벤트를 처음 부터 재생하면 과거 잔액 상태는 언제든 재구성할 수 있다.
- 이벤트 리스트는 불변이고 상태 기계 로직은 결정론적이므로 이벤트 이력을 재생하여 만들어낸 상태는 언제나 동일하다.
재현성을 갖추면 감사관이 던지는 까다로운 질문에 답할 수 있다.
- 시작부터 계정 잔액을 알고 싶은 시점까지 이벤트를 재생하면 알 수 있다.
- 이벤트 이력에서 계정 잔액을 다시 계산해 보면 잔액이 정확한지 확인 수 있다.
- 새로운 코드에 동일한 이벤트 이력을 입력으로 주고 같은 결과가 나오는지 보면 된다.
감사 가능 시스템이어야 한다는 요건 때문에 이벤트 소싱이 지갑 서비스 구현의 실질적인 솔루션으로 채택되는 경우가 많다.
명령 -질의 책임 분리(CQRS)
클라이언트는 여전히 계정 잔액을 알 수 없다.
이벤트 소싱 프레임워크 외부의 클라이언트가 상태를 알도록 할 방법이 필요하다.
- 직관적인 해결책 하나는 상태 이력 데이터베이스의 읽기 전용 사본을 생성한 다음 외부와 공유하는 것이다.
이벤트 소싱은 이와는 조금 다른 해결책을 제시한다.
- 이벤트 소싱은 상태, 즉 계정 잔액을 공개하는 대신 모든 이벤트를 외부에 보낸다. 따라서 이벤트를 수신하는 외부 주체가 직접 상태를 재구축할 수 있다.
=> 이런 설계 철학을 명령-질의 책임 분리(CQRS)라고 한다. - CQRS에서는 상태 기록을 담당하는 상태 기계는 하나고, 읽기 전용 상태 기계는 여러 개 있을 수 있다.
- 읽기 전용 상태 기계는 상태 뷰를 만들고, 해당 뷰는 질의에 이용된다.
읽기 전용 상태 기계는 이벤트 큐에서 다양한 상태 표현을 도출할 수 있다.
예를 들어)
- 클라이언트의 잔액 질의 요청을 처리하기 위해 별도 데이터베이스에 상태를 기록하는 등의 작업을 할 수도 있다.
- 이중 청구 등의 문제를 쉽게 조사할 수 있도록 하기 위해 특정 기간 동안의 상태를 복원할 수도 있다.
일기 전용 상태 기계는 실제 상태에 어느 정도 뒤쳐질 수 있으나 결국에는 같아진다. 따라서 일관성 모델을 따른다 할 수 있다.
3단계: 상세 설계
높은 성능과 안정성 및 확장성을 달성하기 위한 기술에 대해 살펴본다.
1. 고성능 이벤트 소싱
1.1 파일 기반의 명령 및 이벤트 목록
명령과 이벤트를 카프카 같은 원격 저장소가 아닌 로컬 디스크에 저장하는 방안을 생각해 볼 수 있다.
=> 이렇게 하면 네트워크를 통한 전송 시간을 피할 수 있다.
1) 이벤트 목록은 추가 연산만 가능한 자료구조에 저장한다.
- 추가는 순차적 쓰기 연산으로 일반적으로 매우 빠르다.
- 운영체제는 보통 순차적 읽기 및 쓰기 연산에 엄청나게 최적화되어 있기 때문에 HDD에서도 잘 작동한다.
- 순차적 디스크 접근은 경우에 따라서 무작위 메모리 접근보다도 빠르게 실행될 수 있다.
2) 명령과 이벤트를 메모리에 캐시하는 방안도 생각해 볼 수 있다.
- 앞서 설명했듯 명령과 이벤트는 지속성 저장소에 보관된 이후에 처리된다.
- 메모리에 캐시해 놓으면 로컬 디스크에서 다시 로드하지 않아도 된다.
구체적인 구현 방법 몇가지를 살펴본다.
- mmap 기술은 최적화 구현에 유용하다.
- mmap를 사용하면 로컬 디스크에 쓰는 동시에, 최근 데이터는 메모리에 자동으로 캐시할 수 있다.
- mmap은 디스크 파일을 메모리 배열에 대응시킨다.
1.2 파일 기반 상태
잔액 정보를 관계형 데이터베이스에 저장했다.
명령 및 이벤트 저장소 최적화 방안과 마찬가지로, 상태 정보도 로컬 디스크에 저장할 수 있다.
- SQLite를 사용하거나, 로컬 파일 기반 키-값 저장소 RocksDB를 사용할 수 있다.
- RcksDB는 쓰기 작업에 최적화된 자료 구조 LSM을 사용한다.
1.3 스냅숏
모든 것이 파일 기반일 때 재현 프로세스의 속도를 높일 방법을 생각해 보자.
- 재현성이라는 개념을 처음 소개했을 때 재현성 확보를 위해 사용한 방법은 상태 기계로 하여금 이벤트를 항상 처음부터 다시 일도록 하는 것이었다.
- 주기적으로 상태 기계를 멈추고 현재 상태를 파일에 저장한다면 시간을 절약할 수 있을 것이다.
=> 이 파일을 스냅숏(snapshot)이라 부른다. - 스냅숏은 과거 특정 시점의 상태로, 변경이 불가능하다.
- 스냅숏을 저장하고 나면 상태 기계는 더 이상 최초 이벤트에서 시작할 필요가 없다.
- 스냅숏을 읽고 , 어느 시점에 만들어졌는지 확인한 다음, 그 시점부터 이벤트 처리를 시작하면 된다.
- 스냅숏을 사용하면 읽기 전용 상태 기계는 해당 데이터가 포함된 스냅숏 하나만 로드하면 된다.
=> CQRS 개념에서는 처음부터 해당 시점까지 모든 이벤트를 순서대로 처리하는 일긱 전용 상태 기계를 사용해야 했다. - 스냅숏은 거대한 이진 파일이며, 일반적으로는 HDFS과 같은 객체 저장소에 저장한다.
2. 신뢰할 수 있는 고성능 이벤트 소싱
2.1 신뢰성 분석
서버노드가 하는 일은 데이터와 연산이라는 두 가지 개념에 관계되어 있다.
데이터 내구성이 보장되는 한, 계산 결과는 코드를 다른 노드에서 돌리면 복구할 수 있다.
=> 데이터의 신뢰성이 훨씬 중요하다.
시스템에는 네 가지 유형의 데이터가 있다.
- 파일 기반 명령
- 파일 기반 이벤트
- 파일 기반 상태
- 상태 스냅숏
각각의 신뢰성 보장 방법을 살펴본다.
- 상태와 스냅숏은 이벤트 목록을 재생하면 언제든 다시 만들 수 있다.
- 상태 및 스냅숏의 안전성을 향상시키려면 이벤트 목록의 신뢰성만 보장하면 된다.
- 이벤트는 명령어에서 만들어지니 명령의 신뢰성만 강력하게 보장하면 충분하지 않다.
- 이벤트 생성은 결정론적 과정이 아니며, 난수나 외부 입출력 등의 무작위적 요소가 포함될 수 있다.
- 명령의 신뢰성 만으로는 이벤트의 재현성을 보장할 수 없다.
- 이벤트는 상태에 변화를 가져오는 과거의 사실이다.
- 이벤트는 불변이며 상태 재구성에 사용할 수 있다.
- 따라서 높은 신뢰성을 보장할 유일한 데이터는 이벤트다.
2.2 합의
높은 안전성을 제공하려면 이벤트 목록을 여러 노드에 복제해야 한다.
복제 과정은 다음을 보장해야 한다.
- 데이터 손실 없음
- 로그 파일 내 데이터의 상대적 순서는 모든 노드에 동일
해당 목표를 달성하는데 합의 기반 복제 방안이 적합하다.
- 해당 알고리즘은 모든 노드가 동일한 이벤트 목록에 합의하도록 보장한다.
래프트 알고리즘을 예로 든다.
- 래프트 알고리즘을 사용하면 노드의 절반 이상이 온라인 상태면 그 모두에 보관된 추가 전용 리스트는 같은 데이터를 가진다.
- 래프트 알고리즘에서 노드는 세가지 역할을 가질 수 있다.
- 리더
- 후보
- 팔로어
- 최대 하나의 노드만 클러스터의 리더가 되고 나머지 노드는 팔로어가 된다.
- 리더는 외부 명령을 수신하고 클러스터 노드 간에 데이터를 안정적으로 복제하는 역할을 담당한다.
- 래프트 알고리즘을 사용하면 과반수 노드가 작동하는 한 시스템은 안정적이다.
2.3 고신뢰성 솔루션
복제 메커니즘을 활용하면 파일 기반 이벤트 소싱 아키텍처에서 단일 장애 지점 문제를 없앨 수 있다.
- 리더는 외부 사용자로부터 들어오는 명령 요청을 받아 이벤트로 변환하고 로컬 이벤트 목록에 추가한다.
- 래프트 알고리즘은 새로운 이벤트를 모든 팔로어에 복제한다.
- 팔로어를 포함한 모든 노드가 이벤트 목록을 처리하고 상태를 업데이트 한다.
- 래프트 알고리즘은 리더와 팔로어가 돌일한 이벤트 목록을 갖도록 하며, 이벤트 소싱은 동일한 이벤트 목록에서 항상 동일한 상태가 만들어지도록 한다.
안정적인 시스템은 장애를 원활하게 처리해야 하므로 노드 장애가 어떻게 처리되는지 살펴본다.
- 리더에 장애가 발생하면 래프트 알고리즘은 나머지 정상 노드 중에서 새 리더를 선출한다.
- 새 리더는 외부사용자로부터 오는 명령을 수신할 책임을 진다.
- 한 노드가 다운되어도 클러스터는 계속 서비스를 제공할 수 잇다.
- 유의할 것은 리더 장애가 명령 목록이 이벤트로 변환되기 전에 발생할 수 있다는 것이다.
- 이런 일이 생기면 클라이언트는 시간 초과 또는 오류 으답을 받는다.
- 그럼 클라이언트는 새로 선출된 리더에게 같으 명령을 다시 보내야 한다.
3. 분산 이벤트 소싱
고성능 이벤트 소싱 아키텍처를 구현하는 방법을 설명했다.
해당 아키텍처는 신뢰성 문제는 해결되지만 다른 문제가 있다.
- 전자 지갑 업데이트 결과는 즉시 받고 싶다. 하지만 CQRS 시스템에서는 요청/응답 흐름이 느릴 수 있다. 클라이언트가 디지털 직바의 업데이트 시점을 정확히 알 수 없어서 주기적 폴링에 의존해야 할 수 있기 때문이다.
- 단일 래프트 그룹의 용량은 제한되어 있다. 일정 규모 이상에서는 데이터를 샤딩하고 분산 튼랜잭션을 구현해야 한다.
3.1 풀 vs 푸시
- 풀 모델에서는 외부 사용자가 읽기 전용 상태 기계에서 주기적으로 실행 상태를 읽는다.
- 모델은 실시간이 아니며, 읽는 주기를 너무 짧게 설정하면 지갑 서비스에 과부하가 걸릴 수도 있다.
- 풀 모델은 외부 사용자와 이벤트 소싱 노드 사이에 역방향 프락시를 추가하면 개선할 수 있다.
- 외부 사용자는 역방향 프락시에 명령을 보내고, 역방향 프락시는 명령을 이벤트 소싱 노드로 전달하는 한편 주기적으로 실행 상태를 질의한다.
- 통신이 실시간으로 이루어지지 않지만, 역방향 플락시를 두고 나면 읽기 전용 상태 기계를 수정하여 응답 속도를 높일 수 있다.
- 읽기 전용 상태 기계로 하여금 이벤트를 수신하자마자 실행 상태를 역방향 프락시에 푸시하도록 하면 사용자에게 실시간으로 응답이 이루어지는 느낌을 줄 수 있다.
3.2 분산 트랜잭션
모든 이벤트 소싱 노드 그룹이 동기적 실행 모델을 채택하면 TC/C나 사가 같은 분산 트랜잭션 솔루션을 재사용할 수 있다.
최종 분산 이벤트 소싱 아키텍처에서 이체는 어떻게 이루어지는지 살펴보자
- 사용자 A가 사가 조정자에게 분산 트랜잭션을 보낸다.
=> 두 개의 연산이 들어있다. A: -1달러, C: +1달러 - 사가 조정자는 단계별 상태 테이블에 레코드를 생성하여 트랜잭션 상태를 추적한다.
- 사가 조정자는 작업 순서를 검토한 후 A: -1달러을 먼저 처리하기로 결정한다.
조정자는 A: -1달러의 명령을 계정 A 정보가 들어 있는 파티션 1로 보낸다. - 파티션 1의 래프트 리더는 A: -1달러 명령을 수신하고 명령 목록에 저장한다. 그런 다음 명령의 유효성을 검사한다. 유요하면 이벤트로 변환한다. 래프트 합의 알고리즘은 여러 노드 사이에 데이터를 동기화하기 위한 것이다. 동기화가 완료되면 이벤트(A의 계정 잔액에서 1달러를 차감하는)가 실행된다.
- 이벤트가 동기화되면 파티션 1의 이벤트 소싱 프레임워크가 CQRS를 사용하여 데이터를 읽기 경로로 동기화한다. 읽기 경로는 상태 및 싱행 상태를 재구성한다.
- 파티션의 1의 읽기 경로는 이벤트 소싱 프레임워크를 호출한 사가 조정자에 상태를 푸시한다.
- 사가 조정자는 파티션 1에서 성공 상태를 수신한다.
- 사가 조정자는 단계별 상태 테이블에 파티션 1의 작업이 성공했음을 나타내는 레코드를 생성한다.
- 첫 번째 작업이 성고했으므로 사가 조정자는 두 번째 작업인 C: +1달러를 실행한다. 조정자는 계정 C의 정보가 포함된 파티션 2에 C: +1달러 명령을 보낸다.
- 파티션 2의 래프트 리더가 C: +1달러 명령을 수신하여 명령 목록에 저장한다. 유효한 명령으로 확인되면 이벤트로 변환된다. 래프트 합의 알고리즘이 여러 노드에 데이터를 동기화한다. 동기화가 끝나면 해당 이벤트(C의 계정에 1달러를 추가하는)가 실행된다.
- 이벤트가 동기화되면 파티션 2의 이벤트 소싱 프레임워크는 CQRS를 사용하여 데이터를 읽기 경로로 동기화한다. 읽기 경로는 상태 및 실행 상태를 재구성한다.
- 파티션 2의 읽기 경로는 이벤트 소싱 프레임워크를 호출한 사가 조정자에게 사태를 푸시한다.
- 사가 조정자는 파티션 2로부터 성공 상태를 받는다.
- 사가 조정자는 단계별 상태 테이블에 파티션 2의 작업이 성공했음을 나타내는 레코드를 생성한다.
- 모든 작업이 성공하고 분산 트랜잭션이 완료된다. 사가 조정자는 호출자에게 결과를 응답한다.
4단계: 마무리
첫 번째 설계안에서는 레디스 같은 인메모리 키-값 저장소를 사용하는 솔루션을 살펴보았다.
- 이 설계의 문제점은 데이터가 내구성이 없다는 것이다.
두 번째 설계안에서는 인베모리 캐시를 트 랜잭션 데이터베이스로 바꿔보았다.
여러 노드에 거친 분산 트랜잭션을 지원하기 위한 2PC, TC/C, 사가와 같은 다양한 트랜잭션 프로토콜을 살펴보았다.
- 트랜잭션 기반 솔루션의 가장 큰 문제는 데이터 감사가 어렵다는 것이다.
다음으로는 이벤트 소싱 방안을 소개
첫 구현은 외부 데이터베이스와 큐를 사용하는 것이었는데 성능이 좋지 않다는 문제가 있다.
- 명령, 이벤트, 상태 데이터를 로컬 파일 시스템에 저장하도록 하여 성능을 개선하는 방안을 제시하였다.
- 그러나 데이터를 한곳에 두면 SPOF가 되는 문제가 잇으므로, 시스템 안전성을 높이기 위해 래프트 합의 알고리즘을 사용하여 이벤트 목록을 여러 노드에 복제하는 방안을 도입하였다.
마지막 개선 사항은 이벤트 소싱에 CQRS 개념을 도입하는 것이다.
- 외부 사용자에게 비동기 이벤트 소싱 프레임워크를 동기식 프레임워크로 제공하기 위해 역방향 프락시를 추가했다.
- TC/C 내지는 사가 프로토콜을 사용하여 여러 노드에 명령 실행을 조율하는 방법도 소개.
'읽은 책 > [책] 가상 면접 사례로 배우는 대규모 시스템 설계 기초 2권' 카테고리의 다른 글
13장. 증권 거래소 (3) | 2024.09.02 |
---|---|
9장. S3와 유사한 객체 저장소 (0) | 2024.08.28 |
11장. 결제 시스템 (0) | 2024.08.25 |
10장. 실시간 게임 순위표 (7) | 2024.08.24 |
8장. 분산 이메일 서비스 (0) | 2024.08.21 |