Запись данных в Clickhouse (и другие векторные базы) может достигать производительности в миллионы строк в секунду. Для этого важно писать данные крупными пакетами (сотни тысяч...миллионы строк в пакете).

Однако аналитические данные часто генерируются прямо на лету. Например, сбор данных о поведении пользователя на сайте. В IO — это один из ключевых источников данных, метрики приходят от миллиардов устройств. И тут крайне важно иметь минимальный путь от Web сервера до базы данных.

Идеальная схема будет включать только Web сервер и базу данных:

Событие -> Nginx -> Логи -> Clickhouse

# Событие будет логироваться Nginx'ом

Упростим нашу задачу до сбора и записи простых наборов метрик во времени:

  • У метрики есть название и значение.
  • Метрика приходит в определенный момент времени.
  • По метрикам можно делать различные выборки — фильтровать по времени и имени.

Подготовка таблицы

В нашем случае таблица будет такой структуры:

CREATE TABLE metrics
( time DateTime,  name String,  value Int64)
ENGINE = MergeTree
PARTITION BY name
ORDER BY time
SETTINGS index_granularity = 8192

# Создание таблицы в Clickhouse

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

Настройка логирования

Самая сложная задача — научить Nginx писать данные в нужном нам формате. Хотя с Nginx'ом она оказывается довольно простой. Собирать метрики будем обычным запросом в формате:

http://tracking.server/?n=metric_name&v=metric_value

# Два аргумента — для названия (n) и значения (v) метрики

Для того, чтобы вставлять данные в Clickhouse мы постараемся получить лог-файл в CSV-формате:

date_time,metric_name,metric_value

# Такой CSV формат можно сразу зафидить в Clickhouse

Например, при отправке пары запросов по адресу http://tracking.server/?n=pageviews&v=1, мы увидим:
2019-04-07 14:26:47,pageviews,1
2019-04-07 14:26:49,pageviews,1

Получить такой формат лога в Nginx'e оказалось довольно просто, но пришлось использовать Lua для получения времени в нужном формате:

map $host $time { # Определяем переменную $time
	default '';
}

log_format track '$time,$arg_n,$arg_v'; # Определяем нужный формат лога

server {
	...

	location / {
	    access_log /var/log/nginx/track.log track;
	    default_type text/plain;

	    content_by_lua "
	        ngx.say('thanks')
	        ngx.var.time = os.date('!%Y-%m-%d %H:%M:%S') # Наполняем $time временем
	    ";
	}
}

# Такая конфигурация позволит записать данные в /var/log/nginx/track.log в нужном формате

Запись в Clickhouse

Чтобы записать данные из лога в Clickhouse, необходимо сначала сменить активный лог-файл (сделать rotate):

mv /var/log/nginx/track.log /tmp/db.log
kill -USR1 `cat /var/run/nginx.pid`

# Ротейтим лог перед записью в БД

Тепер можно записывать данные в Clickhouse:

cat /tmp/db.log | clickhouse-client --query "INSERT INTO metrics FORMAT CSV" && rm /tmp/db.log

# Обычный метод записи, ведь формат и данные уже готовы

Ну и объединить это в цикл с желаемым окном:

while :
do
  mv /var/log/nginx/track.log /tmp/db.log
  kill -USR1 `cat /var/run/nginx.pid`
  cat /tmp/db.log | clickhouse-client --query "INSERT INTO metrics FORMAT CSV" && rm /tmp/db.log
  sleep 10
done

# Ротейтим и записываем лог в базу каждые 10 секунд

Буферизация

Дополнительной оптимизацией может стать использование буфера. Если нагрузка на запись будет неравномерная (скажем, то 10 записей, то 10 тысяч записей), это имеет смысл. Работает все, как всегда, просто — достаточно создать специальную таблицу:

CREATE TABLE metrics_buffer
AS metrics
ENGINE = Buffer(default, metrics, 16, 10, 100, 1000, 10000, 10000, 100000)

# Создаем буфер для таблицы с метриками

Так Clickhouse создаст специальную таблицу, данные которой будут находиться только в памяти. Она будет повторять структуру нашей главной таблицы, и будет периодически сбрасывать в нее данные на основе указанных ограничений. Это повышает производительность вставки данных в основную таблицу, т.к. небольшие пакеты буферизируются и объединяются в более крупные.

Внимание, буферы чистятся при перезагрузке базы или сервера. Убедитесь, что у нее есть fail-over, если используете ее на продакшне.

Чтение данных также нужно делать из буфера вместо основной таблицы. Clickhouse сам замерджит данные из двух таблиц и выдаст правильный результат:

SELECT time, name, value FROM metrics_buffer

# Читать данные необходимо также из буфера

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

Мы тестировали решение на самом мелком дроплете на DigitalOcean:

Если не учитывать задержки Интернета, то такое решение способно обслужить около 6...7 тыс. запросов в секунду (что и понятно, это же простой Nginx без оверхеда):

ab -n 100000 -c 16 "http://127.0.0.1/track?n=test&v=1"
This is ApacheBench, Version 2.3 <$Revision: 1807734 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

...

Concurrency Level:      16
Time taken for tests:   15.553 seconds
Complete requests:      100000
Failed requests:        0
Total transferred:      19000000 bytes
HTML transferred:       700000 bytes
Requests per second:    6429.44 [#/sec] (mean)
Time per request:       2.489 [ms] (mean)
Time per request:       0.156 [ms] (mean, across all concurrent requests)
Transfer rate:          1192.96 [Kbytes/sec] received

# Тестирование пропускной способности

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

curl "rt.onthe.io/track?n=5caa00c8e8261-[имя_метрики]&v=1"

# Для трекинга

После отправки данных достаточно будет зайти по адресу:
https://rt.onthe.io/?metric=5caa00c8e8261-[имя_метрики]

# Для просмотра

Самое главное

Использование связки Nginx log + Clickhouse позволяет получить высокопроизводительное решение для сбора, хранения и аналитики временных рядов.