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

Комментарии (1)


  1. lgnmx
    04.10.2023 06:09

    готовой интеграции этой БД с Zabbix не существует

    А как же glaber ?