PostgreSQL: Випадок у вакуумі

Один з наших клієнтів, який експлуатує PostgreSQL під великим навантаженням, зіткнувся з проблемою, пов'язаною з переповненням лічильника транзакцій (xid wraparound), причому виходу з неї штатними засобами не існувало. Ми вирішили проблему з допомогою хірургічного втручання і випустили патч, що запобігає виникненню таких ситуацій у майбутньому.
У цій замітці ми розповімо, як і чому може виникнути проблема і як її не допустити.
Пристрій лічильника транзакцій PostgreSQL
Однією з найпривабливіших особливостей PostgreSQL є можливість роботи в умовах високої конкуренції за дані: читають транзакції не блокують пишуть і навпаки. Все це відбувається завдяки механізму многоверсионности (MVCC). Реалізація заснована на тому, що кожна транзакція в PostgreSQL має свій номер (ідентифікатор), званий xid. Номери весь час збільшуються, так що транзакція з меншим номером вважається почалася раніше, а транзакція з великим номером — пізніше. Кожен рядок у таблиці має, крім іншого, два додаткових системних поля, які не відображаються у запитах користувачів: вони називаються xmin і xmax. Поле xmin зберігає номер транзакції, яка створила цю рядок, а xmax — номер транзакції, яка її видалила (якщо, звичайно, таке відбулося). Таким чином, кожен рядок може мати кілька версій з різною областю видимості. Такий підхід до організації зберігання даних називається версионным.
Користувача рядка в PostgreSQL ніколи не змінюється, змінюються лише системні поля. Оновлення даних (UPDATE) позначає рядок як віддалену, тобто встановлює xmax = xid_current, і створює нову копію рядка з оновленим змістом, у якій xmin = xid_current.
Коли PostgreSQL зчитує дані з таблиць, це завжди відбувається в контексті деякого знімка даних (snapshot). При створенні знімка даних у ньому запам'ятовується поточний номер транзакції, за яким з декількох версій рядка можна вибрати ту, яка буде видна в контексті поточного знімка. Крім того, в знімок включається список всіх не завершених у даний момент транзакцій, оскільки зміни, зроблені такими транзакціями, не повинні потрапити в знімок.
Версії рядків, що вийшли з областей видимості всіх діючих транзакцій, стають не потрібними. Щоб утримати базу даних від надлишкового росту, спеціальний фонової процес, званий autovacuum, видаляє старі версії рядків — ті, в яких xmax молодше всіх працюючих на поточний момент транзакцій.
Лічильник транзакцій має розмір 32 біта, тобто може зберігати приблизно чотири мільярди значень. Це, звичайно, не так вже й багато. Звучали пропозиції зробити його 64-бітним, однак не варто забувати, що в цьому випадку за рахунок накладних витрат помітно зріс обсяг бази — адже в кожному рядку зберігаються xmin і xmax. Уявіть собі, що межа лічильника у 2^32-1 досягнуто. Додаємо одиницю — і лічильник переповнюється, і скидається в нуль. Це призвело б до катастрофи — адже PostgreSQL розраховує на те, що номери транзакцій завжди збільшуються.
Звичайно ж, є механізм, що запобігає таку ситуацію. По-перше, простір номерів транзакцій закільцьовано: молодше насправді вважається не та транзакція, у якій номер менше, а та, яка відстоїть від іншого менше, ніж на пів-кола. По-друге, в ході очищення (VACUUM) таблиць виконується так звана заморожування. Процес vacuum/autovacuum, крім видалення «мертвих» рядків зі старим xmax, обробляє також і «живі» рядки зі старим значенням xmin. Рядки, чий xmin набагато менше найстарішою з запущених транзакцій і «вік» перевищує vacuum_freeze_min_age, «заморожуються» (позначається спеціальними службовими бітами). Вони перестає підкорятися звичайними правилами видимості і завжди вважається старше будь-якої звичайної транзакції. Таким чином, очищення постійно заморожує старі рядки, слідуючи по колу за лічильником транзакцій.
лічильник транзакцій в PostgreSQL
Вік найстарішої транзакції в базі зберігається в системному каталозі:
SELECT datname, age(datfrozenxid) FROM pg_database;

Також ведеться статистика по кожній таблиці:
SELECT relname, age(relfrozenxid) FROM pg_class;

Фонові процеси стежать за транзакціями в автоматичному режимі, але при експлуатації PostgreSQL адміністратор повинен стежити за тим, щоб вік найстарішої транзакції в базі не наближався до 2^31 (пол-кола, половина від всіх допустимих значень). Тоді PostgreSQL може гарантувати правильність визначення віку транзакції (з урахуванням циклічності лічильника). Якщо ж вік datfrozenxid наблизився до зазначеної точці, то PostgreSQL не зможе видавати номери транзакцій і припинить роботу з міркувань збереження даних, вимагаючи ручного втручання і проведення очищення (VACUUM).
Саме тому слід уникати наддовгих транзакцій, за час яких лічильник встигає збільшитися на 2 мільярди.
Проблема і лікування
Саме переповнення datfrozenxid сталося біля одного з наших клієнтів. Адміністратор вручну запустив команду VACUUM FREEZE, яка пропрацювала 8 днів. За цей час під навантаженням було видано близько 2^31 нових номерів транзакцій. Варто відзначити, що, хоча VACUUM працює поза транзакції, але при запуску створює знімок даних, з допомогою якого відрізняє застарілі версії рядків від актуальних. Система зупинилася і вимагала ручного втручання, але, незважаючи на те, що обслуговування було проведено, вже не змогла запуститися в робочому режимі.
Проблема полягала в тому, що перед оновленням змінних, розташованих в поділюваної пам'яті, команда VACUUM намагалася отримати новий номер транзакції, щоб переконатися в тому, що заморозка пройшла коректно і в системі немає транзакцій «з майбутнього». Оскільки доступні номери закінчилися, команда завершувалася з помилкою, із-за чого змінні, що відповідають за діапазон доступних номерів, не оновлювалися. Для виправлення проблеми було вироблено два рішення: оперативне і постійне.
Оперативне рішення було потрібно для того, щоб якомога швидше відновити роботу БД замовника. Для цього нам довелося вручну відредагувати змінні в поділюваної пам'яті.
  • Вибираємо номер (xid) найстарішої транзакції, що зберігаються в БД:
    SELECT datfrozenxid FROM pg_database ORDER BY age(datfrozenxid) DESC LIMIT 1;
  • Підключаємося з допомогою gdb до будь-якого процесу PostgreSQL і виконуємо команду:
    set ShmemVariableCache->oldestXid = <выбранный_xid>
  • Перезапускаємо PostgreSQL
В результаті цих дій є значення, що відповідають за діапазон доступних транзакцій, були оновлені як в поділюваної пам'яті, так і на диску, і СУБД замовника знову змогла продовжити нормальну роботу.
Постійне вирішення проблеми полягає в тому, щоб команда VACUUM взагалі не отримувала окремий номер транзакції. Патч і інструкція по відтворенню ситуації були вислані в список розсилки hackers. Швидше за все це виправлення буде включено в найближчий мінорний реліз PostgreSQL.
Зміни в 9.6
Неприємну властивість заморозки полягає в тому, що для цього необхідно просканувати всю таблицю. Звичайна очищення (vacuum) працює хитріше: якщо в процесі роботи виявляється, що всі версії рядків на сторінці є актуальними (тобто xmax = 0), така сторінка наголошується в спеціальному файлі, який називається картою видимість (visibility map). До таких сторінок vacuum більше не повертається до тих пір, поки в них не відбудуться якісь зміни (при яких відмітка у карті видно автоматично знімається).
На жаль, заморожування не використовує карту видимості: адже навіть на сторінках з виключно актуальними версіями рядків можуть відмовитися ще не заморожені номери транзакцій в поле xmin. А періодичне повне сканування може викликати проблеми при дуже великому розмірі таблиці.
В PostgreSQL 9.6, бета-версія якого вже випущена, ця складність була подолана. Карта видимості тепер розширена таким чином, щоб містити в собі і «карту заморозки»: у ній будуть відзначатися сторінки, на яких всі транзакції вже заморожені.
Моніторинг переповнення лічильника транзакцій
Для контролю переповнення потрібно переглядати вік транзакцій datfrozenxid з системного каталогу pg_database. Якщо ви користуєтеся системою Zabbix, спробуйте клієнт моніторингу mamonsu, в якому вже є потрібна метрика. Клієнт доступний за адресою: mamonsu.
Висновки
Такі складні продукти, як реляційні СУБД, ніколи не бувають абсолютно вільні від помилок. Незважаючи на надійність PostgreSQL, при експлуатації ви можете зіткнутися з неприємними проблемами. Співтовариство надає підтримку, але, по-перше, вам доведеться виконати серйозну роботу з оформлення повідомлення про помилку (щоб розробники змогли відтворити вашу ситуацію) і по-друге, ніхто не гарантує термін виправлення.
Ось чому, працюючи з системами, критичними для бізнесу, корисно мати технічну підтримку від компанії-вендора, розробники якої розбираються у вихідному коді, можуть виконати необхідні виправлення і зроблять це за мінімально можливий час.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.