Brug af partitionering i MySQL til Zabbix med et stort antal overvågningsobjekter

Til overvågning af servere og tjenester har vi i lang tid, og stadig med succes, brugt en kombineret løsning baseret på Nagios og Munin. Men denne kombination har en række ulemper, så vi, ligesom mange andre, udnytter aktivt Zabbix. I denne artikel vil vi fortælle om, hvordan man med minimale anstrengelser kan løse problemet med ydeevnen ved at øge antallet af målte metrikker og væksten af MySQL-databasevolumener

Problemer ved brug af MySQL-database sammen med Zabbix

Så længe databasen var lille, og antallet af gemte metrikker var begrænset, gik alt godt. Den indbyggede proces housekeeper, som kører Zabbix Server, fjernede med succes forældede poster fra databasen og forhindrede den i at vokse. Men da antallet af målte metrikker voksede, og databasen nåede en vis størrelse, blev alt værre. Housekeeper kunne ikke længere fjerne data inden for den tildelte tidsramme, og gamle data blev tilbageholdt i databasen. Under driften af housekeeper opstod der en øget belastning på Zabbix Server, som kunne vare i lang tid. Det blev klart, at der måtte gøres noget ved den opståede situation.

Dette er et kendt problem; praktisk talt alle, der har arbejdet med store mængder overvågning i Zabbix, har stødt på det samme. Der var også flere løsninger: for eksempel at erstatte MySQL med PostgreSQL eller endda Elasticsearch, men den mest enkle og afprøvede løsning var at overgå til partitionering af tabellerne, der opbevarer metrikdataene i MySQL-databasen. Vi besluttede at gå netop denne vej.

Overgangen fra almindelige MySQL-tabeller til partitionerede

Zabbix er godt dokumenteret, og tabellerne, hvor den opbevarer metrikker, er kendt. Disse tabeller er: history, hvor float-værdier gemmes, history_str, hvor korte stringværdier opbevares, history_text, hvor lange tekstværdier gemmes, og history_uint, hvor heltalsværdier opbevares. Der er også en tabel trends, som gemmer ændringsdynamik, men den besluttede vi ikke at røre, da dens størrelse er lille, og vi vil vende tilbage til den lidt senere.

Generelt set var det klart, hvilke tabeller der skulle behandles. Vi besluttede at lave partitioner hver uge, undtagen den sidste, baseret på månedens numre, dvs. fire partitioner pr. måned: fra den 1. til den 7., fra den 8. til den 14., fra den 15. til den 21., og fra den 22. til den 1. (næste måned). Vanskeligheden lå i, at vi skulle forvandle de nødvendige tabeller til partitionerede "on-the-fly", uden at stoppe arbejdet med Zabbix Server og indsamlingen af metrics.

Mærkeligt nok hjalp selve datastrukturen i tabellerne os i dette. history For eksempel har tabellen

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

hvorved

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

Som vi kan se, skal hver metric i sidste ende indsættes i tabellen med to meget vigtige og praktiske felter itemid og clock. Dermed kan vi fuldt ud oprette en midlertidig tabel, f.eks. med navnet history_tmp, konfigurere partitioneringen for den, og derefter overføre alle data fra tabellen history, og derefter omdøbe tabellen history i history_old, og tabellen history_tmp i history, hvorefter vi tilføjer de data, som vi ikke har tilføjet fra history_old i history og fjerner history_old. Dette kan gøres helt sikkert, vi vil ikke miste noget, da de nævnte felter itemid og clock sikrer forbindelsen mellem den specifikke metric og det specifikke tidspunkt, ikke et eller andet rækkefølge nummer.

Selve overgangsproceduren

Opmærksomhed! Det er meget ønskeligt at lave en fuld sikkerhedskopi af databasen, før man påbegynder nogen handlinger. Vi er alle mennesker og kan lave fejl i kommandoerne, hvilket kan føre til datatab. Ja, en sikkerhedskopi sikrer ikke maksimal aktualitet, men det er bedre at have en end ingen.

Så vi slukker og stopper ingenting. Det vigtigste er, at der er tilstrækkeligt med ledig plads på disken på selve MySQL-serveren, dvs. at der for hver af de ovennævnte tabeller history, history_text, history_str, history_uint, mindst skal være plads til at oprette en tabel med suffikset "_tmp", idet man tager hensyn til, at den vil have samme volumen som den oprindelige tabel.

Vi vil ikke beskrive alt flere gange for hver af de nævnte tabeller, men vil tage det som eksempel på kun én af dem — tabellen history.

Så lad os oprette en tom tabel history_tmp baseret på strukturen af tabellen history.

CREATE TABLE `history_tmp` LIKE `history`;

Vi opretter de partitioner, vi har brug for. For eksempel vil vi gøre dette for en måned. Hver partition oprettes baseret på en partitioneringsregel, der er baseret på værdien af feltet clock, som vi sammenligner med tidsstemplet:

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

Denne operator tilføjer partitionering til den tabel, vi har oprettet history_tmp. Lad os præcisere, at de data, hvis feltværdi clock er mindre end "2019-02-01 00:00:00", vil falde ind under partitionen p20190201, derefter vil data, hvis feltværdi clock er større end "2019-02-01 00:00:00", men mindre end "2019-02-07 00:00:00", falde ind under partitionen p20190207 og så videre.

Vigtig bemærkning: Hvad sker der, hvis vi får data i den partitionerede tabel, hvor feltværdien clock er større end eller lig med "2019-03-01 00:00:00"? Da der ikke er en passende partition for disse data, vil de ikke blive indsat i tabellen og gå tabt. Derfor er det nødvendigt, at du husker at oprette yderligere partitioner i tide for at undgå sådanne datatab (som nævnt nedenfor).

Så, den midlertidige tabel er klar. Vi indlæsser data. Processen kan tage et stykke tid, men heldigvis blokere den ikke for andre forespørgsler, så du skal blot have lidt tålmodighed:

INSERT IGNORE INTO `history_tmp` SELECT * FROM history;

Nøgleordet IGNORE ved den første indlæsning er ikke obligatorisk, da der alligevel ikke er nogen data i tabellen, men det vil være nyttigt ved forlængelsen af data. Desuden kan det være nyttigt, hvis du måtte afbryde indlæsningsprocessen og starte den forfra.

Efter et stykke tid (måske endda flere timer) er den første dataindlæsning blevet gennemført. Som du forstår, indeholder tabellen history_tmp ikke alle data fra tabellen history, men kun dem, der var i den på det tidspunkt, hvor forespørgslen blev udført. Her har du faktisk et valg: enten laver vi endnu et forsøg (hvis indlæsningsprocessen tog lang tid), eller så går vi straks videre til omdøbning af tabellerne, som nævnt tidligere. Lad os først tale om den anden gennemgang. Først skal vi forstå tidspunktet for den seneste indsatte post i history_tmp:

SELECT max(clock) FROM history_tmp;

Antag, at du har fået: 1551045645. Nu bruger vi den opnåede værdi til den anden dataindsættelsesrunde:

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

Denne runde bør afsluttes betydeligt hurtigere. Men hvis den første runde tog timer, og den anden også tager lang tid, kan det være fornuftigt at lave en tredje runde, der udføres på samme måde som den anden.

Til sidst udfører vi igen operationen for at få tidspunktet for den seneste indsættelse af en post i history_tmp, ved at udføre:

SELECT max(clock) FROM history_tmp;

Lad os antage, at du har fået 1551085645. Gem denne værdi — vi får brug for den til yderligere indsættelse.

Og nu, når den primære dataindsættelse i history_tmp er afsluttet, går vi videre til at omdøbe tabellerne:

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

Vi har formateret denne blok som en enkelt transaktion for at undgå situationen med at indsætte data i en ikke-eksisterende tabel, da tabellen efter det første RENAME ikke vil eksistere før det andet RENAME udføres. history Men selv hvis der kommer data til tabellen mellem RENAME-operationerne, og selve tabellen ikke er der endnu (på grund af omdøbningen), vil vi få et lille antal indsætningsfejl, som vi kan ignorere (vi har overvågning, ikke en bank). history Nu har vi en ny tabel

med partitionering, men den mangler data, som blev indsamlet under den seneste dataindsættelse i tabellen history . Men vi har disse data i tabellen history_tmpog vi vil nu overføre dem derfra. Til dette skal vi bruge den tidligere gemte værdi 1551085645. Hvorfor gemte vi denne værdi i stedet for at bruge det maksimale tidspunkt for indsættelse fra den nuværende tabel history_old INSERT IGNORE INTO `history` SELECT * FROM history_old WHERE clock>=1551045645; history? Потому что новые данные уже в неё поступают и мы получим неверное время. Итак, дозаливаем данные:

Efter afslutningen af denne operation har vi i den nye, partitionerede tabel

alle de data, der var i den gamle, plus dem, der allerede er kommet efter omdøbningen af tabellen. Tabel history er ikke længere nødvendig. Den kan enten slettes med det samme, eller man kan lave en backup af den inden sletning (hvis man er paranoid). history_old Hele den ovenfor beskrevne proces skal gentages for tabellerne

Hvad der skal rettes i indstillingerne for Zabbix Server history_str, history_text og history_uint.

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

Nu ligger vedligeholdelse af databasen og dathistorikken på vores skuldre. Det betyder, at Zabbix ikke længere skal slette gamle data — vi håndterer det selv. For at sørge for, at Zabbix Server ikke forsøger at rense dataene selv, skal du gå ind i Zabbix webinterface, vælge «Administration» i menuen, derefter undermenuen «Generelt», og i dropdown-menuen til højre vælge «Rens historie». På den fremkomne side skal du fjerne alle flueben for gruppen «Historie» og trykke på knappen «Opdater». Dette vil forhindre unødvendig rensning af tabeldata. history* via housekeeper.

Bemærk også gruppen «Ændringsdynamik» på denne side. Det er netop tabel trends, som vi lovede at vende tilbage til. Hvis den også er blevet for stor og har brug for partitionering, skal du fjerne fluebenene i denne gruppe, og derefter behandle denne tabel på samme måde som for tabellerne history*.

Fremtidig vedligeholdelse af databasen

Som nævnt tidligere, for korrekt funktion på partitionerede tabeller, er det vigtigt at oprette partitioner til tiden. Det kan gøres på følgende måde:

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

Derudover, da vi har oprettet partitionerede tabeller og forbudt Zabbix Server at rense dem, er det nu vores ansvar at fjerne gamle data. Heldigvis er der slet ingen problemer med dette. Det gøres enkelt ved at fjerne den partition, hvis data vi ikke længere har brug for.

For eksempel:

ALTER TABLE history DROP PARTITION p20190201;

I modsætning til DELETE FROM operatorer med angivelse af datointerval, udføres DROP PARTITION på et par sekunder, belaster slet ikke server og fungerer ligeså problemfrit i tilfælde af brug af MySQL replikation.

Konklusion

Den beskrevne løsning er blevet testet over tid. Mængden af data vokser, men der er ikke bemærket nogen mærkbar nedsættelse af ydeevnen.

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster