RDBMS를 이벤트 소싱 스토리지로 사용


119

RDBMS (예 : SQL Server)를 사용하여 이벤트 소싱 데이터를 저장하는 경우 스키마는 어떻게 생겼을까 요?

나는 추상적 인 의미에서 몇 가지 변이를 보았지만 구체적인 것은 아니다.

예를 들어 "제품"엔터티가 있고 해당 제품에 대한 변경 사항은 가격, 비용 및 설명의 형태로 제공 될 수 있습니다. 내가 다음과 같은지에 대해 혼란 스럽습니다.

  1. 제품에 대한 모든 필드가 포함 된 "ProductEvent"테이블이 있습니다. 여기서 각 변경은 해당 테이블의 새 레코드를 의미하고 "누가, 무엇을, 어디서, 왜, 언제, 어떻게"(WWWWWH)를 적절하게 의미합니다. 비용, 가격 또는 설명이 변경되면 제품을 나타내는 완전히 새로운 행이 추가됩니다.
  2. 외래 키 관계를 사용하여 제품 테이블에 조인 된 별도의 테이블에 제품 비용, 가격 및 설명을 저장합니다. 이러한 속성이 변경되면 적절하게 WWWWWH로 새 행을 작성합니다.
  3. WWWWWH와 이벤트를 나타내는 직렬화 된 개체를 "ProductEvent"테이블에 저장합니다. 즉, 지정된 제품에 대한 응용 프로그램 상태를 다시 빌드하려면 이벤트 자체를 내 응용 프로그램 코드에서로드, 역 직렬화 및 재생해야합니다. .

특히 위의 옵션 2에 대해 걱정합니다. 극단적으로 생각해 보면 제품 테이블은 속성 당 거의 하나의 테이블이 될 것입니다. 여기서 주어진 제품에 대한 애플리케이션 상태를로드하려면 각 제품 이벤트 테이블에서 해당 제품에 대한 모든 이벤트를로드해야합니다. 이 테이블 폭발은 나에게 잘못된 냄새가 난다.

나는 "상황에 따라 다르다"고 확신하고 "정답"은 하나도 없지만 받아 들일 수있는 것과 완전히 받아 들일 수없는 것에 대한 느낌을 얻으려고 노력하고 있습니다. 또한 NoSQL이 여기에서 도움이 될 수 있음을 알고 있습니다. 여기서 이벤트는 집계 루트에 대해 저장 될 수 있습니다. 즉, 개체를 다시 빌드 할 이벤트를 가져 오기 위해 데이터베이스에 대한 단일 요청 만 수행 할 수 있습니다. 순간 그래서 나는 대안을 찾고 있습니다.


2
가장 간단한 형식 : [Event] {AggregateId, AggregateVersion, EventPayload}. 집계 유형은 필요하지 않지만 선택적으로 저장할 수 있습니다. 이벤트 유형은 필요하지 않지만 선택적으로 저장할 수 있습니다. 그것은 일어난 일들의 긴 목록이고, 다른 것은 단지 최적화 일뿐입니다.
Yves Reynhout

7
# 1과 # 2에서 확실히 멀리 떨어져 있습니다. 모든 것을 blob으로 직렬화하고 그런 방식으로 저장합니다.
조나단 올리버

답변:


109

이벤트 저장소는 이벤트의 특정 필드 또는 속성에 대해 알 필요가 없습니다. 그렇지 않으면 모델을 수정할 때마다 데이터베이스를 마이그레이션해야합니다 (좋은 구식 상태 기반 지속성에서와 마찬가지로). 따라서 옵션 1과 2는 전혀 권장하지 않습니다.

다음은 Ncqrs 에서 사용되는 스키마 입니다. 보시다시피 "Events"테이블은 관련 데이터를 CLOB (예 : JSON 또는 XML)로 저장합니다. 이는 옵션 3에 해당합니다 (일반 "이벤트"테이블이 하나만 필요하기 때문에 "ProductEvents"테이블이 없습니다. Ncqrs에서 집계 루트에 대한 매핑은 "EventSources"테이블을 통해 발생합니다. 여기서 각 EventSource는 실제 집계 루트.)

Table Events:
    Id [uniqueidentifier] NOT NULL,
    TimeStamp [datetime] NOT NULL,

    Name [varchar](max) NOT NULL,
    Version [varchar](max) NOT NULL,

    EventSourceId [uniqueidentifier] NOT NULL,
    Sequence [bigint], 

    Data [nvarchar](max) NOT NULL

Table EventSources:
    Id [uniqueidentifier] NOT NULL, 
    Type [nvarchar](255) NOT NULL, 
    Version [int] NOT NULL

Jonathan Oliver의 Event Store 구현의 SQL 지속성 메커니즘 은 기본적으로 BLOB 필드 "Payload"가있는 "커밋"이라는 하나의 테이블로 구성됩니다. 이것은 Ncqrs에서와 거의 동일하지만 이벤트의 속성을 바이너리 형식으로 직렬화한다는 점만 있습니다 (예를 들어 암호화 지원을 추가 함).

Greg Young 은 Greg의 웹 사이트에 광범위하게 문서화 된 유사한 접근 방식을 권장합니다 .

그의 프로토 타입 "이벤트"테이블의 스키마는 다음과 같습니다.

Table Events
    AggregateId [Guid],
    Data [Blob],
    SequenceNumber [Long],
    Version [Int]

9
좋은 대답입니다! EventSourcing을 사용하기 위해 계속 읽고있는 주요 주장 중 하나는 기록을 쿼리하는 기능입니다. 모든 관심 데이터가 XML 또는 JSON으로 직렬화 될 때 쿼리에 효율적인보고 도구를 만들려면 어떻게해야합니까? 테이블 기반 솔루션을 찾고있는 흥미로운 기사가 ​​있습니까?
Marijn Huizendveld

11
@MarijnHuizendveld 아마도 이벤트 저장소 자체에 대해 쿼리하고 싶지 않을 것입니다. 가장 일반적인 솔루션은 이벤트를보고 또는 BI 데이터베이스에 프로젝션하는 두 개의 이벤트 핸들러를 연결하는 것입니다. 이러한 핸들러에 대해 이벤트 히스토리를 재생합니다.
Dennis Traub

1
@Denis Traub 귀하의 답변에 감사드립니다. 이벤트 저장소 자체에 대해 쿼리하지 않는 이유는 무엇입니까? 새로운 BI 사례가 나올 때마다 전체 이력을 다시 재생해야한다면 상당히 지저분 해지고 강렬 해 질까 봐 걱정됩니다.
Marijn Huizendveld

1
모델의 데이터를 최신 상태로 저장하기 위해 이벤트 스토어 외에 테이블도 있어야한다고 생각했습니다. 그리고 모델을 읽기 모델과 쓰기 모델로 분할했습니다. 쓰기 모델은 이벤트 저장소에 반대되고 이벤트 저장소 martials는 읽기 모델로 업데이트됩니다. 읽기 모델에는 시스템의 엔터티를 나타내는 테이블이 포함되어 있으므로 읽기 모델을 사용하여보고 및보기를 수행 할 수 있습니다. 뭔가 오해 한 게 틀림 없어요.
theBoringCoder

10
@theBoringCoder Event Sourcing과 CQRS가 혼란 스럽거나 적어도 머릿속에 으 깨진 것 같습니다. 그들은 자주 함께 발견되지만 같은 것이 아닙니다. CQRS를 사용하면 읽기 및 쓰기 모델을 분리하고 Event Sourcing을 사용하면 애플리케이션의 단일 소스로 이벤트 스트림을 사용할 수 있습니다.
브라이언 앤더슨

7

GitHub 프로젝트 CQRS.NET 에는 몇 가지 다른 기술로 EventStores를 수행 할 수있는 방법에 대한 몇 가지 구체적인 예가 있습니다. 글을 쓰는 시점에 Linq2SQL 을 사용하는 SQL 과 함께 사용할 SQL 스키마 가 있습니다. 하나는 MongoDB , 하나는 DocumentDB (Azure에있는 경우 CosmosDB) 및 EventStore를 사용하는 하나 (위에서 언급 한대로)입니다. 플랫 파일 스토리지와 매우 유사한 Table Storage 및 Blob Storage와 같은 Azure에는 더 많은 것이 있습니다.

여기서 요점은 모두 동일한 원칙 / 계약을 준수한다는 것입니다. 이들은 모두 단일 장소 / 컨테이너 / 테이블에 정보를 저장하고, 메타 데이터를 사용하여 한 이벤트를 다른 이벤트와 식별하고 전체 이벤트를 그대로 '그냥'저장합니다. 일부 경우에는 지원 기술에서 그대로 유지됩니다. 따라서 문서 데이터베이스, 관계형 데이터베이스 또는 플랫 파일을 선택하는지 여부에 따라 이벤트 저장소의 동일한 의도에 모두 도달하는 여러 가지 방법이 있습니다 (언제든지 마음이 바뀌고 마이그레이션하거나 지원해야하는 경우 유용합니다. 둘 이상의 스토리지 기술).

프로젝트 개발자로서 우리가 선택한 몇 가지 통찰력을 공유 할 수 있습니다.

첫째로 우리는 여러 가지 이유로 (정수 대신 고유 한 UUID / GUID를 사용하더라도) 전략적인 이유로 순차 ID가 발생하므로 ID가 키에 대해 충분히 고유하지 않았으므로 기본 ID 키 열을 데이터 / 개체 유형을 사용하여 실제 (응용 프로그램의 의미에서) 고유 한 키 여야합니다. 나는 어떤 사람들이 그것을 저장할 필요가 없다고 말하는 것을 알고 있지만 그것은 당신이 그린 필드인지 또는 기존 시스템과 공존 해야하는지에 달려 있습니다.

유지 관리상의 이유로 단일 컨테이너 / 테이블 / 컬렉션을 사용했지만 엔티티 / 객체별로 별도의 테이블을 사용했습니다. 실제로 응용 프로그램에 "CREATE"권한이 필요하거나 (일반적으로 좋은 생각이 아닙니다 ... 일반적으로 항상 예외 / 제외가 있음) 새 엔티티 / 개체가 존재하거나 배포 될 때마다 새로운 저장 용기 / 테이블 / 수집을 만들어야했습니다. 로컬 개발에는 고통스럽고 느리고 프로덕션 배포에는 문제가 있음을 발견했습니다. 그렇지 않을 수도 있지만 그것은 우리의 실제 경험이었습니다.

기억해야 할 또 다른 사항은 액션 X가 발생하도록 요청하면 다양한 이벤트가 발생할 수 있으므로 명령 / 이벤트에 의해 생성 된 모든 이벤트 / 유용한 내용을 알 수 있습니다. 또한 다른 개체 유형에 걸쳐있을 수 있습니다. 예를 들어 장바구니에서 "구매"를 푸시하면 계정 및 창고 이벤트가 실행될 수 있습니다. 소비 애플리케이션은이 모든 것을 알고 싶어 할 수 있으므로 CorrelationId를 추가했습니다. 이는 소비자가 요청의 결과로 발생한 모든 이벤트를 요청할 수 있음을 의미합니다. 당신은스키마 있습니다.

특히 SQL의 경우 인덱스와 파티션이 적절하게 사용되지 않으면 성능이 실제로 병목 현상이된다는 것을 발견했습니다. 스냅 샷을 사용하는 경우 이벤트를 역순으로 스트리밍해야합니다. 우리는 몇 가지 다른 인덱스를 시도해 보았고 실제로 프로덕션 환경에서 실제 응용 프로그램을 디버깅하는 데 몇 가지 추가 인덱스가 필요하다는 것을 발견했습니다. 다시 당신은 스키마 .

다른 프로덕션 내 메타 데이터는 프로덕션 기반 조사 중에 유용했으며 타임 스탬프를 통해 이벤트가 지속되고 발생하는 순서에 대한 통찰력을 얻을 수있었습니다. 이는 방대한 양의 이벤트를 발생시킨 특히 이벤트 중심 시스템에 대한 지원을 제공하여 네트워크 및 네트워크를 통한 시스템 배포와 같은 성능에 대한 정보를 제공했습니다.


훌륭합니다. 감사합니다. 이 질문을 작성한 후 오랫동안 github의 Inforigami.Regalo 라이브러리의 일부로 몇 가지를 직접 만들었습니다. RavenDB, SQL Server 및 EventStore 구현. 웃음으로 파일 기반 작업에 대해 궁금해했습니다. :)
Neil Barnwell

1
건배. 나는 단지 결과보다는 최근에 그것을 접하고 배운 교훈 중 일부를 공유하는 다른 사람들을 위해 주로 대답을 추가했습니다.
cdmdotnet

3

Datomic을 살펴보고 싶을 것입니다.

Datomic은 유연한 시간 기반 사실 의 데이터베이스입니다. 탄력적 인 확장 성과 ACID 트랜잭션을 통해 쿼리 및 조인을 지원하는 .

여기에 자세한 답변을 썼습니다

여기서 Datomic의 디자인을 설명하는 Stuart Halloway의 강연을 볼 수 있습니다.

Datomic은 사실을 적시에 저장하므로 이벤트 소싱 사용 사례 등에 사용할 수 있습니다.


2

도메인 모델이 발전함에 따라 솔루션 (1 & 2)이 매우 빠르게 문제가 될 수 있다고 생각합니다. 새 필드가 생성되고 일부는 의미가 변경되며 일부는 더 이상 사용되지 않을 수 있습니다. 결국 테이블에는 수십 개의 nullable 필드가 있으며 이벤트로드가 엉망이됩니다.

또한 이벤트 저장소는 쓰기에만 사용해야하며 집계 속성이 아닌 이벤트를로드하기 위해 쿼리 만해야합니다. 그것들은 별개의 것입니다 (즉, CQRS의 본질입니다).

해결 방법 3 사람들이 일반적으로하는 일,이를 달성하는 데는 여러 가지 방법이 있습니다.

예를 들어 EventFlow CQRS 를 SQL Server와 함께 사용하면 다음 스키마가있는 테이블이 생성됩니다.

CREATE TABLE [dbo].[EventFlow](
    [GlobalSequenceNumber] [bigint] IDENTITY(1,1) NOT NULL,
    [BatchId] [uniqueidentifier] NOT NULL,
    [AggregateId] [nvarchar](255) NOT NULL,
    [AggregateName] [nvarchar](255) NOT NULL,
    [Data] [nvarchar](max) NOT NULL,
    [Metadata] [nvarchar](max) NOT NULL,
    [AggregateSequenceNumber] [int] NOT NULL,
 CONSTRAINT [PK_EventFlow] PRIMARY KEY CLUSTERED 
(
    [GlobalSequenceNumber] ASC
)

어디:

  • GlobalSequenceNumber : 간단한 전역 식별로, 프로젝션 (readmodel)을 만들 때 누락 된 이벤트를 정렬하거나 식별하는 데 사용할 수 있습니다.
  • BatchId : 원자 적으로 삽입 된 이벤트 그룹 식별 (TBH, 이것이 왜 유용한 지 알 수 없음)
  • AggregateId : 집계의 식별
  • 데이터 : 직렬화 된 이벤트
  • 메타 데이터 : 이벤트의 기타 유용한 정보 (예 : 역 직렬화에 사용되는 이벤트 유형, 타임 스탬프, 명령의 발신자 ID 등)
  • AggregateSequenceNumber : 동일한 집합 내의 시퀀스 번호 (순서대로 쓰기가 발생할 수없는 경우 유용하므로 낙관적 동시성을 위해이 필드를 사용)

그러나 처음부터 생성하는 경우 YAGNI 원칙을 따르고 사용 사례에 필요한 최소한의 필드로 생성하는 것이 좋습니다.


BatchId는 잠재적으로 CorrelationId 및 CausationId와 관련 될 수 있다고 주장합니다. 이벤트의 원인을 파악하고 필요한 경우 함께 연결하는 데 사용됩니다.
Daniel Park

그것은 수. 그러나 이것은 그렇습니다.이를 사용자 정의하는 방법 (예 : 요청의 ID로 설정)을 제공하는 것이 합리적이지만 프레임 워크는 그렇게하지 않습니다.
Fabio Marreco

1

가능한 힌트는 디자인에 이어 "천천히 변경되는 치수"(유형 = 2)가 다음을 다루는 데 도움이됩니다.

  • 발생하는 이벤트 순서 (대리 키를 통해)
  • 각 상태의 내구성 (유효 기간-유효 기간)

왼쪽 접기 기능도 구현할 수 있어야하지만 향후 쿼리 복잡성을 고려해야합니다.


1

나는 이것이 늦은 대답이라고 생각하지만 처리량 요구 사항이 높지 않으면 RDBMS를 이벤트 소싱 저장소로 사용하는 것이 완전히 가능하다는 점을 지적하고 싶습니다. 설명하기 위해 작성한 이벤트 소싱 원장의 예를 보여 드리겠습니다.

https://github.com/andrewkkchan/client-ledger-service 위는 이벤트 소싱 원장 웹 서비스입니다. https://github.com/andrewkkchan/client-ledger-core-db 위의 내용은 RDBMS를 사용하여 상태를 계산하므로 트랜잭션 지원과 같은 RDBMS와 함께 제공되는 모든 이점을 누릴 수 있습니다. https://github.com/andrewkkchan/client-ledger-core-memory 그리고 버스트를 처리하기 위해 메모리에서 처리 할 다른 소비자가 있습니다.

RDBMS는 특히 삽입이 항상 추가 될 때 삽입 속도가 느리기 때문에 위의 실제 이벤트 저장소가 여전히 Kafka에 있다고 주장 할 수 있습니다.

이 질문에 대해 이미 제공된 아주 좋은 이론적 답변과는 별도로 코드가 그림을 제공하는 데 도움이되기를 바랍니다.


감사. SQL 기반 구현을 구축 한 지 오래되었습니다. 어딘가에 클러스터 된 키에 대해 비효율적 인 선택을하지 않는 한 RDBMS가 삽입에 느린 이유를 잘 모르겠습니다. 추가 전용은 괜찮습니다.
Neil Barnwell
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.