Uso de particionamiento en MySQL para Zabbix con un gran número de objetos de monitoreo

Para la supervisión de servidores y servicios, hemos utilizado durante mucho tiempo, y aún seguimos utilizando con éxito, una solución combinada basada en Nagios y Munin. Sin embargo, esta combinación tiene una serie de desventajas, por lo que nosotros, al igual que muchos otros, explotamos activamente Zabbix. En este artículo, hablaremos de cómo se puede resolver el problema de rendimiento con un esfuerzo mínimo a medida que aumenta el número de métricas recopiladas y el crecimiento de las bases de datos MySQL.

Problemas del uso de bases de datos MySQL junto con Zabbix

Mientras la base de datos era pequeña y el número de métricas almacenadas era reducido, todo funcionaba estupendamente. El proceso integrado housekeeper, que ejecuta el propio Zabbix Server, eliminaba con éxito los registros obsoletos de la base de datos, evitando que creciera. Sin embargo, cuando el número de métricas recopiladas creció y el volumen de la base de datos alcanzó un cierto tamaño, todo empeoró. El housekeeper dejó de poder eliminar datos en el tiempo asignado, quedando datos antiguos en la base de datos. Durante la operación del housekeeper, había una carga aumentada en el Zabbix Server, que podría persistir durante mucho tiempo. Se hizo evidente que había que encontrar una solución a la situación.

Este es un problema conocido; prácticamente todos los que han trabajado con grandes volúmenes de monitoreo en Zabbix se han encontrado con esto. También hubo varias soluciones: por ejemplo, cambiar MySQL por PostgreSQL o incluso Elasticsearch, pero la solución más simple y probada fue la transición al particionamiento de las tablas que almacenan datos de métricas en la base de datos MySQL. Decidimos ir precisamente por este camino.

Transición de tablas MySQL normales a particionadas

Zabbix está bien documentado y las tablas donde almacena métricas son conocidas. Estas tablas son: history, donde se almacenan valores float, history_str, donde se almacenan valores de cadena cortos, history_text, donde se almacenan valores de texto largos y history_uint, donde se almacenan valores enteros. También hay una tabla trends, que almacena la dinámica de los cambios, pero decidimos no tocarla, ya que su tamaño es pequeño y más tarde volveremos a ella.

En general, era claro qué tablas debían ser procesadas. Decidimos hacer particiones semanales, excepto la última, basándonos en 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 (del mes siguiente). La dificultad radicaba en que teníamos que convertir las tablas necesarias en particionadas "al vuelo", sin interrumpir el funcionamiento de Zabbix Server y la recolección de métricas.

Curiosamente, la propia estructura de los datos de las tablas nos ayudó en esto. Por ejemplo, la tabla 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',

en la que

KEY `history_1` (`itemid`,`clock`)

Como podemos ver, cada métrica se registra en la tabla con dos campos muy importantes y convenientes para nosotros, itemid y clock. De esta manera, podemos crear una tabla temporal, por ejemplo, llamada history_tmp, configurar la partición para ella y luego volcar todos los datos de la tabla history, y después renombrar la tabla history en history_old, y la tabla history_tmp en history, después de lo cual añadimos los datos que nos faltan de history_old en history y eliminamos history_old. Esto se puede hacer de manera absolutamente segura, no perderemos nada, ya que los campos mencionados anteriormente itemid y clock aseguran la vinculación de una métrica específica a un tiempo concreto, no a un número de orden.

El propio procedimiento de transición

¡Atención! Es muy recomendable, antes de realizar alguna acción, hacer una copia de seguridad completa de la base de datos. Todos somos humanos y podemos cometer errores en la introducción de comandos, lo que podría llevar a la pérdida de datos. Sí, una copia de seguridad no garantizará la máxima actualidad, pero es mejor tener una que no tener ninguna.

Así que no apagamos ni detenemos nada. Lo principal es que en el servidor MySQL haya suficiente espacio libre en disco, es decir, que para cada una de las tablas mencionadas anteriormente history, history_text, history_str, history_uint, haya como mínimo espacio suficiente para crear una tabla con el sufijo "_tmp", considerando que tendrá el mismo volumen que la tabla original.

No vamos a describir todo varias veces para cada una de las tablas mencionadas y lo consideraremos solo con el ejemplo de una de ellas: la tabla history.

Así que, creamos 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. Para este ejemplo, haremos esto para un mes. Cada partición se crea en base a la regla de particionamiento basada en el valor del campo clock, que estamos comparando 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 añade particionamiento a la tabla que hemos creado. history_tmpAclaramos que los datos cuyo valor en el campo clock es menor a "2019-02-01 00:00:00" irán a la partición p20190201, luego los datos cuyo valor en el campo clock es mayor a "2019-02-01 00:00:00" pero menor a "2019-02-07 00:00:00" irán a la partición p20190207 y así sucesivamente.

Una nota importante: ¿Qué sucederá si en nuestra tabla particionada aparecen datos cuyo valor en el campo clock sea mayor o igual a "2019-03-01 00:00:00"? Dado que no hay una partición adecuada para estos datos, no llegarán a la tabla y se perderán. Por lo tanto, es necesario recordar crear particiones adicionales a tiempo para evitar tales pérdidas de datos (como se menciona a continuación).

Bien, la tabla temporal está preparada. Cargamos los datos. El proceso puede tardar bastante tiempo, pero afortunadamente no bloquea otras consultas, así que sólo hay que tener paciencia:

INSERT IGNORE INTO `history_tmp` SELECT * FROM history;

La palabra clave IGNORE durante la carga inicial no es obligatoria, ya que no hay datos en la tabla, sin embargo, será necesaria al cargar datos posteriormente. Además, puede ser útil si tuviste que interrumpir el proceso de carga y empezarlo de nuevo.

Así que, después de un tiempo (posiblemente incluso varias horas), se completó la primera carga de datos. Como entenderás, ahora la tabla history_tmp contiene no todos los datos de la tabla history, sino solo aquellos que estaban en ella en el momento de iniciar la consulta. Aquí, de hecho, tienes una elección: podemos hacer otro pase (si el proceso de carga fue largo), o pasar directamente a renombrar las tablas, como se mencionó anteriormente. Hablemos primero del segundo pase. Para comenzar, necesitamos entender el tiempo de la última inserción en history_tmp:

SELECT max(clock) FROM history_tmp;

Supongamos que obtuviste: 1551045645Ahora utilizamos el valor obtenido en el segundo paso de la carga de datos:

INSERT IGNORE INTO `history_tmp` SELECT * FROM history WHERE clock>=1551045645;

Este paso debería terminar significativamente más rápido. Pero si el primer paso tomó horas y el segundo también toma un tiempo prolongado, puede ser correcto hacer un tercer paso que se ejecute de manera exactamente igual al segundo.

Al final, volvemos a realizar la operación para obtener el tiempo de la última inserción del registro en history_tmp, ejecutando:

SELECT max(clock) FROM history_tmp;

Supongamos que has obtenido 1551085645. Guarda este valor, ya que lo necesitaremos para la carga adicional.

Y ahora, en efecto, cuando la carga inicial de datos en history_tmp ha terminado, procedemos a renombrar las tablas:

BEGIN;
RENAME TABLE history TO history_old;
RENAME TABLE history_tmp TO history;
COMMIT;

Hemos estructurado este bloque como una transacción para evitar el momento de inserción de datos en una tabla inexistente, ya que después del primer RENAME y antes de ejecutar el segundo RENAME, la tabla history no existirá. Pero incluso si durante las operaciones RENAME llegan algunos datos a la tabla y la tabla aún no existirá (debido al cambio de nombre), tendremos un pequeño número de errores de inserción que se pueden ignorar (tenemos monitoreo, no es un banco). history Ahora tenemos una nueva tabla

con particionamiento, pero carece de datos que fueron obtenidos durante el último paso de inserción de datos en la tabla history . Pero esos datos los tenemos en la tabla history_tmpy los vamos a agregar desde allí ahora. Para esto, necesitaremos el valor guardado anteriormente 1551085645. ¿Por qué guardamos este valor en lugar de usar el tiempo máximo de inserción ya de la tabla actual? history_old INSERT IGNORE INTO `history` SELECT * FROM history_old WHERE clock>=1551045645; history? Потому что новые данные уже в неё поступают и мы получим неверное время. Итак, дозаливаем данные:

Una vez que esta operación ha terminado, tenemos en la nueva tabla particionada

todas las datos que estaban en la antigua, más los que ya llegaron después de renombrar la tabla. La tabla history ya no es necesaria. Se puede eliminar de inmediato, o se puede hacer una copia de seguridad antes de eliminarla (si tienes paranoia). history_old Todo el proceso descrito anteriormente debe repetirse para las tablas

Qué ajustes deben corregirse en la configuración de Zabbix Server history_str, history_text y history_uint.

Что нужно поправить в настройках Zabbix Server

Ahora, el mantenimiento de la base de datos en lo que respecta a la historia de datos recae en nosotros. Esto significa que Zabbix ya no debe eliminar datos antiguos; nosotros nos encargaremos de eso. Para que el servidor Zabbix no intente limpiar los datos por sí mismo, debe acceder a la interfaz web de Zabbix, seleccionar en el menú 'Administración', luego el submenú 'General', y después en el desplegable de la derecha seleccionar 'Limpiar historial'. En la página que aparece, debe desmarcar todas las casillas para el grupo 'Historia' y hacer clic en el botón 'Actualizar'. Esto evitará la limpieza innecesaria de las tablas. historia* a través de housekeeper.

Preste atención en esta misma página al grupo 'Dinámica de cambios'. Esta es exactamente la tabla trends, a la que prometimos volver. Si también se ha vuelto demasiado grande y necesita particionarse, desmarque las casillas en este grupo, y luego procese esta tabla de la misma manera que se hizo con las tablas historia*.

Mantenimiento adicional de la base de datos

Como se mencionó anteriormente, para un funcionamiento normal en tablas particionadas, es necesario crear particiones a tiempo. Esto se puede hacer así:

ALTER TABLE `history` ADD PARTITION (PARTITION p20190307 VALUES LESS THAN (UNIX_TIMESTAMP("2019-03-07 00:00:00")));

Además, dado que hemos creado tablas particionadas y prohibido que el servidor Zabbix las limpie, la eliminación de datos antiguos ahora es nuestra responsabilidad. Afortunadamente, aquí no hay ningún problema en absoluto. Esto se realiza simplemente eliminando la partición cuyos datos han dejado de ser útiles para nosotros.

Por ejemplo:

ALTER TABLE history DROP PARTITION p20190201;

A diferencia de los operadores DELETE FROM con un rango de fechas, DROP PARTITION se ejecuta en unos pocos segundos, no carga en absoluto servidor y funciona de manera igualmente fluida en caso de ser utilizado en la replicación de MySQL.

Conclusión

La solución descrita ha sido probada en el tiempo. El volumen de datos crece, pero no se ha observado una desaceleración notable en el rendimiento.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster