Pull to refresh

Comments 35

Не очень понял. Куда мы должны отправить запрос, в EventStore или StateDb?
AigizK, запрос на чтение в «традиционной» модели отправляется в state db (часто это реляционная база даннных); в ленивой модели запрос уходит в event store (который не только журналирует события, но и индексирует их). так понятней?
Но разделение DB же как раз позволяет настроить ES быстрым на запись, а state db на чтение. И при необходимости мы можем держать много копий state db. А вы как раз жертвуете этим преимуществом в замен актуальным данным.
В существующей реализации, event store на самом деле (внутри) — две базы, одна — журнал, другая — индекс. Можно рассматривать их совокупность как одну event store, либо как event store + state db, где стейт — это просто индексы событий.
Под ES подразумевал базы для записи, где сохраняете все события. Сейчас работаю над проектом, где используется подход ES+CQRS. Поэтому некое представление, как это работает, есть.
Я не совсем понял, что вы хотите сделать:
при переигрывание событий, чтоб заполнить StateDB, доставать только определенные события
или
пользовательский запрос направить на базу, где хранятся сами события и от StateDB отказаться
Идея в том что не строится state db согласной некой модели данных, а только записываются события и они же индексируются (простые индексы по полям, или более сложные композитные индексы). вместо того чтобы записывать изменения в таблицу Users (например), и искать пользователя там, мы просто ищем события которые дают нам минимально необходимое понимание о том что произошло (в случае примера в статье, UserCreated(id) и последний EmailChanged(user: id). Ищем их не перебором, а по индексам (индексы могут быть определены как заранее, так и после записи событий).

При этой модели, state db в классическом понимании нет, как нету и «единой» модели данных.

Так понятней, или все еще плохо объяснил?
мне кажется, важно вот какое замечание сделать: разделение на write side и read side было и остается, но read side представляет собой не около-конечную модель данных а «сырые индексы» и сборка моделей происходит (в большинстве случаев) в рантайме. Соответственно, нет необходимости «предугадывать» будущее.
Т.е. ReadDb содержит инфу, скажем как быстро из WriteDB достать события UserCreated и EmailChanged и мы уже напрямую обращаемся к базе WtiteDb, так?
по сути, да. индексы содержат все проиндексированные данные и соотв. ссылки по которым быстро достаются любые события и откуда можно узнать все данные которые были записаны в событии.
1.Поступило событие
2.Вы пишете его в WriteDb и получаете допустим некий ключ, по которому можно быстро получить это событие
3. Пишите этот ключ в ReadDb
4. Пользователь делает запрос, разбираете, выясняете какие ключи вам нужны, достаете по ним данные из WriteDb

В итоге:
1. пользователь так же будет получать не актуальную информацию(хотя не настолько как в традиционном варианте), потому что пока вы считываете ключи, WriteDb может измениться, а новые ключи еще не успели добавить в ReadDb
2. изменений может быть очень много и вместо одного запроса, который бы вернул статистику изменения курса, вам нужно отправить много запросов(ведь вы предлагаете доставать отдельные события по ключу)
3. если запросов на чтение много, то БД на запись так же будет тормозить
отличный анализ!

по итогам:
1. более актуальная информация чем в традиционном варианте — уже лучше.
2. как я упоминал в статье, конечно есть случаи когда массовые выборки будут медленными, и так или иначе агрегировать и/или кешировать данные надо. идеального мира нет :)
3. не обязательно, это зависит от того как она устроена. append only дает определенные преимущества.
Тогда подробнее расскажите, как устроен у вас WriteDb
пока что ничего сильно сложного (но проект еще очень молодой). сейчас writedb (журнал) реализуется через MVStore (движок от H2), так как в моей текущей модели я разрабатываю приложения со «встроенным» хранилищем (через интерфейс можно, конечно, добавить любые другие реализации. например, более раннии инкарнации этого проекта использовали postgresql по умолчанию).

если разбивать файлы в которых лежат необходимые ключи (например, consistent hashing или на исторические «отрезки») и хранить на независимых устройствах (в терминах дисков ли, или сетевых устройств) то можно обеспечить сценарий в котором чтение из writedb практически независимо от записи. пока что этого ф-ционала нет, но это интересная тема!
Есть еще один подход. Оставить традиционный способ заполнения ReadDb и натравить на него ElasticSearch.
1. делаете запрос в ElasticSearch
2. ElasticSearch возвращает ИД из ReadDb
3. Достаете данные из ReadDb

Такой подход легко масштабируется
масштабируется — да. но это не решает проблему (которая для кого-то проблема, а для кого-то — нет) с нежеланием предугадывать будущее (соотв., строить read side модели). у меня это было задачей :)
Начну с простого вопроса:
А почему вы решили, что для хранения состояния нужна обязательно реляционная бд? Фаулер пишет буквально следующее:
Application states can be stored either in memory or on disk. Since an application state is purely derivable from the event log, you can cache it anywhere you like. A system in use during a working day could be started at the beginning of the day from an overnight snapshot and hold the current application state in memory.

То есть можно вообще не создавать read model и кешировать в памяти доменную модель из которой и брать нужные вещи, In-memory cache можно восстанавливать по событиям при старте приложения.
Разделение на Read модель и доменную модель описывается паттерном CQRS, который хоть и хорошо сочетается с EventSourcing но все же отдельный паттерн, не требующий ни EventSourcing ни даже отдельных бд для чтения и записи.
Спасибо за комментарий!

Я не писал, что нужна обязательно реляционная база данных, выше в комментариях я написал «часто это реляционная база данных». Доменную модель и правда можно хранить где угодно. Если доменная модель в памяти, это не значит что read side нету, просто она в памяти.

Можно прекрасно делать то что вы описываете («кешировать в памяти доменную модель из которой и брать нужные вещи, In-memory cache можно восстанавливать по событиям при старте приложения») однако это не решает ту задачу которая была передо мной — избежать планирования доменной модели («предугадывание будущего»), и дорогостоящих «переигрываний» событий.
То есть вы хотите вообще не иметь никакой модели? В этом случае ни о какой консистентности данных говорить не приходится вообще. (Даже об eventually consistency.) Вам просто не на чем проверять эту консистентность. Нет, если ваша система хранит разрозненные данные, можно так делать, наверное, Но, боюсь, для того чтобы получить что-нибудь полезное из этой кучи, вам придется применять методы применяемые для работы с большими данными.

Прямо скажем, идея "давайте использовать EventStore как read-model" лежит на поверхности, и как раз в варианте "давайте просто читать последнее релевантное событие" (особенно если ES так организован, что позволяет это сделать сравнительно быстро). Проблемы этого подхода начинаются в тот момент, когда вам надо-таки прочитать всю историю агрегата (например, если вам надо для пользователя достать все теги, на которые он подписан, а каждый тег вы храните отдельным событием).

Конечно, на поверхности! Ничего сверх-нового. Мои усилия на тему чтения истории агрегата сейчас таковы: 1) оптимизация поиска (индексы используют CQengine, память/диск) 2) организация события в таком виде при котором легко запрашивать без over-fetching.
Мои усилия на тему чтения истории агрегата сейчас таковы: 1) оптимизация поиска (индексы используют CQengine, память/диск)

Индексы синхронные?


организация события в таком виде при котором легко запрашивать без over-fetching.

… то есть вы подстроили события (которые write-model) под то, как удобно читать (то есть read-model)?

1. Явно выраженных гарантий по поводу индексов еще нет (но скорее всего будут), пока это факт реализации и некоторых тестов — индексы появляются по факту коммита транзакции (scope транзакций — команда), перед тем как контроль «вернется» к отправителю команды, то есть по сути идут вместе с транзакцией.
2. Только в некотором смысле — в том плане что я сохраняю данные в событиях в таком виде в котором использование индексов для того чтобы найти эти события было настолько тривиальным насколько это возможно. Случаи, когда таки приходится делать агрегацию событий для оптимизации конечно происходят, но я стараюсь это свести к минимуму.
Явно выраженных гарантий по поводу индексов еще нет (но скорее всего будут), пока это факт реализации и некоторых тестов — индексы появляются по факту коммита транзакции (scope транзакций — команда), перед тем как контроль «вернется» к отправителю команды, то есть по сути идут вместе с транзакцией.

То есть вы пожертвовали скоростью записи ради индексов?


Вы понимаете, что ваши индексы — это и есть ваша доменная модель?

Да, это «вывернутая наизнанку» доменная модель, однако в этой модели мне не надо угадывать что будет в будущем, просто честно собирать данные и «разворачивать» доменную модель так как мне нужно.

Индекс — это и есть попытка угадать, какие данные вам понадобятся.

Да, вы правы, это можно и так рассматривать! :)

С моей личной практической точки зрения, это тот «низкий» порог «угадывания» который меня устроил. Мне не нужно угадывать какие будут структуры моделей, как нужно со временем менять модели и мигрировать данные, мне нужно только время от времени добавлять индексы к полям, причем совсем необязательно перед тем как события записаны — индексы можно добавить в любой момент.

… и каждый индекс будет просаживать производительность записи EventStore.

да, пока нет конфигурации на тему когда индексирование должно происходить, индексы будут просаживать производительность записи приблизительно также как и в традиционных реляционных бд

… а если вы их сделаете асинхронными, вы получите eventual consistency без возможности управления ей (или вы сразу закладываетесь на то, что данные, которые вы получили, неактуальны?)

логично, черт побери — мы плаваем между двумя трейд-оффами тут. либо быстро но eventually consistent, либо медленно. есть другие предложения?

Просто если вы согласны на eventually consistent, то его надо закладывать в проект заранее, а не "мы пока не знаем, какие индексы будут".

пока что в проекте нет eventual consistency, поведение ближе к стандартном поведению многих баз данных. Есть предложения на тему как лучше всего «заложить» это в проект заранее? Я пока себе это представляю как набор требований выставляемых «поставщиком» конкретной команды — насколько ему важно гарантия проиндексированных данных, или хватит гарантии журналирования, чтобы перейти к следующим шагам быстрее.

Лично я вообще воспринимаю работу с eventual consistency с другой стороны — это поведение команды на запись: она должна знать, относительно каких данных она была консистентна (а, значит, либо запрос на чтение должен отдавать идентификатор версии, либо мы тащим с собой все данные для проверки консистентности), и проверять к каким данным она применяется (естественно, уже в рамках транзакции), а дальше уже применять ту или иную стратегию сведения.


Для "честного" ES это в реальности делается очень просто — каждый запрос на чтение возвращает номер события, относительно которого данные прочитаны (и актуальны), каждая команда на запись проверяет, что в агрегате нет новых событий после указанного (а тривиальная стратегия сведения сводится к ошибке "данные изменились, накатите заново").

Пробовали такой подход. В итоге решили отказаться: медленно, сложные запросы.
relgames есть примеры сложных запросов? какие инструменты использовались и где были максимальные проблемы с производительностью?

Спасибо!
Sign up to leave a comment.

Articles