Використання партиціонування в MySQL для Zabbix з великою кількістю об'єктів моніторингу

Для моніторингу серверів і служб у нас давно і все ще успішно використовується комбіноване рішення на базі Nagios і Munin. Однак ця зв'язка має ряд недоліків, тому ми, як і багато хто, активно експлуатуємо Zabbix. У цій статті ми розповімо про те, як мінімальними зусиллями можна вирішити проблему з продуктивністю при збільшенні кількості метрик, що знімаються, і зростанні обсягів БД MySQL

Проблеми використання БД MySQL спільно з Zabbix

Поки БД була маленькою і кількість метрик, що зберігаються в ній, невеликою, все було чудово. Штатний процес housekeeper, який запускає сам Zabbix Server успішно видаляв застарілі записи з БД, не даючи їй зростати. Однак, як тільки кількість метриків, що знімаються, зросла і обсяг БД досяг певного розміру, все стало гірше. Houserkeeper перестав встигати видаляти дані за відведений йому інтервал часу, у БД стали залишатися старі дані. Під час роботи housekeeper виникало підвищене навантаження на Zabbix Server, яке могло триматися довгий час. Стало зрозуміло, що треба якось вирішувати ситуацію, що склалася.

Це відома проблема, практично кожен, хто працював з великими обсягами моніторингу на Zabbix, стикався з тим самим. Рішень теж було кілька: наприклад, заміна MySQL на PostgreSQL або навіть Elasticsearch, але найпростішим і апробованим рішенням був перехід до партиціонування таблиць, що зберігають дані метрик у БД MySQL. Ми вирішили піти саме цим шляхом.

Перехід від звичайних таблиць MySQL до партиціонованих

Zabbix непогано документований і таблиці, де він зберігає відомі метрики. Це таблиці: history, де зберігаються float значення, history_str, де зберігаються короткі строкові значення, history_text, де зберігаються довгі текстові значень та history_uint, де зберігаються цілі численні значення. Є ще таблиця trends, яка зберігає динаміку змін, але ми вирішили не чіпати, оскільки її розмір невеликий і трохи пізніше ми до неї повернемося.

Загалом які таблиці треба обробити було зрозуміло. Ми вирішили робити партиції щотижня, крім останньої, з урахуванням чисел місяця, тобто. по чотири партиції на місяць: з 1-го по 7-е, з 8-го по 14-те, з 15 по 21-е та з 22-го по 1-е (наступного місяця). Проблема була в тому, що потрібно було необхідні нам таблиці перетворити на партиціоновані «на льоту», не перериваючи роботи Zabbix Server і збирання метрик.

Як не дивно, на допомогу нам прийшла сама структура даних таблиць. Наприклад таблиця history має таку структуру:

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

при цьому

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

Як бачимо, кожна метрика в результаті заноситься до таблиці з двома дуже важливими та зручними для нас полями. itemid и годинник. Таким чином, ми можемо створювати тимчасову таблицю, наприклад, з ім'ям history_tmpналаштувати для неї партиціонування і потім перелити туди всі дані з таблиці history, а потім перейменувати таблицю history в history_old, а таблицю history_tmp в history, після чого дозалити ті дані, які у нас недозалиті з history_old в history та видалити history_old. Робити це можна абсолютно безпечно, ми нічого не втратимо, адже вказані вище поля itemid и годинник забезпечують прив'язку метрики до конкретного часу, а не до якогось порядкового номера.

Сама процедура переходу

Увага! Дуже бажано, перед початком якихось дій зробити повну резервну копію з бази даних. Ми всі живі люди і можемо припуститися помилки в наборі команд, що може призвести до втрати даних. Так. резервна копія не забезпечить максимальної актуальності, але краще мати таку, ніж жодна.

Отже, нічого не вимикаємо та не зупиняємо. Головне, щоб у самому MySQL-сервері було достатньо вільного місця на диску, тобто. щоб для кожної з перелічених вище таблиць history, history_text, history_str, history_uintщонайменше вистачило місця на створення таблиці з суфіксом «_tmp», враховуючи, що вона буде такого ж обсягу як і вихідна таблиця.

Ми не будемо описувати все кілька разів для кожної з перелічених таблиць і розглянемо все на прикладі лише однієї з них — таблиці history.

Отже, створюємо порожню таблицю history_tmp на основі структури таблиці history.

CREATE TABLE `history_tmp` LIKE `history`;

Створюємо потрібні нам партиції. Наприклад, зробимо це на місяць. Кожна партиція створюється з урахуванням правила партиціонування, заснованого на значенні поля годинник, яке ми порівнюємо з позначкою часу:

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

Цей оператор додає партиціонування для створеної нами таблиці. history_tmp. Уточнимо, що дані, у яких значення поля годинник менше «2019-02-01 00:00:00» потраплять до партиції p20190201, потім дані у яких значення поля годинник більше «2019-02-01 00:00:00» та менше «2019-02-07 00:00:00» потраплять до партиції p20190207 і так далі.

Важливе зауваження: А що станеться, якщо у нас у партизованій таблиці з'являться дані, у яких значення поля clock буде більше або дорівнює «2019-03-01 00:00:00»? Оскільки для цих даних немає відповідної партиції, вони до таблиці не потраплять і будуть втрачені. Тому, вам необхідно не забувати своєчасно створювати додаткові партиції, щоб уникнути таких втрат даних (що нижче).

Отже, тимчасову таблицю підготовлено. Заливаємо дані. Процес може зайняти досить тривалий час, але на щастя він не блокує будь-які інші запити, так що треба лише запастися терпінням:

INSERT IGNORE INTO `history_tmp` SELECT * FROM history;

Ключове слово IGNORE при початковій заливці не є обов'язковим, адже даних у таблиці все одно немає, проте воно вам знадобиться при дозаливці даних. Крім того, воно може бути корисним, якщо при заливанні даних вам довелося перервати цей процес і почати заново.

Отже, через якийсь час (можливо навіть кілька годин) перша заливка даних пройшла. Як ви розумієте, тепер таблиця history_tmp містить не всі дані з таблиці historyа лише ті, які були в ній на момент початку виконання запиту. Тут саме у вас є вибір: або ми робимо ще один прохід (якщо процес заливання тривав довго), або одночасно переходимо до перейменування таблиць, про яке йшлося вище. Давайте спершу про другий прохід. Для початку нам потрібно зрозуміти час останнього вставленого запису в history_tmp:

SELECT max(clock) FROM history_tmp;

Допустимо, ви отримали: 1551045645. Тепер використовуємо отримане значення на другому проході заливання даних:

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

Цей прохід має закінчитись значно швидше. Але якщо перший прохід виконувався годинник, а другий теж тривалий час, можливо, буде правильним зробити і третій прохід, який виконується абсолютно аналогічно другому.

Наприкінці ми знову виконуємо операцію отримання часу останньої вставки запису history_tmp, виконавши:

SELECT max(clock) FROM history_tmp;

Допустимо, ви отримали 1551085645. Збережіть це значення – воно нам знадобиться для дозаливки.

А тепер власне, коли первинне заливання даних у history_tmp закінчилася, приступаємо до перейменування таблиць:

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

Ми оформили цей блок як одну транзакцію, щоб уникнути моменту вставки даних у неіснуючу таблицю, адже після першого RENAME до моменту виконання другого RENAME таблиця history існувати не буде. Але навіть якщо між операціями RENAME до таблиці history прийдуть якісь дані, а самої таблиці ще не буде (через перейменування) ми отримаємо невелику кількість помилок вставки, якими можна знехтувати (у нас моніторинг, а не банк).

Тепер ми маємо нову таблицю history з партиціонуванням, але в ній не вистачає даних, отриманих під час останнього проходу вставки даних у таблицю history_tmp. Але ці дані у нас є у таблиці history_old і ми їх зараз звідти здолаємо. Для цього нам знадобиться раніше збережене значення 1551085645. Чому ми зберегли це значення, а не використовували максимальний час заливки вже з поточної таблиці history? Тому що нові дані вже до неї надходять, і ми отримаємо невірний час. Отже, дозаливаємо дані:

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

Після закінчення цієї операції, у нас у новій, партиціонованій таблиці history є всі дані, які були у старій, плюс ті, що вже прийшли після перейменування таблиці. Таблиця history_old нам більше не потрібна. Можна її відразу видалити, а можна перед видаленням зробити з неї резервну копію (якщо у вас є параноя).

Весь вищеописаний процес необхідно повторити для таблиць history_str, history_text и history_uint.

Що потрібно виправити в налаштуваннях Zabbix Server

Тепер обслуговування бази даних у частині історії даних лягати на наші плечі. Це означає, що Zabbix більше не повинен видаляти старі дані — ми займатимемося цим самі. Щоб Zabbix Server не намагався чистити дані сам, вам необхідно зайти до web-інтерфейсу Zabbix, вибрати в меню «Адміністрування», потім підменю «Загальні», потім у списку праворуч вибрати «Очищення історії». На сторінці, що з'явилася, потрібно зняти всі галочки для групи «Історія» і натиснути на кнопку «Оновити». Це запобігатиме непотрібному нам очищенню таблиць history* через housekeeper.

Зверніть увагу на цій самій сторінці на групу «Динаміка змін». Це якраз таблиця trends, до якої ми обіцяли повернутися. Якщо вона у вас також стала занадто великою і потребує партиціонування, приберіть галочки і в цій групі, а потім обробіть цю таблицю так само, як робилося для таблиць history*.

Подальше обслуговування бази даних

Як було написано раніше, для нормальної роботи на партиціонованих таблицях необхідно вчасно створювати партиції. Робити це можна так:

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

Крім того, оскільки ми створили партиціоновані таблиці і заборонили Zabbix Server'у їх чистити, то і видалення старих даних тепер наша турбота. На щастя, тут взагалі немає жодних проблем. Це робиться просто видаленням тієї партиції, дані якої нам стали непотрібні.

Наприклад:

ALTER TABLE history DROP PARTITION p20190201;

На відміну від операторів DELETE FROM із зазначенням діапазону дат, DROP PARTITION виконується за пару секунд, зовсім не навантажує сервер і так само безпроблемно працює у разі використання у MySQL реплікації.

Висновок

Описане рішення перевірено часом. Обсяг даних зростає, але якогось відчутного уповільнення продуктивності немає.

Джерело: habr.com

Купити надійний хостинг для сайтів із захистом від DDoS, VPS VDS сервери 🔥 Купити надійний хостинг для сайтів із захистом від DDoS, VPS VDS сервери | ProHoster