programming6 MIN READ

[시스템 디자인] 12. 전자 지갑

[시스템 디자인] 12. 전자 지갑

시스템 디자인 시리즈의 글입니다.

전자지갑은 유저가 지갑에 돈을 넣어놓고 필요할 때 사용. 다른 사용자 지갑으로 직접 송금을 지원한다. 전자지갑 어플리케이션의 백엔드를 설계해보자.

0. 내 설계

  • 일단 DB는 무조건 RDB를 써야할듯. (결제시스템 신뢰기반, 트랜잭션기반 → 실패, 장애시 롤백이 되어야함)
API
  • GET /api/v1/balance : 잔액 조회
  • POST /api/v1/deposite : 돈을 입금함
  • POST /api/v1/withdraw : 돈을 출금함
  • 순서가 중요하니까 동기식으로 개발하지않을까
  • 부하분산 및 retry를 위해 큐 도입할수있을듯

1. 문제 이해

  • 전자 지갑 간 이체
  • 1M TPS
  • 99.99% 안정성
    • 일반 은행의 기준보다 빡빡하다
  • 트랜잭션
  • 재현성

개략적 추정

  • tps 거론 = transaction 기반 DB 사용
  • tps별 DB 벤치마크 결과
  • 일반 RDB : 1K TPS 지원한다 가정, 1M TPS위해 1K DB 필요
  • 이체명령 = 입금, 출금 연산 = 2K DB 필요

2. 개략적 설계

API 설계

  • POST /v1/wallet/balance_transfer
    • 한 지갑에서 다른 지갑으로 자금 이체
    • amount는 string 타입 추천

인메모리 샤딩

  • 지갑 어플리케이션은 모든 사용자 계정의 잔액을 유지해야함
  • <사용자, 잔액> 관계의 key-value 저장소가 good
  • redis는 좋은 선택 중 하나.
  • 클러스터 구성 및 균등 파티셔닝(샤딩) 필요
  • 파티션 주소를 저장하거나, zookeeper를 사용해 관리
  • 지갑서비스 : 이체 명령 처리를 담당
    • 이체 명령 수신
    • 이체 명령 유효성 검증 (validation)
    • 명령 유효 → 두 계정의 잔액 갱신
    • 무상태 서비스 ⇒ 수평적 확장 용이

분산 트랜잭션

  • 데이터베이스 샤딩
    • 서로 다른 DB노드를 갱신하려는 연산을 atomic하게(=원자적으로) 수행해야함
    • redis보다 RDB 노드 사용이 good
    • 서로다른 노드를 업데이트해야하는데 이 작업이 정확히 동시에 처리된다는 보장은 여전히 X
  • 2단계 커밋 (2PC)
방법
  1. 조정자가 DB에 읽기 및 쓰기 작업 수행
  2. 대상 DB 모두에 락이 걸림
  3. 대상 DB 모두에게 트랜잭션 준비 요청
  4. 조정자는 DB의 응답을 받아 다음 절차 수행
    1. 모든 DB가 ‘yes’ 응답시 트랜잭션 커밋 요청
    2. 하나라도 ‘no’ 응답시 모든 DB에 트랜잭션 중단 요청
  • 저수준방안. (데이터베이스 자체에 의존하는 방안)
  • 모든 DB가 X/Open XA 표준 만족
  • 다른 노드의 메세지 기다리는동안 락이 계속 유지됨 → 성능 저하
  • 조정자가 SPOF가 됨
  • 두 단계가 하나의 transaction
  • TC/C (Try-Confirm/Cancel)
방법
  1. 조정자가 모든 DB에 트랜잭션을 위한 자원 예약 요청
  2. 모든 DB로부터 회신을 받음
    1. 모두 ‘yes’ 으답시 DB에 작업확인 요청 = Try-Confirm
    2. 하나라도 ‘no’ 응답시 모든 DB에 취소 요청 = Try-Cancel
  • 고수준 방안. (undo 절차를 비즈니스로직으로 어플리케이션이 구현)
  • 각 단계가 별도 transaction
  • 이미 종료된 기존 트랜잭션의 효과를 되돌리기 위해, 또다른 트랜잭션을 수행함.
  • 특정 DB 종류에 구애받지 않음
  • 어플리케이션 계층의 비즈니스로직에서 세부사항을 관리하고 분산 트랜잭션의 복잡성을 처리해야함
  • 단계별 상태 테이블
    • 지갑서비스 장애시 과거 작업기록을 복구하려면?
    • 각 단계 상태정보를 트랜잭션 데이터베이스에 저장
  • 불균형 상태
    • 분산 트랜잭션 실행 도중에는 항상 데이터 불일치 발생.
    • 저수준방안 → 우리가 신경쓸필요없음
    • 고수준방안 → 우리가 직접 처리
  • 잘못된 순서 실행
    • 단계별 상태테이블에 취소명령에대한 플래그를 저장
    • 시도명려잉 도착했을때 먼저 도착한 취소명령이 있다면 실패를 반환
  • 병렬 수행 가능 ⇒ latency에 민감하다면 추천.
  • SAGA
방법
  1. 모든 연산은 순서대로 정렬. 각 연산은 독립 트랜잭션으로 실행
  2. 연산은 첫번째부터 마지막까지 순서대로 실행.
  3. 연산실패시 전체 프로세스는 실패 연산부터 역순으로 보상트랜잭션을 통해 롤백
  • MSA 에서는 사실상 표준
  • 연산 실행 순서 조율
    • 분산조율 : MSA에서 모든 서비스가 다른 서비슫의 이벤트를 구독하여 작업 수행. 탈중앙화 방식. 비동기식 통신. 내부적으로 state machine 유지.
    • 중앙 집중형 조율: 하나의 조정자가 모든 서비스가 올바른 순서로 작업 실행하도록 조율. 일반적으로 선호됨. 복잡한 상황을 잘 처리
  • 병렬수행 불가능.

이벤트 소싱

  • 배경
    • 감사를 받을 수 잇음.
    • 감사 질문 (특정시점의 잔액, 잔액의 정확성, 변경후 로직 검증)에 대해 체계적으로 답하기 위한 설계 철학 중 하나가 이벤트소싱
  • 정의
    • 명령
      • 외부에서 전달된 의도가 명확한 요청
      • 순서가 중요하여 FIFO 큐에 저장
      • 무작위성이나 I/O가 포함될 수 잇음.
      • 유효하지 않을 수 잇음
    • 이벤트
      • 명령 이행 결과
      • 검증된 사실 (실행이 끝난 상태)
      • deterministic(결정론적)임.
      • 하나의 명령으로 여러개의 이벤트가 만들어 질 수 잇음.
      • 같은 명령에 항상 동일한 이벤트가 생성되는것은 아님.
      • 명령 순서를 따라야하므로 FIFO 큐에 저장
    • 상태
      • 이벤트가 적용될 때 변경되는 내용
    • 상태 기계
      • 시스템이 상태를 가지고잇으면 상태를 특정 규칙에 따라 transistion(전환)함
      • 명령의 유효성을 검사
      • 이벤트를 생성
      • 이벤트를 적용하여 상태 갱신
      • deterministic하게 동작해야함
  • 재현성
    • DB는 특정 시점의 잔액이 얼마인지만 나타냄
    • 이벤트를 처음부터 다시 재생하면 과거 잔액 상태를 언제든 재구성 가능
    • 재현성을 갖추면 감사에 쉽게 답이 가능해짐.

CQRS (명령-질의 책임 분리)

image

  • 클라이언트는 여전히 계정의 잔액을알 수 없음
  • 상태이력 DB의 읽기전용 사본을 생성해 외부와 공유하면 가능해짐
  • 상태기록 담당하는 상태기계 1개
  • 읽기전용 상태기계 여러개 → state view를 만들고 query(질의)함
  • readonly state machine은 다양한 상태 표현을 도출
  • 조금 뒤쳐질수는 있으나 결과적 일관성을 보장

3. 상세 설계

높은 성능, 안정성, 확장성을 달성하기 위한 기술들을 알아보자

고성능 이벤트 소싱

image

  • 파일기반의 명령 및 이벤트 목록
    • 네트워크 전송시간을 줄일 수 있음
    • 로컬디스크에 추가연산만 하는것은 순차적 쓰기 연산으로 매우 빠름. HDD에서도 잘 동작
    • 최근 명령과 이벤트를 메모리에 캐시 → mmap 활용
  • 파일기반 상태
    • 잔액정보를 파일기반 RDB에 저장 (SQLite, RocksDB)
  • 스냅샷
    • 과거 특정 시점의 상태를 스냅샷으로 저장
    • 큰 binary 파일이며 일반적으로 HDFS에 저장

신뢰할 수 있는 고성능 이벤트 소싱

  • 신뢰성 분석
    • 데이터 내구성이 보장만 되면 언제든 복구 가능.
    • 상태,. 스냅샷 = 이벤트목록 재생시 언제든 복구 가능
    • 명령어 = 명령에서 이벤트를 생성하는 과정은 deterministic하지않음.
    • 이벤트 자체는 historical fact이며 immutable함
    • 이벤트 = 높은 신뢰성을 보장할 유일한 데이터
  • 합의
    • 이벤트 목록을 여러 노드에 복제해야 높은 안정성 제공 가능
    • 데이터 손실이 없어야함
    • 로그 파일 내 데이터는 모든 노드에 동일하게 있어야함
    • 합의기반 복제 방안이 적합
    • 합의기간 알고리즘(Raft Algoritm)으로 가정
    • 정족수를 만족하면 전체 시스템은 정상으로 판단하고 정상동작
    • 하나의 노드만 리더, 나머지는 팔로워가됨
  • 고신뢰성 솔루션
    • SPOF 문제 해결
    • 이벤트 소싱은 동일한 이벤트 목록에서 항상 동일한 상태가 만들어지게함

분산 이벤트 소싱

  • 전자지갑 업데이트는 빨라야하는데 CQRS 는 흐름이 느림
  • 데이터 샤딩 및 분산 트랜잭션 구현이 필요

풀 vs 푸시 CQRS

  • 풀모델
    • 외부 사용자와 이벤트 소싱 노드 사이에 reverse proxy 추가
    • reverse proxy를 놨다면 읽기전용 상태 기계를 수정하여 응답속도 높이기 가능
    • 읽기전용 상태 기계가 이벤트 수신시 바로 reverse proxy에 푸시하도록함
    • 클라이언트는 주기적으로 실행상태를 질의해 조회함
  • 푸시모델
    • 읽기 전용 상태 기계가 이벤트를 수신하자마자 실행상태를 reverse proxy에 푸시하도록함
    • 사용자에게 실시간으로 응답이 이뤄지는 느낌을 줄수있음

분산 트랜잭션

image

  • 모든 이벤트 소싱 노드 그룹이 동기적 실행 모델을 채택하면 TC/C or SAGA 솔루션을 재사용할 수 있음
  • 각 파티션마다 해당 솔루션으로 구성됨