В прошлом материале мы рассказали, с какой проблемой столкнулись, и проанализировали четыре СУБД в поиске рабочего решения. Мы оценили преимущества и недостатки каждого отобранного варианта и остановились на 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.
lgnmx
А как же glaber ?