[Из песочницы] Снимки событий в Axonframework 3, улучшаем производительность

Обзор фреймврока Axonframework


Axonframework это фреймфорк реализующий несколько принципов и паттернов проектирования такие как:

CQRS — разделяет обработку запросов на чтение и запись данных
Event Sourcing — это когда состояние приложения хранится как цепочка событий
DDD Aggregate — доменный объект (domain object) который хранит состояние

Один из недостатков хранения конечного состояния приложения в виде цепочки событий — это количество хранимых и обрабатываемых событий. К счастью, Axonframework позволяет создавать снимок событий (snapshot event), который содержит в себе результат нескольких событий (domain event).

Снимки событий


Снимок событий (snapshot event) — это результирующие значения нескольких событий (domain event). Это позволяет быстрее воссоздавать состояние Агрегата (Aggregate). Важно понимать, что снимок создаётся из событий которые применялись для конкретного Агрегата с уникальным идентификатором.

Например (рис. 1), зададим в конфигурации создание снимка на каждые два события (порог = 2 — для наглядности примера). В таком случае, когда два события изменят состояние Агрегата, то создастся один снимок c результирующими значениями предыдущих двух событий.

_rzu9apczumi6zijbcafynqajwy.png
Рис 1. Снимок двух событий. (порог=2)

Рассмотрим пример посложнее (рис. 2), в конфигурации также указан порог равный 2, чтобы снимок создавался каждые два события. Когда 2 события изменят состояние Агрегата, то создастся один снимок. Далее другие 2 события изменяют состояние Агрегата и новый снимок не создаётся, а обновляется уже существующий.

pt27ot4awkpcgksfxfv7lxo7idg.png
Рис. 2 Результат цепочки событий в одном снимке (порог=2)

Производительность


С одной стороны когда в приложении накапливается длинная цепочка событий, то требуется время для чтения и обработки большого числа событий для воссоздания состояния Агрегата. С другой стороны, если создавать снимок, то состояние Агрегата будет воссоздано быстро, но потребуется время на создание снимка. Необходимо находить баланс между этими двумя ситуациями.

8zhehbkall5aane2widtwjath-k.png
Рис. 3 Производительность без создания снимка

ctag4-2ofkhzu5_osyqvav8uuki.png
Рис. 4 Производительность с созданием снимка (порог = 3)

По умолчанию, снимок создаётся в потоке который вызвал метод scheduleSnapshot (). Такая настройка не рекомендуется для боевой среды (см рис. 4/запись).

Ниже приведём пример кода с применением ThreadPoolExecutor (…) который предоставит отдельный поток для создания снимка. В таком случае, наш клиент не заметит замедления в работе приложения и выделенное время на создание снимка.

Код


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

AxonConfig.java

@Autowired
private EventStore eventStore;

@Bean
public SpringAggregateSnapshotterFactoryBean springAggregateSnapshotterFactoryBean() {
   return new SpringAggregateSnapshotterFactoryBean();
}
@Bean
public SpringAggregateSnapshotter snapshotter(ParameterResolverFactory parameterResolverFactory, EventStore eventStore, TransactionManager transactionManager) {
   Executor executor = Executors.newFixedThreadPool(10);
   return new SpringAggregateSnapshotter(eventStore, parameterResolverFactory, executor, transactionManager);
}

@Bean("reservationRepository")
public EventSourcingRepository reservationRepository(Snapshotter snapshotter, ParameterResolverFactory parameterResolverFactory) {
   return new EventSourcingRepository(reservationAggregateFactory(), eventStore, parameterResolverFactory, new EventCountSnapshotTriggerDefinition(snapshotter, 50));
}

@Bean(name = "reservationAggregateFactory")
public AggregateFactory reservationAggregateFactory() {
   SpringPrototypeAggregateFactory aggregateFactory = 
   new SpringPrototypeAggregateFactory<>();
   aggregateFactory.setPrototypeBeanName("reservation");
   return aggregateFactory;
}


Reservation.java

@Aggregate(repository = "reservationRepository")
public class Reservation {
        //…
}


Стоит отметить, что в ветке обсуждения Google Groups содержатся полезные примеры кода и обсуждения.

Выбор порогового значения для создания снимков

5.1. Теоретический путь

Посчитаем количество событий которые могут применяться к Агрегату в EventListener классе. Затем теоретически оценим среднее количество событий применяемых к Агрегату в типичной ситуации и значение несколько меньше этого установим в качестве порогового для создания снимков. Так можно поступить если приложение только создано и нет реальных данных для анализа.

5.2. Практический путь

Проанализируем данные из базы данных, при этом будем считать, что база данных используется MongoDB и она работает внутри докер контейнера.

> docker exec -it  mongo 
> show dbs
admin           0.000GB
axonframework   0.000GB
local           0.000GB

> use axonframework
switched to db axonframework

> show collections
domainevents
sagas
snapshotevents

> db.domainevents.findOne()

{
 "_id” : ObjectId("5bb1dc8d4446d63bcc765feb”),
 "aggregateIdentifier” : "b1e320d5–58aa-4b9b-a667-aa724900592f”,
 "type” : "Reservation”,
 "sequenceNumber” : NumberLong(0),
 "serializedPayload” : "b1e320d5–58aa-4b9b-a667-aa724900592f124000”,
 "timestamp” : "2018–10–01T08:36:29.434Z”,
 "payloadType” : "com.example.ReservationStarted”,
 "payloadRevision” : null,
 "serializedMetaData” : "traceIdb090b86a-ec89–484b-ae9f-e4fa0f9bcd39correlationIdb090b86a-ec89–484b-ae9f-e4fa0f9bcd39”,
 "eventIdentifier” : "f324f021–50b4–4e91–84d0-f8c4425f3eb9”
}


Каждое хранящееся событие содержит поле aggregateIdentifier, по которому посчитаем количество событий примененных к каждому Агрегату простым запросом:

db.domainevents.aggregate([ 
    {$group: {_id: "$aggregateIdentifier", count: {$sum: 1} } },
    {$sort : {count : -1} }
]);

{ "_id" : "0d84afd1-f199-45c8-b50e-7d9ebfa4c8fb", "count" : 136 }
{ "_id" : "49de7c32-38ea-435a-b837-ccdb61ec0baa", "count" : 136 }
{ "_id" : "12957b0b-af05-47c4-a3d8-968b75cf9ffb", "count" : 136 }
{ "_id" : "97a24559-ee3a-43e7-a6be-1eb6840b662a", "count" : 132 }
{ "_id" : "b6aeb1af-0620-4b02-8de3-c2446c2f7d83", "count" : 132 }
{ "_id" : "b385aaf4-3338-489f-8d1b-4600d5e088b9", "count" : 132 }
{ "_id" : "5970327f-9551-4945-94e9-3844c0cd3543", "count" : 132 }
...
{ "_id" : "0182239h-3948-3334-98t5-9643j4ld8346", "count" : 1 }


Пороговое значение для создания снимков можно выбрать меньше среднего чтобы снимки создавались эффективно. В данном случае значение 50 вполне подойдёт.

Проверка активации снимков

> mongo
> show dbs
admin           0.000GB
axonframework   0.000GB
local           0.000GB

> use axonframework
> show collections
domainevents
sagas
snapshotevents

> db.domainevents.count()
515
> db.snapshotevents.count()
7


Если коллекция snapshotevents не пустая и содержит в себе снимки, то создание снимков активировано успешно.

Другие возможности создания снимков


В документации упоминаются и другие вариации по активации создания снимков, например:

  • число событий созданных с момента последнего снимка превысило пороговое значение
  • время на инициализацию Агрегата истекло
  • временная задержка и т.д. и т.п.

© Habrahabr.ru