Данные стали бесценным активом, позволяющим компаниям лучше понимать своих пользователей, прогнозировать их поведение и определять тренды. EventNative – проект с открытым исходным кодом, разработанный командой из Jitsu, который позволяет упростить сбор данных о событиях. EventNative поддерживает работу с несколькими хранилищами данных, и ClickHouse – одно из них.
В этой статье мы расскажем как настроить EventNative с ClickHouse, а также в ней приводятся советы по эксплуатации и повышению производительности и надежности.
Загрузка данных в ClickHouse – не такая простая задача, как может показаться на первый взгляд. Обработка потоков с миллионами событий из различных приложений (где у каждого события может быть своя собственная структура) может вызывать значительные затруднения. Ситуация может стать особенно сложной, если в одной продакшн-среде работают разные версии одного и того же приложения (например, разные версии iOS-приложения).
Архитектура EventNative максимально эффективна и надежна. Она состоит из легковесного HTTP-сервера, принимающего поток событий (состоящий из JSON-объектов) и буферизующего его на локальный диск. Отдельный поток приложения занимается обработкой этого буфера, маппингом JSON-объектов с таблицами в ClickHouse, настройкой схемы и хранением данных.
Быстрый запуск ClickHouse и EventNative
В этом разделе мы расскажем об установке узла с ClickHouse и EventNative c помощью официальных Docker-образов.
Обратите внимание, что здесь мы рассматриваем настройку окружения для разработки. В продакшн-сценариях вы можете развернуть несколько узлов EventNative и подключить реплики ClickHouse, чтобы убедиться в доступности данных и масштабировать пропускную способность.
1. Скачиваем Docker-образы
docker pull ksense/eventnative:latest && docker pull yandex/clickhouse-server:latest
2. Запускаем ClickHouse
mkdir ./clickhouse_data && docker run --name clickhouse-test -p 8123:8123 -v $PWD/clickhouse_data:/var/lib/clickhouse yandex/clickhouse-server
3. Настраиваем EventNative
Добавьте следующий текст в ./eventnative.yaml
server:
auth:
- server_secret: 'ia7i92rqp3mh' # access token. We will need it later for sending events through HTTP API
destinations:
clickhouse:
mode: stream
clickhouse:
dsns:
- "http://default:@host.docker.internal:8123?read_timeout=5m&timeout=5m"
db: default
data_layout:
mappings:
fields:
- src: /field_1/sub_field_1
action: remove
- src: /field_2/sub_field_1
dst: /field_10/sub_field_1
action: move
- src: /field_3/sub_field_1/sub_sub_field_1
dst: /field_20
action: move
type: DateTime
- dst: /constant_field
action: constant
value: 1000
Также создайте директорию для логов: mkdir ./eventnative-logs
4. Запускаем EventNative
docker run -d -t --name eventnative-test -p 8001:8001 \
-v $PWD/eventnative.yaml:/home/eventnative/app/res/eventnative.yaml \
-v $PWD/eventnative-logs:/home/eventnative/logs/events/ ksense/eventnative:latest
5. Отправляем тестовое событие и проверяем, что оно записалось в ClickHouse
Добавьте следующий объект в ./api.json
:
{
"eventn_ctx": {
"event_id": "19b9907d-e814-42d8-a16d-c5da51e01531"
},
"field_1": {
"sub_field_1": "text1",
"sub_field_2": 100
},
"field_2": "text2",
"field_3": {
"sub_field_1": {
"sub_sub_field_1": "2020-09-25T12:38:27"
}
}
}
Запустите следующую команду:
curl -X POST -H "Content-Type: application/json" -d @./api.json \
'http://localhost:8001/api/v1/s2s/event?token=ia7i92rqp3mh'
echo 'SELECT * FROM events;' | curl 'http://localhost:8123/' --data-binary @-
Вы увидите одно событие в базе данных. Тест сработал!
6. Тестируем буферизацию событий
Одна из основных фич EventNative'а — это буфферизация. События записываются во внутреннюю очередь с сохранением на диск. Если база данных (в нашем случае ClickHouse) недоступна, данные не будут потеряны. События будут сохранены локально и попадут в базу данных когда ClickHouse станет доступным опять.
Давайте протестируем эту фичу:
Добавьте следующий JSON в ./api2.json
:
{
"eventn_ctx": {
"event_id": "4748c7bb-50d4-43a7-91b4-21a5bcccb12e"
},
"field_1": {
"sub_field_1": "text1",
"sub_field_2": 100
},
"field_2": "text2",
"field_3": {
"sub_field_1": {
"sub_sub_field_1": "2020-09-25T12:38:27"
}
}
}
Остановите ClickHouse
docker stop clickhouse-test
Отошлите событие
curl -X POST -H "Content-Type: application/json" -d @./api2.json 'http://localhost:8001/api/v1/s2s/event?token=ia7i92rqp3mh'
Проверьте что ClickHouse действительно не работает
echo 'SELECT * FROM events;' | curl 'http://localhost:8123/' --data-binary @-
Запустите ClickHouse опять
docker start clickhouse-test
Подождите 60 секунд и убедись, что событие не потерялось
echo 'SELECT * FROM events;' | curl 'http://localhost:8123/' --data-binary @-
Управление схемой с помощью EventNative и ClickHouse
EventNative спроектирован так, что вам не потребуется создавать схемы таблиц и поддерживать их. EventNative позаботится об этом автоматически! Каждое поле в JSON-объекте будет сопоставляться с полем в SQL. Если поле отсутствует, оно будет автоматически создано в базе ClickHouse.
Это особенно полезно, если одна команда разработчиков занимается структурой событий, а другая работает с ClickHouse. Приведем пример: фронтенд-разработчик может начать отправлять очень простые данные для отслеживания просмотров страницы продукта (id продукта и его стоимость), а затем будет добавлять более сложные поля (валюту и изображения). Потому что они могут пригодиться.
Пример:
JSON с событием
{
"product_id": "1e48fb70-ef12-4ea9-ab10-fd0b910c49ce",
"product_price": 399.99,
"price_currency": "USD",
"product_type": "supplies",
"product_release_start": "2020-09-25T12:38:27",
"images": {
"main": "picture1",
"sub": "picture2"
}
}
Автоматически сгенерированная структура таблицы:
"product_id" String,
"product_price" Float64,
"price_currency" String,
"product_type" String,
"product_release_start" String,
"images_main" String,
"images_sub" String
По умолчанию, все поля не будут допускать запись Null. Проблема в том, что Nullable-поля влияют на производительность. Но в случае если Null значения необходимы - можно сконфигурировать список полей:
nullable_fields: ['product_id', 'product_price', 'price_currency']
(См полное описание конфигурации mapping'а)
Больше о конфигурация mapping'а
С помощью изменения настроек EventNative может применять к входящим JSON-объектам определенные преобразования, например:
Удалять поля
Переименовывать поля (включая перенос поля в другой узел)
Явно определять тип поля
Подставлять константу
Пример
- src: /field_1/sub_field_1
action: remove
- src: /field_2/sub_field_1
dst: /field_10/sub_field_1
action: move
- src: /field_3/sub_field_1/sub_sub_field_1
dst: /field_20
action: move
type: DateTime
- dst: /constant_field
action: constant
value: 1000
В результате применения правил следующий JSON
{
"eventn_ctx": {
"event_id": "19b9907d-e814-42d8-a16d-c5da51e01530"
},
"field_1": {
"sub_field_1": "text1",
"sub_field_2": 100
},
"field_2": "text2",
"field_3": {
"sub_field_1": {
"sub_sub_field_1": "2020-09-25T12:38:27"
}
}
}
Превратится в такую запись в базе данных
{
"eventn_ctx_eventn_id": "19b9907d-e814-42d8-a16d-c5da51e01530",
"field_1_sub_field_1": "text1",
"field_1_sub_field_2": 100,
"field_2": "text2",
"field_3_sub_field_1_sub_sub_field_1": "2020-09-25T12:38:27.763000Z",
"constant_field": 1000
}
Полное описание этой функции вы можете изучить в документации
Советы по повышению производительности
ReplacingMergeTree (или ReplicatedReplacingMergeTree) – лучший движок для данных, созданных EventNative, и вот почему:
Обычно данные, созданные EventNative используются в запросах с агрегатными функциями (например, при подсчете количества событий, удовлетворяющих некоторому условию, за заданный период времени). Движки из семейства MergeTree показывают отличную производительность при работе с такими запросами.
ReplacingMergeTree (в отличие от MergeTree) имеет приятный побочный эффект – дедупликация данных. Зачастую ошибки в данных обнаруживаются уже после их загрузки. Иногда их необходимо перезаписывать. Поскольку EventNative может хранить логи событий в течении некоторого времени, вы можете написать скрипт для исправления данных и их повторной записи. ReplacingMergeTree позволит избежать дублирования данных (при условии, что у каждого события есть уникальный идентификатор, который используется в качестве ключа)
Если целевая таблица отсутствует, EventNative создаст таблицу с помощью ReplacingMergeTree или ReplicatedReplacingMergeTree (если размер кластера больше 1). Однако, этот механизм можно настроить вручную. Подробнее о создании таблиц можно прочитать в документации