
В прошлом материале мы рассказали, с какой проблемой столкнулись, и проанализировали четыре СУБД в поиске рабочего решения. Мы оценили преимущества и недостатки каждого отобранного варианта и остановились на ClickHouse. Несмотря на то, что готовой интеграции этой БД с Zabbix не существует, CH отлично подходил как решение под наши инженерные задачи.
БД в Zabbix
Прежде чем мы перейдем к рассказу о реализации, расскажем о специфике работы БД в Zabbix. Вся ее логика вынесена в отдельную библиотеку — zbxhistory. Она используется сервером и прокси для сохранения данных мониторинга. В классе history описывается интерфейс, который имплементируется каждой реализацией подключения к хранилищу данных.

Кроме функций для выполнения CRUD-операций, в конкретном классе могут содержаться дополнительные функции для преобразования полученных значений. Так, в реализации history можно найти zbx_history_init — она создает под каждый тип данных Zabbix соответствующий коннектор с базой в зависимости от параметров файла.

Конфигурация экспортируется из классов server.c или proxy.c, где описаны переменные, содержащие информацию о наших источниках данных, хранимых типах и настройках буфера.
На этом этапе способ реализации понятен. Оставалось определиться, как мы будем взаимодействовать с CH. У него есть множество возможных коннекторов, но лучше всего описана работа с HTTP API. Конечно, лучшим вариантом с точки зрения производительности является использование проприетарного протокола CH, но доступной документации по нему нет, а поисковик вообще отсылает к изучению исходников clickhouse-client. Поэтому для прототипа решили остановиться на HTTP-протоколе.
За дело
Итак, мы создали класс clickhouse.c в нашей библиотеке и реализовали основные функции: init, destroy, add_values, get_values. Мы добавили для каждого типа данных буфер, который накапливал бы их перед записью в БД. Данный буфер сбрасывается по достижению какого-то ограниченного времени жизни либо при заполнении. В качестве стандартных значений указали 10 000 строк или 10 секунд. Это снизило нагрузку на базу, уменьшив количество «засечек», которые позднее предстоит смержить в более крупные файлы данных.
Пример инициализации коннектора, где мы заполнили дефолтные значения параметров и указали функции для использования высокоуровневым zbx_history_iface_t., выглядит так:
int zbx_history_clickhouse_init(zbx_history_iface_t* hist, unsigned char value_type, char** error) { zbx_clickhouse_data_t* data; if (0 != curl_global_init(CURL_GLOBAL_ALL)) { *error = zbx_strdup(*error, "Cannot initialize cURL library"); return FAIL; } data = (zbx_clickhouse_data_t*)zbx_malloc(NULL, sizeof(zbx_clickhouse_data_t)); memset(data, 0, sizeof(zbx_clickhouse_data_t)); data->base_url = zbx_strdup(NULL, CONFIG_HISTORY_CLICKHOUSE_STORAGE_URL); zbx_rtrim(data->base_url, "/"); data->buffer.data = NULL; data->buffer.alloc = 0; data->buffer.offset = 0; data->buffer.lastflush = time(NULL); data->buffer.num = 0; hist->value_type = value_type; hist->data.clickhouse_data = data; hist->destroy = clickhouse_destroy; hist->add_values = clickhouse_add_values; hist->flush = clickhouse_flush; hist->get_values = clickhouse_get_values; hist->requires_trends = 0; return SUCCEED; }
Дорабатывая конфиги, мы переделали функцию создания history_iface_t, чтобы назначить хранилище для каждого типа и не ломать интеграцию с Elasticsearch.
После реализации серверной части создали схему таблиц в БД. Для каждой из них указали необходимый набор полей, в качестве движка использовали MergeTree, настроили правила партиционирования и TTL (о нём поговорим позднее).
CREATE TABLE zabbix.history ( `itemid` UInt64, `clock` UInt32, `ns` UInt32, `value` Float64 ) ENGINE = MergeTree PARTITION BY toYYYYMM(CAST(clock, 'date')) ORDER BY (itemid, clock) TTL CAST(clock, 'date') + toIntervalSecond(2592000) SETTINGS index_granularity = 8192
Куча дебаг-сообщений, десяток сборок, и (о, чудо!) данные начали записываться.
Дорабатываем web-интерфейс и API
В web-интерфейсе добавили в конфигурационный файл параметры для подключения к БД (URL, username, password, database) и реализовали чтение из CH по HTTP API по аналогии с Elasticsearch.

В целом, отличается только процесс парсинга полученного JSON-сообщения[1] и запрос данных из БД. Для этих целей мы создали класс /include/classes/helpers/CClickhouseHelper.php, где предусмотрены функции Query для запроса данных и ParseResult для преобразования полученного JSON’a в многомерный массив.
public static function query($method, $request = null) { global $HISTORY; $time_start = microtime(true); $result = []; $options = [ 'http' => [ 'header' => "Content-Type: text/plain; charset=UTF-8", 'method' => $method, 'protocol_version' => 1.1, ignore_errors' => false // To get error messages from Clickhouse. ] ]; if ($request) { $options['http']['content'] = $request; } try { $result = file_get_contents($HISTORY['storages']['clickhouse']['url'], false, stream_context_create($options)); if($result){ $result = self::parseResult($result); } } catch (Exception $e) { error($e->getMessage()); } CProfiler::getInstance()->profileClickhouse(microtime(true) - $time_start, $method, $request); return $result; }
Переменная $HISTORY содержится в конфигурационном файле /conf/zabbix.conf.php и хранит в себе креденшелы для подключения к базе. Для подключения к БД нам понадобилась авторизация — для этого мы передали данные в виде параметров url:
?database=zabbix?username=zabbix?password=123
$HISTORY['storages'] = [ 'elastic' => [ 'url' => '', 'types' => [] ], 'clickhouse' => [ 'url' => '', 'types' => [] ] ];
На заметку: все эти параметры можно указать в самом файле, либо они подтягиваются из env-переменных.
После реализации хелпера приступили к доработке классов CHistoryManager[2] и CHistory[3]. Логика в них достаточно простая. Тем не менее стоит учитывать, что под разные типы данных нужно на ходу генерировать SQL-запросы для выборки истории из таблиц. Помимо этого, данные через API должны возвращаться в типе string. В результате получили работающие графики:

На последнем этапе решения нам оставалось реализовать расчет трендов по историческим данным. Перед нами было два пути. Для начала мы попробовали сделать это при помощи Zabbix-сервера, но процесс оказался слишком трудоемким. Расчет трендов описан в отдельной библиотеке, которая заточена сугубо на работу с классическими базами. Для того чтобы эта история стала рабочей, нам нужно было реализовать интерфейс, который смог бы имплементировать разные СУБД, как, например, библиотека zbxhistory.
Второй раз мы подошли к проблеме при помощи движка AggregatingMergeTree на стороне CH. Он изменяет логику слияния кусков и агрегирует данные с одинаковым первичным ключом в рамках определенной выборки. И это сработало. Данные (min, max, avg) в таблицах трендов рассчитывались автоматически каждый час при поступлении исторических данных в таблицы history и history_uint. После мы доработали функцию извлечения данных в web-интерфейсе, чтобы при запросе с интервалом более двух дней использовались тренды. Вот какой у нас вышел результат.

Все еще в интерфейсе: Time to Live
Ранее мы уже затрагивали тему TTL (Time to Live) — параметр, который задает время жизни данных. Он устанавливается в секундах, и при его исчерпании можно выполнить следующие действия: зачистить старые блоки, агрегировать старые данные, изменить уровень компрессии, переместить данные между зонами хранения. Подробнее обо всех возможностях и способах управления TTL можно почитать здесь.
Хоть с управлением TTL как операцией мы сталкивались редко, нам нужна была полноценная интеграция. Поэтому мы вынесли параметры управления временем жизни в меню Housekeeper в web-интерфейсе, доработав контроллер Housekeeper API, дизайн страницы Housekeeper и добавив два новых поля ch_history_global, ch_history в таблицу config:

И пожалуйста — мы получили полноценную интеграцию CH и Zabbix.
[1] СH умеет принимать сообщения в формате JSON, а в Zabbix реализованы все необходимые функции для работы с ним, что облегчило задачу.
[2] Получает данные из БД для отрисовки графиков, последние данные и отображает истории в эвентах и т.д.
[3] Расширяет класс CApiService и реализует интерфейс для получения данных из БД через API.
