카테고리 없음

데이터 중심 애플리케이션 설계 (11장 스트림 처리)

보리시스템 2024. 7. 19.

[2부 분산 데이터]

11장: 스트림 처리
1. 이벤트 스트림 전송
1.1 메시징 시스템
1.2 파티셔닝된 로그

2. 데이터베이스와 스트림

2.1 시스템 동기화 유지하기
2.2 변경 데이터 캡처
2.3 이벤트 소싱

3. 스트림 처리
3.1 스트림 처리의 사용
3.2 시간에 관한 추론
3.3 스트림 조인

3.4 내결함성

 


 

11장 스트림 처리

- 일괄처리의 경우 입력을 사전에 알려진 유한크기로 한정한다는 중요한 가정이 있음
=> 즉 초기에 출력을 시작할 수 없음
=> 그래서 일괄 처리 프로세서는 인위적으로 일정 기간씩 데이터 청크를 나눠야 함

- 스트림 처리
=> 매초가 끝나는 시점에 1초 분량의 데이터를 처리하거나 고정된 시간 조각이라는 개념을 완전히 버리고 단순히 이벤트가 발생할 때마다 처리함
=> 스트림? 시간 흐름에 따라 점진적으로 생산된 데이터

- 데이터 관리 메커니즘으로서의 이벤트 스트림
=> 스트림을 표현하는 방법, 저장하는 방법, 네트워크상에서 진송하는 방법
=> 데이터베이스와 스트림에서 스트림과 데이터베이스 사이의 관계
=> 연속적으로 스트림을 처리하는 점근법, 도구를 탐색하고 이를 이용해 애플리케이선을 구축하는 방법

 


 

1. 이벤트 스트림 전송

- 스트림 처리 환경에서의 작업
=> 입력이 파일(바이트의 연속)일 때 대개 첫번째 단계로 파일을 분석해 레코드의 연속으로 바꾸는 처리를 함
=> 생산자(producer)가 이벤트를 한 번 만들면(발행자 publisher, 발송자 sender) 해당 이벤트를 복수의 소비자(consumer, 구독자 subscriber, 수신자 recipient)가 처리할 수 있음
=> 파일 시스템에서는 관련 레코드 집합을 파일이름으로 식별하지만 스트림 시스템에서는 대개 토픽이나 스트림으로 관련 이벤트를 묶음

- 알림 메커니즘
=> 이론상으로는 파일이나 데이터베이스가 있으면 생산자와 소비자를 연결하기에 충분
=> 생산자는 만들어낸 모든 이벤트를 데이터스토어에 기록하고 각 소비자는 주기적으로 데이터 스토어를 폴링해 마지막으로 처리한 이벤트 이후에 새로 발생한 이벤트가 있는지 확인
=> 폴링이 잦을수록 새로운 이벤트를 반환하는 요청 비율이 낮아지기 때문에 폴링을 수행하는 오버헤드가 키짐
=> 오히려 새로운 이벤트가 나타날때마다 소비자에게 알리는 것이 비용적으로 나음
=> 데이터베이스는 전통적으로 알림 메커니즘을 강력하게 지원하지는 않고, 트리거 기능이 있음

 


 
1.1 메시징 시스템

- 메시징 시스템
=> 새로운 이벤트에 대해 소비자에게 알려주려고 쓰이는 일반적인 방법
* 생산자는 이벤트를 포함한 메시지를 전송하면 메시지는 소비자에게 전달됨

- 구축 방법
=> 가장 간단한 방법은 생산자와 소비자 사이에 유닉스 파이프나 TCP 연결과 같은 직접 통신 채널을 사용
=> 그러나 메시징 시스템 대부분은 이 기본 모델을 확장함
=> 특히 유니스 파이프와 TCP는 전송자 하나를 정확히 수신자 하나에 연결함
=> 반면 메시징 시스템은 다수의 생산자 노드가 같은 토픽으로 메시지를 전송할 수 있고, 다수의 소비자 노드가 토픽 하나에서 메시지를 받아갈 수 있음

- 발행/구독 모델에서의 시스템들의 접근법에 대한 질문
1) 생산자가 소비자가 메시지를 처리하는 속도보다 빠르게 메시지를 전송한다면 어떻게 될까?
=> 3가지 선택지가 있음
=> 시스템은 1. 메시지를 버리거나 2. 큐에 메시지를 버퍼링하거나 3. 배압(backpressure)을 작용
* 배압? 흐름제어(low control)라고도 하며, 생산자가 메시지를 더 보내지 못하게 막는 것임

2) 노드가 죽거나 일사적으로 오프라인이 된다면 손실되는 메시지가 있을까?
* 메시지 유실의 허용여부는 애플리케이션에 따라 달라짐
=> 데이터베이스를 사용할 때처럼 지속성을 갖추려면 디스크에 기록하거나 복제본 생성을 하거나 둘 모두를 해야 함
=> 따라서 비용이 듦
=> 때로 메시지를 잃어도 괜찮다면 같은 하드웨어에서 처리량은 높이고 지연시간은 낮출 수 있음

 

  • 생산자에서 소비자로 메시지를 직접 전달하기
- 많은 메시지 시스템은 중간 노드를 통하지 않고 생산자와 소비자를 네트워크로 직접 봉신함
=> 직접 메시징 시스템은 설계 상황에서는 잘 동작하지만 일반적으로 메시지가 유실될 수 있는 가능성을 고려해서 애플리케이션 코드를 작성해야 함

- 하지만 히용 가능한 결함은 상당히 제한적임
=> 프로토콜이 네트위크 상에서 패킷 유실을 감지하고 재전송하더라도 직접 메시징 시스템은 일반적으로 생산자와 소비자가 항상 온라인 상태라고 가정함
=> 소비자가 오프라인이라면 메시지를 전달하지 못하는 상태에 있는 동안 전송된 메시지는 잃어버릴 수 있음
=> 일부 프로토콜은 실패한 메시지 전송을 생산자가 재시도하게끔 하지만 생산자 장비가 죽어버리면 재시도 하려고 했던 메시지버퍼를 잃어버릴 수 있기 때문에 문제가 있음

 

  • 메시지 브로커
- 메시징 시스템의 대안으로 메시지 브로커(메시지 큐)를 사용함
=> 메시지 브로커는 근본적으로 메시지 스트림을 처리하는 데 최적화된 데이터베이스의 일종
=> 메시지 브로커는 서버로 구동되고 생산자와 소비자는 서버의 클라이언트로 접속함
=> 생산자는 브로커로 메시지를 전송하고 소비자는 브로커에서 메시지를 읽어 전송받음

 

  • 메시지 브로커와 데이터베이스의 비교

- 보관여부
=> 데이터베이스 명시적으로 데이터가 삭제될 때까지 데이터를 보관
=> 메시지 브로커 대부분은 소비자에게 데이터 배달이 성공할 경우 자동으로 메시지를 식제(=> 오랜 기간 데이터를 저장하는 용도로는 적당하지 않음)
* 작업 집합이 상당히 작다고 가정함. 즉 큐 크기 작음. 소비자가 느려 메시지 브로커가 많은 메시지를 버퍼링해야 한다면(메시자를 메모리 안에 다 넣을 수 없으면 디스크로 내보낼 수도 있음) 개별 메시지 처리 시간이 길어지고 전체 처리랑이 저하됨

- 색인
* 메커니즘은 다르지만 둘 다 본질적으로 클라이언트가 데이터에서 필요한 부분을 선택하는 방법임
=> 데이터베이스는 보조색인을 지원하고 데이터 검색을 위한 다양한 방법을 지원
=> 메시지 브로커는 특정 패턴과 부합하는 토픽의 부분집합을 구독하는 방식을 지원

- 질의
=> 데이터베이스에 질의할 때 그 결과는 일반적으로 질의 시점의 데이터 스냅숏을 기준으로 함
* 다른 클라이언트가 이어서 질의 결과를 바꾸는 어떤 데이터를 데이터베이스에 기록한다면 첫번째 클라이언트는 다시 질의하거나 데이터 변화를 폴링하지 않는다면 앞선 결과가 기간이 지나 유효하지 않다는 점을 알 길이 없음
=> 메시지 브로커는 임의 질의를 지원하지 않지만 데이터가 변하면(즉 전달할 새로운 메시지가 생겼을 때) 클라이언트에게 알려줌

 

  • 복수 소비자
- 복수 소비자가 같은 토픽에서 메시지를 읽을 때 사용하는 주요 패턴 2가지
1) 로드밸런싱
=> 여러 소비자가 하나의 토픽을 소비하는 작업을 공유
=> 각 메시지는 소비자 중 하나로 전달됨
=> 따라서 소비자들은 해당 토픽의 메시지를 처리하는 작업을 공유
=> 브로커는 메시지를 전달할 소비자를 임의로 지정
=> 이 패턴은 메시지를 처리하는 비용이 비싸서 처리를 병렬화하기 위해 소비자를 추가
하고 싶을 때 유용
* AMQP는 같은 큐를 소비하는 클라이언트를 여러 개 둬서 로드밸런싱을 구현할 수 있음
* JMS에서는 이러한 방식을 공유구독(shared subscription)이라고 함

2) 팬 아웃
=> 각 메시지를 복수 개의 소비자로 전달

=> 각 메시지는 모든 소비자에게 전달됨
=> 여러 독립직인 소비자가 브로드캐스팅된 동일한 메시지를 간섭없이 청취할 수 있음
=> 같은 입력 파일을 읽어 여러 다른 일괄 처리 작업에서 사용하는 것과 동일
* AMQP에서는 바인딩 교환, JMS에서는 토픽 구독으로 제공됨

- 이 두 가지 패턴은 함께 사용 가능

=> 예를 들어 두 개의 소비자 그룹에서 하나의 토픽을 구독하고 각 그룹은 모든 메시지를 받지만 그룹 내에서는 각 메시지를 하나의 노드만 받게 하는 식임

 

  • 확인 응답과 재전송
- 메시지를 잃어버리지 않기 위해 메시지 브로커는 확인 응답을 사용
=> 소비자는 언제라도 장애가 발생할 수 있음
=> 브로커가 메시지를 소비자에게 전달했지만 소비자가 메시지를 처리하지 못하거나 부분적으로만 처리한 후 장애가 나는 경우가 생김
=> 클라이언트는 메시지 처리가 끝났을 때 브로커가 메시지를 큐에서 제거할 수 있게 브로커에게 명시적으로 알려야 함

- 부하 균형분산과 결합할 때 이런 재전송 행위는 흥미롭게도 메시지 순서에 영향을 미침
=> 메시지 브로커는 부하 균형 분산과 메시지 재전송을 조합하면 필연적으로 메시지 순서가 빈경됨
=> 소비자마다 독립된 큐를 사용하면(부하 균형 분산기능을 사용하지 않는다면) 이 문제를 피할 수 있음
=> 하지만 매시지간 인과성이 있다면 이것은 매우 중요한 문제임

 


 
1.2 파티셔닝된 로그

- 네트워크상에서 패킷을 전송하거나 네트워크 서비스에 요칭하는 작업은 보통 영구적 추적을 남기지 않는 일시적 연산임
=> 메시지 브로커가 메시지를 디스크에 지속성있게 기록하더라도 메시지가 소비자에게 전달된 후 즉시 삭제함
=> 브로커는 메시지를 일시적으로 보관하는 개념으로 만들어졌기 때문임

- 로그 기반 메시지 브로커(log-based message brokcr)의 기본 아이디어
=> 데이터베이스의 지속성있는 저장 방법과 메시징 시스템의 지연시간이 짧은 알림 기능을 조합

 

  • 로그를 사용한 메시지 저장소
- 로그는 단순히 디스크에 저장된 추가 전용 레코드의 연속
=> 브로커를 구현할 때도 같은 구조를 사용함
=> 생성자가 보낸 메시지는 로그 끝에 추가하고 소비자는 로그를 순차적으로 읽어 메시지를 받음
=> 소비자가 로그 끝에 도달하면 새 메시지가 추가했다는 알림을 기다림
=> 디스크 하나를 쓸 때보다 처리량을 높이기 위해 확장하는 방법으로 로그를 파티셔닝함
=> 각 파티션 내에서 브로커는 모든 메시지에 '오프셋'이라는 단조 증가하는 순번을 부여

 

  • 로그 방식과 전통적인 메시징 방식의 비교
- 로그 기반 접근법은 팬아웃메 저장 방식을 제공
=> 소비자가 서로 영향없이 독립적으로 로그를 읽을 수 있고 메시지를 읽어도 로그에서 삭제되지 않기 때문임
=>  개별 메시지를 소비자클 클라이언트에게 할당하지 않고 소비자 그룹 간 로드밸런싱하기 위해 브로키는 소비자 그룹의 노드들에게 전체 파티션을 할당할 수 있음
=> 각 클라이언트는 할당된 파티션의 메시지를 모두 소비함
=> 일반적으로 소비자에 로그 파티션이 할당되면 소비자는 단일 스레드로 파티션에서 순차적으로 메시지를 읽음

- 이런 방식의 로드밸런싱의 불리한 점

1) 토픽 하나를 소비하는 작업을 공유하는 노드 수는 많아야 해당 토픽의 로그 파티션 수로 제한됨
* 같은 파티션 내 메시지는 같은 노드로 전달되기 때문임
2) 특정 메시지 처리가 느리면 파티션 내 후속 메시지 처리가 지연됨

- 소비자 오프셋
=> 파티션 하나를 순서대로 처리하면 메시지를 어디까지 처리했는지 알기 쉬움
=> 소비자의 현재 오프셋보다 작은 오프셋을 가진 메시지는 이미 처리한 메시지고 소비자의 현재 오프셋보다 큰 오프셋을 가
진 메시지는 아직 치리하지 않은 메시지임
=> 따라서 브로커는 모든 개별 메시지마다 보내는 확인 응답을 추적할 필요가 없음
=> 단지 주기적으로 소비자 오프셋을 기록하면 됨
=> 이 방법을 이용하면 추적 오비헤드가 감소하고 일괄처리와 파이프라이닝을 수행할 수 있는 기회를 제공해 로그 기반 시스
텝의 처리량을 늘리는 데 도움을 줌

- 디스크 공간 사용
=> 로그를 계속 추가한다면 결국 디스크 공간을 전부 사용하게 됨
=> 디스크 공간을 재사용하기 위해 로그를 여러 조각으로 나누고 오래된 조각을 삭제하거나 보관 저장소로 이동함
=> 소비자 처리 속도가 느려 메시지가 생산되는 속도를 따라잡지 못하면 소비자가 너무 뒤처져 소비자 오프셋이 이미 삭제한 조각을 가리킬 수도 있음 (메시지 일부를 잃어버릴 가능성이 있음)
=> 로그는 크기가 제한된 버퍼로 구현하고 버퍼 가득 차면 오래된 메시지 순서대로 버림(원형버퍼 circularbutter 또는 링버퍼 ringbuffer)
=> 실제로 배치된 시스템이 디스크 기록 대역폭을 전부 사용하는 경우는 드묾
=> 그래서 로그는 일반적으로 하드디스크 버퍼에 수일에서 수주 간 메시지를 보관할 수 있음
=> 메시지 보관 기간과 관계없이 모든 메시지를 디스크에 기록하기 때문에 로그 처리량은 일정함

- 소비자가 생산자를 따라갈 수 없을 때
=>소비자가 메시지를 전송하는 생산자를 따라갈 수 없을 때의 선택지? 가능한 디스크 공간으로 제한된 고정 크기의 버퍼를 사용하는 버퍼링 형태
=> 소비자가 로그의 헤드로부터 얼마나 떨어졌는지 모니티링하면 눈에 띄게 뒤처지는 경우 경고할 수 있음
=> 버퍼가 커질수록 사람이 소비자 처리가 느린 문제를 고쳐 메시지를 잃기 전에 따라 잡을 시간을 충분히 벌 수 있음

 

  • 소비자 오프셋
- 파티션 하나를 순서대로 처리하면 메시지를 어디까지 처리했는지 알기 쉬움
=> 소비자의 현재 오프셋보다 작은 오프셋을 가진 메시지는 이미 처리한 메시지고 소비자의 현재 오프셋보다 큰 오프셋을 가
진 메시지는 아직 치리하지 않은 메시지임
=> 따라서 브로커는 모든 개별 메시지마다 보내는 확인 응답을 추적할 필요가 없음
=> 단지 주기적으로 소비자 오프셋을 기록하면 됨
=> 이 방법을 이용하면 추적 오비헤드가 감소하고 일괄처리와 파이프라이닝을 수행할 수 있는 기회를 제공해 로그 기반 시스
텝의 처리량을 늘리는 데 도움을 줌

 

  • 디스크 공간 사용
- 로그를 계속 추가한다면 결국 디스크 공간을 전부 사용하게 됨
=> 디스크 공간을 재사용하기 위해 로그를 여러 조각으로 나누고 오래된 조각을 삭제하거나 보관 저장소로 이동함
=> 소비자 처리 속도가 느려 메시지가 생산되는 속도를 따라잡지 못하면 소비자가 너무 뒤처져 소비자 오프셋이 이미 삭제한 조각을 가리킬 수도 있음 (메시지 일부를 잃어버릴 가능성이 있음)
=> 로그는 크기가 제한된 버퍼로 구현하고 버퍼 가득 차면 오래된 메시지 순서대로 버림(원형버퍼 circularbutter 또는 링버퍼 ringbuffer)
=> 실제로 배치된 시스템이 디스크 기록 대역폭을 전부 사용하는 경우는 드묾
=> 그래서 로그는 일반적으로 하드디스크 버퍼에 수일에서 수주 간 메시지를 보관할 수 있음
=> 메시지 보관 기간과 관계없이 모든 메시지를 디스크에 기록하기 때문에 로그 처리량은 일정함

 

  • 소비자가 생산자를 따라갈 수 없을 때
- 가능한 디스크 공간으로 제한된 고정 크기의 버퍼를 사용하는 버퍼링 형태
=> 소비자가 로그의 헤드로부터 얼마나 떨어졌는지 모니티링하면 눈에 띄게 뒤처지는 경우 경고할 수 있음
=> 버퍼가 커질수록 사람이 소비자 처리가 느린 문제를 고쳐 메시지를 잃기 전에 따라 잡을 시간을 충분히 벌 수 있음

 

  • 오래된 메시지 재생
- 로그 기반 메시지 브로커는 메시지를 소비하는 게 오히려 파일을 읽는 작입과 더 유사한데 로그를 변화시키지 않는 읽기 진용 연산이기 때문임
=> 소비자의 출력을 제외한 메시지 처리의 유일한 부수효과는 소비자 오프셋 이동
=> 하지만 소비자 오프셋은 소비자 관리 아래에 있기 때문에 필요하다면 쉽게 조작할 수 있음
=> 로그 기반 메시징과 일괄처리는 변환처리를 반복해도 입력 데이터에 영향을 전혀 주지 않고 파생 데이터를 만듦
=> 로그 기반 메시징 시스템은 많은 실험을 할 수도 있고, 오류와 버그를 복구하기 쉽기 때문에 조직 내에서 데이터 플로를 통합하는 좋은 도구임

 

2. 데이터베이스와 스트림

- 이벤트는 특정 시점에 발생한 사건을 기록한 레코드
=> 사건은 사용자 활동이나 측정 판독일 수 있지만 데이터베이스에 기록하는 것일 수도 있음
=> 데이터베이스에 뭔가를 기록한다는 사실은 캡처해서 저장하고 처리할 수 있는 이벤트임
=> 복제로그는 데이터베이스 기록 이벤트의 스트림임
=> 데이터베이스가 트랜잭션을 처리할 때 리더는 데이터베이스 기록 이벤트를 생산
=> 팔로워는 기록 스트림을 해당 데이터베이스 복제본에 기록해 완전히 동일한 데이터 복사본을 만듦
=> 복제 로그 이벤트는 데이터에 변경이 발생했음을 나타냄

 
2.1 시스템 동기화 유지하기

- 데이터베이스 전체를 덤프하는 작업이 너무 느리면 대안으로 사용하는 방법으로 이중 기록(dual write)이 있음
=> 이중기록을 사용하면 데이터가 변할 때마다 애플리케이선 코드에서 명시적으로 각 시스템에 기록함
=> 동시성 감지 메커니즘을 따로 사용하지 않으면 동시에 쓰기가 발생해도 알아차리지 못한채 한 값이 다른 값을 덮어씀

- 이중쓰기의 다른 문제는 한쪽 쓰기가 성공할때 다른쪽 쓰기는 실패할 수 있다는 점임
=> 내결합성 문제로 두 시스템 간 불일치가 발생하는 현상이 발생함

- 동시 성공 또는 동시 실패를 보장하는 방식은 원자적 커밋 문제임
=> 이 문제를 해결하는 데 비용이 많이 듦
=> 단일 리더 부재 데이터베이스 하나를 사용한다면 리더가 쓰기 순서를 결정함
=> 따라서 상태기제 복제방법은 데이터베이스 복제본 사이에서 작동함

 
2.2 변경 데이터 캡처

- 최근들어 변경 데이터 캡처(change data capture, CDC)에 관심이 높아지고 있음
=> 변경 데이터 캡처는 데이터베이스에 기록하는 모든 데이터의 변화를 관찰해 다른 시스템으로 데이터를 복제할 수 있는 형태로 추출하는 과정임
=> CDC는 데이터가 기록되자마자 변경내용을 스트림으로 제공할 수 있으면 특히 유용함

 

  • 변경 데이터 캡처의 구현
- 변경 데이터 캡치는 메시지 브로커와 동일하게 비동기 방식으로 동작함
=> 변경 데이터 캡처는 파생 데이터 시스템이 레코드 시스템의 정확한 데이터 복제본을 가지게 하기 위해 레코드 시스템에 발생하는 모든 변경사항을 파생 데이터 시스템에 반영하는 것을 보장하는 메커니즘임
* 레코드 데이터베이스 시스템은 변경사항을 커밋하기 전에 변경사항이 소비지에게 적용될 때까지 기다리지 않음

- 구현 방법
1) 변경 데이터 캡처는 본질적으로 변경사항을 캡처할 데이터베이스 하나를 리더로 하고 나머지를 팔로워로 함
=> 로그 기반 메시지 브로커는 원본 데이터베이스에서 변경 이벤트를 전송하기에 적합함
=> 로그 기반 메시지 브로커는 매시지 순서를 유지하기 때문임

2) 데이터베이스 트리거를 사용하기도 함
=> 데이터 테이블의 모든 변화를 관찰히는 트리거를 등록하고 변경 로그 테이블에 해당 항목을 추가하는 방식
=> 하지만 이 방식은 고장나기 쉽고 성능 오버헤드가 상당함

 

  • 초기 스냅숏
- 데이터베이스 스냅숏은 변경 로그의 위치나 오프셋에 대응돼야 함. 그래야 스냅숏 이후에 변경사항을 적용할 시점을 알 수 있음
=> 데이터베이스에서 발생한 모든 변경로그가 있다면 로그를 재현해서 데이터베이스의 전체 상태를 재구축할 수 있음
=> 그러나 대부분 모든 변경사항을 영구적으로 보관하는 일은 디스크 공간이 너무 많이 필요하고 모든 로그를 재생하는 작업도 너무 오래 걸림
=> 그래서 로그를 적당히 잘라야 함 
=> 일부 CDC 도구는 이런 스냅숏 기능을 내장하고 있으나 수작업으로 진행해야 하는 CDC 도구도 있음

 

  • 로그 컬렉션
- 로그 컬렉션의 원리? 저장 엔진은 주기적으로 같은 키의 로그 레코드를 찾아 중복을 제거하고 각 키에 대해 가장 최근에 갱신된 내용만 유지함로그 컬렉션
=> 로그 히스토리의 양을 제한한다면 새로운 파생 데이터 시스템을 추가할 때마다 스냅숏을 만들어야 하기 때문에 로그컴팩션(log compaction)이라는 대안이 있음
=> 컴팩션과 병합과정은 백그라운드로 실행됨
=> CDC 시스템에서 모든 변경에 기본키가 포함되게 하고 키의 모든 갱신이 해당 키의 이전 값을 교체한다면 특정 키에 대해 최신 쓰기만 유지하면 충분함

 


 
2.3 이벤트 소싱

- 이벤트 소싱
=> 도메인 주도 설계 커뮤니티에서 개발한 기법
=> 이벤트 소싱은 변경 데이터 캡처와 유사하게 애플리케이션 상태 변화를 모두 변경 이벤트 로그로 저장함
=> 변경 데이터 캡처와 가장 큰 차이점은 이 아이디어를 적용하는 추상화 레벨이 다르다는 점임
=> 이벤트 소싱을 사용하면 애플리게이션을 지속해서 개선하기가 매우 유리함. 어떤 상황이 발생한 후에 상황 파악이 쉽기 때문에 디버깅에 도움이 되고 애플리케션이 버그를 방지함

- 이벤트 로그에서 현재 상태 파생하기
=> 이벤트소싱을 사용하는 애플리케이션은 시스템에 기록한 데이터를 표현한 이벤트 로그를 가져와 사용자에게 보여주기에 적당한 애플리케이션 상태로 변환해야 함
=> 이벤트 소싱을 사용하는 애플리게이션은 일반적으로 이벤트 로그에서 파생된 현재 상태의 스냅숏을 저장하는 메커니즘이 있기 때문에 전체 로그를 반복해서 재처리할 필요는 없음
=> 하지만 이 메커니즘은 장애 발생 시 읽고 복구하는 성능을 높여주는 최적화에 불과함

- 명령과 이벤트
=> 이벤트 소싱 철학은 이벤트와 명령(사용자 요청이 처음 도착했을 때의 요청)을 구분하는 데 주의함
=> 이벤트는 생성시점에 사실(Fact)이 됨
=> 사용자가 나중에 예약을 변경하거나 취소하더라도 이전에 특정 좌석을 예약했다면 변경이나 취소는 나중에 추가된 독립적인 이벤트임
=> 이벤트 스트림 소비자는 이벤트를 거절하지 못함
=> 소비자가 이벤트를 받은 시점에는 이벤트는 이 미불변 로그의 일부분임
=> 다른 소비자도 이미 받았을 것임
=> 따라서 명령의 유효성은 이벤트가 되기 전에 동기식으로 검증해야 함 (직렬성 트랜잭션을 사용해 원자적으로 명령을 검증하고 이벤트를 발행할 수 있음)

 

  • 상태와 스트림 그리고 불변성
- 상태가 변할때마다 해당 상태는 시간이 흐름에 따라 변한 이벤트의 마지막 결과임
=> 모든 변경로그(change log)는 시간이 지남에 따라 바뀌는 상태를 나타냄
=> 변경 로그를 지속성있게 저장한다면 상태를 간단히 재생성할 수 있는 효과가 있음
=> 로그 컴팩션은 각 레코드의 최신 버전만을 보유하고 덮어쓰여진 버전은 삭제함

- 불변 이벤트의 장점
=> 추가만 하는 불변 이벤트 로그를 썼다면 문제상황의 진단과 복구가 훨씬 쉬움
=> 현재 상태보다 훨씬 많은 정보를 포함하는 장점이 있음

 

  • 동일한 이벤트 로그로 여러 가지 뷰 만들기
- 이벤트 로그로 뷰 만들기
=> 불번 이벤트 로그에서 가변 상태를 분리하면 동일한 이벤트 로그로 다른 여러 읽기 전용 뷰를 만들 수 있음
=> 기존 데이터를 새로운 방식으로 표현하는 새 기능을 추가하려면 이벤트 로그를 사용해 신규 기능용으로 분리한 읽기 최적화된 뷰를 구축할 수 있음
=> 데이터를 쓰는 형식과 읽는 형식을 분리해 다양한 읽기뷰를 허용한다면 상당한 유연성을 얻을 수 있음(명령과 질의 책임의 분리, CQRS)

- 동시성 제어
=> 이벤트 소싱과 변경데이터 캡처의 가장 큰 단점은 이벤트 로그의 소비가 대개 비둥기로 이뤄진다는 것임
=>  그래서 사용자가 로그에 이벤트를 기록하고 이어서 로그에서 파생된 뷰를 읽어도 기록한 이벤트가 아직 읽기 뷰에 반영되지 않았을 가능성이 있음
=> 해결책으로는 읽기 뷰의 갱신과 로그에 이벤트를 추가하는 작업을 동기식으로 수행하는 방법임
* 이 방법을 쓰려면 트랜잭션에서 여러 쓰기를 원자적 단위로 결합해야 하므로 이벤트 로그와 읽기 뷰를 같은 저장 시스템에 담아야 함. 다른 시스템에 있다면 분산 트랜잭션이 필요함

- 불변성의 한계
=> 이벤트 소스 모델을 사용하지 않는 많은 시스템에서도 불변성에 의존함
=> 영구적으로 모든 변화의 불변 히스토리를 유지하는 것이 가능한 정도는 데이터셋이 뒤틀리는 양에 따라 다름
=> 대부분 데이터를 추가하는 작업이고 갱신이나 삭제는 드물게 발생하는 작업부하는 불변으로 만들기 쉬움
=> 상대적으로 작은 데이터셋에서 매우 빈번히 갱신과 삭제를 하는 작업부하는 불변히 히스토리가 감당하기 힘들 정도로 커지거나 파편화 문제가 발생할 수도 있음
=> 컴백선과 가비지 컬렉션의 성능문제가 견고한 운영을 하는 데 큰 골칫거리가 되기도 함

 


 

3. 스트림 처리

- 스트림을 처리하는 방법
1) 이벤트에서 데이터를 꺼내 데이터베이스나 캐시, 검색 색인 또는 유사한 저장소 시스템에 기록하고 다른 클라이언트가 이 시스템에 해당 데이터를 질의
2) 이벤트를 사용자에 직접 보냄
3) 하나 이상의 입력 스트림을 처리해 하나 이상의 출력 스트림을 생산

 


 
3.1 스트림 처리의 사용

- 복잡한 이벤트 처리(complex event proccesing, CEP) 
=> CEP는 특정 이벤트 패턴을 검색해야 하는 애플리케이션에 특히 적합함
=> CEP는 정규표현식으로 문자열에서 특정 문자 패턴을 찾는 방식과 유사하게 스트림에서 특정 이벤트 패턴을 찾는 규칙을 규정할 수 있음

- 스트림 분석
=> CEP와 스트림 분석 사이의 경계는 불분명하지만 일반적으로 분석은 연속한 특정 이벤트 패턴을 찾는 것보다 대량의 이벤트를 집계하고 통계적 지표를 뽑는 것을 더 우선함
=> 스트림 분석 시스템은 확률적 알고리즘을 사용하기도 함

- 구체화 뷰 유지하기
=> 이벤트 소싱에서 애플리케이션 상태(일종의 구체화 뷰)는 이벤트 로그를 적용함으로써 유지됨
=> 스트림 분석 시나리오와는 달리 어떤 시간 윈도우 내의 이벤트만 고려하면 보통 충분치 않음
=> 구체화 뷰를 만들려면 잠재적으로 임의의 시간 범위에 발생한 모든 이벤트가 필요함

- 스트림 상에서 검색하기
=> 복수 이벤트로 구성된 패턴을 찾는 CEP 외에도 전문 검색 질의와 같은 복잡한 기준을 기반으로 개별 이벤트를 검색해야 하는 경우도 있음
=> 질의를 먼저 저장하고 CEP와 같이 문서는 질의를 지나가면서 실행 소스 프레임위크가 분석 용도로 실행됨

- 메시지 전달과 RPC
=> 메시지 전달 시스템을 RPC 대안으로 사용할 수 있음
=> 유사 RPC 시스템과 스트림 처리 사이에 겹치는 영역이 있음
=> 액터 프레임워크를 이용한 스트림 처리도 가능함. 하지만 액터 프레임워크는 대부분 장애 상황에서 메시지 전달을 보장하지 않기 때문에 추가적인 재시도 로직을 구현하지 않으면 처리에 내결함성을 보장하지 못함

 


 
3.2 시간에 관한 추론

- 이벤트 시간 대 처리 시간
=> 처리가 지연되는 데는 네트워크 결함, 메시지 브로커나 처리자에서 경쟁을 유발하는 성능 문제, 스트림 소비자의 재시작, 결함에서 복구하는 도중이나 코드 상의 버그를 고친 후 과거 이벤트의 재처리 등의 이유가 있음
=> 게다가 메시지가 지연되면 메시지 순서를 예측하지 못할 수도 있음

- 준비 여부 인식
=> 이벤트 시간 기준으로 윈도우를 정의할 때 발생하는 까다로운 문제는 특정 윈도우에서 모든 이벤트가 도착했다거나 아직도 이벤트가 계속 들어오고 있는지를 확신할 수 없다는 점임
=> 타임아옷을 설정하고 얼마 동안 새 이벤트가 들어오지 않으면 윈도우가 준비됐다고 선인할 수 있지만 일부 이벤트는 네트워크 중단 때문에 지연돼 다른 장비 어딘가에 버퍼링 됐을 가능성도 여전히 있음
=> 윈도우를 이미 종료한 후에 도착한 낙오자 이벤트를 처리할 방법이 필요함
=> 방법 2가지
1) 낙오자 이벤트는 무시함. 정상적인 환경에서는 낙오자 이벤트는 대체로 적은 비율을 차지하기 때문임. 놓친 이벤트의 수를 지표로 추적해 많은 양의 데이터가 누락되는 경우 경고를 보낼 수 있음
2) 수정값(낙오자 이벤트가 포함된 원도우를 기준으로 갱신된 값임)을 발행함

- 어쨌든 어떤 시계를 사용할 것인가?
=> 이벤트가 시스템의 여러 지점에 버퍼링 됐을 때 이벤트에 타임스탬프를 할당하는 것은 더 어려움
=> 이벤트의 타임스탬프는 모바일 장치 로컬 시계를 따르는 실제 사용자와 상호작용이 발생했던 실제 시각이어야 함
=> 잘못된 장치 시계를 조정하는 방법은 3가지 타임 스탬프를 로그로 남기는 것임
1) 이벤트가 발생한 시간, 장치 시계를 따름
2) 이벤트를 서버로 보낸 시간, 장치 시계를 따름
3) 서버에서 이벤트를 받은 시간, 서버 시계를 따름

- 윈도우 유형
=> 윈도우는 이벤트 수를 세거나 윈도우 내 평균값을 구하는 등 집계를 할 때 사용함
=> 일반적으로 사용하는 윈도우 유형
1) 텀블링 윈도우(Tumbling window)
=> 텀블링 윈도우의 크기는 고정 길이임
=> 모든 이벤트는 정확히 한 윈도우에 속함

2) 홉핑 윈도우(Hopping window)
=> 홈핑 윈도우도 고정길이를 사용함
=> 그러나 홈핑 윈도우는 결과를 매끄럽게 만들기 위해 윈도우를 중첩할 수 있음

3) 슬라이딩 윈도우(Sliding window)
=> 각 시간 간격 사이에서 발생한 모든 이벤트를 포함함

4) 세션 윈도우(Session window)
=> 이전 원도우 유형과는 다르게 세선 윈도우는 고정된 기간이 없음
=> 대신 같은 사용자가 짧은 시간 동안 발생시킨 모든 이벤트를 그룹화해서 세션 윈도우를 정의함
=> 그리고 일정 시간이 지나 사용자가 비활성화되면 원도우를 종료함

 


 
3.3 스트림 조인

- 스트림 스트림 조인(윈도우 조인)
=> 조인을 위한 적절한 윈도우 선택이 필요함
=> 예를 들면 1시간 이내에 발생한 검색과 클릭을 조인하는 방법을 선택할 수 있음
=>클릭 이벤트에 검색에 관한 세부 내용을 추가하는 작업은 이벤트를 조인하는 것과 동일하지 않음
=> 사용자가 클릭한 검색 결과만 알려주지 클릭하지 않은 검색 결과를 알려주지는 않음
=> 검색 품질을 측정하기 위해서는 정확한 클릭율이 필요함
=> 클릭율을 구하려면 검색이벤트와 클릭이벤트가 모두 필요함
=> 이런 유형의 조인을 구현하려면 스트림 처리자가 상태(state)를 유지해야 함

- 스트림 테이블 조인(스트림 강화)
=> 사용자 활동 이벤트를 스트림으로 간주하고 스트림 처리자에서 동일한 조인을 지속적으로 수행하는 게 자연스러움
=> 이때 입력은 사용자ID를 포함한 활동 이벤트 스트림이고 출력은 해당 ID를 가진 사용자 프로필 정보가 추가된 활동 이벤트임
=> 이 과정을 데이터베이스의 정보로 활동 이벤트를 강화한다고 함
=> 또 다른 방법은 네트워크 왕복 없이 로컬에서 질의가 가능하도록 스트림 처리자 내부에 데이티베이스 사본을 적재하는 것임
=> 데이터베이스의 로컬 사본 용량이 충분히 작으면 메모리 내 해시테이블에 넣는 것이 가능함
=> 스트림 스트림 조인과 매우 비슷하지만 가장 큰 차이점은 스트림 테이블 조인을 할 때, 테이블 변경 로그 스트림 쪽은 시작시간까지 이어지는 윈도우를 사용하며 레코드의 새 버전으로 오래된 것을 덮어쓴다는 점임
* 스트림 테이블 조인을 할 때 스트림 입력 쪽은 윈도우를 전혀 유지하지 않을 수도 있음

- 테이블 테이블 조인(구체화 뷰 유지)
=> 스트림 처리자에서 캐시 유지를 구현하려면 트윗 이벤트 스트림(전송과 삭제)과 팔로우 관계 이벤트 스트림(팔로우와 언팔로우)이 필요함
=> 스트림 처리는 새로운 트윗이 도착했을 때 어떤 타임라인을 갱신해야 하는지 알기 위해 각 사용자의 팔로우 집합이 포함된 데이터베이스를 유지해야 함
=> 스트림 처리 구현의 또 다른 방법으로는 트위과 팔로우, 두 테이블을 조인하는 질의에 대한 구체화 뷰를 유지하는 것임

- 조인의 시간 의존성
=> 위 3가지 유형은 모두 스트림 처리자가 하나의 조인 입력을 기반으로 한 특정 상태를 유지해야 하고 다른 조인 입력에서 온 메시지에 그 상태를 유지한다는 공통점이 있음
=> 상태를 유지하는 이벤트의 순서는 매우 중요함
=> 천천히 변하는 차원(slowly changing dimension, SCD) 문제는 조인되는 레코드의 특정 버전을 가리키는데 유일한 식별자를 사용해 해결함

 
3.4 내결함성

- 마이크로 일괄 처리
=> 스트림을 작은 블록으로 나누고 각 블록을 소형 일괄 처리와 같이 다루는 방법인 마이크로 일괄 처리가 있음
=> 스파크 스트리밍에서 사용함
=> 마이크로 일괄처리 크기는 일반적으로 약 1초 정도로 성능상 타협한 결과임
=> 일괄처리 크기가 작을수록 스케줄링과 코디네이션 비용이 커짐
=> 반면 일괄처리가 클수록 스트림 처리의 결과를 보기까지 지연시간이 길어짐
=> 마이크로 일괄처리는 일괄처리 크기와 같은 텀블링 윈도우를 암묵적으로 지원함
=> 아파치 플링크는 변형된 접근법을 사용하는데 주기적으로 상태의 롤링 체크포인트를 생성하고 지속성 있는 저장소에 저장함

- 원자적 커밋 재검토
=> 장애가 발생했을 때 정확히 한 번 처리되는 것처럼 보일려면 처리가 성공했을 때만 모든 출력과 이벤트 처리의 부수 효과가 발생하게 해야 함
=> 이런 효과는 원자적으로 모두 일어나거나 또는 모두 일어나지 않아야 하지만 서로 동기화가 깨지면 안됨

- 멱등성
=> 결국 목표는 처리 효과가 2번 나타나는 일 없이 안전하게 재처리하기 위해 실패한 태스크의 부분 출력을 버리는 것임
=> 분산 트랜잭션이 이 목표를 달성하는 방법이지만 그밖의 다른 방법으로 멱등성에 의존하는 방법이 있음
=> 멱등 연산은 여러번 수행하더라도 오직 한 번 수행한 것과 같은 효과를 내는 연산임
=> 연산 자체가 멱등적이지 않아도 약간의 여분 메타데이터로 연산을 멱등적으로 만들 수 있음

- 실패 후에 상태 재구축하기
=> 원도우 집계나 조인용 테이블과 색인처럼 상태가 필요한 스트림 처리는 실패 후에도 해당 상태가 복구됨을 보장해야 함
=> 원격 데이터 저장소에 상태를 유지하고 복제하는 방법이 있음
* 개별 메시지를 원격 데이터베이스에 질의하는 건 느림
=> 다른 방법으로는 스트림 처리자의 로컬에 상태를 유지하고 주기적으로 복제하는 것임
* 스트림 처리자가 실패한 작업을 복구할 때 새 태스크는 복제된 상태를 읽어 데이터 손실없이 처리를 재개할 수 있음
=> 어떤 경우에는 상태 복제가 필요 없을 수도 있음. 입력 스트림을 사용해 재구축할 수 있기 때문임
=> 그러나 이. ㅗ든 트레이드오프는 기반 인프라스트럭처의 성능 특성에 달려있음