Para monitorear servidores y servicios, hemos utilizado durante mucho tiempo, y aún con éxito, una solución combinada basada en Nagios y Munin. Sin embargo, este grupo tiene una serie de inconvenientes, por lo que, como muchos, estamos explotando activamente
Zabbix . En este artículo, hablaremos sobre cómo puede resolver el problema de rendimiento con un esfuerzo mínimo al aumentar la cantidad de métricas eliminadas y aumentar el volumen de la base de datos MySQL
Problemas al usar una base de datos MySQL con Zabbix
Si bien la base de datos era pequeña y la cantidad de métricas almacenadas en ella era pequeña, todo fue maravilloso. El proceso habitual de limpieza que inicia el servidor Zabbix eliminó con éxito los registros obsoletos de la base de datos, evitando que crezca. Sin embargo, tan pronto como el número de métricas capturadas aumentó y el tamaño de la base de datos alcanzó cierto tamaño, todo empeoró. Houserkeeper dejó de administrar la eliminación de datos para el intervalo de tiempo asignado, los datos antiguos comenzaron a permanecer en la base de datos. Durante la operación del ama de llaves hubo un aumento de carga en el servidor Zabbix, que podría durar mucho tiempo. Quedó claro que era necesario resolver de alguna manera la situación actual.
Este es un problema conocido, casi todos los que trabajaron con grandes volúmenes de monitoreo en Zabbix enfrentaron lo mismo. También hubo varias soluciones: por ejemplo, reemplazar MySQL con PostgreSQL o incluso Elasticsearch, pero la solución más simple y probada fue cambiar a tablas de partición que almacenan datos métricos en la base de datos MySQL. Decidimos ir por este camino.
Migración de tablas MySQL regulares a particionadas
Zabbix está bien documentado y se conocen las tablas donde almacena las métricas. Estas son tablas:
history
, donde se almacenan los valores flotantes,
history_str
, donde se almacenan los valores de cadena corta,
history_text
, donde se almacenan los valores de texto largos, y
history_uint
, donde se almacenan los valores enteros. También hay una tabla de
trends
que almacena la dinámica de los cambios, pero decidimos no tocarla, porque su tamaño es pequeño y un poco más tarde volveremos a ella.
En general, las tablas que se necesitaban procesar estaban claras. Decidimos hacer particiones para cada semana, con la excepción de la última, en función de los números del mes, es decir. cuatro particiones por mes: del 1 al 7, del 8 al 14, del 15 al 21 y del 22 al 1 (próximo mes). La dificultad era que necesitábamos convertir las tablas que necesitábamos en particionadas "sobre la marcha", sin interrumpir Zabbix Server y recopilar métricas.
Curiosamente, la estructura misma de estas tablas nos ayudó en esto. Por ejemplo, la tabla de
history
tiene la siguiente estructura:
`itemid` bigint(20) unsigned NOT NULL, `clock` int(11) NOT NULL DEFAULT '0', `value` double(16,4) NOT NULL DEFAULT '0.0000', `ns` int(11) NOT NULL DEFAULT '0',
mientras que
KEY `history_1` (`itemid`,`clock`)
Como puede ver, cada métrica eventualmente se ingresa en una tabla con dos campos
itemid y
clock muy importantes y convenientes para nosotros. Por lo tanto, podemos crear una tabla temporal, por ejemplo, con el nombre
history_tmp
, configurar una
history_tmp
para ella y luego transferir todos los datos de la tabla de
history
allí, y luego cambiar el nombre de la tabla de
history
a
history_old
y la tabla de
history_tmp
a
history
, luego agregar los datos que hemos rellenado de
history_old
a
history
y eliminar
history_old
. Puede hacerlo de forma completamente segura, no perderemos nada, porque los campos
itemid y
clock indicados anteriormente proporcionan una métrica de enlace a una hora específica y no a algún tipo de número de serie.
El procedimiento de transición en sí
Atencion Es muy deseable, antes de comenzar cualquier acción, hacer una copia de seguridad completa de la base de datos. Todos somos personas vivas y podemos cometer un error en el conjunto de comandos, lo que puede conducir a la pérdida de datos. Si una copia de seguridad no proporcionará la máxima relevancia, pero es mejor tener una que ninguna.
Por lo tanto, no apague ni pare nada. Lo principal es que en el servidor MySQL debería haber una cantidad suficiente de espacio libre en el disco, es decir, de modo que para cada una de las tablas anteriores
history
,
history_text
,
history_str
,
history_uint
, al menos, haya suficiente espacio para crear una tabla con el sufijo "_tmp", dado que será la misma cantidad que la tabla original.
No describiremos todo varias veces para cada una de las tablas anteriores y consideraremos todo con el ejemplo de solo una de ellas: la tabla de
history
.
Por lo tanto, cree una tabla vacía
history_tmp
basada en la estructura de la tabla
history
.
CREATE TABLE `history_tmp` LIKE `history`;
Creamos las particiones que necesitamos. Por ejemplo, hagámoslo por un mes. Cada partición se crea en función de la regla de partición, en función del valor del campo del
reloj , que comparamos con la marca de tiempo:
ALTER TABLE `history_tmp` PARTITION BY RANGE( clock ) ( PARTITION p20190201 VALUES LESS THAN (UNIX_TIMESTAMP("2019-02-01 00:00:00")), PARTITION p20190207 VALUES LESS THAN (UNIX_TIMESTAMP("2019-02-07 00:00:00")), PARTITION p20190214 VALUES LESS THAN (UNIX_TIMESTAMP("2019-02-14 00:00:00")), PARTITION p20190221 VALUES LESS THAN (UNIX_TIMESTAMP("2019-02-21 00:00:00")), PARTITION p20190301 VALUES LESS THAN (UNIX_TIMESTAMP("2019-03-01 00:00:00")) );
Este operador agrega particiones a la tabla
history_tmp
que creamos. Aclaremos que los datos para los cuales el valor del campo del
reloj es menor que “2019-02-01 00:00:00” caerán en la partición
p20190201 , luego los datos para los cuales el valor del campo del
reloj es mayor que “2019-02-01 00:00:00” pero menor "2019-02-07 00:00:00" caerá en la fiesta
p20190207 y así sucesivamente.
Nota importante: ¿Y qué sucede si tenemos datos en la tabla particionada donde el valor del campo del reloj es mayor o igual que "2019-03-01 00:00:00"? Como no hay una partición adecuada para estos datos, no caerán en la tabla y se perderán. Por lo tanto, no debe olvidarse de crear particiones adicionales de manera oportuna, para evitar dicha pérdida de datos (sobre lo que se detalla más adelante)
Entonces, la tabla temporal está preparada. Rellene los datos. El proceso puede llevar bastante tiempo, pero afortunadamente no bloquea ninguna otra solicitud, por lo que solo debe ser paciente:
INSERT IGNORE INTO `history_tmp` SELECT * FROM history;
La palabra clave IGNORE no es necesaria durante el llenado inicial, ya que todavía no hay datos en la tabla, sin embargo, la necesitará al agregar datos. Además, puede ser útil si tuviera que interrumpir este proceso y comenzar de nuevo al completar los datos.
Entonces, después de un tiempo (tal vez incluso unas pocas horas), la primera carga de datos ha pasado. Como comprenderá, ahora la tabla
history_tmp
no contiene todos los datos de la tabla de
history
, sino solo los datos que estaban en ella en el momento en que comenzó la consulta. Aquí, de hecho, tiene una opción: o hacemos un pase más (si el proceso de llenado duró mucho tiempo), o inmediatamente procedemos a cambiar el nombre de las tablas mencionadas anteriormente. Primero, tomemos el segundo pase. Primero, debemos entender la hora del último registro insertado en
history_tmp
:
SELECT max(clock) FROM history_tmp;
Supongamos que recibió:
1551045645 . Ahora usamos el valor obtenido en el segundo paso de llenado de datos:
INSERT IGNORE INTO `history_tmp` SELECT * FROM history WHERE clock>=1551045645;
Este pasaje debería terminar mucho más rápido. Pero si el primer pase se realizó horas, y el segundo también se realizó durante mucho tiempo, puede ser correcto hacer el tercer pase, que se realiza de manera completamente similar al segundo.
Al final, nuevamente realizamos la operación de obtener el tiempo de la última inserción del registro en
history_tmp
haciendo:
SELECT max(clock) FROM history_tmp;
Digamos que tienes
1551085645 . Mantenga este valor: lo necesitaremos para rellenarlo.
Y ahora, en realidad, cuando
history_tmp
datos primarios en
history_tmp
, procedemos a cambiar el nombre de las tablas:
BEGIN; RENAME TABLE history TO history_old; RENAME TABLE history_tmp TO history; COMMIT;
Diseñamos este bloque como una transacción para evitar el momento de insertar datos en una tabla inexistente, porque después del primer RENAME hasta que se ejecute el segundo RENAME, la tabla de
history
no existirá. Pero incluso si llegan algunos datos entre las operaciones RENAME en la tabla de
history
y la tabla en sí aún no existe (debido al cambio de nombre), obtendremos un pequeño número de errores de inserción que pueden descuidarse (tenemos monitoreo, no el banco).
Ahora tenemos una nueva tabla de
history
con particiones, pero no tiene suficientes datos que se recibieron durante el último paso de inserción de datos en la tabla
history_tmp
. Pero tenemos estos datos en la tabla
history_old
y ahora los compartimos desde allí. Para esto, necesitaremos el valor previamente guardado 1551085645. ¿Por qué guardamos este valor y ya no usamos el tiempo de llenado máximo de la tabla de
history
actual? Porque ya hay nuevos datos y llegaremos en el momento equivocado. Entonces, medimos los datos:
INSERT IGNORE INTO `history` SELECT * FROM history_old WHERE clock>=1551045645;
Después del final de esta operación, tenemos en la nueva tabla de
history
particionada todos los datos que estaban en la anterior, más los datos que vinieron después de que la tabla fue renombrada. La tabla
history_old
ya no es necesaria. Puede eliminarlo inmediatamente o puede hacer una copia de seguridad (si tiene paranoia) antes de eliminarlo.
Todo el proceso descrito anteriormente debe repetirse para las
history_str
,
history_text
e
history_uint
.
Lo que debe arreglarse en la configuración del servidor Zabbix
Ahora el mantenimiento de la base de datos con respecto al historial de datos descansa sobre nuestros hombros. Esto significa que Zabbix ya no debería eliminar datos antiguos, lo haremos nosotros mismos. Para que el servidor Zabbix no intente limpiar los datos en sí, debe ir a la interfaz web de Zabbix, seleccionar "Administración" en el menú, luego el submenú "General", luego seleccionar "Borrar historial" en la lista desplegable de la derecha. En la página que aparece, desmarca todas las casillas de verificación para el grupo "Historial" y haz clic en el botón "Actualizar". Esto evitará que las tablas de
history*
sean limpiadas por nosotros a través del ama de llaves.
En la misma página, preste atención al grupo "Dinámica de los cambios". Esta es solo la tabla de
trends
, a la que prometimos volver. Si también se ha vuelto demasiado grande para usted y necesita particionarse, desmarque también este grupo y luego procese esta tabla exactamente como lo hizo para las tablas de
history*
.
Mantenimiento adicional de la base de datos
Como se escribió anteriormente, para el funcionamiento normal en tablas particionadas, es necesario crear particiones a tiempo. Puedes hacer esto así:
ALTER TABLE `history` ADD PARTITION (PARTITION p20190307 VALUES LESS THAN (UNIX_TIMESTAMP("2019-03-07 00:00:00")));
Además, dado que creamos tablas particionadas y prohibimos que Zabbix Server las limpie, la eliminación de datos antiguos ahora es nuestra preocupación. Afortunadamente, no hay problemas en absoluto. Esto se hace simplemente eliminando la partición cuyos datos ya no necesitamos.
Por ejemplo:
ALTER TABLE history DROP PARTITION p20190201;
A diferencia de las declaraciones DELETE FROM con un rango de fechas, DROP PARTITION se completa en un par de segundos, no carga el servidor y funciona igual de bien cuando se usa la replicación en MySQL.
Conclusión
La solución descrita está probada en el tiempo. El volumen de datos está creciendo, pero no hay una desaceleración notable en el rendimiento.