본문 바로가기

SearchDeveloper/데이터 중심 애플리케이션 설계

[12] 데이터 시스템의 미래 (끝)

지금까지 책의 내용은 현재 존재하는 것을 설명했다면 이번 장은 미래에는 어떻게 돼야 하는지 설명한다.

지금까지 설명했던 아이디어들을 함께 모아 그걸 기반으로 미래를 고찰한다.

데이터 통합

파생 데이터 시스템과의 결합

전문 검색 색인 같은 복잡한 검색 기능이 필요하면 전문적인 정보 탐색 도구가 필요하고, 이 시스템에 사본을 유지해야 한다.

데이터 사본을 여러 저장소 시스템에 유지해야할 때 입력과 출력을 분명히 해야한다.

  • 어디서 처음 기록하는제, 원본 출처, 올바른 형식으로 어떻게 넣는지 등
  • ex. CDC 를 활용하면 색인은 전적으로 DB 에서 파생되므로 일관성이 보장된다. 근데 앺이 색인, DB에 직접 기록하게 하면 둘의 동기화가 깨질 수 있다.

→ 파생 데이터 시스템은 이벤트 로그를 기반으로 갱신하면 결정적이고 멱등성을 지녀 결함 복구가 상당히 쉬워진다.

전체 순서화

이벤트 로그의 순서 전체를 보장하기엔 어려움이 있다.

  • 대부분 순서를 결정하려면 단일 리더 노드를 통해야 하는데, 이벤트 처리량이 단일 노드의 처리량을 넘으면 파티셔닝을 해야하고, 그럼 두 이벤트의 순서가 애매해진다.
  • 내결함성을 위해 각 데이터센터마다 독립적인 리더를 두면 각 데이터센터에서 나온 이벤트는 서로 순서가 정해지지 않는다.

→ 단일 노드의 처리량 넘어서는 규모와 지리적으로 분산된 설정으로 잘 동작하는 합의 알고리즘 설계는 아직 해결되지 않았다.

인과성이 존재하는 이벤트 순서가 꼬이면?

sns 에서 친구 끊기 이벤트 직후 남은 친구들에게 방금 끊은 친구 뒷담 메시지 전송 이벤트를 날리면, 두 이벤트는 인과적인 순서가 존재한다.

해결을 위한 시도는

  • 논리적 타임스탬프 사용 - 타임스탬프라는 일련번호로 순서를 결정할 순 있지만 잘못된 순서 이벤트의 대한 처리는 따로 필요함
  • 사용자가 본 상태를 기록하고 해당 이벤트에 고유 식별자 부여 후 참조
  • 충돌 해소 알고리즘

스트림 처리와 일괄 처리

둘 다 출력 외에 다른 부수 효과가 없고 입력 데이터가 유지되기 때문에 내결함성에 도움이 되고 데이터플로가 간결해진다.

스트림 처리는 입력의 변화를 빠르게 파생 뷰에 반영할 수 있고, 일괄 처리는 누적된 과거 데이터를 재처리에 새 파생 뷰를 만들 수 있다.

람다 아키텍처

람다 아키텍처는

  • 일괄 처리의 과거 데이터 재처리 + 스트림 처리의 최근 갱신 데이터 처리의 조합
  • 입력 데이터를 불변 이벤트로서 증가만하는 데이터셋에 추가한다. (이벤트 소싱 개념처럼)
  • 두 개 다른 시스템을 병행해서 운용 (ex. 하둡 맵리듀스 일괄처리 + 스톰 스트림 처리)

하지만 저자가 생각하는 문제점은..

  • 두 개 시스템에서 같은 로직을 유지해야하는 운영상 복잡함
  • 두 출력을 병합하기 위한 복잡함
  • 과거 데이터 재처리 비용

그래서 한 시스템 내애서 일괄, 스트림 통합

  • 이벤트 스트림 다루는 로그 기반 메시지 브로커에서 과거 이벤트를 재생할 수 있다.
  • 스트림에서 일괄 처럼 실패해도 정확히 한 번 실행된 것처럼 보이기 위해 실패 태스크의 부분 출력 폐기할 수 있어야한다.

데이터베이스 언번들링

무엇인가: 데이터베이스의 다양한 구성 요소를 분리하거나 해제하는 과정

구현 방법: 분산 트랜잭션 보다 비동기 이벤트 로그로 구현하는게 더 좋다.

  • 동기 분산 트랜잭션은 지역적 결함이 대규모 장애로 이어지는 경향이 있지만
  • 비동기 이벤트 로그는 장애나 성능 저하에 잘 견디고 버퍼링도 가능

목표: 몇개의 다른 DB 를 결합해 단일 sw 보다 더 부하도 잘 견디고 성능도 좋아지게 하는 것

(elsboo: 언번들링은 분리하는건데 목표가 왜 결합하는걸까?)

데이터플로 주변 애플리케이션 설계하는 법

파생 데이터를 만들기 위한 변환 함수

  • 전문 검색 색인에선 어간 추출, 오타 교정, 동의어 등의 자연어 처리 함수
  • 머신러닝에선 모델링
  • UI 캐시 만드는 함수
  • 보조색인용 파생 함수

앺 코드와 상태 분리

  • 웹 앺은 대부분 상태 비저장 서비스로 배포
  • 상태 저장은 DB 에서
  • (상태와 파생 데이터를 같은 의미로 쓰였는진 모르겠지만) 파생 데이터의 유지는 상태 변경 순서와 내결함성이 중요하기 때문에 전통적인 비동기 메시징 시스템은 맞지 않다.

스트림 처리

  • 자주 변하는 환율을 얻기 위해
    • 마이크로서비스 접근법: 환율 서비스나 DB에 질의
    • 데이터플로 접근법: 환율 스트림을 구독해 로컬DB 에 저장해놓고 질의 → 로컬이라 훨씬 빠르고 내결함성도 강하다.

캐시

  • 모든 검색 결과를 미리 계산해놓는 건 불가능하니 가장 공통적인 질의 집합 결과만 미리 계산 해두기
  • (엘스부: ES query cache 는 filter query 만, request cache 는 size:0 만 캐싱)

웹에서 클라에게 상태 변경 알려주기

  • 기존 http 통신은 stateless 지만
  • 최신 프로토콜인 서버 전송 이벤트(EventSource API)나 웹소켓을 사용하면
  • 웹브라우저가 서버와 통신 채널을 통해 TCP 접속 유지하면서 서버가 주도적으로 메시지를 브라우저에게 보낼 수 있다.
  • 리액트 같은 최신 도구는 사용자 입력 스트림이나 서버 응답 스트림을 구독해 클라 상태를 관리하지만 기존 시스템은 이벤트 스트림을 지원 안하는 경우가 많기 때문에 재구축이 필요할 수도 있다. (이벤트 스트림은 상태 변경 전파하는데 1초 이내라 속도가 빠른게 장점임)

애플리케이션 정확성을 올리기 위한 방법

정확하고 신뢰성 있는 애플리케이션을 구축하기 위해 여러 방법이 있었다.

  • 40년 동안 트랜잭션(원자성, 격리성, 지속성) 이 있었지만 약하다. (ex. 완화된 격리 수준, p.232)
    • (elsboo 쓰기 스큐와 팬텀이 약한 이유인가?)
  • 직렬성과 원자적 커밋 - 확실하지만 비싸다. 한 데이터센터 내에서만 제공하고 확장성과 내결함성은 제한한다.
  • 그럼 확실한 방법 몇 가지를 제안할게

DB 엔드포인트 간 논증이 중요해

애플리케이션이 아무리 안전하다고 해도 애플리케이션이 정확하지 않은 데이터를 DB 에 넣는다거나 DB 에 데이터 지우는 버그가 있으면 어쩔 수 없이 정확성은 떨어진다. CDC 같은 불변 데이터를 만드는게 해결책이라고는 했지만 실수로 연산이 두 번 실행되는 경우엔 해결책이 될 수 없다.

정확히 한 번 실행

연산 중 결함이 있어 재시도를 하더라도 결함이 없었던 것과 동일한(멱등한) 결과를 얻기 위해 계산을 조정하는 것이다. (ex. count 연산자를 결함으로 인해 2번 실행되도 1번만 카운트 될 수 있도록)

이를 보장하기 위한 중복 제거 패턴이 있다.

① TCP 는 전송 장애나면 재전송하고 중복 패킷 제거 → DB 로부터 트랜잭션 커밋 여부 응답을 받지 못한 경우엔 트랜잭션이 재시도 되는 경우는 막지 못함

<비멱등 전송 트랜잭션>

BEGIN TRANSACTION;
UPDATE accounts SET balance = balance + 11.00 WHERE account_id = 1234;
UPDATE accounts SET balance = balance - 11.00 WHERE account_id = 4321;
COMMIT;

② 2단계 커밋을 사용하면 중복 트랜잭션은 억제 할 수 있다 → 사용자와 애플리케이션 네트워크가 불안정해서 요청을 정말로 2번 보내는 경우는 막지 못함

③ 최종 해결책: 연산 식별자

UUID 같은 연산 고유 식별자를 요청할 때 같이 보내서 중복 식별자가 또 들어오면 처리하지 못하게 막아버린다

ALTER TABLE requests ADD UNIQUE (request_id);
BEGIN TRANSACTION;

INSERT INTO requests
(request_id, from_account, to_account, amount)
VALUES('0286FDB8-D7E1-423F-B40B-792B3608036C', 4321, 1234, 11.00);

UPDATE accounts SET balance = balance + 11.00 WHERE account_id = 1234;
UPDATE accounts SET balance = balance - 11.00 WHERE account_id = 4321;
COMMIT;

request_id 가 고유키여서 동일한 키로 인서트 요청들어오면 막아버린다.

연산식별자 같은 종단 간 논증은 데이터 무결성 검사나 암호화에도 적용할 수 있다.

애플리케이션 <-> DB 처리에 결함이 있어 재시도를 해도 결함 없던것처럼 멱등한 결과를 얻기 위한(=정확히 한 번실행) 해결 단계

1. TCP 단에서 전송 장애나면 재전송하고 중복 패킷 제거 -> 트랜잭션 재시도 되는 경우는 못 막음
2. 2단계 커밋 사용 -> 사용자와 앺 간 네트워크 장애 나서 실제 2번 시도하는 경우는 못 막음
3. 최종 해결: UUID 같은 연산 고유 식별자를 요청할 때 같이 보내서 중복 식별자가 또 들어오면 처리하지 못하게 막아버린다

제약조건 강제하기

계정 정보에서 ID 는 유일해야 하고 극장에서 두 사람이 같은 자리를 예약할 수 없게 하려면 유일성에 대한 제약 조건이 필요하다.

유일성 제약 조건을 강제하려면 합의가 필요하다. - 중복 연산이 생기면 하나만 수용하고 나머지는 버려야함

  • 단일 노드를 리더로 만들고 모든 결정을 리더에서 하게 하는 방법 → 리더 장애가 나면 다시 합의 문제로 돌아가야함
  • 유일성이 필요한 값 기준으로 파티셔닝 → 로그 기반 메시징

로그 기반 메시징

로그는 모든 소비자가 동일한 순서로 메시지를 보도록 보장한다. (=전체 순서 브로드캐스트, total order broadcast)

스트림 처리자는 단일 스레드에서 한 로그 파티션의 메시지를 순차적으로 소비하므로, 유일성이 필요한 값 기준으로 파티셔닝하면 어떤 로그가 처음으로 들어왔는지 파악할 수 있다.

예시) 여러 사용자가 같은 사용자명을 쓰고자 함

  1. 사용자명의 해시값을 기준으로 파티셔닝
  2. 스트림 처리자는 로그 읽고 로컬 DB 조회해서 이미 있는 사용자명인지 확인 후 성공 / 거부 메시지를 출력 스트림에 방출
  3. 클라는 성공 / 거부 메시지 오길 기다림

근본 원리
: 충돌이 발생할 수 있는 쓰기를 같은 파티션으로 라우팅하고 순서대로 처리한다.

적시성과 무결성

일관성은 적시성과 무결성이 합쳐졌다고 생각한다.

  • 적시성(Timeliness): 사용자가 항상 시스템의 최신 상태를 볼 수 있어야함
  • 무결성(Integrity): 데이터에 손상이 없어야 함 (무결성을 위반하는게 더 치명적)

트랜잭션과 스트림을 비교하자면, 트랜잭션은 적시성, 무결성을 모두 보장하지만 스트림은 무결성만 보장한다.

왜?

  • 트랜잭션은 선형성이 있어 트랜잭션 커밋이 완료된 후에 쓴 내용을 볼 수 있음 → 항상 최신 상태로 관측 가능
  • 스트림 처리는 설계상 비동기식이기 때문에 사용자가 완료될 때까지 기다리지 않고 바로 볼 수 있음 → 최신 상태를 보지 않을 수 도 있음 → 적시성을 보장하지는 않음
  • 스트림의 정확히 한 번, 결과적으로 한 번 실행은 무결성을 보존하는 매커니즘. 다음과 같은 방법으로 무결성을 보장함
    • 쓰기 연산을 단일 메시지로 표현함. 원자적으로 기록하기 쉽고 이벤트 소싱에도 잘 맞다.
    • 파생함수를 통해 모든 시스템에 상태 갱신
    • 클라의 요청 ID 를 활용. 중복 억제와 멱등성 보장한다.
    • 메시지를 불변으로 만들고 파생 데이터 재처리. 멱등한 결과를 낼 수 있다.

제약조건의 완화

비즈니스 맥락에서 꼭 유일성을 강제하지 않아도 해결할 수 있는 방법이 있다.

  • 두 사용자가 동시에 좌석 예약하려하면 한 사용자에게 다른 거 고르라고 부탁함
  • 있는 재고보다 주문이 더 되버리면 사과하면서 할인 쿠폰 제공
  • 항공사에서 일부러 한 자리에 두 예약 받음
  • 계좌 잔고보다 더 출금을 많이하면 그대로 두고 나중에 빚 갚으라고 함

→ 최신데이터를 보게 하는 적시성은 보장하지 않지만 무결성은 반드시 보장

책 후기

10쪽을 읽고 정리하는데 2시간이 걸릴 정도로 이해하는데 오랜 시간이 걸렸다.

카프카, 엘라스틱서치 처럼 데이터 처리 시스템을 한 두개 정도 익힌 상태에서 책을 읽는게 좋은 것 같다. 이론을 대입하고 상상할 수 있어 책의 내용이 좀 더 가시화되는 느낌이기 때문이다.

23년 4월 부터 9월까지 완독하는데 5개월이나 걸렸다.

다른 데이터 시스템을 공부할 때 이 책의 내용들이 배경 지식이 되어 도움이 됐으면 좋겠다.

 

레퍼런스

데이터 중심 애플리케이션 설계 12장

'SearchDeveloper > 데이터 중심 애플리케이션 설계' 카테고리의 다른 글

[11] 스트림 처리  (1) 2023.09.26
[10] 일괄 처리  (0) 2023.08.14
[9] 일관성과 합의  (0) 2023.08.10
[8] 분산 시스템의 골칫거리 3가지  (0) 2023.08.06
[7] 트랜잭션  (0) 2023.07.22