Про вплив full-page writes

Налаштовуючи postgresql.conf, Ви могли помітити, що там є параметр full_page_writes. Наявний поруч з ним коментар говорить щось про часткову запису сторінок і люди, як правило, залишають його в стані on — що не погано, це я поясню далі в даній статті. Тим не менш, дуже корисно розуміти що full_page_writes робить, так як вплив на роботи системи може бути значним.

На відміну від мого минулого посту про налаштування чекпоінтів, це не є посібником про те, як треба налаштовувати сервер. Тут не так багато всього того, що Ви могли б налаштувати, насправді, але я покажу Вам, як деякі рішення на рівні програми (наприклад, вибір типів даних) можуть взаємодіяти з записом повних сторінок.

Частковий запис / «Розірвані» сторінки
Так що ж таке повна запис сторінок? Як говорить коментар postgresql.conf, це спосіб оговтатися від часткової запису сторінок — PostgreSQL використовує сторінки розміру 8kB (за замовчуванням), в той час як інші частини стека використовують відмінні розміри шматків. Файлова система Linux як правило використовує 4kB сторінки (можливо використовувати сторінки меншого розміру, але 4kB є максимумом на x86), на апаратному рівні, старі приводи використовують сектори 512B, в той час як пишуть нові дані більш великими шматками (зазвичай 4kB, або навіть 8kB).

Таким чином, коли PostgreSQL записує 8kB сторінку, інші шари сховища можуть розбивати її на менші шматки, що обробляються окремо. Це являє з себе проблему атомарности запису. 8ми килобайтная PostgreSQL'вская сторінка може бути розбита на дві 4kB сторінки файлової системи, а потім ще на 512B сектори. Тепер, що станеться, якщо сервер вийде з ладу (збій живлення, помилка ядра,...)?

Навіть якщо сервер використовує систему зберігання, призначену для того, щоб справлятися з такими збоями (SSD з конденсаторами, RAID контролери з батареями, ...), ядро вже розділяє дані на 4kB сторінки. Існує можливість що база даних записала 8kB сторінку з даними, але тільки частина від неї потрапила на диск до збою.

З цієї точки зору, Ви напевно думаєте, що це саме те, заради чого ми маємо транзакційний лог (WAL) і Ви маєте рацію! Так що, після запуску сервера, база буде читати WAL (з останнього виконаного чекпоінта), і застосує зміни знову, щоб переконатися, що файли з даними вірні. Просто.

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

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

Збільшення запису
Звичайно ж, негативним наслідком цього є збільшення розміру WAL'а — зміна одного байта на 8kB сторінці призведе до її повного запису в WAL. Повна запис сторінки відбувається тільки на першій запису після чекпоінта, тобто зменшуючи частоту чекпоінтів — це один із способів поліпшити ситуацію, насправді, існує невеликий «вибух» повного запису сторінок після чекпоінта, після чого відносно небагато повних записів відбувається до його закінчення.

UUID проти BIGSERIAL ключів
Все ще залишаються деякі несподівані взаємодії з проектними рішеннями, прийнятими на рівні програми. Давайте припустимо, що ми маємо просту таблицю з первинним ключем, UUID або BIGSERIAL, і ми пишемо в неї дані. Буде різниця в розмірі генерованого WAL'а (припускаючи що ми пишемо однакову кількість рядків)?

Представляється розумним очікувати приблизно однаково розміру WAL'ів в обох випадках, але наступні діаграми наочно демонструють, що існує величезна різниця на практиці:
image

Тут показані розміри WAL'ів, отримані в результаті годинного тесту, розігнаного до 5000 инсертов в секунду. З BIGSERIAL'ом в якості первинного ключа, це вилилося в ~2GB WAL'аЮ в той час як UUID видав понад 40GB. Різниця більш ніж відчутна, і велика частина WAL'а пов'язана з індексом, що стоять за первинним ключем. Давайте подивимося на типи записів у WAL'е:
image

Очевидно, абсолютна більшість записів — це полностраничные образи (FPI), тобто результат повного запису сторінок. Але чому це відбувається?

Звичайно, це пов'язано з притаманною UUID'у випадковістю. Нові BIGSERIAL'и послідовні, у зв'язку з чим і пишуться ті ж гілки btree індексу. Так як тільки перша зміна сторінки викликає повну запис сторінки, така мала кількість записів WAL'а є FPI'мі. З UUID зовсім інша справа, звичайно ж, значення абсолютно не послідовні і кожен інсерт цілком ймовірно потрапить у нову гілку індексу (припускаючи що індекс є досить великим).

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

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

Раптово різниця між типами даних зникла — доступ здійснюється випадково в обох випадках, приводячи приблизно до ідентичного розміру вироблених WAL'ів. Іншою відмінністю є те, що велика частина WAL'а асоціюється з «heap», тобто таблицями, а не індексами. «HOT» випадки були відтворені для можливості HOT UPDATE оптимізації (тобто апдейти без необхідності чіпати індекс), що практично повністю виключає весь пов'язаний з індексами трафік WAL.

Але Ви можете протестувати що велика частина додатків не змінює весь набір даних. Зазвичай, тільки мала частина даних «активна» — людей цікавлять повідомлення за останні кілька днів на форумах, невирішені замовлення в інтернет-магазинах, і т. д. Як це впливає на результати?

На щастя, pgbench підтримує нерівномірні розподілу, і, наприклад, з експоненціальним розподілом, що стосуються 1% набору даних ~25% часу, діаграми будуть виглядати наступним чином:
image

Якщо зробити розподіл ще більш асиметричним, що стосуються 1% даних ~75% часу:
image

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

8kB і 4kB сторінки
Ще одне цікаве питання — скільки WAL трафіку можна заощадити, використовуючи менші сторінки в PostgreSQL (що вимагає компіляції інтерфейсу пакета). У кращому випадку, це може зберегти до 50% WAL'а, завдяки логированию тільки 4kB, замість 8kB сторінок. Для навантаження з рівномірно розподіленими апдейтами виглядає наступним чином:
image

Загалом економія не зовсім 50%, але зменшення з ~140GB до ~90GB все одно досить відчутно.

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

По-перше, мені цікаво, уразливі досі файлові системи Linux для часткових записів? Параметр був представлений в PosqtgreSQL версії 8.1, що вийшла в 2005, так що, можливо, багато покращення файлових систем з тих пір вирішили цю проблему. Ймовірно це не універсальний підхід для будь-яких робочих навантажень, але, можливо, враховуючи деякі додаткові умови (наприклад, використання 4kB сторінок в PostgreSQL) його буде достатньо? До того ж, PostgreSQL ніколи не перезаписує тільки частина 8kB сторінки, а тільки повну сторінку.

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

По-друге, багато сучасні системи покладаються на репліки, що використовують потокове реплікацію — замість того, щоб чекати поки сервер перезавантажиться після збою обладнання (що може тривати досить довго) і потім витрачати більше часу на проведення відновлення, системи просто перейдуть на hot standby. Якщо база даних на пошкодженому майстра була прибрана (і потім склонирована з нового майстра), часткові записи не є проблемою.

Але, я боюся, якщо ми почнемо рекомендувати такий підхід, тоді «Я не знаю, яким чином дані були пошкоджені, я просто зробив full_page_writes=off на системах!» стане одним з найбільш поширених пропозицій прямо перед загибеллю DBA (разом з «Я бачив цю змію на reddit, вона не отруйна»).

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

Деякі рішення на рівні додатки можуть збільшити випадковість запису в таблиці та індекси — приміром UUID'и по своїй природі випадкові, перетворюючи навіть звичайну навантаження від инсертов у випадкові апдейти індексів. Схема, використана в прикладах була досить тривіальна — на практиці ж, там були б вторинні індекси, зовнішні ключі і т. д. Використання ж BIGSERIAL в якості первинних ключів (і залишаючи UUID в якості побічних ключів) може як мінімум зменшить збільшення запису.

Я дійсно зацікавлений в обговоренні необхідності повного запису сторінок на різних ядрах/файлових системах. На жаль, я не знайшов великої кількості ресурсів, якщо ви володієте будь-якою відповідною інформацією, дайте мені знати.
Джерело: Хабрахабр

0 коментарів

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