11. 스트림 처리
- 10장에서는 일괄처리에 대해 설명했습니다.
- 일괄처리란 입력으로 파일 집합을 읽어 출력으로 새로운 파일 집합을 생성하는 기술
- 그러나, 입력을 사전에 알려진 유한한 크기로 한정한다는 중요한 가정이 있습니다.
- 현실에서는 많은 데이터가 시간에 따라 도착하기 때문에 한정적이지 않습니다.
- 일간 일괄 처리의 문제점은 입력의 변화가 하루가 지나야 반영됩니다.
- 따라서 고정된 시간 조각이라는 개념을 완전히 버리고 단순히 이벤트가 발생할 때마다 처리해야 합니다. 이가 스트림 처리의 기본 개념입니다.
아래에서는 데이터 관리 메커니즘으로 이벤트 스트림을 설명합니다.
#
이벤트 스트림 전송- 스트림 처리 문맥에서 레코드는 보통 이벤트라고 하지만 특정 시점에 일어난 사건에 대한 세부 사항을 포함하는, 작고 독립된 불변 객체라는 점에서 본질적으로 동일합니다.
- 일괄 처리에서 한 번 기록하면 여러 작업에서 읽을 수 있습니다. 스트리밍에서도 이와 비슷합니다.
- 생산자(producer)가 이벤트를 한 번 만들면 해당 이벤트를 복수의 소비자(consumer, 구독자(subscriber) 또는 수신자(recipient))가 처리할 수 있습니다.
- 파일 시스템에서는 관련 레코드 집합을 파일 이름으로 식별하지만 스트림 시스템에서는 대개 토픽(topic) 이나 스트림으로 관련 이벤트를 묶습니다.
- 이론상으로는 파일이나 데이터베이스가 있으면 생산자와 소비자를 연결하기는 충분하며지만, 지연 시간이 낮으면서 지속해서 처리하는 방식을 지향할 때 데이터스토어를 이런 용도에 맞게 설계하지 않았다면 폴링 방식은 비용이 큽니다.
- 데이터베이스는 전통적으로 알림 메커니즘을 강력하게 지원하지 않습니다.
- 관계형 데이터베이스에서는 보통 트리거(trigger) 기능이 있습니다.
- 그러나 트리거는 기능이 제한적이고 데이터베이스를 설계한 이후에 도입된 개념이며, 이벤트 알림 전달 목적으로 개발된 도구는 따로 있습니다.
#
메시징 시스템- 새로운 이벤트에 대해 소비자에게 알려주려고 쓰이는 일반적인 방법은 메시징 시스템(messaging system) 을 사용하는 것입니다.
- 생산자는 이벤트를 포함한 메시지를 전송하고, 메시지는 소비자에게 전달됩니다.
- 메시징 시스템을 구축하는 가장 간단한 방법은 생산자와 소비자 사이에 유닉스 파이프나 TCP 연결과 같은 직접 통신 채널을 사용하는 방법입니다.
- 발행/구독(publish/subscribe) 모델에서는 여러 시스템들이 다양한 접근법을 사용합니다. 아래 두 질문이 이 시스템을 구별하는데 상당히 도움이 됩니다.
- 생산자가 소비자가 메시지를 처리하는 속도보다 빠르게 메시지를 전송한다면 어떻게 될지?
- 세가지 선택지인 메시지를 버리거나, 큐에 메시지를 버퍼링하거나, 생산자를 적용합니다.
- 노드가 죽거나 일시적으로 오프라인이 된다면 어떻게 될까? 손실되는 메시지가 있을까?
- 디스크에 기록하거나 복제본 생성을 하거나, 둘 모두를 해야합니다.
- 생산자가 소비자가 메시지를 처리하는 속도보다 빠르게 메시지를 전송한다면 어떻게 될지?
- 메시지의 유실을 허용할지 말지는 애플리케이션에 따라 상당히 다릅니다.
#
생산자에서 소비자로 메시지를 직접 전달하기- 많은 메시지 시스템은 중간 노드를 통하지 않고 생산자와 소비자를 네트워크로 직접 통신합니다.
- UDP 멀티캐스트는 낮은 지연이 필수인 주식 시장과 같은 금융 산업에서 널리 사용됩니다.
- ZeroMQ 같은 브로커가 필요없는 메시징 라이브러리는 TCP 또는 IP 멀티캐스팅 상에서 발행/구독 메시징을 구현합니다.
- StatsD과 BruBeck은 네트워크 상의 모든 장비로부터 지표를 수집하고 모니터링하고 UDP 메시징을 사용합니다.
- 소비자가 네트워크에 서비스를 노출하면 생산자는 HTTP나 RPC 요청을 직접 보낼 수 있습니다.
- 직접 메시징 시스템은 설계 상호아에서는 잘동작하지만 일반적으로 메시지가 유실될 수 있는 가능성을 고려해서 애플리케이션 코드를 작성해야 합니다.
- 즉, 직접 메시징 시스템은 일반적으로 생산자와 소비자가 항상 온라인 상태라고 가정합니다.
- 소비자가 오프라인이라면 메시지를 전달하지 못하는 상태에 있는 동안 전송된 메시지는 잃어버릴 수 있습니다.
- 일부 프로토콜은 실패한 메시지 전송을 생산자가 재시도하게끔 하지만 생산자 장비가 죽어버리면 재시도하려고 했던 메시지 버퍼를 잃어버릴 수 있기 때문에 문제가 있습니다.
#
메시지 브로커- 직접 메시징 시스템의 대안으로 널리 사용되는 방법은 메시지 브로커(메시지 큐) 를 통해 메시지를 보내는 것입니다.
- 메시지 브로커는 근본적으로 메시지 스트림를 처리하는 데 최적화된 데이터베이스의 일종입니다.
- 브로커에 데이터가 모이기 때문에 이 시스템은 클라이언트의 상태 변경(접속, 접속 해제, 장애)에 쉽게 대처할 수 있습니다.
- 지속성 문제가 생산자와 소비자에서 브로커로 옮겨갔기 때문입니다.
- 큐 대기를 하면 소비자는 일반적으로 비동기로 동작합니다.
- 생산자가 메시지를 보낼 때 생산자는 브로커가 해당 메시지를 버퍼에 넣었는지만 확인하고 소비자가 메시지를 처리하기까지 기다리지 않습니다.
- 메시지를 소비자로 배달하는 것은 정해지지 않은 미래 시점이지만 때로는 큐에 백로그가 있다면 상당히 늦을 수 있습니다.
#
메시지 브로커와 데이터베이스의 비교- 어떤 메시지 브로커는 XA 또는 JTA를 이용해 2단계 커밋을 수행하기도 합니다. 메시지 브로커와 데이터베이스에는 중요한 실용적 차이가 있지만 이 특징은 데이터베이스의 속성과 상당히 비슷합니다.
- 데이터베이스는 명시적으로 데이터 삭제될 때까지 데이터를 보관합니다. 반면 메시지 브로커 대부분은 소비자에게 데이터 배달이 성공할 경우 자동으로 메시지를 삭제합니다.
- 메시지 브로커는 대부분 메시지를 빨리 지우기 때문에 작업 집합이 상당히 작다고 가정합니다. 즉 큐 크기가 작습니다.
- 데이터베이스는 보조 색인을 지원하고 데이터 검색을 위한 다양한 방법을 지원하는 반면 메시지 브로커는 특정 패턴과 부합하는 토픽의 부분 집합을 구독하는 방식을 지원합니다.
- 데이터베이스에 질의할 때 그 결과는 일반적으로 질의 시점의 데이터 스냅숏을 기준으로 합니다.
#
복수 소비자- 복수 소비자가 같은 토픽에서 메시지를 읽을 때 사용하는 주요 패턴은 두가지입니다.
- 로드밸런싱
- 각 메시지는 소비자 중 하나로 전달됩니다. 따라서 소비자들은 해당 토픽의 메시지를 처리하는 작업을 공유합니다.
- 브로커는 메시지를 전달할 소비자를 임의로 지정합니다.
- 팬 아웃
- 각 메시지는 모든 소비자에게 전달됩니다.
- 로드밸런싱
- 이 두가지 패턴은 함께 사용 가능합니다.
#
확인 응답과 재전송- 소비자들은 언제라도 장애가 발생할 수 있습니다.
- 메시지를 잃어버리지 않기 위해 메시지 브로커는 확인 응답을 사용합니다.
- 브로커가 확인 응답을 받기 전에 클라이언트로의 연결이 닫히거나 타임아웃되면 브로커는 메시지가 처리되지 않았다고 가정하고 다른 소비자에게 다시 전송합니다.
- 부하 균형 분산과 결합할 때 이런 재전송 행위는 메시지 순서에 영향을 미치게 됩니다.
- 메시지 브로커는 JMS와 AMQP 표준에서 요구하는 대로 메시지 순서를 유지하려 노력할지라도 부하 균형 분산과 메시지 재전송을 조합하면 필연적으로 메시지 순서가 변경됩니다.
- 그러나, 부하 균형 분산 기능을 사용하지 않는다면 이 문제를 피할 수 있습니다.
#
파티셔닝된 로그- 네트워크 상에서 패킷을 전송하거나 네트워크 서비스에 요청하는 작업은 보통 영구적 추적을 남기지 않는 일시적 연산입니다.
- 메시지 브로커가 메시지를 디스커에 지속성 있게 기록하더라도 메시지가 소비자에게 전달된 후 즉시 삭제합니다.
- 데이터베이스와 파일 시스템의 접근법은 이와 반대입니다.
- 일반적으로 데이터베이스나 파일에 저장하는 모든 데이터는 적어도 누군가 명시적으로 다시 삭제할 때까지는 영구적으로 보관된다고 간주합니다.
- 개념의 차이는 파생 데이터를 생성하는 방식에 큰 영향을 미칩니다.
- 메시징 시스템에 새로운 소비자를 추가하면 일반적으로 소비자를 등록한 시점 이후에 전송된 메시지부터 받기 시작합니다.
- 데이터베이스의 지속성 있는 저장 방법과 메시징 시스템의 지연 시간이 짧은 알림 기능을 조합한 것이 로그 기반 메시지 브로커(log-based message broker) 입니다.
#
로그를 사용한 메시지 저장소- 로그는 단순히 디스크에 저장된 추가 전용 레코드의 연속입니다.
- 브로커를 구현할 때도 생산자가 보낸 메시지는 로그 끝에 추가하고 소비자는 로그를 순차적으로 읽어 메시지를 받습니다.
- 디스크 하나를 쓸 때보다 처리량을 높이기 위해 확장하는 방법으로 로그를 파티셔닝 하는 방법이 있습니다.
- 다른 파티션은 다른 장비에서 서비스할 수 있습니다.
- 각 파티션 내에서 브로커는 모든 메시지에 오프셋이라고 부르는, 단조 증가하는 순번을 부여합니다.
- 파티션이 추가 전용이고 따라서 파티션 내 전체 메시지는 전체 순서가 있기 때문에 순번을 부여하는 것은 타당합니다.
- 단 다른 파티션 간 메시지의 순서는 보장하지 않습니다.
- 아파치 카프카(Apache Kafka), 아마존 키네시스 스트림(Amazon Kinesis Stream), 트위터의 분산 로그(DistributedLog)가 이런 방식으로 동작하는 로그 기반 메시지 브로커입니다.
- 구글 클라우드 Pub/Sub은 아키텍처는 비슷하지만 노출된 API는 로그 추상화가 아닌 JMS 형식입니다.
- 이런 메시지 브로커는 모든 메시지를 디스크에 저장하지만 여러 장비에 메시지를 파티셔닝해 초당 수백만 개의 메시지를 처리할 수 있고 메시지를 복제함으로써 장애에 대비할 수 있습니다.
#
로그 방식과 전통적인 메시징 방식의 비교- 로그 기반 접근법은 당연히 팬 아웃 메시징 방식을 제공합니다.
- 소비자가 서로 영향 없이 독립적으로 로그를 읽을 수 있고 메시지를 읽어도 로그에서 삭제되지 않기 때문입니다.
- 개별 메시지를 소비자 클라이언트에게 할당하지 않고 소비자 그룹 간 로드밸런싱하기 위해 브로커는 소비자 그룹의 노드들에게 전체 파티션을 할당할 수 있습니다.
- 각 클라이언트는 할당된 파티션의 메시지를 모두 소비합니다.
- 일반적으로 소비자에 로그 파티션이 할당되면 소비자는 단일 스레드로 파티션에서 순차적으로 메시지를 읽습니다.
- 이러한 거친 방식의 로드밸런싱 방법은 몇 가지 불리한 면이 있습니다.
- 토픽 하나를 소비하는 작업을 공유하는 노드 수는 많아야 해당 토픽의 로그 파티션 수로 제한됩니다. 같은 파티션 내 메시지는 같은 노드로 전달되기 때문입니다.
- 특정 메시지 처리가 느리면 파티션 내 후속 메시지 처리가 지연됩니다
- 즉, 메시지를 처리하는 비용이 비싸고 메시지 단위로 병렬화 처리하고 싶지만 메시지 순서는 그렇게 중요하지 않다면 JMS/AMQP 방식의 메시지 브로커가 적합합니다.
- 반면 처리량이 많고 메시지를 처리하는 속도가 빠르지만 메시지 순서가 중요하다면 로그 기반 접근법이 효과적입니다.
#
소비자 오프셋- 파티션 하나를 순서대로 처리하면 메시지를 어디까지 처리했는지는 알기 쉽습니다.
- 브로커는 모든 개별 메시지마다 보내는 확인 응답을 추적할 필요가 없습니다.
- 메시지 오프셋은 단일 리더 데이터베이스 복제에서 널리 쓰는 로그 순차 번호(log sequence number) 와 상당히 유사합니다.
- 데이터베이스 복제에서 팔로워가 리더와 연결이 끊어졌다가 다시 접속할 때 로그 순차 번호를 사용합니다.
- 소비자 노드에 장애가 발생하면 소비자 그룹 내 다른 노드에 장애가 발생한 소비자의 파티션을 할당하고 마지막 기록된 오프셋부터 메시지를 처리하기 시작합니다.
- 장애가 발생한 소비자가 처리했지만 아직 오프셋을 기록하지 못한 메시지가 있다면 이 메시지는 두 번 처리되는 문제가 있습니다.
#
디스크 공간 사용- 로그를 계속 추가한다면 결국 디스크 공간을 전부 사용하게 됩니다.
- 디스크 공간을 재사용하기 위해 실제로는 로그를 여러 조각으로 나누고 가끔 오래된 조각을 삭제하거나 보관 저장소로 이동합니다.
- 소비자가 처리 속도가 느려 메시지가 생산되는 속도를 따라잡지 못하면 소비자가 너무 뒤처져 소비자 오프셋이 이미 삭제한 조각을 가리킬 수 도 있습니다.
- 즉 메시지 일부를 잃어버릴 가능성이 있습니다.
- 결과적으로 로그는 크기가 제한된 버퍼로 구현하고 버퍼가 가득차면 오래된 메시지 순서대로 버립니다.
- 이러한 버퍼를 원형 버퍼(circular buffer) 또는 링 버퍼(ring buffer) 라고 합니다.
- 메시지 보관 기간과 관계없이 모든 메시지를 디스크에 기록하기 때문에 로그 처리량은 일정합니다.
- 이러한 동작은 기본적으로 메모리에 메시지를 유지하고 큐가 너무 커질 때만 디스크에 기록하는 메시징 시스템과는 반대입니다.
#
소비자가 생산자를 따라갈 수 없을 때- 소비자가 메시지를 전송하는 생산자를 따라갈 수 없을 때 선택할 수 있는 선택지 세 가지가 있습니다.
- 메시지 버리기, 버퍼링, 배압 적용하기
- 로그 기반 접근법을 이 방식으로 분류하자면 대용량이지만 고정 크기의 버퍼를 사용하는 버퍼링 형태입니다.
- 소비자가 뒤처져 필요한 메시지가 디스크에 보유한 메시지보다 오래되면 필요한 메시지는 읽을 수 없습니다.
- 그래서 브로커는 버퍼 크기를 넘는 오래된 메시지를 자연스럽게 버립니다.
- 어떤 소비자가 너무 뒤처져서 메시지를 읽기 시작해도 해당 소비자만 영향을 받고 다른 소비자들의 서비스를 망치지는 않습니다. 이는 운영상 상당한 장점입니다.
- 전통적인 메시지 브로커와 대조적입니다. 전통적 메시지 브로커는 소비자가 중단되면 그 소비자가 사용하던 큐를 삭제해줍니다. 그렇지 않으면 메모리를 계속 뺏기게 됩니다.
#
오래된 메시지 재생- AMQP와 JMS 유형의 메시지 브로커에서 메시지를 처리하고 확인 응답하는 작업은 브로커에서 메시지를 제거하기 때문에 파괴적 연산입니다. 반면 로그 기반 메시지 브로커는 메시지를 소비하는 게 오히려 파일을 읽는 작업과 더 유사한데 로그를 변화시키지 않는 읽기 전용 연산이기 때문입니다.
- 소비자의 출력을 제외한, 메시지 처리의 유일한 부수 효과는 소비자 오프셋 이동입니다.
- 이는 필요하다면 쉽게 조작할 수 있습니다.
- 위 점은 일괄 처리와 유사한 측면입니다.
- 로그 기반 메시징과 일괄 처리는 변환 처리를 반복해도 입력 데이터에 영향을 전혀 주지 않고 파생 데이터를 만듭니다.
- 로그 기반 메시징 시스템은 많은 실험을 할 수 있고 오류와 버그를 복구하기 쉽기 때문에 조직 내에서 데이터플로를 통합하는데 좋은 도구입니다.
#
데이터베이스와 스트림- 브로커와 데이터베이스는 전통적으로 전혀 다른 범주의 도구로 생각되지만 로그 기반 브로커는 데이터베이스에서 아이디어를 얻어 시징에 적용할 수 있으며 이 반대도 가능합니다. 즉, 메시징과 스트림에서 아이디어를 가져와 데이터베이스에 적용할 수 있습니다.
- 이벤트는 특정 시점에 발생한 사건을 기록한 레코드입니다.
- 사건은 측정 판독일 수도 있지만 데이터베이스에 기록하는 것일 수 도 있습니다.
- 데이터베이스와 스트림 사이의 연결점이 단지 디스크에 로그를 저장하는 물리적 저장소 이상입니다.
- 복제 로그는 데이터베이스 기록 이벤트의 스트림입니다.
- 데이터베이스가 트랜잭션을 처리할 때 리더는 데이터베이스 기록 이벤트를 생산합니다.
#
시스템 동기화 유지하기- 이 책에서 데이터 저장과 질의, 처리 요구사항을 모두 만족하는 단일 시스템은 없습니다.
- 실제로 대부분의 중요 애플리케이션이 요구사항을 만족하기 위해 몇 가지 다른 기술의 조합이 필요합니다.
- 사용자 요청에 대응하기 위한 OLTP 데이터베이스, 공통 요청의 응답 속도를 높이기 위한 캐시, 검색 질의를 다루기 위한 전문 색인, 분석용 데이터 웨어하우스가 그 예시입니다.
- 이 시스템은 각각은 데이터의 복제본을 가지고 있고 그 데이터는 목적에 맞게 최적화된 형태로 각각 저장됩니다.
- 관련이 있거나 동일한 데이터가 여러 다른 장소에서 나타나기 때문에 서로 동기화가 필 수 입니다.
- 일반적으로 데이터 웨어하우스는 벌크 로드합니다.
- 주기적으로 데이터베이스 전체를 덤프하는 작업이 너무 느리면 대안으로 사용하는 방법으로 이중 기록(dual write) 가 있습니다.
- 이중 기록을 사용하면 데이터가 변할 때마다 애플리케이션 코드에서 명시적으로 각 시스템에 기록합니다.
- 이중 기록은 몇가지 심각한 문제가 있으며, 대표적인 예시로 경쟁 조건이 있습니다.
- 이중 쓰기의 다른 문제는 한쪽 쓰기가 성공할 때 다른 쪽 쓰기는 실패할 수 있다는 점입니다.
#
변경 데이터 캡처- 최근 들어 변경 데이터 캡처(change data capture, CDC) 에 관심이 높아지고 잇습니다.
- 변경 데이터 캡처는 데이터베이스에 기록하는 모든 데이터의 변화를 관찰해 다른 시스템으로 데이터블을 복제할 수 있는 형태로 추출화는 과정입니다.
#
변경 데이터 캡처의 구현- 검색 색인과 데이터 웨어하우스에 저장된 데이터는 레코드 시스템에 저장된 데이터의 또 다른 뷰일 뿐이므로 로그 소비자를 파생 데이터 시스템이라 할 수 있습니다.
- 변경 데이터 캡처는 본질적으로 변경 사항을 캡처할 데이터베이스 하나를 리더로 하고 나머지를 팔로워로 합니다.
- 로그 기반 메시지 브로커는메시지 순서를 유지하기 때문에 원본 데이터베이스에서 변경 이벤트를 전송하기에 적합합니다.
- 변경 데이터 캡처를 구현하는데 데이터베이스 트리거를 사용하기도 합니다.
- 데이터 테이블의 모든 변화를 관찰하는 트리거를 등록하고 변경 로그 테이블에 해당 항목을 추가하는 방식입니다.
- 이 방식은 고장나기 쉽고 성능 오버헤드가 상당합니다.
- 변경 데이터 캡처는 메시지 브로커와 동일하게 비동식 방식으로 동작합니다.
- 운영상 이점이 있는 설계로 느린 소비자가 추가되도 레코드 시스템에 미치는 영향이 적습니다. 그러나, 복제 지연의 모든 문제가 발생하는 단점이 있습니다.
#
초기 스냅숏- 데이터베이스에서 발생한 모든 변경 로그가 있다면 로그를 재현해서 데이터베이스의 전체 상태를 재구축할 수 있습니다.
- 그러나 대부분 모든 변경 사항을 영구적으로 보관하는 일은 디스크 공간이 너무 많이 필요하고 모든 로그를 재생하는 작업도 너무 오래 걸립니다. 즉, 로그를 적당히 잘라야 합니다.
- 전문 색인은 예시로 들면, 전체 데이터베이스 복사본이 필요합니다. (스냅숏을 사용)
- 데이터베이스 스냅숏은 변경 로그의 위치나 오프셋에 대응돼야 합니다.
- 이를 통해 이후에 변경 사항을 적용할 시점을 알 수 잇습니다.
#
로그 컴팩션- 로그 컴팩션(log compaction) 은 주기적으로 같은 키의 로그 레코드를 찾아 중복을 제거하고 각 키에 대해 가장 최근에 갱신된 내용만 유지합니다.
- 컴팩션과 병합 과정은 백그라운드로 실행됩니다.
- 로그 구조화 저장 엔진에서 특별한 널 값(톰스톤, tombstone) 으로 갱신하는 것은 키의 삭제를 의미하고 로그 컴팩션을 수행할 때 실제로 값을 제거합니다.
- 툼스톤은 키를 덮어쓰거나 삭제하지 않는 한 영구적으로 유지됩니다.
- 로그 기반 메시지 브로커와 데이터 캡처에서도 이를 적용할 수 있습니다.
- 아파치 카프카는 로그 컴팩션 기능을 제공합니다.
#
변경 스트림용 API 지원- 최근 데이터베이스들은 기능 개선이나 리버스 엔지니어링을 통해 CDC 지원을 하기보다 점진적으로 변경 스트림을 기본 인터페이스로서 지원하기 시작했습니다.
- 카프카 커넥트(Kafka Connect)는 카프카를 광범위한 데이터 시스템용 변경 데이터 캡처 도구로 활용하기 위한 노력의 일환입니다.
- 변경 이벤트를 스트림하는 데 카프카를 사용하면 검색 색인과 같은 파생 데이터 시스템을 갱신하는데 사용 가능하고 이번 장 후반부에 설명할 스트림 처리 시스템에도 이벤트 공금이 가능합니다.
#
이벤트 소싱- 앞의 아이디어는 이벤트 소싱(event sourcing) 은 유시한 부분이 있습니다.
- 이벤트 소싱은 도메인 주도 설계(domain-driven design, DDD) 커뮤니티에서 개발한 기법입니다.
- 이벤트 소싱은 변경 데이터 캡처와 유사하게 애플리케이션 상태 변화를 모두 변경 이벤트 로그로 저장합니다. 변경 데이터 캡처와 가장 큰 차이점은 이 아이디어를 적용하는 추상화 레벨이 다르다는 점입니다.
- 변경 데이터 캡처에서 애플리케이션은 데이터베이스를 변경 가능한 방식으로 사용해 레코드를 자유롭게 갱신하고 삭제합니다.
- 이벤트 소싱에서 애플리케이션 로직은 이벤트 로그에 기록된 불변 이벤트를 기반으로 명시적으로 구축합니다.
- 이벤트 소싱은 데이터 모델링에 쓸 수 있는 강력한 기법입니다.
- 애플리케이션 관점에서 사용자의 행동을 불변 이벤트로 기록하는 방식은 변경 가능한 데이터베이스 상에서 사용자의 행동에 따른 효과를 기록하는 방식보다 훨씬 유의미합니다.
- 이벤트 소싱을 사용하면 애플리케이션을 지속해서 개선하기가 매우 유리합니다. 어떤 상황이 발생한 후 상황 파악이 쉽기 때문에 디버깅에 도움이 되고 애플리케이션 버그를 방지합니다.
#
이벤트 로그에서 현재 상태 파생하기- 이벤트 로그 그 자체로는 그렇게 유용하지 않습니다. (현재 상태가 중요하지 수정 히스토리는 중요하지 않습니다.)
- 이벤트 소싱을 사용하는 애플리케이션은 시스템에 기록한 데이터를 표현한 이벤트 로그를 가져와 사용자에게 보여주기에 적당한 애플리케이션 상태로 변환해야합니다.
- 변경 데이터 캡처와 마찬가지로 이벤트 로그를 재현하면 현재 시스템 상태를 재구성할 수 있습니다.
- 이벤트 소싱을 사용하는 애플리케이션은 일반적으로 이벤트 로그에서 파생된 현재 상태의 스냅숏을 저장하는 메커니즘이 있기 때문에 전체 로그를 반복해서 재처리할 필요는 없습니다.
#
명령과 이벤트- 이벤트 소싱 철학은 이벤트와 명령(command) 을 구분하는데 주의합니다.
- 애플리케이션은 먼저 명령이 실행한지 확인합니다. 무결성이 검증되고 명령이 승인되면 명령은 지속성 있는 불변 이벤트가 됩니다.
- 이벤트는 생성 시점에 사실(fact)가 됩니다.
- 즉, 이후에 예약을 변경하거나 취소해도 특정 시점에 이를 예약했다는 사실은 진실입니다.
- 이벤트 스트림 소비자는 이벤트를 거절하지 못합니다. 소비자가 이벤트를 받은 시점에서는 이벤트는 이미 불변 로그의 일부분이며 다 소비자도 마찬가지입니다.
- 즉, 명령의 유효성은 이벤트가 되기 전에 동기식으로 검증해야 합니다.
#
상태와 스트림 그리고 불변성- 상태가 어떻게 바뀌었든 항상 이러한 변화를 일으킨 일련의 이벤트가 있습니다.
- 모든 변경 로그(changelog) 는 시간이 지남에 따라 바뀌는 상태를 나타냅니다.
- 변경 로그를 지속성 있게 저장한다면 상태를 간단히 재생성할 수 있는 효과가 있습니다.
- 이벤트 로그를 레코드 시스템으로 생각하고 모든 변경 가능 상태를 이벤트 로그로부터 파생된 것으로 생각하면 시스템을 거치는 데이터 흐름에 관해 추론하기가 쉽습니다.
트랜잭션 로그는 데이터베이스에 적용된 모든 변경 사항을 기록합니다. 로그는 고속으로 덧붙여지고, 덫붙이기가 로그를 변경하는 유일한 방법입니다. 이러한 측면에서 데이터베이스의 내용은 로그의 최근 레코드 값을 캐시하고 있는 셈입니다. 즉, 로그가 진실입니다. 데이터베이스는 로그의 부분 집합의 캐시입니다. 캐시한 부부 집합은 로그로부터 가져온 각 레코드와 색인의 최신 값입니다.
#
불변 이벤트의 장점- 데이터베이스에 잘못된 데이터를 기록했을 때 코드가 데이터를 덮어썻다면 복구하기가 매우 어렵습니다. 추가만 하는 불변 이벤트 로그를 썼다면 문제 상황의 진단과 복구가 훨씬 쉽습니다.
- 불변 이벤트는 현재 상태보다 훨씬 많은 정보를 포함합니다.
- 예시로 장바구니에 항목을 넣었다가 제거한 경우, 현재 정보에는 없지만 분석가에게는 고객이 특정 항목을 구매하려 했다가 하지 않았다는 것을 알 수 있는 유용한 정보입니다.
#
동일한 이벤트 로그로 여러 가지 뷰 만들기- 불변 이벤트 로그에서 가변 상태를 분리하면 동일한 이벤트 로그로 다른 여러 읽기 전용 뷰를 만들 수 있습니다.
- 이는 한 스트림이 여러 소비자를 가질 때와 동일한 방식으로 작동합니다.
- 이벤트 로그에서 데이터베이스로 변환하는 명시적인 단계가 있으면 시간이 흐름에 따라 애플리케이션을 반전시키기 쉽습니다.
- 일반적으로 데이터에 어떻게 질의하고 접근하는지 신경 쓰지 않는다면 데이터 저장은 상당히 직관적인 작업입니다.
- 데이터를 쓰는 형식과 읽는 형식을 분리해 다양한 읽기 뷰를 허용한다면 상당한 유연성을 얻을 수 있습니다.
- 위 개념을 명령과 질의 책임의 분리(command query responsibility segregation, CQRS) 라 부릅니다.
- 데이터베이스와 스키마 설계의 전통적인 접근법은 데이터가 질의를 받게 될 형식과 같은 형식으로 데이터를 기록해야 한다는 잘못된 생각에 기초합니다.
- 읽기 최적화된 뷰는 데이터를 비정규화하는 것이 전적으로 합리적입니다.
#
동시성 제어- 이벤트 소싱과 변경 데이터 캡처의 가장 큰 단점은 이벤트 로그의 소비가 대개 비동기로 이뤄진다는 점입니다.
- 사용자가 로그에 이벤트를 기록하고 이어서 로그에서 파생된 뷰를 읽어도 기록한 이벤트가 아직 읽기 뷰에 반영되지 않았을 가능성이 있습니다.
- 해결책 중 하나는 읽기 뷰의 갱신과 로그에 이벤트를 추가하는 작업을 동기적으로 수행하는 방법입니다.
- 이벤트 로그로 현재 상태를 만들면 동시성 제어 측면이 단순해집니다.
- 다중 객체 트랜잭션은 단일 사용자 동작이 여러 다른 장소의 데이터를 변경해야 할 때 필요합니다.
- 이벤트를 로그에 추가하기만 하면 되며 원자적으로 만들기쉽습니다.
#
불변성의 한계- 이벤트 소스 모델을 사용하지 않는 많은 시스템에서도 불변성에 의존합니다.
- 영구적으로 모든 변화의 불변 히스토리를 유지하는 것은 데이터셋이 뒤틀리는 양에 따라 다릅니다.
- 대부분 데이터를 추가하는 작업이고 갱신이나 삭제는 드물게 발생하는 작업부하는 불변으로 만들기 쉽습니다.
- 반대로 빈번한 갱신과 삭제를 하는 작업부하는 불변 히스토리가 감당하기 힘들 정도로 커지거나 파편화 문제가 발생할 수도 있습니다.
- 성능적인 이유 외에도 데이터가 모두 불변성임에도 관리상의 이유로 데이터를 삭제할 필요가 있는 상황일 수 있습니다.
- 히스토리를 새로 쓰고 문제가 되는 데이터를 처음부터 기록하지 않았단 것 처럼 희망하는 경우가 있습니다.
- 데이토믹(Datomic)은 이 기능을 적출(exicision) 이라 부르고 포씰 버전 관리 시스템에서는 셔닝(shunning) 이라는 비슷한 개념입니다.
- 데이터를 진짜로 삭제하는 작업은 놀라울 정도로 어려우며, 많은 곳에 복제본이 남아있기 때문입니다.
#
스트림 처리- 스트림을 처리하는 방법은 크게 세가지가 있습니다.
- 이벤트에서 데이터를 꺼내 데이터베이스나 캐시, 검색 색인 또는 유사한 저장소 시스템에 기록하고 다른 클라이언트가 이 시스템에 해당 데이터를 질의합니다.
- 이벤트를 사용자에게 직접 보냅니다.
- 하나 이상의 입력 스트림을 처리해 하나 이상의 출력 스트림을 생산합니다.
- 이 챕터에서는 3번재 선택지에 대해 설명하며 스트림을 처리하는 코드 조각을 연산자(operator) 나 작업(job) 이라 부릅니다.
- 일괄 처리 작업과 가장 크게 다른 점은 스트림은 끝나지 않는다는 점입니다.
#
스트림 처리의 사용스트림 처리는 특정 상황이 발생하면 조직에 고를 해주는 모니터링 목적으로 오랜 기간 사용되어 왔습니다.
- 사기 감시 시스템은 신용카드의 사용패턴이 기대치 않게 변경되는지 확인해서 도난된 것으로 의심되면 결제를 막습니다.
- 거래 시스템은 금융 시장의 가격 변화를 감지해서 특정 규칙에 따라 거래를 실행해야 합니다.
- 제조 시스템은 공장의 기계 상태를 모니터링하다 오작동을 발견하면 문제를 빨리 규명해야 합니다.
- 군사 첩보 시스템은 잠재적 침략자의 활동을 추적해 공격 신호가 있으면 경보를 발령해야 합니다.
#
복잡한 이벤트 처리- 복잡한 이벤트 처리(complex event processing, CEP) 는 이벤트 스트림 분석용으로 개발된 방법입니다.
- CEP는 특정 이벤트 패턴을 검색해야 하는 애플리케이션에 특히 적합합니다.
- CEP 시스템은 감지할 이벤트 패턴을 설명하는데 종종 SQL 같은 고수준 선언형 질의 언어나 그래픽 사용자 인터페이스를 사용하기도 합니다.
- 질읜느 처리 엔진에 제출하고 처리 엔진은 입력 스트림을 소비해 필요한 매칭을 수행하는 상태 기계를 내부적으로 유지합니다. 해당 매치를 발견하면 엔진은 복잡한 이벤트(complex event)를 방출합니다.
- 이러한 시스템에서 질의와 데이터의 관계는 일반적 데이터베이스와 비교했을 때 반대입니다.
- 질의는 오랜 기간 저장되고 입력 스트림으로부터 들어오는 이벤트는 지속적으로 질의를 지나 흘러가면서 이벤트 패턴에 매칭되는 질의를 찾습니다.
#
스트림 분석- 스트림 처리를 사용하는 다른 영역으로 스트림 분석(analytics) 가 있습니다.
- 분석은 연속한 특정 이벤트 패턴보다는 대량의 이벤트를 집계하고 통계적 지표를 뽑는 것을 더 우선합니다.
- 특정 유형의 이벤트 빈도 측정(시간당 얼마나 자주 발생하는지)
- 특정 기간에 걸친 값의 이동 평균(rolling average) 계산
- 이전 시간 간격과 현재 통계값의 비교(추세를 감지하거나 지난 주 대비 비정상적으로 높거나 낮은 지표에 대한 경고)
- 위의 통계들은 고정된 시간 간격 기준으로 계산하며 집계 시간 간격을 윈도우(window) 라고 합니다.
- 스트림 분석 시스템은 때로 확률적 알고리즘을 사용하기도 합니다.
- 대표적인 분석 용도로 설계된 분산 스트림으로 아파치 스톰, 스파크 스트리밍, 플링크, 콩코드, 쌈자, 카프카 스트림 등이 있습니다. 호스팅 서비스로는 구글 클라우드 데이터플로, 애저 스트림 분석이 있습니다.
#
구체화 뷰 유지하기- 데이터베이스 변경에 대한 스트림은 캐시, 검색 색인, 데이터 웨어하우스 같은 파생 데이터 시스템이 원본 데이터베이스의 최신 내용을 따라잡게 하는데 쓸 수 있습니다.
- 이런 예들은 구체화 뷰를 유지하는 특별한 사례로 볼 수 있습니다.
- 데이터셋에 대한 또 다른 뷰를 만들어 효율적으로 질의할 수 있게하고 기반이 되는 데이터가 변경될 때마다 뷰를 갱신합니다.
- 이벤트 소싱에서 애플리케이션 상태는 이벤트 로그를 적용함으로써 유지됩니다.
- 구체화 뷰를 만들려면 잠재적으로 임의의 시간 범위에 발생한 모든 이벤트가 필요합니다.
- 대부분 제한된 기간의 윈도우에서 동작하는 일부 분석 지향 프레임워의 가정과 이벤트를 영원히 유지해야 할 필요성은 서로 상반되지만 이론상으로는 어떤 스트림 처리자라도 구체화 뷰를 유지하는 데 사용할 수 있습니다.
#
스트림 상에서 검색하기- 복수 이벤트로 구성된 패턴을 찾는 CEP외에도 전문 검색 질의와 같은 복잡한 기준을 기반으로 개별 이벤트를 검색해야 하는 경우도 있습니다.
- 대표적인 예시로 엘라스틱서치의 여과 기능이 있습니다.
- 스트림 검색은 질의를 먼저 저장하고, CEP와 같이 문서는 질의를 지나가면서 실행됩니다.
- 모든 질의에 대해 모든 문서를 테스트할 수 있습니다. 다만 많은 질의가 있다면 느려질 것이므로 이에 대한 최적화를 진행할 수도 있습니다.
#
메시지 전달과 RPC- 메시지 전달 시스템을 RPC 대안으로 사용할 수 있습니다. 즉, 액터 모델 등에서 쓰이는 서비스 간 통신 메커니즘으로 사용할 수 있습니다. 다만 몇가지 차이가 있습니다.
- 액터 프레임워크는 주로 동시성을 관리하고 통신 모듈을 분산 실행하는 메커니즘입니다 .반면 스트림 처리는 기본적으로 데이터 관리 기법입니다.
- 액터 간 통신은 주로 단기적이고 일대일입니다. 반면 이벤트 로그는 지속성이 있고 다중 구독이 가능합니다.
- 액터는 임의의 방식으로 통신할 수 있습니다. 그러나 스트림 처리자는 대개 비순환 파이프라인에 설정됩니다.
- 유사 RPC 시스템와 스트림 처리 사이에 겹치는 영역이 있습니다.
- ex) 아파치 스톰의 분산 RPC(distributed RPC) 기능, 이벤트 스트림을 처리하는 노드 집합에 질의를 맡기를 수 있음
- 액터 프레임워크를 이용한 스트림 처리도 가능합니다. 그러나 액터 프레임워크는 대부분 장애 상황에서 메시지 전달을 보장하지 않기 때문에 내결함성을 보장하지 못합니다.
#
시간에 관한 추론- 스트림 처리자는 종종 시간을 다뤄야할 때가 있으나 이 개념은 까다롭습니다.
- 일괄 처리에서 태스크는 과거에 쌓인 대량의 이벤트를 빠르게 처리합니다. 그러나 프로세스를 숫행하는 시간과 이벤트가 실제로 발생한 시간과는 아무 관계가 없기 때문입니다.
- 일괄 처리는 몇 분 안에 과거 이벤트 1년 치를 읽어야 할 수도 있으며 대부분의 경우는 이부분이 더 중요한 정보입니다.
- 그러나 많은 스트림 처리 프레임워크는 윈도우 시간을 결정할 때 처리하는 장비의 시스템 시계(처리 시간)을 이용합니다. 이 접근법은 간단하다는 장점이 있습니다.
- 이벤트 생성과 처리 사이의 간격이 무시할 정도로 작다면 합리적이나, 처리가 지연되면 문제가 많이 생깁니다.
#
이벤트 시간 대 처리 시간- 처리가 지연되는 데는 많은 이유가 있습니다.
- 큐 대기, 네트워크 결함, 메시지 브로커나 처리자에서 경쟁을 유발하는 성능 문제, 스트림 소비자의 재시작, 결함에서 복구하는 도중 과거 버그가 있던 이벤트의 재처리 등
- 메시지가 지연되면 메시지 순서를 예측 못할 수도 있습니다.
- 이벤트 시간과 처리 시간을 혼동하면 좋지 않은 데이터가 만들어집니다
- 즉, 실제 요청률은 안정적이지만 백로그를 처리하는 동안 요청이 비정상적으로 튀는 것처럼 보입니다.
#
준비 여부 인식- 이벤트 시간 기준으로 윈도우를 정의할 때 발생하는 까다로운 문제는 특정 윈도우에서 모든 이벤트가 도착했다거나 아직도 이벤트가 계속 들어오고 있는 지를 확신할 수 없다는 점입니다.
- 타임아웃을 설정하고 얼마 동안 새 이벤트가 들어오지 않으면 윈도우가 준비되었다고 선언할 수 있지만 일부 이벤트는 네트워크 중단 때문에 지연되어 다른 장비 어딘가에 버퍼링될 가능성도 여전히 존재합니다.
- 따라서 윈도를 이미 종료한 후에 도착한 낙오자 이벤트를 처리할 방법이 필요합니다. 크게 두가지로 구성됩니다.
- (1) 낙오자 이벤트를 무시합니다.
- (2) 수정 값을 발행합니다.
#
어쨋든 어떤 시계를 사용할 것인가- 이벤트가 시스템의 여러 지점에 버퍼링됐을 때 이벤트에 타임스탬프를 할당하는 것은 더 어렵습니다.
- 이벤트의 타임 스탬프는 모바일 로컬 시계를 따르는, 실제 사용자와 상호작용이 발생했던 실제 시각이어야 합니다.
- 그러나 이 도한 항상 신뢰하기는 어렵습니다.
- 잘못된 장비 시계를 조정하는 한 가지 방법은 세 가지 타임스탬프를 로그로 남기는 것입니다.
- 이벤트가 발생한 시간, 장치 시계를 따릅니다.
- 이벤트를 서버로 보낸 시간, 장치 시계를 따릅니다.
- 서버에서 이벤트를 받은 시간, 서버 시계를 따릅니다.
- 두번째와 세번째의 타임스탬프 차이를 구하면 장치 시계와 서비 시계 간의 오프셋을 추정할 수 있습니다.
- 이를 통해 이벤트가 실제로 발생한 시간을 추측할 수 있습니다.
- 위 문제는 스트림 처리뿐만아니라 일괄 처리에서도 동일하게 문제가 발생합니다. 다만 스트림 처리를 할 때가 시간의 흐름을 잘 알 수 있기 때문에 이 문제가 더 두드러집니다.
#
윈도우 유형일반적으로 사용되는 윈도우 유형은 다음과 같습니다.
- 텀블링 윈도우(Tumbling window)
- 크기는 고정 길이며 중복이 없습니다.
- ex) 10:03:00 ~ 10:03:59, 10:04:00 ~ 10:04:59
- 홉핑 윈도우(Hopping window)
- 고정길이를 사용하며, 중첩 가능합니다.
- ex) 5분 / 10:03:00 ~ 10:07:59, 10:04:00 ~ 10:08:59
- 슬라이딩 윈도우(Sliding window)
- 각 시간 간격 사이에 발생한 모든 이벤트 포함
- ex) 10:03:39, 10:08:12 초 이벤트는 타임스탬프가 5분 이하라서 5분 슬라이딩 윈도우에 포함됩니다.
- 세션 윈도우(Session window)
- 고정된 기간이 없습니다, 사용자 정의
#
스트림 조인- 스트림 처리는 데이터베 파이프라인을 끝이 없는 데이터셋의 증분 처리로 일반화하기 때문에 스트림에서도 조인에 대한 필요성은 정확히 동일합니다.
- 스트림 상에서 새로운 이벤트가 언제든 나타날 수 있다는 사실은 스트림 상에서 수행하는 조인보다 훨씬 어렵게 만듭니다. 조인의 유형을 크게 다음처럼 나눌 수 있습니다.
- 스트림 스트림 조인(stream-stream join)
- 스트림 테이블 조인(stream-table join)
- 테이블 테이블 조인(table-table join)
#
스트림 스트림 조인(윈도우 조인)- 웹 사이트에 검색 기능이 있고 거기서 검색된 URL의 최신 경향을 알고 싶은 경우
- 클릭율을 알아야 하는데 이는 검색 이벤트와 클릭 이벤트 모두가 필요합니다.
- 이런 유형의 조인을 구현하려면 스트림 처리자가 상태(state) 를 유지해야 합니다.
- 검색 이벤트나 클릭 이벤트가 발생할 때마다 해당 색인에 추가하고 스트림 처리자는 같은 세션 ID로 이미 도착한 다른 이벤트가 있는지 다른 색인을 확인해야 합니다.
#
스트림 테이블 조인(스트림 강화)- 앞의 내용에서 사용자 활동 이벤트 집합과 사용자 프로필 데이터베이스를 조인하는 예제가 있었습니다.
- 이 경우는 사용자 활동 이벤트를 스트림으로 간주하고 스트림 처리자에서 동일한 조인을 지속적으로 수행하는 게 자연스럽습니다.
- 입력은 사용자 ID를 포함한 활동 이벤트 스트림이고 출력은 해당 ID를 가진 사용자 프로필 정보가 추가된 활동 이벤트입니다. 이 과정을 데이터베이스의 정보로 활동 이벤트를 강화(enriching) 한다고 합니다.
- 조인을 수행하기 위해 스트림 처리는 한 번에 하나의 활동 이벤트를 대상으로 데이터베이스에서 이벤트의 사용자 ID를 찾아 활동 이벤트에 프로필 정보를 추가해야합니다.)
- 그러나 원격 질의는 느리고 데이터베이스에 과부하를 줄 위험이 있습니다.
- 또 다른 방법으로 네트워크 왕복 없이 로컬에서 질의가 가능핟로고 스트림 처리자 내부에 데이터베이스 사본을 적재하는 것입니다.
- 용량에 따라 메모리 내에 해시 테이블을 넣거나 색인을 디스크에 넣을 수도 있습니다.
- 일괄 처리 작업은 입력으로 데이터베이스의 특정 시점 스냅숏을 사용하는 반면 스트림 처리는 오랜 기간 수행하기 때문에 시간이 지나며 데이터베이스의 내용이 변할 가능성이 높습니다.
- 따라서 복사본을 최신으로 유지합니다. (변경 데이터 캡처 사용)
- 그러므로 활동 이벤트와 프로필 갱신이라는 두 개의 스트림을 조인할 수 있습니다.
- 스트림 테이블 조인은 실제로 스트림 스트림 조인과 비슷합니다.
- 가장 큰 차이점은 스트림 테이블 조인을 할 때, 테이블 변경 로그 스트림쪽은 "시작 시간"까지 이어지는 윈도우를 사요하며 레코드의 새 버전으로 오래된 것을 덮어쓴다는 점입니다.
#
테이블 테이블 조인(구체화 뷰 유지)- 트위터의 타임라인을 예시로 들면 사용자가 자신의 홈 타임라인을 볼 때 사용자가 팔로우한 사람 모두를 순회하여 최근 트윗을 찾아 병합하는 것은 너무 비용이 많이 듭니다.
- 대신 일종의 타임라인 캐시를 사용합니다. 아래의 이벤트 처리가 필요합니다.
- 사용자 u가 새로운 트윗을 보낼시 u를 팔로잉하는 모든 사용자의 타임라인에 트윗을 추가
- 사용자가 트윗을 삭제하면 모든사용자의 타임라인에서 해당 트윗을 삭제합니다.
- 사용자 u1이 사용자 u2를 팔로우하기 시작하면 u2의 최근 트윗을 u1의 타임라인에 추가합니다.
- 사용자 u1이 사용자 u2 팔로우를 취소했을 때 사용자 u2의 트윗을 사용자 u1의 타임라인에서 제거합니다.
- 스트림 처리자에서 이 캐시 유지를 구현하려면 트윗 이벤트 스트림(전송과 삭제)과 팔로우 관계 이벤트 스트림(팔로우와 언팔로우)이 필요합니다.
- 이러런 스트림 처리를 구현하는 다른 방법은 질의에 대한 구체화 뷰를 유지하는 것입니다.
인스타도 이렇던데
#
조인의 시간 의존성- 앞의 세 가지 조인 유형은 공통점이 많습니다.
- 모두 스트림 처리자가 하나의 조인 입력을 기반으로 한 특정 상태를 유지해야하고 다른 조인 입력에서 온 메시지에 그 상태를 질으히바니다.
- 상태를 유지하는 이벤트의 순서는 매우 중요합니다.
- 단일 파티션 내 이벤트 순서는 보존되지만 다른 스트림이나 다른 파티션 사이에서 순서를 보장하는 일반적인 방법은 없습니다.
- 시간 의존성은 많은 곳에서 발생합니다.
- 복수 개의 스트림에 걸친 이벤트 순서가 결정되지 않으면 조인도 비결정적입니다.
- 즉, 입력 스트림의 이벤트가 다른 식으로 배치될지도 모릅니다.
- 이 문제르르 데이터 웨어하우스에서는 천천히 변하는 차원(slowly changing dimension, SCD) 라고 합니다.
#
내결함성- 일괄 처리 프레임워크는 쉽게 결합에 대처할 수 있습니다.
- 일부 태스크가 실패할지라도 일괄 처리 작업의 결과가 아무런 문제가 없었던 작업의 결과와 동일함을 보장합니다.
- 레코드를 여러 번 처리할 수 있다는 뜻이지만 출력은 한 번만 처리된 것으로 보이며 이 원리를 정확히 한 번 시맨틱(exactly-once semantics) 이라 하지만 결과적으로 한 번(effectively-once) 라는 용어가 그 의미를 잘 설명합니다.
- 스트림 처리에서도 동일한 내결함성 문제가 발생합니다.
- 다만 출력을 노출하기 전에 태스트가 완료될 때까지 기다리는 것은 해결책으로 사용할 수 없습니다.
- 스트림은 무한하고 그래서 처리를 절대 완료할 수 없기 때문입니다.
#
마이크로 일괄 처리와 체크포인트- 한 가지 해결책은 스트림을 작은 블록으로 나누고 각 블록을 소형 일괄 처리와 같이 다루는 방법입니다.
- 이를 마이크로 일괄 처리(microbatching) 이라고 합니다.
- 일괄 처리 크기가 작을수록 스케줄링과 코디네이션 비용이 커지며, 일괄 처리가 클수록 스트림 처리의 결과를 보기까지 지연시간이 길어집니다.
- 마이크로 일괄 처리는 일괄 처리 크기와 같은 텀블링 윈도우를 암묵적으로 지원합니다.
- 아파치 플링크는 주기적으로 상태의 롤링 체크포인트를 생성하고 지속성있는 저장소에 저장합니다.
- 스트림 연산자에 장애가 발생하면 스트림 연산자는 가장 최근 체크포인트에서 재시작하고 해당 체크포인트와 장애 발생 사이의 출력을 버립니다.
- 스트림 처리 프레임워크 내에서 마이크로 일괄 처리와 체크포인트 접근법은 일괄 처리와 같이 정확히 한 번 시맨틱을 지원합니다.
- 그러나 출력이 스트림 처리자를 떠나는 경우, 실패한 일괄 처리 출력을 더이상 지울 수 없습니다. 즉, 실패한 태스크를 재시작하면 외부 부수 효과가 두 번 발생합니다.
- 따라서 마이크로 일괄 처리와 체크포인트 접근법만으로는 이 문제를 방지하기에 충분하지 않습니다.
#
원자적 커밋 재검토- 장애가 발생했을 때 정확히 한 번 처리되는 것처럼 보일려면 처리가 성공했을 때만 모든 출력와 이벤트 처리의 부수 효과가 발생하게 해야합니다.
- 원자적으로 모두 일어나거나 일어나지 않아야 하지만 서로 동기화가 깨지면 안됩니다.
#
멱등성- 목표는 처리 효과가 두 번 나타나는 일 없이 안전하게 재처리하기 위해 실패한 태스크의 부분 출력을 버리는 것입니다.
- 방법 중 하나는 멱등성(idempotence) 에 의존하는 것입니다.
- 멱등 연산은 여러 번 수행하더라도 오직 한 번 수행한 것과 같은 효과를 내는 연산입니다.
- ex) 키-값 저장소에서 하나의 키에 고정된 특정 값을 설정하는 것
- 연산 자체가 멱등적이지 않아도 약간의 여분 메타데이터로 연산을 멱등적으로 만들 수 있습니다.
- ex) 카프카로부터 메시지를 소비할 때 모든 메시지에는 영속적이고 단조 증가하는 오프셋이 있는 경우, 외부 데이터베이스에 값을 기록할 때, 마지막 그 값을 기록하라고 트리거한 메시지의 오프셋을 함께 포함한다면 이미 갱신이 적용됐는지 확인할 수 있어서 막을 수 있습니다.
- ex) 스톰의 트라이던트도 유사한 아이디어로 진행합니다.
- 처리 중인 한 노드에서 다른 노드로 장애 복구가 발생할 때 죽었다고 생각되지만 실제로는 살아있는 노드의 간섭을 방지하기 위해 펜싱이 필요합니다.
- 모든 주의 사항이 있음에도 멱등 연산은 정확히 한 번 시맨틱을 달성하는데 적은 오버헤드만 드는 효율적인 방법입니다.
#
실패 후 상태 재구축하기- 윈도우 집계(카운터나 평균, 히스토그램)나 조인용 테이블과 색인처럼 상태가 필요한 스트림처리는 실패 후에도 해당 상태가 복구됨을 보장해야 합니다.
- 한 가지 방법은 원격 데이터 저장소에 상태를 유지하는 것입니다.
- 다만 이는 개별 메시지를 원격 데이터베이스에 질의하는 것이므로 느립니다.
- 다른 방법으로는 스트림 처리자의 로컬에 상태를 유지하고 주기적으로 복제하는 것입니다.
- 즉, 스트림 처리자가 실패한 작업을 복구할 때 새 태스크는 복제된 상태를 읽어 데이터 손실 없이 처리를 재개할 수 있습니다.
- ex) 플링크는 주기적으로 연산자 상태의 스냅숏을 캡처합니다.
- 어떤 경우는 상태 복제가 필요 없을 수 있습니다. 입력 스트림을 사용해 재구축할 수 있기 때문입니다.
- 상당히 작은 크기의 윈도우를 집계해 만든 상태면 입력 이벤트를 재생해도 충분히 빠릅니다.
- 모든 트레이드오프는 기반 infrastructure의 성능 특성에 달려있습니다.
- 어떤 시스템은 디스크 접근 지연 시간보다 네트워크 지연이 더 짧고 네트워크 대역폭이 디스크 대역폭과 비슷할 수도 있습니다.
- 모든 상황을 만족하는 이상적인 트레이드 오프는 없습니다.
#
정리- 이 장에서는 이벤트 스트림을 설명, 목적, 처리 방법에 대해 이야기했습니다.
- 스트림 처리에서 메시지 브로커와 이벤트 로그는 파일 시스템과 같은 역할을 합니다.
- 두 유형의 메시지 브로커가 있습니다.
- AMQP/JMS 스타일 메시지 브로커
- 브로커는 개별 메시지를 소비자에게 할당하고 소비자는 받은 메시지를 처리하는 데 성공하면 개별 확인 응답을 보냅니다. 브로커가 확인 응답을 받은 메시지는 삭제됩니다.
- RPC와 같이 비동기 양식에 적절합니다.
- 로그 기반 메시지 브로커
- 브로커는 한 파티션 내의 모든 메시지를 동일한 소비자 노드에 할당하고 항상 같은 순서로 메시지를 전달합니다. 병렬화는 파티션을 나누는 방식을 씁니다.
- 소비자는 최근에 처리한 메시지의 오프셋을 체크포인트로 남겨 진행 상황을 추적하고 브로커는 메시지를 디스크에 유지하기 때문에 필요한 경우 뒤로 돌아가 이전 메시지를 다시 읽을 수 있습니다.
- 데이터베이스에서 사용되는 복제 로그와 로그 구조화 저장 엔진과 유사합니다. 이는 스트림 처리 시스템이 입력 스트림을 소비해 파생된 상태나 파생된 출력 스트림을 생성할 때 특히 적합합니다.
- AMQP/JMS 스타일 메시지 브로커
- 어디서 스트림이 흘러오는지에 따라 몇 가지 가능성이 있습니다.
- 사용자 활동 이벤트, 주기적인 센서 판독값, 데이터 피드 등등
- 데이버테이스에 기록하는 작업을 스트림처럼 생각하는 것이 유용합니다.
- 변경 데이터 캡처를 통해 명시적으로 이벤트 소싱을 통해 변경 로그를 캡처할 수 있습니다.
- 로그 컴팩션을 사용하면 스트림에서 데이터베이스 내용의 전체 사본을 유지할 수 있습니다.
- 데이터베이스를 스트림처럼 표현하면 여러 시스템을 손쉽게 통합하는 기회가 열립니다.
- 변경 로그를 소비해 그 로그를 파생 시스템에 적용하면 검색 색인, 캐시, 분석용 시스템과 같은 파생 데이터 시스템을 항상 최신 상태로 유지할 수 있습니다.
- 또한 처음부터 시작해 현재까지 모든 변경 로그를 소비하면 기존 데이터의 새로운 뷰를 구성하는 것도 가능합니다.
- 상태를 스트림 형태로 유지하고 메시지를 재생하는 기능은 다양한 스트림 처리 프레임워크에서 스트림을 조인하거나 내결함성을 확보하기 위한 기술의 기초입니다.
- 이벤트 패턴 검색(복잡한 이벤트 처리), 윈도우 집계 연산(스트림 분석), 파생 데이터 최신성 유지(구체화 뷰)를 포함한 스트림 처리의 목적 몇가지가 있습니다.
- 스트림 처리에서 발생하는 세 가지 유형의 조인을 구별합니다.
- 스트림 스트림 조인
- 두 입력 스트림은 활동 이벤트로 구성하고 조인 연산자는 시간 윈도우 내에 발생한 이벤트를 검색합니다.
- Ex) 같은 사용자가 취한 행동 중 시간 차가 30분 이내인 두 개의 행동 매칭
- 스트림 테이블 조인
- 한 입력 스트림은 활동 이벤트로 구성하고 다른 스트림은 데이터베이스의 변경 로그로 구성합니다.
- 변경 로그는 데이터베이스의 최신 상태의 사본을 로컬에 유지합니다.
- 조인 연산자는 각 활동 이벤트마다 데이터베이스 질의하고 조인한 데이터를 추가한 활동 이벤트를 출력하빈다.
- 테이블 테이블 조인
- 양쪽 스트림이 모두 데이터베이스의 변경 로그 입니다.
- 한 쪽의 모든 변경을 다른 쪽의 최신 상태와 조인합니다. 조인의 결과는 두 테이블을 조인한 구체화 뷰의 변경 스트림이 됩니다.
- 스트림 스트림 조인
- 스트림 처리자에서 내결함성과 정확히 한 번 시맨틱을 달성하는 기법에 대해 이야기 했습니다.
- 일괄처리처럼 실패한 태스트의 부분 출력을 버려야하나 스트림 처리는 오랜 기간 실행되고 출력을 생산하기에 모든 출력을 버리는 것은 어렵습니다.
- 대신 마이크로 일괄 처리, 체크포인팅, 트랜잭션, 멱등적 쓰기 등을 기반으로 한 세밀한 단위의 복구 메커니즘을 사용합니다.