• Sy chevron_right

      Временный запрет на чтение данных из таблиц истории при запуске Zabbix 3.4

      pubsub.slavino.sk / sysadmblog · Sunday, 29 November, 2020 - 08:00 edit · 2 minutes

    В статье Предзагрузка кэша значений при запуске сервера Zabbix я уже анонсировал эту функциональность и теперь настало время описать соответствующую заплатку.

    Готовую заплатку с реализацией временного запрета на чтение данных из таблиц истори при запуске сервера Zabbix можно найти по ссылке zabbix3_4_12_valuecache_grace_period.patch .

    В файл конфигурации conf/zabbix_server.conf добавим опцию с названием DBSyncersGracePeriod, которая будет принимать время с момента запуска сервера в секундах, в течение которого чтение данных из хранилищ истории будет запрещено:
    Index: zabbix-3.4.12-1+buster/conf/zabbix_server.conf
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/conf/zabbix_server.conf
    +++ zabbix-3.4.12-1+buster/conf/zabbix_server.conf
    @@ -413,6 +413,14 @@ DBUser=zabbix
    # Default:
    # StartDBSyncers=4

    +### Option: DBSyncersGracePeriod
    +# Time after server startup, during which reading from history storage will be prohibited.
    +#
    +# Mandatory: no
    +# Range: 0-86400
    +# Default:
    +# DBSyncersGracePeriod=0
    +
    ### Option: HistoryCacheSize
    # Size of history cache, in bytes.
    # Shared memory size for storing history data.
    Теперь добавим в сервер Zabbix загрузку этой опции из файла конфигурации. Отредактируем файл src/zabbix_server/server.c следующим образом:
    Index: zabbix-3.4.12-1+buster/src/zabbix_server/server.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/zabbix_server/server.c
    +++ zabbix-3.4.12-1+buster/src/zabbix_server/server.c
    @@ -176,6 +176,8 @@ int CONFIG_HOUSEKEEPING_FREQUENCY = 1;
    int CONFIG_MAX_HOUSEKEEPER_DELETE = 5000; /* applies for every separate field value */
    int CONFIG_HISTSYNCER_FORKS = 4;
    int CONFIG_HISTSYNCER_FREQUENCY = 1;
    +int CONFIG_HISTSYNCER_GRACE_PERIOD = 0;
    +
    int CONFIG_CONFSYNCER_FORKS = 1;
    int CONFIG_CONFSYNCER_FREQUENCY = 60;

    @@ -556,6 +558,8 @@ static void zbx_load_config(ZBX_TASK_EX
    MANDATORY, MIN, MAX */
    {"StartDBSyncers", &CONFIG_HISTSYNCER_FORKS, TYPE_INT,
    PARM_OPT, 1, 100},
    + {"DBSyncersGracePeriod", &CONFIG_HISTSYNCER_GRACE_PERIOD, TYPE_INT,
    + PARM_OPT, 0, SEC_PER_DAY},
    {"StartDiscoverers", &CONFIG_DISCOVERER_FORKS, TYPE_INT,
    PARM_OPT, 0, 250},
    {"StartHTTPPollers", &CONFIG_HTTPPOLLER_FORKS, TYPE_INT,
    Внесём аналогичные фиктивные изменения в Zabbix-прокси, отредактировав файл src/libs/zabbix_proxy/proxy.c следующим образом:
    Index: zabbix-3.4.12-1+buster/src/zabbix_proxy/proxy.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/zabbix_proxy/proxy.c
    +++ zabbix-3.4.12-1+buster/src/zabbix_proxy/proxy.c
    @@ -173,6 +173,7 @@ int CONFIG_PROXYDATA_FREQUENCY = 1;

    int CONFIG_HISTSYNCER_FORKS = 4;
    int CONFIG_HISTSYNCER_FREQUENCY = 1;
    +int CONFIG_HISTSYNCER_GRACE_PERIOD = 0;
    int CONFIG_CONFSYNCER_FORKS = 1;

    int CONFIG_VMWARE_FORKS = 0;
    К счастью, в исходном коде уже имеется переменная CONFIG_SERVER_STARTUP_TIME, содержащая отметку времени запуска сервера Zabbix. Осталось только внести условие в функцию zbx_history_get_values в файле src/libs/zbxhistory/history.c:
    Index: zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxhistory/history.c
    +++ zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.c
    @@ -34,6 +34,9 @@ extern char *CONFIG_HISTORY_STR_STORAGE;
    extern char *CONFIG_HISTORY_TEXT_STORAGE;
    extern char *CONFIG_HISTORY_LOG_STORAGE;

    +extern int CONFIG_SERVER_STARTUP_TIME;
    +extern int CONFIG_HISTSYNCER_GRACE_PERIOD;
    +
    zbx_history_iface_t history_ifaces[ITEM_VALUE_TYPE_MAX];

    /************************************************************************************
    @@ -162,6 +165,12 @@ int zbx_history_get_values(zbx_uint64_t
    int ret, pos;
    zbx_history_iface_t *writer = &history_ifaces[value_type];

    + if (time(NULL) - CONFIG_SERVER_STARTUP_TIME < CONFIG_HISTSYNCER_GRACE_PERIOD)
    + {
    + zabbix_log(LOG_LEVEL_DEBUG, "waiting for cache load, exiting");
    + return FAIL;
    + }
    +
    zabbix_log(LOG_LEVEL_DEBUG, "In %s() itemid:" ZBX_FS_UI64 " value_type:%d start:%d count:%d end:%d",
    __function_name, itemid, value_type, start, count, end);
    Я пробовал возвращать ответ SUCCEED, как это сделано в Glaber , но в таком случае срабатывают триггеры с функцией str. Видимо в таком случае функция str считает, что значение есть, но оно пустое. В итоге функция не находит искомую подстроку и срабатывает триггер. Если же возвращать ответ FAIL, как это сделано в заплатке выше, триггеры и вычисляемые элементы данных в подобных случаях просто переходят в неподдерживаемое состояние.

    Značky: #debian, #linux, #zabbix, #3.4, #Linux, #buster

    • Sy chevron_right

      Предзагрузка кэша значений при запуске сервера Zabbix 3.4

      pubsub.slavino.sk / sysadmblog · Sunday, 22 November, 2020 - 08:00 edit · 21 minutes

    Сервер Zabbix при запуске начинает оценивать истинность выражений триггеров, для чего ему нужны значения элементов данных, фигурирующих в выражении. В выражениях триггеров могут фигурировать функции, обращающиеся не только к последним значениям, но и к ряду значений за определённый период времени. Так как при запуске сервера Zabbix его кэш значений ещё пуст, Zabbix начинает выполнять огромное количество мелких запросов к таблицам истории. Классические транзакционные СУБД справляются с подобной нагрузкой относительно легко, хотя и для них она может оказаться тяжёлой при использовании медленной дисковой подсистемы и малом количестве оперативной памяти.

    При хранении исторических данных в таблицах со сжатием, или в таблицах с индексами, оптимизированных для выполнения запросов, извлекающих данные длинными последовательностями, запуск сервера Zabbix становится более тяжёлым. Например, при хранении исторических данных в таблицах на движке TokuDB сервера MySQL со сжатием, запуск сервера Zabbix закономерно порождает высокую нагрузку от СУБД на процессор.

    В случае с ClickHouse ситуация ещё тяжелее: для поиска единичного результата ClickHouse фактически читает с диска, расжимает и просматривает данные колонок всех секций, попадающих под условия запроса. Большой запас свободной оперативной памяти может снизить нагрузку на диски, т.к. горячие данные с файловой системы могут уместиться в буферный кэш ядра операционной системы. В ClickHouse имеется возможность использовать кэш разжатых данных, но по моим наблюдениям в этот кэш сохраняются только те данные, которые были выданы клиенту в качестве результатов запросов. Т.к. у сервера Zabbix есть собственный кэш значений, в который он сохраняет результаты прошлых запросов, то кэш разжатых данных ClickHouse оказывается бесполезен. Поэтому нагрузка на процессор, возникающая из-за необходимости многократно разжимать одни и те же данные, никуда не денется.

    В качестве паллиатива в Glaber были реализованы два подхода к решению проблемы:
    1. Сервер Zabbix, прежде чем приступить к опросу и оценке истинности выражений триггеров, заполняет кэш значений данными из таблиц истории за указанный период времени, не более указанного количества значений для каждого из элементов данных. Таким образом повышается вероятность найти необходимые данные в кэше значений при последующих попытках оценить истинность выражений триггеров.
    2. Временный запрет на чтение данных из таблиц истории, действующий после запуска сервера Zabbix. Пока действует временный запрет, кэш значений наполняется данными, снятыми непосредственно с контролируемого оборудования. По мере наполнения кэша значений этими данными становится возможным оценить выражения триггеров, не обращаясь к таблицам истории. После снятия запрета количество данных, которые необходимо прочитать из таблиц истории, значительно снижается.
    Оба решения я назвал паллиативными неспроста. У них имеется ряд недостатков, не позволяющих считать их идеальными:
    1. На практике некоторые триггеры могут использовать данные, находящиеся далеко за пределами загруженного в кэш значений периода времени. При попытках увеличить период времени загружаемых данных в кэш значений попадёт много данных, которые не будут востребованы и будут просто бесполезно занимать оперативную память. Кроме того, если сервер Zabbix по каким-то причинам долгое время не писал данные в таблицы истории, то при запуске в кэш значений не будет прочитано никаких данных и мы вернёмся к первоначальной ситуации. Наконец, запросы на чтение значений из таблиц хранилища порождают не только триггеры, но и вычисляемые элементы данных. Если у элемента данных, фигурирующего в выражении вычисляемого элемента данных, нет триггера, то значения этого элемента данных не будут загружены при запуске сервера Zabbix и по-прежнему будут извлекаться единичными запросами к хранилищу.
    2. При временном запрете не чтение данных из таблиц истории триггеры и вычисляемые элементы данных, которые не нашли в кэше значений нужных им данных, будут переходить в неподдерживаемое состояние. Их оценка будет восстанавливаться либо при накоплении достаточных данных в кэше значений, либо по истечении периода временного запрета на чтение из таблиц истории. Пока триггер находится в неподдерживаемом состоянии, он заведомо не может сработать и часть имеющихся проблем будет скрыта, пока триггер вновь не стане поддерживаемым.
    Идею реализации предзагрузки кэша значений из ClickHouse можно понять вот из этой правки в репозитории Glaber . Правка не полная, некоторые используемые в ней функции попали в другие правки.

    Проблема, во-первых в том, что у меня для хранения исторических данных используются отдельные таблицы и эта правка мне не годится.

    Во-вторых, мне не нравится в этой правке то, что библиотеки zbxdbcache и zbxhistory становятся зависимыми друг от друга. Несмотря на то, что библиотека zbxdbcache должна пользоваться библиотекой zbxhistory, в этой правке добавляетя зависимость библиотеки zbxhistory от zbxdbcache. Чтобы не возникло циклической зависимости импортируемых заголовочных файлов, в библиотеке zbxhistory просто сделано объявление, что необходимая функция находится где-то за пределами файла. Связывание библиотек друг с другом при этом выполняется компоновщиком.

    Я пошёл более сложным путём и реализовал в библиотеке zbxhistory отдельную функцию zbx_history_preload_values, подобную zbx_history_get_values, с той лишь разницей, что она загружает исторические данные не для одного элемента данных, а для списка элементов данных.

    Функция zbx_history_preload_values затем используется в библиотеке zbxdbcache, в функции zbx_vc_preload, которая, в свою очередь, вызывается уже сервером Zabbix.

    Готовую заплатку с реализацией предзагрузки данных в кэш значений из ClickHouse при запуске сервера Zabbix можно найти по ссылке zabbix3_4_12_valuecache_preloading.patch . Заплатка с реализацией временного запрета на чтение из истории после запуска сервера Zabbix будет описана в отдельной статье. Ниже я подробно опишу первую заплатку.

    Доработка файла конфигурации

    Для управления предзагрузкой кэша значений введём в файл конфигурации две новые опции: ValueCachePreloadAge и ValueCachePreloadCount. Опция ValueCachePreloadAge будет указывать период времени, значения за который необходимо загрузить в кэш. Опция ValueCachePreloadCount ограничивает количество загружаемых значений для каждого из элементов данных. В качестве декларации наших намерений доработаем пример файла конфигурации conf/zabbix_server.conf:
    Index: zabbix-3.4.12-1+buster/conf/zabbix_server.conf
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/conf/zabbix_server.conf
    +++ zabbix-3.4.12-1+buster/conf/zabbix_server.conf
    @@ -450,6 +450,26 @@ DBUser=zabbix
    # Default:
    # ValueCacheSize=8M

    +### Option: ValueCachePreloadAge
    +# Maximum age of values to prefill value cache on start server.
    +# Will be loaded history values for all items with triggers.
    +# Setting to 0 ValueCachePreloadAge and ValueCachePreloadCount disables preloading value cache.
    +#
    +# Mandatory: no
    +# Range: 0-2592000
    +# Default:
    +# ValueCachePreloadAge=0
    +
    +### Option: ValueCachePreloadCount
    +# Maximum number of values for every item to prefill value cache on start server.
    +# Will be loaded history values for all items with triggers.
    +# Setting to 0 ValueCachePreloadAge and ValueCachePreloadCount disables preloading value cache.
    +#
    +# Mandatory: no
    +# Range: 0-86400
    +# Default:
    +# ValueCachePreloadCount=0
    +
    ### Option: Timeout
    # Specifies how long we wait for agent, SNMP device or external check (in seconds).
    #

    Доработка сервера Zabbix и Zabbix-прокси

    Теперь добавим в сервер Zabbix поддержку чтения этих опций конфигурации в соответствующие переменные:
    Index: zabbix-3.4.12-1+buster/src/zabbix_server/server.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/zabbix_server/server.c
    +++ zabbix-3.4.12-1+buster/src/zabbix_server/server.c
    @@ -191,6 +191,9 @@ zbx_uint64_t CONFIG_TRENDS_CACHE_SIZE =
    zbx_uint64_t CONFIG_VALUE_CACHE_SIZE = 8 * ZBX_MEBIBYTE;
    zbx_uint64_t CONFIG_VMWARE_CACHE_SIZE = 8 * ZBX_MEBIBYTE;

    +int CONFIG_VALUE_CACHE_PRELOAD_AGE = 0;
    +int CONFIG_VALUE_CACHE_PRELOAD_COUNT = 0;
    +
    int CONFIG_UNREACHABLE_PERIOD = 45;
    int CONFIG_UNREACHABLE_DELAY = 15;
    int CONFIG_UNAVAILABLE_DELAY = 60;
    @@ -591,6 +594,10 @@ static void zbx_load_config(ZBX_TASK_EX
    PARM_OPT, 128 * ZBX_KIBIBYTE, __UINT64_C(2) * ZBX_GIBIBYTE},
    {"ValueCacheSize", &CONFIG_VALUE_CACHE_SIZE, TYPE_UINT64,
    PARM_OPT, 0, __UINT64_C(64) * ZBX_GIBIBYTE},
    + {"ValueCachePreloadAge", &CONFIG_VALUE_CACHE_PRELOAD_AGE, TYPE_INT,
    + PARM_OPT, 0, SEC_PER_MONTH},
    + {"ValueCachePreloadCount", &CONFIG_VALUE_CACHE_PRELOAD_COUNT, TYPE_INT,
    + PARM_OPT, 0, 86400},
    {"CacheUpdateFrequency", &CONFIG_CONFSYNCER_FREQUENCY, TYPE_INT,
    PARM_OPT, 1, SEC_PER_HOUR},
    {"HousekeepingFrequency", &CONFIG_HOUSEKEEPING_FREQUENCY, TYPE_INT,
    Аналогичные фиктивные изменения внесём в Zabbix-прокси:
    Index: zabbix-3.4.12-1+buster/src/zabbix_proxy/proxy.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/zabbix_proxy/proxy.c
    +++ zabbix-3.4.12-1+buster/src/zabbix_proxy/proxy.c
    @@ -187,6 +187,9 @@ zbx_uint64_t CONFIG_TRENDS_CACHE_SIZE =
    zbx_uint64_t CONFIG_VALUE_CACHE_SIZE = 0;
    zbx_uint64_t CONFIG_VMWARE_CACHE_SIZE = 8 * ZBX_MEBIBYTE;

    +int CONFIG_VALUE_CACHE_PRELOAD_AGE = 0;
    +int CONFIG_VALUE_CACHE_PRELOAD_COUNT = 0;
    +
    int CONFIG_UNREACHABLE_PERIOD = 45;
    int CONFIG_UNREACHABLE_DELAY = 15;
    int CONFIG_UNAVAILABLE_DELAY = 60;

    Массив загруженных значений

    Для временного хранения значений, загруженных из таблиц истории, но ещё не помещённых в кэш значений, нам понадобится новая структура данных - массив из элементов, содержащих идентификатор элемента данных, тип его значения, отметку времени и само значение. Структура данных, которая будет содержать все необходимые поля одного элемента, будет называться zbx_valuecache_record_t и будет сделана по аналогии со структурой zbx_history_record_t, объявленной в файле include/zbxhistory.h.

    Для работы с массивом, состоящим из таких структур, нужно будет реализовать соответствующие операции. К счастью, в Zabbix уже есть макроопределение ZBX_VECTOR_DECL(<название>, <тип_элемента>), позволяющее создавать функции zbx_vector_<название>_<операция> для работы с массивами необходимых структур данных. Правда, эти функции не умеют очищать и освобождать память, динамически распределённую внутри самих элементов данных, поэтому нужно определить дополнительные функции, которые будут это делать.

    Итак, внесём необходимые правки в файлы src/libs/zbxhistory/history.h и src/libs/zbxhistory/history.c:
    Index: zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.h
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxhistory/history.h
    +++ zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.h
    @@ -29,6 +29,8 @@ typedef void (*zbx_history_destroy_func_
    typedef int (*zbx_history_add_values_func_t)(struct zbx_history_iface *hist, const zbx_vector_ptr_t *history);
    typedef int (*zbx_history_get_values_func_t)(struct zbx_history_iface *hist, zbx_uint64_t itemid, int start,
    int count, int end, zbx_vector_history_record_t *values);
    +typedef int (*zbx_history_preload_values_func_t)(struct zbx_history_iface *hist, const zbx_vector_uint64_t *itemids,
    + int age, int count, zbx_vector_valuecache_record_t *values);
    typedef void (*zbx_history_flush_func_t)(struct zbx_history_iface *hist);

    struct zbx_history_iface
    @@ -40,6 +42,7 @@ struct zbx_history_iface
    zbx_history_destroy_func_t destroy;
    zbx_history_add_values_func_t add_values;
    zbx_history_get_values_func_t get_values;
    + zbx_history_preload_values_func_t preload_values;
    zbx_history_flush_func_t flush;
    };
    Index: zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxhistory/history.c
    +++ zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.c
    @@ -26,6 +26,7 @@
    #include "../zbxalgo/vectorimpl.h"

    ZBX_VECTOR_IMPL(history_record, zbx_history_record_t);
    +ZBX_VECTOR_IMPL(valuecache_record, zbx_valuecache_record_t);

    extern char *CONFIG_HISTORY_STORAGE;
    extern char *CONFIG_HISTORY_UINT_STORAGE;
    @@ -248,6 +313,27 @@ void zbx_history_record_vector_destroy(z

    /******************************************************************************
    * *
    + * Function: zbx_valuecache_record_vector_destroy *
    + * *
    + * Purpose: destroys value vector and frees resources allocated for it *
    + * *
    + * Parameters: vector - [IN] the value vector *
    + * *
    + * Comments: Use this function to destroy value vectors created by *
    + * zbx_vc_get_values_by_* functions. *
    + * *
    + ******************************************************************************/
    +void zbx_valuecache_record_vector_destroy(zbx_vector_valuecache_record_t *vector)
    +{
    + if (NULL != vector->values)
    + {
    + zbx_valuecache_record_vector_clean(vector);
    + zbx_vector_valuecache_record_destroy(vector);
    + }
    +}
    +
    +/******************************************************************************
    + * *
    * Function: zbx_history_record_clear *
    * *
    * Purpose: frees resources allocated by a cached value *
    @@ -271,6 +357,28 @@ void zbx_history_record_clear(zbx_histor

    /******************************************************************************
    * *
    + * Function: zbx_valuecache_record_clear *
    + * *
    + * Purpose: frees resources allocated by a cached value *
    + * *
    + * Parameters: value - [IN] the cached value to clear *
    + * *
    + ******************************************************************************/
    +void zbx_valuecache_record_clear(zbx_valuecache_record_t *value)
    +{
    + switch (value->value_type)
    + {
    + case ITEM_VALUE_TYPE_STR:
    + case ITEM_VALUE_TYPE_TEXT:
    + zbx_free(value->value.str);
    + break;
    + case ITEM_VALUE_TYPE_LOG:
    + history_logfree(value->value.log);
    + }
    +}
    +
    +/******************************************************************************
    + * *
    * Function: zbx_history_value2str *
    * *
    * Purpose: converts history value to string format *
    @@ -329,3 +437,30 @@ void zbx_history_record_vector_clean(zbx

    zbx_vector_history_record_clear(vector);
    }
    +
    +/******************************************************************************
    + * *
    + * Function: zbx_valuecache_record_vector_clean *
    + * *
    + * Purpose: releases resources allocated to store history records *
    + * *
    + * Parameters: vector - [IN] the history record vector *
    + * *
    + ******************************************************************************/
    +void zbx_valuecache_record_vector_clean(zbx_vector_valuecache_record_t *vector)
    +{
    + int i;
    +
    + for (i = 0; i < vector->values_num; i++)
    + switch (vector->values[i].value_type)
    + {
    + case ITEM_VALUE_TYPE_STR:
    + case ITEM_VALUE_TYPE_TEXT:
    + zbx_free(vector->values[i].value.str);
    + break;
    + case ITEM_VALUE_TYPE_LOG:
    + history_logfree(vector->values[i].value.log);
    + }
    +
    + zbx_vector_valuecache_record_clear(vector);
    +}

    Базовая доработка zbxhistory

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

    Добавим функцию zbx_history_preload_values в файл src/libs/zbxhistory/history.c:
    Index: zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxhistory/history.c
    +++ zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.c
    @@ -189,6 +190,70 @@ int zbx_history_get_values(zbx_uint64_t

    /************************************************************************************
    * *
    + * Function: zbx_history_preload_values *
    + * *
    + * Purpose: gets item values from history storage *
    + * *
    + * Parameters: itemids - [IN] the required item identifiers *
    + * value_type - [IN] the item value type *
    + * age - [IN] the maximum age of values in seconds *
    + * count - [IN] the maximum number of item values to read *
    + * values - [OUT] the item history data values *
    + * *
    + * Return value: SUCCEED - the history data were read successfully *
    + * FAIL - otherwise *
    + * *
    + * Comments: This function reads <count> values of every specified item, *
    + * but not older than <age> in seconds. *
    + * If <count> is zero, will be readed all values not older than <age>. *
    + * If <age> is zero, will be readed <count> values of every specified *
    + * item. *
    + * If <count> and <age> is zeros both, loading will return no data. *
    + * *
    + ************************************************************************************/
    +int zbx_history_preload_values(const zbx_vector_uint64_t *itemids, int value_type, int age,
    + int count, zbx_vector_valuecache_record_t *values)
    +{
    + const char *__function_name = "zbx_history_preload_values";
    + int ret, pos;
    + zbx_history_iface_t *writer = &history_ifaces[value_type];
    +
    + zabbix_log(LOG_LEVEL_DEBUG, "In %s() value_type:%d age:%d count:%d",
    + __function_name, value_type, age, count);
    +
    + if (NULL == writer->preload_values)
    + {
    + zabbix_log(LOG_LEVEL_DEBUG, "End of %s(): value_type:%d, "
    + "no function for preloading values", __function_name, value_type);
    + return SUCCEED;
    + }
    +
    + pos = values->values_num;
    + ret = writer->preload_values(writer, itemids, age, count, values);
    +
    + if (SUCCEED == ret && SUCCEED == zabbix_check_log_level(LOG_LEVEL_TRACE))
    + {
    + int i;
    + char buffer[MAX_STRING_LEN];
    +
    + for (i = pos; i < values->values_num; i++)
    + {
    + zbx_valuecache_record_t *h = &values->values[i];
    +
    + zbx_history_value2str(buffer, sizeof(buffer), &h->value, value_type);
    + zabbix_log(LOG_LEVEL_TRACE, ZBX_FS_UI64 " %d.%09d %s",
    + h->itemid, h->timestamp.sec, h->timestamp.ns, buffer);
    + }
    + }
    +
    + zabbix_log(LOG_LEVEL_DEBUG, "End of %s():%s values:%d", __function_name, zbx_result_string(ret),
    + values->values_num - pos);
    +
    + return ret;
    +}
    +
    +/************************************************************************************
    + * *
    * Function: zbx_history_requires_trends *
    * *
    * Purpose: checks if the value type requires trends data calculations *
    Внесём объявление этой функции в заголовочный файл include/zbxhistory.h:
    Index: zabbix-3.4.12-1+buster/include/zbxhistory.h
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/include/zbxhistory.h
    +++ zabbix-3.4.12-1+buster/include/zbxhistory.h
    @@ -47,6 +63,8 @@ int zbx_history_init(char **error);
    void zbx_history_add_values(const zbx_vector_ptr_t *values);
    int zbx_history_get_values(zbx_uint64_t itemid, int value_type, int start, int count, int end,
    zbx_vector_history_record_t *values);
    +int zbx_history_preload_values(const zbx_vector_uint64_t *itemids, int value_type, int age,
    + int count, zbx_vector_valuecache_record_t *values);

    int zbx_history_requires_trends(int value_type);
    Функция zbx_history_preload_values обращается к реализациям подобной же функции для конкретных типов хранилищ. Нужно предусмотреть в структуре с реализацией типа хранилища zbx_history_iface соответствующее поле preload_values и объявить определение типа с указателем на функцию нужной нам сигнатуры. Обе правки внесём в заголовочный файл src/libs/zbxhistory/history.h:
    Index: zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.h
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxhistory/history.h
    +++ zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.h
    @@ -29,6 +29,8 @@ typedef void (*zbx_history_destroy_func_
    typedef int (*zbx_history_add_values_func_t)(struct zbx_history_iface *hist, const zbx_vector_ptr_t *history);
    typedef int (*zbx_history_get_values_func_t)(struct zbx_history_iface *hist, zbx_uint64_t itemid, int start,
    int count, int end, zbx_vector_history_record_t *values);
    +typedef int (*zbx_history_preload_values_func_t)(struct zbx_history_iface *hist, const zbx_vector_uint64_t *itemids,
    + int age, int count, zbx_vector_valuecache_record_t *values);
    typedef void (*zbx_history_flush_func_t)(struct zbx_history_iface *hist);

    struct zbx_history_iface
    @@ -40,6 +42,7 @@ struct zbx_history_iface
    zbx_history_destroy_func_t destroy;
    zbx_history_add_values_func_t add_values;
    zbx_history_get_values_func_t get_values;
    + zbx_history_preload_values_func_t preload_values;
    zbx_history_flush_func_t flush;
    };
    Библиотека zbxhistory почти готова, осталось внести правки в конкретные реализации различных типов хранилищ.

    Доработка хранилищ zbxhistory

    Первым делом доработаем хранилища типов SQL и Elasticsearch. Т.к. реализовывать предзагрузку в этих хранилищах я не собираюсь, но поле preload_values структуры zbx_history_iface всё же нужно инициализировать, сделаем это, отредактировав файлы src/libs/zbxhistory/history_sql.c и src/libs/zbxhistory/history_elastic.c следующим образом:
    Index: zabbix-3.4.12-1+buster/src/libs/zbxhistory/history_elastic.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxhistory/history_elastic.c
    +++ zabbix-3.4.12-1+buster/src/libs/zbxhistory/history_elastic.c
    @@ -934,6 +934,7 @@ int zbx_history_elastic_init(zbx_history
    hist->add_values = elastic_add_values;
    hist->flush = elastic_flush;
    hist->get_values = elastic_get_values;
    + hist->preload_values = NULL;
    hist->requires_trends = 0;

    return SUCCEED;
    Index: zabbix-3.4.12-1+buster/src/libs/zbxhistory/history_sql.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxhistory/history_sql.c
    +++ zabbix-3.4.12-1+buster/src/libs/zbxhistory/history_sql.c
    @@ -718,6 +718,7 @@ int zbx_history_sql_init(zbx_history_ifa
    hist->add_values = sql_add_values;
    hist->flush = sql_flush;
    hist->get_values = sql_get_values;
    + hist->preload_values = NULL;

    switch (value_type)
    {
    Теперь займёмся хранилищем ClickHouse. Реализация функции clickhouse_preload_values выполнена по аналогии с функцией clickhouse_get_value. Введена также вспомогательная функция history_parse_valuecache, которая извлекает данные из строки в формате JSON и заполняет структуру типа zbx_valuecache_record_t. Внесённые правки в файле src/libs/zbxhistory/history_clickhouse.c выглядят следующим образом:
    Index: zabbix-3.4.12-1+buster/src/libs/zbxhistory/history_clickhouse.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxhistory/history_clickhouse.c
    +++ zabbix-3.4.12-1+buster/src/libs/zbxhistory/history_clickhouse.c
    @@ -166,6 +166,67 @@ out:
    return ret;
    }

    +static int history_parse_valuecache(struct zbx_json_parse *jp, unsigned char value_type,
    + zbx_valuecache_record_t *vcr)
    +{
    + char *value = NULL;
    + size_t value_alloc = 0;
    + int ret = FAIL;
    +
    + if (SUCCEED != zbx_json_value_by_name_dyn(jp, "itemid", &value, &value_alloc))
    + goto out;
    +
    + vcr->itemid = atoi(value);
    +
    + if (SUCCEED != zbx_json_value_by_name_dyn(jp, "clock", &value, &value_alloc))
    + goto out;
    +
    + vcr->timestamp.sec = atoi(value);
    +
    + if (SUCCEED != zbx_json_value_by_name_dyn(jp, "ns", &value, &value_alloc))
    + goto out;
    +
    + vcr->timestamp.ns = atoi(value);
    +
    + if (SUCCEED != zbx_json_value_by_name_dyn(jp, "value", &value, &value_alloc))
    + goto out;
    +
    + vcr->value = history_str2value(value, value_type);
    +
    + if (ITEM_VALUE_TYPE_LOG == value_type)
    + {
    +
    + if (SUCCEED != zbx_json_value_by_name_dyn(jp, "timestamp", &value, &value_alloc))
    + goto out;
    +
    + vcr->value.log->timestamp = atoi(value);
    +
    + if (SUCCEED != zbx_json_value_by_name_dyn(jp, "logeventid", &value, &value_alloc))
    + goto out;
    +
    + vcr->value.log->logeventid = atoi(value);
    +
    + if (SUCCEED != zbx_json_value_by_name_dyn(jp, "severity", &value, &value_alloc))
    + goto out;
    +
    + vcr->value.log->severity = atoi(value);
    +
    + if (SUCCEED != zbx_json_value_by_name_dyn(jp, "source", &value, &value_alloc))
    + goto out;
    +
    + vcr->value.log->source = zbx_strdup(NULL, value);
    + }
    +
    + vcr->value_type = value_type;
    +
    + ret = SUCCEED;
    +
    +out:
    + zbx_free(value);
    +
    + return ret;
    +}
    +
    static void clickhouse_log_error(CURL *handle, CURLcode error, const char *errbuf)
    {
    long http_code;
    @@ -457,6 +518,158 @@ static void clickhouse_destroy(zbx_histo

    /************************************************************************************
    * *
    + * Function: clickhouse_preload_values *
    + * *
    + * Purpose: gets history data from history storage for warming up the values cache *
    + * *
    + * Parameters: hist - [IN] the history storage interface *
    + * age - [IN] the maximum age of values in seconds *
    + * count - [IN] the maximum number of item values to read *
    + * values - [OUT] the item history data values *
    + * *
    + * Return value: SUCCEED - the history data were read successfully *
    + * FAIL - otherwise *
    + * *
    + * Comments: This function reads <count> values of every specified item, *
    + * but not older than <age> in seconds. *
    + * If <count> is zero, will be readed all values not older than <age>. *
    + * If <age> is zero, will be readed <count> values of every specified *
    + * item. *
    + * If <count> and <age> is zeros both, loading will return no data. *
    + * *
    + ************************************************************************************/
    +static int clickhouse_preload_values(zbx_history_iface_t *hist, const zbx_vector_uint64_t *itemids,
    + int age, int count, zbx_vector_valuecache_record_t *values)
    +{
    + const char *__function_name = "clickhouse_preload_values";
    +
    + zbx_clickhouse_data_t *data = hist->data;
    + size_t sql_alloc = 0, sql_offset;
    + int ret = SUCCEED, num = 0, i;
    + CURLcode err;
    + struct curl_slist *curl_headers = NULL;
    + char *sql = NULL, errbuf[CURL_ERROR_SIZE];
    + const char *p = NULL;
    + struct zbx_json_parse jp, jp_sub, jp_data, jp_item;
    + zbx_valuecache_record_t vcr;
    +
    + zabbix_log(LOG_LEVEL_DEBUG, "In %s()", __function_name);
    +
    + if ((0 == age) && (0 == count))
    + {
    + zabbix_log(LOG_LEVEL_INFORMATION, "skipped preload from ClickHouse table %s",
    + value_type_table[hist->value_type]);
    + return SUCCEED;
    + }
    +
    + if (0 == itemids->values_num)
    + {
    + zabbix_log(LOG_LEVEL_INFORMATION, "nothing to preload from ClickHouse table %s",
    + value_type_table[hist->value_type]);
    + return SUCCEED;
    + }
    +
    + if (NULL == (data->handle = curl_easy_init()))
    + {
    + zabbix_log(LOG_LEVEL_ERR, "cannot initialize cURL session");
    +
    + return FAIL;
    + }
    +
    + if (ITEM_VALUE_TYPE_LOG == hist->value_type)
    + zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset,
    + "SELECT itemid, clock, ns, value, timestamp, source, severity, logeventid"
    + " FROM %s"
    + " WHERE itemid IN (" ZBX_FS_UI64,
    + value_type_table[hist->value_type],
    + itemids->values[0]);
    + else
    + zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset,
    + "SELECT itemid, clock, ns, value"
    + " FROM %s"
    + " WHERE itemid IN (" ZBX_FS_UI64,
    + value_type_table[hist->value_type],
    + itemids->values[0]);
    +
    + for (i = 1; i < itemids->values_num; i++)
    + zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, "," ZBX_FS_UI64, itemids->values[i]);
    +
    + zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, ")");
    +
    + if (age > 0)
    + zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset,
    + " AND clock > toUInt32(now()) - %d",
    + age);
    +
    + zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, " ORDER BY itemid ASC, clock DESC");
    +
    + if (count > 0)
    + zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset,
    + " LIMIT %d BY itemid",
    + count);
    +
    + zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, " FORMAT JSON");
    +
    + curl_headers = curl_slist_append(curl_headers, "Content-Type: application/json");
    +
    + curl_easy_setopt(data->handle, CURLOPT_URL, data->base_url);
    + curl_easy_setopt(data->handle, CURLOPT_POSTFIELDS, sql);
    + curl_easy_setopt(data->handle, CURLOPT_WRITEFUNCTION, curl_write_cb);
    + curl_easy_setopt(data->handle, CURLOPT_WRITEDATA, &page_r);
    + curl_easy_setopt(data->handle, CURLOPT_HTTPHEADER, curl_headers);
    + curl_easy_setopt(data->handle, CURLOPT_FAILONERROR, 1L);
    + curl_easy_setopt(data->handle, CURLOPT_ERRORBUFFER, errbuf);
    +
    + zabbix_log(LOG_LEVEL_DEBUG, "sending query to %s; post data: %s", data->base_url, sql);
    +
    + page_r.offset = 0;
    + *errbuf = '\0';
    + if (CURLE_OK != (err = curl_easy_perform(data->handle)))
    + {
    + clickhouse_log_error(data->handle, err, errbuf);
    + ret = FAIL;
    + goto out;
    + }
    +
    + zabbix_log(LOG_LEVEL_DEBUG, "received from ClickHouse: %s", page_r.data);
    +
    + zbx_json_open(page_r.data, &jp);
    + zbx_json_brackets_open(jp.start, &jp_sub);
    + zbx_json_brackets_by_name(&jp_sub, "data", &jp_data);
    +
    + while (NULL != (p = zbx_json_next(&jp_data, p)))
    + {
    + if (SUCCEED != zbx_json_brackets_open(p, &jp_item))
    + continue;
    +
    + if (SUCCEED != history_parse_valuecache(&jp_item, hist->value_type, &vcr))
    + continue;
    +
    + zbx_vector_valuecache_record_append_ptr(values, &vcr);
    + num++;
    + }
    +
    + if (0 < num)
    + zabbix_log(LOG_LEVEL_INFORMATION, "%d values were preloaded from ClickHouse table %s",
    + num, value_type_table[hist->value_type]);
    + else
    + zabbix_log(LOG_LEVEL_INFORMATION, "no values were preloaded from ClickHouse table %s",
    + value_type_table[hist->value_type]);
    +
    +out:
    + clickhouse_close(hist);
    +
    + curl_slist_free_all(curl_headers);
    +
    + zbx_free(sql);
    +
    + zabbix_log(LOG_LEVEL_DEBUG, "End of %s()", __function_name);
    +
    + return ret;
    +}
    +
    +/************************************************************************************
    + * *
    * Function: clickhouse_get_values *
    * *
    * Purpose: gets item history data from history storage *
    @@ -723,6 +936,7 @@ int zbx_history_clickhouse_init(zbx_hist
    hist->add_values = clickhouse_add_values;
    hist->flush = clickhouse_flush;
    hist->get_values = clickhouse_get_values;
    + hist->preload_values = clickhouse_preload_values;
    hist->requires_trends = 0;

    return SUCCEED;

    Доработка zbxdbcache

    Нам понадобится функция, которая вернёт список идентификаторов элементов данных, к которым привязаны триггеры и для которых поэтому нужно загрузить данные в кэш значений. Для загрузки значений разных типов нам понядобятся разные списки, т.к. значения хранятся в разных таблицах и могут обрабатываться разными реализациями хранилищ. Добавим в файл src/libs/zbxdbcache/dbconfig.c функцию с именем DCconfig_get_itemids_by_valuetype, которая будет возвращать идентификаторы элементов данных с указанным типом значения:
    Index: zabbix-3.4.12-1+buster/src/libs/zbxdbcache/dbconfig.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxdbcache/dbconfig.c
    +++ zabbix-3.4.12-1+buster/src/libs/zbxdbcache/dbconfig.c
    @@ -6101,6 +6101,39 @@ void DCconfig_get_items_by_itemids(DC_IT

    /******************************************************************************
    * *
    + * Function: DCconfig_get_itemids_by_valuetype *
    + * *
    + * Purpose: Get item IDs for specified value type *
    + * *
    + * Parameters: value_type - [IN] value type *
    + * itemids - [OUT] vector with item IDs *
    + * Return value: *
    + * Number of elements found *
    + * *
    + ******************************************************************************/
    +void DCconfig_get_itemids_by_valuetype(int value_type, zbx_vector_uint64_t *itemids)
    +{
    + const ZBX_DC_ITEM *item;
    +
    + zbx_hashset_iter_t iter;
    +
    + LOCK_CACHE;
    +
    + zbx_hashset_iter_reset(&config->items, &iter);
    +
    + while (NULL != (item = zbx_hashset_iter_next(&iter)))
    + {
    + if ((item->value_type == value_type) && (NULL != item->triggers))
    + {
    + zbx_vector_uint64_append(itemids, item->itemid);
    + }
    + }
    +
    + UNLOCK_CACHE;
    +}
    +
    +/******************************************************************************
    + * *
    * Function: dc_preproc_item_init *
    * *
    * Purpose: initialize new preprocessor item from configuration cache *
    Эта функция будет нужна в файле src/libs/zbxdbcache/valuecache.c, поэтому для её использования там её нужно объявить в заголовочном файле include/dbcache.h:
    Index: zabbix-3.4.12-1+buster/include/dbcache.h
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/include/dbcache.h
    +++ zabbix-3.4.12-1+buster/include/dbcache.h
    @@ -558,6 +558,7 @@ void DCconfig_clean_items(DC_ITEM *items
    int DCget_host_by_hostid(DC_HOST *host, zbx_uint64_t hostid);
    void DCconfig_get_hosts_by_itemids(DC_HOST *hosts, const zbx_uint64_t *itemids, int *errcodes, size_t num);
    void DCconfig_get_items_by_keys(DC_ITEM *items, zbx_host_key_t *keys, int *errcodes, size_t num);
    +void DCconfig_get_itemids_by_valuetype(int value_type, zbx_vector_uint64_t *itemids);
    void DCconfig_get_items_by_itemids(DC_ITEM *items, const zbx_uint64_t *itemids, int *errcodes, size_t num);
    void DCconfig_get_preprocessable_items(zbx_hashset_t *items, int *timestamp);
    void DCconfig_get_functions_by_functionids(DC_FUNCTION *functions,
    Добавим в файл src/libs/zbxdbcache/valuecache.c функцию zbx_vc_preload_values, которая будет принимать массив значений типа zbx_valuecache_record_t, загруженный функцией zbx_history_preload_values и добавлять их в кэш значений. Функция zbx_vc_preload_values сделана на основе функции zbx_vc_add_value, добавляющей в кэш значений одно значение.
    Index: zabbix-3.4.12-1+buster/src/libs/zbxdbcache/valuecache.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxdbcache/valuecache.c
    +++ zabbix-3.4.12-1+buster/src/libs/zbxdbcache/valuecache.c
    @@ -2609,6 +2615,65 @@ out:
    }

    /******************************************************************************
    + * *
    + * Function: zbx_vc_preload_values *
    + * *
    + * Purpose: adds items values to the value cache *
    + * *
    + * Parameters: values - [IN] values, that needed to load to value cache *
    + * *
    + ******************************************************************************/
    +void zbx_vc_preload_values(zbx_vector_valuecache_record_t *values)
    +{
    + zbx_vc_item_t *item;
    + int i, failed = 0;
    + zbx_valuecache_record_t *value;
    +
    + if (NULL == vc_cache)
    + return;
    +
    + vc_try_lock();
    +
    + /* Adding values from the tail to the head, because the list of values
    + * is ordered in descending order of clock field */
    + for (i = values->values_num - 1; i >= 0; i--)
    + {
    + value = &values->values[i];
    +
    + if (NULL != (item = zbx_hashset_search(&vc_cache->items, &value->itemid)))
    + {
    + zbx_history_record_t record = {value->timestamp, value->value};
    +
    + if (0 == (item->state & ZBX_ITEM_STATE_REMOVE_PENDING))
    + {
    + vc_item_addref(item);
    +
    + /* If the new value type does not match the item's type in cache we can't */
    + /* change the cache because other processes might still be accessing it */
    + /* at the same time. The only thing that can be done - mark it for removal */
    + /* so it could be added later with new type. */
    + /* Also mark it for removal if the value adding failed. In this case we */
    + /* won't have the latest data in cache - so the requests must go directly */
    + /* to the database. */
    + if (item->value_type != value->value_type ||
    + FAIL == vch_item_add_value_at_head(item, &record))
    + {
    + item->state |= ZBX_ITEM_STATE_REMOVE_PENDING;
    + failed++;
    + }
    +
    + vc_item_release(item);
    + }
    + }
    + }
    +
    + zabbix_log(LOG_LEVEL_INFORMATION, "%d values successfully loaded to value cache",
    + values->values_num - failed);
    +
    + vc_try_unlock();
    +}
    +
    +/******************************************************************************
    * *
    * Function: zbx_vc_destroy *
    * *
    Теперь осталось реализовать функцию zbx_vc_preload в том же файле src/libs/zbxdbcache/valuecache.c, которая пройдётся по всем типам значений, сформирует список элементов данных этого типа значений при помощи функции DCconfig_get_itemids_by_valuetype, запросит значения у соответствующего хранилища при помощи функции zbx_history_preload_values и отправит эти значения в кэш значений при помощи функции zbx_vc_preload_values. Настройки периода загрузки и ограничение на количество значений для одного элемента данных функция zbx_vc_preload возьмёт из переменных CONFIG_VALUE_CACHE_PRELOAD_AGE и CONFIG_VALUE_CACHE_PRELOAD_COUNT, которые были заполнены сервером Zabbix значениями из файла конфигурации.
    Index: zabbix-3.4.12-1+buster/src/libs/zbxdbcache/valuecache.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxdbcache/valuecache.c
    +++ zabbix-3.4.12-1+buster/src/libs/zbxdbcache/valuecache.c
    @@ -75,6 +75,12 @@ static int vc_locked = 0;
    /* the value cache size */
    extern zbx_uint64_t CONFIG_VALUE_CACHE_SIZE;

    +/* the maximum age of data to preloading to value cache */
    +extern int CONFIG_VALUE_CACHE_PRELOAD_AGE;
    +
    +/* the maximum number of values per one item to preloading to value cache */
    +extern int CONFIG_VALUE_CACHE_PRELOAD_COUNT;
    +
    ZBX_MEM_FUNC_IMPL(__vc, vc_mem)

    #define VC_STRPOOL_INIT_SIZE (1000)
    @@ -2674,6 +2680,35 @@ out:

    /******************************************************************************
    * *
    + * Function: zbx_vc_preload *
    + * *
    + * Purpose: preload value cache *
    + * *
    + ******************************************************************************/
    +void zbx_vc_preload()
    +{
    + zbx_vector_valuecache_record_t values;
    + zbx_vector_uint64_t itemids;
    + int value_type;
    +
    + zbx_vector_valuecache_record_create(&values);
    +
    + zbx_vector_uint64_create(&itemids);
    + for (value_type = 0; value_type < ITEM_VALUE_TYPE_MAX; value_type++)
    + {
    + DCconfig_get_itemids_by_valuetype(value_type, &itemids);
    + zbx_history_preload_values(&itemids, value_type, CONFIG_VALUE_CACHE_PRELOAD_AGE,
    + CONFIG_VALUE_CACHE_PRELOAD_COUNT, &values);
    + zbx_vector_uint64_clear(&itemids);
    + }
    + zbx_vector_uint64_destroy(&itemids);
    +
    + zbx_vc_preload_values(&values);
    + zbx_vector_valuecache_record_destroy(&values);
    +}
    +
    +/******************************************************************************
    + * *
    * Function: zbx_vc_destroy *
    * *
    * Purpose: destroys value cache *
    Добавим объявление этой функции в файл src/libs/zbxdbcache/valuecache.h, чтобы её можно было вызвать в процессе запуска сервера Zabbix:
    Index: zabbix-3.4.12-1+buster/src/libs/zbxdbcache/valuecache.h
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxdbcache/valuecache.h
    +++ zabbix-3.4.12-1+buster/src/libs/zbxdbcache/valuecache.h
    @@ -76,6 +76,8 @@ zbx_vc_stats_t;

    int zbx_vc_init(char **error);

    +void zbx_vc_preload();
    +
    void zbx_vc_destroy(void);

    void zbx_vc_lock(void);

    Доработка сервера Zabbix

    Сделаем финальный штрих и добавим вызов функции zbx_vc_preload в файл src/zabbix_server/server.c после инициализации кэша значений, после загрузки конфигурации из базы данных, но перед тем, как главный процесс сервера Zabbix запустит подчинённые процессы:
    Index: zabbix-3.4.12-1+buster/src/zabbix_server/server.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/zabbix_server/server.c
    +++ zabbix-3.4.12-1+buster/src/zabbix_server/server.c
    @@ -1021,6 +1028,9 @@ int MAIN_ZABBIX_ENTRY(int flags)

    DBclose();

    + /* preload values to values cache */
    + zbx_vc_preload();
    +
    if (0 != CONFIG_IPMIPOLLER_FORKS)
    CONFIG_IPMIMANAGER_FORKS = 1;

    Выбор значений для опций конфигурации

    Для оценки среднего количества значений, используемого в триггерах, можно воспользоваться таким запросом:
    SELECT AVG(CASE
    WHEN functions.parameter LIKE '%s' THEN CAST(REPLACE(functions.parameter, 's', '') AS DECIMAL)
    WHEN functions.parameter LIKE '%m' THEN CAST(REPLACE(functions.parameter, 'm', '') AS DECIMAL) * 60
    WHEN functions.parameter LIKE '%h' THEN CAST(REPLACE(functions.parameter, 'h', '') AS DECIMAL) * 3600
    WHEN functions.parameter LIKE '%d' THEN CAST(REPLACE(functions.parameter, 'd', '') AS DECIMAL) * 86400
    WHEN functions.parameter LIKE '%w' THEN CAST(REPLACE(functions.parameter, 'w', '') AS DECIMAL) * 604800
    ELSE 0
    END
    /
    CASE
    WHEN items.delay LIKE '%s' THEN CAST(REPLACE(items.delay, 's', '') AS DECIMAL)
    WHEN items.delay LIKE '%m' THEN CAST(REPLACE(items.delay, 'm', '') AS DECIMAL) * 60
    WHEN items.delay LIKE '%h' THEN CAST(REPLACE(items.delay, 'h', '') AS DECIMAL) * 3600
    WHEN items.delay LIKE '%d' THEN CAST(REPLACE(items.delay, 'd', '') AS DECIMAL) * 86400
    WHEN items.delay LIKE '%w' THEN CAST(REPLACE(items.delay, 'w', '') AS DECIMAL) * 604800
    ELSE 0
    END) AS count
    FROM functions
    JOIN triggers ON triggers.triggerid = functions.triggerid
    AND triggers.status = 0
    JOIN items ON items.itemid = functions.itemid
    AND items.status = 0
    JOIN hosts ON hosts.hostid = items.hostid
    AND hosts.status = 0
    WHERE functions.function IN ('min', 'max', 'avg', 'count', 'delta', 'nodata');
    Поскольку это среднее значение, то в опцию ValueCachePreloadCount можно вписать удвоенное значение из результата запроса.

    Для оценки среднего временного интервала, используемого в триггерах, можно воспользоваться таким запросом:
    SELECT AVG(CASE
    WHEN functions.parameter LIKE '%s' THEN CAST(REPLACE(functions.parameter, 's', '') AS DECIMAL)
    WHEN functions.parameter LIKE '%m' THEN CAST(REPLACE(functions.parameter, 'm', '') AS DECIMAL) * 60
    WHEN functions.parameter LIKE '%h' THEN CAST(REPLACE(functions.parameter, 'h', '') AS DECIMAL) * 3600
    WHEN functions.parameter LIKE '%d' THEN CAST(REPLACE(functions.parameter, 'd', '') AS DECIMAL) * 86400
    WHEN functions.parameter LIKE '%w' THEN CAST(REPLACE(functions.parameter, 'w', '') AS DECIMAL) * 604800
    ELSE 0
    END) AS age
    FROM functions
    JOIN triggers ON triggers.triggerid = functions.triggerid
    AND triggers.status = 0
    JOIN items ON items.itemid = functions.itemid
    AND items.status = 0
    JOIN hosts ON hosts.hostid = items.hostid
    AND hosts.status = 0
    WHERE functions.function IN ('min', 'max', 'avg', 'count', 'delta', 'nodata');
    Аналогично, поскольку это среднее значение, то в опцию ValueCachePreloadAge можно вписать удвоенное значение из результата запроса.

    Если вам захочется, чтобы в кэш гарантированно попадали данные для расчёта всех триггеров, тогда можно заменить в запросах функцию AVG на MAX и подставить в файл конфигурации полученные значения. Если вам хватит оперативной памяти для такого большого кэша значений, то вам повезло :)

    Značky: #clickhouse, #linux, #zabbix, #debian, #Linux, #buster, #3.4

    • Sy chevron_right

      Поддержка хранилища ClickHouse в сервере Zabbix 3.4

      pubsub.slavino.sk / sysadmblog · Sunday, 15 November, 2020 - 08:00 edit · 11 minutes

    Продолжаем доработку сервера Zabbix. В прошлый раз мы добавили в библиотеку zbxhistory поддержку возможности использования хранилищ разного типа . На этот раз нужно добавить поддержку хранилища нового типа, которую будем реализовывать на базе поддержки хранилища Elasticsearch, частично заимствуя фрагменты из кода поддержки SQL-хранилищ.

    Готовую заплатку для сервера Zabbix с реализацией поддержки хранения исторических данных в ClickHouse можно найти по ссылке zabbix3_4_12_server_storage_per_table.patch .

    Ниже описаны внесённые заплаткой доработки и объяснение их логики.

    У меня ушло некоторое время на изучение функций для работы со структурами данных в формате JSON. Чтобы не пришлось вспоминать их снова, опишу те из них, которыми я пользовался непосредственно в описываемой заплатке.

    Функции Zabbix для формирования JSON

    Заголовочный файл include/zbxjson.h, файл с реализацией функций - src/libs/zbxjson/json.c

    zbx_json_init(json)

    Создание JSON, в котором корневым элементом является словарь. Фактически, в пустой буфер будут добавлены фигурные скобки {} , текущий указатель будет указывать на закрывающую скобку, уровень вложенности увеличится с 0 до 1, а в статусе будет ZBX_JSON_EMPTY.

    zbx_json_initarray(json)

    Работает аналогично zbx_json_init, но корневым элементом структуры JSON будет массив, а в буфер вместо фигурных скобок {} будут вставлены квадратные скобки [] .

    zbx_json_clean(json)

    Очищает буфер от хранящейся в нём структуры JSON. Сама память при этом не освобождается.

    zbx_json_free(json)

    Освобождает буфер, который был занят сформированной структурой JSON.

    zbx_json_addobject(json, name)

    Вставляет в то место буфера, куда указывает текущий указатель:
    1. запятую , , если текущий статус равен ZBX_JSON_COMMA,
    2. текст "name": , если аргумент name не равен NULL,
    3. пару фигурных скобок {} .
    Текущий указатель передвигается на вставленную закрывающую фигурную скобку, уровень вложенности увеличивается на единицу, в статус записывается ZBX_JSON_EMPTY.

    Весь текст, который ранее лежал в буфере после указателя, сдвигается так, что оказывается позади вставленного текста.

    zbx_json_addarray(json, name)

    Работает аналогично zbx_json_addobject, но вместо фигурных скобок вставляются квадратные [] .

    Вставляет в то место буфера, куда указывает текущий указатель:
    1. запятую , , если текущий статус равен ZBX_JSON_COMMA,
    2. текст "name": , если аргумент name не равен NULL,
    3. пару квадратных скобок [] .
    Текущий указатель передвигается на вставленную закрывающую квадратную скобку, уровень вложенности увеличивается на единицу, в статус записывается ZBX_JSON_EMPTY.

    Весь текст, который ранее лежал в буфере после указателя, сдвигается так, что оказывается позади вставленного текста.

    zbx_json_addstring(json, name, string, type)

    Вставляет в то место буфера, куда указывает текущий указатель:
    1. запятую , , если текущий статус равен ZBX_JSON_COMMA,
    2. текст "name": , если аргумент name не равен NULL,
    3. строку string .
    Если тип строки type равен ZBX_JSON_TYPE_STRING, то строка string заключается в двойные кавычки. Если string равен NULL, то добавляется строка null без кавычек.

    Текущий указатель передвигается на символ, следующий за последним вставленным, уровень вложенности не меняется, в статус записывается ZBX_JSON_COMMA.

    Весь текст, который ранее лежал в буфере после указателя, сдвигается так, что оказывается позади вставленного текста.

    zbx_json_adduint64(json, name, value)

    Вставляет в то место буфера, куда указывает текущий указатель:
    1. запятую , , если текущий статус равен ZBX_JSON_COMMA,
    2. текст "name": , если аргумент name не равен NULL,
    3. 64-битное беззнаковое число value.
    Текущий указатель передвигается на символ, следующий за последним вставленным, уровень вложенности не меняется, в статус записывается ZBX_JSON_COMMA.

    Весь текст, который ранее лежал в буфере после указателя, сдвигается так, что оказывается позади вставленного текста.

    zbx_json_addint64(json, name, value)

    Вставляет в то место буфера, куда указывает текущий указатель:
    1. запятую , , если текущий статус равен ZBX_JSON_COMMA,
    2. текст "name": , если аргумент name не равен NULL,
    3. 64-битное число value со знаком.
    Текущий указатель передвигается на символ, следующий за последним вставленным, уровень вложенности не меняется, в статус записывается ZBX_JSON_COMMA.

    Весь текст, который ранее лежал в буфере после указателя, сдвигается так, что оказывается позади вставленного текста.

    zbx_json_close(json)

    Передвигает текущий указатель так, что пропускается закрывающая фигурная } или квадратная скобка ] .

    Уровень вложенности уменьшается на единицу, статус меняется на ZBX_JSON_COMMA.

    Функции Zabbix для разбора JSON

    zbx_json_open(buffer, json)

    buffer - строка с завершающим нулём, содержащая текст JSON.

    json - структура с указателями на начало и конец фрагмента JSON в буфере buffer.

    Проверяет, что текст в буфере buffer является правильным JSON, инициализирует структуру json. При ошибках возвращает FAIL, в случае успеха - SUCCEED.

    zbx_json_next(json, p)

    json - структура с указателями на начало и конец фрагмента JSON, являющегося массивом.

    p - указатель внутри фрагмента json, указывающий на начальный символ элемента массива или словаря.

    Ищет следующий элемент массива или словаря и возвращает указатель на его начальный символ. Если указанный в p элемент был последним, возвращает NULL.

    zbx_json_pair_by_name(json, name)

    json - структура с указателями на начало и конец фрагмента JSON, являющегося словарём.

    name - имя ключа в словаре, значение которого нужно найти.

    Возвращает указатель на первый символ значения или NULL, если указанного ключа нет в словаре.

    zbx_json_brackets_open(p, json)

    p - указатель на открывающую скобку, указывающую на начало фрагмента JSON, который нужно найти.

    json - структура с указателями на начало и конец фрагмента JSON, являющегося массивом или словарём.

    Ищет в указанном фрагменте JSON открывающую скобку, затем находит парную ей закрывающую скобку и записывает указатели на начало и конец найденного фрагмента в структуру json.

    При ошибках возвращает FAIL, при успешном завершении - SUCCEED.

    zbx_json_brackets_by_name(json, name, json_out)

    json - структура с указателями на начало и конец фрагмента JSON, являющегося словарём.

    name - имя ключа в словаре, значение которого нужно найти.

    json_out - фрагмент JSON, являющийся значением ключа name.

    Ищет в указанном фрагменте JSON указанный ключ, находит открывающую и закрывающую скобки, в структуру json_out записывает указатели на начало и конец найденного фрагмента.

    При ошибках возвращает FAIL, при успешном завершении - SUCCEED.

    zbx_json_value_by_name_dyn(json, name, string, string_alloc)

    json - структура с указателями на начало и конец фрагмента JSON, являющегося словарём.

    name - имя ключа в словаре, значение которого нужно найти.

    string - указатель на указатель на буфер, в который будет помещено найденное значение.

    string_alloc - указатель на переменную с размером буфера.

    Ищет в указанном фрагменте JSON, являющемся словарём, значение элемента с указанным ключом name. Найденное значение записывается в буфер. Если в буфере не было достаточно места для сохранения найденного значения, функция выделяет под буфер другой фрагмент памяти, обновляет указатель на буфер и его размер.

    Новая функция zbx_json_adddbl

    Немного опережая события, заранее добавим в код Zabbix дополнительную функцию zbx_json_adddbl, которая позже понадобится нам для формирования JSON с данными, вставляемыми в таблицу history с числами с плавающей запятой. Объявление функции добавим в файл include/zbxjson.h, а реализацию функции добавим в файл src/libs/zbxjson/json.c следующим образом:
    Index: zabbix-3.4.12-1+buster/include/zbxjson.h
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/include/zbxjson.h
    +++ zabbix-3.4.12-1+buster/include/zbxjson.h
    @@ -160,6 +160,7 @@ void zbx_json_addarray(struct zbx_json *
    void zbx_json_addstring(struct zbx_json *j, const char *name, const char *string, zbx_json_type_t type);
    void zbx_json_adduint64(struct zbx_json *j, const char *name, zbx_uint64_t value);
    void zbx_json_addint64(struct zbx_json *j, const char *name, zbx_int64_t value);
    +void zbx_json_adddbl(struct zbx_json *j, const char *name, double value);
    int zbx_json_close(struct zbx_json *j);

    int zbx_json_open(const char *buffer, struct zbx_json_parse *jp);
    Index: zabbix-3.4.12-1+buster/src/libs/zbxjson/json.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxjson/json.c
    +++ zabbix-3.4.12-1+buster/src/libs/zbxjson/json.c
    @@ -385,6 +385,14 @@ void zbx_json_addint64(struct zbx_json *
    zbx_json_addstring(j, name, buffer, ZBX_JSON_TYPE_INT);
    }

    +void zbx_json_adddbl(struct zbx_json *j, const char *name, double value)
    +{
    + char buffer[MAX_ID_LEN];
    +
    + zbx_snprintf(buffer, sizeof(buffer), ZBX_FS_DBL, value);
    + zbx_json_addstring(j, name, buffer, ZBX_JSON_TYPE_INT);
    +}
    +
    int zbx_json_close(struct zbx_json *j)
    {
    if (1 == j->level)

    Доработка основы библиотеки zbxhistory

    В файле src/libs/zbxhistory/history.c раскомментируем ранее добавленный нами комментарий с намёком на поддержку ClickHouse:
    Index: zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxhistory/history.c
    +++ zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.c
    @@ -62,8 +63,8 @@ int zbx_history_init(char **error)
    {
    if (elastic_url = zbx_strstartswith(opts[i], "elastic,"))
    ret = zbx_history_elastic_init(&history_ifaces[i], i, elastic_url, error);
    - /*else if (clickhouse_url = zbx_strstartswith(opts[i], "clickhouse,"))
    - ret = zbx_history_clickhouse_init(&history_ifaces[i], i, clickhouse_url, error);*/
    + else if (clickhouse_url = zbx_strstartswith(opts[i], "clickhouse,"))
    + ret = zbx_history_clickhouse_init(&history_ifaces[i], i, clickhouse_url, error);
    else
    ret = zbx_history_sql_init(&history_ifaces[i], i, error);
    В добавленном коде используется указатель на строку clickhouse_url, добавим его объявление:
    Index: zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxhistory/history.c
    +++ zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.c
    @@ -50,6 +50,7 @@ int zbx_history_init(char **error)
    {
    int i, ret;
    char *elastic_url;
    + char *clickhouse_url;
    const char *opts[] = {
    CONFIG_HISTORY_STORAGE,
    CONFIG_HISTORY_STR_STORAGE,
    Объявление функции zbx_history_clickhouse_init нужно добавить в заголовочный файл src/libs/zbxhistory/history.h:
    Index: zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.h
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxhistory/history.h
    +++ zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.h
    @@ -49,4 +49,7 @@ int zbx_history_sql_init(zbx_history_ifa
    /* elastic hist */
    int zbx_history_elastic_init(zbx_history_iface_t *hist, unsigned char value_type, const char *url, char **error);

    +/* ClickHouse hist */
    +int zbx_history_clickhouse_init(zbx_history_iface_t *hist, unsigned char value_type, const char *url, char **error);
    +
    #endif

    Добавление файла history_clickhouse.c

    Перед дальнейшими действиями скопируем файл src/libs/zbxhistory/history_elastic.c в файл src/libs/zbxhistory/history_clickhouse.c и заменим все упоминания Elasticsearch на ClickHouse, в том числе в отладочных сообщениях, комментариях и именах функций.

    Теперь нужно прописать новый файл history_clickhouse.c в Make-файлы src/libs/zbxhistory/Makefile.am и src/libs/zbxhistory/Makefile.in, чтобы они участвовали в процессе сборки:
    Index: zabbix-3.4.12-1+buster/src/libs/zbxhistory/Makefile.am
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxhistory/Makefile.am
    +++ zabbix-3.4.12-1+buster/src/libs/zbxhistory/Makefile.am
    @@ -5,4 +5,5 @@ noinst_LIBRARIES = libzbxhistory.a
    libzbxhistory_a_SOURCES = \
    history.c history.h \
    history_sql.c \
    - history_elastic.c
    + history_elastic.c \
    + history_clickhouse.c
    Index: zabbix-3.4.12-1+buster/src/libs/zbxhistory/Makefile.in
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxhistory/Makefile.in
    +++ zabbix-3.4.12-1+buster/src/libs/zbxhistory/Makefile.in
    @@ -120,7 +120,8 @@ am__v_AR_1 =
    libzbxhistory_a_AR = $(AR) $(ARFLAGS)
    libzbxhistory_a_LIBADD =
    am_libzbxhistory_a_OBJECTS = history.$(OBJEXT) history_sql.$(OBJEXT) \
    - history_elastic.$(OBJEXT)
    + history_elastic.$(OBJEXT) \
    + history_clickhouse.$(OBJEXT)
    libzbxhistory_a_OBJECTS = $(am_libzbxhistory_a_OBJECTS)
    AM_V_P = $(am__v_P_@AM_V@)
    am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
    @@ -366,7 +367,8 @@ noinst_LIBRARIES = libzbxhistory.a
    libzbxhistory_a_SOURCES = \
    history.c history.h \
    history_sql.c \
    - history_elastic.c
    + history_elastic.c \
    + history_clickhouse.c

    all: all-am

    @@ -417,6 +419,7 @@ distclean-compile:
    -rm -f *.tab.c

    @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/history.Po@am__quote@
    +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/history_clickhouse.Po@am__quote@
    @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/history_elastic.Po@am__quote@
    @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/history_sql.Po@am__quote@
    Получилось два полностью аналогичных по сути модуля поддержки хранилищ с разными именами. Продолжим переделку нового модуля. Сначала пройдусь кратко по мелким изменениям.

    В функции clickhouse_writer_flush была удалена обработка сообщений об ошибках Elasticsearch при успешном коде ответа HTTP, т.к. ClickHouse о любых ошибках выполнения запросов всегда сообщает соответствующим кодом статуса HTTP:
    @@ -402,19 +401,6 @@
    zbx_vector_ptr_append(&retries, msg->easy_handle);
    curl_multi_remove_handle(writer.handle, msg->easy_handle);
    }
    - else if (CURLE_OK == curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, (char **)&curl_page)
    - && SUCCEED == clickhouse_is_error_present(&curl_page->page, &error))
    - {
    - zabbix_log(LOG_LEVEL_WARNING, "%s() %s: %s", __function_name,
    - "cannot send data to ClickHouse", error);
    - zbx_free(error);
    -
    - /* If the error is due to ClickHouse internal problems (for example an index */
    - /* became read-only), we put the handle in a retry list and */
    - /* remove it from the current execution loop */
    - zbx_vector_ptr_append(&retries, msg->easy_handle);
    - curl_multi_remove_handle(writer.handle, msg->easy_handle);
    - }
    }

    previous = running;
    Было удалено макроопределение константы ZBX_IDX_JSON_ALLOCATE, т.к. в коде поддержки ClickHouse оно не использовалось.

    В структуре zbx_clickhouse_data_t было удалено поле post_url, т.к. оказалось достаточно уже имеющегося в структуре поля base_url.

    Были удалены функции history_value2str и clickhouse_is_error_present (бывшая elastic_is_error_present), т.к. они больше не используются.

    Что касается доработок по существу, то они затрагивают функции clickhouse_get_values и clickhouse_add_values. Приведу обе функции полностью в окончательном виде:
    /************************************************************************************
    * *
    * Function: clickhouse_get_values *
    * *
    * Purpose: gets item history data from history storage *
    * *
    * Parameters: hist - [IN] the history storage interface *
    * itemid - [IN] the itemid *
    * start - [IN] the period start timestamp *
    * count - [IN] the number of values to read *
    * end - [IN] the period end timestamp *
    * values - [OUT] the item history data values *
    * *
    * Return value: SUCCEED - the history data were read successfully *
    * FAIL - otherwise *
    * *
    * Comments: This function reads <count> values from [<start>,<end>] interval or *
    * all values from the specified interval if count is zero. *
    * *
    ************************************************************************************/
    static int clickhouse_get_values(zbx_history_iface_t *hist, zbx_uint64_t itemid, int start, int count, int end,
    zbx_vector_history_record_t *values)
    {
    const char *__function_name = "clickhouse_get_values";

    zbx_clickhouse_data_t *data = hist->data;
    size_t sql_alloc = 0, sql_offset;
    int ret = SUCCEED;
    CURLcode err;
    struct curl_slist *curl_headers = NULL;
    char *sql = NULL, errbuf[CURL_ERROR_SIZE];
    const char *p = NULL;
    struct zbx_json_parse jp, jp_sub, jp_data, jp_item;
    zbx_history_record_t hr;

    zabbix_log(LOG_LEVEL_DEBUG, "In %s()", __function_name);

    if (NULL == (data->handle = curl_easy_init()))
    {
    zabbix_log(LOG_LEVEL_ERR, "cannot initialize cURL session");

    return FAIL;
    }

    if (ITEM_VALUE_TYPE_LOG == hist->value_type)
    zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset,
    "SELECT clock, ns, value, timestamp, source, severity, logeventid"
    " FROM %s"
    " WHERE itemid=" ZBX_FS_UI64,
    value_type_table[hist->value_type], itemid);
    else
    zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset,
    "SELECT clock, ns, value"
    " FROM %s"
    " WHERE itemid=" ZBX_FS_UI64,
    value_type_table[hist->value_type], itemid);

    if (0 < start)
    zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, " AND clock>%d", start);

    if (0 < end)
    zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, " AND clock<=%d", end);

    zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, " ORDER BY clock DESC");

    if (0 < count)
    zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, " LIMIT %d", count);

    zbx_snprintf_alloc(&sql, &sql_alloc, &sql_offset, " FORMAT JSON");

    curl_headers = curl_slist_append(curl_headers, "Content-Type: application/json");

    curl_easy_setopt(data->handle, CURLOPT_URL, data->base_url);
    curl_easy_setopt(data->handle, CURLOPT_POSTFIELDS, sql);
    curl_easy_setopt(data->handle, CURLOPT_WRITEFUNCTION, curl_write_cb);
    curl_easy_setopt(data->handle, CURLOPT_WRITEDATA, &page_r);
    curl_easy_setopt(data->handle, CURLOPT_HTTPHEADER, curl_headers);
    curl_easy_setopt(data->handle, CURLOPT_FAILONERROR, 1L);
    curl_easy_setopt(data->handle, CURLOPT_ERRORBUFFER, errbuf);

    zabbix_log(LOG_LEVEL_DEBUG, "sending query to %s; post data: %s", data->base_url, sql);

    page_r.offset = 0;
    *errbuf = '\0';
    if (CURLE_OK != (err = curl_easy_perform(data->handle)))
    {
    clickhouse_log_error(data->handle, err, errbuf);
    ret = FAIL;
    goto out;
    }

    zabbix_log(LOG_LEVEL_DEBUG, "received from ClickHouse: %s", page_r.data);

    zbx_json_open(page_r.data, &jp);
    zbx_json_brackets_open(jp.start, &jp_sub);
    zbx_json_brackets_by_name(&jp_sub, "data", &jp_data);

    while (NULL != (p = zbx_json_next(&jp_data, p)))
    {
    if (SUCCEED != zbx_json_brackets_open(p, &jp_item))
    continue;

    if (SUCCEED != history_parse_value(&jp_item, hist->value_type, &hr))
    continue;

    zbx_vector_history_record_append_ptr(values, &hr);
    }

    out:
    clickhouse_close(hist);

    curl_slist_free_all(curl_headers);

    zbx_free(sql);

    zabbix_log(LOG_LEVEL_DEBUG, "End of %s()", __function_name);

    return ret;
    }

    /************************************************************************************
    * *
    * Function: clickhouse_add_values *
    * *
    * Purpose: sends history data to the storage *
    * *
    * Parameters: hist - [IN] the history storage interface *
    * history - [IN] the history data vector (may have mixed value types) *
    * *
    ************************************************************************************/
    static int clickhouse_add_values(zbx_history_iface_t *hist, const zbx_vector_ptr_t *history)
    {
    const char *__function_name = "clickhouse_add_values";

    zbx_clickhouse_data_t *data = hist->data;
    int i, num = 0;
    ZBX_DC_HISTORY *h;
    struct zbx_json json;
    size_t buf_offset = 0;

    zabbix_log(LOG_LEVEL_DEBUG, "In %s()", __function_name);

    if (ITEM_VALUE_TYPE_LOG == hist->value_type)
    zbx_snprintf_alloc(&data->buf, &data->buf_alloc, &buf_offset,
    "INSERT INTO %s(itemid, value, timestamp, source, severity, logeventid, clock, ns)"
    " FORMAT JSONEachRow\n", value_type_table[hist->value_type]);
    else
    zbx_snprintf_alloc(&data->buf, &data->buf_alloc, &buf_offset,
    "INSERT INTO %s(itemid, value, clock, ns) FORMAT JSONEachRow\n",
    value_type_table[hist->value_type]);

    for (i = 0; i < history->values_num; i++)
    {
    h = (ZBX_DC_HISTORY *)history->values[i];

    if (hist->value_type != h->value_type)
    continue;

    zbx_json_init(&json, ZBX_JSON_ALLOCATE);

    zbx_json_adduint64(&json, "itemid", h->itemid);

    switch (h->value_type)
    {
    case ITEM_VALUE_TYPE_STR:
    case ITEM_VALUE_TYPE_TEXT:
    zbx_json_addstring(&json, "value", h->value.str, ZBX_JSON_TYPE_STRING);
    break;
    case ITEM_VALUE_TYPE_LOG:
    zbx_json_addstring(&json, "value", h->value.log->value, ZBX_JSON_TYPE_STRING);
    break;
    case ITEM_VALUE_TYPE_FLOAT:
    zbx_json_adddbl(&json, "value", h->value.dbl);
    break;
    case ITEM_VALUE_TYPE_UINT64:
    zbx_json_adduint64(&json, "value", h->value.ui64);
    break;
    }

    if (ITEM_VALUE_TYPE_LOG == h->value_type)
    {
    const zbx_log_value_t *log;

    log = h->value.log;

    zbx_json_adduint64(&json, "timestamp", log->timestamp);
    zbx_json_addstring(&json, "source", ZBX_NULL2EMPTY_STR(log->source), ZBX_JSON_TYPE_STRING);
    zbx_json_adduint64(&json, "severity", log->severity);
    zbx_json_adduint64(&json, "logeventid", log->logeventid);
    }

    zbx_json_adduint64(&json, "clock", h->ts.sec);
    zbx_json_adduint64(&json, "ns", h->ts.ns);

    zbx_json_close(&json);

    zbx_snprintf_alloc(&data->buf, &data->buf_alloc, &buf_offset, "%s\n", json.buffer);

    zbx_json_free(&json);

    num++;
    }

    if (num > 0)
    clickhouse_writer_add_iface(hist);

    zabbix_log(LOG_LEVEL_DEBUG, "End of %s()", __function_name);

    return num;
    }
    При доработке функции clickhouse_get_values массив строковых констант value_type_str был заменён на массив строковых констант value_type_table:
    -const char      *value_type_str[] = {"dbl", "str", "log", "uint", "text"};
    +const char *value_type_table[] = {"history", "history_str", "history_log", "history_uint", "history_text"};
    Из всех сделанных изменений отдельно остановлюсь на исправлении одной из ошибок, которая перекочевала в файл history_clickhouse.c из файла history_elastic.c. Не могу скзать, является ли это ошибкой в исходном файле, но в коде поддержки ClickHouse эта проблема проявлялась следующим образом: в журнале сервера Zabbix при попытках вставки новых данных в таблицы истории в файле /var/log/zabbix/zabbix_server.log появлялись ошибки "400 Bad Request", хотя на первый взгляд данные в таблицы всё-таки записывались.

    Оказалось, что часть запросов к ClickHouse были попросту пустыми POST-запросами. Более пристальное изучение причин проблемы позволило обнаружить ошибку: при формировании запроса к ClickHouse на вставку данных иногда, при попытке добавить в него очередное значение, данные в буфере попросту очищались. Получившийся пустой запрос и выполнялся, из-за чего ClickHouse периодически сообщал об ошибках, а графики в веб-интерфейсе прерывались.

    В функции добавки значений использовался уже распределённый ранее буфер hist->data->buf, но при каждом вызове этой функции считалось, что его размер buf_alloc равен нулю. Вот как это выглядит в исходном модуле history_elastic.c:
    static int      elastic_add_values(zbx_history_iface_t *hist, const zbx_vector_ptr_t *history)
    {
    const char *__function_name = "elastic_add_values";

    zbx_elastic_data_t *data = hist->data;
    int i, num = 0;
    ZBX_DC_HISTORY *h;
    struct zbx_json json_idx, json;
    size_t buf_alloc = 0, buf_offset = 0;
    Чтобы устранить ошибку, я решил вынести переменную с размером буфера из тела функции в структуру, содержащую указатель на буфер:
     typedef struct
    {
    char *base_url;
    char *buf;
    + size_t buf_alloc;
    CURL *handle;
    }
    zbx_clickhouse_data_t;
    После этой доработки сервер Zabbix, наконец, начал исправно писать данные в ClickHouse.

    Značky: #3.4, #buster, #debian, #zabbix, #Linux, #linux, #clickhouse

    • Sy chevron_right

      Поддержка нескольких типов хранилищ в сервере Zabbix 3.4

      pubsub.slavino.sk / sysadmblog · Sunday, 8 November, 2020 - 08:00 edit · 7 minutes

    В доработанном веб-интерфейсе Zabbix реализована возможность индивидуальной настройких типа хранилища для каждой из таблиц истории. Теперь необходимо реализовать возможность настройки хранилища для каждой из таблиц на стороне сервера Zabbix. Я решил прибегнуть к следующей схеме: в файле конфигурации я добавил по одной опции для каждой из таблиц истории, в каждой опции указывается через запятую тип хранилища и URL, по которому оно доступно. Если для опции не указано значение, то используется хранилище по умолчанию - база данных SQL.

    В файле конфигурации сервера Zabbix это может выглядеть следующим образом:
    HistoryStorage=clickhouse,http://zabbix:zabbix@localhost:8123/?database=zabbix
    HistoryUintStorage=clickhouse,http://zabbix:zabbix@localhost:8123/?database=zabbix
    HistoryStrStorage=elastic,http://hostnameelastic:9200
    HistoryTextStorage=elastic,http://hostnameelastic:9200
    HistoryLogStorage=elastic,http://hostnameelastic:9200
    Готовую заплатку с реализацией раздельного выбора типа хранилища для каждой из таблиц истории можно найти по ссылке zabbix3_4_12_server_storage_per_table.patch .

    Ниже в логическом порядке описываются изменения, вносимые этой заплаткой.

    Кстати, заплатки описываются так подробно по двум обстоятельствам:
    • Несмотря на то, что Zabbix - это программное обеспечение со свободным исходным кодом, разработку этого программного обеспечения ведёт коммерческая организация. Эта коммерческая компания зарабатывает деньги на поддержке своего продукта и у неё нет желания заниматься поддержкой дополнительных спорных функций, внесённых в код сторонними разработчиками. Представьте, что какой-то Вася реализовал в Zabbix поддержку хранения исторических данных в том же Clickhouse. Клиент, оплативший коммерческую поддержку Zabbix, устанавливает себе Zabbix и пытается воспользоваться ClickHouse в качестве хранилища исторических данных. Даже если с кодом Васи нет никаких проблем и он аккуратно написан, у клиента может возникнуть множество самых разных проблем, связанных с эксплуатацией непосредственно самого ClickHouse. В компании нет специалистов, знакомых с Clickhouse, поэтому компания будет вынуждена отказать клиенту в поддержке. Возникнет вопрос - за что же тогда платит клиент, если компания не осуществляет поддержку функций, реализованных в её продукте? Вот поэтому разработчики Zabbix обычно не принимают в исходный код своего продукта никаких заплаток, вносящих в код продукта глобальные изменения. Zabbix, являясь программным обеспечением со свободным исходным кодом, фактически не принадлежит сообществу, а принадлежит коммерческой компании.
    • Из заплатки самой по себе не так легко понять логику вносимых ей изменений. Если понадобится адаптировать заплатку к другой версии Zabbix, то нужно будет разбираться в имеющейся заплатке и исходном коде той версии Zabbix, для которой эта заплатка была сделана, а потом повторить все эти изменения в другой версии Zabbix. Описание, подобное приведённому ниже, должно помочь во-первых, понять логику изменений, вносимых заплаткой, а во-вторых, помочь внести подобные изменения мелкими кусочками в другую версию Zabbix.

    Доработка файла конфигурации

    Задекларируем наши намерения, доработав соответствующим образом пример файла конфигурации conf/zabbix_server.conf:
    Index: zabbix-3.4.12-1+buster/conf/zabbix_server.conf
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/conf/zabbix_server.conf
    +++ zabbix-3.4.12-1+buster/conf/zabbix_server.conf
    @@ -133,19 +133,40 @@ DBUser=zabbix
    # Default (for MySQL):
    # DBPort=3306

    -### Option: HistoryStorageURL
    -# History storage HTTP[S] URL.
    +### Option: HistoryStorage
    +# Storage type and HTTP[S] URL for double type values history.
    #
    # Mandatory: no
    # Default:
    -# HistoryStorageURL=
    +# HistoryStorage=

    -### Option: HistoryStorageTypes
    -# Comma separated list of value types to be sent to the history storage.
    +### Option: HistoryUintStorage
    +# Storage type and HTTP[S] URL for unsigned integer type values history.
    #
    # Mandatory: no
    # Default:
    -# HistoryStorageTypes=uint,dbl,str,log,text
    +# HistoryUintStorage=
    +
    +### Option: HistoryStrStorage
    +# Storage type and HTTP[S] URL for string type values history.
    +#
    +# Mandatory: no
    +# Default:
    +# HistoryStrStorage=
    +
    +### Option: HistoryTextStorage
    +# Storage type and HTTP[S] URL for text type values history.
    +#
    +# Mandatory: no
    +# Default:
    +# HistoryTextStorage=
    +
    +### Option: HistoryLogStorage
    +# Storage type and HTTP[S] URL for log type values history.
    +#
    +# Mandatory: no
    +# Default:
    +# HistoryLogStorage=

    ############ ADVANCED PARAMETERS ################

    Доработка сервера Zabbix и Zabbix-прокси

    Теперь удалим поддержку чтения опций конфигурации HistoryStorageUrl и HistoryStorageOpts из исходного текста сервера Zabbix в файле src/zabbix_server/server.c и добавим в него поддержку чтения новых опций конфигурации HistoryStorage, HistoryUintStorage, HistoryStrStorage, HistoryTextStorage, HistoryLogStorage:
    Index: zabbix-3.4.12-1+buster/src/zabbix_server/server.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/zabbix_server/server.c
    +++ zabbix-3.4.12-1+buster/src/zabbix_server/server.c
    @@ -258,8 +258,11 @@ char *CONFIG_TLS_PSK_FILE = NULL;
    #endif

    char *CONFIG_SOCKET_PATH = NULL;
    -char *CONFIG_HISTORY_STORAGE_URL = NULL;
    -char *CONFIG_HISTORY_STORAGE_OPTS = NULL;
    +char *CONFIG_HISTORY_STORAGE = NULL;
    +char *CONFIG_HISTORY_UINT_STORAGE = NULL;
    +char *CONFIG_HISTORY_STR_STORAGE = NULL;
    +char *CONFIG_HISTORY_TEXT_STORAGE = NULL;
    +char *CONFIG_HISTORY_LOG_STORAGE = NULL;

    int get_process_info_by_thread(int local_server_num, unsigned char *local_process_type, int *local_process_num);

    @@ -438,9 +441,6 @@ static void zbx_set_defaults(void)

    if (NULL == CONFIG_SSL_KEY_LOCATION)
    CONFIG_SSL_KEY_LOCATION = zbx_strdup(CONFIG_SSL_KEY_LOCATION, DATADIR "/zabbix/ssl/keys");
    -
    - if (NULL == CONFIG_HISTORY_STORAGE_OPTS)
    - CONFIG_HISTORY_STORAGE_OPTS = zbx_strdup(CONFIG_HISTORY_STORAGE_OPTS, "uint,dbl,str,log,text");
    #endif

    #ifdef HAVE_SQLITE3
    @@ -499,8 +499,11 @@ static void zbx_validate_config(ZBX_TASK
    err |= (FAIL == check_cfg_feature_str("SSLCALocation", CONFIG_SSL_CA_LOCATION, "cURL library"));
    err |= (FAIL == check_cfg_feature_str("SSLCertLocation", CONFIG_SSL_CERT_LOCATION, "cURL library"));
    err |= (FAIL == check_cfg_feature_str("SSLKeyLocation", CONFIG_SSL_KEY_LOCATION, "cURL library"));
    - err |= (FAIL == check_cfg_feature_str("HistoryStorageURL", CONFIG_HISTORY_STORAGE_URL, "cURL library"));
    - err |= (FAIL == check_cfg_feature_str("HistoryStorageTypes", CONFIG_HISTORY_STORAGE_OPTS, "cURL library"));
    + err |= (FAIL == check_cfg_feature_str("HistoryStorage", CONFIG_HISTORY_STORAGE, "cURL library"));
    + err |= (FAIL == check_cfg_feature_str("HistoryUintStorage", CONFIG_HISTORY_UINT_STORAGE, "cURL library"));
    + err |= (FAIL == check_cfg_feature_str("HistoryStrStorage", CONFIG_HISTORY_STR_STORAGE, "cURL library"));
    + err |= (FAIL == check_cfg_feature_str("HistoryTextStorage", CONFIG_HISTORY_TEXT_STORAGE, "cURL library"));
    + err |= (FAIL == check_cfg_feature_str("HistoryLogStorage", CONFIG_HISTORY_LOG_STORAGE, "cURL library"));
    #endif

    #if !defined(HAVE_LIBXML2) || !defined(HAVE_LIBCURL)
    @@ -696,9 +699,15 @@ static void zbx_load_config(ZBX_TASK_EX
    PARM_OPT, 1, 100},
    {"StartPreprocessors", &CONFIG_PREPROCESSOR_FORKS, TYPE_INT,
    PARM_OPT, 1, 1000},
    - {"HistoryStorageURL", &CONFIG_HISTORY_STORAGE_URL, TYPE_STRING,
    + {"HistoryStorage", &CONFIG_HISTORY_STORAGE, TYPE_STRING,
    + PARM_OPT, 0, 0},
    + {"HistoryUintStorage", &CONFIG_HISTORY_UINT_STORAGE, TYPE_STRING,
    + PARM_OPT, 0, 0},
    + {"HistoryStrStorage", &CONFIG_HISTORY_STR_STORAGE, TYPE_STRING,
    + PARM_OPT, 0, 0},
    + {"HistoryTextStorage", &CONFIG_HISTORY_TEXT_STORAGE, TYPE_STRING,
    PARM_OPT, 0, 0},
    - {"HistoryStorageTypes", &CONFIG_HISTORY_STORAGE_OPTS, TYPE_STRING_LIST,
    + {"HistoryLogStorage", &CONFIG_HISTORY_LOG_STORAGE, TYPE_STRING,
    PARM_OPT, 0, 0},
    {NULL}
    };
    Поскольку Zabbix-прокси реализован на основе сервера Zabbix, аналогичные фиктивные изменения нужно внести в исходный код Zabbix-прокси в файле src/zabbix_proxy/proxy.c:
    Index: zabbix-3.4.12-1+buster/src/zabbix_proxy/proxy.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/zabbix_proxy/proxy.c
    +++ zabbix-3.4.12-1+buster/src/zabbix_proxy/proxy.c
    @@ -251,8 +251,11 @@ char *CONFIG_TLS_PSK_FILE = NULL;

    char *CONFIG_SOCKET_PATH = NULL;

    -char *CONFIG_HISTORY_STORAGE_URL = NULL;
    -char *CONFIG_HISTORY_STORAGE_OPTS = NULL;
    +char *CONFIG_HISTORY_STORAGE = NULL;
    +char *CONFIG_HISTORY_UINT_STORAGE = NULL;
    +char *CONFIG_HISTORY_STR_STORAGE = NULL;
    +char *CONFIG_HISTORY_TEXT_STORAGE = NULL;
    +char *CONFIG_HISTORY_LOG_STORAGE = NULL;

    int get_process_info_by_thread(int local_server_num, unsigned char *local_process_type, int *local_process_num);

    Доработка основы библиотеки zbxhistory

    Суть вносимых изменений особенно наглядно можно увидеть в следующей заплатке для файла src/libs/zbxhistory/history.c. Вместо использования фиксированного URL для всех таблиц из переменной CONFIG_HISTORY_STORAGE_URL и переменной CONFIG_HISTORY_STORAGE_OPTS, которая предписывает использовать этот URL для таблиц указанных в ней типов, мы определяем тип хранилища для каждой из таблиц и вызываем функцию инициализации соответствующего модуля, передавая ей URL для доступа к этой конкретной таблице:
    Index: zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxhistory/history.c
    +++ zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.c
    @@ -27,8 +27,11 @@

    ZBX_VECTOR_IMPL(history_record, zbx_history_record_t);

    -extern char *CONFIG_HISTORY_STORAGE_URL;
    -extern char *CONFIG_HISTORY_STORAGE_OPTS;
    +extern char *CONFIG_HISTORY_STORAGE;
    +extern char *CONFIG_HISTORY_UINT_STORAGE;
    +extern char *CONFIG_HISTORY_STR_STORAGE;
    +extern char *CONFIG_HISTORY_TEXT_STORAGE;
    +extern char *CONFIG_HISTORY_LOG_STORAGE;

    zbx_history_iface_t history_ifaces[ITEM_VALUE_TYPE_MAX];

    @@ -46,16 +49,23 @@ zbx_history_iface_t history_ifaces[ITEM_
    int zbx_history_init(char **error)
    {
    int i, ret;
    - /* TODO: support per value type specific configuration */
    -
    - const char *opts[] = {"dbl", "str", "log", "uint", "text"};
    + char *elastic_url;
    + const char *opts[] = {
    + CONFIG_HISTORY_STORAGE,
    + CONFIG_HISTORY_STR_STORAGE,
    + CONFIG_HISTORY_LOG_STORAGE,
    + CONFIG_HISTORY_UINT_STORAGE,
    + CONFIG_HISTORY_TEXT_STORAGE
    + };

    for (i = 0; i < ITEM_VALUE_TYPE_MAX; i++)
    {
    - if (NULL == CONFIG_HISTORY_STORAGE_URL || NULL == strstr(CONFIG_HISTORY_STORAGE_OPTS, opts[i]))
    - ret = zbx_history_sql_init(&history_ifaces[i], i, error);
    + if (elastic_url = zbx_strstartswith(opts[i], "elastic,"))
    + ret = zbx_history_elastic_init(&history_ifaces[i], i, elastic_url, error);
    + /*else if (clickhouse_url = zbx_strstartswith(opts[i], "clickhouse,"))
    + ret = zbx_history_clickhouse_init(&history_ifaces[i], i, clickhouse_url, error);*/
    else
    - ret = zbx_history_elastic_init(&history_ifaces[i], i, error);
    + ret = zbx_history_sql_init(&history_ifaces[i], i, error);

    if (FAIL == ret)
    return FAIL;
    В доработанном коде функции zbx_history_init добавлен закомментированный участок, обозначающий будущую поддержку хранилища ClickHouse.

    Из кода удалён комментарий о необходимости реализовать раздельную возможность настройки типов хранилищ для каждой из таблиц, потому что именно это мы сейчас и делаем.

    В коде используется новая функция zbx_startswith для проверки, что строка начинается с указанного префикса.

    При доработке изменилась сигнатура функции zbx_history_elastic_init, теперь ей передаётся дополнительный аргумент - elastic_url.

    Остановимся поподробнее на двух последних обстоятельствах.

    Новая функция zbx_startswith

    Во-первых, добавим объявление новой функции zbx_startswith в файл include/common.h и её реализацию в файл src/libs/zbxcommon/str.c:
    Index: zabbix-3.4.12-1+buster/include/common.h
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/include/common.h
    +++ zabbix-3.4.12-1+buster/include/common.h
    @@ -1114,6 +1114,7 @@ char *zbx_time2str(time_t time);
    #define ZBX_NULL2STR(str) (NULL != str ? str : "(null)")
    #define ZBX_NULL2EMPTY_STR(str) (NULL != (str) ? (str) : "")

    +char *zbx_strstartswith(const char *str, const char *prefix);
    char *zbx_strcasestr(const char *haystack, const char *needle);
    int cmp_key_id(const char *key_1, const char *key_2);
    int zbx_strncasecmp(const char *s1, const char *s2, size_t n);
    Index: zabbix-3.4.12-1+buster/src/libs/zbxcommon/str.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxcommon/str.c
    +++ zabbix-3.4.12-1+buster/src/libs/zbxcommon/str.c
    @@ -1827,6 +1827,38 @@ char *zbx_time2str(time_t time)
    return buffer;
    }

    +/******************************************************************************
    + * *
    + * Function: zbx_startswith *
    + * *
    + * Purpose: compare start of string str with string prefix *
    + * *
    + * Parameters: str - [IN] null terminated source string *
    + * prefix - [IN] null terminated prefix string *
    + * *
    + * Return value: pointer to rest of str or NULL, if prefix not found *
    + * *
    + * Author: Vladimir Stupin *
    + * *
    + ******************************************************************************/
    +char *zbx_strstartswith(const char *str, const char *prefix)
    +{
    + if (NULL == prefix)
    + return (char *)str;
    +
    + if (NULL == str)
    + return NULL;
    +
    + while ('\0' != *prefix)
    + {
    + if ('\0' == *str || *str != *prefix)
    + return NULL;
    + str++;
    + prefix++;
    + }
    + return (char *)str;
    +}
    +
    int zbx_strncasecmp(const char *s1, const char *s2, size_t n)
    {
    if (NULL == s1 && NULL == s2)

    Доработка поддержки Elasticsearch

    Во-вторых, изменим объявление функции zbx_history_elastic_init в заголовочном файле src/libs/zbxhistory/history.h:
    Index: zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.h
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxhistory/history.h
    +++ zabbix-3.4.12-1+buster/src/libs/zbxhistory/history.h
    @@ -47,6 +47,6 @@ struct zbx_history_iface
    int zbx_history_sql_init(zbx_history_iface_t *hist, unsigned char value_type, char **error);

    /* elastic hist */
    -int zbx_history_elastic_init(zbx_history_iface_t *hist, unsigned char value_type, char **error);
    +int zbx_history_elastic_init(zbx_history_iface_t *hist, unsigned char value_type, const char *url, char **error);

    #endif
    Осталось лишь соответствующим образом изменить реализацию этой функции в файле src/libs/zbxhistory/history_elastic.c:
    Index: zabbix-3.4.12-1+buster/src/libs/zbxhistory/history_elastic.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/libs/zbxhistory/history_elastic.c
    +++ zabbix-3.4.12-1+buster/src/libs/zbxhistory/history_elastic.c
    @@ -37,8 +37,6 @@

    const char *value_type_str[] = {"dbl", "str", "log", "uint", "text"};

    -extern char *CONFIG_HISTORY_STORAGE_URL;
    -
    typedef struct
    {
    char *base_url;
    @@ -912,7 +910,7 @@ static void elastic_flush(zbx_history_if
    * FAIL - otherwise *
    * *
    ************************************************************************************/
    -int zbx_history_elastic_init(zbx_history_iface_t *hist, unsigned char value_type, char **error)
    +int zbx_history_elastic_init(zbx_history_iface_t *hist, unsigned char value_type, const char *url, char **error)
    {
    zbx_elastic_data_t *data;

    @@ -924,7 +922,7 @@ int zbx_history_elastic_init(zbx_history

    data = zbx_malloc(NULL, sizeof(zbx_elastic_data_t));
    memset(data, 0, sizeof(zbx_elastic_data_t));
    - data->base_url = zbx_strdup(NULL, CONFIG_HISTORY_STORAGE_URL);
    + data->base_url = zbx_strdup(NULL, url);
    zbx_rtrim(data->base_url, "/");
    data->buf = NULL;
    data->post_url = NULL;

    Značky: #3.4, #debian, #Linux, #linux, #buster, #zabbix

    • Sy chevron_right

      Доработка веб-интерфейса Zabbix 3.4 для работы с ClickHouse

      pubsub.slavino.sk / sysadmblog · Sunday, 1 November, 2020 - 08:00 · 17 minutes

    Теперь, когда у нас имеется настроенный сервер Clickhouse с заготовленными в нём таблицами истории и тенденций Zabbix , можно попробовать доработать веб-интерфейс Zabbix для работы с Clickhouse. Реализацию поддержки Clickhouse будем делать на базе уже имеющейся поддержки хранилищ Elasticsearch и SQL. Поскольку ClickHouse использует SQL-образный синтаксис запросов, но может возвращать ответ в виде JSON по протоколу HTTP, то нам пригодятся фрагменты кода реализации поддержки как того, так и другого типа хранилища. Разработка и отладка заплатки выполнялась на данных, скопированных в Clickhouse из хранилища SQL при помощи описанного ранее скрипта copy_data.py .

    Получившуюся заплатку с неописанными здесь мелкими изменениями в комментариях к другим функциям можно взять по ссылке zabbix3_4_12_frontend_clickhouse.patch .
    Описанная здесь реализация поддержки ClickHouse отличается от реализации из Glaber , следующим:
    • вместо модуля curl для php для обращения к API ClickHouse используется штатная функция file_get_contents,
    • вместо типа DateTime для колонок clock используется тип UInt32, что приближает поддержку ClickHouse к родной структуре таблиц Zabbix,
    • реализована поддержка работы с таблицами истории журнального типа - history_log,
    • реализовано раздельное хранение исторических данных в таблицах history, history_uint, history_str, history_text, history_log, что приближает поддержку ClickHouse к родной структуре таблиц Zabbix,
    • веб-интерфейс поддерживает индивидуальный выбор типа хранилища для каждой из таблиц истории: SQL, Elasticsearch или ClickHouse,
    • при отображении графиков используются данные из таблиц trends и trends_uint, которые должны быть доступны по URL таблиц history и history_uint соответственно,
    • реализованы оптимизации запросов на страницах последних данных и графиков нескольких элементов данных: вместо отдельных запросов по каждому элементу данных выполняется от одного до 5 запросов по каждому из типов элементов данных. Внутри каждого запроса данные подзапросов объединяются при помощи выражения UNION ALL.
    Я старался скрупулёзно воспроизводить стиль исходного кода, однако не всегда этот код мне кажется идеальным. В частности, в коде присутствуют микрооптимизации, кэширующие соответствие типов значений элементов данных URL'ам хранилища. Эти микрооптимизации на фоне обращений к самим хранилищам экономят настолько мизерное количество ресурсов, что лучше было бы обойтись вообще без них - код бы от этого стал только нагляднее. Не совсем понятно, почему в некоторых не самых тяжёлых функциях реализовано объединение запросов при помощи UNION ALL, а в наиболее тяжёлых - не реализовано. Наконец, сама поддержка различных хранилищ не выполнена в стиле ООП: нет базового класса хранилища и нет отдельных реализаций хранилищ в виде классов, отнаследованных от базового класса. Вместо этого поддержка разных типов хранилищ реализована прямо в коде классов CHistoryManager, CHistory, CTrend, из которых два последних используют часть методов из первого.

    У веб-интерфейса есть интересная особенность. На странице просмотра графика могут использоваться данные из таблицы тенденций, даже если данные есть в таблице истории. На выбор таблицы-источника данных влияет длительность хранения исторических данных, указанная в свойствах самого элемента данных. Также если в настройках на странице «Администрирование» - «Общие», в разделе «Очистка истории» отмечена галочка «Переопределить период хранения истории элементов данных», то используется значение, указанное в поле «Период хранения данных». В моём случае в этом поле было указано значение 60d, а в таблице истории имелись данные за 365 дней, включая те данные, которые были сгенерированы по таблицам тенденций. Когда я поменял значение в этом поле на 365d, на графиках стали отображаться только данные из таблиц истории.

    Ниже описаны внесённые заплаткой доработки веб-интерфейса и их обоснование.

    Файл конфигурации

    Первым делом поправим пример файла конфигурации frontends/php/conf/zabbix.conf.php.example. В нём можно увидеть, что в переменной конфигурации $HISTORY['types'] можно указать список таблиц, для которых будет использоваться Elasticsearch. Делается это следующим обрзом:
    $HISTORY['types'] = ['uint', 'text'];
    Поскольку нам нужно достичь возможности использовать одно из трёх разных хранилищ, я решил изменить формат этой переменной так, чтобы для каждой из таблиц можно было указывать тип её хранилища:
    $HISTORY['type'] = [
    'uint' => 'clickhouse',
    'text' => 'elastic'
    ];
    При этом в переменной $HISTORY['url'], как и раньше, будет указывается URL хранилища. Только теперь это может быть как URL хранилища Elasticsearch, так и URL хранилища Clickhouse.

    Отобразим логику изменений файла конфигурации в примере этого файла следующим образом:
    Index: zabbix-3.4.12-1+buster/frontends/php/conf/zabbix.conf.php.example
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/frontends/php/conf/zabbix.conf.php.example
    +++ zabbix-3.4.12-1+buster/frontends/php/conf/zabbix.conf.php.example
    @@ -17,10 +17,13 @@ $ZBX_SERVER_NAME = '';

    $IMAGE_FORMAT_DEFAULT = IMAGE_FORMAT_PNG;

    -// Elasticsearch url (can be string if same url is used for all types).
    +// Elasticsearch or ClickHouse url.
    $HISTORY['url'] = [
    - 'uint' => 'http://localhost:9200',
    + 'uint' => 'http://login:password@localhost:8123/?database=zabbix',
    'text' => 'http://localhost:9200'
    ];
    -// Value types stored in Elasticsearch.
    -$HISTORY['types'] = ['uint', 'text'];
    +// Value types stored in Elasticsearch or ClickHouse.
    +$HISTORY['types'] = [
    + 'uint' => 'clickhouse',
    + 'text' => 'elastic'
    +];

    Вспомогательный класс для работы с Clickhouse

    В отличие от Glaber , в моей доработке веб-интерфейса Zabbix для обращения к Clickhouse не используется модуль php-curl, а используется встроенная в PHP функция file_get_contents, что позволяет обойтись прежними зависимостями при установке веб-интерфейса.

    Для выполнения запросов к Clickhouse добавим файл frontends/php/include/classes/helpers/CClickHouseHelper.php со вспомогательным классом CClickHouseHelper:
    <?php

    /**
    * A helper class for working with ClickHouse.
    */
    class CClickHouseHelper {

    /**
    * Perform request to ClickHouse.
    *
    * @param string $method HTTP method to be used to perform request
    * @param string $endpoint requested url
    * @param mixed $request data to be sent
    *
    * @return string result
    */
    private static function request($method, $endpoint, $query) {
    $options = [
    'http' => [
    'header' => "Content-Type: application/json; charset=UTF-8",
    'method' => $method,
    'ignore_errors' => true // To get error messages from ClickHouse.
    ]
    ];

    $query .= ' FORMAT JSONCompact';
    $options['http']['content'] = $query;

    try {
    $response = file_get_contents($endpoint, false, stream_context_create($options));
    }
    catch (Exception $e) {
    error($e->getMessage());
    }

    return json_decode($response, true);
    }

    public static function values($method, $endpoint, $query = null, $columns = null, $map = null) {
    #file_put_contents('/var/log/nginx/chartlog.log', "$query\n\n", FILE_APPEND);
    $response = self::request($method, $endpoint, $query);

    $values = [];
    foreach ($response['data'] as $row) {
    $value = [];
    for($i = 0; $i < count($row); $i++)
    {
    if ($columns) {
    $column = $columns[$i];
    } else {
    $column = $response['meta'][$i]['name'];
    }

    if ($map && array_key_exists($column, $map))
    {
    $column = $map[$column];
    }

    $value[$column] = $row[$i];
    }
    $values[] = $value;
    }
    #$json = json_encode($values, true);
    #file_put_contents('/var/log/nginx/chartlog.log', "$json\n", FILE_APPEND);
    return $values;
    }

    public static function value($method, $endpoint, $query = null, $column = 'value') {
    $values = self::values($method, $endpoint, $query, [$column]);

    if ((count($values) > 0) && array_key_exists($column, $values[0])) {
    return $values[0][$column];
    }
    return null;
    }
    }
    В отличие от Glaber , этот класс выполняет запросы в формате JSON, а не TSV. В классе есть функция request, которая выполняет переданные ей запросы и возвращает в ответ данные, извлечённые из JSON. Эта функция вызывается только из функций values и value.

    Функция values позволяет выполнить SQL-запрос на получение множества строк данных. Clickhouse вместе с данными ответа также возвращает имена колонок. Если не указывать аргументы columns и map, то при формировании результата будут использоваться имена колонок, которые вернул Clickhouse. Результат будет представлять собой список словарей: каждая строчка списка будет соответствовать одной строке из результата выполнения запроса, а каждый словарь в строке будет в ключе содержать имя колонки, а в значении ключа - значение этой колонки.

    Если указать функции values аргумент columns, то вместо возвращённых сервером Clickhouse имён колонок будут использоваться указанные, в порядке их указания в списке columns.

    Если указать функции values аргумент maps, являющийся словарём, то вместо возвращённых сервером Clickhouse имён колонок будут возвращаться значения из словаря. maps должен быть словарём, в котором ключами являются имена колонок, возвращённых сервером Clickhouse, а их значениями - желаемые имена колонок.

    Функция value возвращает одно значение, возвращённое запросом, или значение null, если запрос ничего не вернул. Если запрос вернёт несколько строк, то будет возвращено значение из первой строки. Если аргумент column не указан, то возвращено будет значение из колонки value.

    В тексте функции values имеются закомментированные строчки, которые могут помочь при отладке запросов к Clickhouse. Раскомментировав их, можно, по желанию, вести журнал запросов и результатов выполнения этих запросов.

    Новый тип хранилища

    Теперь добавим новое определение источника данных в файл frontends/php/include/defines.inc.php:
    Index: zabbix-3.4.12-1+buster/frontends/php/include/defines.inc.php
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/frontends/php/include/defines.inc.php
    +++ zabbix-3.4.12-1+buster/frontends/php/include/defines.inc.php
    @@ -41,6 +41,7 @@ define('ZBX_PERIOD_DEFAULT', 3600); // 1
    // by default set to 86400 seconds (24 hours)
    define('ZBX_HISTORY_PERIOD', 86400);

    +define('ZBX_HISTORY_SOURCE_CLICKHOUSE', 'clickhouse');
    define('ZBX_HISTORY_SOURCE_ELASTIC', 'elastic');
    define('ZBX_HISTORY_SOURCE_SQL', 'sql');

    Доработка класса CHistoryManager

    В файле frontends/php/include/classes/api/managers/CHistoryManager.php определён класс CHistoryManager, который отвечает за работу с таблицами истории непосредственно самого веб-интерфейса. Потребуется доработать функции getLastValues, getValueAt, getGraphAggregation, getAggregatedValue и getMinClock. Начнём, однако, не с этого, а с введения новых вспомогательных фукнций.

    Новая функция getClickHouseEndpoints

    Вместо функции getElasticsearchUrl введём аналогичную по смыслу фукнцию getClickHouseEndpoints, которая будет использовать вспомогательную функцию getClickhouseUrl:
    Index: zabbix-3.4.12-1+buster/frontends/php/include/classes/api/managers/CHistoryManager.php
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/frontends/php/include/classes/api/managers/CHistoryManager.php
    +++ zabbix-3.4.12-1+buster/frontends/php/include/classes/api/managers/CHistoryManager.php
    @@ -968,6 +1321,51 @@ class CHistoryManager {
    return $cache[$value_type];
    }

    + private static function getClickHouseUrl($value_name) {
    + static $urls = [];
    + static $invalid = [];
    +
    + // Additional check to limit error count produced by invalid configuration.
    + if (array_key_exists($value_name, $invalid)) {
    + return null;
    + }
    +
    + if (!array_key_exists($value_name, $urls)) {
    + global $HISTORY;
    +
    + $urls[$value_name] = $HISTORY['url'][$value_name];
    + }
    +
    + return $urls[$value_name];
    + }
    +
    + /**
    + * Get endpoints for ClickHouse requests.
    + *
    + * @param mixed $value_types value type(s)
    + *
    + * @return array ClickHouse query endpoints
    + */
    + public static function getClickHouseEndpoints($value_types) {
    + if (!is_array($value_types)) {
    + $value_types = [$value_types];
    + }
    +
    + $endpoints = [];
    +
    + foreach (array_unique($value_types) as $type) {
    + if (self::getDataSourceType($type) === ZBX_HISTORY_SOURCE_CLICKHOUSE) {
    + $index = self::getTypeNameByTypeId($type);
    +
    + if (($url = self::getClickHouseUrl($index)) !== null) {
    + $endpoints[$type] = $url;
    + }
    + }
    + }
    +
    + return $endpoints;
    + }
    +
    private static function getElasticsearchUrl($value_name) {
    static $urls = [];
    static $invalid = [];
    Функция getClickhouseEndpoints возвращает URL для доступа к таблицам истории указанных типов значений.

    Доработка функции getLastValues

    Функция getLastValues последовательно обращается к хранилищам каждого типа и запрашивает у него последние значения тех элементов данных, которые хранятся в соответствующем хранилище. Результаты запросов складываются в общую копилку и возвращаются в качестве результата. Добавим в функцию поддержку хранилища ClickHouse:
    Index: zabbix-3.4.12-1+buster/frontends/php/include/classes/api/managers/CHistoryManager.php
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/frontends/php/include/classes/api/managers/CHistoryManager.php
    +++ zabbix-3.4.12-1+buster/frontends/php/include/classes/api/managers/CHistoryManager.php
    @@ -37,6 +37,12 @@ class CHistoryManager {
    $results = [];
    $grouped_items = self::getItemsGroupedByStorage($items);

    + if (array_key_exists(ZBX_HISTORY_SOURCE_CLICKHOUSE, $grouped_items)) {
    + $results += $this->getLastValuesFromClickHouse($grouped_items[ZBX_HISTORY_SOURCE_CLICKHOUSE], $limit,
    + $period
    + );
    + }
    +
    if (array_key_exists(ZBX_HISTORY_SOURCE_ELASTIC, $grouped_items)) {
    $results += $this->getLastValuesFromElasticsearch($grouped_items[ZBX_HISTORY_SOURCE_ELASTIC], $limit,
    $period
    Теперь нужно реализовать функцию getLastValuesFromClickhouse. Я реализовал два варианта функции. Первый просто последовательно запрашивает последнее значение каждого из указанных в запросе элементов данных и объединяет результаты запросов, как это сделано в функции getLastValuesFromElasticsearch. Второй вариант из запрашиваемых значений формирует группы по их типам. Для каждого типа значений формируется единый запрос, объединяющий результаты отдельных запросов при помощи выражения UNION ALL. Таким образом можно увеличить отзывчивость веб-интерфейса, сократив количество HTTP-запросов к серверу Clickhouse. Первый вариант фигурирует в коде под именем _getLastValuesFromClickHouse, а второй - более эффективный - под именем getLastValuesFromClickHouse:
    Index: zabbix-3.4.12-1+buster/frontends/php/include/classes/api/managers/CHistoryManager.php
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/frontends/php/include/classes/api/managers/CHistoryManager.php
    +++ zabbix-3.4.12-1+buster/frontends/php/include/classes/api/managers/CHistoryManager.php
    @@ -51,6 +57,77 @@ class CHistoryManager {
    }

    /**
    + * ClickHouse specific implementation of getLastValues.
    + *
    + * @see CHistoryManager::getLastValues
    + */
    + private function _getLastValuesFromClickHouse($items, $limit, $period) {
    + $results = [];
    +
    + foreach ($items as $item) {
    + $endpoints = self::getClickHouseEndpoints($item['value_type']);
    + if ($endpoints) {
    + $query =
    + 'SELECT *'.
    + ' FROM '.self::getTableName($item['value_type']).
    + ' WHERE itemid='.($item['itemid'] + 0).
    + ($period ? ' AND clock>'.(time() - $period) : '').
    + ' ORDER BY clock DESC';
    +
    + if ($limit > 0) $query .= ' LIMIT '.$limit;
    +
    + $values = CClickHouseHelper::values('POST', reset($endpoints), $query);
    + if ($values) {
    + $results[$item['itemid']] = $values;
    + }
    + }
    + }
    +
    + return $results;
    + }
    +
    + /**
    + * ClickHouse specific implementation of getLastValues.
    + *
    + * @see CHistoryManager::getLastValues
    + */
    + private function getLastValuesFromClickHouse($items, $limit, $period) {
    + $results = [];
    + $type_queries = [];
    +
    + foreach ($items as $item) {
    + $query =
    + 'SELECT *'.
    + ' FROM '.self::getTableName($item['value_type']).
    + ' WHERE itemid='.($item['itemid'] + 0).
    + ($period ? ' AND clock>'.(time() - $period) : '').
    + ' ORDER BY clock DESC';
    +
    + if ($limit > 0) $query .= ' LIMIT '.$limit;
    +
    + $type_queries[$item['value_type']][] = $query;
    + }
    +
    + foreach ($type_queries as $value_type => $queries) {
    + $endpoints = self::getClickHouseEndpoints($value_type);
    + if ($endpoints) {
    + $query =
    + 'SELECT *'.
    + ' FROM ('.implode(' UNION ALL ', $queries).')';
    +
    + $values = CClickHouseHelper::values('POST', reset($endpoints), $query);
    +
    + foreach($values as $row) {
    + $itemid = $row['itemid'];
    + $results[$itemid][] = $row;
    + }
    + }
    + }
    +
    + return $results;
    + }
    +
    + /**
    * Elasticsearch specific implementation of getLastValues.
    *
    * @see CHistoryManager::getLastValues
    Можно пойти дальше и написать вариант функции, который использует специфический тип запросов, поддерживаемый ClickHouse: LIMIT 1 BY itemid. В таком случае можно будет упростить запрос и обойтись без выражений UNION ALL.

    Доработка функции getValueAt

    Аналогичным образом доработаем функцию getValueAt, которая ищет значение элемента данных, соответствующее указанной отметки времени:
    Index: zabbix-3.4.12-1+buster/frontends/php/include/classes/api/managers/CHistoryManager.php
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/frontends/php/include/classes/api/managers/CHistoryManager.php
    +++ zabbix-3.4.12-1+buster/frontends/php/include/classes/api/managers/CHistoryManager.php
    @@ -172,6 +249,9 @@ class CHistoryManager {
    */
    public function getValueAt($item, $clock, $ns) {
    switch (self::getDataSourceType($item['value_type'])) {
    + case ZBX_HISTORY_SOURCE_CLICKHOUSE:
    + return $this->getValueAtFromClickHouse($item, $clock, $ns);
    +
    case ZBX_HISTORY_SOURCE_ELASTIC:
    return $this->getValueAtFromElasticsearch($item, $clock, $ns);

    @@ -181,6 +261,74 @@ class CHistoryManager {
    }

    /**
    + * ClickHouse specific implementation of getValueAt.
    + *
    + * @see CHistoryManager::getValueAt
    + */
    + private function getValueAtFromClickHouse($item, $clock, $ns) {
    + $value = null;
    + $table = self::getTableName($item['value_type']);
    +
    + $endpoints = self::getClickHouseEndpoints($item['value_type']);
    + if ($endpoints) {
    + $url = reset($endpoints);
    +
    + $query = 'SELECT value'.
    + ' FROM '.$table.
    + ' WHERE itemid='.($item['itemid']+0).
    + ' AND clock='.($clock+0).
    + ' AND ns='.($ns+0).
    + ' LIMIT 1';
    + $value = CClickHouseHelper::value('POST', $url, $query);
    + if ($value !== null) {
    + return $value;
    + }
    +
    + $query = 'SELECT DISTINCT clock'.
    + ' FROM '.$table.
    + ' WHERE itemid='.($item['itemid']+0).
    + ' AND clock='.($clock+0).
    + ' AND ns<'.($ns+0);
    + $max_clock = CClickHouseHelper::value('POST', $url, $query, 'clock');
    +
    + if ($max_clock === null) {
    + $query = 'SELECT MAX(clock) AS clock'.
    + ' FROM '.$table.
    + ' WHERE itemid='.($item['itemid']+0).
    + ' AND clock<'.($clock+0).
    + (ZBX_HISTORY_PERIOD ? ' AND clock>='.($clock - ZBX_HISTORY_PERIOD) : '');
    +
    + $max_clock = CClickHouseHelper::value('POST', $url, $query, 'clock');
    + }
    +
    + if ($max_clock === null) {
    + return $value;
    + }
    +
    + if ($clock == $max_clock) {
    + $query = 'SELECT value'.
    + ' FROM '.$table.
    + ' WHERE itemid='.($item['itemid']+0).
    + ' AND clock='.($clock+0).
    + ' AND ns<'.($ns+0).
    + ' LIMIT 1';
    + }
    + else {
    + $query = 'SELECT value'.
    + ' FROM '.$table.
    + ' WHERE itemid='.($item['itemid']+0).
    + ' AND clock='.($max_clock+0).
    + ' ORDER BY itemid, clock, ns DESC'.
    + ' LIMIT 1';
    + }
    +
    + $value = CClickHouseHelper::value('POST', $url, $query);
    +
    + }
    + return $value;
    + }
    +
    + /**
    * Elasticsearch specific implementation of getValueAt.
    *
    * @see CHistoryManager::getValueAt
    Оптимизированной версии фукнции тут нет, потому что функция выполняет только один запрос, извлекающий единственное значение.

    Доработка функции getGraphAggregation

    Функция getGraphAggregation возвращает агрегированные данные одного или нескольких элементов данных для отрисовки графика, доступного по ссылкам на странице просмотра последних данных. Поскольку на одном графике могут отображаться кривые нескольких элементов данных, то данные для каждой из кривых можно получать либо отдельными запросами, либо сгруппированными запросами с выражениями UNION ALL. Первый вариант функции фигурирует ниже под именем _getGraphAggregationFromClickHouse, а второй, оптимизированный вариант функции можно найти по имени getGraphAggregationFromClickHouse:
    Index: zabbix-3.4.12-1+buster/frontends/php/include/classes/api/managers/CHistoryManager.php
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/frontends/php/include/classes/api/managers/CHistoryManager.php
    +++ zabbix-3.4.12-1+buster/frontends/php/include/classes/api/managers/CHistoryManager.php
    @@ -345,6 +493,12 @@ class CHistoryManager {
    $grouped_items = self::getItemsGroupedByStorage($items);

    $results = [];
    + if (array_key_exists(ZBX_HISTORY_SOURCE_CLICKHOUSE, $grouped_items)) {
    + $results += $this->getGraphAggregationFromClickHouse($grouped_items[ZBX_HISTORY_SOURCE_CLICKHOUSE],
    + $time_from, $time_to, $width, $size, $delta
    + );
    + }
    +
    if (array_key_exists(ZBX_HISTORY_SOURCE_ELASTIC, $grouped_items)) {
    $results += $this->getGraphAggregationFromElasticsearch($grouped_items[ZBX_HISTORY_SOURCE_ELASTIC],
    $time_from, $time_to, $width, $size, $delta
    @@ -361,6 +515,114 @@ class CHistoryManager {
    }

    /**
    + * ClickHouse specific implementation of getGraphAggregation.
    + *
    + * @see CHistoryManager::getGraphAggregation
    + */
    + private function _getGraphAggregationFromClickHouse(array $items, $time_from, $time_to, $width, $size, $delta) {
    + $group_by = 'itemid';
    + $sql_select_extra = '';
    +
    + if ($width !== null && $size !== null && $delta !== null) {
    + $calc_field = 'round('.$width.'*modulo(clock+'.$delta.','.$size.')/('.$size.'),0)';
    +
    + $sql_select_extra = ','.$calc_field.' AS i';
    + $group_by .= ','.$calc_field;
    + }
    +
    + $results = [];
    +
    + foreach ($items as $item) {
    + $endpoints = self::getClickHouseEndpoints($item['value_type']);
    + if ($endpoints) {
    + if ($item['source'] === 'history') {
    + $sql_select = 'COUNT(*) AS count,AVG(value) AS avg,MIN(value) AS min,MAX(value) AS max';
    + $sql_from = ($item['value_type'] == ITEM_VALUE_TYPE_UINT64) ? 'history_uint' : 'history';
    + }
    + else {
    + $sql_select = 'SUM(num) AS count,AVG(value_avg) AS avg,MIN(value_min) AS min,MAX(value_max) AS max';
    + $sql_from = ($item['value_type'] == ITEM_VALUE_TYPE_UINT64) ? 'trends_uint' : 'trends';
    + }
    +
    + $query =
    + 'SELECT itemid,'.$sql_select.$sql_select_extra.',MAX(clock) AS max_clock'.
    + ' FROM '.$sql_from.
    + ' WHERE itemid='.($item['itemid']+0).
    + ' AND clock>='.($time_from+0).
    + ' AND clock<='.($time_to+0).
    + ' GROUP BY '.$group_by;
    +
    + $values = CClickHouseHelper::values('POST', reset($endpoints), $query, null, ['max_clock' => 'clock']);
    +
    + $results[$item['itemid']]['source'] = $item['source'];
    + $results[$item['itemid']]['data'] = $values;
    + }
    + }
    +
    + return $results;
    + }
    +
    + /**
    + * ClickHouse specific implementation of getGraphAggregation.
    + *
    + * @see CHistoryManager::getGraphAggregation
    + */
    + private function getGraphAggregationFromClickHouse(array $items, $time_from, $time_to, $width, $size, $delta) {
    + $group_by = 'itemid';
    + $sql_select_extra = '';
    + $query_extra = '';
    +
    + if ($width !== null && $size !== null && $delta !== null) {
    + $calc_field = 'round('.$width.'*modulo(clock+'.$delta.','.$size.')/('.$size.'),0)';
    +
    + $sql_select_extra = ','.$calc_field.' AS i';
    + $group_by .= ','.$calc_field;
    + $query_extra = ',i';
    + }
    +
    + $results = [];
    + $url_queries = [];
    + foreach ($items as $item) {
    + $endpoints = self::getClickHouseEndpoints($item['value_type']);
    + if ($endpoints) {
    + if ($item['source'] === 'history') {
    + $sql_select = 'COUNT(*) AS count,AVG(value) AS avg,MIN(value) AS min,MAX(value) AS max';
    + $sql_from = ($item['value_type'] == ITEM_VALUE_TYPE_UINT64) ? 'history_uint' : 'history';
    + }
    + else {
    + $sql_select = 'SUM(num) AS count,AVG(value_avg) AS avg,MIN(value_min) AS min,MAX(value_max) AS max';
    + $sql_from = ($item['value_type'] == ITEM_VALUE_TYPE_UINT64) ? 'trends_uint' : 'trends';
    + }
    +
    + $query =
    + 'SELECT itemid,'.$sql_select.$sql_select_extra.',MAX(clock) AS max_clock'.
    + ' FROM '.$sql_from.
    + ' WHERE itemid='.($item['itemid']+0).
    + ' AND clock>='.($time_from+0).
    + ' AND clock<='.($time_to+0).
    + ' GROUP BY '.$group_by;
    +
    + $results[$item['itemid']]['source'] = $item['source'];
    + $url_queries[reset($endpoints)][] = $query;
    + }
    + }
    +
    + foreach ($url_queries as $url => $queries) {
    + $query =
    + 'SELECT itemid,count,avg,min,max'.$query_extra.',max_clock'.
    + ' FROM ('.implode(' UNION ALL ', $queries).')';
    +
    + $values = CClickHouseHelper::values('POST', $url, $query, null, ['max_clock' => 'clock']);
    +
    + foreach($values as $row) {
    + $results[$row['itemid']]['data'][] = $row;
    + }
    + }
    +
    + return $results;
    + }
    +
    + /**
    * Elasticsearch specific implementation of getGraphAggregation.
    *
    * @see CHistoryManager::getGraphAggregation

    Доработка функции getAggregatedValue

    Функция getAggregatedValue, как следует из её названия, возвращает агрегированное значение элемента данных. Функция агрегации указывается в аргументе aggregation (значением может быть строка «MAX», «MIN», «AVG», «COUNT», «SUM»), интересующий элемент данных - в аргументе item, а начальная отметка времени, начиная с которого нужно вернуть агрегированное значение, указывается в аргументе time_from. Из аргумента item на самом деле используется только идентификатор элемента данных, доступный по ключу itemid. По понятным причинам, оптимизированной версии функции getAggregatedValueFromClickHouse нет - здесь происходит запрос только по одному элементу данных:
    Index: zabbix-3.4.12-1+buster/frontends/php/include/classes/api/managers/CHistoryManager.php
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/frontends/php/include/classes/api/managers/CHistoryManager.php
    +++ zabbix-3.4.12-1+buster/frontends/php/include/classes/api/managers/CHistoryManager.php
    @@ -585,6 +847,9 @@ class CHistoryManager {
    */
    public function getAggregatedValue(array $item, $aggregation, $time_from) {
    switch (self::getDataSourceType($item['value_type'])) {
    + case ZBX_HISTORY_SOURCE_CLICKHOUSE:
    + return $this->getAggregatedValueFromClickHouse($item, $aggregation, $time_from);
    +
    case ZBX_HISTORY_SOURCE_ELASTIC:
    return $this->getAggregatedValueFromElasticsearch($item, $aggregation, $time_from);

    @@ -594,6 +859,27 @@ class CHistoryManager {
    }

    /**
    + * ClickHouse specific implementation of getAggregatedValue.
    + *
    + * @see CHistoryManager::getAggregatedValue
    + */
    + private function getAggregatedValueFromClickHouse(array $item, $aggregation, $time_from) {
    + $value = null;
    + $endpoints = self::getClickHouseEndpoints($item['value_type']);
    + if ($endpoints) {
    + $query =
    + 'SELECT '.$aggregation.'(value) AS value'.
    + ' FROM '.self::getTableName($item['value_type']).
    + ' WHERE clock>'.$time_from.
    + ' AND itemid='.($item['itemid']+0).
    + ' HAVING COUNT(*)>0';
    +
    + $value = CClickHouseHelper::value('POST', reset($endpoints), $query);
    + }
    + return $value;
    + }
    +
    + /**
    * Elasticsearch specific implementation of getAggregatedValue.
    *
    * @see CHistoryManager::getAggregatedValue

    Доработка функции getMinClock

    Функция getMinClock принимает список элементов данных в аргументе items, для которых нужно найти наименьшую отметку времени. Насколько я понимаю, эта функция используется при попытке открыть график в последних данных за всё время. Здесь выполняется один запрос с выражениями UNION ALL для объединения результатов запросов ко всем таблицам в ClickHouse. Интересно, что выражение UNION ALL используется и в функции getMinClockFromSql. Собственно, после того, как я наткнулся на эту функцию, мне и пришла в голову идея оптимизировать остальные функции, уменьшив количество запросов к ClickHouse при помощи выражения UNION ALL.
    Index: zabbix-3.4.12-1+buster/frontends/php/include/classes/api/managers/CHistoryManager.php
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/frontends/php/include/classes/api/managers/CHistoryManager.php
    +++ zabbix-3.4.12-1+buster/frontends/php/include/classes/api/managers/CHistoryManager.php
    @@ -718,6 +1004,10 @@ class CHistoryManager {

    $min_clock = [];

    + if (array_key_exists(ZBX_HISTORY_SOURCE_CLICKHOUSE, $storage_items)) {
    + $min_clock[] = $this->getMinClockFromClickHouse($storage_items[ZBX_HISTORY_SOURCE_CLICKHOUSE], $source);
    + }
    +
    if (array_key_exists(ZBX_HISTORY_SOURCE_ELASTIC, $storage_items)) {
    $min_clock[] = $this->getMinClockFromElasticsearch($storage_items[ZBX_HISTORY_SOURCE_ELASTIC]);
    }
    @@ -750,6 +1040,66 @@ class CHistoryManager {
    }

    /**
    + * ClickHouse specific implementation of getMinClock.
    + *
    + * @see CHistoryManager::getMinClock
    + */
    + private function getMinClockFromClickHouse(array $items, $source) {
    + $url_queries = [];
    + $endpoints = self::getClickHouseEndpoints(array_keys($items));
    + foreach ($items as $type => $itemids) {
    + if (!$itemids) {
    + continue;
    + }
    +
    + if (!array_key_exists($type, $endpoints)) {
    + continue;
    + }
    +
    + $url = $endpoints[$type];
    +
    + switch ($type) {
    + case ITEM_VALUE_TYPE_FLOAT:
    + $sql_from = $source;
    + break;
    + case ITEM_VALUE_TYPE_STR:
    + $sql_from = 'history_str';
    + break;
    + case ITEM_VALUE_TYPE_LOG:
    + $sql_from = 'history_log';
    + break;
    + case ITEM_VALUE_TYPE_UINT64:
    + $sql_from = $source.'_uint';
    + break;
    + case ITEM_VALUE_TYPE_TEXT:
    + $sql_from = 'history_text';
    + break;
    + default:
    + $sql_from = 'history';
    + }
    +
    + $url_queries[$url][] =
    + 'SELECT MIN(clock) AS min_clock'.
    + ' FROM '.$sql_from.
    + ' WHERE itemid IN ('.implode(',', $itemids).')';
    + }
    +
    + $min_clock = [];
    + foreach ($url_queries as $url => $queries) {
    + $query =
    + 'SELECT MIN(min_clock) AS min'.
    + ' FROM ('.implode(' UNION ALL ', $queries).')';
    +
    + $clock = CClickHouseHelper::value('POST', $url, $query, 'min');
    + if ($clock !== null) {
    + $min_clock[] = $clock;
    + }
    + }
    +
    + return min($min_clock);
    + }
    +
    + /**
    * Elasticsearch specific implementation of getMinClock.
    *
    * @see CHistoryManager::getMinClock

    Доработка класса CHistory

    В файле frontends/php/include/classes/api/services/CHistory.php определён класс CHistory, который отвечает за работу метода API history.get. Метод позволяет получать значения указанных элементов данных за указанный период. По сути в этом классе есть только одна публичная функция get и по одной приватной функции с реализацией каждого из типов хранилищ. Доработаем саму функцию get и добавим функцию getFromClickHouse с реализацией доступа к хранилищу ClickHouse:
    Index: zabbix-3.4.12-1+buster/frontends/php/include/classes/api/services/CHistory.php
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/frontends/php/include/classes/api/services/CHistory.php
    +++ zabbix-3.4.12-1+buster/frontends/php/include/classes/api/services/CHistory.php
    @@ -118,6 +118,9 @@ class CHistory extends CApiService {
    ]);

    switch (CHistoryManager::getDataSourceType($options['history'])) {
    + case ZBX_HISTORY_SOURCE_CLICKHOUSE:
    + return $this->getFromClickHouse($options);
    +
    case ZBX_HISTORY_SOURCE_ELASTIC:
    return $this->getFromElasticsearch($options);

    @@ -127,6 +130,139 @@ class CHistory extends CApiService {
    }

    /**
    + * ClickHouse specific implementation of get.
    + *
    + * @see CHistory::get
    + */
    + private function getFromClickHouse($options) {
    + $result = [];
    + $sql_parts = [
    + 'select' => ['history' => 'h.itemid'],
    + 'from' => [],
    + 'where' => [],
    + 'group' => [],
    + 'order' => [],
    + 'limit' => null
    + ];
    +
    + if (!$table_name = CHistoryManager::getTableName($options['history'])) {
    + $table_name = 'history';
    + }
    +
    + $endpoints = CHistoryManager::getClickHouseEndpoints($options['history']);
    + if (!$endpoints) {
    + return $result;
    + }
    + $url = reset($endpoints);
    +
    + $sql_parts['from']['history'] = $table_name.' h';
    +
    + // itemids
    + if ($options['itemids'] !== null) {
    + $sql_parts['where']['itemid'] = dbConditionInt('h.itemid', $options['itemids'], false, true, false);
    + }
    +
    + // time_from
    + if ($options['time_from'] !== null) {
    + $sql_parts['where']['clock_from'] = 'h.clock>='.($options['time_from']+0);
    + }
    +
    + // time_till
    + if ($options['time_till'] !== null) {
    + $sql_parts['where']['clock_till'] = 'h.clock<='.($options['time_till']+0);
    + }
    +
    + // filter
    + if (is_array($options['filter'])) {
    + $this->dbFilter($sql_parts['from']['history'], $options, $sql_parts);
    + }
    +
    + // search
    + if (is_array($options['search'])) {
    + zbx_db_search($sql_parts['from']['history'], $options, $sql_parts);
    + }
    +
    + // output
    + if ($options['output'] == API_OUTPUT_EXTEND) {
    + unset($sql_parts['select']['clock']);
    + $sql_parts['select']['history'] = 'h.*';
    + }
    + elseif ($options['output'] != API_OUTPUT_COUNT) {
    + unset($sql_parts['select']['clock']);
    + $sql_parts['select']['history'] = implode(',', $options['output']);
    + }
    +
    + // countOutput
    + if ($options['countOutput']) {
    + $options['sortfield'] = '';
    + $sql_parts['select'] = ['count(*) as rowscount'];
    +
    + // groupCount
    + if ($options['groupCount']) {
    + foreach ($sql_parts['group'] as $key => $fields) {
    + $sql_parts['select'][$key] = $fields;
    + }
    + }
    + }
    +
    + // sorting
    + $sql_parts = $this->applyQuerySortOptions($table_name, $this->tableAlias(), $options, $sql_parts);
    +
    + // limit
    + if (zbx_ctype_digit($options['limit']) && $options['limit']) {
    + $sql_parts['limit'] = $options['limit'];
    + }
    +
    + $sql_parts['select'] = array_unique($sql_parts['select']);
    + $sql_parts['from'] = array_unique($sql_parts['from']);
    + $sql_parts['where'] = array_unique($sql_parts['where']);
    + $sql_parts['order'] = array_unique($sql_parts['order']);
    +
    + $sql_select = '';
    + $sql_from = '';
    + $sql_order = '';
    +
    + if ($sql_parts['select']) {
    + $sql_select .= implode(',', $sql_parts['select']);
    + }
    +
    + if ($sql_parts['from']) {
    + $sql_from .= implode(',', $sql_parts['from']);
    + }
    +
    + $sql_where = $sql_parts['where'] ? ' WHERE '.implode(' AND ', $sql_parts['where']) : '';
    +
    + if ($sql_parts['order']) {
    + $sql_order .= ' ORDER BY '.implode(',', $sql_parts['order']);
    + }
    +
    + if ($sql_parts['limit'] > 0) {
    + $sql_limit = ' LIMIT '.$sql_parts['limit'];
    + }
    + $query = 'SELECT '.$sql_select.
    + ' FROM '.$sql_from.
    + $sql_where.
    + $sql_order.
    + $sql_limit;
    +
    + $values = CClickHouseHelper::values('POST', $url, $query);
    + foreach ($values as $row) {
    + if ($options['countOutput']) {
    + $result = $row;
    + }
    + else {
    + $result[] = $row;
    + }
    + }
    +
    + if (!$options['preservekeys']) {
    + $result = zbx_cleanHashes($result);
    + }
    +
    + return $result;
    + }
    +
    + /**
    * SQL specific implementation of get.
    *
    * @see CHistory::get

    Доработка класса CTrend

    Почти всё, сказанное про класс CHistory, справедливо и для класса CTrend. В файле frontends/php/include/classes/api/services/CTrend.php определён класс CTrend, который отвечает за работу метода API trend.get. Метод позволяет получать из таблиц тенденций агрегированные почасовые значения указанных элементов данных за указанный период. В этом классе есть только одна публичная функция get и по одной приватной функции с реализацией каждого из типов хранилищ.

    Поскольку в реализации поддержки Elasticsearch нет поддержки таблиц тенденций, а поддержка ClickHouse была сделана на базе поддержки Elasticsearch, то поддержки таблиц тенденций не должно быть и в реализации ClickHouse. Собственно, поэтому в файле конфигурации не предусмотрена возможность указать тип используемого хранилища для таблиц тенденций. Я же реализовал поддержку таблиц тенденций в неявном предположении, что таблица тенденций находится в том же хранилище ClickHouse, что и основная таблица с историческими данными. То есть, если для таблицы history используется хранилище ClickHouse, то неявно предполагается, что по той же ссылке должна быть доступна и таблица trends. И если в ClickHouse хранится таблица history_uint, то по той же ссылке должна быть доступна таблица trends_uint.

    Интересно, что раньше в Zabbix не было методов API для доступа к таблицам тенденций, но в сети гуляли заплатки с реализацией метода trend.get, выполненные по аналогии с методом history.get. Когда же разработчики Zabbix решили добавить метод API для доступа к таблицам тенденций, то реализовали метод trend.get несколько иначе. В частности, методу trend.get не нужно указывать тип запрашиваемых значений элементов данных, метод ищет данные во всех таблицах и возвращает результат поиска в обеих таблицах тенденций.

    Итак, доработаем саму функцию get и добавим функцию getFromClickHouse с реализацией доступа к хранилищу ClickHouse:
    Index: zabbix-3.4.12-1+buster/frontends/php/include/classes/api/services/CTrend.php
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/frontends/php/include/classes/api/services/CTrend.php
    +++ zabbix-3.4.12-1+buster/frontends/php/include/classes/api/services/CTrend.php
    @@ -71,11 +71,15 @@ class CTrend extends CApiService {
    }
    }

    - foreach ([ZBX_HISTORY_SOURCE_ELASTIC, ZBX_HISTORY_SOURCE_SQL] as $source) {
    + foreach ([ZBX_HISTORY_SOURCE_CLICKHOUSE, ZBX_HISTORY_SOURCE_ELASTIC, ZBX_HISTORY_SOURCE_SQL] as $source) {
    if (array_key_exists($source, $storage_items)) {
    $options['itemids'] = $storage_items[$source];

    switch ($source) {
    + case ZBX_HISTORY_SOURCE_CLICKHOUSE:
    + $data = $this->getFromClickHouse($options);
    + break;
    +
    case ZBX_HISTORY_SOURCE_ELASTIC:
    $data = $this->getFromElasticsearch($options);
    break;
    @@ -92,6 +96,103 @@ class CTrend extends CApiService {
    }
    }
    }
    +
    + return $result;
    + }
    +
    + /**
    + * ClickHouse specific implementation of get.
    + *
    + * @see CTrend::get
    + */
    + private function getFromClickHouse($options) {
    + $sql_where = [];
    +
    + if ($options['time_from'] !== null) {
    + $sql_where['clock_from'] = 't.clock>='.($options['time_from']+0);
    + }
    +
    + if ($options['time_till'] !== null) {
    + $sql_where['clock_till'] = 't.clock<='.($options['time_till']+0);
    + }
    +
    + if (!$options['countOutput']) {
    + $sql_limit = ($options['limit'] && zbx_ctype_digit($options['limit'])) ? $options['limit'] : null;
    +
    + $sql_fields = [];
    +
    + if (is_array($options['output'])) {
    + foreach ($options['output'] as $field) {
    + if ($this->hasField($field, 'trends') && $this->hasField($field, 'trends_uint')) {
    + $sql_fields[] = 't.'.$field;
    + }
    + }
    + }
    + elseif ($options['output'] == API_OUTPUT_EXTEND) {
    + $sql_fields[] = 't.*';
    + }
    +
    + // An empty field set or invalid output method (string). Select only "itemid" instead of everything.
    + if (!$sql_fields) {
    + $sql_fields[] = 't.itemid';
    + }
    +
    + $result = [];
    +
    + foreach ([ITEM_VALUE_TYPE_FLOAT, ITEM_VALUE_TYPE_UINT64] as $value_type) {
    + $endpoints = CHistoryManager::getClickHouseEndpoints($value_type);
    + if (!$endpoints) {
    + continue;
    + }
    +
    + if ($sql_limit !== null && $sql_limit <= 0) {
    + break;
    + }
    +
    + $sql_from = ($value_type == ITEM_VALUE_TYPE_FLOAT) ? 'trends' : 'trends_uint';
    +
    + if ($options['itemids'][$value_type]) {
    + $sql_where['itemid'] = dbConditionInt('t.itemid', array_keys($options['itemids'][$value_type]), false, true, false);
    +
    + $query = 'SELECT '.implode(',', $sql_fields).
    + ' FROM '.$sql_from.' t'.
    + ' WHERE '.implode(' AND ', $sql_where);
    +
    + if ($sql_limit > 0) $query .= ' LIMIT '.$sql_limit;
    +
    + $values = CClickHouseHelper::values('POST', reset($endpoints), $query);
    +
    + if ($sql_limit !== null) {
    + $sql_limit -= count($values);
    + }
    +
    + $result = array_merge($result, $values);
    + }
    + }
    +
    + $result = $this->unsetExtraFields($result, ['itemid'], $options['output']);
    + }
    + else {
    + $result = 0;
    +
    + foreach ([ITEM_VALUE_TYPE_FLOAT, ITEM_VALUE_TYPE_UINT64] as $value_type) {
    + if ($options['itemids'][$value_type]) {
    + $endpoints = CHistoryManager::getClickHouseEndpoints($value_type);
    + if (!$endpoints) {
    + continue;
    + }
    +
    + $sql_from = ($value_type == ITEM_VALUE_TYPE_FLOAT) ? 'trends' : 'trends_uint';
    + $sql_where['itemid'] = dbConditionInt('t.itemid', array_keys($options['itemids'][$value_type]), false, true, false);
    +
    + $query = 'SELECT COUNT(*) AS rowcount'.
    + ' FROM '.$sql_from.' t'.
    + ' WHERE '.implode(' AND ', $sql_where);
    +
    + $result += CClickHouseHelper::value('POST', reset($endpoints), $query, 'rowcount');
    + }
    + }
    + }

    return $result;
    }

    Značky: #zabbix, #Linux, #debian, #linux, #3.4

    • Sy chevron_right

      Подготовка ClickHouse для хранения истории и тенденций Zabbix

      pubsub.slavino.sk / sysadmblog · Sunday, 25 October, 2020 - 08:00 edit · 3 minutes

    Создание таблиц истории

    Подключение к серверу Clickhouse можно при помощи команды следующего вида:
    $ clickhouse-client -u zabbix --ask-password zabbix
    В Glaber 'е используется одна таблица вместо таблиц history, history_uint, history_str и history_text. Таблица history_log не поддерживается. В отличие от Glaber, я решил скрупулёзно воспроизвести схему данных, принятую в Zabbix.

    Однако, если писать данные в таблицы небольшими порциями, менее 8192 строк за раз, Clickhouse не выполняет слияние таких маленьких фрагментов в фоновом режиме. В процессе всего нескольких часов работы Zabbix в Clickhouse могут накопиться сотни тысяч фрагментов. При необходимости перезапуска Clickhouse в таком случае можно столкнуться с интересной проблемой: Clickhouse запущен, но не открывает порты на прослушивание и не принимает подключения. В это время Clickhouse перебирает все фрагменты таблиц, чтобы составить их каталог. Запуск может затянуться на несколько часов.

    Чтобы Clickhouse своевременно сливал фрагменты таблиц в фоновом режиме, нужно чтобы в каждом фрагменте было не мнее 8192 строк. Но даже если ваш сервер Zabbix генерирует более 8192 новых значений в секунду, они будут во-первых распределяться между процессами DBSyncers (количество которых настраивается через опцию конфигурации StartDBSyncers), а во-вторых - они будут распределяться между разными таблицами истории. В моей практике наибольшая доля данных приходилась на таблицу history_uint, а history_log во многих случаях не использовалась вовсе.

    По умолчанию процессы DBSyncers запускаются раз в секунду. Уменьшать их количество не всегда возможно, т.к. эти же процессы используются и для чтения данных из таблиц истории, когда необходимых данных нет в кэше значений. Особенно высока потребность в большом количестве процессов DBSyncers при старте Zabbix, когда кэш значений ещё пуст. Я пробовал делать заплатку для Zabbix, которая добавляет поддержку опции конфигурации DBSyncersPeriod и позволяет настраивать периодичность записи процессами DBSyncers. Такое решение не прошло проверку практикой, при малом количестве новых значений в секунду и большом количестве DBSyncers для накопления достаточного объёма данных приходится выполнять запись раз в 2-5 минут. И это без учёта неравномерности распределения данных по разным таблицам!

    Таким образом, решение использовать буферную таблицу в Glaber было вполне оправданным . Поэтому настоящие таблицы истории в моём варианте можно создать при помощи следующих запросов:
    CREATE TABLE real_history_uint
    (
    itemid UInt64,
    clock UInt32,
    ns UInt32,
    value UInt64
    ) ENGINE = MergeTree()
    PARTITION BY toYYYYMMDD(toDateTime(clock))
    ORDER BY (itemid, clock);

    CREATE TABLE real_history
    (
    itemid UInt64,
    clock UInt32,
    ns UInt32,
    value Float64
    ) ENGINE = MergeTree()
    PARTITION BY toYYYYMMDD(toDateTime(clock))
    ORDER BY (itemid, clock);

    CREATE TABLE real_history_str
    (
    itemid UInt64,
    clock UInt32,
    ns UInt32,
    value String
    ) ENGINE = MergeTree()
    PARTITION BY toYYYYMMDD(toDateTime(clock))
    ORDER BY (itemid, clock);

    CREATE TABLE real_history_text
    (
    itemid UInt64,
    clock UInt32,
    ns UInt32,
    value String
    ) ENGINE = MergeTree()
    PARTITION BY toYYYYMMDD(toDateTime(clock))
    ORDER BY (itemid, clock);

    CREATE TABLE real_history_log
    (
    itemid UInt64,
    clock UInt32,
    timestamp DateTime,
    source FixedString(64),
    severity UInt32,
    value String,
    logeventid UInt32,
    ns UInt32
    ) ENGINE = MergeTree()
    PARTITION BY toYYYYMMDD(toDateTime(clock))
    ORDER BY (itemid, clock);
    Как можно заметить, в отличие от Glaber , здесь таблицы разделены не на помесячные, а на посуточные секции. Для удаления ненужных секций в дальнейшем можно будет воспользоваться запросами следующего вида:
    ALTER TABLE real_history_uint DROP PARTITION 20190125;

    Создание буферных таблиц истории

    Теперь нужно создать буферные таблицы, с которыми непосредственно будет работать сам Zabbix. При вставке данных в буферную таблицу данные сохраняются в оперативной памяти и не записываются в нижележащую таблицу, пока не будет достигнуто одно из условий. При чтении данных из буферной таблицы данные ищутся как в самой буферной таблице, так и в нижележащей таблице на диске. Я создал буферные таблицы следующим образом:
    CREATE TABLE history_uint
    (
    itemid UInt64,
    clock UInt32,
    ns UInt32,
    value UInt64
    ) ENGINE = Buffer(zabbix, real_history_uint, 8, 30, 60, 8192, 65536, 262144, 67108864);

    CREATE TABLE history
    (
    itemid UInt64,
    clock UInt32,
    ns UInt32,
    value Float64
    ) ENGINE = Buffer(zabbix, real_history, 8, 30, 60, 8192, 65536, 262144, 67108864);

    CREATE TABLE history_str
    (
    itemid UInt64,
    clock UInt32,
    ns UInt32,
    value String
    ) ENGINE = Buffer(zabbix, real_history_str, 8, 30, 60, 8192, 65536, 262144, 67108864);

    CREATE TABLE history_text
    (
    itemid UInt64,
    clock UInt32,
    ns UInt32,
    value String
    ) ENGINE = Buffer(zabbix, real_history_text, 8, 30, 60, 8192, 65536, 262144, 67108864);

    CREATE TABLE history_log
    (
    itemid UInt64,
    clock UInt32,
    timestamp DateTime,
    source FixedString(64),
    severity UInt32,
    value String,
    logeventid UInt32,
    ns UInt32
    ) ENGINE = Buffer(zabbix, real_history, 8, 30, 60, 8192, 65536, 262144, 67108864);
    Для всех созданных буферных таблиц действуют следующие условия записи данных в реальную таблицу:
    • 30 секунд - минимальное время, которое должно пройти со момента предыдущей записи в реальную таблицу, прежде чем буферная таблица запишет данные в реальную таблицу,
    • 60 секунд - максимальное время с момента предыдущей записи в реальную таблицу, по прошествии которого операция записи будет выполнена вне зависимости от всех остальных условий,
    • 8192 строк - минимальное количество записей, которое должно быть в буферной таблице, прежде чем буферная таблица запишет данные в реальную таблицу,
    • 65536 строк - максимальное количество записей, которое должно быть в буферной таблице, по достижении которого операция записи будет выполнена вне зависимости от всех остальных условий,
    • 256 килобайт - минимальный объём данных, который должен накопиться в буферной таблице, прежде чем буферная таблица запишет данные в реальную таблицу,
    • 64 мегабайта - максимальный объём данных, который должен накопиться в буферной таблице, по достижении которого операция записи будет выполнена вне зависимости от всех остальных условий.
    Итак, буферная таблица ждёт либо выполнения одного из условий максимума, либо выполнения всех условий минимума, после чего данные будут записаны в реальную таблицу.

    Виртуальные таблицы тенденций в ClickHouse

    Т.к. в моём случае внедрение Zabbix состоялось давно и вокруг него уже было написано значительное количество различных скриптов, использующих данные из таблиц тенденций, то мне нужно было сделать переход на ClickHouse максимально мягким. Для этого я воспользовался готовым решением Михаила Макурова, которое он продемонстрировал на одном из слайдов своей презентации. Воспользуемся агрегирующими материализованными представлениями ClickHouse, создав их при помощи следующих запросов:
    CREATE MATERIALIZED VIEW trends
    ENGINE = AggregatingMergeTree() PARTITION BY toYYYYMMDD(toDateTime(clock))
    ORDER BY (itemid, clock)
    AS SELECT
    itemid,
    toUInt32(toStartOfHour(toDateTime(clock))) AS clock,
    count(value) AS num,
    min(value) AS value_min,
    avg(value) AS value_avg,
    max(value) AS value_max
    FROM real_history
    GROUP BY itemid, clock;

    CREATE MATERIALIZED VIEW trends_uint
    ENGINE = AggregatingMergeTree() PARTITION BY toYYYYMMDD(toDateTime(clock))
    ORDER BY (itemid, clock)
    AS SELECT
    itemid,
    toUInt32(toStartOfHour(toDateTime(clock))) AS clock,
    count(value) AS num,
    min(value) AS value_min,
    toUInt64(avg(value)) AS value_avg,
    max(value) AS value_max
    FROM real_history_uint
    GROUP BY itemid, clock;
    Эти представления материализованные, а это значит, что они будут вычисляться не при поступлении запроса, а будут храниться на диске. Представления будут автоматически обновляться сервером Clickhouse по мере вставки новых данных в таблицы истории. Серверу Zabbix не нужно будет вычислять эти данные самостоятельно, т.к. всю необходимую работу за него будет делать Clickhouse.

    Скрипты

    Итак, структура таблиц готова, теперь нужно разобраться с обслуживанием таблиц, в том числе удалением устаревших секций таблиц истории и материализованных видов таблиц тенденций, а также с переносом данных. Для обеих задач я решил написать скрипты на Python, воспользовавшись модулем Python для работы с Clickhouse, который называется clickhouse-driver. Из всех рассмотренных мной модулей для языка Python этот модуль приглянулся по следующим причинам:
    • единственный, который использует двоичный протокол ClickHouse, а не использует доступ по HTTP,
    • снабжён файлом README и каталогом с документацией. Документация также доступна онлайн: Welcome to clickhouse-driver .
    Я собрал deb-пакеты с модулем clickhouse-driver, воспользовавшись своей статьёй Создание deb-пакетов для модулей Python . Вместе с этим модулем понадобилось также собрать deb-пакеты с требуемыми ей модулями clickhouse-cityhash и zstd.

    Обслуживание таблиц

    Для удаления устаревших секций таблиц и для принудительного слияния фрагментов секций я написал скрипт на Python, который назвал maintein_tables.py:
    #!/usr/bin/python
    # -*- coding: UTF-8 -*-

    from clickhouse_driver import Client
    from datetime import datetime, timedelta

    try:
    c = Client(host='localhost',
    port=9000,
    connect_timeout=3,
    database='zabbix',
    user='zabbix',
    password='zabbix')
    except clickhouse_driver.errors.ServerException:
    print >>sys.stderr, 'Cannot connect to database'
    sys.exit(1)

    def maintein_table(c, database, table, keep_interval):
    """
    Удаление устаревших разделов указанной таблицы и оптимизация оставшихся разделов

    c - подключение к базе данных
    database - имя базы данных, в которой нужно произвести усечение таблицы
    table - имя таблицы, которую нужно усечь
    keep_interval - период, данные за который нужно сохранить, тип - timedelta
    """
    now = datetime.now()

    rows = c.execute('''SELECT partition,
    COUNT(*)
    FROM system.parts
    WHERE database = '%s'
    AND table = '%s'
    GROUP BY partition
    ORDER BY partition
    ''' % (database, table))
    for partition, num in rows:
    if now - datetime.strptime(partition, '%Y%m%d') > keep_interval:
    print 'drop partition %s %s %s' % (database, table, partition)
    c.execute('ALTER TABLE %s.%s DROP PARTITION %s' % (database, table, partition))
    elif num > 1:
    print 'optimize partition %s %s %s' % (database, table, partition)
    c.execute('OPTIMIZE TABLE %s.%s PARTITION %s FINAL DEDUPLICATE' % (database, table, partition))

    maintein_table(c, 'zabbix', 'trends', timedelta(days=3650))
    maintein_table(c, 'zabbix', 'trends_uint', timedelta(days=3650))
    maintein_table(c, 'zabbix', 'real_history', timedelta(days=365))
    maintein_table(c, 'zabbix', 'real_history_uint', timedelta(days=365))
    maintein_table(c, 'zabbix', 'real_history_str', timedelta(days=7))
    maintein_table(c, 'zabbix', 'real_history_text', timedelta(days=7))
    maintein_table(c, 'zabbix', 'real_history_log', timedelta(days=7))
    c.disconnect()
    Запрос для удаления устаревших секций таблиц уже был приведён, а для слияния фрагментов секций таблиц в скрипте используется запрос следующего вида:
    OPTIMIZE TABLE history PARTITION 20200521 FINAL DEDUPLICATE;
    В примере скрипт настроен на хранение числовых исторических данных в течение года, текстовых и журнальных данных - в течение семи дней и тенденций в течение 10 лет. При необходимости можно поменять настройки подключения к серверу Clickhouse и настройки длительности хранения данных в таблицах.

    Скрипт можно также скачать по ссылке maintein_tables.py

    Копирование данных

    Михаил Макуров реализовал поддержку хранения исторических данных Zabbix в ClickHouse на основе поддержки хранения исторических данных в ElasticSearch. В документации Zabbix упоминается, что при хранении исторических данных в ElasticSearch таблицы тенденций не используются. Стало быть, таблицы тенденций не используются и при хранении исторических данных в ClickHouse. Для решения этой проблемы были созданы материализованные представления trends и trends_uint, которые описаны выше.

    При переключении существующей инсталляции Zabbix на использование ClickHouse или ElasticSearch не составляет особого труда перенести содержимое таблиц истории из старого хранилища в новое. А вот таблицы тенденций копировать просто некуда. Скопировать их можно было бы в таблицы истории, но в таблицах тенденций нет точных значений, а есть лишь минимальные, средние и максимальные значения за час, а также количество значений в исходной выборке. Чтобы сохранить возможность видеть данные из таблиц тенденций на графиках, можно попытаться сгенерировать выборку, удовлетворяющую этим условиям, и поместить получившиеся значения в таблицы истории.

    Поскольку и в дальнейшем хотелось бы иметь возможность просматривать графики за тот период, который изначально был выбран для таблиц тенденций, такой подход позволил бы сразу оценить:
    • сколько места на диске займёт точная история за период, аналогичный периоду хранения таблиц тенденций,
    • насколько хорошо ClickHouse будет справляться с такой нагрузкой.
    Итак, кроме функций копирования содержимого таблиц истории, понадобятся также функции для генерирования правдоподобных исторических данных на основе таблиц тенденций.

    Скрипт copy_data.py выполняет полное копирование таблиц истории, а также дополняет таблицы истории правдоподобными данными, сгенерированными на основе таблиц тенденций.

    В начале скрипта можно найти настройки, которые будут использоваться для подключения к базе данных с исходными данными и к целевой базе данных. В конце скрипта можно найти вызовы функций копирования данных:
    copy_history('history_str', ('itemid', 'clock', 'ns', 'value'))
    copy_history('history_text', ('itemid', 'clock', 'ns', 'value'))
    copy_history('history_log', ('itemid', 'clock', 'timestamp', 'source', 'severity', 'value', 'logeventid', 'ns'))

    clock_min, clock_max = copy_history('history', ('itemid', 'clock', 'ns', 'value'), interval=10800)
    copy_trends('trends', 'history', clock_max=clock_min)

    clock_min, clock_max = copy_history('history_uint', ('itemid', 'clock', 'ns', 'value'), interval=1800)
    copy_trends('trends_uint', 'history_uint', clock_max=clock_min, int_mode=True)
    Сначала копируется содержимое таблиц history_str, history_text и history_log, потом копируется таблица history порциями по 3 часа, потом в таблицу history вносятся данные, сгенерированные из данных таблицы trends, и, наконец, таблица history_uint копируется порциями по полчаса и в неё вносятся данные, сгенерированные из данных таблицы trends_uint.

    Функция copy_history перед началом работы выполняет запрос, который находит минимальное и максимальное значение отметок времени в таблице. Этот запрос может выполняться очень долго, поэтому в функциях предусмотрена возможность указания минимального и максимального значения отметок времени в аргументах clock_min и clock_max. Кроме того, указывая эти значения вручную, можно точно настраивать период времени, данные за который нужно обработать. Это может быть полезно, например, для того, чтобы скопировать данные до конца суток, предшествующих переключению Zabbix на Clickhouse. После переключения можно указать период времени, за который накопились новые данные с момента прошлого копирования.

    Если вы не собираетесь копировать данные из таблиц тенденций, то вызовы функций copy_trends можно закомментировать. Можно скопировать тенденции после переключения Zabbix на Clickhouse - скрипт не имеет жёстко предписанной последовательности действий и может быть адаптирован под необходимую вам последовательность действий.

    Скрипт осуществляет вставку данных в ClickHouse порциями по 1048576 строк. Размер порции можно настраивать при помощи аргумента portion функций copy_history и copy_trends. При настройке portion стоит учитывать, что объём вставляемых за один раз данных не должно превышать значения настройки max_memory_usage из файла конфигурации /etc/clickhouse-server/users.xml сервера Clickhouse.

    В процессе тестирования скрипта при переносе данных из базы данных PostgreSQL выяснилась интересная особенность: PostgreSQL не поддерживает колонки с числами с плавающей запятой, а вместо этого используется тип Decimal, имеющий более ограниченную точность в представлении данных. Из-за меньшей точности данные в таблице тенденций чисел с плавающей запятой оказывается невозможно сгенерировать такую правдоподобную выборку данных, которая точно соответствовала бы указанным свойствам. Если сгенерированная выборка не удовлетворяет указанным свойствам, то сгенерированные данные всё-таки вставляются в таблицу истории, но скрипт выдаёт предупреждение о подозрительных данных тенденций.

    Скрипт не приведён в статье из-за его относительно большого объёма. Скрипт можно взять по ссылке copy_data.py

    Пасхальное яйцо

    При выходе из клиента ClickHouse 1 января 2020 года заметил поздравление с новым годом:
    db.server.tld :) exit
    Happy new year.

    Značky: #zabbix, #Linux, #linux, #clickhouse, #buster, #debian

    • Sy chevron_right

      Установка и настройка сервера ClickHouse

      pubsub.slavino.sk / sysadmblog · Sunday, 18 October, 2020 - 08:00 edit · 3 minutes

    Пакеты с клиентом и сервером Clickhouse имеются в официальных репозиториях Debian Buster. Для их установки можно воспользоваться следующей командой:
    # apt-get install clickhouse-server clickhouse-client
    Для работы серверу Clickhouse требуется поддержка дополнительных процессорных инструкций SSE 4.2. Чтобы проверить наличие поддержки этих инструкций и пересобрать Clickhouse, если они не поддерживаются, обратитесь к статье Пересборка Clickhouse для процессоров без поддержки SSE 4.2 .

    В каталоге /etc/clickhouse-server находится файл config.xml с настройками сервера и файл users.xml с настройками пользователей. Оба файла хорошо прокомментированы, но из-за обилия настроек ориентироваться в них довольно тяжело. Я переименовал эти файлы, чтобы создать более компактные файлы конфигурации:
    # cd /etc/clickhouse-server/
    # cp users.xml users.xml.sample
    # cp config.xml config.xml.sample
    В файл конфигурации config.xml я вписал следующие настройки:
    <?xml version="1.0"?>
    <yandex>
    <logger>
    <level>warning</level>
    <log>/var/log/clickhouse-server/clickhouse-server.log</log>
    <errorlog>/var/log/clickhouse-server/clickhouse-server.err.log</errorlog>
    <size>10M</size>
    <count>10</count>
    </logger>
    <display_name>ufa</display_name>
    <http_port>8123</http_port>
    <tcp_port>9000</tcp_port>
    <listen_host>0.0.0.0</listen_host>
    <max_connections>4096</max_connections>
    <keep_alive_timeout>3</keep_alive_timeout>
    <max_concurrent_queries>16</max_concurrent_queries>
    <uncompressed_cache_size>1073741824</uncompressed_cache_size>
    <mark_cache_size>5368709120</mark_cache_size>
    <path>/var/lib/clickhouse/</path>
    <tmp_path>/var/lib/clickhouse/tmp/</tmp_path>
    <user_files_path>/var/lib/clickhouse/user_files/</user_files_path>
    <users_config>users.xml</users_config>
    <default_profile>default</default_profile>
    <default_database>zabbix</default_database>
    <timezone>Asia/Yekaterinburg</timezone>
    <mlock_executable>true</mlock_executable>
    <builtin_dictionaries_reload_interval>3600</builtin_dictionaries_reload_interval>
    <max_session_timeout>3600</max_session_timeout>
    <default_session_timeout>60</default_session_timeout>
    <max_table_size_to_drop>0</max_table_size_to_drop>
    <max_partition_size_to_drop>0</max_partition_size_to_drop>
    <format_schema_path>/var/lib/clickhouse/format_schemas/</format_schema_path>
    </yandex>
    Смысл большинства настроек можно понять из их названия. Кратко опишу некоторые из них:
    • display_name - отображаемое в клиенте имя сервера,
    • max_connections - максимальное количество подключений от клиентов,
    • max_concurrent_queries - максимальное количество одновременно обрабатываемых запросов. Т.к. каждый запрос обслуживается конвейером из нескольких потоков, то каждый запрос порождает нагрузку как минимум на одно процессорное ядро. Лучше всего будет выполнять одновременно количество запросов, не превышающее количество процессорных ядер сервера или виртуальной машины.
    • uncompressed_cache_size задаёт размер кэша несжатых данных в байтах. Если предполагается, что на сервере часто будут выполняться короткие запросы, этот кэш поможет снизить нагрузку на дисковую подсистему. Обратите внимание, что в настройках пользователя должно быть разрешено использование кэша несжатых данных в опции use_uncompressed_cache.
    • mark_cache_size - кэш меток. Метки являются своего рода индексами данных. Сервер Clickhouse не хочет запускаться, если значение этой настройки меньше 5 гигабайт. Хорошая новость в том, что память под этот кэш будет выделяться по мере необходимости.
    • path - путь к файлам базы данных,
    • default_database - имя базы данных, с которой будут работать клиенты, не указавшие какую-то определённую базу данных,
    • timezone - часовой пояс сервера.
    Файл users.xml я привёл к следующему виду:
    <?xml version="1.0"?>
    <yandex>
    <users>
    <zabbix>
    <password>zabbix</password>
    <networks>
    <ip>127.0.0.1</ip>
    </networks>
    <profile>default</profile>
    <quota>default</quota>
    </zabbix>
    </users>
    <profiles>
    <default>
    <max_memory_usage>2147483648</max_memory_usage>
    <max_query_size>1048576</max_query_size>
    <max_ast_elements>1000000</max_ast_elements>
    <use_uncompressed_cache>1</use_uncompressed_cache>
    <load_balancing>random</load_balancing>
    <readonly>0</readonly>
    </default>
    <readonly>
    <readonly>1</readonly>
    </readonly>
    </profiles>
    <quotas>
    <default>
    <interval>
    <duration>3600</duration>
    <queries>0</queries>
    <errors>0</errors>
    <result_rows>0</result_rows>
    <read_rows>0</read_rows>
    <execution_time>0</execution_time>
    </interval>
    </default>
    </quotas>
    </yandex>
    Файл состоит из трёх секций:
    • users - пользователи базы данных. Каждый пользователь содержит ссылку на профиль и квоту,
    • profiles - профили содержат настройки пользователей,
    • quotas - квоты содержат ограничения на выполнение запросов от пользователей.
    В примере конфигурации выше описан пользователь zabbix с паролем zabbix, который может устанавливать подключения к серверу только с IP-адреса 127.0.0.1, использует профиль default и квоту default.

    В профиле default выставлены следующие настройки:
    • max_memory_usage - максимальный объём памяти, который сервер может выделить пользователю для обработки его запросов, в примере настроено ограничение в 2 гигабайта,
    • max_query_size - максимальный размер одного запроса, по умолчанию - 256 килобайт, в примере - 1 мегабайт,
    • max_ast_elements - максимальное количество элементов в дереве синтаксического разбора, по умолчанию - 50 тысяч элементов, в примере - 1 миллион элементов,
    • use_uncompressed_cache - значение этой опции разрешает или запрещает использование кэша несжатых данных, в примере значение 1 разрешает его использование,
    • readonly - значение этой опции разрешает или запрещает запросы на изменение данных, в примере значение 0 разрешает изменение данных.
    В квоте default выставлено единственное ограничение - длительность обработки запроса ограничена одним часом.

    Включим автозапуск сервера:
    # systemctl enable clickhouse-server.service
    Запустим сервер:
    # systemctl start clickhouse-server.service

    Решение проблем

    Если спустя некоторое время в журнале /var/log/clickhouse-server/clickhouse-server.err.log появляются ошибки следующего вида:
    2020.04.17 10:44:51.741280 [ 6317714 ] {} <Error> HTTPHandler: std::exception. Code: 1001, type: std::system_error, e.what() = Resource temporarily unavailable
    То может помочь увеличение переменной ядра vm.max_map_count следующей командой:
    # sysctl -w vm.max_map_count = 524288
    Если изменение этой настройки помогло справиться с проблемой, можно прописать её в файл /etc/sysctl.conf, чтобы оно автоматически применялось при загрузке системы:
    vm.max_map_count=524288
    В документации ядра Linux эта переменная ядра объясняется следующим образом:

    This file contains the maximum number of memory map areas a process may have. Memory map areas are used as a side-effect of calling malloc, directly by mmap and mprotect, and also when loading shared libraries.

    While most applications need less than a thousand maps, certain programs, particularly malloc debuggers, may consume lots of them, e.g., up to one or two maps per allocation.

    The default value is 65536.

    Перевод:

    Этот файл содержит максимальное количество участков памяти, которое может иметь процесс. Участки памяти косвенно создаются при вызове malloc, а напрямую - при вызове mmap и mprotect, а также при загрузке разделяемых библиотек.

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

    Значение по умолчанию - 65536.


    Značky: #Linux, #debian, #linux, #buster

    • Sy chevron_right

      Пересборка ClickHouse для процессоров без поддержки SSE 4.2

      pubsub.slavino.sk / sysadmblog · Sunday, 11 October, 2020 - 08:00 edit · 1 minute

    В официальных репозиториях Debian Buster появились пакеты с сервером и клиентом Clickhouse. По умолчанию сервер Clickhouse собран с использованием процессорных инструкций SSE 4.2, т.к. именно такие системные требования указаны на официальной странице проекта.

    Для проверки, поддерживает ли процессор SSE 4.2, можно воспользоваться следующей командой:
    $ grep -q sse4_2 /proc/cpuinfo && echo "SSE 4.2 supported" || echo "SSE 4.2 not supported"
    При попытке запустить сервер ClickHouse на процессоре, не поддерживающем этот набор инструкций, в журнале /var/log/messages можно будет обнаружить сообщения следующего вида:
    Aug  9 18:33:45 buster kernel: [    7.571795] traps: clickhouse-serv[257] trap invalid opcode ip:7f89f23 sp:7ffda2789a98 error:0 in clickhouse[400000+f8a5000]
    Мой домашний компьютер не отличается новизной, поэтому для экспериментов дома мне пришлось пересобрать пакеты с Clickhouse. Сборочные скрипты автоматически определяют поддержку инструкций SSE 4.2 и при её отсутствии выполняют сборку так, чтобы пакеты работали без них.

    Впишем в файл /etc/apt/sources.list дополнительные репозитории с исходными текстами:
    deb-src http://mirror.yandex.ru/debian/ buster main contrib non-free
    deb-src http://mirror.yandex.ru/debian/ buster-updates main contrib non-free
    deb-src http://mirror.yandex.ru/debian/ buster-proposed-updates main contrib non-free
    deb-src http://mirror.yandex.ru/debian-security/ buster/updates main contrib non-free
    Обновим список пакетов, доступных через репозитории:
    # apt-get update
    Установим пакеты, которые потребуются нам для сборки ClickHouse из исходных текстов:
    # apt-get build-dep clickhouse
    И скачаем пакет с исходными текстами:
    $ apt-get source clickhouse
    Переходим в каталог с распакованными исходными текстами, запускаем dch и описываем изменения.
    $ cd clickhouse-18.16.1+ds
    $ dch -i
    В открывшемся редакторе дописываем к номеру версии fix1 и описываем изменения:
    clickhouse (18.16.1+ds-4fix1) UNRELEASED; urgency=medium

    * Version with no need CPU with support SSE4.2 instruction set..

    -- Vladimir Stupin <vladimir@stupin.su> Tue, 14 Jan 2020 11:28:11 +0500
    Собираем пакет:
    $ debuild -us -uc
    Для сборки потребуется довольно много оперативной памяти. Я пытался собрать пакет на виртуальной машине с 2 гигабайтами оперативной памяти, потом увеличил до 3 и до 4, но этого объёма оказывалось по-прежнему недостаточно для того, чтобы собрать библиотеку libclickhouse.so из объектных файлов. Вернул виртуальной машине 2 гигабайта оперативной памяти и подключил раздел подкачки размером 8 гигабайт. Сборка шла долго, но всё-таки завершилась успешно.

    Если сборка завершается неудачно, а в тексте ошибки имеются такие строки:
    CMake Error: Error: generator : Unix Makefiles
    Does not match the generator used previously: Ninja
    Either remove the CMakeCache.txt file and CMakeFiles directory or choose a different binary directory.
    То можно попробовать удалить пакет ninja-build:
    # apt-get purge ninja-build
    Затем можно попробовать запустить сборку пакета снова.

    После успешной сборки можно будет выйти из сборочного каталога и установить появившиеся рядом с ним двоичные пакеты:
    # dpkg -i clickhouse-server_18.16.1+ds-4fix1_amd64.deb clickhouse-client_18.16.1+ds-4fix1_amd64.deb clickhouse-common_18.16.1+ds-4fix1_amd64.deb
    Или можно воспользоваться утилитой aptly, чтобы создать собственный репозиторий и поместить в него эти пакеты. В таком случае для установки пакетов в систему будет достаточно:
    1. подключить этот репозиторий в файле /etc/apt/sources.list,
    2. обновить список пакетов, доступных через репозитории, командой apt-get update,
    3. поставить пакеты, например, командой apt-get install clickhouse-server clickhouse-client

    Značky: #linux, #debian, #Linux, #clickhouse

    • Sy chevron_right

      Ошибки аутентификации SNMPv3 в Zabbix 3.4

      pubsub.slavino.sk / sysadmblog · Sunday, 4 October, 2020 - 08:00

    Для контроля доступности устройства по SNMP обычно я использую элементы данных типа «Внутренний Zabbix» с ключом «zabbix[host,snmp,available]», который описан на странице документации 8 Внутренние проверки . Подробнее о недоступности узлов можно прочитать здесь: 12 Настройки недостижимости/недоступности хостов . Итак, когда сервер Zabbix решает, что узел больше не доступен по SNMP, значение элемента данных с ключом «zabbix[host,snmp,available]» становится равным нулю. Можно настроить триггер, который будет срабатывать при нулевом значении этого ключа.

    После того, как в сети появились узлы, опрашиваемые по SNMPv3, отсутствие срабатываний этого триггера перестало быть надёжным критерием доступности узла по SNMP. Все элементы данных SNMPv3 на узле могут быть в неподдерживаемом состоянии, однако Zabbix при этом считает узел доступным по SNMP и триггер не срабатывает. Как выяснилось, Zabbix не считает проблемой, если узел ответил ошибкой аутентификации. Формально устройство действительно отвечает по протоколу SNMP, но фактически данные с него не снимаются.

    Я решил испрвить эту ситуацию, в очередной раз внеся правку в исходный текст Zabbix. К счастью, сделать это оказалось совсем не сложно. Интересующий нас фрагмент кода находится в файле src/zabbix_server/poller/checks_snmp.c в функции zbx_get_snmp_response_error. Удалим специальную обработку ошибок аутентификации SNMPv3, интерпретируя эти ошибки как недоступность элемента данных:
    Index: zabbix-3.4.12-1+buster/src/zabbix_server/poller/checks_snmp.c
    ===================================================================
    --- zabbix-3.4.12-1+buster.orig/src/zabbix_server/poller/checks_snmp.c
    +++ zabbix-3.4.12-1+buster/src/zabbix_server/poller/checks_snmp.c
    @@ -391,17 +391,7 @@ static int zbx_get_snmp_response_error(c
    {
    zbx_snprintf(error, max_error_len, "Cannot connect to \"%s:%hu\": %s.",
    interface->addr, interface->port, snmp_api_errstring(ss->s_snmp_errno));
    -
    - switch (ss->s_snmp_errno)
    - {
    - case SNMPERR_UNKNOWN_USER_NAME:
    - case SNMPERR_UNSUPPORTED_SEC_LEVEL:
    - case SNMPERR_AUTHENTICATION_FAILURE:
    - ret = NOTSUPPORTED;
    - break;
    - default:
    - ret = NETWORK_ERROR;
    - }
    + ret = NETWORK_ERROR;
    }
    else if (STAT_TIMEOUT == status)
    {
    Эту тривиальную заплатку можно взять по ссылке zabbix3_4_12_snmpv3_auth_errors.patch .

    Značky: #zabbix, #3.4, #Linux