Utilizarea partiționării în MySQL pentru Zabbix cu un număr mare de obiecte de monitorizare

Pentru monitorizarea serverelor și a serviciilor, utilizăm de mult timp o soluție combinată bazată pe Nagios și Munin, cu succes continuu. Cu toate acestea, această combinație are o serie de dezavantaje, așa că, la fel ca mulți alții, ne-am orientat activ Zabbix. În acest articol, vom explica cum, cu eforturi minime, se poate rezolva problema legată de performanță atunci când numărul metricelor monitorizate crește și volumul bazei de date MySQL se extinde.

Probleme legate de utilizarea bazei de date MySQL împreună cu Zabbix

Până când baza de date era mică și numărul metricelor stocate în ea era mic, totul funcționa excelent. Procesul de întreținere housekeeper care pornea automat de Zabbix Server reușea să elimine înregistrările învechite din baza de date, împiedicându-i astfel creșterea. Cu toate acestea, de îndată ce numărul metricelor monitorizate a crescut și volumul bazei de date a atins o dimensiune oarecare, lucrurile s-au deteriorat. Housekeeper a încetat să elimine datele în intervalul de timp alocat, iar în baza de date au rămas date vechi. În timpul funcționării housekeeper-ului, s-a simțit o sarcină crescută pe Zabbix Server, care putea dura mult timp. A devenit clar că trebuie să se facă ceva pentru a rezolva situația.

Aceasta este o problemă cunoscută; aproape toți cei care au lucrat cu volume mari de monitorizare pe Zabbix s-au confruntat cu aceeași situație. Au existat și mai multe soluții: de exemplu, înlocuirea MySQL cu PostgreSQL sau chiar Elasticsearch, dar cea mai simplă și testată soluție a fost trecerea la partiționarea tablourilor care stochează datele metricelor în baza de date MySQL. Am decis să mergem exact pe această cale.

Trecerea de la tablouri MySQL obișnuite la cele partiționate

Zabbix este bine documentat, iar tablourile în care stochează metricile sunt cunoscute. Acestea sunt tablouri: history, unde sunt stocate valori float, history_str, unde sunt stocate valori scurte de tip string, history_text, unde sunt stocate valori lungi de text și history_uint, unde sunt stocate valori întregi. Există și un tablou trends, care stochează dinamica modificărilor, dar am decis să nu-l atingem, deoarece dimensiunea sa este mică și ne vom întoarce la el mai târziu.

În general, a fost clar ce tabele trebuie procesate. Am decis să facem partiții pentru fiecare săptămână, cu excepția ultimei, bazându-ne pe zilele lunii, adică câte patru partiții pe lună: de la 1 la 7, de la 8 la 14, de la 15 la 21 și de la 22 la 1 (a lunii următoare). Dificultatea a fost că trebuia să transformăm tabelele necesare în tabele partiționate "pe loc", fără a întrerupe funcționarea serverului Zabbix și colectarea metricilor.

Cumva, structura de date a tabelelor ne-a venit în ajutor. De exemplu, tabela history are următoarea structură:

`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',

în acest context,

CHEIE `history_1` (`itemid`,`clock`)

Cum vedem, fiecare metrică este înregistrată în cele din urmă în tabelă cu două câmpuri foarte importante și utile pentru noi: itemid și clock. Astfel, putem crea o tabelă temporară, de exemplu, cu numele history_tmp, să configurăm partiționarea pentru aceasta și apoi să transferăm toate datele din tabela history, după care să redenumim tabela history în history_old, iar tabela history_tmp în history, după care să adăugăm datele care nu au fost încă transferate din history_old în history și să ștergem history_old. Putem face asta complet în siguranță, nu vom pierde nimic, deoarece câmpurile menționate mai sus itemid și clock asigură legătura metricii specifice cu un anumit moment, nu cu un număr de ordine.

Procedura de tranziție

Atenție! Este foarte recomandat, înainte de a începe orice acțiune, să facem o copie de rezervă completă a bazei de date. Suntem cu toții oameni și putem greși în introducerea comenzilor, ceea ce poate duce la pierderi de date. Da, copia de rezervă nu va asigura actualitatea maximă, dar este mai bine să avem una decât să nu avem deloc.

Așadar, nu oprim și nu închidem nimic. Principalul este să existe suficient spațiu liber pe disc pe serverul MySQL, adică pentru fiecare din tabelele enumerate mai sus history, history_text, history_str, history_uint, să fie disponibil, cel puțin, suficient loc pentru crearea unei tabele cu sufixul „_tmp”, având în vedere că aceasta va avea același volum ca și tabela inițială.

Nu vom descrie totul de mai multe ori pentru fiecare din tabelele menționate și vom considera totul pe exemplul doar uneia dintre ele — tabela history.

Așadar, creăm o tabelă goală history_tmp bazată pe structura tabelei history.

CREATE TABLE `history_tmp` LIKE `history`;

Creăm partițiile necesare. De exemplu, vom face acest lucru pentru o lună. Fiecare partiție este creată pe baza unei reguli de partiționare, bazate pe valoarea câmpului clock, pe care o comparăm cu marca de timp:

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"))
);

Această comandă adaugă partiționare pentru tabela creată de noi history_tmp. Să precizăm că datele, al căror câmp clock este mai mic decât „2019-02-01 00:00:00” vor cădea în partiția p20190201, apoi datele al căror câmp clock este mai mare decât „2019-02-01 00:00:00” dar mai mic decât „2019-02-07 00:00:00” vor cădea în partiția p20190207 și așa mai departe.

Notă importantă: Ce se va întâmpla dacă în tabela noastră partiționată apar date al căror câmp clock va fi mai mare sau egal cu „2019-03-01 00:00:00”? Deoarece nu există o partiție adecvată pentru aceste date, ele nu vor fi incluse în tabel și se vor pierde. Prin urmare, este necesar să nu uitați să creați la timp partiții suplimentare, pentru a evita astfel de pierderi de date (despre care vom discuta mai jos).

Așadar, tabela temporară este pregătită. Începem încărcarea datelor. Procesul poate dura destul de mult timp, dar din fericire, nu blochează alte cereri, deci trebuie doar să aveți răbdare:

INSERT IGNORE INTO `history_tmp` SELECT * FROM history;

Cuvântul cheie IGNORE la încărcarea inițială nu este obligatoriu, deoarece nu există date în tabel, totuși îl veți avea nevoie la o încărcare ulterioară a datelor. În plus, acesta poate fi util dacă a trebuit să întrerupeți acest proces și să începeți din nou.

Așadar, după un timp (poate chiar câteva ore), prima încărcare a datelor a fost completă. Așa cum vă imaginați, acum tabelul history_tmp conține nu toate datele din tabelul history, ci doar cele care au fost în el în momentul începerii execuției cererii. Aici, în principiu, aveți opțiunea: fie facem o altă trecere (dacă procesul de încărcare a durat mult), fie trecem direct la redenumirea tabelelor, despre care am discutat mai sus. Să discutăm mai întâi despre a doua trecere. Pentru început, trebuie să înțelegem timpul ultimei înregistrări inserate în history_tmp:

SELECT max(clock) FROM history_tmp;

Să presupunem că ați obținut: 1551045645. Acum folosim valoarea obținută în a doua trecere a încărcării datelor:

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

Această trecere ar trebui să se termine semnificativ mai repede. Dar dacă prima trecere a durat ore, iar a doua durează de asemenea mult, poate ar fi corect să facem și o a treia trecere, care se desfășoară complet similar celei de-a doua.

La final, efectuam din nou operația de a obține timpul ultimei inserții a înregistrării în history_tmp, executând:

SELECT max(clock) FROM history_tmp;

Presupunem că ați obținut 1551085645. Salvați această valoare — ne va fi necesară pentru suplimentarea datelor.

Și acum, când încărcarea inițială a datelor în history_tmp s-a încheiat, începem redenumirea tabelelor:

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

Am structurat acest bloc ca o singură tranzacție, pentru a evita inserarea datelor în o tabelă inexistentă, deoarece după prima redenumire, până la finalizarea celei de-a doua redenumiri, tabela history nu va exista. Dar chiar dacă între operațiile de redenumire în tabel vor veni unele date, iar tabela în sine nu va fi încă disponibilă (din cauza redenumirii), vom obține un număr mic de erori de inserare, care pot fi neglijate (avem monitorizare, nu un banc). history Acum avem o nouă tabelă

cu partiționare, dar îi lipsesc datele care au fost obținute în timpul ultimei treceri a inserției de date în tabel history . Dar aceste date le avem în tabelul history_tmpși le vom completa acum. Pentru aceasta, ne va trebui valoarea salvată anterior 1551085645. De ce am salvat această valoare și nu am folosit timpul maxim de încărcare deja din tabelul curent history_old INSERT IGNORE INTO `history` SELECT * FROM history_old WHERE clock>=1551045645; history? Потому что новые данные уже в неё поступают и мы получим неверное время. Итак, дозаливаем данные:

După finalizarea acestei operații, avem în noua noastră tabelă, parționată

toate datele care erau în vechea, plus cele care au venit deja după redenumirea tabelei. Tabela history nu ne mai este necesară. O putem șterge imediat sau putem face o copie de rezervă înainte de a o șterge (dacă sunteți paranoici). history_old Întregul proces descris mai sus trebuie să fie repetat pentru tabelele

Ce trebuie corectat în setările Zabbix Server history_str, history_text și history_uint.

Ce trebuie să corectăm în setările Zabbix Server

Acum, gestionarea bazei de date în ceea ce privește istoricul datelor revine în sarcina noastră. Acest lucru înseamnă că Zabbix nu va mai trebui să ștergă datele vechi — ne vom ocupa noi de acest lucru. Pentru a preveni ca Zabbix Server să încerce să curețe datele singur, trebuie să accesați interfața web a Zabbix, să selectați din meniul „Administrare”, apoi submeniu „General”, iar în lista derulantă din dreapta să alegeți „Curățare istoric”. Pe pagina care apare, trebuie să debifați toate casetele pentru grupul „Istoric” și să apăsați pe butonul „Actualizare”. Acest lucru va preveni curățarea inutilă a tabelului nostru. history* prin housekeeper.

Rețineți pe aceeași pagină grupul „Dinamică a modificărilor”. Aceasta este exact tabela trends, la care am promis că ne vom întoarce. Dacă aceasta a devenit și ea prea mare și necesită particionare, debifați casetele și în acest grup, apoi tratați această tabelă exact așa cum a fost făcut pentru tabelele history*.

Întreținerea ulterioară a bazei de date

Așa cum s-a menționat anterior, pentru a funcționa corect cu tabelele partitionate, este necesar să se creeze la timp particiile. Acest lucru se poate face astfel:

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

În plus, deoarece am creat tabele partitionate și am interzis Zabbix Server-ului să le curețe, ștergerea datelor vechi este acum responsabilitatea noastră. Din fericire, nu există probleme de acest fel. Acest lucru se face pur și simplu prin ștergerea acelei particii a cărei date nu ne mai sunt necesare.

De exemplu:

ALTER TABLE history DROP PARTITION p20190201;

Spre deosebire de operatorii DELETE FROM cu specificarea unei interval de date, DROP PARTITION se execută în câteva secunde, fără a crea o sarcină mare serverul și funcționează la fel de bine în cazul utilizării replicării MySQL.

Concluzie

Soluția descrisă a fost testată în timp. Volumul de date crește, dar nu am observat o încetinire semnificativă a performanței.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster