tag:blogger.com,1999:blog-44284735640973797252024-03-14T06:42:34.240+05:00Ещё один блог сисадминаmorbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.comBlogger362125tag:blogger.com,1999:blog-4428473564097379725.post-81021665766134995232020-11-29T13:00:00.001+05:002020-11-29T13:00:01.126+05:00Временный запрет на чтение данных из таблиц истории при запуске Zabbix 3.4В статье <a href="https://vladimir-stupin.blogspot.com/2020/11/zabbix-34.html">Предзагрузка кэша значений при запуске сервера Zabbix</a> я уже анонсировал эту функциональность и теперь настало время описать соответствующую заплатку.
<br /><br />
Готовую заплатку с реализацией временного запрета на чтение данных из таблиц истори при запуске сервера Zabbix можно найти по ссылке <a href="http://stupin.su/files/zabbix/zabbix3_4_12_valuecache_grace_period.patch">zabbix3_4_12_valuecache_grace_period.patch</a>.
<br /><br />
В файл конфигурации conf/zabbix_server.conf добавим опцию с названием DBSyncersGracePeriod, которая будет принимать время с момента запуска сервера в секундах, в течение которого чтение данных из хранилищ истории будет запрещено:
<pre style="background-color: lightgrey;">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.</pre>
Теперь добавим в сервер Zabbix загрузку этой опции из файла конфигурации. Отредактируем файл src/zabbix_server/server.c следующим образом:
<pre style="background-color: lightgrey;">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,</pre>
Внесём аналогичные фиктивные изменения в Zabbix-прокси, отредактировав файл src/libs/zabbix_proxy/proxy.c следующим образом:
<pre style="background-color: lightgrey;">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;</pre>
К счастью, в исходном коде уже имеется переменная CONFIG_SERVER_STARTUP_TIME, содержащая отметку времени запуска сервера Zabbix. Осталось только внести условие в функцию zbx_history_get_values в файле src/libs/zbxhistory/history.c:
<pre style="background-color: lightgrey;">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);
</pre>
Я пробовал возвращать ответ SUCCEED, как это сделано в <a href="https://glaber.io/">Glaber</a>, но в таком случае срабатывают триггеры с функцией str. Видимо в таком случае функция str считает, что значение есть, но оно пустое. В итоге функция не находит искомую подстроку и срабатывает триггер. Если же возвращать ответ FAIL, как это сделано в заплатке выше, триггеры и вычисляемые элементы данных в подобных случаях просто переходят в неподдерживаемое состояние.
morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0tag:blogger.com,1999:blog-4428473564097379725.post-25426098957335219062020-11-22T13:00:00.003+05:002020-11-22T13:00:03.324+05:00Предзагрузка кэша значений при запуске сервера Zabbix 3.4Сервер Zabbix при запуске начинает оценивать истинность выражений триггеров, для чего ему нужны значения элементов данных, фигурирующих в выражении. В выражениях триггеров могут фигурировать функции, обращающиеся не только к последним значениям, но и к ряду значений за определённый период времени. Так как при запуске сервера Zabbix его кэш значений ещё пуст, Zabbix начинает выполнять огромное количество мелких запросов к таблицам истории. Классические транзакционные СУБД справляются с подобной нагрузкой относительно легко, хотя и для них она может оказаться тяжёлой при использовании медленной дисковой подсистемы и малом количестве оперативной памяти.
<br /><br />
При хранении исторических данных в таблицах со сжатием, или в таблицах с индексами, оптимизированных для выполнения запросов, извлекающих данные длинными последовательностями, запуск сервера Zabbix становится более тяжёлым. Например, при хранении исторических данных в таблицах на движке TokuDB сервера MySQL со сжатием, запуск сервера Zabbix закономерно порождает высокую нагрузку от СУБД на процессор.
<br /><br />
В случае с ClickHouse ситуация ещё тяжелее: для поиска единичного результата ClickHouse фактически читает с диска, расжимает и просматривает данные колонок всех секций, попадающих под условия запроса. Большой запас свободной оперативной памяти может снизить нагрузку на диски, т.к. горячие данные с файловой системы могут уместиться в буферный кэш ядра операционной системы. В ClickHouse имеется возможность использовать кэш разжатых данных, но по моим наблюдениям в этот кэш сохраняются только те данные, которые были выданы клиенту в качестве результатов запросов. Т.к. у сервера Zabbix есть собственный кэш значений, в который он сохраняет результаты прошлых запросов, то кэш разжатых данных ClickHouse оказывается бесполезен. Поэтому нагрузка на процессор, возникающая из-за необходимости многократно разжимать одни и те же данные, никуда не денется.
<br /><br />
В качестве паллиатива в <a href="https://glaber.io/">Glaber</a> были реализованы два подхода к решению проблемы:
<ol>
<li>Сервер Zabbix, прежде чем приступить к опросу и оценке истинности выражений триггеров, заполняет кэш значений данными из таблиц истории за указанный период времени, не более указанного количества значений для каждого из элементов данных. Таким образом повышается вероятность найти необходимые данные в кэше значений при последующих попытках оценить истинность выражений триггеров.</li>
<li>Временный запрет на чтение данных из таблиц истории, действующий после запуска сервера Zabbix. Пока действует временный запрет, кэш значений наполняется данными, снятыми непосредственно с контролируемого оборудования. По мере наполнения кэша значений этими данными становится возможным оценить выражения триггеров, не обращаясь к таблицам истории. После снятия запрета количество данных, которые необходимо прочитать из таблиц истории, значительно снижается.</li>
</ol>
Оба решения я назвал паллиативными неспроста. У них имеется ряд недостатков, не позволяющих считать их идеальными:
<ol>
<li>На практике некоторые триггеры могут использовать данные, находящиеся далеко за пределами загруженного в кэш значений периода времени. При попытках увеличить период времени загружаемых данных в кэш значений попадёт много данных, которые не будут востребованы и будут просто бесполезно занимать оперативную память. Кроме того, если сервер Zabbix по каким-то причинам долгое время не писал данные в таблицы истории, то при запуске в кэш значений не будет прочитано никаких данных и мы вернёмся к первоначальной ситуации. Наконец, запросы на чтение значений из таблиц хранилища порождают не только триггеры, но и вычисляемые элементы данных. Если у элемента данных, фигурирующего в выражении вычисляемого элемента данных, нет триггера, то значения этого элемента данных не будут загружены при запуске сервера Zabbix и по-прежнему будут извлекаться единичными запросами к хранилищу.</li>
<li>При временном запрете не чтение данных из таблиц истории триггеры и вычисляемые элементы данных, которые не нашли в кэше значений нужных им данных, будут переходить в неподдерживаемое состояние. Их оценка будет восстанавливаться либо при накоплении достаточных данных в кэше значений, либо по истечении периода временного запрета на чтение из таблиц истории. Пока триггер находится в неподдерживаемом состоянии, он заведомо не может сработать и часть имеющихся проблем будет скрыта, пока триггер вновь не стане поддерживаемым.</li>
</ol>
Идею реализации предзагрузки кэша значений из ClickHouse можно понять вот из этой <a href="https://gitlab.com/mikler/glaber/-/commit/4cf9e751c68e9f31090875cf5f0ee1eb07411b86">правки в репозитории Glaber</a>. Правка не полная, некоторые используемые в ней функции попали в другие правки.
<br /><br />
Проблема, во-первых в том, что у меня для хранения исторических данных используются отдельные таблицы и эта правка мне не годится.
<br /><br />
Во-вторых, мне не нравится в этой правке то, что библиотеки zbxdbcache и zbxhistory становятся зависимыми друг от друга. Несмотря на то, что библиотека zbxdbcache должна пользоваться библиотекой zbxhistory, в этой правке добавляетя зависимость библиотеки zbxhistory от zbxdbcache. Чтобы не возникло циклической зависимости импортируемых заголовочных файлов, в библиотеке zbxhistory просто сделано объявление, что необходимая функция находится где-то за пределами файла. Связывание библиотек друг с другом при этом выполняется компоновщиком.
<br /><br />
Я пошёл более сложным путём и реализовал в библиотеке zbxhistory отдельную функцию zbx_history_preload_values, подобную zbx_history_get_values, с той лишь разницей, что она загружает исторические данные не для одного элемента данных, а для списка элементов данных.
<br /><br />
Функция zbx_history_preload_values затем используется в библиотеке zbxdbcache, в функции zbx_vc_preload, которая, в свою очередь, вызывается уже сервером Zabbix.
<br /><br />
Готовую заплатку с реализацией предзагрузки данных в кэш значений из ClickHouse при запуске сервера Zabbix можно найти по ссылке <a href="http://stupin.su/files/zabbix/zabbix3_4_12_valuecache_preloading.patch">zabbix3_4_12_valuecache_preloading.patch</a>. Заплатка с реализацией временного запрета на чтение из истории после запуска сервера Zabbix будет описана в отдельной статье. Ниже я подробно опишу первую заплатку.
<h3>Доработка файла конфигурации</h3>
Для управления предзагрузкой кэша значений введём в файл конфигурации две новые опции: ValueCachePreloadAge и ValueCachePreloadCount. Опция ValueCachePreloadAge будет указывать период времени, значения за который необходимо загрузить в кэш. Опция ValueCachePreloadCount ограничивает количество загружаемых значений для каждого из элементов данных. В качестве декларации наших намерений доработаем пример файла конфигурации conf/zabbix_server.conf:
<pre style="background-color: lightgrey;">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).
#</pre>
<h3>Доработка сервера Zabbix и Zabbix-прокси</h3>
Теперь добавим в сервер Zabbix поддержку чтения этих опций конфигурации в соответствующие переменные:
<pre style="background-color: lightgrey;">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,</pre>
Аналогичные фиктивные изменения внесём в Zabbix-прокси:
<pre style="background-color: lightgrey;">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;</pre>
<h3>Массив загруженных значений</h3>
Для временного хранения значений, загруженных из таблиц истории, но ещё не помещённых в кэш значений, нам понадобится новая структура данных - массив из элементов, содержащих идентификатор элемента данных, тип его значения, отметку времени и само значение. Структура данных, которая будет содержать все необходимые поля одного элемента, будет называться zbx_valuecache_record_t и будет сделана по аналогии со структурой zbx_history_record_t, объявленной в файле include/zbxhistory.h.
<br /><br />
Для работы с массивом, состоящим из таких структур, нужно будет реализовать соответствующие операции. К счастью, в Zabbix уже есть макроопределение ZBX_VECTOR_DECL(<название>, <тип_элемента>), позволяющее создавать функции zbx_vector_<название>_<операция> для работы с массивами необходимых структур данных. Правда, эти функции не умеют очищать и освобождать память, динамически распределённую внутри самих элементов данных, поэтому нужно определить дополнительные функции, которые будут это делать.
<br /><br />
Итак, внесём необходимые правки в файлы src/libs/zbxhistory/history.h и src/libs/zbxhistory/history.c:
<pre style="background-color: lightgrey;">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);
+}</pre>
<h3>Базовая доработка zbxhistory</h3>
Теперь, когда выполнена необходимая подготовка, можно приступить к реализации функции zbx_history_preload_values, которая будет обращаться к реализациям различных типов хранилищ и просить их загрузить значения в указанный массив.
<br /><br />
Добавим функцию zbx_history_preload_values в файл src/libs/zbxhistory/history.c:
<pre style="background-color: lightgrey;">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 *</pre>
Внесём объявление этой функции в заголовочный файл include/zbxhistory.h:
<pre style="background-color: lightgrey;">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);
</pre>
Функция zbx_history_preload_values обращается к реализациям подобной же функции для конкретных типов хранилищ. Нужно предусмотреть в структуре с реализацией типа хранилища zbx_history_iface соответствующее поле preload_values и объявить определение типа с указателем на функцию нужной нам сигнатуры. Обе правки внесём в заголовочный файл src/libs/zbxhistory/history.h:
<pre style="background-color: lightgrey;">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;
};</pre>
Библиотека zbxhistory почти готова, осталось внести правки в конкретные реализации различных типов хранилищ.
<h3>Доработка хранилищ zbxhistory</h3>
Первым делом доработаем хранилища типов SQL и Elasticsearch. Т.к. реализовывать предзагрузку в этих хранилищах я не собираюсь, но поле preload_values структуры zbx_history_iface всё же нужно инициализировать, сделаем это, отредактировав файлы src/libs/zbxhistory/history_sql.c и src/libs/zbxhistory/history_elastic.c следующим образом:
<pre style="background-color: lightgrey;">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)
{</pre>
Теперь займёмся хранилищем ClickHouse. Реализация функции clickhouse_preload_values выполнена по аналогии с функцией clickhouse_get_value. Введена также вспомогательная функция history_parse_valuecache, которая извлекает данные из строки в формате JSON и заполняет структуру типа zbx_valuecache_record_t. Внесённые правки в файле src/libs/zbxhistory/history_clickhouse.c выглядят следующим образом:
<pre style="background-color: lightgrey;">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;</pre>
<h3>Доработка zbxdbcache</h3>
Нам понадобится функция, которая вернёт список идентификаторов элементов данных, к которым привязаны триггеры и для которых поэтому нужно загрузить данные в кэш значений. Для загрузки значений разных типов нам понядобятся разные списки, т.к. значения хранятся в разных таблицах и могут обрабатываться разными реализациями хранилищ. Добавим в файл src/libs/zbxdbcache/dbconfig.c функцию с именем DCconfig_get_itemids_by_valuetype, которая будет возвращать идентификаторы элементов данных с указанным типом значения:
<pre style="background-color: lightgrey;">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 *</pre>
Эта функция будет нужна в файле src/libs/zbxdbcache/valuecache.c, поэтому для её использования там её нужно объявить в заголовочном файле include/dbcache.h:
<pre style="background-color: lightgrey;">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,</pre>
Добавим в файл 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, добавляющей в кэш значений одно значение.
<pre style="background-color: lightgrey;">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 *
* *</pre>
Теперь осталось реализовать функцию 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 значениями из файла конфигурации.
<pre style="background-color: lightgrey;">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 *</pre>
Добавим объявление этой функции в файл src/libs/zbxdbcache/valuecache.h, чтобы её можно было вызвать в процессе запуска сервера Zabbix:
<pre style="background-color: lightgrey;">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);</pre>
<h3>Доработка сервера Zabbix</h3>
Сделаем финальный штрих и добавим вызов функции zbx_vc_preload в файл src/zabbix_server/server.c после инициализации кэша значений, после загрузки конфигурации из базы данных, но перед тем, как главный процесс сервера Zabbix запустит подчинённые процессы:
<pre style="background-color: lightgrey;">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;</pre>
<h3>Выбор значений для опций конфигурации</h3>
Для оценки среднего количества значений, используемого в триггерах, можно воспользоваться таким запросом:
<pre style="background-color: lightgrey;">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');</pre>
Поскольку это среднее значение, то в опцию ValueCachePreloadCount можно вписать удвоенное значение из результата запроса.
<br /><br />
Для оценки среднего временного интервала, используемого в триггерах, можно воспользоваться таким запросом:
<pre style="background-color: lightgrey;">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');</pre>
Аналогично, поскольку это среднее значение, то в опцию ValueCachePreloadAge можно вписать удвоенное значение из результата запроса.
<br /><br />
Если вам захочется, чтобы в кэш гарантированно попадали данные для расчёта всех триггеров, тогда можно заменить в запросах функцию AVG на MAX и подставить в файл конфигурации полученные значения. Если вам хватит оперативной памяти для такого большого кэша значений, то вам повезло :)
morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com1tag:blogger.com,1999:blog-4428473564097379725.post-67210296438009344452020-11-15T13:00:00.005+05:002020-11-15T13:00:06.330+05:00Поддержка хранилища ClickHouse в сервере Zabbix 3.4Продолжаем доработку сервера Zabbix. В прошлый раз мы <a href="https://vladimir-stupin.blogspot.com/2020/11/zabbix-34.html">добавили в библиотеку zbxhistory поддержку возможности использования хранилищ разного типа</a>. На этот раз нужно добавить поддержку хранилища нового типа, которую будем реализовывать на базе поддержки хранилища Elasticsearch, частично заимствуя фрагменты из кода поддержки SQL-хранилищ.
<br /><br />
Готовую заплатку для сервера Zabbix с реализацией поддержки хранения исторических данных в ClickHouse можно найти по ссылке <a href="http://stupin.su/files/zabbix/zabbix3_4_12_server_clickhouse.patch">zabbix3_4_12_server_storage_per_table.patch</a>.
<br /><br />
Ниже описаны внесённые заплаткой доработки и объяснение их логики.
<br /><br />
У меня ушло некоторое время на изучение функций для работы со структурами данных в формате JSON. Чтобы не пришлось вспоминать их снова, опишу те из них, которыми я пользовался непосредственно в описываемой заплатке.
<h3>Функции Zabbix для формирования JSON</h3>
Заголовочный файл include/zbxjson.h, файл с реализацией функций - src/libs/zbxjson/json.c
<h4>zbx_json_init(json)</h4>
Создание JSON, в котором корневым элементом является словарь. Фактически, в пустой буфер будут добавлены фигурные скобки <span style="background-color: lightgrey;">{}</span>, текущий указатель будет указывать на закрывающую скобку, уровень вложенности увеличится с 0 до 1, а в статусе будет ZBX_JSON_EMPTY.
<h4>zbx_json_initarray(json)</h4>
Работает аналогично zbx_json_init, но корневым элементом структуры JSON будет массив, а в буфер вместо фигурных скобок <span style="background-color: lightgrey;">{}</span> будут вставлены квадратные скобки <span style="background-color: lightgrey;">[]</span>.
<h4>zbx_json_clean(json)</h4>
Очищает буфер от хранящейся в нём структуры JSON. Сама память при этом не освобождается.
<h4>zbx_json_free(json)</h4>
Освобождает буфер, который был занят сформированной структурой JSON.
<h4>zbx_json_addobject(json, name)</h4>
Вставляет в то место буфера, куда указывает текущий указатель:
<ol>
<li>запятую <span style="background-color: lightgrey;">,</span>, если текущий статус равен ZBX_JSON_COMMA,</li>
<li>текст <span style="background-color: lightgrey;">"name":</span>, если аргумент name не равен NULL,</li>
<li>пару фигурных скобок <span style="background-color: lightgrey;">{}</span>.</li>
</ol>
Текущий указатель передвигается на вставленную закрывающую фигурную скобку, уровень вложенности увеличивается на единицу, в статус записывается ZBX_JSON_EMPTY.
<br /><br />
Весь текст, который ранее лежал в буфере после указателя, сдвигается так, что оказывается позади вставленного текста.
<h4>zbx_json_addarray(json, name)</h4>
Работает аналогично zbx_json_addobject, но вместо фигурных скобок вставляются квадратные <span style="background-color: lightgrey;">[]</span>.
<br /><br />
Вставляет в то место буфера, куда указывает текущий указатель:
<ol>
<li>запятую <span style="background-color: lightgrey;">,</span>, если текущий статус равен ZBX_JSON_COMMA,</li>
<li>текст <span style="background-color: lightgrey;">"name":</span>, если аргумент name не равен NULL,</li>
<li>пару квадратных скобок <span style="background-color: lightgrey;">[]</span>.</li>
</ol>
Текущий указатель передвигается на вставленную закрывающую квадратную скобку, уровень вложенности увеличивается на единицу, в статус записывается ZBX_JSON_EMPTY.
<br /><br />
Весь текст, который ранее лежал в буфере после указателя, сдвигается так, что оказывается позади вставленного текста.
<h4>zbx_json_addstring(json, name, string, type)</h4>
Вставляет в то место буфера, куда указывает текущий указатель:
<ol>
<li>запятую <span style="background-color: lightgrey;">,</span>, если текущий статус равен ZBX_JSON_COMMA,</li>
<li>текст <span style="background-color: lightgrey;">"name":</span>, если аргумент name не равен NULL,</li>
<li>строку <span style="background-color: lightgrey;">string</span>.</li>
</ol>
Если тип строки type равен ZBX_JSON_TYPE_STRING, то строка string заключается в двойные кавычки. Если string равен NULL, то добавляется строка <span style="background-color: lightgrey;">null</span> без кавычек.
<br /><br />
Текущий указатель передвигается на символ, следующий за последним вставленным, уровень вложенности не меняется, в статус записывается ZBX_JSON_COMMA.
<br /><br />
Весь текст, который ранее лежал в буфере после указателя, сдвигается так, что оказывается позади вставленного текста.
<h4>zbx_json_adduint64(json, name, value)</h4>
Вставляет в то место буфера, куда указывает текущий указатель:
<ol>
<li>запятую <span style="background-color: lightgrey;">,</span>, если текущий статус равен ZBX_JSON_COMMA,</li>
<li>текст <span style="background-color: lightgrey;">"name":</span>, если аргумент name не равен NULL,</li>
<li>64-битное беззнаковое число value.</li>
</ol>
Текущий указатель передвигается на символ, следующий за последним вставленным, уровень вложенности не меняется, в статус записывается ZBX_JSON_COMMA.
<br /><br />
Весь текст, который ранее лежал в буфере после указателя, сдвигается так, что оказывается позади вставленного текста.
<h4>zbx_json_addint64(json, name, value)</h4>
Вставляет в то место буфера, куда указывает текущий указатель:
<ol>
<li>запятую <span style="background-color: lightgrey;">,</span>, если текущий статус равен ZBX_JSON_COMMA,</li>
<li>текст <span style="background-color: lightgrey;">"name":</span>, если аргумент name не равен NULL,</li>
<li>64-битное число value со знаком.</li>
</ol>
Текущий указатель передвигается на символ, следующий за последним вставленным, уровень вложенности не меняется, в статус записывается ZBX_JSON_COMMA.
<br /><br />
Весь текст, который ранее лежал в буфере после указателя, сдвигается так, что оказывается позади вставленного текста.
<h4>zbx_json_close(json)</h4>
Передвигает текущий указатель так, что пропускается закрывающая фигурная <span style="background-color: lightgrey;">}</span> или квадратная скобка <span style="background-color: lightgrey;">]</span>.
<br /><br />
Уровень вложенности уменьшается на единицу, статус меняется на ZBX_JSON_COMMA.
<h3>Функции Zabbix для разбора JSON</h3>
<h4>zbx_json_open(buffer, json)</h4>
buffer - строка с завершающим нулём, содержащая текст JSON.
<br /><br />
json - структура с указателями на начало и конец фрагмента JSON в буфере buffer.
<br /><br />
Проверяет, что текст в буфере buffer является правильным JSON, инициализирует структуру json. При ошибках возвращает FAIL, в случае успеха - SUCCEED.
<h4>zbx_json_next(json, p)</h4>
json - структура с указателями на начало и конец фрагмента JSON, являющегося массивом.
<br /><br />
p - указатель внутри фрагмента json, указывающий на начальный символ элемента массива или словаря.
<br /><br />
Ищет следующий элемент массива или словаря и возвращает указатель на его начальный символ. Если указанный в p элемент был последним, возвращает NULL.
<h4>zbx_json_pair_by_name(json, name)</h4>
json - структура с указателями на начало и конец фрагмента JSON, являющегося словарём.
<br /><br />
name - имя ключа в словаре, значение которого нужно найти.
<br /><br />
Возвращает указатель на первый символ значения или NULL, если указанного ключа нет в словаре.
<h4>zbx_json_brackets_open(p, json)</h4>
p - указатель на открывающую скобку, указывающую на начало фрагмента JSON, который нужно найти.
<br /><br />
json - структура с указателями на начало и конец фрагмента JSON, являющегося массивом или словарём.
<br /><br />
Ищет в указанном фрагменте JSON открывающую скобку, затем находит парную ей закрывающую скобку и записывает указатели на начало и конец найденного фрагмента в структуру json.
<br /><br />
При ошибках возвращает FAIL, при успешном завершении - SUCCEED.
<h4>zbx_json_brackets_by_name(json, name, json_out)</h4>
json - структура с указателями на начало и конец фрагмента JSON, являющегося словарём.
<br /><br />
name - имя ключа в словаре, значение которого нужно найти.
<br /><br />
json_out - фрагмент JSON, являющийся значением ключа name.
<br /><br />
Ищет в указанном фрагменте JSON указанный ключ, находит открывающую и закрывающую скобки, в структуру json_out записывает указатели на начало и конец найденного фрагмента.
<br /><br />
При ошибках возвращает FAIL, при успешном завершении - SUCCEED.
<h4>zbx_json_value_by_name_dyn(json, name, string, string_alloc)</h4>
json - структура с указателями на начало и конец фрагмента JSON, являющегося словарём.
<br /><br />
name - имя ключа в словаре, значение которого нужно найти.
<br /><br />
string - указатель на указатель на буфер, в который будет помещено найденное значение.
<br /><br />
string_alloc - указатель на переменную с размером буфера.
<br /><br />
Ищет в указанном фрагменте JSON, являющемся словарём, значение элемента с указанным ключом name. Найденное значение записывается в буфер. Если в буфере не было достаточно места для сохранения найденного значения, функция выделяет под буфер другой фрагмент памяти, обновляет указатель на буфер и его размер.
<h3>Новая функция zbx_json_adddbl</h3>
Немного опережая события, заранее добавим в код Zabbix дополнительную функцию zbx_json_adddbl, которая позже понадобится нам для формирования JSON с данными, вставляемыми в таблицу history с числами с плавающей запятой. Объявление функции добавим в файл include/zbxjson.h, а реализацию функции добавим в файл src/libs/zbxjson/json.c следующим образом:
<pre style="background-color: lightgrey;">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)</pre>
<h3>Доработка основы библиотеки zbxhistory</h3>
В файле src/libs/zbxhistory/history.c раскомментируем ранее добавленный нами комментарий с намёком на поддержку ClickHouse:
<pre style="background-color: lightgrey;">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);</pre>
В добавленном коде используется указатель на строку clickhouse_url, добавим его объявление:
<pre style="background-color: lightgrey;">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,</pre>
Объявление функции zbx_history_clickhouse_init нужно добавить в заголовочный файл src/libs/zbxhistory/history.h:
<pre style="background-color: lightgrey;">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</pre>
<h3>Добавление файла history_clickhouse.c</h3>
Перед дальнейшими действиями скопируем файл src/libs/zbxhistory/history_elastic.c в файл src/libs/zbxhistory/history_clickhouse.c и заменим все упоминания Elasticsearch на ClickHouse, в том числе в отладочных сообщениях, комментариях и именах функций.
<br /><br />
Теперь нужно прописать новый файл history_clickhouse.c в Make-файлы src/libs/zbxhistory/Makefile.am и src/libs/zbxhistory/Makefile.in, чтобы они участвовали в процессе сборки:
<pre style="background-color: lightgrey;">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@
</pre>
Получилось два полностью аналогичных по сути модуля поддержки хранилищ с разными именами. Продолжим переделку нового модуля. Сначала пройдусь кратко по мелким изменениям.
<br /><br />
В функции clickhouse_writer_flush была удалена обработка сообщений об ошибках Elasticsearch при успешном коде ответа HTTP, т.к. ClickHouse о любых ошибках выполнения запросов всегда сообщает соответствующим кодом статуса HTTP:
<pre style="background-color: lightgrey;">@@ -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;</pre>
Было удалено макроопределение константы ZBX_IDX_JSON_ALLOCATE, т.к. в коде поддержки ClickHouse оно не использовалось.
<br /><br />
В структуре zbx_clickhouse_data_t было удалено поле post_url, т.к. оказалось достаточно уже имеющегося в структуре поля base_url.
<br /><br />
Были удалены функции history_value2str и clickhouse_is_error_present (бывшая elastic_is_error_present), т.к. они больше не используются.
<br /><br />
Что касается доработок по существу, то они затрагивают функции clickhouse_get_values и clickhouse_add_values. Приведу обе функции полностью в окончательном виде:
<pre style="background-color: lightgrey;">/************************************************************************************
* *
* 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;
}</pre>
При доработке функции clickhouse_get_values массив строковых констант value_type_str был заменён на массив строковых констант value_type_table:
<pre style="background-color: lightgrey;">-const char *value_type_str[] = {"dbl", "str", "log", "uint", "text"};
+const char *value_type_table[] = {"history", "history_str", "history_log", "history_uint", "history_text"};</pre>
Из всех сделанных изменений отдельно остановлюсь на исправлении одной из ошибок, которая перекочевала в файл history_clickhouse.c из файла history_elastic.c. Не могу скзать, является ли это ошибкой в исходном файле, но в коде поддержки ClickHouse эта проблема проявлялась следующим образом: в журнале сервера Zabbix при попытках вставки новых данных в таблицы истории в файле /var/log/zabbix/zabbix_server.log появлялись ошибки "400 Bad Request", хотя на первый взгляд данные в таблицы всё-таки записывались.
<br /><br />
Оказалось, что часть запросов к ClickHouse были попросту пустыми POST-запросами. Более пристальное изучение причин проблемы позволило обнаружить ошибку: при формировании запроса к ClickHouse на вставку данных иногда, при попытке добавить в него очередное значение, данные в буфере попросту очищались. Получившийся пустой запрос и выполнялся, из-за чего ClickHouse периодически сообщал об ошибках, а графики в веб-интерфейсе прерывались.
<br /><br />
В функции добавки значений использовался уже распределённый ранее буфер hist->data->buf, но при каждом вызове этой функции считалось, что его размер buf_alloc равен нулю. Вот как это выглядит в исходном модуле history_elastic.c:
<pre style="background-color: lightgrey;">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;</pre>
Чтобы устранить ошибку, я решил вынести переменную с размером буфера из тела функции в структуру, содержащую указатель на буфер:
<pre style="background-color: lightgrey;"> typedef struct
{
char *base_url;
char *buf;
+ size_t buf_alloc;
CURL *handle;
}
zbx_clickhouse_data_t;</pre>
После этой доработки сервер Zabbix, наконец, начал исправно писать данные в ClickHouse.morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com1tag:blogger.com,1999:blog-4428473564097379725.post-53193653984965073112020-11-08T13:00:00.003+05:002020-11-08T13:00:01.924+05:00Поддержка нескольких типов хранилищ в сервере Zabbix 3.4В <a href="https://vladimir-stupin.blogspot.com/2020/11/zabbix-34-clickhouse.html">доработанном веб-интерфейсе Zabbix</a> реализована возможность индивидуальной настройких типа хранилища для каждой из таблиц истории. Теперь необходимо реализовать возможность настройки хранилища для каждой из таблиц на стороне сервера Zabbix. Я решил прибегнуть к следующей схеме: в файле конфигурации я добавил по одной опции для каждой из таблиц истории, в каждой опции указывается через запятую тип хранилища и URL, по которому оно доступно. Если для опции не указано значение, то используется хранилище по умолчанию - база данных SQL.
<br /><br />
В файле конфигурации сервера Zabbix это может выглядеть следующим образом:
<pre style="background-color: lightgrey;">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</pre>
Готовую заплатку с реализацией раздельного выбора типа хранилища для каждой из таблиц истории можно найти по ссылке <a href="http://stupin.su/files/zabbix/zabbix3_4_12_server_storage_per_table.patch">zabbix3_4_12_server_storage_per_table.patch</a>.
<br /><br />
Ниже в логическом порядке описываются изменения, вносимые этой заплаткой.
<br /><br />
Кстати, заплатки описываются так подробно по двум обстоятельствам:
<ul>
<li>Несмотря на то, что Zabbix - это программное обеспечение со свободным исходным кодом, разработку этого программного обеспечения ведёт коммерческая организация. Эта коммерческая компания зарабатывает деньги на поддержке своего продукта и у неё нет желания заниматься поддержкой дополнительных спорных функций, внесённых в код сторонними разработчиками. Представьте, что какой-то Вася реализовал в Zabbix поддержку хранения исторических данных в том же Clickhouse. Клиент, оплативший коммерческую поддержку Zabbix, устанавливает себе Zabbix и пытается воспользоваться ClickHouse в качестве хранилища исторических данных. Даже если с кодом Васи нет никаких проблем и он аккуратно написан, у клиента может возникнуть множество самых разных проблем, связанных с эксплуатацией непосредственно самого ClickHouse. В компании нет специалистов, знакомых с Clickhouse, поэтому компания будет вынуждена отказать клиенту в поддержке. Возникнет вопрос - за что же тогда платит клиент, если компания не осуществляет поддержку функций, реализованных в её продукте? Вот поэтому разработчики Zabbix обычно не принимают в исходный код своего продукта никаких заплаток, вносящих в код продукта глобальные изменения. Zabbix, являясь программным обеспечением со свободным исходным кодом, фактически не принадлежит сообществу, а принадлежит коммерческой компании.</li>
<li>Из заплатки самой по себе не так легко понять логику вносимых ей изменений. Если понадобится адаптировать заплатку к другой версии Zabbix, то нужно будет разбираться в имеющейся заплатке и исходном коде той версии Zabbix, для которой эта заплатка была сделана, а потом повторить все эти изменения в другой версии Zabbix. Описание, подобное приведённому ниже, должно помочь во-первых, понять логику изменений, вносимых заплаткой, а во-вторых, помочь внести подобные изменения мелкими кусочками в другую версию Zabbix.</li>
</ul>
<h3>Доработка файла конфигурации</h3>
Задекларируем наши намерения, доработав соответствующим образом пример файла конфигурации conf/zabbix_server.conf:
<pre style="background-color: lightgrey;">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 ################</pre>
<h3>Доработка сервера Zabbix и Zabbix-прокси</h3>
Теперь удалим поддержку чтения опций конфигурации HistoryStorageUrl и HistoryStorageOpts из исходного текста сервера Zabbix в файле src/zabbix_server/server.c и добавим в него поддержку чтения новых опций конфигурации HistoryStorage, HistoryUintStorage, HistoryStrStorage, HistoryTextStorage, HistoryLogStorage:
<pre style="background-color: lightgrey;">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}
};</pre>
Поскольку Zabbix-прокси реализован на основе сервера Zabbix, аналогичные фиктивные изменения нужно внести в исходный код Zabbix-прокси в файле src/zabbix_proxy/proxy.c:
<pre style="background-color: lightgrey;">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);
</pre>
<h3>Доработка основы библиотеки zbxhistory</h3>
Суть вносимых изменений особенно наглядно можно увидеть в следующей заплатке для файла src/libs/zbxhistory/history.c. Вместо использования фиксированного URL для всех таблиц из переменной CONFIG_HISTORY_STORAGE_URL и переменной CONFIG_HISTORY_STORAGE_OPTS, которая предписывает использовать этот URL для таблиц указанных в ней типов, мы определяем тип хранилища для каждой из таблиц и вызываем функцию инициализации соответствующего модуля, передавая ей URL для доступа к этой конкретной таблице:
<pre style="background-color: lightgrey;">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;</pre>
В доработанном коде функции zbx_history_init добавлен закомментированный участок, обозначающий будущую поддержку хранилища ClickHouse.
<br /><br />
Из кода удалён комментарий о необходимости реализовать раздельную возможность настройки типов хранилищ для каждой из таблиц, потому что именно это мы сейчас и делаем.
<br /><br />
В коде используется новая функция zbx_startswith для проверки, что строка начинается с указанного префикса.
<br /><br />
При доработке изменилась сигнатура функции zbx_history_elastic_init, теперь ей передаётся дополнительный аргумент - elastic_url.
<br /><br />
Остановимся поподробнее на двух последних обстоятельствах.
<h3>Новая функция zbx_startswith</h3>
Во-первых, добавим объявление новой функции zbx_startswith в файл include/common.h и её реализацию в файл src/libs/zbxcommon/str.c:
<pre style="background-color: lightgrey;">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)</pre>
<h3>Доработка поддержки Elasticsearch</h3>
Во-вторых, изменим объявление функции zbx_history_elastic_init в заголовочном файле src/libs/zbxhistory/history.h:
<pre style="background-color: lightgrey;">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</pre>
Осталось лишь соответствующим образом изменить реализацию этой функции в файле src/libs/zbxhistory/history_elastic.c:
<pre style="background-color: lightgrey;">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;</pre>morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com2tag:blogger.com,1999:blog-4428473564097379725.post-31890835957271884812020-11-01T13:00:00.002+05:002020-11-01T13:00:00.641+05:00Доработка веб-интерфейса Zabbix 3.4 для работы с ClickHouseТеперь, когда у нас имеется <a href="https://vladimir-stupin.blogspot.com/2020/10/clickhouse-zabbix.html">настроенный сервер Clickhouse с заготовленными в нём таблицами истории и тенденций Zabbix</a>, можно попробовать доработать веб-интерфейс Zabbix для работы с Clickhouse. Реализацию поддержки Clickhouse будем делать на базе уже имеющейся поддержки хранилищ Elasticsearch и SQL. Поскольку ClickHouse использует SQL-образный синтаксис запросов, но может возвращать ответ в виде JSON по протоколу HTTP, то нам пригодятся фрагменты кода реализации поддержки как того, так и другого типа хранилища. Разработка и отладка заплатки выполнялась на данных, скопированных в Clickhouse из хранилища SQL при помощи описанного ранее скрипта <a href="http://stupin.su/files/zabbix/copy_data.py">copy_data.py</a>.
<br /><br />
Получившуюся заплатку с неописанными здесь мелкими изменениями в комментариях к другим функциям можно взять по ссылке <a href="http://stupin.su/files/zabbix/zabbix3_4_12_frontend_clickhouse.patch">zabbix3_4_12_frontend_clickhouse.patch</a>.
<br />
Описанная здесь реализация поддержки ClickHouse отличается от реализации из <a href="https://glaber.io/">Glaber</a>, следующим:
<ul>
<li>вместо модуля curl для php для обращения к API ClickHouse используется штатная функция file_get_contents,</li>
<li>вместо типа DateTime для колонок clock используется тип UInt32, что приближает поддержку ClickHouse к родной структуре таблиц Zabbix,</li>
<li>реализована поддержка работы с таблицами истории журнального типа - history_log,</li>
<li>реализовано раздельное хранение исторических данных в таблицах history, history_uint, history_str, history_text, history_log, что приближает поддержку ClickHouse к родной структуре таблиц Zabbix,</li>
<li>веб-интерфейс поддерживает индивидуальный выбор типа хранилища для каждой из таблиц истории: SQL, Elasticsearch или ClickHouse,</li>
<li>при отображении графиков используются данные из таблиц trends и trends_uint, которые должны быть доступны по URL таблиц history и history_uint соответственно,</li>
<li>реализованы оптимизации запросов на страницах последних данных и графиков нескольких элементов данных: вместо отдельных запросов по каждому элементу данных выполняется от одного до 5 запросов по каждому из типов элементов данных. Внутри каждого запроса данные подзапросов объединяются при помощи выражения UNION ALL.</li>
</ul>
Я старался скрупулёзно воспроизводить стиль исходного кода, однако не всегда этот код мне кажется идеальным. В частности, в коде присутствуют микрооптимизации, кэширующие соответствие типов значений элементов данных URL'ам хранилища. Эти микрооптимизации на фоне обращений к самим хранилищам экономят настолько мизерное количество ресурсов, что лучше было бы обойтись вообще без них - код бы от этого стал только нагляднее. Не совсем понятно, почему в некоторых не самых тяжёлых функциях реализовано объединение запросов при помощи UNION ALL, а в наиболее тяжёлых - не реализовано. Наконец, сама поддержка различных хранилищ не выполнена в стиле ООП: нет базового класса хранилища и нет отдельных реализаций хранилищ в виде классов, отнаследованных от базового класса. Вместо этого поддержка разных типов хранилищ реализована прямо в коде классов CHistoryManager, CHistory, CTrend, из которых два последних используют часть методов из первого.
<br /><br />
У веб-интерфейса есть интересная особенность. На странице просмотра графика могут использоваться данные из таблицы тенденций, даже если данные есть в таблице истории. На выбор таблицы-источника данных влияет длительность хранения исторических данных, указанная в свойствах самого элемента данных. Также если в настройках на странице «Администрирование» - «Общие», в разделе «Очистка истории» отмечена галочка «Переопределить период хранения истории элементов данных», то используется значение, указанное в поле «Период хранения данных». В моём случае в этом поле было указано значение 60d, а в таблице истории имелись данные за 365 дней, включая те данные, которые были сгенерированы по таблицам тенденций. Когда я поменял значение в этом поле на 365d, на графиках стали отображаться только данные из таблиц истории.
<br /><br />
Ниже описаны внесённые заплаткой доработки веб-интерфейса и их обоснование.
<h3>Файл конфигурации</h3>
Первым делом поправим пример файла конфигурации frontends/php/conf/zabbix.conf.php.example. В нём можно увидеть, что в переменной конфигурации $HISTORY['types'] можно указать список таблиц, для которых будет использоваться Elasticsearch. Делается это следующим обрзом:
<pre style="background-color: lightgrey;">$HISTORY['types'] = ['uint', 'text'];</pre>
Поскольку нам нужно достичь возможности использовать одно из трёх разных хранилищ, я решил изменить формат этой переменной так, чтобы для каждой из таблиц можно было указывать тип её хранилища:
<pre style="background-color: lightgrey;">$HISTORY['type'] = [
'uint' => 'clickhouse',
'text' => 'elastic'
];</pre>
При этом в переменной $HISTORY['url'], как и раньше, будет указывается URL хранилища. Только теперь это может быть как URL хранилища Elasticsearch, так и URL хранилища Clickhouse.
<br /><br />
Отобразим логику изменений файла конфигурации в примере этого файла следующим образом:
<pre style="background-color: lightgrey;">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'
+];</pre>
<h3>Вспомогательный класс для работы с Clickhouse</h3>
В отличие от <a href="https://glaber.io/">Glaber</a>, в моей доработке веб-интерфейса Zabbix для обращения к Clickhouse не используется модуль php-curl, а используется встроенная в PHP функция file_get_contents, что позволяет обойтись прежними зависимостями при установке веб-интерфейса.
<br /><br />
Для выполнения запросов к Clickhouse добавим файл frontends/php/include/classes/helpers/CClickHouseHelper.php со вспомогательным классом CClickHouseHelper:
<pre style="background-color: lightgrey;"><?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;
}
}</pre>
В отличие от <a href="https://glaber.io/">Glaber</a>, этот класс выполняет запросы в формате JSON, а не TSV. В классе есть функция request, которая выполняет переданные ей запросы и возвращает в ответ данные, извлечённые из JSON. Эта функция вызывается только из функций values и value.
<br /><br />
Функция values позволяет выполнить SQL-запрос на получение множества строк данных. Clickhouse вместе с данными ответа также возвращает имена колонок. Если не указывать аргументы columns и map, то при формировании результата будут использоваться имена колонок, которые вернул Clickhouse. Результат будет представлять собой список словарей: каждая строчка списка будет соответствовать одной строке из результата выполнения запроса, а каждый словарь в строке будет в ключе содержать имя колонки, а в значении ключа - значение этой колонки.
<br /><br />
Если указать функции values аргумент columns, то вместо возвращённых сервером Clickhouse имён колонок будут использоваться указанные, в порядке их указания в списке columns.
<br /><br />
Если указать функции values аргумент maps, являющийся словарём, то вместо возвращённых сервером Clickhouse имён колонок будут возвращаться значения из словаря. maps должен быть словарём, в котором ключами являются имена колонок, возвращённых сервером Clickhouse, а их значениями - желаемые имена колонок.
<br /><br />
Функция value возвращает одно значение, возвращённое запросом, или значение null, если запрос ничего не вернул. Если запрос вернёт несколько строк, то будет возвращено значение из первой строки. Если аргумент column не указан, то возвращено будет значение из колонки value.
<br /><br />
В тексте функции values имеются закомментированные строчки, которые могут помочь при отладке запросов к Clickhouse. Раскомментировав их, можно, по желанию, вести журнал запросов и результатов выполнения этих запросов.
<h3>Новый тип хранилища</h3>
Теперь добавим новое определение источника данных в файл frontends/php/include/defines.inc.php:
<pre style="background-color: lightgrey;">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');</pre>
<h3>Доработка класса CHistoryManager</h3>
В файле frontends/php/include/classes/api/managers/CHistoryManager.php определён класс CHistoryManager, который отвечает за работу с таблицами истории непосредственно самого веб-интерфейса. Потребуется доработать функции getLastValues, getValueAt, getGraphAggregation, getAggregatedValue и getMinClock. Начнём, однако, не с этого, а с введения новых вспомогательных фукнций.
<h4>Новая функция getClickHouseEndpoints</h4>
Вместо функции getElasticsearchUrl введём аналогичную по смыслу фукнцию getClickHouseEndpoints, которая будет использовать вспомогательную функцию getClickhouseUrl:
<pre style="background-color: lightgrey;">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 = [];</pre>
Функция getClickhouseEndpoints возвращает URL для доступа к таблицам истории указанных типов значений.
<h4>Доработка функции getLastValues</h4>
Функция getLastValues последовательно обращается к хранилищам каждого типа и запрашивает у него последние значения тех элементов данных, которые хранятся в соответствующем хранилище. Результаты запросов складываются в общую копилку и возвращаются в качестве результата. Добавим в функцию поддержку хранилища ClickHouse:
<pre style="background-color: lightgrey;">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</pre>
Теперь нужно реализовать функцию getLastValuesFromClickhouse. Я реализовал два варианта функции. Первый просто последовательно запрашивает последнее значение каждого из указанных в запросе элементов данных и объединяет результаты запросов, как это сделано в функции getLastValuesFromElasticsearch. Второй вариант из запрашиваемых значений формирует группы по их типам. Для каждого типа значений формируется единый запрос, объединяющий результаты отдельных запросов при помощи выражения UNION ALL. Таким образом можно увеличить отзывчивость веб-интерфейса, сократив количество HTTP-запросов к серверу Clickhouse. Первый вариант фигурирует в коде под именем _getLastValuesFromClickHouse, а второй - более эффективный - под именем getLastValuesFromClickHouse:
<pre style="background-color: lightgrey;">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</pre>
Можно пойти дальше и написать вариант функции, который использует специфический тип запросов, поддерживаемый ClickHouse: LIMIT 1 BY itemid. В таком случае можно будет упростить запрос и обойтись без выражений UNION ALL.
<h4>Доработка функции getValueAt</h4>
Аналогичным образом доработаем функцию getValueAt, которая ищет значение элемента данных, соответствующее указанной отметки времени:
<pre style="background-color: lightgrey;">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</pre>
Оптимизированной версии фукнции тут нет, потому что функция выполняет только один запрос, извлекающий единственное значение.
<h4>Доработка функции getGraphAggregation</h4>
Функция getGraphAggregation возвращает агрегированные данные одного или нескольких элементов данных для отрисовки графика, доступного по ссылкам на странице просмотра последних данных. Поскольку на одном графике могут отображаться кривые нескольких элементов данных, то данные для каждой из кривых можно получать либо отдельными запросами, либо сгруппированными запросами с выражениями UNION ALL. Первый вариант функции фигурирует ниже под именем _getGraphAggregationFromClickHouse, а второй, оптимизированный вариант функции можно найти по имени getGraphAggregationFromClickHouse:
<pre style="background-color: lightgrey;">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</pre>
<h4>Доработка функции getAggregatedValue</h4>
Функция getAggregatedValue, как следует из её названия, возвращает агрегированное значение элемента данных. Функция агрегации указывается в аргументе aggregation (значением может быть строка «MAX», «MIN», «AVG», «COUNT», «SUM»), интересующий элемент данных - в аргументе item, а начальная отметка времени, начиная с которого нужно вернуть агрегированное значение, указывается в аргументе time_from. Из аргумента item на самом деле используется только идентификатор элемента данных, доступный по ключу itemid. По понятным причинам, оптимизированной версии функции getAggregatedValueFromClickHouse нет - здесь происходит запрос только по одному элементу данных:
<pre style="background-color: lightgrey;">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</pre>
<h4>Доработка функции getMinClock</h4>
Функция getMinClock принимает список элементов данных в аргументе items, для которых нужно найти наименьшую отметку времени. Насколько я понимаю, эта функция используется при попытке открыть график в последних данных за всё время. Здесь выполняется один запрос с выражениями UNION ALL для объединения результатов запросов ко всем таблицам в ClickHouse.
Интересно, что выражение UNION ALL используется и в функции getMinClockFromSql. Собственно, после того, как я наткнулся на эту функцию, мне и пришла в голову идея оптимизировать остальные функции, уменьшив количество запросов к ClickHouse при помощи выражения UNION ALL.
<pre style="background-color: lightgrey;">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</pre>
<h3>Доработка класса CHistory</h3>
В файле frontends/php/include/classes/api/services/CHistory.php определён класс CHistory, который отвечает за работу метода API history.get. Метод позволяет получать значения указанных элементов данных за указанный период. По сути в этом классе есть только одна публичная функция get и по одной приватной функции с реализацией каждого из типов хранилищ. Доработаем саму функцию get и добавим функцию getFromClickHouse с реализацией доступа к хранилищу ClickHouse:
<pre style="background-color: lightgrey;">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</pre>
<h3>Доработка класса CTrend</h3>
Почти всё, сказанное про класс CHistory, справедливо и для класса CTrend. В файле frontends/php/include/classes/api/services/CTrend.php определён класс CTrend, который отвечает за работу метода API trend.get. Метод позволяет получать из таблиц тенденций агрегированные почасовые значения указанных элементов данных за указанный период. В этом классе есть только одна публичная функция get и по одной приватной функции с реализацией каждого из типов хранилищ.
<br /><br />
Поскольку в реализации поддержки Elasticsearch нет поддержки таблиц тенденций, а поддержка ClickHouse была сделана на базе поддержки Elasticsearch, то поддержки таблиц тенденций не должно быть и в реализации ClickHouse. Собственно, поэтому в файле конфигурации не предусмотрена возможность указать тип используемого хранилища для таблиц тенденций. Я же реализовал поддержку таблиц тенденций в неявном предположении, что таблица тенденций находится в том же хранилище ClickHouse, что и основная таблица с историческими данными. То есть, если для таблицы history используется хранилище ClickHouse, то неявно предполагается, что по той же ссылке должна быть доступна и таблица trends. И если в ClickHouse хранится таблица history_uint, то по той же ссылке должна быть доступна таблица trends_uint.
<br /><br />
Интересно, что раньше в Zabbix не было методов API для доступа к таблицам тенденций, но в сети гуляли заплатки с реализацией метода trend.get, выполненные по аналогии с методом history.get. Когда же разработчики Zabbix решили добавить метод API для доступа к таблицам тенденций, то реализовали метод trend.get несколько иначе. В частности, методу trend.get не нужно указывать тип запрашиваемых значений элементов данных, метод ищет данные во всех таблицах и возвращает результат поиска в обеих таблицах тенденций.
<br /><br />
Итак, доработаем саму функцию get и добавим функцию getFromClickHouse с реализацией доступа к хранилищу ClickHouse:
<pre style="background-color: lightgrey;">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;
}</pre>morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0tag:blogger.com,1999:blog-4428473564097379725.post-49183425967427991242020-10-25T13:00:00.003+05:002021-05-24T11:46:23.923+05:00Подготовка ClickHouse для хранения истории и тенденций Zabbix<h3>Создание таблиц истории</h3>
Подключиться к серверу Clickhouse можно при помощи команды следующего вида:
<pre style="background-color: black; color: white;">$ clickhouse-client -u zabbix --ask-password zabbix</pre>
В <a href="https://glaber.io/">Glaber</a>'е используется одна таблица вместо таблиц history, history_uint, history_str и history_text. Таблица history_log не поддерживается. В отличие от Glaber, я решил скрупулёзно воспроизвести схему данных, принятую в Zabbix.
<br /><br />
Однако, если писать данные в таблицы небольшими порциями, менее 8192 строк за раз, Clickhouse не выполняет слияние таких маленьких фрагментов в фоновом режиме. В процессе всего нескольких часов работы Zabbix в Clickhouse могут накопиться сотни тысяч фрагментов. При необходимости перезапуска Clickhouse в таком случае можно столкнуться с интересной проблемой: Clickhouse запущен, но не открывает порты на прослушивание и не принимает подключения. В это время Clickhouse перебирает все фрагменты таблиц, чтобы составить их каталог. Запуск может затянуться на несколько часов.
<br /><br />
Чтобы Clickhouse своевременно сливал фрагменты таблиц в фоновом режиме, нужно чтобы в каждом фрагменте было не мнее 8192 строк. Но даже если ваш сервер Zabbix генерирует более 8192 новых значений в секунду, они будут во-первых распределяться между процессами DBSyncers (количество которых настраивается через опцию конфигурации StartDBSyncers), а во-вторых - они будут распределяться между разными таблицами истории. В моей практике наибольшая доля данных приходилась на таблицу history_uint, а history_log во многих случаях не использовалась вовсе.
<br /><br />
По умолчанию процессы DBSyncers запускаются раз в секунду. Уменьшать их количество не всегда возможно, т.к. эти же процессы используются и для чтения данных из таблиц истории, когда необходимых данных нет в кэше значений. Особенно высока потребность в большом количестве процессов DBSyncers при старте Zabbix, когда кэш значений ещё пуст. Я пробовал делать заплатку для Zabbix, которая добавляет поддержку опции конфигурации DBSyncersPeriod и позволяет настраивать периодичность записи процессами DBSyncers. Такое решение не прошло проверку практикой, при малом количестве новых значений в секунду и большом количестве DBSyncers для накопления достаточного объёма данных приходится выполнять запись раз в 2-5 минут. И это без учёта неравномерности распределения данных по разным таблицам!
<br /><br />
Таким образом, решение использовать буферную таблицу в <a href="https://glaber.io/">Glaber</a> было вполне оправданным. Поэтому настоящие таблицы истории в моём варианте можно создать при помощи следующих запросов:
<pre style="background-color: lightgrey;">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);</pre>
Как можно заметить, в отличие от <a href="https://glaber.io/">Glaber</a>, здесь таблицы разделены не на помесячные, а на посуточные секции. Для удаления ненужных секций в дальнейшем можно будет воспользоваться запросами следующего вида:
<pre style="background-color: lightgrey;">ALTER TABLE real_history_uint DROP PARTITION 20190125;</pre>
<h3>Создание буферных таблиц истории</h3>
Теперь нужно создать буферные таблицы, с которыми непосредственно будет работать сам Zabbix. При вставке данных в буферную таблицу данные сохраняются в оперативной памяти и не записываются в нижележащую таблицу, пока не будет достигнуто одно из условий. При чтении данных из буферной таблицы данные ищутся как в самой буферной таблице, так и в нижележащей таблице на диске. Я создал буферные таблицы следующим образом:
<pre style="background-color: lightgrey;">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_log, 8, 30, 60, 8192, 65536, 262144, 67108864);</pre>
Для всех созданных буферных таблиц действуют следующие условия записи данных в реальную таблицу:
<ul>
<li>30 секунд - минимальное время, которое должно пройти со момента предыдущей записи в реальную таблицу, прежде чем буферная таблица запишет данные в реальную таблицу,</li>
<li>60 секунд - максимальное время с момента предыдущей записи в реальную таблицу, по прошествии которого операция записи будет выполнена вне зависимости от всех остальных условий,</li>
<li>8192 строк - минимальное количество записей, которое должно быть в буферной таблице, прежде чем буферная таблица запишет данные в реальную таблицу,</li>
<li>65536 строк - максимальное количество записей, которое должно быть в буферной таблице, по достижении которого операция записи будет выполнена вне зависимости от всех остальных условий,</li>
<li>256 килобайт - минимальный объём данных, который должен накопиться в буферной таблице, прежде чем буферная таблица запишет данные в реальную таблицу,</li>
<li>64 мегабайта - максимальный объём данных, который должен накопиться в буферной таблице, по достижении которого операция записи будет выполнена вне зависимости от всех остальных условий.</li>
</ul>
Итак, буферная таблица ждёт либо выполнения одного из условий максимума, либо выполнения всех условий минимума, после чего данные будут записаны в реальную таблицу.
<h3>Виртуальные таблицы тенденций в ClickHouse</h3>
Т.к. в моём случае внедрение Zabbix состоялось давно и вокруг него уже было написано значительное количество различных скриптов, использующих данные из таблиц тенденций, то мне нужно было сделать переход на ClickHouse максимально мягким. Для этого я воспользовался готовым решением Михаила Макурова, которое он продемонстрировал на одном из слайдов своей презентации. Воспользуемся агрегирующими материализованными представлениями ClickHouse, создав их при помощи следующих запросов:
<pre style="background-color: lightgrey;">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;</pre>
Эти представления материализованные, а это значит, что они будут вычисляться не при поступлении запроса, а будут храниться на диске. Представления будут автоматически обновляться сервером Clickhouse по мере вставки новых данных в таблицы истории. Серверу Zabbix не нужно будет вычислять эти данные самостоятельно, т.к. всю необходимую работу за него будет делать Clickhouse.
<h3>Скрипты</h3>
Итак, структура таблиц готова, теперь нужно разобраться с обслуживанием таблиц, в том числе удалением устаревших секций таблиц истории и материализованных видов таблиц тенденций, а также с переносом данных. Для обеих задач я решил написать скрипты на Python, воспользовавшись модулем Python для работы с Clickhouse, который называется clickhouse-driver. Из всех рассмотренных мной модулей для языка Python этот модуль приглянулся по следующим причинам:
<ul>
<li>единственный, который использует двоичный протокол ClickHouse, а не использует доступ по HTTP,</li>
<li>снабжён файлом README и каталогом с документацией. Документация также доступна онлайн: <a href="https://clickhouse-driver.readthedocs.io/en/latest/quickstart.html">Welcome to clickhouse-driver</a>.</li>
</ul>
Я собрал deb-пакеты с модулем clickhouse-driver, воспользовавшись своей статьёй <a href="https://vladimir-stupin.blogspot.com/2013/10/deb-python.html">Создание deb-пакетов для модулей Python</a>. Вместе с этим модулем понадобилось также собрать deb-пакеты с требуемыми ей модулями clickhouse-cityhash и zstd.
<h3>Обслуживание таблиц</h3>
Для удаления устаревших секций таблиц и для принудительного слияния фрагментов секций я написал скрипт на Python, который назвал maintein_tables.py:
<pre style="background-color: lightgrey;">#!/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()</pre>
Запрос для удаления устаревших секций таблиц уже был приведён, а для слияния фрагментов секций таблиц в скрипте используется запрос следующего вида:
<pre style="background-color: lightgrey;">OPTIMIZE TABLE history PARTITION 20200521 FINAL DEDUPLICATE;</pre>
В примере скрипт настроен на хранение числовых исторических данных в течение года, текстовых и журнальных данных - в течение семи дней и тенденций в течение 10 лет. При необходимости можно поменять настройки подключения к серверу Clickhouse и настройки длительности хранения данных в таблицах.
<br /><br />
Скрипт можно также скачать по ссылке <a href="http://stupin.su/files/zabbix/maintein_tables.py">maintein_tables.py</a>
<h3>Копирование данных</h3>
Михаил Макуров реализовал поддержку хранения исторических данных Zabbix в ClickHouse на основе поддержки хранения исторических данных в ElasticSearch. В документации Zabbix упоминается, что при хранении исторических данных в ElasticSearch таблицы тенденций не используются. Стало быть, таблицы тенденций не используются и при хранении исторических данных в ClickHouse. Для решения этой проблемы были созданы материализованные представления trends и trends_uint, которые описаны выше.
<br /><br />
При переключении существующей инсталляции Zabbix на использование ClickHouse или ElasticSearch не составляет особого труда перенести содержимое таблиц истории из старого хранилища в новое. А вот таблицы тенденций копировать просто некуда. Скопировать их можно было бы в таблицы истории, но в таблицах тенденций нет точных значений, а есть лишь минимальные, средние и максимальные значения за час, а также количество значений в исходной выборке. Чтобы сохранить возможность видеть данные из таблиц тенденций на графиках, можно попытаться сгенерировать выборку, удовлетворяющую этим условиям, и поместить получившиеся значения в таблицы истории.
<br /><br />
Поскольку и в дальнейшем хотелось бы иметь возможность просматривать графики за тот период, который изначально был выбран для таблиц тенденций, такой подход позволил бы сразу оценить:
<ul>
<li>сколько места на диске займёт точная история за период, аналогичный периоду хранения таблиц тенденций,</li>
<li>насколько хорошо ClickHouse будет справляться с такой нагрузкой.</li>
</ul>
Итак, кроме функций копирования содержимого таблиц истории, понадобятся также функции для генерирования правдоподобных исторических данных на основе таблиц тенденций.
<br /><br />
Скрипт copy_data.py выполняет полное копирование таблиц истории, а также дополняет таблицы истории правдоподобными данными, сгенерированными на основе таблиц тенденций.
<br /><br />
В начале скрипта можно найти настройки, которые будут использоваться для подключения к базе данных с исходными данными и к целевой базе данных. В конце скрипта можно найти вызовы функций копирования данных:
<pre style="background-color: lightgrey;">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)</pre>
Сначала копируется содержимое таблиц history_str, history_text и history_log, потом копируется таблица history порциями по 3 часа, потом в таблицу history вносятся данные, сгенерированные из данных таблицы trends, и, наконец, таблица history_uint копируется порциями по полчаса и в неё вносятся данные, сгенерированные из данных таблицы trends_uint.
<br /><br />
Функция copy_history перед началом работы выполняет запрос, который находит минимальное и максимальное значение отметок времени в таблице. Этот запрос может выполняться очень долго, поэтому в функциях предусмотрена возможность указания минимального и максимального значения отметок времени в аргументах clock_min и clock_max. Кроме того, указывая эти значения вручную, можно точно настраивать период времени, данные за который нужно обработать. Это может быть полезно, например, для того, чтобы скопировать данные до конца суток, предшествующих переключению Zabbix на Clickhouse. После переключения можно указать период времени, за который накопились новые данные с момента прошлого копирования.
<br /><br />
Если вы не собираетесь копировать данные из таблиц тенденций, то вызовы функций copy_trends можно закомментировать. Можно скопировать тенденции после переключения Zabbix на Clickhouse - скрипт не имеет жёстко предписанной последовательности действий и может быть адаптирован под необходимую вам последовательность действий.
<br /><br />
Скрипт осуществляет вставку данных в ClickHouse порциями по 1048576 строк. Размер порции можно настраивать при помощи аргумента portion функций copy_history и copy_trends. При настройке portion стоит учитывать, что объём вставляемых за один раз данных не должно превышать значения настройки max_memory_usage из файла конфигурации /etc/clickhouse-server/users.xml сервера Clickhouse.
<br /><br />
В процессе тестирования скрипта при переносе данных из базы данных PostgreSQL выяснилась интересная особенность: PostgreSQL не поддерживает колонки с числами с плавающей запятой, а вместо этого используется тип Decimal, имеющий более ограниченную точность в представлении данных. Из-за меньшей точности данные в таблице тенденций чисел с плавающей запятой оказывается невозможно сгенерировать такую правдоподобную выборку данных, которая точно соответствовала бы указанным свойствам. Если сгенерированная выборка не удовлетворяет указанным свойствам, то сгенерированные данные всё-таки вставляются в таблицу истории, но скрипт выдаёт предупреждение о подозрительных данных тенденций.
<br /><br />
Скрипт не приведён в статье из-за его относительно большого объёма. Скрипт можно взять по ссылке <a href="http://stupin.su/files/zabbix/copy_data.py">copy_data.py</a>
<h3>Пасхальное яйцо</h3>
При выходе из клиента ClickHouse 1 января 2020 года заметил поздравление с новым годом:
<pre style="background-color: black; color: white;">db.server.tld :) exit
Happy new year.</pre>morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0tag:blogger.com,1999:blog-4428473564097379725.post-50042369921524765182020-10-18T13:00:00.001+05:002020-10-18T13:00:05.028+05:00Установка и настройка сервера ClickHouseПакеты с клиентом и сервером Clickhouse имеются в официальных репозиториях Debian Buster. Для их установки можно воспользоваться следующей командой:
<pre style="background-color: black; color: white;"># apt-get install clickhouse-server clickhouse-client</pre>
Для работы серверу Clickhouse требуется поддержка дополнительных процессорных инструкций SSE 4.2. Чтобы проверить наличие поддержки этих инструкций и пересобрать Clickhouse, если они не поддерживаются, обратитесь к статье <a href="https://vladimir-stupin.blogspot.com/2020/10/clickhouse-sse-42.html">Пересборка Clickhouse для процессоров без поддержки SSE 4.2</a>.
<br /><br />
В каталоге /etc/clickhouse-server находится файл config.xml с настройками сервера и файл users.xml с настройками пользователей. Оба файла хорошо прокомментированы, но из-за обилия настроек ориентироваться в них довольно тяжело. Я переименовал эти файлы, чтобы создать более компактные файлы конфигурации:
<pre style="background-color: black; color: white;"># cd /etc/clickhouse-server/
# cp users.xml users.xml.sample
# cp config.xml config.xml.sample</pre>
В файл конфигурации config.xml я вписал следующие настройки:
<pre style="background-color: lightgrey;"><?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></pre>
Смысл большинства настроек можно понять из их названия. Кратко опишу некоторые из них:
<ul>
<li>display_name - отображаемое в клиенте имя сервера,</li>
<li>max_connections - максимальное количество подключений от клиентов,</li>
<li>max_concurrent_queries - максимальное количество одновременно обрабатываемых запросов. Т.к. каждый запрос обслуживается конвейером из нескольких потоков, то каждый запрос порождает нагрузку как минимум на одно процессорное ядро. Лучше всего будет выполнять одновременно количество запросов, не превышающее количество процессорных ядер сервера или виртуальной машины.</li>
<li>uncompressed_cache_size задаёт размер кэша несжатых данных в байтах. Если предполагается, что на сервере часто будут выполняться короткие запросы, этот кэш поможет снизить нагрузку на дисковую подсистему. Обратите внимание, что в настройках пользователя должно быть разрешено использование кэша несжатых данных в опции use_uncompressed_cache.</li>
<li>mark_cache_size - кэш меток. Метки являются своего рода индексами данных. Сервер Clickhouse не хочет запускаться, если значение этой настройки меньше 5 гигабайт. Хорошая новость в том, что память под этот кэш будет выделяться по мере необходимости.</li>
<li>path - путь к файлам базы данных,</li>
<li>default_database - имя базы данных, с которой будут работать клиенты, не указавшие какую-то определённую базу данных,</li>
<li>timezone - часовой пояс сервера.</li>
</ul>
Файл users.xml я привёл к следующему виду:
<pre style="background-color: lightgrey;"><?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></pre>
Файл состоит из трёх секций:
<ul>
<li>users - пользователи базы данных. Каждый пользователь содержит ссылку на профиль и квоту,</li>
<li>profiles - профили содержат настройки пользователей,</li>
<li>quotas - квоты содержат ограничения на выполнение запросов от пользователей.</li>
</ul>
В примере конфигурации выше описан пользователь zabbix с паролем zabbix, который может устанавливать подключения к серверу только с IP-адреса 127.0.0.1, использует профиль default и квоту default.
<br /><br />
В профиле default выставлены следующие настройки:
<ul>
<li>max_memory_usage - максимальный объём памяти, который сервер может выделить пользователю для обработки его запросов, в примере настроено ограничение в 2 гигабайта,</li>
<li>max_query_size - максимальный размер одного запроса, по умолчанию - 256 килобайт, в примере - 1 мегабайт,</li>
<li>max_ast_elements - максимальное количество элементов в дереве синтаксического разбора, по умолчанию - 50 тысяч элементов, в примере - 1 миллион элементов,</li>
<li>use_uncompressed_cache - значение этой опции разрешает или запрещает использование кэша несжатых данных, в примере значение 1 разрешает его использование,</li>
<li>readonly - значение этой опции разрешает или запрещает запросы на изменение данных, в примере значение 0 разрешает изменение данных.</li>
</ul>
В квоте default выставлено единственное ограничение - длительность обработки запроса ограничена одним часом.
<br /><br />
Включим автозапуск сервера:
<pre style="background-color: black; color: white;"># systemctl enable clickhouse-server.service</pre>
Запустим сервер:
<pre style="background-color: black; color: white;"># systemctl start clickhouse-server.service</pre>
<h3>Решение проблем</h3>
Если спустя некоторое время в журнале /var/log/clickhouse-server/clickhouse-server.err.log появляются ошибки следующего вида:
<pre style="background-color: lightgrey;">2020.04.17 10:44:51.741280 [ 6317714 ] {} <Error> HTTPHandler: std::exception. Code: 1001, type: std::system_error, e.what() = Resource temporarily unavailable</pre>
То может помочь увеличение переменной ядра vm.max_map_count следующей командой:
<pre style="background-color: black; color: white;"># sysctl -w vm.max_map_count = 524288</pre>
Если изменение этой настройки помогло справиться с проблемой, можно прописать её в файл /etc/sysctl.conf, чтобы оно автоматически применялось при загрузке системы:
<pre style="background-color: lightgrey;">vm.max_map_count=524288</pre>
В <a href="https://www.kernel.org/doc/Documentation/sysctl/vm.txt">документации ядра Linux</a> эта переменная ядра объясняется следующим образом:
<blockquote>
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.
<br /><br />
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.
<br /><br />
The default value is 65536.
</blockquote>
Перевод:
<blockquote>
Этот файл содержит максимальное количество участков памяти, которое может иметь процесс. Участки памяти косвенно создаются при вызове malloc, а напрямую - при вызове mmap и mprotect, а также при загрузке разделяемых библиотек.
<br /><br />
Хотя большинству приложений требуется меньше тысячи участков, некоторые программы, в частности отладчики malloc, могут потреблять значительное их количество, от одного до двух участков при каждом выделении памяти.
<br /><br />
Значение по умолчанию - 65536.
</blockquote>morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0tag:blogger.com,1999:blog-4428473564097379725.post-34480111079031676552020-10-11T13:00:00.000+05:002020-10-11T13:00:10.202+05:00Пересборка ClickHouse для процессоров без поддержки SSE 4.2В официальных репозиториях Debian Buster появились пакеты с сервером и клиентом Clickhouse. По умолчанию сервер Clickhouse собран с использованием процессорных инструкций SSE 4.2, т.к. именно такие <a href="https://clickhouse.yandex/%E2%80%8Bdocs/%E2%80%8Bru/%E2%80%8Bgetting_started/#%E2%80%8Bsistemnye-trebovaniia">системные требования</a> указаны на официальной странице проекта.
<br /><br />
Для проверки, поддерживает ли процессор SSE 4.2, можно воспользоваться следующей командой:
<pre style="background-color: black; color: white;">$ grep -q sse4_2 /proc/cpuinfo && echo "SSE 4.2 supported" || echo "SSE 4.2 not supported"</pre>
При попытке запустить сервер ClickHouse на процессоре, не поддерживающем этот набор инструкций, в журнале /var/log/messages можно будет обнаружить сообщения следующего вида:
<pre style="background-color: lightgrey;">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]</pre>
Мой домашний компьютер не отличается новизной, поэтому для экспериментов дома мне пришлось пересобрать пакеты с Clickhouse. Сборочные скрипты автоматически определяют поддержку инструкций SSE 4.2 и при её отсутствии выполняют сборку так, чтобы пакеты работали без них.
<br /><br />
Впишем в файл /etc/apt/sources.list дополнительные репозитории с исходными текстами:
<pre style="background-color: lightgrey;">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</pre>
Обновим список пакетов, доступных через репозитории:
<pre style="background-color: black; color: white;"># apt-get update</pre>
Установим пакеты, которые потребуются нам для сборки ClickHouse из исходных текстов:
<pre style="background-color: black; color: white;"># apt-get build-dep clickhouse</pre>
И скачаем пакет с исходными текстами:
<pre style="background-color: black; color: white;">$ apt-get source clickhouse</pre>
Переходим в каталог с распакованными исходными текстами, запускаем dch и описываем изменения.
<pre style="background-color: black; color: white;">$ cd clickhouse-18.16.1+ds
$ dch -i</pre>
В открывшемся редакторе дописываем к номеру версии fix1 и описываем изменения:
<pre style="background-color: lightgrey;">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</pre>
Собираем пакет:
<pre style="background-color: black; color: white;">$ debuild -us -uc</pre>
Для сборки потребуется довольно много оперативной памяти. Я пытался собрать пакет на виртуальной машине с 2 гигабайтами оперативной памяти, потом увеличил до 3 и до 4, но этого объёма оказывалось по-прежнему недостаточно для того, чтобы собрать библиотеку libclickhouse.so из объектных файлов. Вернул виртуальной машине 2 гигабайта оперативной памяти и подключил раздел подкачки размером 8 гигабайт. Сборка шла долго, но всё-таки завершилась успешно.
<br /><br />
Если сборка завершается неудачно, а в тексте ошибки имеются такие строки:
<pre style="background-color: lightgrey;">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.</pre>
То можно попробовать удалить пакет ninja-build:
<pre style="background-color: black; color: white;"># apt-get purge ninja-build</pre>
Затем можно попробовать запустить сборку пакета снова.
<br /><br />
После успешной сборки можно будет выйти из сборочного каталога и установить появившиеся рядом с ним двоичные пакеты:
<pre style="background-color: black; color: white;"># 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</pre>
Или можно воспользоваться утилитой <a href="https://vladimir-stupin.blogspot.com/2019/08/debian-aptly.html">aptly, чтобы создать собственный репозиторий</a> и поместить в него эти пакеты. В таком случае для установки пакетов в систему будет достаточно:
<ol>
<li>подключить этот репозиторий в файле /etc/apt/sources.list,</li>
<li>обновить список пакетов, доступных через репозитории, командой apt-get update,</li>
<li>поставить пакеты, например, командой apt-get install clickhouse-server clickhouse-client</li>
</ol>
morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com2tag:blogger.com,1999:blog-4428473564097379725.post-14715439386767449702020-10-04T13:00:00.001+05:002020-10-04T13:00:00.157+05:00Ошибки аутентификации SNMPv3 в Zabbix 3.4
Для контроля доступности устройства по SNMP обычно я использую элементы данных типа «Внутренний Zabbix» с ключом «zabbix[host,snmp,available]», который описан на странице документации <a href="https://www.zabbix.com/documentation/3.4/ru/manual/config/items/itemtypes/internal">8 Внутренние проверки</a>. Подробнее о недоступности узлов можно прочитать здесь: <a hfre="https://www.zabbix.com/documentation/3.4/ru/manual/appendix/items/unreachability">12 Настройки недостижимости/недоступности хостов</a>. Итак, когда сервер Zabbix решает, что узел больше не доступен по SNMP, значение элемента данных с ключом «zabbix[host,snmp,available]» становится равным нулю. Можно настроить триггер, который будет срабатывать при нулевом значении этого ключа.
<br /><br />
После того, как в сети появились узлы, опрашиваемые по SNMPv3, отсутствие срабатываний этого триггера перестало быть надёжным критерием доступности узла по SNMP. Все элементы данных SNMPv3 на узле могут быть в неподдерживаемом состоянии, однако Zabbix при этом считает узел доступным по SNMP и триггер не срабатывает. Как выяснилось, Zabbix не считает проблемой, если узел ответил ошибкой аутентификации. Формально устройство действительно отвечает по протоколу SNMP, но фактически данные с него не снимаются.
<br /><br />
Я решил испрвить эту ситуацию, в очередной раз внеся правку в исходный текст Zabbix. К счастью, сделать это оказалось совсем не сложно. Интересующий нас фрагмент кода находится в файле src/zabbix_server/poller/checks_snmp.c в функции zbx_get_snmp_response_error. Удалим специальную обработку ошибок аутентификации SNMPv3, интерпретируя эти ошибки как недоступность элемента данных:
<pre style="background-color: lightgrey;">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)
{</pre>
Эту тривиальную заплатку можно взять по ссылке <a href="http://stupin.su/files/zabbix/zabbix3_4_12_snmpv3_auth_errors.patch">zabbix3_4_12_snmpv3_auth_errors.patch</a>.
morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0tag:blogger.com,1999:blog-4428473564097379725.post-91910954580325698352020-09-27T13:00:00.000+05:002020-09-27T13:00:06.574+05:00Настройки таймаута и количества попыток для запросов SNMP в Zabbix 3.4До версии 2.2.0 в Zabbix для опроса по SNMP использовались настройки опроса по умолчанию. По умолчанию в библиотеке SNMP таймаут опроса составлял 1 секунду, а в случае неудачи делалось до 5 дополнительных попыток опроса. В Zabbix 2.2 для опроса по SNMP используется значение опции Timeout из файла конфигурации и делается только одна попытка опроса.
<br /><br />
Ранее, чтобы уменьшить количество вызовов скриптов внешнего опроса, в конфигурации моих сереров Zabbix было выставлено максимально возможное значение таймаута - 30 секунд, а каждый такой скрипт запрашивал как можно больше значений у устройства и отправлял полученные значения в Zabbix при помощи утилиты zabbix_sender. Если скрипт успевал снять все необходимые данные, укладываясь в отведённые для его работы 30 секунд, то всё хорошо. Если скрипт не укладывался в 30 секунд, то процедуру опроса делили на несколько частей, так чтобы каждая из них уложилась в 30 секунд. Затем в Zabbix'е заводили по отдельному элементу данных для вызова скрипта, указывая ему какую именно часть данных нужно снять с устройства.
<br /><br />
При переходе с Zabbix 2.0 на 2.2 изменение настроек таймаута SNMP привело к большим проблемам: использование процессов Poller выросло до 100% и сервер перестал успевать опрашивать оборудование в необходимом темпе. Происходило это потому, что первый же запрос SNMP к недоступному устройству растягивался до 30 секунд, в течение которых процесс просто ждал ответа от устройства, не занимаясь больше ничем.
<br /><br />
Если вы не используете скриптов внешнего опроса или значение таймаута в конфигурации Zabbix имеет маленькое значение, или на вашем сервере Zabbix много оперативной памяти, то для вас это изменение не будет представлять никаких особых проблем - можно просто увеличить количество процессов Poller. Мне же в моём случае было просто жалко тратить оперативную память на процессы, которые фактически ничем не занимались. Прежний Zabbix без проблем справлялся с опросом, используя значительно меньше оперативной памяти, значит и новый тоже может. Я тогда выполнил откат до прежней версии Zabbix и стал изучать исходные тексты Zabbix с целью вернуть прежние значения таймаута в запросы SNMP.
<br /><br />
Получившиеся исправления ранее уже были описаны в рамках более крупной статьи <a href="https://vladimir-stupin.blogspot.com/2013/12/zabbix-220-debian-wheezy.html">Установка и настройка Zabbix 2.2.0 в Debian Wheezy</a>, но там им не уделялось достаточного внимания. На этот раз я опишу заплатку более подробно.
<br /><br />
Задеклалируем изменения, которые собираемся внести, отредактировав пример файла конфигурации conf/zabbix_server.conf:
<pre style="background-color: lightgrey;">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
@@ -439,6 +439,26 @@ DBUser=zabbix
Timeout=4
+### Option: SNMPTimeout
+# Specifies how long we wait for SNMP device (in seconds).
+#
+# Mandatory: no
+# Range: 1-30
+# Default:
+# SNMPTimeout=1
+
+SNMPTimeout=1
+
+### Option: SNMPRetries
+# Specifies how many times to trying request for SNMP device
+#
+# Mandatory: no
+# Range: 1-10
+# Default:
+# SNMPRetries=3
+
+SNMPRetries=3
+
### Option: TrapperTimeout
# Specifies how many seconds trapper may spend processing new data.
#
</pre>
Объявим переменные CONFIG_SNMP_TIMEOUT и CONFIG_SNMP_RETRIES в том же файле, где объявлена переменная CONFIG_TIMEOUT. Это файл src/libs/zbxconf/cfg.c:
<pre style="background-color: lightgrey;">Index: zabbix-3.4.12-1+buster/src/libs/zbxconf/cfg.c
===================================================================
--- zabbix-3.4.12-1+buster.orig/src/libs/zbxconf/cfg.c
+++ zabbix-3.4.12-1+buster/src/libs/zbxconf/cfg.c
@@ -31,6 +31,8 @@ char *CONFIG_LOG_FILE = NULL;
int CONFIG_LOG_FILE_SIZE = 1;
int CONFIG_ALLOW_ROOT = 0;
int CONFIG_TIMEOUT = 3;
+int CONFIG_SNMP_TIMEOUT = 1;
+int CONFIG_SNMP_RETRIES = 3;
static int __parse_cfg_file(const char *cfg_file, struct cfg_line *cfg, int level, int optional, int strict);
</pre>
Пропишем эти же объявления в заголовочный файл include/cfg.h, на этот раз - как внешние объявления:
<pre style="background-color: lightgrey;">Index: zabbix-3.4.12-1+buster/include/cfg.h
===================================================================
--- zabbix-3.4.12-1+buster.orig/include/cfg.h
+++ zabbix-3.4.12-1+buster/include/cfg.h
@@ -46,6 +46,8 @@ extern char *CONFIG_LOG_FILE;
extern int CONFIG_LOG_FILE_SIZE;
extern int CONFIG_ALLOW_ROOT;
extern int CONFIG_TIMEOUT;
+extern int CONFIG_SNMP_TIMEOUT;
+extern int CONFIG_SNMP_RETRIES;
struct cfg_line
{
</pre>
Теперь нужно научить сервер Zabbix и Zabbix-прокси извлекать значения из файлов конфигурации в эти переменные. Отредактируем файлы src/zabbix_server/server.c и src/zabbix_proxy/proxy.c следующим образом:
<pre style="background-color: lightgrey;">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
@@ -602,6 +602,10 @@ static void zbx_load_config(ZBX_TASK_EX
PARM_OPT, 0, 0},
{"Timeout", &CONFIG_TIMEOUT, TYPE_INT,
PARM_OPT, 1, 30},
+ {"SNMPTimeout", &CONFIG_SNMP_TIMEOUT, TYPE_INT,
+ PARM_OPT, 1, 30},
+ {"SNMPRetries", &CONFIG_SNMP_RETRIES, TYPE_INT,
+ PARM_OPT, 1, 10},
{"TrapperTimeout", &CONFIG_TRAPPER_TIMEOUT, TYPE_INT,
PARM_OPT, 1, 300},
{"UnreachablePeriod", &CONFIG_UNREACHABLE_PERIOD, TYPE_INT,
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
@@ -623,6 +623,10 @@ static void zbx_load_config(ZBX_TASK_EX
PARM_OPT, 0, 0},
{"Timeout", &CONFIG_TIMEOUT, TYPE_INT,
PARM_OPT, 1, 30},
+ {"SNMPTimeout", &CONFIG_SNMP_TIMEOUT, TYPE_INT,
+ PARM_OPT, 1, 30},
+ {"SNMPRetries", &CONFIG_SNMP_RETRIES, TYPE_INT,
+ PARM_OPT, 1, 10},
{"TrapperTimeout", &CONFIG_TRAPPER_TIMEOUT, TYPE_INT,
PARM_OPT, 1, 300},
{"UnreachablePeriod", &CONFIG_UNREACHABLE_PERIOD, TYPE_INT,
</pre>
Реализация функций опроса по SNMP находится в файле src/zabbix_server/poller/checks_snmp.c, но прежде чем редактировать его, заменим в заголовочном файле src/zabbix_server/poller/checks_snmp.h объявление внешней переменной CONFIG_TIMEOUT на объявления внешних переменных CONFIG_SNMP_TIMEOUT и CONFIG_SNMP_RETRIES:
<pre style="background-color: lightgrey;">Index: zabbix-3.4.12-1+buster/src/zabbix_server/poller/checks_snmp.h
===================================================================
--- zabbix-3.4.12-1+buster.orig/src/zabbix_server/poller/checks_snmp.h
+++ zabbix-3.4.12-1+buster/src/zabbix_server/poller/checks_snmp.h
@@ -26,7 +26,8 @@
#include "sysinfo.h"
extern char *CONFIG_SOURCE_IP;
-extern int CONFIG_TIMEOUT;
+extern int CONFIG_SNMP_TIMEOUT;
+extern int CONFIG_SNMP_RETRIES;
#ifdef HAVE_NETSNMP
void zbx_init_snmp(void);
</pre>
А теперь можно отредактировать сам файл src/zabbix_server/poller/checks_snmp.c:
<pre style="background-color: lightgrey;">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
@@ -456,8 +456,10 @@ static struct snmp_session *zbx_snmp_ope
break;
}
- session.timeout = CONFIG_TIMEOUT * 1000 * 1000; /* timeout of one attempt in microseconds */
- /* (net-snmp default = 1 second) */
+ session.timeout = CONFIG_SNMP_TIMEOUT * 1000 * 1000; /* timeout of one attempt in microseconds */
+ /* (net-snmp default = 1 second) */
+ session.retries = CONFIG_SNMP_RETRIES - 1; /* number of retries after failed attempt */
+ /* (net-snmp default = 5) */
#ifdef HAVE_IPV6
if (SUCCEED != get_address_family(item->interface.addr, &family, error, max_error_len))
@@ -1095,7 +1097,7 @@ static int zbx_snmp_walk(struct snmp_ses
pdu->max_repetitions = max_vars;
}
- ss->retries = (0 == bulk || (1 == max_vars && == level) ? 1 : 0);
+ ss->retries = (0 == bulk || (1 == max_vars && 0 == level) ? 1 : 0) * (CONFIG_SNMP_RETRIES - 1);
/* communicate with agent */
status = snmp_synch_response(ss, pdu, &response);
@@ -1304,7 +1306,7 @@ static int zbx_snmp_get_values(struct sn
goto out;
}
- ss->retries = (1 == mapping_num && 0 == level ? 1 : 0);
+ ss->retries = (1 == mapping_num && 0 == level ? 1 : 0) * (CONFIG_SNMP_RETRIES - 1);
retry:
status = snmp_synch_response(ss, pdu, &response);
</pre>
При помощи команды «grep -R CONFIG_TIMEOUT | grep -i snmp» в исходных текстах можно найти ещё один любопытный фрагмент в файле libs/zbxdbcache/dbconfig.c, где можно увидеть отключение повторных попыток опроса по SNMP всего узла, если он не отвечает по SNMP:
<pre style="background-color: lightgrey;">static void DCincrease_disable_until(const ZBX_DC_ITEM *item, ZBX_DC_HOST *host, int now)
{
switch (item->type)
{
case ITEM_TYPE_ZABBIX:
if (0 != host->errors_from)
host->disable_until = now + CONFIG_TIMEOUT;
break;
case ITEM_TYPE_SNMPv1:
case ITEM_TYPE_SNMPv2c:
case ITEM_TYPE_SNMPv3:
if (0 != host->snmp_errors_from)
host->snmp_disable_until = now + CONFIG_TIMEOUT;
break;
case ITEM_TYPE_IPMI:
if (0 != host->ipmi_errors_from)
host->ipmi_disable_until = now + CONFIG_TIMEOUT;
break;
case ITEM_TYPE_JMX:
if (0 != host->jmx_errors_from)
host->jmx_disable_until = now + CONFIG_TIMEOUT;
break;
default:
/* nothing to do */;
}
}</pre>
Вопрос, стоит ли исправлять значение таймаута для проверок SNMP в этой функции, я оставлю на размышление. Если значение CONFIG_SNMP_TIMEOUT меньше CONFIG_TIMEOUT, то опрос по SNMP после временных перебоев будет приостанавливаться на меньшее время и, соответственно, восстанавливаться быстрее. Нагрузка на процессы Poller при этом может слегка повыситься, т.к. доступность узлов SNMP будет проверяться чаще. Если вы всё же решитесь поменять значение таймаута в этой функции, то вот очевидный патч для файла libs/zbxdbcache/dbconfig.c:
<pre style="background-color: lightgrey;">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
@@ -448,7 +448,7 @@ static void DCincrease_disable_until(con
case ITEM_TYPE_SNMPv2c:
case ITEM_TYPE_SNMPv3:
if (0 != host->snmp_errors_from)
- host->snmp_disable_until = now + CONFIG_TIMEOUT;
+ host->snmp_disable_until = now + CONFIG_SNMP_TIMEOUT;
break;
case ITEM_TYPE_IPMI:
if (0 != host->ipmi_errors_from)
</pre>
Полную версию описанной здесь заплатки можно взять по ссылке <a href="http://stupin.su/files/zabbix/zabbix3_4_12_snmp_timeout_retries.patch">zabbix3_4_12_snmp_timeout_retries.patch</a>.morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0tag:blogger.com,1999:blog-4428473564097379725.post-82893638945298581382020-09-20T13:00:00.000+05:002020-09-20T13:00:03.723+05:00Исправление доступа к периодам обслуживания в Zabbix 3.4В статье <a href="https://vladimir-stupin.blogspot.com/2019/06/zabbix-34.html">Исправление ручного закрытия проблем в Zabbix 3.4</a> я уже описывал, как дать права на ручное закрытие проблем пользователям, не имеющим прав редактирования узла. Похожая ситуация возникла и с редактированием периодов обслуживания. Для того, чтобы предоставить пользователю права редактировать периоды обслуживания, нужно дать ему права на редактирование самого узла. Пользователь, имеющий доступ к редактированию узла в Zabbix, может злоупотребить своими правами и просто снять с контроля узел на время его обслуживания. Ещё хуже, если этот пользователь по окончании обслуживания забудет вернуть узел на контроль.
<br /><br />
Чтобы избежать подобных злоупотреблений, может быть лучше выдать права создавать и редактировать периоды обслуживания всем пользователям, имеющим права на просмотр соответствующей группы узлов. Именно такое изменение внёс в исходный код веб-интерфейса Zabbix мой коллега Кирилл. Изменил он два файла: frontends/php/include/classes/api/services/CMaintenance.php и frontends/php/maintenance.php. Первый файл исправляет права доступа в методах <a href="https://www.zabbix.com/documentation/3.4/manual/api/reference/maintenance">API maintenance</a>, причём права удалять периоды обслуживания при помощи метода delete пользователю, не имеющему доступа к группе узлов, не предоставляется. Второй файл исправляет права доступа к периодам обслуживания непосредственно через сам веб-интерфейс Zabbix. Получившаяся заплатка выглядит следующим образом:
<pre style="background-color: lightgrey;">Index: zabbix-3.4.12-1+buster/frontends/php/include/classes/api/services/CMaintenance.php
===================================================================
--- zabbix-3.4.12-1+buster.orig/frontends/php/include/classes/api/services/CMaintenance.php
+++ zabbix-3.4.12-1+buster/frontends/php/include/classes/api/services/CMaintenance.php
@@ -276,7 +276,6 @@ class CMaintenance extends CApiService {
// hosts permissions
$options = [
'hostids' => $hostids,
- 'editable' => true,
'output' => ['hostid'],
'preservekeys' => true
];
@@ -289,7 +288,6 @@ class CMaintenance extends CApiService {
// groups permissions
$options = [
'groupids' => $groupids,
- 'editable' => true,
'output' => ['groupid'],
'preservekeys' => true
];
@@ -458,7 +456,6 @@ class CMaintenance extends CApiService {
'selectGroups' => ['groupid'],
'selectHosts' => ['hostid'],
'selectTimeperiods' => API_OUTPUT_EXTEND,
- 'editable' => true,
'preservekeys' => true
]);
@@ -580,7 +577,6 @@ class CMaintenance extends CApiService {
$db_hosts = API::Host()->get([
'output' => [],
'hostids' => $hostids,
- 'editable' => true,
'preservekeys' => true
]);
@@ -598,7 +594,6 @@ class CMaintenance extends CApiService {
$db_groups = API::HostGroup()->get([
'output' => [],
'groupids' => $groupids,
- 'editable' => true,
'preservekeys' => true
]);
Index: zabbix-3.4.12-1+buster/frontends/php/maintenance.php
===================================================================
--- zabbix-3.4.12-1+buster.orig/frontends/php/maintenance.php
+++ zabbix-3.4.12-1+buster/frontends/php/maintenance.php
@@ -105,7 +105,6 @@ if (isset($_REQUEST['maintenanceid'])) {
$dbMaintenance = API::Maintenance()->get([
'output' => API_OUTPUT_EXTEND,
'selectTimeperiods' => API_OUTPUT_EXTEND,
- 'editable' => true,
'maintenanceids' => getRequest('maintenanceid'),
]);
if (empty($dbMaintenance)) {
@@ -412,7 +411,6 @@ if (!empty($data['form'])) {
'maintenanceids' => $data['maintenanceid'],
'real_hosts' => true,
'output' => ['hostid'],
- 'editable' => true
]);
$data['hostids'] = zbx_objectValues($data['hostids'], 'hostid');
@@ -458,7 +456,6 @@ if (!empty($data['form'])) {
// get groups
$data['all_groups'] = API::HostGroup()->get([
- 'editable' => true,
'output' => ['groupid', 'name'],
'real_hosts' => true,
'preservekeys' => true
@@ -475,7 +472,6 @@ if (!empty($data['form'])) {
$data['hosts'] = API::Host()->get([
'output' => ['hostid', 'name'],
'real_hosts' => true,
- 'editable' => true,
'groupids' => $data['twb_groupid']
]);
@@ -483,7 +479,6 @@ if (!empty($data['form'])) {
$hostsSelected = API::Host()->get([
'output' => ['hostid', 'name'],
'real_hosts' => true,
- 'editable' => true,
'hostids' => $data['hostids']
]);
$data['hosts'] = array_merge($data['hosts'], $hostsSelected);
@@ -532,7 +527,6 @@ else {
'search' => [
'name' => ($filter['name'] === '') ? null : $filter['name']
],
- 'editable' => true,
'sortfield' => $sortField,
'sortorder' => $sortOrder,
'limit' => $config['search_limit'] + 1</pre>
Эту заплатку можно взять по ссылке <a href="http://stupin.su/files/zabbix/zabbix3_4_12_permit_edit_maintenances.patch">zabbix3_4_12_permit_edit_maintenances.patch</a>.
morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0tag:blogger.com,1999:blog-4428473564097379725.post-44665806214859066982020-09-13T13:00:00.001+05:002020-09-13T13:00:04.058+05:00Местонахождение устройства в панели проблем Zabbix 3.4На стартовой странице веб-интерфейса Zabbix по умолчанию отображается список актуальных проблем. Самая важная информация в этом списке - это время начала проблемы, узел сети, срабтавший триггер и длительность проблемы, но нет никакой информации о местоположении устройства. Если у вас небольшая сеть, расположенная не более чем по нескольким десяткам адресов, то хорошая система именования устройств может решить проблему поиска местонахождения устройства. Если же количество адресов, по которым находятся устройства, достигает нескольких тысяч, то правильное именование устройств становится трудной задачей.<br />
<br />
В Zabbix'е к каждому устройству можно прикрепить так называемые «инвентарные данные», среди которых есть поле адреса. Было бы неплохо показывать это поле в списке проблем, чтобы можно было без лишних телодвижений определить адрес устройства. К сожалению, Zabbix не предоставляет для этого штатных средств. Но к счастью, это можно сделать, внеся в исходный текст веб-интерфейса Zabbix небольшую правку.<br />
<br />
Интересующий нас виджет находится в файле frontends/php/app/views/monitoring.widget.problems.view.php<br />
<br />
Этот виджет фигурирует в списке маршрутов в файле frontends/php/include/classes/mvc/CRouter.php:<br />
<pre style="background-color: lightgrey;">'widget.problems.view' => ['CControllerWidgetProblemsView', 'layout.widget', 'monitoring.widget.problems.view'],</pre>
Класс CControllerWidgetProblemsView описан в файле frontends/php/app/controllers/CControllerWidgetProblemsView.php. Именно в этом классе готовятся данные, которые потом будут использованы в виджете для отображения. Данные об узлах, связанных с триггерами, в этом классе формируется при помощи функции getTriggersHostsList.<br />
<br />
Определение функции getTriggersHostsList находится в файле frontends/php/include/triggers.inc.php, для получения списка узлов с триггерами используется <a href="https://www.zabbix.com/documentation/3.4/manual/api/reference/host/get">метод API host.get</a>:<br />
<pre style="background-color: lightgrey;">$db_hosts = $hostids
? API::Host()->get([
'output' => ['hostid', 'name', 'status', 'maintenanceid', 'maintenance_status', 'maintenance_type'],
'selectGraphs' => API_OUTPUT_COUNT,
'selectScreens' => API_OUTPUT_COUNT,
'hostids' => array_keys($hostids),
'preservekeys' => true
])
: [];</pre>Внесём правку, которая добавит в этот список строку местоположения устройства из его инвентарных данных:<br />
<pre style="background-color: lightgrey;">Index: zabbix-3.4.12-1+buster/frontends/php/include/triggers.inc.php
===================================================================
--- zabbix-3.4.12-1+buster.orig/frontends/php/include/triggers.inc.php
+++ zabbix-3.4.12-1+buster/frontends/php/include/triggers.inc.php
@@ -2170,6 +2170,7 @@ function getTriggersHostsList(array $tri
'output' => ['hostid', 'name', 'status', 'maintenanceid', 'maintenance_status', 'maintenance_type'],
'selectGraphs' => API_OUTPUT_COUNT,
'selectScreens' => API_OUTPUT_COUNT,
+ 'selectInventory' => ['location'],
'hostids' => array_keys($hostids),
'preservekeys' => true
])</pre>Теперь эти данные нужно отобразить в виджете. Внесём соответствующую правку в файл frontends/php/app/views/monitoring.widget.problems.view.php:<br />
<pre style="background-color: lightgrey;">Index: zabbix-3.4.12-1+buster/frontends/php/app/views/monitoring.widget.problems.view.php
===================================================================
--- zabbix-3.4.12-1+buster.orig/frontends/php/app/views/monitoring.widget.problems.view.php
+++ zabbix-3.4.12-1+buster/frontends/php/app/views/monitoring.widget.problems.view.php
@@ -54,6 +54,7 @@ $table = (new CTableInfo())
$show_recovery_data ? _('Status') : null,
_('Info'),
($data['sortfield'] === 'host') ? [_('Host'), $sort_div] : _('Host'),
+ ($data['sortfield'] === 'location') ? [_('Location'), $sort_div] : _('Location'),
[
($data['sortfield'] === 'problem') ? [_('Problem'), $sort_div] : _('Problem'),
' &bullet; ',
@@ -198,11 +199,19 @@ foreach ($data['data']['problems'] as $e
];
}
+ $trigger_hosts = array_values($data['data']['triggers_hosts'][$trigger['triggerid']]);
+ $locations = array();
+ foreach($trigger_hosts as $host)
+ {
+ $locations[] = $host['inventory']['location'];
+ }
+
$table->addRow(array_merge($row, [
$show_recovery_data ? $cell_r_clock : null,
$show_recovery_data ? $cell_status : null,
makeInformationList($info_icons),
$triggers_hosts[$trigger['triggerid']],
+ join(', ', $locations),
$description,
(new CCol(
($problem['r_eventid'] != 0)</pre>Как видно, в правке:<br />
<ol><li>в таблицу был добавлен заголовок новой колонки Location,</li>
<li>по каждому из триггеров формируется строка со списком адресов узлов, на значения элементов данных из которых опирается этот триггер,</li>
<li>строки с адресами через запятую с пробелом склеиваются в одну строку,</li>
<li>полученная строка добавляется в строку таблицы, в колонку Location.</li>
</ol>Готовую заплатку можно взять по ссылке <a href="http://stupin.su/files/zabbix/zabbix3_4_12_frontend_location.patch">zabbix3_4_12_frontend_location.patch</a>.morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0tag:blogger.com,1999:blog-4428473564097379725.post-90585591018988311342020-09-06T13:00:00.002+05:002020-09-07T11:54:14.436+05:00Оптимизация Linux при использовании SSDВ прошлых статьях были описаны <a href="https://vladimir-stupin.blogspot.com/2020/08/ssd-micron-5200-max.html">процедура обновления прошивки твердотельного накопителя Micron модели SSD 5200 MAX</a> и <a href="https://vladimir-stupin.blogspot.com/2020/08/smart-ssd-zabbix.html">шаблон Zabbix для контроля основных показателей его состояния</a>. В этой статье пойдёт речь о том, какие дополнительные настройки Linux можно сделать для того, чтобы увеличить производительность системы при работе с твердотельными накопителями и увеличить срок службы самих накопителей.<br />
<h3>Изменение планировщика ввода-вывода</h3>По умолчанию Linux использует планировщик ввода-вывода cfq, который стремится упорядочить блоки данных так, чтобы уменьшить количество позиционирований головки с дорожки на дорожку диска. Для SSD это не имеет смысла, но приводит к задержке мелких операций ввода-вывода. Вместо планировщика cfq рекомендуется использовать планировщик deadline, который стремится сократить время ожидания выполнения каждой из операций ввода-вывода.<br />
<br />
Изменить планировщик диска sda можно при помощи следующей команды:<br />
<pre style="background-color: black; color: white;"># echo "deadline" > /sys/block/sda/queue/scheduler</pre>Для того, чтобы выбранный планировщик диска применялся при загрузке системы, можно поставить пакет sysfsutils:<br />
Оптимизация Linux при использовании SSD<br />
<pre style="background-color: black; color: white;"># apt-get install sysfsutils</pre>И прописать планировщик в файл /etc/sysfs.conf:<br />
<pre style="background-color: lightgrey;">block/sda/queue/scheduler = deadline</pre>Другой способ сделать изменения постоянными - создать файл /etc/udev/rules.d/60-ssd.rules со следующими правилами udev:<br />
<pre style="background-color: lightgrey;">ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="deadline"</pre>Это правило для всех не вращающихся дисков с именем sd* будет устанавливать планировщик deadline.<br />
<h3>Размер страницы</h3>Коэффициент усиления записи можно несколько уменьшить, если операционная система знает о размере страниц. В таком случае операционная система будет объединять изменения в смежных логических секторах, принадлежащих одной и той же странице, в одну операцию записи.<br />
<br />
Например, по данным SMART размер логического сектора диска равен 512 байтам, а размер страницы равен 4096 байт:<br />
<pre style="background-color: black; color: white;"># smartctl -i /dev/sda | grep Sector
Sector Sizes: 512 bytes logical, 4096 bytes physical</pre>Убедиться в том, что ядро операционной системы Linux знает о размере физического сектора, можно следующим образом:<br />
<pre style="background-color: black; color: white;"># cat /sys/block/sda/queue/physical_block_size
4096</pre><h3>Увеличение резерва страниц</h3>Т.к. каждая страница SSD имеет ограниченный ресурс перезаписи, контроллер SSD в общем случае не записывает изменившиеся данные в ту же страницу, а использует другую. Чтобы с точки зрения операционной системы записанные данные оставались доступными по тому же адресу, контроллер использует специальный каталог соответствия страниц, который отображает линейное адресное пространство диска в реальные страницы. Чем больше неиспользуемых страниц имеется в распоряжении контроллера, тем больше у него возможностей выбирать наименее изношенные страницы для очередной операции записи, тем больше возможностей для уменьшения количества операций очистки блоков.<br />
<br />
Часть общего объёма страниц диска закладывается в резерв. Напрмер, SSD объёмом 480 Гигабайт может иметь реальный объём 512 Гигабайт, а разница используется как раз для равномерного использования ресурса всех страниц.<br />
<br />
Кроме того, в файловой системе может иметься свободное место, не занятое никакими данными. Это свободное место на SSD можно приобщить к резерву. Для этого операционная система может сообщать диску о неиспользуемых ею страницах при помощи ATA-команды <a href="https://ru.wikipedia.org/wiki/TRIM">TRIM</a>. Для этого SSD должен поддерживать операцию TRIM, а файловая система должна поддерживать опцию монитрования discard.<br />
<br />
Проверить наличие поддержки TRIM в SSD можно при помощи утилиты hdparm:<br />
<pre style="background-color: black; color: white;"># hdparm -I /dev/sda | grep TRIM
* Data Set Management TRIM supported (limit 8 blocks)
* Deterministic read ZEROs after TRIM</pre>Вторая строчка означает, что секторы, над которыми произведена команда TRIM, при попытке чтения будут возвращать нули. Другим возможным режимом может быть «Deterministic read after TRIM», когда при чтении возвращаются не нули, а какая-то другая всегда одинаковая последовательность данных.<br />
<br />
Если на странице руководства man mount среди опций интересующей файловой системы имеется опция discard, то файловую систему можно перемонтировать с поддержкой этой опции.<br />
<br />
Сначала посмотрим, с какими опциями смонтирована файловая система:<br />
<pre style="background-color: black; color: white;"># findmnt /
TARGET SOURCE FSTYPE OPTIONS
/ /dev/md0 ext4 rw,relatime,errors=remount-ro,data=ordered</pre>Перемонтируем файловую систему, добавив к списку опций remount и discard:<br />
<pre style="background-color: black; color: white;"># mount -o remount,rw,relatime,errors=remount-ro,data=ordered,discard /</pre>Убеждаемся, что новая опция добавилась к текущему списку:<br />
<pre style="background-color: black; color: white;"># findmnt /
TARGET SOURCE FSTYPE OPTIONS
/ /dev/md0 ext4 rw,relatime,discard,errors=remount-ro,data=ordered</pre>Чтобы отключить опцию discard, можно повторить процедуру перемонтирования, указав вместо опции discard опцию nodiscard.<br />
<br />
Чтобы при перезагрузке операционная система монтировала файловую систему с опцией discard, нужно добавить её к списку опций монитрования в файле /etc/fstab. Например, строчка монтирования может выглядеть следующим образом:<br />
<pre style="background-color: lightgrey;">UUID=324f1a70-5229-4376-afbb-eb274c8e60aa / ext4 errors=remount-ro,discard 0 1</pre>Чтобы сообщить диску о неиспользуемых секторах, которые были освобождены до включения опции discard, или при отключенной опции discard, можно воспользоваться командой fstrim:<br />
<pre style="background-color: black; color: white;"># fstrim -v /
/: 146,6 MiB (153755648 bytes) trimmed</pre>Кроме увеличения ресурса диска, использование TRIM и discard может приводить к увеличению скорости операций записи и чтения. Т.к. у контроллера есть в распоряжении много очищенных блоков, ему не придётся тратить время на их очистку для записи новых данных. При этом операция очистки блока может выполняться в фоновом режиме, когда SSD не занят выполнением операций чтения или записи.<br />
<br />
Если на пути между файловой системой и диском имеются менеджер томов LVM или RAID-массив, то информация о неиспользуемых секторах может застревать в этих подсистемах и не доходить до SSD. Чтобы LVM сообщал о неиспользуемых секторах на нижележащий уровень, нужно в секции devices из файла конфигурации /etc/lvm/lvm.conf выставить следующую опцию:<br />
<pre style="background-color: lightgrey;">issue_discards = 1</pre>Убедиться в том, что TRIM корректно передаётся нижележащему хранилищу, можно при помощи команды:<br />
<pre style="background-color: black; color: white;"># lsblk -D</pre>Если TRIM поддерживается на всех уровнях, то в столбцах DISC-GRAN и DISC-MAX будут ненулевые значения:<br />
<pre style="background-color: black; color: white;">NAME DISC-ALN DISC-GRAN DISC-MAX DISC-ZERO
sda 0 4K 2G 0
└─sda1 0 4K 2G 0
└─md0 0 4K 2G 0
sdb 0 4K 2G 0
└─sdb1 0 4K 2G 0
└─md0 0 4K 2G 0</pre>Если же TRIM не используется, то можно увидеть такую картину:<br />
<pre style="background-color: black; color: white;">NAME DISC-ALN DISC-GRAN DISC-MAX DISC-ZERO
sda 0 0B 0B 0
├─sda1 0 0B 0B 0
│ └─md0 0 0B 0B 0
└─sda2 0 0B 0B 0
└─md1 0 0B 0B 0
├─vg0-mon--disk 0 0B 0B 0
└─vg0-mon--swap 0 0B 0B 0
sdb 0 0B 0B 0
├─sdb1 0 0B 0B 0
│ └─md0 0 0B 0B 0
└─sdb2 0 0B 0B 0
└─md1 0 0B 0B 0
├─vg0-mon--disk 0 0B 0B 0
└─vg0-mon--swap 0 0B 0B 0</pre>Возможны промежуточные варианты, когда файловая система отправляет операции TRIM на нижележащий уровень, но дальше эти операции не проходят.<br />
<h3>Файловые системы в оперативной памяти</h3>Т.к. интенсивный ввод-вывод снижает ресурс SSD, лучше избегать использовать SSD для хранения временных файлов. Например, раздел /tmp можно расположить в оперативной памяти. Временно это можно сделать при помощи такой команды:<br />
<pre style="background-color: black; color: white;"># mount -t tmpfs tmpfs -o relatime,nodev,nosuid,noexec,mode=1777 /tmp</pre>Если нужно ограничить максимальный размер файлов во временной файловой системе, к опциям noatime и nosuid можно добавить опцию size с указанием этого размера:<br />
<pre style="background-color: black; color: white;"># mount -t tmpfs tmpfs -o relatime,nodev,nosuid,noexec,mode=1777,size=1G /tmp</pre>Если временную файловую систему нужно монтировать автоматически при загрузке системы, нужно добавить в файл /etc/fstab соответствующую строчку:<br />
<pre style="background-color: lightgrey;">tmpfs /tmp tmpfs relatime,nodev,nosuid,noexec,mode=1777,size=1G 0 0</pre>Если файловая система уже не смонтирована, то теперь смонтировать её можно простой командоу:<br />
<pre style="background-color: black; color: white;"># mount /tmp</pre>Аналогичным образом можно монтировать другие временные файловые системы. Например:<br />
<pre style="background-color: black; color: white;">tmpfs /var/tftp tmpfs relatime,nodev,nosuid,noexec,uid=tftp,gid=tftp,mode=0760,size=32M 0 0</pre><h3>Использованные материалы</h3><ul><li><a href="https://ru.wikipedia.org/wiki/TRIM">TRIM</a></li>
<li><a href="https://habr.com/ru/post/203578/">Как обуздать усиление записи в SSD?</a></li>
<li><a href="https://habr.com/ru/post/205100/">Усиление записи. Часть 2</a></li>
<li><a href="http://vasilisc.com/trim-ssd">Включение TRIM на SSD</a></li>
<li><a href="https://blog.elcomsoft.com/ru/2018/12/zhizn-posle-trim-kak-vosstanovit-udalyonnyie-dannyie-s-nakopiteley-ssd/">Жизнь после Trim: как восстановить удалённые данные с накопителей SSD</a></li>
<li><a href="http://blog.neutrino.es/2013/howto-properly-activate-trim-for-your-ssd-on-linux-fstrim-lvm-and-dmcrypt/">How to properly activate TRIM for your SSD on Linux: fstrim, lvm and dm-crypt</a></li>
<li><a href="https://klondike-studio.ru/blog/raid-ssd-trim/">RAID+SSD TRIM мифы и реальность</a></li>
<li><a href="https://flashdba.com/2014/06/20/understanding-flash-blocks-pages-and-program-erases/">Understanding Flash: Blocks, Pages and Program / Erases</a></li>
<li><a href="https://flashdba.com/2014/09/17/understanding-flash-the-flash-translation-layer/">Understanding Flash: The Flash Translation Layer</a></li>
<li><a href="https://wiki.debian.org/SSDOptimization">SSDOptimization</a></li>
</ul>morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0tag:blogger.com,1999:blog-4428473564097379725.post-44466952945367205722020-08-30T13:00:00.001+05:002020-08-30T13:00:03.438+05:00Контроль параметров S.M.A.R.T. накопителей SSD через Zabbix<style>table { border-collapse: collapse; } th, td { border-style: solid; border-width: 1px; }</style><br />
В этой статье описывается доработка шаблона Zabbix из статьи <a href="https://vladimir-stupin.blogspot.com/2017/05/smart-zabbix.html">Контроль параметров S.M.A.R.T. жёстких дисков через Zabbix</a>. Имеющийся шаблон пригоден только для контроля жётских дисков, а в этой статье я опишу доработки, которые позволят контролировать как состояние жёстких дисков, так и накопителей SSD Micron 5200 MAX. Этот шаблон может подойти и для других моделей накопителей, если они поддерживают необходимые атрибуты S.M.A.R.T., но на других накопителях не тестировался.<br />
<br />
Попутно в шаблон были внесены доработки, аналогичные описанным в статье <a href="https://vladimir-stupin.blogspot.com/2019/10/zabbix-smart-raid.html">Контроль в Zabbix параметров SMART дисков, подключенных к аппаратному RAID-массиву</a>. Вместо общего порога для всех дисков по количесвту перемещённых секторов и секторов, ожидающих перемещения, на этот раз в шаблоне предусмотрена индивидуальная настройка порогов для каждого из дисков.<br />
<h3>Атрибуты S.M.A.R.T. и коэффициент усиления записи</h3>Документация на атрибуты S.M.A.R.T. доступна по ссылке <a href="https://www.micron.com/-/media/client/global/documents/products/technical-note/solid-state-storage/tnfd22_client_ssd_smart_attributes.pdf">TN-FD-22: Client SATA SSD SMART Attribute Reference</a><br />
<br />
С точки зрения контроля состояния накопителей SSD наиболее интересны следующие атрибуты:<br />
<table><th>Идентификатор атрибута</th><th>Название атрибута</th><th>Пояснение</th>
<tr><td>202</td><td>Percent_Lifetime_Used</td><td>Процент использования ресурса диска (100% - полностью изношен)</td></tr>
<tr><td>246</td><td>Total_Host_Sector_Write</td><td>Количество записанных секторов</td></tr>
<tr><td>247</td><td>Host_Program_Page_Count</td><td>Количество записанных страниц</td></tr>
<tr><td>248</td><td>Bckgnd_Program_Page_Cnt</td><td>Количество страниц, записанных контроллером</td></tr>
</table>На твердотельных накопителях единицей чтения и записи является страница, размер которой обычно больше логического размера сектора диска. По мере повторных перезаписей страница изнашивается и запись на неё становится всё менее надёжной. Количество гарантированных производителем успешных перезаписей страницы называется ресурсом. Чтобы снизить вероятность потери информации, контроллер накопителя ведёт учёт количества операций перезаписи каждой страницы. При любом изменении информации в логическом секторе диска контроллер выбирает из всех имеющихся свободных страниц наименее изношенные и копирует данные на неё. Контроллер имеет возможность посчитать изношенность всего накопителя в целом и отражает это значение в атрибуте Percent_Lifetime_Used.<br />
<br />
Также производитель регламентирует в технических характеристиках накопителя гарантированный объём записанных на диск данных - TBW, Total Bytes Written. Например, исходя из технических характеристик накопителей, указанных на странице <a href="https://www.micron.com/solutions/technical-briefs/micron-5200-series-of-sata-ssds">Micron 5200 series of SATA SSDs</a>, на SSD Micron модели 5200 MAX 480Gb можно записать 4.38 петабайт данных. Контроллер накопителя ведёт учёт количества записанных 512-байтных секторов в атрибуте Total_Host_Sector_Write.<br />
<br />
Страницы группируются в блоки. Для того, чтобы записать в страницу новое содержимое, необходимо выполнить операцию очистки всего блока. Из-за этого перезапись одного логического сектора может приводить к перезаписи в несколько раз большего объёма данных на SSD. Отношение реально записанного объёма данных к объёму, который просила записать операционная система, называется коэффициентом усиления записи (Write Amplification Factor). Посчитать его можно воспользовавшись значениями атрибутов S.M.A.R.T. Host_Program_Page_Count и Bckgnd_Program_Page_Cnt по следующей формуле:<br />
<pre style="background-color: lightgrey;">WAF = (Host_Program_Page_Count + Bckgnd_Program_Page_Cnt) / Host_Program_Page_Count</pre><h3>Доработка конфигурации агента Zabbix</h3>Во-первых, нам полезно определять тип накопителя: жёсткий диск или твердотельный накопитель.<br />
<br />
Для этого я воспользовался полем Rotation Rate, в котором содержится частота вращения диска в оборотах в минуту. Если это поле не содержит числа, то будем считать накопитель твердотельным. В таком случае частота вращения диска равняется нулю. Добавим в файл конфигурации Zabbix-агента /etc/zabbix/zabbix_agentd.conf «пользовательский параметр» для определения частоты вращения диска:<br />
<pre style="background-color: lightgrey;">UserParameter=smart.rpm[*],/usr/bin/sudo /usr/sbin/smartctl -i $1 2>&1 | /usr/bin/awk -F: '$$1 ~ /^Rotation Rate$/ { match($$2, /[0-9]+/); if (RSTART > 0) { print substr($$2, RSTART, RLENGTH); } else { print 0 } }'</pre>Для контроля процента использованного ресурса, объёма записанных данных и коэффициента усиления записи добавим в конфигурацию Zabbix-агента /etc/zabbix/zabbix_agentd.conf ещё три «пользовательских параметра»:<br />
<pre style="background-color: lightgrey;">UserParameter=smart.ssd.used[*],/usr/bin/sudo /usr/sbin/smartctl -A $1 2>&1 | /usr/bin/awk 'BEGIN { p = 0; } /^202 / { p = $$10; } END { print p; }'
UserParameter=smart.ssd.written[*],/usr/bin/sudo /usr/sbin/smartctl -A $1 2>&1 | /usr/bin/awk 'BEGIN { w = 0; } /^246 / { w = $$10 * 512; } END { print w; }'
UserParameter=smart.ssd.waf[*],/usr/bin/sudo /usr/sbin/smartctl -A $1 2>&1 | /usr/bin/awk 'BEGIN { hw = 1; cw = 0; } /^247 / { hw = $$10; } /^248 / { cw = $$10; } END { print (hw + cw) / hw; }'</pre>После внесения изменений в конфигурацию Zabbix-агента, не забудьте его перезапустить:<br />
<pre style="background-color: black; color: white;"># systemctl restart zabbix-agent</pre><h3>Доработка шаблонов для Zabbix</h3>Я обновил два шаблона, описанных ранее, для контроля параметров S.M.A.R.T. твердотельных накопителей. Взять их можно по прежним ссылкам:<br />
<ul><li><a href="http://stupin.su/files/zabbix/Template_App_SMART.xml">Template_App_SMART.xml</a> - шаблон с элементами данных типа «Zabbix-агент»,</li>
<li><a href="http://stupin.su/files/zabbix/Template_App_SMART_Active.xml">Template_App_SMART_Active.xml</a> - шаблон с элементами данных типа «Zabbix-агент (активный)».</li>
</ul>В обоих шаблонах имеется элемент данных для низкоуровневого обнаружения, который находит все имеющиеся в системе диски, поддерживающие S.M.A.R.T.:<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh7OQ2NxlfPXiT-1eT6EioRbADLBazYmaRLG_1bMpPtw758xQHIMAZuBfhWxzzpZH9hRI7DofG5mCGdIvP89RBHkVmAAezfTCAyoUfUlTd_sUqoco_JgfHHKbSjOd0__JuCZ_rurEG5STJy/s1600/smart2_lld.png.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh7OQ2NxlfPXiT-1eT6EioRbADLBazYmaRLG_1bMpPtw758xQHIMAZuBfhWxzzpZH9hRI7DofG5mCGdIvP89RBHkVmAAezfTCAyoUfUlTd_sUqoco_JgfHHKbSjOd0__JuCZ_rurEG5STJy/s640/smart2_lld.png.png" width="640" height="126" data-original-width="1139" data-original-height="225" /></a><br />
Есть прототипы элементов данных, с помощью которых контролируется: статус здоровья диска, количество перемещённых секторов, секторов, ожидающих перемещения, температура жёсткого диска. Значения этих данных для каждого из жёстких дисков снимаются раз в 10 минут. Раз в час для каждого жёсткого диска запрашивается модель и серийный номер - они могут пригодиться, когда понадобится заменить один из жёстких дисков:<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiKH8eSgKrfWQlzYXnHwiYGC1Bvd6WU23IONM1zqDemiRPf7p6hjm0wOOqGrUnMDt1AJfWSL30QqJaRNl7DjcrbeIkzE9mRaiDvJSCgh7jw6x0IH_4p7Bsdcz47hTGum8PxAGdKnGprhEuz/s1600/smart2_itemprototypes.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiKH8eSgKrfWQlzYXnHwiYGC1Bvd6WU23IONM1zqDemiRPf7p6hjm0wOOqGrUnMDt1AJfWSL30QqJaRNl7DjcrbeIkzE9mRaiDvJSCgh7jw6x0IH_4p7Bsdcz47hTGum8PxAGdKnGprhEuz/s640/smart2_itemprototypes.png" width="640" height="325" data-original-width="1269" data-original-height="644" /></a><br />
Имеется три прототипа триггеров, который будут созданы для каждого обнаруженного жёсткого диска. Самый главный триггер срабатывает в том случае, когда S.M.A.R.T. явным образом сообщает о неисправности диска. Два других триггера срабатывают при превышении лимита неисправных секторов или секторов, ожидающих перемещения:<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgonWAbu-KCNPAvaCCk1n6T9Y3FpUcQc0-c93hkHfUHP9By7resaGqHF6xcI2t39iPZRYoI6x8x7DWOBbU1hNR-Q80kDSbnNgzppSq-chnSB8BuzYfgkBqOZZ8HPEYXoB6COSCjpUelgked/s1600/smart2_triggerprototypes.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgonWAbu-KCNPAvaCCk1n6T9Y3FpUcQc0-c93hkHfUHP9By7resaGqHF6xcI2t39iPZRYoI6x8x7DWOBbU1hNR-Q80kDSbnNgzppSq-chnSB8BuzYfgkBqOZZ8HPEYXoB6COSCjpUelgked/s640/smart2_triggerprototypes.png" width="640" height="169" data-original-width="1254" data-original-height="331" /></a><br />
Лимиты для двух последних триггеров можно задать через соответствующие макросы - {$SMART_REALLOCATED_LIMIT} и {$SMART_PENDING_LIMIT}:<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgMWm_uQ0XxpKpPBZ3pFYhnjkVahs6yFbUjFU57ziyN-9HZTmu9RsG51fztzOLISBm2fHG79RMQ3Ntn-ms3ZF8kfzNKBkhjXBxKqqM62AgibfH_45bEIsxj-ScYYl0177MfPi5TVsoMD5OV/s1600/smart_macros.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgMWm_uQ0XxpKpPBZ3pFYhnjkVahs6yFbUjFU57ziyN-9HZTmu9RsG51fztzOLISBm2fHG79RMQ3Ntn-ms3ZF8kfzNKBkhjXBxKqqM62AgibfH_45bEIsxj-ScYYl0177MfPi5TVsoMD5OV/s640/smart_macros.png" width="640" height="153" data-original-width="1351" data-original-height="322" /></a><br />
На картинке заданы нулевые лимиты, поэтому триггеры будут срабатывать при появлении хотя бы одного подозрительного сектора на диске. Если вы посчитали, что проблемных секторов не так уж и много, то можно задать новые значения макросов индивидуально в самом наблюдаемом узле Zabbix.<br />
<br />
Как можно заметить, в выражениях триггеров эти макросы используются в виде {$SMART_REALLOCATED_LIMIT:"{#SMART}"}. Макросы такого вида описаны в <a href="https://www.zabbix.com/documentation/3.4/ru/manual/config/macros/usermacros#%D0%BA%D0%BE%D0%BD%D1%82%D0%B5%D0%BA%D1%81%D1%82_%D0%BF%D0%BE%D0%BB%D1%8C%D0%B7%D0%BE%D0%B2%D0%B0%D1%82%D0%B5%D0%BB%D1%8C%D1%81%D0%BA%D0%B8%D1%85_%D0%BC%D0%B0%D0%BA%D1%80%D0%BE%D1%81%D0%BE%D0%B2">Руководстве по Zabbix, 7 Настройка, 10 Макросы, 2 Пользовательские макросы, Контекст пользовательских макросов</a>.<br />
<br />
При срабатывании триггера вида «/dev/sda: Количество перемещённых секторов 13 > 0» можно переопределить значение макроса для конкретного диска. Чтобы погасить этот триггер, на уровне узла можно определить макрос {$SMART_REALLOCATED_LIMIT:"/dev/sda"} со значением 13. Порог срабатывания триггеров на других жёстких дисках останется прежним - будет использоваться значение по умолчанию, взятое из шаблона.<br />
<br />
Наконец, снимаемые данные в случае упомянутых твердотельных накопителей Micron SSD 5200 MAX выглядят следующим образом:<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiJBMiyfv0xfjv-T-eUsbYZKkJM8-vykkJiGpGJdxV9gz21vWG7CIDioSvZqKUGUcyijlNmLWTuQiQ3PAWAhj77kZaicTTWSrqolV6aEICmVRISDTCInDr8cpSFjReCOx8UqNgOO1Fr5t-h/s1600/smart2_lastdata.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiJBMiyfv0xfjv-T-eUsbYZKkJM8-vykkJiGpGJdxV9gz21vWG7CIDioSvZqKUGUcyijlNmLWTuQiQ3PAWAhj77kZaicTTWSrqolV6aEICmVRISDTCInDr8cpSFjReCOx8UqNgOO1Fr5t-h/s640/smart2_lastdata.png" width="640" height="434" data-original-width="986" data-original-height="668" /></a>morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0tag:blogger.com,1999:blog-4428473564097379725.post-87802399287980258092020-08-23T13:00:00.001+05:002020-08-23T13:00:05.905+05:00Обновление прошивок на SSD Micron 5200 MAXПосле перевода таблиц истории и тенденций Zabbix, хранившихся в MySQL, с движка InnoDB на TokuDB, базы данных на всех серверах Zabbix ужались примерно в 10 раз. Проблемы с нехваткой места на дисках решены и больше не приходится урезать глубину хранения истории или тенденций.<br />
<br />
Т.к. базы данных стали значительно меньше, появился смысл использовать накопители SSD меньшего объёма с программным RAID вместо SAS-дисков и аппаратных RAID-контроллеров. При сравнимой цене накопителей SSD и SAS-дисков, SSD позволяют обеспечить более высокую скорость работы дисковой подсистемы.<br />
<br />
После консультации с более опытным коллегой (привет, Рамиль), уже имеющим опыт использования накопителей SSD, выбор остановился на накопителе SSD 480GB SATA 2.5« 5200 MAX <mtfddak480tdn> Micron (MTFDDAK480TDN-1A T1ZABYY).<br />
<br />
Накопители SSD содержат микросхемы флеш-памяти, в которых читать или записывать можно только страницу целиком. При этом записывать данные можно только в пустую страницу. Страницы группируются в блоки и очистить можно только блок целиком. У каждой страницы количество перезаписей ограничено, по достижении которого данные могут начать записываться с ошибками. Если страницу долгое время не читать, то данные на ней могут исказиться и пропасть. Контроллеру флеш-памяти в SSD приходится учитывать все эти ограничения. Для каждой страницы он должен вести учёт, содержит ли страница данные или она пустая, сколько раз её перезаписывали, как давно её последний раз читали. Прежде чем очистить блок, контроллеру нужно скопировать страницы с данными из этого блока в пустые страницы другого блока. Записывать данные лучше всего в те страницы, которые имеют меньшее количество перезаписей. При каждом перемещении данных из одной страницы в другую контроллеру нужно отметить соответствие линейного адреса страницы, которым манипулирует контроллер SATA, реальному положению страницы во флеш-памяти. Страницы, которые давно не читались, нужно периодически перечитывать, чтобы информация на них не пропала.<br />
<br />
Вот почему версия прошивки SSD может оказывать значительно большее влияние на срок службы и надёжность, чем версия прошивки обычного жёсткого диска. Именно поэтому стоит обновить прошивку SSD до самой свежей версии, прежде чем вводить сервер в эксплуатацию.<br />
<br />
Эта инструкция по обновлению прошивок SSD была написана по просьбе менее опытного коллеги (привет, Кирилл), которому придётся продолжить работу, начатую мной.<br />
<h3>Сайт производителя</h3>На сайте производителя <a href="https://www.micron.com/">www.micron.com</a> доступны утилита, прошивки и документация. Для их получения нужно зарегистрироваться на сайте. Список субъектов федерации на сайте довольно забавен. В частности, в выпадающем списке нет Башкирии, зато есть «Пермякия», которую я и выбрал по сочетанию сходства с реальным названием и близости географического расположения.<br />
<br />
На странице <a href="https://www.micron.com/products/solid-state-drives/enterprise-ssd-downloads">Enterprise SSD Downloads</a> в разделе msecli Software for Linux Systems можно найти установщик фирменной утилиты msecli для Linux. В этом разделе есть ссылка на скачивание <a href="https://www.micron.com/-/media/client/global/documents/products/software/storage-executive-software/msecli/msecli_linux.run">Download</a>.<br />
<br />
На той же странице <a href="https://www.micron.com/products/solid-state-drives/enterprise-ssd-downloads">Enterprise SSD Downloads</a> в разделе Storage Executive Command Line Interface можно найти документацию на фирменную утилиту msecli. В этом разделе есть ссылка на скачивание <a href="https://www.micron.com/-/media/client/global/documents/products/software/storage-executive-software/msecli/storage_executive_cli_user_guide.pdf">Download</a>.<br />
<br />
На странице <a href="https://www.micron.com/support/software-and-drivers">Software and Drivers</a> в разделе 5200 D1MU020/D1MU420/D1MU520/D1MU820 Storage Executive Firmware Update можно найти свежие прошивки для дисков. В нём есть ссылка на скачивание <a href="https://www.micron.com/-/media/client/global/documents/products/software/5200-firmware/5200_d1mu_020_420_520_820_fwbin.zip">Download</a>.<br />
<h3>Подготовка загрузочной флешки для обновления прошивок</h3>На странице <a href="https://clonezilla.org/liveusb.php">Clonezilla Live on USB flash drive or USB hard drive</a> описана процедура подготовки загрузочной флешки с Clonezilla. На странице <a href="https://clonezilla.org/downloads.php">Clonezilla Live Download</a> можно скачать zip-архив для распаковки на флешку. Я выбрал стабильную версию на основе Debian и попал на страницу <a href="https://clonezilla.org/downloads/download.php?branch=stable">Downloads</a>. В меню выбрал архитектуру amd64, формат zip, репозитории auto и нажал кнопку Download.<br />
<br />
После скачивания zip-архива нужно разметить флешку, распаковать на неё этот архив и установить загрузчик.<br />
<br />
Не вставляя флешку, определяем имена имеющихся в системе дисков:<br />
<pre style="background-color: black; color: white;">~# ls /dev/sd*
/dev/sda /dev/sda1 /dev/sda2 /dev/sdb /dev/sdb1 /dev/sdb2</pre>Теперь вставляем флешку и повторяем операцию:<br />
<pre style="background-color: black; color: white;">~# ls /dev/sd*
/dev/sda /dev/sda1 /dev/sda2 /dev/sdb /dev/sdb1 /dev/sdb2 /dev/sdc /dev/sdc1</pre>Видно, что в системе появилось новое блочное устройство /dev/sdc, на котором определился один раздел.<br />
<br />
Запустим утилиту fdisk для изменения разделов на блочном устройстве /dev/sdc:<br />
<pre style="background-color: black; color: white;">~# fdisk /dev/sdc
Welcome to fdisk (util-linux 2.29.2).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.</pre>Создадим на диске пустую таблицу разделов типа DOS командой o:<br />
<pre style="background-color: black; color: white;">Command (m for help): o
Created a new DOS disklabel with disk identifier 0x2806276a.</pre>Посмотрим на список имеющихся разделов, введя команду p:<br />
<pre style="background-color: black; color: white;">Command (m for help): p
Disk /dev/sdc: 14,4 GiB, 15502147584 bytes, 30277632 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x2806276a</pre>Создаём новый первичнй раздел от начала до конца свободного места на флешке. Вводим команду создания нового раздела n:<br />
<pre style="background-color: black; color: white;">Command (m for help): n
Partition type
p primary (0 primary, 0 extended, 4 free)
e extended (container for logical partitions)</pre>Утилита спрашивает, раздел какого типа нужно создать: первичный или расширенный. В качестве загрузочного раздела можно использовать только первичный. Расширенный раздел может быть только один. Максимальное количество первичных и расширенных разделов в сумме может быть не больше 4. Внутри расширенного раздела можно создавать практически не ограниченное количество логических разделов, но ни один из них не может быть загрузочным. Т.к. нам нужно загрузить Clonezilla с этой флешки, выбираем первичный раздел, нажимая букву p:<br />
<pre style="background-color: black; color: white;">Select (default p): p</pre>Выбираем номер раздела, по умолчанию предлагается создать раздел с номером 1. Соглашаемся на предложение нажатием Enter:<br />
<pre style="background-color: black; color: white;">Partition number (1-4, default 1): </pre>Выбираем сектор, начиная с которого должен начаться раздел. Соглашаемся с предложением по умолчанию начать раздел с сектора 2048 нажатием Enter:<br />
<pre style="background-color: black; color: white;">First sector (2048-30277631, default 2048): </pre>Выбираем сектор, которым должен заканчиваться раздел. Можно ввести номер сектора, а можно указать размер раздела в секторах, килобайтах, мегабайтах, гигабайтах, терабайтах или петабайтах. По умолчанию предлагается последний сектор из неразмеченного пространства.<br />
<br />
Clonezilla требует создавать раздел размером не менее 200 мегабайт. На самом деле это ложь и раздела размером 200 мегабайт не хватит, т.к. даже zip-архив больше 200 мегабайт. Кроме того, нужно создать раздел с запасом для размещения утилиты для msecli_Linux.run и прошивок для SSD. Подойдёт раздел размером 512 мегабайт, но стоит сделать его меньше на зарезервированные в начале 2048 секторов. Поступим проще, просто посчитаем номер последнего сектора: 512*1024*1024/512=1048576. Введём получившееся число:<br />
<pre style="background-color: black; color: white;">Last sector, +sectors or +size{K,M,G,T,P} (2048-30277631, default 30277631): 1048576
Created a new partition 1 of type 'Linux' and of size 511 MiB.</pre>Раздел нужного размера создан, но нужно поменять его тип. Для этого введём команду t:<br />
<pre style="background-color: black; color: white;">Command (m for help): t
Selected partition 1</pre>Программа не спрашивает у нас номер раздела, т.к. он всего один, а сразу предлагает ввести номер типа раздела или ввести команду L, чтобы увидеть список всех возможных идентификаторов типов разделов. После ввода L получаем такой список:<br />
<pre style="background-color: black; color: white;">Partition type (type L to list all types): L
0 Empty 24 NEC DOS 81 Minix / old Lin bf Solaris
1 FAT12 27 Hidden NTFS Win 82 Linux swap / So c1 DRDOS/sec (FAT-
2 XENIX root 39 Plan 9 83 Linux c4 DRDOS/sec (FAT-
3 XENIX usr 3c PartitionMagic 84 OS/2 hidden or c6 DRDOS/sec (FAT-
4 FAT16 <32m 40 Venix 80286 85 Linux extended c7 Syrinx
5 Extended 41 PPC PReP Boot 86 NTFS volume set da Non-FS data
6 FAT16 42 SFS 87 NTFS volume set db CP/M / CTOS / .
7 HPFS/NTFS/exFAT 4d QNX4.x 88 Linux plaintext de Dell Utility
8 AIX 4e QNX4.x 2nd part 8e Linux LVM df BootIt
9 AIX bootable 4f QNX4.x 3rd part 93 Amoeba e1 DOS access
a OS/2 Boot Manag 50 OnTrack DM 94 Amoeba BBT e3 DOS R/O
b W95 FAT32 51 OnTrack DM6 Aux 9f BSD/OS e4 SpeedStor
c W95 FAT32 (LBA) 52 CP/M a0 IBM Thinkpad hi ea Rufus alignment
e W95 FAT16 (LBA) 53 OnTrack DM6 Aux a5 FreeBSD eb BeOS fs
f W95 Ext'd (LBA) 54 OnTrackDM6 a6 OpenBSD ee GPT
10 OPUS 55 EZ-Drive a7 NeXTSTEP ef EFI (FAT-12/16/
11 Hidden FAT12 56 Golden Bow a8 Darwin UFS f0 Linux/PA-RISC b
12 Compaq diagnost 5c Priam Edisk a9 NetBSD f1 SpeedStor
14 Hidden FAT16 <3 61 SpeedStor ab Darwin boot f4 SpeedStor
16 Hidden FAT16 63 GNU HURD or Sys af HFS / HFS+ f2 DOS secondary
17 Hidden HPFS/NTF 64 Novell Netware b7 BSDI fs fb VMware VMFS
18 AST SmartSleep 65 Novell Netware b8 BSDI swap fc VMware VMKCORE
1b Hidden W95 FAT3 70 DiskSecure Mult bb Boot Wizard hid fd Linux raid auto
1c Hidden W95 FAT3 75 PC/IX bc Acronis FAT32 L fe LANstep
1e Hidden W95 FAT1 80 Old Minix be Solaris boot ff BBT</pre>
Нам нужен раздел для размещения файловой системы FAT 32, флешка имеет линейную адресацию секторов, поэтому наиболее подходящим выбором будет вариант «W95 FAT32 (LBA)». Вводим тип c и нажимаем Enter:
<pre style="background-color: black; color: white;">Partition type (type L to list all types): c
Changed type of partition 'Linux' to 'W95 FAT32 (LBA)'.</pre>Осталось пометить раздел как загрузочный. Вводим команду a:
<pre style="background-color: black; color: white;">Command (m for help): a
Selected partition 1
The bootable flag on partition 1 is enabled now.</pre>Раздел 1 помечен как загрузочный.
Посмотрим на список разделов снова. Для этого введём команду p:
<pre style="background-color: black; color: white;">Command (m for help): p
Disk /dev/sdc: 14,4 GiB, 15502147584 bytes, 30277632 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x2806276a
Device Boot Start End Sectors Size Id Type
/dev/sdc1 * 2048 1048576 1046529 511M c W95 FAT32 (LBA)</pre>Всё верно, вводим команду w для записи изменений на флешку:
<pre style="background-color: black; color: white;">Command (m for help): w
The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks.</pre>fdisk сообщает, что таблица разделов записана.
Теперь можно создать пустую файловую систему FAT 32 на флешке в разделе 1. Для этого воспользуемся такой командой:
<pre style="background-color: black; color: white;">~# mkfs.vfat -F 32 /dev/sdc1
mkfs.fat 4.1 (2017-01-24)</pre>Готово. Теперь нужно смонтировать раздел в какой-нибудь пустой каталог. У меня для подобных целей имеется каталог /mnt:
<pre style="background-color: black; color: white;">~# mount /dev/sdc1 /mnt/</pre>Теперь нужно распаковать содержимое zip-архива с Clonezilla в этот каталог:
<pre style="background-color: black; color: white;">~# cd /mnt
/mnt# unzip /home/stupin/Downloads/clonezilla-live-2.6.6-15-amd64.zip</pre>Полный вывод второй команды не привожу, т.к. в нём перечисляются все извлечённые файлы, которых много.
Теперь переходим в каталог utils/linux и запускаем команду установки загрузчика:
<pre style="background-color: black; color: white;">/mnt# cd utils/linux/
/mnt/utils/linux# bash makeboot.sh /dev/sdc1
This command will install MBR and syslinux/extlinux bootloader on /dev/sdc
Clonezilla files are assumed to be on /dev/sdc1
--------------------------------------------
Machine: Unknown product name:
Model: Kingston DataTraveler 3.0 (scsi)
Disk /dev/sdc: 15.5GB
Sector size (logical/physical): 512B/512B
Partition Table: msdos
Disk Flags:
Number Start End Size Type File system Flags
1 1049kB 537MB 536MB primary fat32 boot, lba
--------------------------------------------
Are you sure you want to continue?</pre>Установщик загрузчика показал нам информацию о разделе, загрузчик для которого мы хотим установить и спрашивает у нас подтверждения. Подтверждаем выбор буквой y:
<pre style="background-color: black; color: white;">[y/n] y
OK! Let's do it!
--------------------------------------------
File system of /dev/sdc1: vfat
--------------------------------------------
Do you want to install MBR on /dev/sdc on this machine "Unknown product name" ?</pre>Установщик спрашивает, хотим ли мы устновить главную загрузочную запись на диск /dev/sdc. Соглашаемся нажатием y:
<pre style="background-color: black; color: white;">[y/n] y
OK! Let's do it!
Running: cat "/mnt/utils/mbr/mbr.bin" > /dev/sdc
--------------------------------------------
Do you want to install the syslinux boot files on /dev/sdc1 on this machine "Unknown product name" ?</pre>Установщик спрашивает, хотим ли мы установить файлы загрузчика syslinux в раздел /dev/sdc1. Подтверждаем наше желание нажатием y:
<pre style="background-color: black; color: white;">[y/n] y
OK! Let's do it!
A filesystem supporting Unix file mode for syslinux is required. Copying syslinux to /tmp/linux_tmp.fZmDyo
'/mnt/utils/linux/x64/syslinux' -> '/tmp/linux_tmp.fZmDyo/syslinux'
Running: "/tmp/linux_tmp.fZmDyo/syslinux" -d syslinux -f -i "/dev/sdc1"
//NOTE// If your USB flash drive fails to boot (maybe buggy BIOS), try to use "syslinux -d syslinux -fs /dev/sdc1", i.e. running with "-fs".
syslinux ran successfully in installation mode.
Done!</pre>Флешка готова. Осталось поместить на неё утилиту и файлы с прошивок. Перейдём в корневой каталог флешки, создадим каталог ssd, поместим в него установщик утилиты msecli и распакованные прошивки:
<pre style="background-color: black; color: white;">/mnt/utils/linux# cd ../..
/mnt# mkdir ssd
/mnt# cd ssd
/mnt/ssd# cp /home/stupin/Downloads/msecli_Linux.run .
/mnt/ssd# unzip /home/stupin/Downloads/5200_D1MU_020_420_520_820_fwbin.zip
Archive: /home/stupin/Downloads/5200_D1MU_020_420_520_820_fwbin.zip
inflating: D1MU020/1.bin
inflating: D1MU420/1.bin
inflating: D1MU520/1.bin
inflating: D1MU820/1.bin
inflating: firmware.properties</pre>Выставим права исполнимости на установщик утилиты msecli_Linux.run:
<pre style="background-color: black; color: white;">/mnt/ssd# chmod +x msecli_Linux.run</pre>Теперь можно выйти из каталога, в который смонтирована флешка, отмонтировать его и на всякий случай снять с флешки образ:
<pre style="background-color: black; color: white;">/mnt/ssd# cd
~# umount /mnt
~# dd bs=1M count=512 if=/dev/sdc of=/home/stupin/Downloads/clonezilla-live-2.6.6-15-amd64.img</pre>Т.к. образ снят под пользователем root, не забываем поменять владельца файла, чтобы потом можно было без проблем удалить его или переместить в другое место:
<pre style="background-color: black; color: white;">~# chown stupin:stupin /home/stupin/Downloads/clonezilla-live-2.6.6-15-amd64.img</pre>В файле /home/stupin/Downloads/clonezilla-live-2.6.6-15-amd64.img будет сохранён образ, который в дальнейшем можно будет записать на любую флешку размером не менее 512 мегабайт. Например, для записи образа на флешку /dev/sdc можно будет воспользоваться такой командой:
<pre style="background-color: black; color: white;">~# dd if=/home/stupin/Downloads/clonezilla-live-2.6.6-15-amd64.img of=/dev/sdc</pre><h3>Обновление прошивок</h3>После загрузки с флешки нужно выбрать в меню загрузку в командную строку. Прежде чем приступить непосредственно к обновлению прошивок, нужно устанавить в систему утилиту msecli. Запускаем установщик:
<pre style="background-color: black; color: white;"># /usr/lib/live/mount/medium/ssd/msecli_Linux.run</pre>Читаем лицензию, принимаем, соглашаемся на установку утилиты.
Узнаём текущие версии прошивок:
<pre style="background-color: black; color: white;"># msecli -F
Device Name FW-Rev
/dev/sda D1MU004
/dev/sdb D1MU004
/dev/sdс D1MU004
Firmware version retrieved successfully
CMD_STATUS : Success
STATUS_CODE : 0
Copyright (C) 2019 Micron Technology, Inc.</pre>Как видно, на трёх имеющихся в системе накопителях SSD определилась прошивка версии D1MU004. При подготовке флешки содержимое zip-архива с прошивками мы распаковали в каталог /ssd. В файле firmware.properties можно найти информацию о совместимости прошивок с моделями накопителей. Для Micron 5200MAX подходит прошивка, находящаяся в каталоге D1MU020.
Обновляем прошивки до свежих версий:
<pre style="background-color: black; color: white;"># msecli -U -i D1MU020 -n /dev/sda
This will update the 5200MAX drive in the system
Are you sure you want to continue(Y|N):Y
Updating firmware on drive /dev/sda (Serial No. XXXXXXXXXXXX)
..................
Device Name : /dev/sda
Firmware Update on /dev/sda Succeded!
CMD_STATUS : Success
STATUS_CODE : 0
Copyright (C) 2019 Micron Technology, Inc.
# msecli -U -i D1MU020 -n /dev/sdb
This will update the 5200MAX drive in the system
Are you sure you want to continue(Y|N):Y
Updating firmware on drive /dev/sdb (Serial No. XXXXXXXXXXXX)
..................
Device Name : /dev/sdb
Firmware Update on /dev/sdb Succeded!
CMD_STATUS : Success
STATUS_CODE : 0
Copyright (C) 2019 Micron Technology, Inc.
# msecli -U -i D1MU020 -n /dev/sdс
This will update the 5200MAX drive in the system
Are you sure you want to continue(Y|N):Y
Updating firmware on drive /dev/sdc (Serial No. XXXXXXXXXXXX)
..................
Device Name : /dev/sdc
Firmware Update on /dev/sdc Succeded!
CMD_STATUS : Success
STATUS_CODE : 0
Copyright (C) 2019 Micron Technology, Inc.</pre>morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0tag:blogger.com,1999:blog-4428473564097379725.post-2173117400314346712020-08-16T13:00:00.000+05:002020-08-16T13:00:05.129+05:00История и тенденции Zabbix в TokuDBОдной из самых тяжело решаемых проблем, с которой сталкиваются системные администраторы, использующие систему мониторинга Zabbix, является проблема недостаточной производительности дисковой подсистемы.<br />
<br />
Первая рекомендация, которой стоит попробовать воспользоваться - это, конечно-же, удаление ненужных элементов данных, пересмотр периодичности их съёма в пользу более длительных интервалов, уменьшение длительности хранения данных. Чем меньше данных в таблицах истории, тем быстрее происходит работа с данными. Ускоряется поиск, т.к. становятся короче индексы, ускоряется чтение, т.к. в выборку для отображения на графике попадает меньше данных, запись данных тоже ускоряется, т.к. чем меньше данных в таблице, тем быстрее обновляются индексы. Кроме того, если все часто требуемые данные будут умещаться в оперативной памяти СУБД, работа с данными существенно ускорится.<br />
<br />
Если первая рекомендация не помогает, тогда нужно приступать к чуть более сложным методам методам: нужно заняться оптимизацией производительности СУБД и сервера.<br />
<br />
В случае с MySQL первым делом нужно убедиться, что база данных не находится в одном файле и, при необходимости, разнести таблицы по разным файлам: сделать полную резервную копию, удалить базы данных, включить innodb_file_per_table=YES, перезапустить MySQL, восстановить базы данных из резервных копий.<br />
<br />
Другой важный шаг: нужно убедиться, что основной буфер СУБД, размер которого настраивается через innodb_buffer_pool_size, имеет максимально доступный объём. Чем больше объём этого буфера, тем больше «горячих», часто требуемых данных, может в нём уместиться. Идеально, если вся СУБД умещается в оперативной памяти целиком. На практике, однако, это редко достижимо, т.к. таблицы истории и тенденций в базе данных Zabbix могут достигать сотен гигабайт. В любом случае, если есть возможность, лучше увеличить объём оперативной памяти на сервере с СУБД.<br />
<br />
Также стоит обратить внимание на размеры журналов innodb_log_file_size: Zabbix пишет много данных и размер этих файлов должен соответствовать объёму данных, которые записываются системой в секунду (лимит для этой опции - 2 гигабайта). Оборотной стороной больших журналов является более длительный запуск сервера MySQL.<br />
<br />
Когда выполнены предыдущие рекомендации - на контроле есть только самое необходимое, данные снимаются с разумными интервалами времени, произведена оптимизация настроек - следующим этапом обычно идёт отключение HouseKeeper'а и секционирование таблиц истории и тенденций. Понять, о том что настало время отключать HouseKeeper, можно обратившись ко внутреннему мониторингу Zabbix. Если на графиках процесс HouseKeeper почти постоянно используется на 100%, а увеличение настроек HouseKeepingFrequency и HouseKeeperDelete не приводят к желаемому эффекту, значит пора. Zabbix не имеет официальной поддержки секционирования таблиц, однако можно найти готовые инструкции для его настройки.<br />
<br />
Ранее я использовал для разбивки таблиц на секции вот эту статью на wiki-странице Zabbix: <a href="https://zabbix.org/wiki/Docs/howto/mysql_partitioning">Docs/howto/mysql partitioning</a>, однако впоследствии стал пользоваться вот этой статьёй: <a href="https://zabbix.org/wiki/Docs/howto/mysql_partition">Docs/howto/mysql partition</a>. У второй статьи есть два преимущества:<br />
<ol><li>при её использовании в базе данных Zabbix не нужно создавать дополнительную нестандартную таблицу manage_partitions,</li>
<li>при её использовании имеется возможность делить таблицы не только на секции месячного или суточного размера, но и на секции произвольного размера, в том числе более мелкого.</li>
</ol>Наконец, в интернете <a href="https://support.zabbix.com/browse/ZBXNEXT-2363">можно</a> <a href="https://www.linux.org.ru/forum/admin/13545067?cid=13635465">встретить</a> <a href="https://www.linux.org.ru/news/opensource/14508212?cid=14513389">советы</a> по смене движка таблиц истории и тенденций с родного для MySQL движка InnoDB на движок TokuDB с технологией «фрактальных индексов». Также вместе с этим движком рекомендуют использовать «кластерные индексы», когда индексы хранятся вместе с данными, и сжатие данных в таблицах.<br />
<br />
Изначально TokuDB был ответвлением MySQL, в котором фирма Tokutek реализовала собственную технологию «фрактальных индексов». Позже исходные тексты TokuDB стали доступны под свободной лицензией и на их основе был реализован плагин, пригодный подключению как к оригинальной СУБД MySQL, так и к её ответвлениям - MariaDB и Percona.<br />
<h3>Включение плагина TokuDB в MariaDB</h3>Мне удавалось успешно настраивать TokuDB на Debian Stretch и Debian Buster. Установку и настройку MariaDB оставим за скобками нашего обсуждения. Будем считать, что система мониторинга уже развёрнута и использует MariaDB, а таблицы истории и тенденций пока что хранятся в таблицах формата InnoDB.<br />
<br />
Первым делом установим пакет с плагином, который добавляет в MariaDB поддержку формата хранения таблиц TokuDB:<br />
<pre style="background-color: black; color: white;"># apt-get install mariadb-plugin-tokudb</pre>Вместе с пакетом будет установлен дополнительный файл конфигурации /etc/mysql/mariadb.conf.d/tokudb.cnf, в котором указан путь к библиотеке libjemalloc. В случае с Debian Stretch это будет путь /usr/lib/x86_64-linux-gnu/libjemalloc.so.1 В случае с Debian Buster это будет путь /usr/lib/x86_64-linux-gnu/libjemalloc.so.2 Прежде чем продолжать, стоит удостовериться, что этот файл действительно сущетсвует в системе, т.к. при обновлении операционной системы до свежего релиза в файле конфигурации мог остаться устаревший путь. В Debian Stretch этот файл устанавливается с пакетом libjemalloc1, а в Debian Buster - пакетом libjemalloc2. Необходимо установить соответствующий пакет и исправить путь к файлу в файле конфигурации.<br />
<br />
Теперь нужно убедиться, что в системе отключена прозрачная поддержка огромных страниц (Transparent Hugepages). Для этого запускаем следующую команду:<br />
<pre style="background-color: black; color: white;">$ cat /sys/kernel/mm/transparent_hugepage/enabled</pre>Если команда поругалась на отсутствие файла, значит прозрачная поддержка огромных страниц уже отключена и делать больше ничего не нужно. Также ничего не нужно делать, если команда вывела следующее:<br />
<pre style="background-color: black; color: white;">always madvise [never]</pre>Если же команда вывела приведённый ниже текст, то прозрачная поддержка огромных страниц включена и её необходимо отключить:<br />
<pre style="background-color: black; color: white;">[always] madvise never</pre>Открываем файл /etc/default/grub, находим переменную GRUB_CMDLINE_LINUX и добавляем в список опций опцию transparent_hugepage=never. В результате должно получиться что-то такое:<br />
<pre style="background-color: lightgrey;">GRUB_CMDLINE_LINUX="ipv6.disable=1 transparent_hugepage=never"</pre>Теперь нужно обновить конфигурацию загрузчика следующей командой:<br />
<pre style="background-color: black; color: white;"># update-grub</pre>Осталось перезагрузить систему и убедиться в том, что прозрачная поддержка огромных страниц действительно отключилась.<br />
<br />
Все описанные выше действия, необходимые для включения плагина TokuDB, можно найти в официальной документации MariaDB, на странице <a href="https://mariadb.com/kb/en/installing-tokudb/">Installing TokuDB</a>.<br />
<h3>Создание новых таблиц истории и тенденций</h3>Если база данных только создана и не содержит исторических данных и данных тенденций, то можно просто удалить существующие таблицы:<br />
<pre style="background-color: lightgrey;">DROP TABLE history;
DROP TABLE history_uint;
DROP TABLE history_str;
DROP TABLE history_log;
DROP TABLE history_text;
DROP TABLE trends;
DROP TABLE trends_uint;</pre>Если же нужно выполнить миграцию существующей инсталляции Zabbix, тогда лучше сначала переименовать существующие таблицы истории и тенденций:<br />
<pre style="background-color: lightgrey;">RENAME TABLE history TO history_bak;
RENAME TABLE history_uint TO history_uint_bak;
RENAME TABLE history_str TO history_str_bak;
RENAME TABLE history_log TO history_log_bak;
RENAME TABLE history_text TO history_text_bak;
RENAME TABLE trends TO trends_bak;
RENAME TABLE trends_uint TO trends_uint_bak;</pre>Вместо прежних таблиц нужно будет создать новые пустые таблицы истории и тенденций, сначала без разбивки на секции, с помощью следующих SQL-запросов:<br />
<pre style="background-color: lightgrey;">CREATE TABLE `history` (
`itemid` bigint unsigned NOT NULL,
`clock` integer DEFAULT '0' NOT NULL,
`value` double(16,4) DEFAULT '0.0000' NOT NULL,
`ns` integer DEFAULT '0' NOT NULL
) ENGINE=TokuDB COMPRESSION=TOKUDB_LZMA;
CREATE INDEX `history_1` ON `history` (`itemid`,`clock`) CLUSTERING=yes;
CREATE TABLE `history_uint` (
`itemid` bigint unsigned NOT NULL,
`clock` integer DEFAULT '0' NOT NULL,
`value` bigint unsigned DEFAULT '0' NOT NULL,
`ns` integer DEFAULT '0' NOT NULL
) ENGINE=TokuDB COMPRESSION=TOKUDB_LZMA;
CREATE INDEX `history_uint_1` ON `history_uint` (`itemid`,`clock`) CLUSTERING=yes;
CREATE TABLE `history_str` (
`itemid` bigint unsigned NOT NULL,
`clock` integer DEFAULT '0' NOT NULL,
`value` varchar(255) DEFAULT '' NOT NULL,
`ns` integer DEFAULT '0' NOT NULL
) ENGINE=TokuDB COMPRESSION=TOKUDB_LZMA;
CREATE INDEX `history_str_1` ON `history_str` (`itemid`,`clock`) CLUSTERING=yes;
CREATE TABLE `history_log` (
`itemid` bigint unsigned NOT NULL,
`clock` integer DEFAULT '0' NOT NULL,
`timestamp` integer DEFAULT '0' NOT NULL,
`source` varchar(64) DEFAULT '' NOT NULL,
`severity` integer DEFAULT '0' NOT NULL,
`value` text NOT NULL,
`logeventid` integer DEFAULT '0' NOT NULL,
`ns` integer DEFAULT '0' NOT NULL
) ENGINE=TokuDB COMPRESSION=TOKUDB_LZMA;
CREATE INDEX `history_log_1` ON `history_log` (`itemid`,`clock`) CLUSTERING=yes;
CREATE TABLE `history_text` (
`itemid` bigint unsigned NOT NULL,
`clock` integer DEFAULT '0' NOT NULL,
`value` text NOT NULL,
`ns` integer DEFAULT '0' NOT NULL
) ENGINE=TokuDB COMPRESSION=TOKUDB_LZMA;
CREATE INDEX `history_text_1` ON `history_text` (`itemid`,`clock`) CLUSTERING=yes;
CREATE TABLE `trends` (
`itemid` bigint unsigned NOT NULL,
`clock` integer DEFAULT '0' NOT NULL,
`num` integer DEFAULT '0' NOT NULL,
`value_min` double(16,4) DEFAULT '0.0000' NOT NULL,
`value_avg` double(16,4) DEFAULT '0.0000' NOT NULL,
`value_max` double(16,4) DEFAULT '0.0000' NOT NULL,
PRIMARY KEY (itemid,clock) CLUSTERING=yes
) ENGINE=TokuDB COMPRESSION=TOKUDB_LZMA;
CREATE TABLE `trends_uint` (
`itemid` bigint unsigned NOT NULL,
`clock` integer DEFAULT '0' NOT NULL,
`num` integer DEFAULT '0' NOT NULL,
`value_min` bigint unsigned DEFAULT '0' NOT NULL,
`value_avg` bigint unsigned DEFAULT '0' NOT NULL,
`value_max` bigint unsigned DEFAULT '0' NOT NULL,
PRIMARY KEY (itemid,clock) CLUSTERING=yes
) ENGINE=TokuDB COMPRESSION=TOKUDB_LZMA;</pre>Эти таблицы пока не разбиты на секции, но уже используют движок TokuDB, сжатие данных по алгоритму LZMA и используют кластерные индексы - индексы, хранящиеся вместе с индексируемыми данными.<br />
<h3>Разбивка таблиц на секции</h3>Разбивку таблиц на секции я проводил в соответствии со статьёй <a href="https://zabbix.org/wiki/Docs/howto/mysql_partition">Docs/howto/mysql partition</a>.<br />
<br />
Я подготовил скрипт, который выводит команды, необходимые для разбивки таблиц истории и тенденций на необходимые секции. Настройки начальной и конечной дат, а также размер каждой секции, задаются прямо в тексте скрипта:<br />
<pre style="background-color: lightgrey;">#!/usr/bin/python
# -*- coding: UTF-8 -*-
from datetime import datetime, timedelta
from pytz import timezone
def table_partitions(table, start, stop, step):
print 'ALTER TABLE `%s` PARTITION BY RANGE (`clock`) (' % table
dt = start
while dt < stop:
name = dt.strftime('%Y%m%d%H%M')
ts = dt.strftime('%s')
dt += step
print 'PARTITION p%s VALUES LESS THAN (%s) ENGINE = TokuDB,' % (name, ts)
name = dt.strftime('%Y%m%d%H%M')
ts = dt.strftime('%s')
print 'PARTITION p%s VALUES LESS THAN (%s) ENGINE = TokuDB' % (name, ts)
print ');'
tz = timezone('UTC')
# Для таблиц тенденций trends и trends_uint
start = datetime(2018, 9, 10, 0, 0, 0, tzinfo=tz)
stop = datetime(2019, 9, 22, 0, 0, 0, tzinfo=tz)
step = timedelta(days=1)
table_partitions('trends', start, stop, step)
table_partitions('trends_uint', start, stop, step)
# Для таблиц истории history и history_uint
start = datetime(2019, 6, 10, 0, 0, 0, tzinfo=tz)
stop = datetime(2019, 9, 22, 0, 0, 0, tzinfo=tz)
step = timedelta(hours=6)
table_partitions('history', start, stop, step)
table_partitions('history_uint', start, stop, step)
# Для таблиц истории history_str, history_text и history_log
start = datetime(2019, 9, 3, 0, 0, 0, tzinfo=tz)
stop = datetime(2019, 9, 22, 0, 0, 0, tzinfo=tz)
step = timedelta(days=1)
table_partitions('history_str', start, stop, step)
table_partitions('history_text', start, stop, step)
table_partitions('history_log', start, stop, step)</pre>Запускаем скрипт, сохраняем выведенные им команды в файл:<br />
<pre style="background-color: black; color: white;">$ ./partitions.py > partitions.sql</pre>Затем подключаемся клиентом MySQL к базе данных zabbix:<br />
<pre style="background-color: black; color: white;">$ mysql -uzabbix -p zabbix</pre>И выполняем в нём команды из файла partitions.sql:<br />
<pre style="background-color: black; color: white;">MariaDB [zabbix]> SOURCE partitions.sql</pre>После выполнения команд таблицы будут разбиты на секции в соответствии с настройками, прописанными в скрипте partitions.py<br />
<h3>Перенос имеющихся данных в новые таблицы</h3>Можно было бы перенести данные из старых таблиц в новые простыми SQL-запросами вида INSERT INTO history_uint SELECT * FROM history_uint_bak, но такие запросы на время их работы будут полностью блокировать вставку новых данных в таблицу, поэтому надо переносить данные порциями. Я в этих целях пользуюсь командами следующего вида:<br />
<pre style="background-color: black; color: white;">$ mysqldump -t -uroot -p zabbix trends_uint_bak | grep ^INSERT | sed 's/^INSERT INTO/INSERT IGNORE/g' | mysql -uroot -p zabbix
$ mysqldump -t -uroot -p zabbix trends_bak | grep ^INSERT | sed 's/^INSERT INTO/INSERT IGNORE/g' | mysql -uroot -p zabbix
$ mysqldump -t -uroot -p zabbix history_bak | grep ^INSERT | sed 's/^INSERT INTO/INSERT IGNORE/g' | mysql -uroot -p zabbix
$ mysqldump -t -uroot -p zabbix history_str_bak | grep ^INSERT | sed 's/^INSERT INTO/INSERT IGNORE/g' | mysql -uroot -p zabbix
$ mysqldump -t -uroot -p zabbix history_text_bak | grep ^INSERT | sed 's/^INSERT INTO/INSERT IGNORE/g' | mysql -uroot -p zabbix
$ mysqldump -t -uroot -p zabbix history_log_bak | grep ^INSERT | sed 's/^INSERT INTO/INSERT IGNORE/g' | mysql -uroot -p zabbix</pre>Это не красивое решение, но оно меня вполне устраивает, т.к. не приводит к длительной блокировке таблиц.<br />
<br />
После переноса данных в новые таблицы старые таблицы можно будет удалить:<br />
<pre style="background-color: lightgrey;">DROP TABLE history_bak;
DROP TABLE history_uint_bak;
DROP TABLE history_str_bak;
DROP TABLE history_log_bak;
DROP TABLE history_text_bak;
DROP TABLE trends_bak;
DROP TABLE trends_uint_bak;</pre><h3>Настройки плагина TokuDB</h3>Просмотрев <a href="https://www.youtube.com/watch?v=RoniHeNHTtI">видеоролик с выступлением Владислава Лесина</a> - одного из нынешних разработчиков TokuDB, работающего над этим плагином в компании Percona - я составил для себя список настроек плагина, на которые следует обратить внимание:<br />
<h4>tokudb_fanout - максимальное количество дочерних узлов</h4>Чем меньше, тем больше памяти для сообщений, тем лучше для нагрузки по записи, тем хуже для нагрузке по выборке, тем хуже использование памяти.<br />
<h4>tokudb_block_size - размер узла в памяти</h4>По умолчанию - 4 мегабайта.<br />
<br />
Большие значения лучше для медленных дисков (с последовательным доступом). 4 мегабайта - оптимальный выбор для вращающихся дисков.<br />
<br />
Для быстрых дисков (с произвольным доступом, как у SSD) меньший размер блока може увеличить производительность.<br />
<h4>tokudb_read_block_size - размер базового узла</h4>По умолчанию - 64 килобайта.<br />
<br />
Меньшие значения лучше для точечных чтений, но приводят к увеличению непоследовательных операций ввода-вывода.<br />
<h4>tokudb_row_format - алгоритм сжатия колонок</h4>Возможны следующие значения:<br />
<ul><li>tokudb_default, tokudb_zlib - среднее сжатие при средней нагрузке на процессор.</li>
<li>tokudb_snappy - хорошее сжатие при низкой нагрузке на процессор.</li>
<li>tokudb_fast, tokudb_quicklz - слабое сжатие при низкой нагрузке на процессор.</li>
<li>tokudb_small, tokudb_lzma - лучшее сжатие при высокой нагрузке на процессор.</li>
<li>tokudb_uncompressed - сжатие не используется.</li>
</ul><h4>tokudb_directio - использование прямого ввода-вывода</h4>Значение OFF позволяет использовать дисковый кэш операционной системы в качестве вторичного кэша для хранения сжатых узлов. Для ограничения использования памяти процессом mysqld нужно использовать cgroups.<br />
<br />
В качестве пищи для размышлений можно принять во внимание настройки, использованные в тесте производительности TokuDB, описание которого доступно по ссылке <a href="https://github.com/Percona-Lab/benchmark-docs/blob/master/benchmarks/mysql-linkbench-smperf01.rst">LinkeBench MySQL</a>:<br />
<pre style="background-color: lightgrey;">tokudb_cache_size = 8G ; default = 12G ?
tokudb_directio = OFF
tokudb_empty_scan = disabled ; default - rl
tokudb_read_block_size = 16K ; default - 64K
tokudb_commit_sync = ON
tokudb_checkpointing_period = 900 ; default = 60
tokudb_block_size = 4M
tokudb_cleaner_iterations = 10000 ; default = 5
tokudb_fanout = 128 ; default = 16</pre>Я ограничился указанием подходящего значения tokudb_cache_size и изменением следующих настроек:<br />
<pre style="background-color: lightgrey;">tokudb_directio = ON
tokudb_row_format = tokudb_lzma
tokudb_empty_scan = disabled</pre><h3>Решение проблем</h3>После обновления версии MariaDB пакетами из репозитория по неизвестным причинам планировщик перестаёт выполнять задачу по обслуживанию секций таблиц: не удаляет устаревшие секции и, что гораздо хуже, не создаёт новые секции таблиц. Последнее приводит к тому, что сервер Zabbix не может вставить в таблицы новые данные. Проявляется это в том, что после полуночи в последних данных на графиках нет данных, а сервер Zabbix ругается в журнал ошибками следующего вида:<br />
<pre style="background-color: lightgrey;">6619:20200604:000100.756 [Z3005] query failed: [1526] Table has no partition for value 1591210860 [insert into history
(itemid,clock,ns,value) values (3827556,1591210860,519948235,0.012016),(3827601,1591210860,574265420,0.016382),
(3827553,1591210860,683308669,7.549000),(3827616,1591210860,684083178,7.715000),(3827591,1591210860,684848189,3.199600),
(3827583,1591210860,685585717,0.016474),(3827504,1591210860,689418268,24.000000),(3827564,1591210860,690132132,3.209600),
(3827610,1591210860,690862622,0.014954),(1284053,1591210860,732901317,3.000000),(1283392,1591210860,737607405,23.000000),
(352809,1591210860,737607405,35.000000),(1309072,1591210860,738428022,11.000000),(3827571,1591210860,740171802,7.187000),
(1308475,1591210860,740185955,3.000000),(1292277,1591210860,743020934,1.000000),(3827619,1591210860,743278260,0.014760),
(3827573,1591210860,743976749,3.254600),(3827598,1591210860,744811430,7.577000),(1284110,1591210860,745749025,21.000000),
(3827580,1591210860,746661186,7.580000),(1279841,1591210860,747623084,5.000000),(3827607,1591210860,748043948,7.717000),
(1282792,1591210860,749216640,15.000000);
]</pre>Если новые секции таблиц не создаются автоматически, то первым делом вручную вызываем обслуживание таблиц, чтобы сервер Zabbix мог начать писать данные:<br />
<pre style="background-color: lightgrey;">CALL partition_maintenance('zabbix', 'trends', 365, 24, 2);
CALL partition_maintenance('zabbix', 'trends_uint', 365, 24, 2);
CALL partition_maintenance('zabbix', 'history', 90, 6, 8);
CALL partition_maintenance('zabbix', 'history_uint', 90, 6, 8);
CALL partition_maintenance('zabbix', 'history_str', 7, 24, 2);
CALL partition_maintenance('zabbix', 'history_text', 7, 24, 2);
CALL partition_maintenance('zabbix', 'history_log', 7, 24, 2);</pre>Далее, чтобы в дальнейшем заработала автоматика, могут помочь следующие действия.<br />
<br />
Сначала обновляем таблицы в базах данных до текущей версии MySQL:<br />
<pre style="background-color: black; color: white;">$ mysql_upgrade --force -uroot -p mysql
$ mysql_upgrade --force -uroot -p zabbix</pre>Затем пересоздаём запланированное задание:<br />
<pre style="background-color: lightgrey;">USE `zabbix`;
DELIMITER $$
CREATE EVENT IF NOT EXISTS `e_part_manage`
ON SCHEDULE EVERY 1 DAY
STARTS '2019-04-04 04:00:00'
ON COMPLETION PRESERVE
ENABLE
COMMENT 'Управление созданием и удалением секций'
DO BEGIN
CALL partition_maintenance('zabbix', 'trends', 365, 24, 2);
CALL partition_maintenance('zabbix', 'trends_uint', 365, 24, 2);
CALL partition_maintenance('zabbix', 'history', 90, 6, 8);
CALL partition_maintenance('zabbix', 'history_uint', 90, 6, 8);
CALL partition_maintenance('zabbix', 'history_str', 7, 24, 2);
CALL partition_maintenance('zabbix', 'history_text', 7, 24, 2);
CALL partition_maintenance('zabbix', 'history_log', 7, 24, 2);
END$$
DELIMITER ;</pre>И напоследок перезапускаем сервер MariaDB:<br />
<pre style="background-color: black; color: white;"># systemctl restart mariadb</pre>Какое из приведённых решений помогает на самом деле, сказать точно не могу, т.к. я пробовал использовать каждый из советов поодиночке и не установил чёткой закономерности, какой из них помогает всегда. Иногда одно действие не лечит проблему и на следующий день можно заметить, что новые секции опять не создались.morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0tag:blogger.com,1999:blog-4428473564097379725.post-50724904424561794372020-08-09T13:00:00.000+05:002020-08-09T13:00:03.590+05:00Настройка сервера MySQLНастраивая MySQL и оптимизируя настройки его производительности, я делал заметки, которые периодически обновлял и дополнял. В результате сформировался контрольный список настроек сервера MySQL, на которые стоит обратить внимание в первую очередь при его первоначальной настройке и при дальнейших подходах по оптимизации его производительности.<br />
<br />
Замечу между прочим, что не стоит уделять слишком много внимания оптимизации производительности одной лишь конфигурации сервера MySQL. Самые лучшие оптимизации можно сделать на стороне приложения, оптимизировав SQL-запросы, изменив структуру таблиц и их индексы. Ну и конечно, не следует ожидать многого от слабого сервера.<br />
<br />
Неплохим подспорьем при настройке производительности могут оказаться рекомендации утилиты mysqltuner. Однако не стоит безоглядно копировать в файл конфигурации всё, что он порекомендует. В частности, нужно учитывать размер доступной на сервере оперативной памяти.<br />
<h3>expire_logs_days</h3>Срок хранения журнала транзакций. Если данные часто и помногу обновляются, то для экономии места стоит задать значение поменьше, например 1 день:<br />
<pre style="background-color: lightgrey;">expire_logs_days = 1</pre>Однако, если вы используете репликацию данных на другой сервер, журналы стоит хранить за такой период времени, который может понадобиться на восстановление репликации при её поломке. В противном случае придётся повторно копировать данные с ведущего сервера на ведомый.<br />
<h3>transaction_isolation</h3>Уровень изоляции транзакций. Значение REPEATABLE-READ не покажет внутри транзакции новые данные, добавленные в другой транзакции. READ-COMMITED - наоборот, позволяет читать внутри транзакции данные, изменённые в других транзакциях.<br />
<br />
Большинству приложений важна целостность и непротиворечивость данных, поэтому они используют базу данных как транзакционную (OLTP). В таком случае лучше использовать значение REPEATABLE-READ.<br />
<br />
Если же приложение использует базу данных для аналитических запросов, то лучше подойдёт значение READ-COMMITED.<br />
<br />
Бывают и приложения со смешанной логикой. Например, Zabbix хранит в базе данных как собственную конфигурацию, что больше соответствует OLTP, так и исторические данные со значениями определённых показателей в конкретные моменты времени, что больше соответствует OLAP. Но, т.к. нужно обеспечить непротиворечивость данных конфигурации, то запросы к таблицам истории тоже придётся выполнять на уровне изоляции транзакций REPEATABLE-READ.<br />
<pre style="background-color: lightgrey;">transaction_isolation = REPEATABLE-READ</pre>Для уверенности стоит поискать настройки, рекомендуемые разработчиками приложения. Если информации найти не удалось, более безопасным выбором будет REPEATABLE-READ.<br />
<h3>innodb_file_per_table</h3><br />
Настройка, предписывающая хранить каждую таблицу базы данных в отдельном файле. Перед её выставлением необходимо сделать резервную копию всех баз данных. Чтобы настройка вступила в силу, нужно:<br />
<ol><li>остановить MySQL,</li>
<li>удалить файлы ibdata1, ib_logfile0 и ib_logfile1,</li>
<li>запустить MySQL снова,</li>
<li>восстановить базы данных из резервных копий.</li>
</ol>При восстановлении данных MySQL поместит каждую таблицу в отдельный файл.<br />
<br />
Когда данные таблиц находятся в отдельных файлах, можно сравнительно легко вернуть в файловую систему место, освободившееся в таблице при удалении данных. Для этого достаточно запустить команду <a href="https://dev.mysql.com/doc/refman/5.7/en/optimize-table.html">OPTIMIZE TABLE или ALTER TABLE ... FORCE</a> над таблицей, которую нужно ужать. В случае, если используется общее хранилище для всех таблиц, неиспользуемое место никогда не возвращается на диск.<br />
<br />
Есть у раздельного хранения данных таблиц и отрицательная сторона: при большом количестве таблиц (и секций таблиц, если они есть) увеличивается время запуска сервера MySQL.<br />
<br />
Также операционная система обычно ограничивает количество одновременно открытых одним пользователем файлов, то серверу MySQL может потребоваться закрывать неиспользуемые файлы, чтобы открыть нужные и уложиться в этот лимит. Этот недостаток можно смягчить использованием настройки table_cache, описанной ниже, и изменением ограничений со стороны операционной системы.<br />
<br />
Если у базы данных небольшой размер, а работать MySQL придётся на каком-нибудь микрокомпьютере, то возможно не стоит хранить данные таблиц в отдельных файлах. Ещё лучше в подобных случаях будет воспользоваться какой-нибудь встраиваемой базой данных, например, SQLite.<br />
<br />
<pre style="background-color: lightgrey;">innodb_file_per_table = 1</pre><h3>table_cache</h3>Количество одновременно открытых файлов таблиц. Позволяет уменьшить количество открытий-закрытий файлов. Стоит оценить количество файлов в каталоге, где хранятся файлы с данными MySQL и выставить значение равного порядка.<br />
<pre style="background-color: lightgrey;">table_cache = 512</pre>Стоит учитывать, что операционная система ограничивает количество одновременно открытых одним пользователем файлов и значение, указанное в опции, не должно быть больше разрешённого операционной системой лимита.<br />
<h3>event_scheduler</h3>Настройка, включающая встроенный в сервер MySQL планировщик задач. Позволяет по расписанию запускать запросы или хранимые процедуры:<br />
<pre style="background-color: lightgrey;">event_scheduler = 1</pre><h3>max_connections</h3>Максимальное количество подключений к базе данных. Для обслуживания каждого подключения MySQL запускает по одному отдельному потоку. Если клиентов, подключающихся к базе данных, больше указанного числа, то подключения сверх лимита попадают в очередь, ожидая освобождения одного из занятых подключений.<br />
<br />
В случае веб-приложений нет особого смысла выставлять эту настройку намного больше количества процессов сервера приложения. Если в php-fpm или uwsgi для работы приложения выделено 16 процессов, то как правило каждый процесс будет устанавливать не более одного подключения к базе данных.<br />
<pre style="background-color: lightgrey;">max_connections = 140</pre><h3>query_cache_size</h3>Кэш результатов прошлых запросов. Если содержимое таблиц, фигурирующих в запросе, не менялось с момента кэширования результата предыдущего такого же запроса, то этот кэш позволит серверу сразу выдать клиенту результат запроса из кэша:<br />
<pre style="background-color: lightgrey;">query_cache_size = 64M</pre>Если содержимое таблиц постоянно меняется, а вероятность повторного выполнения запроса низка, то отключение этого кэша никак не скажется на производительности СУБД, но позволит сэкономить немного оперативной памяти. Для отключения кэша запросов можно указать такие опции:<br />
<pre style="background-color: lightgrey;">query_cache_type = 0
query_cache_size = 0</pre><h3>general_log_file</h3>Общий журнал. Содержит, например, сведения о запусках и остановках сервера.<br />
<pre style="background-color: lightgrey;">general_log_file = /var/log/mysql/mysql.log</pre><h3>log_error</h3>Журнал ошибок:<br />
<pre style="background-color: lightgrey;">log_error = /var/log/mysql/mysql.err</pre><h3>log_warnings</h3>Не выводить предупреждающие сообщения в журнале ошибок:<br />
<pre style="background-color: lightgrey;">log_warnings = 0</pre><h3>character-set-server и collation-server</h3>Настройка кодировки сервера по умолчанию и настроек сортировки и сравнения символов:<br />
<pre style="background-color: lightgrey;">character-set-server = utf8
collation-server = utf8_general_ci</pre><h3>join_buffer_size</h3>Буфер, используемый для соединения таблиц друг с другом. При недостаточном объёме буфера соединение будет осуществляться с использованием диска:<br />
<pre style="background-color: lightgrey;">join_buffer_size = 16M</pre><h3>innodb_buffer_pool_size и innodb_buffer_pool_instances</h3>Размер буферного пула. Пожалуй самая важная настройка сервера. Указывает количество оперативной памяти, которую сервер может использовать для обработки данных. Если размер базы данных на диске меньше этого значения, то содержимое всей базы данных может уместиться в оперативной памяти, благодаря чему может быть достигнута максимально возможная производительность сервера.<br />
<br />
Если под MySQL выделен отдельный сервер, можно рассчитать размер этого буфера исходя из общего размера доступной на сервере памяти и объёма буферов под обработку запросов от каждого из максимально возможного количества клиентов (max_connections):<br />
<pre style="background-color: lightgrey;">innodb_buffer_pool_size = 512M</pre>В интернете можно встретить рекомендации делить буферные пулы размерами больше гигабайта на несколько экземпляров, чтобы на каждый из экземпляров приходилось, например, по одному гигабайту:<br />
<pre style="background-color: lightgrey;">innodb_buffer_pool_size = 10G
innodb_buffer_pool_instances = 10</pre><h3>innodb_flush_method</h3>Метод записи данных в файловую систему. Часто встречается рекомендация не использовать дисковый кэш операционной системы, т.к. у сервера MySQL есть собственные буферы, а двойная буферизация замедляет работу и повышает вероятность повреждения данных:<br />
<pre style="background-color: lightgrey;">innodb_flush_method = O_DIRECT</pre><h3>innodb_log_file_size, innodb_log_buffer_size и innodb_flush_log_at_trx_commit</h3>Настройки журналов транзакций. Если в журнал данные пишутся помногу и часто, имеет смысл увеличить как размер самого журнала, чтобы серверу реже приходилось создавать новые файлы и переоткрывать их, так и увеличить размер буфера записи, чтобы снизить частоту блокирования сервера в ожидании записи в журнал транзакций:<br />
<pre style="background-color: lightgrey;">innodb_log_file_size = 256M
innodb_log_buffer_size = 8M</pre>Таких файлов у MySQL два. Рекомендуется, чтобы размер каждого из них составлял 1/4 от размера innodb_buffer_pool_size. Однако размер файла журнала должен быть меньше 2 гигабайт - это внутреннее ограничение MySQL.<br />
<br />
У журнала транзакций имеется настройка, аналогичная настройке буферного пула innodb_flush_method:<br />
<pre style="background-color: lightgrey;">innodb_flush_log_at_trx_commit = 2</pre>Возможны следующие значения:<br />
<ul><li>1 - каждая транзакция после фиксации записывается на диск (наибольшая надёжность),</li>
<li>2 - транзакция после фиксации записывается в кэш операционной системы (компромисс между надёжностью и производительностью),</li>
<li>0 - нет требования специально сохранять данные транзакции после её фиксации, данные сохраняются по мере заполнения буфера транзакций (наибольшая производительность, но высокий риск потери данных).</li>
</ul><h3>Временные файлы</h3>При необходимости сортировки больших выборок данных MySQL использует раздел для временных файлов. Из-за этого выполнение больших запросов может происходить заметно дольше. Чтобы ускорить обработку таких запросов, можно создать файловую систему для временных файлов в оперативной памяти.<br />
<br />
Создаём точку монтирования, например /mysql-tmp:<br />
<pre style="background-color: black; color: white;"># cd /
# mkdir mysql-tmp</pre>Добавляем в файл /etc/fstab строчку для монитрования раздела размером, например, 512 мегабайт:<br />
<pre style="background-color: lightgrey;">tmpfs /mysql-tmp tmpfs relatime,nodev,nosuid,noexec,uid=mysql,gid=mysql,mode=0760,size=512M 0 0</pre>Смонтируем временный раздел:<br />
<pre style="background-color: black; color: white;"># mount /mysql-tmp</pre>Теперь нужно указать в файле конфигурации сервера MySQL внутри секции server соответствующую опцию:<br />
<pre style="background-color: lightgrey;">tmpdir = /mysql-tmp</pre>И перезапустить MySQL:<br />
<pre style="background-color: lightgrey;"># systemctl restart mysql</pre>Стоит учитывать, что если места на этом разделе окажется недостаточно, запрос не выполнится и MySQL сообщит об ошибке выполнения запроса.morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0tag:blogger.com,1999:blog-4428473564097379725.post-8416243851395126922020-08-02T13:00:00.000+05:002020-08-02T13:00:01.341+05:00Правка service-файла snmptrapdПосле обновления на одном из серверов Debian Wheezy до Debian Stretch перестала работать обработка трапов демоном snmptrapd. Как выяснилось, проблема была в том, что snmptrapd был запущен не с теми опциями, которые были указаны в файле с его настройками. В файле /etc/default/snmptrapd была указана переменная с опциями:<br />
<pre style="background-color: lightgrey;">TRAPDOPTS='-Lf /dev/null -n -t -Oqnet'</pre>Реально же демон snmptrapd запускался с опциями -Lsd -f<br />
<br />
Из-за этого в скрипт обработки трапов OID'ы попадали в символьном виде:<br />
<pre style="background-color: lightgrey;">SNMPv2-SMI::enterprises.1332.3.1.1.4.5.0</pre>А скрипт был расчитан на обработку OID'ов в числовом виде:<br />
<pre style="background-color: lightgrey;">.1.3.6.1.4.1.1332.3.1.1.4.5.0</pre>После обновления операционной системы на сервере с Debian Wheezy до Debian Stretch, в нём поменялась система инициализации с System V Init на Systemd.<br />
<br />
В комплекте с Systemd поставляется такой service-файл /lib/systemd/system/snmptrapd.service для запуска snmptrapd:<br />
<pre style="background-color: lightgrey;">[Unit]
Description=Simple Network Management Protocol (SNMP) Trap Daemon.
After=network.target
ConditionPathExists=/etc/snmp/snmptrapd.conf
[Service]
Environment="MIBSDIR=/usr/share/snmp/mibs:/usr/share/snmp/mibs/iana:/usr/share/snmp/mibs/ietf:/usr/share/mibs/site:/usr/share/snmp/mibs:/usr/share/mibs/iana:/usr/share/mibs/ietf:/usr/share/mibs/netsnmp"
Type=simple
ExecStart=/usr/sbin/snmptrapd -Lsd -f
ExecReload=/bin/kill -HUP $MAINPID
[Install]
WantedBy=multi-user.target</pre>Как видно, опции, с которыми должен запускаться snmptrapd, в нём прошиты жёстко, а не берутся из файла /etc/default/snmptrapd.<br />
<br />
Создал вместо этого стандартного service-файла свой собственный файл /etc/systemd/system/snmptrapd.service со следующим содержимым:<br />
<pre style="background-color: lightgrey;">[Unit]
Description=Simple Network Management Protocol (SNMP) Trap Daemon.
After=network.target
ConditionPathExists=/etc/snmp/snmptrapd.conf
[Service]
Environment="MIBSDIR=/usr/share/snmp/mibs:/usr/share/snmp/mibs/iana:/usr/share/snmp/mibs/ietf:/usr/share/mibs/site:/usr/share/snmp/mibs:/usr/share/mibs/iana:/usr/share/mibs/ietf:/usr/share/mibs/netsnmp"
EnvironmentFile=/etc/default/snmptrapd
Type=simple
ExecStart=/usr/sbin/snmptrapd $TRAPDOPTS -f
ExecReload=/bin/kill -HUP $MAINPID
[Install]
WantedBy=multi-user.target</pre>Чтобы о новом service-файле узнал systemd, нужно выполнить следующую команду:<br />
<pre style="background-color: black; color: white;"># systemctl daemon-reload</pre>А чтобы демон snmptrapd запустился с новыми опциями, нужно его перезапустить:<br />
<pre style="background-color: black; color: white;"># systemctl restart snmptrapd</pre>Теперь опции для snmptrapd стали браться из файла /etc/default/snmptrapd, как и было до этого.morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0tag:blogger.com,1999:blog-4428473564097379725.post-8474497681686014152020-07-26T13:00:00.000+05:002020-07-26T13:00:03.706+05:00Отключение сообщений об ошибках на стандартный ввод-вывод в net-snmpЕсть у меня один скрипт на Python, использующий привязки к библиотеке Net-SNMP. Скрипт отмечает результаты своей работы в базе данных и выводит сообщения об ошибках только при каких-то совсем уж неожиданных ошибках, например, при отсутствии связи с этой базой данных. Если же скрипт отработал шататно, он не должен выводить никаких сообщений. Поскольку сркипт регулярно запускался через планировщик задач cron, то при ошибках весь вывод скрипта отправлялся мне по почте. В общем, всё как обычно.<br />
<br />
Раньше скрипт выполнял запросы только по протоколам SNMP первой и второй версии. Когда же в сети появились устройства, доступные только по протоколу SNMP третьей версии, скрипт начал выводить ошибки такого вида:<br />
<pre style="background-color: lightgrey;">Authentication failed for backup</pre>backup - это имя пользователя SNMPv3, под которым не удавалось выполнить SNMP-запросы. <br />
<br />
Поскольку скрипт анализирует результаты запросов SNMP, то эти диагностические сообщение лишь засоряют вывод скрипта. Как оказалось, эти сообщения выводит сама библиотека Net-SNMP и я не нашёл никаких способов для того, чтобы их отключить, например, через конфигурацию /etc/snmp/snmp.conf. Чтобы отключить эти сообщения, потребуется поправить исходный текст библиотеки. Скачаем и распакуем пакет с исходными текстами net-snmp:<br />
<pre style="background-color: black; color: white;">$ apt-get source net-snmp</pre>Наши правки к исходному коду оформим в виде заплатки при помощи инструмента quilt, подробнее о котором я писал в заметке <a href="https://vladimir-stupin.blogspot.com/2019/12/quilt.html">Использование quilt для подготовки заплат</a>. Создадим новую заплатку:<br />
<pre style="background-color: black; color: white;">$ quilt new removed-redundant-log-message</pre>Добавим в будущую заплатку файл:<br />
<pre style="background-color: black; color: white;">$ quilt add snmplib/snmpusm.c</pre>Открываем файл snmplib/snmpusm.c в текстовом редакторе, находим и удаляем две строчки с вызовом функции snmp_log:<br />
<pre style="background-color: lightgrey;"> snmp_log(LOG_WARNING, "Authentication failed for %s\n",
user->name);
</pre>В этом же файле при подобных ошибках функции snmp_log не вызываются и это единственный вызов snmp_log в этом файле. Скорее всего этот вызов функции был оставлен по ошибке, а не удалён вместе с другими подобными вызовами.<br />
<br />
Чтобы изменения в файле snmplib/snmpusm.c попали в заплатку, обновляем заплатку:<br />
<pre style="background-color: black; color: white;">$ quilt refresh</pre>Можно прокомментировать сделанные в пакете изменения и изменить версию пакета:<br />
<pre style="background-color: black; color: white;">$ dch -i</pre>В запустившемся редакторе описываем последние изменения:<br />
<pre style="background-color: lightgrey;">net-snmp (5.7.3+dfsg-5-stupin1) UNRELEASED; urgency=medium
* Fixed SNMPv3 time widnow logic to work with D-Link switches
* Removed redundant log message "Authentication failed from"
-- Vladimir Stupin <vladimir@stupin.su> Fri, 15 May 2020 16:01:26 +0500
</pre>Теперь можно пересобрать deb-пакеты, выполнив команду:<br />
<pre style="background-color: black; color: white;">$ debuild -us -uc</pre>В вышестоящем каталоге появятся готовые deb-пакеты, которые можно установить в систему командой dpkg -i или поместить в репозиторий пакетов при помощи aptly. Почитать об использовании aptly можно в другой моей заметке <a href="https://vladimir-stupin.blogspot.com/2019/08/debian-aptly.html">Создание своего репозитория Debian при помощи aptly</a>.<br />
<br />
После установки исправлений надоедливые сообщения об ошибках пропали.morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0tag:blogger.com,1999:blog-4428473564097379725.post-51200883973948817482020-07-19T13:00:00.000+05:002020-07-19T13:00:05.421+05:00Сертификат удостоверяющего центра Active Directory для веб-серверовНа работе в локальной сети имеется ряд веб-серверов, для которых понадобилось сделать поддержку протокола HTTPS. Поскольку веб-серверы находятся в локальной сети, то публичные удостоверяющие центры сертификаты для них выдать не могут. Можно было сгенерировать самоподписанные сертификаты, но тогда понадобилось бы вносить каждый сертификат в список доверенных в каждом браузере. Но есть выход получше. Т.к. компьютеры под управлением Windows в локальной сети объединены в домен Active Directory, они все должны доверять удостоверяющему центру Active Directory. Можно сгенерировать сертификаты для веб-серверов, подписать их в удостоверяющем центре Active Directory и тогда все браузеры, использующие системное хранилище сертификатов, на таких компьютерах будут автоматически доверять этим сертификатам.<br />
<br />
Насколько я знаю, из самой популярной тройки браузеров только Firefox в конфигурации не использует системное хранилище сертификатов. Если учитывать, что большинство компьютеров в локальных сетях предприятий обычно работают под управлением Windows и включены в домен Active Directory, а большая часть пользователей используют браузеры Chrome и Internet Explorer, то таким образом можно внедрить HTTPS на веб-серверах в локальной сети максимально гладко. В случае с Firefox можно либо включить использование системного хранилища сертификатов Windows в настройках браузера, либо вручную импортировать в браузер корневой сертификат удостоверяющего центра Active Directory. Он всего один и срок его действия больше, чем у сертификатов веб-серверов.<br />
<br />
По возможности лучше генерировать для каждого веб-сервера индивидуальный сертификат и тогда при взломе одного веб-сервера трафик остальных останется защищённым, т.к. злоумышленник не сможет использовать приватный ключ со взломанного сервера для расшифровки перехваченного трафика других веб-серверов или организации атаки посредника. Я же для экономии времени предпочёл сегенерировать один сертификат сразу для всех веб-серверов, находящихся в моей зоне ответственности. Чтобы при необходимости продлить сертификат мне не пришлось бы вспоминать значения полей из запроса на сертификат и не пропустить по невнимательности ни одного из веб-серверов, я подготовил файл конфигурации cert-web.ini такого вида:<br />
<pre style="background-color: lightgrey;">[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
[req_distinguished_name]
countryName = RU
countryName_default = RU
stateOrProvinceName = Bashkortostan Republic
stateOrProvinceName_default = Bashkortostan Republic
localityName = Ufa
localityName_default = Ufa
organizationalUnitName = My Department
organizationalUnitName_default = My Department
commonName = My Company
commonName_default = My Company
commonName_max = 64
[ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = server1.domain1.tld
DNS.2 = server2.domain1.tld
DNS.3 = server3.domain1.tld
DNS.4 = server4.domain2.tld
DNS.5 = server5.domain2.tld
DNS.6 = server6.domain3.tld</pre>В конфигурации упоминаются:<br />
<ul><li>RU - код страны,</li>
<li>Bashkortostan Republic - административная единица внутри страны,</li>
<li>Ufa - название населённого пункта,</li>
<li>My Company - название компании,</li>
<li>My Department - название подразделения компании,</li>
<li>serverX.domainY.tld - доменные имена веб-серверов в локальной сети.</li>
</ul>Генерируем приватный ключ:<br />
<pre style="background-color: black; color: white;">$ openssl genrsa -out cert-web.key 2048</pre>Генерируем запрос на сертификат в соответствии с файлом конфигурации:<br />
<pre style="background-color: black; color: white;">$ openssl req -config cert-web.ini -new -key cert-web.key -out cert-web.csr</pre>Запрос на сертификат из файла cert-web.csr я передал администратору домена Active Directory, который подписал его в удостоверяющем центре и вернул мне подписанный сертификат cert-web.cer в формате DER.<br />
<br />
Полученный сертификат надо преборазовать из формата DER в формат PEM:<br />
<pre style="background-color: black; color: white;">$ openssl x509 -inform der -in cert-web.cer -out cert-web.crt</pre>Осталось соединить приватный ключ и сертификат в формате PEM для использования получившегося файла веб-сервером:<br />
<pre style="background-color: black; color: white;">$ cat cert-web.key cert-web.crt > cert-web.pem</pre>Остаётся положить получившийся один файл на веб-серверы и задействовать их использование в конфигурациях веб-серверов. Если использовать систему автоматизированного управления конфигурациями, то эта задача не займёт много времени. После освоения Ansible я стал раскладывать сертификаты на веб-серверы именно с её помощью.<br />
<br />
Для того, чтобы получить корневой сертификат удостоверяющего центра Active Directory, нужно зайти веб-браузером на сервер с удостоверяющим центром, где можно будет найти и скачать корневой сертификат. В моём случае корневой сертификат удостоверяющего центра был доступен по ссылке вида: https://domain1.tld/certsrv/certnew.cer?ReqID=CACert&Renewal=2&Mode=inst&Enc=b64<br />
<br />
Т.к. на веб-серверах был доступен API, который использовался на других серверах, то корневой сертификат понадобилось добавить так же и на эти серверы. В случае с Debian это можно сделать способом, описанным ниже.<br />
<br />
Сначала устанавливаем стандартные сертификаты удостоверяющих центров, если они ещё не были установлены:<br />
<pre style="background-color: black; color: white;"># apt-get install ca-certificates</pre>Кладём корневой сертификат нашего удостоверяющего центра в каталог /usr/local/share/ca-certificates/, предназначенный специально для дополнительных сертификатов удостоверяющих центров.<br />
<br />
Обновляем список корневых сертификатов, которым должна доверять библиотека openssl:<br />
<pre style="background-color: black; color: white;"># update-ca-certificates</pre>После этого все установленные в системе программы должны начать доверять сгенерированным нами сертификатам веб-серверов. Если этого не случилось и какая-то программа или модуль не начали доверять новым сертификатам, изучите документацию. Например, для того, чтобы модуль urllib2 для Python начал доверять сертификатам, мне понадобилось передавать библиотеке urllib2 дополнительные настройки, описанные в заметке: <a href="https://vladimir-stupin.blogspot.com/2019/11/ssl-urllib2.html">Проверка действительности SSL-сертификата в urllib2</a>morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0tag:blogger.com,1999:blog-4428473564097379725.post-1607343844023600632019-12-29T13:00:00.000+05:002019-12-29T13:00:03.413+05:00Решение проблемы с SSL/TLS в SylpheedНа рабочем компьютере решил обновить Debian с релиза Stretch до Buster и столкнулся с проблемой: при попытке проверить почту Sylpheed на POP3-сервере вылетает окно с ошибкой:<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLoXk62hHEShSEzTX82Fva6-MmmqPB73-4DSFBWsnGjTiOnuJcbXzOH08I3etj1T2f41gSBRR4dhZUykaolF5zcf1sJ3chGCTrAlympkLHf1KH6NcO62GzB3U7O5nqxFvm-6Y94QOPBRBy/s1600/sylpheed_ssl_error.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiLoXk62hHEShSEzTX82Fva6-MmmqPB73-4DSFBWsnGjTiOnuJcbXzOH08I3etj1T2f41gSBRR4dhZUykaolF5zcf1sJ3chGCTrAlympkLHf1KH6NcO62GzB3U7O5nqxFvm-6Y94QOPBRBy/s1600/sylpheed_ssl_error.png" data-original-width="168" data-original-height="156" /></a><br />
Если попытаться запустить Sylpheed через окно терминала, можно увидеть чёть больше подробностей:<br />
<pre style="background-color: black; color: white;">$ sylpheed
(sylpheed:7387): LibSylph-WARNING **: 15:42:26.003: SSL_connect() failed with error 1, ret = -1 (error:1425F102:SSL routines:ssl_choose_client_version:unsupported protocol)
(sylpheed:7387): LibSylph-WARNING **: 15:42:26.003: can't start TLS session.
(sylpheed:7387): LibSylph-WARNING **: 15:42:26.004: [15:42:26] Сбой сокета.
</pre>Как видно, POP3-сервер не поддерживает требуемую версию протокола SSL/TLS.<br />
<br />
В прошлом мне уже приходилось разбираться с подобной проблемой при отправке уведомлений из Zabbix на сервер Jabber: <a href="https://vladimir-stupin.blogspot.com/2019/06/libiksemel-jabber-tls-handshake-failed.html">Пересборка libiksemel для решения проблемы JABBER tls handshake failed в Zabbix</a>. В этот раз я попробовал пойти тем же путём, установил утилиту командной строки gnutls-cli-debug и воспользоваться ей для проверки возможностей Jabber-сервера.<br />
<br />
Устанавливаем пакет gnutls-bin:<br />
<pre style="background-color: black; color: white;"># apt-get install gnutls-bin</pre>Вызываем утилиту для получения списка возможностей Jabber-сервера:<br />
<pre style="background-color: black; color: white">$ gnutls-cli-debug -p 995 mail.server.ru</pre>Утилита выводит следующую информацию:<br />
<pre style="background-color: black; color: white;">GnuTLS debug client 3.6.7
Checking mail.server.ru:995
whether we need to disable TLS 1.2... no
whether we need to disable TLS 1.1... no
whether we need to disable TLS 1.0... no
whether %NO_EXTENSIONS is required... no
whether %COMPAT is required... no
for TLS 1.0 (RFC2246) support... yes
for TLS 1.1 (RFC4346) support... no
fallback from TLS 1.1 to... TLS 1.0
for TLS 1.2 (RFC5246) support... no
for TLS 1.3 (RFC8446) support... no
|<1>| FFDHE groups advertised, but server didn't support it; falling back to server's choice
TLS1.2 neg fallback from TLS 1.6 to... TLS1.0
for HTTPS server name... unknown
for certificate chain order... sorted
for safe renegotiation (RFC5746) support... yes
for encrypt-then-MAC (RFC7366) support... no
for ext master secret (RFC7627) support... no
for heartbeat (RFC6520) support... no
for version rollback bug in RSA PMS... no
for version rollback bug in Client Hello... no
whether the server ignores the RSA PMS version... no
whether small records (512 bytes) are tolerated on handshake... yes
whether cipher suites not in SSL 3.0 spec are accepted... yes
whether a bogus TLS record version in the client hello is accepted... yes
whether the server understands TLS closure alerts... yes
whether the server supports session resumption... no
for anonymous authentication support... no
|<1>| FFDHE groups advertised, but server didn't support it; falling back to server's choice
for ephemeral Diffie-Hellman support... yes
|<1>| FFDHE groups advertised, but server didn't support it; falling back to server's choice
for RFC7919 Diffie-Hellman support... no
for ephemeral EC Diffie-Hellman support... no
for curve SECP256r1 (RFC4492)... no
for curve SECP384r1 (RFC4492)... no
for curve SECP521r1 (RFC4492)... no
for curve X25519 (RFC8422)... no
for AES-GCM cipher (RFC5288) support... no
for AES-CCM cipher (RFC6655) support... no
for AES-CCM-8 cipher (RFC6655) support... no
for AES-CBC cipher (RFC3268) support... yes
for CAMELLIA-GCM cipher (RFC6367) support... no
for CAMELLIA-CBC cipher (RFC5932) support... yes
for 3DES-CBC cipher (RFC2246) support... yes
for ARCFOUR 128 cipher (RFC2246) support... yes
for CHACHA20-POLY1305 cipher (RFC7905) support... no
for MD5 MAC support... yes
for SHA1 MAC support... yes
for SHA256 MAC support... no
for max record size (RFC6066) support... no
for OCSP status response (RFC6066) support... no
</pre>Сервер POP3 не поддерживает новейшие версии протокола TLS1.2 и TLS1.1, но поддерживает TLS1.0. Видимо почтовый клиент пытается использовать более безопасную версию протокола и не соглашается на TLS1.0.<br />
<br />
Решение проблемы несколько затянулось, т.к. первоначально я пошёл по ложному следу. Попробовал удалить сначала пакет openssl, а потом libgnutls30. Пакет sylpheed попадал в список удаляемых пакетов лишь во втором случае. На самом деле sylpheed зависел не от библиотеки libgnutls30, а от библиотеки libssl1.1. Для изменения настроек OpenSSL, которые используются по умолчанию, оказалось достаточно поправить файл /etc/ssl/openssl.cnf.<br />
<br />
В файле были прописаны такие настройки:<br />
<pre style="background-color: lightgrey;">[system_default_sect]
MinProtocol = TLSv1.2
CipherString = DEFAULT@SECLEVEL=2</pre>Для того, чтобы sylpheed успешно подключился к почтовому серверу, оказалось достаточно поменять минимальный требуемый протокол, вот так:<br />
<pre style="background-color: lightgrey;">[system_default_sect]
MinProtocol = TLSv1.0
CipherString = DEFAULT@SECLEVEL=2</pre>morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com2tag:blogger.com,1999:blog-4428473564097379725.post-87978427947358587482019-12-22T13:00:00.002+05:002020-06-30T17:48:40.341+05:00Использование quilt для подготовки заплатВ прошлом я уже описывал несколько заплаток, которые накладываю на Zabbix для решения различных проблем:<br />
<ul><li><a href="https://vladimir-stupin.blogspot.com/2013/12/zabbix-220-debian-wheezy.html">Установка и настройка Zabbix 2.2.0 в Debian Wheezy</a></li>
<li><a href="https://vladimir-stupin.blogspot.com/2019/06/zabbix-34.html">Исправление ручного закрытия проблем в Zabbix 3.4</a></li>
</ul>Кроме этих описанных заплаток имеется ещё несколько специфичных заплаток, которые следаны для интеграции Zabbix со сторонними системами и нигде не описаны, т.к. вряд-ли кого-то заинтересуют.<br />
<br />
Кроме того, накладывать заплатки приходится не только на сам Zabbix, но и на связанные с ним библиотеки:<br />
<ul><li><a href="https://vladimir-stupin.blogspot.com/2019/06/libiksemel-jabber-tls-handshake-failed.html">Пересборка libiksemel для решения проблемы JABBER tls handshake failed в Zabbix</a></li>
<li><a href="https://vladimir-stupin.blogspot.com/2016/09/snmpv3-usmtimewindow.html">Стандарт SNMPv3 и суровая действительность USM_TIME_WINDOW</a></li>
</ul>Правки приходится делать в разных пакетах, не только связанных непосредственно с Zabbix, из-за чего я даже завёл репозиторий для доработанных пакетов. В заметке <a href="https://vladimir-stupin.blogspot.com/2019/08/debian-aptly.html">Создание своего репозитория Debian при помощи aptly</a> можно найти ещё несколько примеров доработанных пакетов, доработке некоторых из которых были посвящены отдельные заметки.<br />
<br />
Количество специфичных заплаток для Zabbix, с которыми приходится работать, со временем только увеличивается. Если aptly помогает упорядочить работу с большим количеством нестандартных пакетов, то quilt помогает упорядочить работу с большим количеством заплаток одного и того же пакета.<br />
<br />
quilt формирует из заплаток стек, позволяя легко вносить обновления в любую из заплаток стека. Для обновления заплатки, погребённой под более поздними, можно отменить изменения, вносимые в исходный код заплатками, лежащими сверху, внести изменения в исходный код, обновить текущую заплатку, а потом снова наложить на код все вышестоящие заплатки.<br />
<br />
Ниже кратко описаны основные команды quilt, которые могут пригодиться для управления заплатками.<br />
<br />
Создаём новую заплатку:<br />
<pre style="background-color: black; color: white;">$ quilt new permit-edit-maintenances</pre>Добавляем в заплатку файлу, которые собираемся менять:<br />
<pre style="background-color: black; color: white;">$ quilt add frontends/php/maintenance.php
$ quilt add frontends/php/include/classes/api/services/CMaintenance.php</pre>Посмотреть список файлов, содержимое которых будет отслеживаться в заплате, можно при помощи команды:<br />
<pre style="background-color: black; color: white;">$ quilt files</pre>Чтобы изменения в файлах не отслеживались в заплате, можно воспользоваться такой командой:<br />
<pre style="background-color: black; color: white;">$ quilt remove config.guess config.sub database/mysql/create.sql database/postgresql/create.sql database/sqlite3/create.sql</pre>Редактируем файлы:<br />
<pre style="background-color: black; color: white;">$ vim frontends/php/maintenance.php
$ vim frontends/php/include/classes/api/services/CMaintenance.php</pre>Посмотреть получившуюся заплатку можно при помощи следующей команды:<br />
<pre style="background-color: black; color: white;">$ quilt diff</pre>Сохранить получившуюся заплатку в каталог debian/patches можно при помощи следующей команды:<br />
<pre style="background-color: black; color: white;">$ quilt refresh</pre>Список всех заплаток можно посмотреть при помощи команды:<br />
<pre style="background-color: black; color: white;">$ quilt series</pre>Текущая редактируемая заплата в выведенном списке будет подсвечена.<br />
<br />
При необходимости редактировать не последнюю заплатку, можно перемещаться по списку заплат. Для перемещения по списку на предыдущую заплату можно воспользоваться командой:<br />
<pre style="background-color: black; color: white;">$ quilt pop</pre>Для применения текущей заплаты и для перехода к следующей по списку можно воспользоваться командой:<br />
<pre style="background-color: black; color: white;">$ quilt push</pre>Импорт заплатки из внешнего источинка осуществяется следующим образом:<br />
<pre style="background-color: black; color: white;">$ quilt import zabbix3_4_12_permit_edit_maintenances.patch</pre>Сразу после импорта заплатку нужно применить:<br />
<pre style="background-color: black; color: white;">$ quilt push</pre>Не стоит продолжать импорт заплаток без применения, т.к. применяться они будут в обратном порядке - послденяя импортированная будет первой применённой. Это сбивает с толку и может вызывать проблемы, если заплатки зависят друг от друга.morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0tag:blogger.com,1999:blog-4428473564097379725.post-76563774844636656862019-12-15T13:00:00.001+05:002020-01-12T11:46:38.141+05:00OpenSMTPd как локальный SMTP-ретрансляторВ прошлом я писал заметки <a href="https://vladimir-stupin.blogspot.com/2014/06/postfix-smtp.html">Postfix как локальный SMTP-ретранслятор</a> и <a href="https://vladimir-stupin.blogspot.com/2016/03/postfix-smtp-freebsd.html">Postfix как локальный SMTP-ретранслятор во FreeBSD</a>, в которых описывал настройку Postfix для пересылки писем администратору системы.<br />
<br />
К списку самых распространённых полноформатных SMTP-серверов для Unix, в который входят Sendmail, Exim, Postfix и, с некоторой натяжкой, qmail, присоединился ещё один - OpenSMTPd. Этот сервер был разработан в рамках проекта OpenBSD и по архитектуре напоминает Postfix и qmail: он тоже состоит из нескольких отдельных взаимодействующих друг с другом процессов.<br />
<br />
OpenSMTPd появился в репозиториях Debian и я решил попробовать настроить его в качестве замены Postfix из указанных выше статей. Первым делом установим OpenSMTPd из пакетов:<br />
<pre style="background-color: black; color: white;"># apt-get install opensmtpd</pre>Приводим файл конфигурации /etc/smtpd.conf к следующему виду:<br />
<pre style="background-color: lightgrey;">listen on lo
table aliases file:/etc/aliases
table secrets file:/etc/secrets
accept from local for local virtual <aliases> deliver to mbox
accept from local for any relay via tls+auth://user@mail.server.net:25 auth <secrets> as "user@server.net"</pre>В файл /etc/aliases прописываем переадресации для получателей писем:<br />
<pre style="background-color: lightgrey;">postmaster: root
root: admin@domain.tld</pre>В файл /etc/secrets прописываем пароли для учётных записей:<br />
<pre style="background-color: lightgrey;">user password</pre>Имя user должно совпадать с указанным в URL релея tls+auth://user@mail.server.net:25.<br />
<br />
Поменяем права доступа к созданным файлам:<br />
<pre style="background-color: black; color: white;"># chown root:root /etc/aliases /etc/secrets /etc/smtpd.conf
# chmod u=rw,go=r /etc/aliases /etc/smtpd.conf
# chmod u=rw,go= /etc/secrets</pre>Проверить правильность файла конфигурации можно при помощи следующей команды:<br />
<pre style="background-color: black; color: white;"># smtpd -n</pre>Запускаем почтовый сервер:<br />
<pre style="background-color: black; color: white;"># systemctl restart opensmtpd</pre>При редактировании файлов, указанных в опциях table, нужно сообщить об изменении демону, чтобы он перечитал содержимое таблиц. Например, после редактирования таблицы aliases нужно воспользоваться такой командой:<br />
<pre style="background-color: black; color: white;"># smtpctl update table aliases</pre>К сожалению, пока что в OpenSMTPd нельзя менять тему писем таким же образом, как это было описано в заметке <a href="https://vladimir-stupin.blogspot.com/2017/03/postfix.html">Смена темы письма в Postfix</a>. У Postfix простой файл конфигурации, но большое количество настроек, взаимодействующих между собой неочевидным образом. В этом плане OpenSMTPd выглядит достойной альтернативой, т.к. сочетает простоту и наглядность настройки с безопасной архитектурой.<br />
<br />
Использованные материалы:<br />
<ul><li><a href="https://cgar.github.io/posts/Simple-OpenSMTPD-Relay.html">A simple relay via OpenSMTPD</a></li>
</ul>morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0tag:blogger.com,1999:blog-4428473564097379725.post-85133675229050675892019-12-08T13:00:00.002+05:002019-12-09T10:06:54.249+05:00Резервное копирование файлов и баз данных через SSHКогда-то давно на работе задумались о резервном копировании серверов. У меня к тому времени уже был наработанный подход к резервному копированию, которым пользуются многие системные администраторы Unix.<br />
<br />
Полную резервную копию сервера снимать не имеет смысла, а стоит снимать резервные копии только самого важного, что может пригодиться при настройке сервера с нуля или для восстановления после ошибок. В случае аварий, когда бывает нужно восстановить что-то из резервной копии, меньше всего хочется бороться с какой-то системой резервного копирования, поэтому вместо какой-то готовой системы лучше ограничиться скриптом. При небольшом количестве серверов и небольшом объёме резервных копий скрипт, снимающий резервные копии последовательно, успевает полностью отработать за несколько часов.<br />
<br />
Поэтому и в тот раз я решил воспользоваться тем же подходом. Единственное дополнение, которое захотелось сделать - это класть резервные копии сразу в два географически разнесённых хранилища. Одна беда - блог я тогда ещё не вёл и не имел привычки выкладывать куда-то полезные наработки, поэтому скрипт на новом рабочем месте пришлось писать заново. Сейчас подумал, что неплохо всё-таки привести этот скрипт в презентабельный вид и написать о нём статью.<br />
<h3>Управление архивом</h3>Начнём с того, что нужно организовать резервные копии в хранилище таким образом, чтобы можно было легко найти нужную резервную копию и удалять устаревшие резервные копии. Файлы будем именовать по схеме YYYYMMDD_filename. При добавлении новой резервной копии filename нужно будет удостовериться, что новый файл не пуст. Если с файлом всё в порядке, то можно снабдить его префиксом YYYYMMDD_, а затем найти и удалить устаревшие резервные копии этого файла. Для выполнения этих функций в скрипте предусмотрены переменная KEEP_DAYS и функция с не самым удачным названием clear_old, которая берёт на себя описанные задачи:<br />
<pre style="background-color: lightgrey;">KEEP_DAYS=14
clear_old()
{
# $1 - backup filename
# Если файл с указанным именем не существует
if [ ! -f "$1" ]
then
return 0
fi
size=`du --bytes "$1" | cut -f1`
# Если размер меньше 512 байт
if [ $size -lt 512 ]
then
# Удаляем сам файл резервной, а старые резервные копии не трогаем
rm $1
return 1
else
# Переименовываем новый файл резервной копии
mv $1 `date "+%Y%m%d"`_$1
# Удаляем предыдущие файлы старше KEEP_DAYS дней
find . -name \*$1 -mtime +$KEEP_DAYS -delete
return 0
fi
}</pre>Чтобы добавить файл в архив, нужно вызвать функцию clear_old и передать ей имя добавляемого в архив файла.<br />
<h3>Резервное копирование локальных файлов</h3>Начнём с простого. Резервные копии нужно снимать не только с других компьютеров, но и с самого компьютера, на котором будет работать скрипт. В моём случае в разное время это был сервер FreeBSD и виртуальная машина с Linux. В дальнейшем скрипт был перемещён на выделенную виртуальную машину с Linux, которая выполняла только функции резервного копирования. Острая необходимость снимать резервные копии самой виртуальной машины, где работал скрипт резервного копирования, отпала, но функции для этого уже были разработаны, поэтому на всякий случай резервные копии локальных файлов продолжают сниматься.<br />
<br />
В случае с FreeBSD функция резервного копирования файлов выглядела следующим образом:<br />
<pre style="background-color: lightgrey;">BACKUP_USER=rbackup
BACKUP_GROUP=rbackup
freebsd_files_local()
{
# $1 - backup filename
touch "$1"
chown $BACKUP_USER:$BACKUP_GROUP "$1"
chmod 0600 "$1"
tar -cjf - -T- --exclude '*.sql.gz' --exclude 'etc/zabbix/xbackup/*' <<END 2>/dev/null > "$1"
/etc/
/usr/local/etc/
/root/
/usr/home/
/usr/local/www/
END
clear_old "$1"
if [ $? -ne 0 ]
then
echo "Backing up local system to file $1 failed"
fi
}</pre>Функция принимает один аргумент - имя файла, в котором нужно сохранить резервную копию файлов. Создаваемый файл архива будет иметь формат .tbz (или .tar.bz2).<br />
<br />
Перед созданием архива сначала создаётся пустой файл, владельцем которого становится пользователь, указанный в переменной BACKUP_USER, группой-владельцем становится группа, указанная в переменной BACKUP_GROUP, права доступа к файлу выставляются таким образом, что читать и писать его может только пользователь, указанный в переменной BACKUP_USER. Это делается для того, чтобы предотвратить чтение файла резервной копии посторонними пользователями в процессе его создания.<br />
<br />
В архив помещаются файлы из каталогов /etc/, /usr/local/etc/, /usr/local/www/, /root/ и /usr/home/, кроме файлов с расширением .sql.gz и файлов из каталога /etc/zabbix/xbackup/. По-сути, в архив сохраняются файлы конфигурации, файлы из домашних каталогов пользователей и файлы веб-приложений, за исключением файлов с резервными копиями баз данных.<br />
<br />
После создания архива для файла вызывается функция clear_old, которая переименовывает файл, снабжая его имя префиксом YYYYMMDD_ с текущей датой, и удаляет устаревшие экземпляры этого файла.<br />
<br />
В переменных BACKUP_USER и BACKUP_GROUP выше указаны пользователь и группа с именем rbackup. Чтобы создать их, можно воспользоваться такими командами:<br />
<pre style="background-color: black; color: white;"># pw add group rbackup
# pw add user rbackup -g rbackup -c "User for backup purposes" -d /usr/home/rbackup -m</pre>В случае с Linux функция резервного копирования файлов была такой:<br />
<pre style="background-color: lightgrey;">linux_files_local()
{
# $1 - backup filename
touch "$1"
chown $BACKUP_USER:$BACKUP_GROUP "$1"
chmod 0600 "$1"
tar -cjf - --files-from=- --exclude 'home/*/.pycharm_helpers/*' --exclude 'root/.cpan/*' --exclude 'root/.cache/*' --exclude 'home/*/.cache/*' --exclude 'usr/local/lib/*' <<END 2>/dev/null > "$1"
/etc/
/root/
/home/
/usr/local/
/usr/lib/zabbix/
/usr/share/mapnik/
/var/www/
/var/lib/dokuwiki/
END
clear_old "$1"
if [ $? -ne 0 ]
then
echo "Backing up local system to file $1 failed"
fi
}</pre>В целом эта функция не отличается от функции для FreeBSD. Резервному копированию подвергаются каталог с файлами конфигурации, домашние каталоги пользователей, каталог веб-приложений, а также каталог /usr/local/, и каталоги с файлами Zabbix и DokuWiki. Из резервного копирования исключаются файлы, создаваемые средой разработки PyCharm (она умеет работать по SSH), каталоги с кэшированными данными, модулями Perl.<br />
<br />
Аналогично FreeBSD, в Linux нужно создать пользователя и группу rbackup, который будут использоваться в качестве владельца резервных копий:<br />
<pre style="background-color: black; color: white;"># groupadd rbackup
# useradd -c "User for backup purposes" -d /home/rbackup -m -g rbackup rbackup</pre><h3>Резервное копирование файлов с удалённых систем</h3>Резервное копирование удалённых файловых систем работает аналогично, с той лишь разницей, что команды резервного копирования запускаются по SSH, а их стандартный вывод сохраняется в файл в локальной файловой системе.<br />
<pre style="background-color: lightgrey;">SSH_PRIVKEY=/root/.ssh/id_rsa
freebsd_files()
{
# $1 - server ip or dns-name
# $2 - server ssh port
# $3 - backup filename
touch "$3"
chown $BACKUP_USER:$BACKUP_GROUP "$3"
chmod 0600 "$3"
ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o Port=$2 -i $SSH_PRIVKEY $BACKUP_USER@$1 "/usr/local/bin/sudo /usr/bin/tar -cjf - -T- --exclude '*.sql.gz' --exclude 'etc/zabbix/xbackup/*'" <<END 2>/dev/null > "$3"
/etc/
/usr/local/etc/
/root/
/usr/home/
/usr/local/www/
END
clear_old "$3"
if [ $? -ne 0 ]
then
echo "Backing up remote system $1:$2 to file $3 failed"
fi
}
</pre>Функции передаются три аргумента:<br />
<ol><li>IP-адрес или доменное имя удалённой системы, резервную копию файлов с которой нужно снять,</li>
<li>порт SSH-сервера на этой системе (было время, когда использовались системы с SSH-сервером на нестандартном порту),</li>
<li>имя файла создаваемого архива.</li>
</ol>Файлы, подлежащие резервному копированию, без особых проблем сможет прочитать только пользователь root. Однако, в целях безопасности, не хотелось бы разрешать удалённый доступ по SSH для пользователя root, пусть аутентификация и производится с использованием ключей. Не хочется также разрешать скрипту резервного копирования выполнять по SSH какие угодно команды. Для того, чтобы скрипт резервного копирования не смог повредить систему, случайно - из-за ошибки администратора, или специально - если доступ к скрипту получил злоумышленник, резервное копирование производится с использованием учётной записи из переменной BACKUP_USER и sudo.<br />
<br />
Для копирования публичного SSH-ключа в домашний каталог пользователя из переменной BACKUP_USER на FreeBSD я пользовался такими командами, которые просто копировал в терминал при настройке нового сервера или виртуальной машины:<br />
<pre style="background-color: black; color: white;"># chown rbackup:rbackup /usr/home/rbackup
# mkdir /home/rbackup/.ssh
# chown rbackup:rbackup /usr/home/rbackup/.ssh
# cat <<END > /usr/home/rbackup/.ssh/authorized_keys
ТУТ ПУБЛИЧНЫЙ SSH-КЛЮЧ
END
# chown rbackup:rbackup /usr/home/rbackup/.ssh/authorized_keys</pre>Для того, чтобы разрешить пользователю rbackup запускать через sudo команду tar для резервного копирования файлов, я использовал запускал visudo и вставлял такие настройки:<br />
<pre style="background-color: lightgrey;">Defaults:rbackup !requiretty
rbackup ALL=(root:ALL) NOPASSWD:/usr/bin/tar -cjf - -T- *</pre>Для Linux аналогичная функция выглядит следующим образом:<br />
<pre style="background-color: lightgrey;">linux_files()
{
# $1 - server ip or dns-name
# $2 - server ssh port
# $3 - backup filename
touch "$3"
chown $BACKUP_USER:$BACKUP_GROUP "$3"
chmod 0600 "$3"
ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o Port=$2 -i $SSH_PRIVKEY $BACKUP_USER@$1 "/usr/bin/sudo /bin/tar -cjf - --files-from=- --exclude 'home/*/.pycharm_helpers/*' --exclude 'root/.cpan/*' --exclude 'root/.cache/*' --exclude 'home/*/.cache/*' --exclude 'usr/local/lib/*'" 2>/dev/null <<END > "$3"
/etc/
/root/
/home/
/usr/local/
/usr/lib/zabbix/
/usr/share/mapnik/
/var/www/
/var/lib/dokuwiki/
END
clear_old "$3"
if [ $? -ne 0 ]
then
echo "Backing up remote system $1:$2 to file $3 failed"
fi
}</pre>Для настройки публичных SSH-ключей использовались аналогичные команды:<br />
<pre style="background-color: black; color: white;"># mkdir /home/rbackup/.ssh
# chown rbackup:rbackup /home/rbackup/.ssh
# cat <<END > /home/rbackup/.ssh/authorized_keys
ТУТ ПУБЛИЧНЫЙ SSH-КЛЮЧ
END
# chown rbackup:rbackup /home/rbackup/.ssh/authorized_keys</pre>Для настройки прав в sudo использовались такие строчки:<br />
<pre style="background-color: lightgrey;">Defaults:rbackup !requiretty
rbackup ALL=(root:ALL) NOPASSWD:/bin/tar -cjf - --files-from=- *</pre><h3>Резервное копирование удалённой базы данных MySQL</h3>MySQL является сетевым сервером, поэтому резервные копии баз данных можно снимать по сети, не прибегая к помощи SSH. Однако, для того, чтобы не заниматься настройкой фильтрации пакетов, а также не гонять по сети лишний объём данных в открытом виде, резервное копирование баз данных было решено выполнять тоже через SSH.<br />
<pre style="background-color: lightgrey;">PASSWORD=тут-пароль
generic_mysql()
{
# $1 - server ip or dns-name
# $2 - server ssh port
# $3 - database name
# $4 - backup filename
touch "$4"
chown $BACKUP_USER:$BACKUP_GROUP "$4"
chmod 0600 "$4"
ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o Port=$2 -i $SSH_PRIVKEY $BACKUP_USER@$1 "mysqldump --single-transaction -u$BACKUP_USER -p$PASSWORD $3 | bzip2" 2>/dev/null > "$4"
clear_old "$4"
if [ $? -ne 0 ]
then
echo "Backing up $3 database from system $1:$2 to file $4 failed"
fi
}</pre>Функция принимает 4 аргумента:<br />
<ol><li>IP-адрес или доменное имя удалённой системы, резервную копию файлов с которой нужно снять,</li>
<li>порт SSH-сервера на этой системе (было время, когда использовались системы с SSH-сервером на нестандартном порту),</li>
<li>имя базы данных, резервную копию которой нужно снять,</li>
<li>имя файла создаваемого архива.</li>
</ol>Функция вызывает mysqldump для получения резервной копии требуемой базы данных, а для сжатия данных перед отправкой через сеть используется компрессор bzip2. Соответственно, резервная копия, которая будет получена при помощи этой функции, имеет формат .sql.bz2.<br />
<br />
Для снятия резервной копии используется пользователь с тем же именем, который является владельцем архивов и используется для подключения к удалённым системам по SSH. Его имя настроено в переменной BACKUP_USER. А для аутентификации этого пользователя используется пароль, указанный в переменной PASSWORD. Понятно, что этот пользователь должен создан и ему должны быть предоставлены права доступа к указанным базам данных. Для этого можно воспользоваться такими запросами:<br />
<pre style="background-color: lightgrey;">CREATE USER 'rbackup'@'localhost' IDENTIFIED BY 'тут-пароль';
FLUSH PRIVILEGES;
GRANT SHOW DATABASES, SELECT, LOCK TABLES, RELOAD ON *.* TO 'rbackup'@'localhost';
FLUSH PRIVILEGES;</pre>Т.к. в функции не используется каких-то специфичных путей к файлам и нет необходимости указывать полный путь к команде, выполняемой через sudo, то эта функция пригодна для использования и с FreeBSD и с Linux.<br />
<h3>Резервное копирование удалённой базы данных PostgreSQL</h3>Функция резервного копирования базы данных PostgreSQL в целом аналогична функции для резервного копирования базы данных MySQL. Для снятия резервной копии используется утилита pg_dump:<br />
<pre style="background-color: lightgrey;">generic_pgsql()
{
# $1 - server ip or dns-name
# $2 - server ssh port
# $3 - database name
# $4 - backup filename
touch "$4"
chown $BACKUP_USER:$BACKUP_GROUP "$4"
chmod 0600 "$4"
ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o Port=$2 -i $SSH_PRIVKEY $BACKUP_USER@$1 "env PGPASSWORD=$PASSWORD pg_dump -U$BACKUP_USER $3 | bzip2" > "$4" 2>/dev/null
clear_old "$4"
if [ $? -ne 0 ]
then
echo "Backing up $3 database from system $1:$2 to file $4 failed"
fi
}</pre>Для снятия резервной копии нужно создать пользователя, указанного в переменной BACKUP_USER, и предоставить ему права доступа ко всем базам данных, резервные копии которых будет необходимо снимать. Войдя в систему под пользователем postgres, создаём пользователя для резервного копирования и вводим его пароль в процессе его создания:<br />
<pre style="background-color: black; color: white;">$ createuser -D -R -I -S -P rbackup</pre>Теперь нужно подключиться к каждой из баз данных при помощи команды psql -d база-данных и выполнить следующие запросы:<br />
<pre style="background-color: lightgrey;">GRANT SELECT ON ALL TABLES IN SCHEMA public TO rbackup;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO rbackup;</pre><h3>Группировка действий по резервному копированию</h3>Для снятия всех необходимых резервных копий с одного сервера можно создавать функции следующего вида:<br />
<pre style="background-color: lightgrey;">backup_server()
{
linux_files 'server.domain.tld' 22 'server.tbz'
generic_mysql 'server.domain.tld' 22 'mysql' 'server_mysql.sql.bz2'
generic_mysql 'server.domain.tld' 22 'base' 'server_base.sql.bz2'
generic_pgsql 'server.domain.tld' 22 'database' 'server_database.sql.bz2'
}</pre><h3>Выполнение резервного копирования</h3>Теперь в скрипте имеются функции для снятия резервных копий каждого сервера. Осталось только создать каталог для хранения резервных копий и выполнить резервное копирование. Для этого создадим каталог и выставим права доступа к нему:<br />
<pre style="background-color: black; color: white;"># mkdir /backups
# chown rbackup:rbackup /backups
# chmod u=rwx,g=rx,o= /backups</pre>Теперь впишем в скрипт команды перехода в каталог с резервными копиями, на всякий случай поменяем маску создаваемых файлов и последовательно вызовем функции резервного копирования всех требуемых серверов:<br />
<pre style="background-color: lightgrey;">cd /backups
umask 0077
backup_server1
backup_server2</pre><h3>Резервное копирование резервных копий</h3>Не стоит складывать все яйца в одну корзину. На случай, если с резервными копиями в основном месте хранения что-нибудь случится, можно выполнить резервное копирование резервных копий на другой сервер. Для этого на другом сервере создаётся аналогичный каталог, в который при помощи rsync синхронизируются изменения из каталога на основном сервере:<br />
<pre style="background-color: lightgrey;">/usr/bin/rsync -a --delete-after -e "ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no -o Port=22 -i $SSH_PRIVKEY" /backups/ $BACKUP_USER@backup-server.domain.tld.:/backups/</pre>Один процесс rsync запускается локально, один процесс rsync запускается удалённо по SSH. Оба процесса обмениваются друг с другом информацией через SSH. Взаимодействуя друг с другом, они копируют недостающие файлы, обновляют фрагменты изменившихся файлов, удаляют файлы, ставшие не нужными.<br />
<h3>Отладочная информация</h3>Для того, чтобы иметь представление о времени начала и завершения тех или иных этапов резервного копирования, можно добавлять в функции или в тело скрипта отладочные сообщения с отметками времени. Например, в свой скрипт я вставил отладочные сообщения, фиксирующие моменты начала и завершения обновления основного архива и моменты начала и завершения синхронизации резервного архива:<br />
<pre style="background-color: lightgrey;">echo "BACKUP FINISHED: "`date "+%Y-%m-%d %H:%M:%S"`
# тут резервное копирование серверов
echo "BACKUP STARTED: "`date "+%Y-%m-%d %H:%M:%S"`
echo "RSYNC STARTED: "`date "+%Y-%m-%d %H:%M:%S"`
# тут вызов rsync
echo "RSYNC FINISHED: "`date "+%Y-%m-%d %H:%M:%S"`</pre><h3>Заключение</h3>Почти каждому системному администратору Unix когда-нибудь приходилось писать свой вариант такого скрипта. Буду рад, если вы поделитесь собственными советами и наработками. Возможно кто-то может поделиться соображениями, в каких случаях такой подход не годится, и какие решения лучше использовать взамен.morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com2tag:blogger.com,1999:blog-4428473564097379725.post-49219184395643440382019-12-01T13:00:00.002+05:002020-01-31T14:11:03.978+05:00Скрипты управления списком IP-адресов в iptables/ipset и ipfw/tableГода 4 назад на работе перевёл всех Zabbix-агентов в активный режим, т.к. этот режим должен быть эффективнее чем опрос обычных пассивных Zabbix-агентов. Для снятия данных с обычных Zabbix-агентов сервер Zabbix сам устанавливает подключение к Zabbix-агенту, запрашивает у него необходимые метрики, после чего отключается. Для этого сервер Zabbix используют процессы poller, каждый из которых бывает занят не только во время активных действий, но и во время ожидания данных от Zabbix-агента. Если же Zabbix-агент работает в активном режиме, то сервер Zabbix не предпринимает никаких активных действий, а ждёт действий со стороны агента. Активный Zabbix-агент подключается к серверу Zabbix, запрашивает у него список метрик, за которыми нужно наблюдать, и периодичность их контроля. После этого Zabbix-агент самостоятельно собирает данные с необходимой периодичностью и отправляет их на сервер Zabbix. В этом случае сервер Zabbix использует процессы trapper, которые работают только во время приёма уже готовых данных. На самом деле на фоне общей нагрузки снижение использования ресурсов оказалось совсем незаметным, но речь сейчас не об этом.<br />
<br />
После перевода Zabbix-агентов в активный режим появилась другая маета (-: или муда в терминологии кайдзен) - бывает нужно вносить в сетевой фильтр IP-адреса сети, в которых есть активные Zabbix-агенты. До поры до времени это требовалось делать очень редко. Потом сеть стала расти очень быстро и вносить новые IP-адреса и сети в сетевой фильтр стало нужно с завидной регулярностью. С одной стороны, чтобы сэкономить время, можно добавлять сразу целые сети. С другой стороны - в Zabbix нет никаких средств защиты от подделки данных: протокол позволяет запросить конфигурацию любого Zabbix-агента, указав его имя, и отправить в Zabbix данные от имени любого другого Zabbix-агента. Сервер Zabbix не имеет даже средств для определения конфликтующих Zabbix-агентов, которые работают на разных компьютерах, но имеют одно и то же сетевое имя, отправляя поочерёдно разные данные.<br />
<br />
Чтобы автоматизировать процесс добавления IP-адресов в сетевой фильтр на сервере Zabbix, а также максимально снизить возможность отправки поддельных данных с любого свободного IP-адреса, решил написать скрипт, который будет извлекать из базы данных Zabbix список IP-адресов интерфейсов из тех сетевых узлов, на которых есть элементы данных, имеющие тип "Zabbix-агент (активный)".<br />
<br />
Для Linux с его iptables и ipset получился такой скрипт под названием ipset_auto.sh, который можно поместить в планировщик задач cron:<br />
<pre style="background-color: lightgrey;">#!/bin/sh
AWK="/usr/bin/awk"
SORT="/usr/bin/sort"
UNIQ="/usr/bin/uniq"
IPSET="/sbin/ipset"
XARGS="/usr/bin/xargs"
update()
{
SET="$1"
NEED_IPS="$2"
CURRENT_IPS=`$IPSET list $SET | $AWK '/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/ { print $0; }'`
DIFF_IPS=`(echo "$NEED_IPS" ; echo -n "$CURRENT_IPS") | $SORT | $UNIQ -u`
ADD_IPS=`(echo "$NEED_IPS" ; echo -n "$DIFF_IPS") | $SORT | $UNIQ -d`
DEL_IPS=`(echo "$CURRENT_IPS" ; echo -n "$DIFF_IPS") | $SORT | $UNIQ -d`
if [ -n "$ADD_IPS" ]
then
echo "--- $SET add ---"
echo "$ADD_IPS"
echo "$ADD_IPS" | $XARGS -n1 $IPSET add $SET
fi
if [ -n "$DEL_IPS" ]
then
echo "--- $SET del ---"
echo "$DEL_IPS"
echo "$DEL_IPS" | $XARGS -n1 $IPSET del $SET
fi
}
# ZABBIX
MYSQL=`$AWK '/^DBUser=/ { split($0, a, "=");
user = a[2]; }
/^DBPassword=/ { split($0, a, "=");
password = a[2]; }
/^DBName=/ { split($0, a, "=");
db = a[2]; }
/^DBHost=/ { split($0, a, "=");
host = a[2]; }
END { if (user && password && host && db)
print "/usr/bin/mysql --connect-timeout=5 -u" user " -p" password " -h" host " " db;
else if (user && password && db)
print "/usr/bin/mysql --connect-timeout=5 -u" user " -p" password " " db; }' /etc/zabbix/zabbix_server.conf`
if [ -z "$MYSQL" ]
then
echo "MYSQL not defined"
exit
fi
NEED_IPS=`$MYSQL -N <<END 2>/dev/null
SELECT DISTINCT interface.ip
FROM items
JOIN hosts ON hosts.hostid = items.hostid
AND hosts.status = 0
AND hosts.proxy_hostid IS NULL
JOIN interface ON interface.hostid = items.hostid
AND interface.type = 1
AND interface.ip <> '127.0.0.1'
WHERE items.type = 7
AND items.status = 0;
END
`
ERROR=$?
if [ $ERROR -ne 0 ]
then
echo "Failed to execute SQL-query"
exit
fi
update "zabbix_auto" "$NEED_IPS"</pre>Для подключения к базе данных (в данном случае это MySQL, но переделка под другие СУБД тривиальна) скрипт использует настройки из файла конфигурации /etc/zabbix/zabbix_server.conf. Список требуемых IP-адресов в переменной NEED_IPS формируется SQL-запросом, который можно переработать под свои нужды. Например, у меня в скрипте есть ещё пара SQL-запросов, управляющих списками IP-адресов в множествах tftp_auto и ciu_auto. В последней строке скрипта функция update обновляет множество zabbix_auto так, чтобы в нём были только IP-адреса из переменной NEED_IPS.<br />
<br />
Для создания множества IP-адресов zabbix_auto в ipset можно воспользоваться командой:<br />
<pre style="background-color: black; color: white;"># ipset create zabbix_auto hash:ip</pre>Для создания правила в iptables, которое разрешит всем IP-адресам из множества zabbix_auto взаимодействовать с сервером Zabbix, можно воспользоваться командой:<br />
<pre style="background-color: black; color: white;"># iptables -A INPUT -p tcp -m set --match-set zabbix_auto src -m tcp --dport 10051 -j ACCEPT</pre>Аналогичный скрипт для ipfw/table называется ipfw_auto.sh и выглядит следующим образом:<br />
<pre style="background-color: lightgrey;">#!/bin/sh
AWK="/usr/bin/awk"
SED="/usr/bin/sed"
SORT="/usr/bin/sort"
UNIQ="/usr/bin/uniq"
XARGS="/usr/bin/xargs"
update()
{
TABLE="$1"
NEED_IPS="$2"
IPFW=`$AWK -v TABLE="$TABLE" '{ split($0, a, "=");
if (a[1] == TABLE)
{
table = a[2];
print "/sbin/ipfw table " a[2];
}
}' /etc/firewall.conf`
if [ -z "$IPFW" ]
then
echo "IPFW not defined"
exit
fi
CURRENT_IPS=`$IPFW list | $SED -e 's/\/32 0$//'`
DIFF_IPS=`(echo "$NEED_IPS" ; echo -n "$CURRENT_IPS") | $SORT | $UNIQ -u`
ADD_IPS=`(echo "$NEED_IPS" ; echo -n "$DIFF_IPS") | $SORT | $UNIQ -d`
DEL_IPS=`(echo "$CURRENT_IPS" ; echo -n "$DIFF_IPS") | $SORT | $UNIQ -d`
if [ -n "$ADD_IPS" ]
then
echo "--- $TABLE add ---"
echo "$ADD_IPS"
echo "$ADD_IPS" | $XARGS -n1 $IPFW add
fi
if [ -n "$DEL_IPS" ]
then
echo "--- $TABLE del ---"
echo "$DEL_IPS"
echo "$DEL_IPS" | $XARGS -n1 $IPFW delete
fi
}
MYSQL=`$AWK '/^DBUser=/ { split($0, a, "=");
user = a[2]; }
/^DBPassword=/ { split($0, a, "=");
password = a[2]; }
/^DBName=/ { split($0, a, "=");
db = a[2]; }
/^DBHost=/ { split($0, a, "=");
host = a[2]; }
END { if (user && password && host && db)
print "/usr/local/bin/mysql --connect-timeout=5 -u" user " -p" password " -h" host " " db;
else if (user && password && db)
print "/usr/local/bin/mysql --connect-timeout=5 -u" user " -p" password " " db; }' /usr/local/etc/zabbix34/zabbix_server.conf`
if [ -z "$MYSQL" ]
then
echo "MYSQL not defined"
exit
fi
# ZABBIX
NEED_IPS=`$MYSQL -N <<END 2>/dev/null
SELECT DISTINCT interface.ip
FROM items
JOIN hosts ON hosts.hostid = items.hostid
AND hosts.status = 0
AND hosts.proxy_hostid IS NULL
JOIN interface ON interface.hostid = items.hostid
AND interface.type = 1
AND interface.ip <> '127.0.0.1'
WHERE items.type = 7
AND items.status = 0;
END
`
ERROR=$?
if [ $ERROR -ne 0 ]
then
echo "Failed to execute SQL-query"
exit
fi
update "table_zabbix_auto" "$NEED_IPS"</pre>Особенность этого скрипта заключается в том, что в ipfw таблицы не имеют имён, а нумеруются. Номер таблицы выясняется через файл /etc/firewall.conf, в котором переменной с именем таблицы присваивается соответствующий номер. Например, для таблицы table_ssh номер задаётся следующим образом:<br />
<pre style="background-color: lightgrey;">table_ssh=100</pre><br />
Подробнее о настройке ipfw/table можно прочитать в одной из моих прошлых заметок: <a href="https://vladimir-stupin.blogspot.com/2016/10/ipfw-freebsd.html">Настройка ipfw во FreeBSD</a>.<br />
<br />
Активные Zabbix-агенты и база данных Zabbix приведены для примера, а вообще эти скрипты можно приспособить для любых других целей. Можно скачивать список IP-адресов с веб-страницы (главное, чтобы её не подменили и чтобы она не оказалась внезапно пустой), можно воспользоваться в каком-нибудь самодельном биллинге для открытия доступа пользователям, прошедшим авторизацию и закрытия доступа пользователям, превысившим лимит. Можно сочетать одно с другим.<br />
<br />
FreeBSD на работе постепенно заменяем на Debian, поэтому скрипт ipfw_auto.sh скоро станет мне не нужным. Что касается Debian, то netfilter/iptables в Debian Buster уже заменён на nftables/nft. Пока что утилита iptables никуда не делась и умеет работать с nftables, но в будущем скрипт ipset_auto.sh тоже утратит актуальность и потребует переработки. Оба скрипта, однако, пока что могут пригодиться кому-нибудь ещё, поэтому решил поделиться ими.<br />
morbohttp://www.blogger.com/profile/16650057587203469226noreply@blogger.com0