Реалізація відновлення після аварій

Сергій Бурладян (Avito)
Сергій Бурладян

Всім привіт, мене звати Сергій Бурладян, я працюю в «Avito» адміністратором баз даних. Я працюю з такими системами:



Це наша центральна база 2 Тб, 4 сервера — 1 майстер, 3 standby. Ще у нас є логічна реплікація на основі londiste (це з Skytools), зовнішній індекс sphinx'а, різні вивантаження у зовнішні системи — така, як DWH, припустимо. Ще у нас є власні напрацювання в області віддаленого виклику процедури, xrpc так звана. Сховище 16 баз. І ще така цифра, що наш бекап займає 6 годин, а його відновлення — близько 12-ти. Мені хотілося б, щоб у разі різних аварій на цих систем простий нашого сайту займав не більше 10-ти хвилин.

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



І як все це не втратити при аварії?

Які можуть бути аварії?



Я розглядаю, в основному, аварії втрати сервера, і плюс для майстра може бути ще така аварія, вибух даних.


Почнемо.



Припустимо, якийсь адміністратор помилково зробив update без where. У нас такий випадок був кілька разів. Як від неї захиститися? Ми захищаємося з допомогою того, що у нас є standby, який застосовує WAL'и з затримкою в 12 годин. Коли сталася така аварія, ми взяли ці дані з standby і завантажили назад на master.



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



Руками це все складно зробити, тому потрібно відразу робити скриптом. Як виглядає аварія? У зовнішніх системах з'являються оголошення, яких вже немає на майстра, sphinx видає при пошуку неіснуючі оголошення, sequences стрибнули назад, логічні репліки, зокрема з-за цього теж, перестали працювати (londiste).



Але не все так погано, це все можна відновити. Ми посиділи, подумали і спланували процедуру відновлення. Зокрема, DWH ми можемо просто вивантажити заново. І безпосередньо, оскільки у нас простий 10 хвилин, то на місячних звітах зміна цих втрачених items просто не видно.

Як відновлювати xrpc? У нас xrpc використовується для геокодинга, для виклику асинхронних процедур на майстрі і для розрахунку карми користувача. Відповідно, якщо ми щось загеокодили, тобто з адреси перетворили його в координати на карті, а потім ця адреса пропав, то нічого страшного, що він залишиться загеокоденным, просто, ми вдруге не будемо такий же адресу геокодить, відповідно, не треба нічого відновлювати. Локальний виклик процедури асинхронний, т. к. він локальний, він розташований на одному сервері бази, навіть на одній базі, і тому, коли базу ми переключили, вона консистентна. Теж нічого не треба відновлювати. Карма користувача. Ми вирішили, що якщо користувач зробив щось погане, а потім сталася аварія, і ми втратили ці погані items, то карму користувачів можна теж не відновлювати. Він же зробив ці погані речі, нехай у нього і залишаться.



Sphinx сайту. У нас є два sphinx — один для сайту, інший для backoffice. Sphinx, який сайту, реалізований таким чином, що повністю перебудовує кожні 10 хвилин весь свій індекс. Відповідно, сталася аварія, відновилися, і через 10 хвилин індекс повністю перебудований і відповідає майстру. А для backoffice ми вирішили, що теж не критично, ми можемо зарефрешить частину оголошень, які змінилися після відновлення, і плюс раз в місяць ми повністю перебудовуємо весь backoffice sphinx'івський, і всі ці аварійні items будуть почищені.

Як відновлювати sequences, щоб вони не стрибали тому? Ми просто вибрали важливі для нас sequences, такі як item_id, user_id, платіжний первинний ключ, і ми після аварії їх прокручувати вперед на 100 тис. (ми вирішили, що нам буде достатньо).

Логічну реплікацію ми відновлюємо за допомогою нашої системи, це патч для londiste, яке робить UNDO для логічної репліки.



Патч Undo — це такі три команди. Безпосередньо сама команда і плюс дві команди додавання/видалення Undo для логічної репліки. І ще replay в londiste ми додали прапор, щоб він передавав TICK_ID з майстра в сесійний змінну Postgres'a.



Це потрібно безпосередньо в самій реалізації Undo, т. до. вона реалізована — просто це тригери на всіх таблицях subscriber'а. Тригер пише в табличку історії, яка безпосередньо операція відбулася. У таблиці. Цей переданий tick_id з майстром він запам'ятовує в цьому записі. Відповідно, коли сталася аварія, логічна репліка опинилася в майбутньому, і її потрібно почистити, щоб відновити зміни, які з недосяжного майбутнього. Це робиться з допомогою виконання зворотних запитів, тобто для insert ми робимо delete, update для ми оновлюємо попередніми значеннями, ну, а для delete — insert.



Руками ми всі це робимо, ми робимо з допомогою скрипта. Яка тут особливість нашого скрипта? У нас три асинхронних standby, відповідно, перш ніж переходити, потрібно з'ясувати, який з них найбільш близький до майстра. Далі, ми вибираємо цей standby, чекаємо, поки він програє залишилися WAL'и з архіву, і вибираємо його для майбутнього майстра. Далі, ми використовуємо Postgres 9.2. Особливості цієї версії в тому, що щоб standby переключилися на новий промоушн і майстер, їх доводиться зупиняти. По ідеї, в 9.4 це вже можна не робити. Відповідно, робимо promote, зрушуємо sequences вперед, виконуємо нашу процедуру Undo, запускаємо standby. І далі теж цікавий момент — потрібно дочекатися, коли standby підключиться до нового майстра. Ми це робимо за допомогою очікування появи timeline нового майстра на відповідному standby.



І ось, виявляється, в Postgres немає такої функції SQL'ной, неможливо зрозуміти timeline на standby. Але ми вирішуємо це таким способом, виявляється можна підключитися по репликационному протоколу Postgres'а до standby, і там після першої команди standby повідомить свій, виділений червоним, timeline.

Такий у нас скрипт відновлення майстра.



Підемо далі. Як ми відновлюємося безпосередньо, коли зовнішні системи якісь розвалюються. Наприклад, standby. Т. к. у нас три standby, як я вже говорив, ми просто беремо, перемикаємося на решту standby, якщо один з них падає. В крайньому випадку, навіть якщо ми втратимо все standby, ми можемо переключити трафік на майстра. Тут буде губитися частина трафіку, але, в принципі, сайт буде працювати. Тут ще була така хитрість — спочатку я весь час створював нові standby з бекапа, потім у нас з'явилися сервера SSD'шні, а я все так само продовжував відновлювати з бекапа standby. Потім виявилося, що якщо брати з бекапа, відновлення займає 12 годин, а якщо просто взяти pg_basebackup з будь-якого працюючого standby, то це займає набагато менше часу. Якщо у вас кілька standby, можна спробувати у вас це перевірити.



Якщо ламається sphinx сайту. Sphinx сайту у нас написаний таким чином, що він повністю перебудовує весь індекс, а sphinx сайту — це всі активні оголошення для сайту. Зараз всі 30 або 35 млн. оголошень на сайті індексуються ось цією системою. Індексація йде з окремої репліки логічної, вона підготовлена спеціально для індексації і зроблена так, що там все розкладено в пам'яті, і відбувається індексація дуже швидко, тому ми можемо робити індексацію кожні 10 хвилин, повністю з нуля. Реплік логічних у нас — по парі. І якщо ми втрачаємо репліку, ми перемикаємося на її резерв. А якщо щось сталося зі sphinx, то через 10 хвилин він повністю переиндексируется, і все буде добре.



Як можна відновити експорт до DWH? Припустимо, що ми експортували, на DWH сталася аварія, ми втратили частину останніх даних. Експорт DWH у нас йде через окрему логічну репліку, і на цій репліці зберігаються останні чотири дні. Ми можемо просто руками заново викликати скрипт експорту і вивантажити всі ці дані. Плюс там є ще архів у півроку. Або, в крайньому випадку, оскільки у нас кілька standby, ми можемо взяти один з них, поставити на паузу і заново вивантажити, взагалі, всі дані з майстра в DWH.



Хгрс у нас реалізований поверх pgq (це Skytools), і завдяки цьому ми можемо робити такі хитрі штуки. Pgq — це, по суті, просто таблиця в базі, в ній зберігаються події. Вона приблизно так виглядає, як на малюнку. Там є час події id транзакції. Коли ми відновили клієнта xrpc, ми можемо взяти і зрушаться тому в цій черзі, і програти заново ті події, яких немає в одержувача.



Xdb — це у нас є сховище з декількох баз. 16 баз розташовані на восьми машинах. Це сховище у нас резервується наступним чином — просто бінарна реплікація Postgres налаштована з однієї машини на іншу. Тобто перша машина резервується standby'їм на другий, друга на третій, відповідно, восьма на першій. До того ж, програвання WAL'ів, там також відбувається затримка в чотири дні, тобто, по суті, у нас є чотири дні бекап будь-який з цих мод.



Зараз я детально розповім про те, що це таке. Логічна репліка побудована у нас на основі можливостей Postgres, це є view'ха майстрі і deferred тригер на потрібних таблицях. За цим триггерам спрацьовує спеціальна функція, яка пише в окрему табличку. Її можна вважати як матеріалізоване уявлення. І далі ця табличка засобами londiste реплікується на логічну ріпку.



Безпосередньо це як-то так виглядає, я не буду на цьому детально зупинятися.



А сам сервер логічної репліки, навіщо це взагалі потрібно? Це окремий сервер. Він характерний тим, що там всі знаходиться в пам'яті, тобто shared_buffers такого розміру, що вся ця табличка та її індекси повністю в нього влазять. Це дозволяє на таких логічних репліках обслуговувати велику навантаження, зокрема, наприклад, одна ріпка обслуговує у нас 7000 транзакцій в секунду, і 1000 подій в чергу з майстра в неї ллється. Т. к. це логічна репліка реалізована засобами londiste і pgq, то там є зручна штука — відстеження, які транзакції вже програлися на цій логічній репліці. І ось на основі цієї штуки можна робити такі речі як Undo.



Я вже говорив, що реплік у нас дві штуки, ми можемо відновлюватися, просто перемикаючись. Якщо одна репліка загубилася, перемикається на другу. Це можливо із-за того, що pgq дозволяє підписати на одну чергу кілька споживачів. Ріпка впала, і далі нам потрібно відновити її копію. Якщо це робити просто засобами londiste, то це займає у нас зараз для ріпки сайту 4 години, для сфінкса — 8 годин, т. к. там викликаються тригери, які нарізають дані для зручної індексації сфінкса, і це все дуже довго. Але виявилося, що є інший спосіб створити впала ріпку — можна зробити pg_dump з працюючою.



Але якщо просто зробити pg_dump і запустити на нього londiste, то це все не запрацює, тому що londiste відстежує і на майстра, і на логічній репліці поточну позицію програної транзакції. Тому там ще потрібно робити додаткові кроки. Потрібно поправити після відновлення dump'а на майстрі tick_id, щоб він відповідав тому tick_id, який на відновленій ріпці. Якщо так, через pg_dump копіювати, то все це займає не більше 15 хвилин.



Сам алгоритм як-то так виглядає.



Backup призначений для захисту від аварій, але безпосередньо з самим бекапом теж можуть відбуватися аварії. Наприклад, в Postgres команда архівування WAL, там не вказано, що потрібно робити fsynk, коли WAL записується в архів. Але це важлива річ і дозволяє захиститися від, припустимо, аварійного перезавантаження архіву. До того ж, у нас бекап ще резервується тим, що він копіюється в зовнішнє хмара. Але в планах: ми хочемо зробити два активних сервера архіву, щоб archive_command писав на обидва WAL. Ще можна сказати, що спочатку ми експериментували з pg_receivexlog для того, що отримувати безпосередньо на самих серверах архіву WAL, але виявилося, що 9.2 його практично неможливо використати, тому що він не робить fsynk, не відстежує, які WAL він вже отримав з майстра, які можна чистити за checkpoint. Зараз в Postgres це доробили. І, можливо, в майбутньому ми будемо використовувати не archive_command, а pg_receivexlog все-таки.



Ми не використовуємо streaming у себе. Тобто те, про що я розповідав, це все засновано лише на WAL архіві. Це було зроблено через те, що складно забезпечити при streaming ще й архів, тому що якщо, наприклад, беремо архів з standby, бекап завершився, а майстер ще не встиг запакувати всі ці WAL'и, потрібні для відновлення бекапа. І ми отримуємо битий бекап. Це можна обійти, якщо у нас, припустимо, standby, з якого ми беремо бекап, відстає на 12 годин, як у нас. Або — в Postgres 9.5 зробили таку настройку archive_mode=always, за якої такої проблеми не буде. Можна буде брати спокійно бекап з standby і отримувати WAL'и безпосередньо теж зі standby в архів.



Недостатньо робити просто бекап, його ще потрібно перевіряти, чи все там коректно у нас забэкапилось. Ми це робимо на тестовому сервері, і для цього написали спеціальний скрипт перевірки бекапа. Він заснований на тому, що перевіряє після відновлення сервера і запуску повідомлення про помилки в балці сервера. І для кожної бази, відновленої на кластері, викликається спеціальна функція перевіряє check_backup, яка виконує додаткові перевірки. Зокрема, така перевірка, що дата останньої транзакції повинна відрізнятися від дати останнього оголошення не більш ніж на хвилину. Тобто якщо ніяких дірок немає, ми вважаємо, що бекап відновлений коректно.



На слайдах можна подивитися, які конкретно помилки ми аналізуємо в балці при перевірці бекапа.

Раніше ми перевіряли бекапи з допомогою виконання vacuum'а всієї бази і вичитування таблиць, але потім вирішили від цього відмовитися, тому що ми вважаємо за поновленому бэкапу ще звіти, і якщо звіти рахуються коректно, немає ніяких дірок, дивних значень, то бекап зроблено коректно.



Я розповідав про асинхронну реплікацію, але іноді хочеться зробити синхронну. У нас Avito складається з безлічі сервісів, один з таких сервісів — це платіжний сервіс. І завдяки тому, що він виділений, ми можемо зробити синхронну реплікацію для нього, оскільки він працює на окремій базі. Там не така велика навантаження і стандартна latency мережі дозволяє нам включити там синхронну реплікацію.



Що можна сказати в кінці? Все-таки, незважаючи на те, що реплікація синхронна, можна в такому режимі працювати і відновлюватися, якщо подивитися на свої зв'язані системи, то можна придумати, і як їх можна відновлювати. Важливо ще тестувати резервні копії.



Ще таке зауваження. У нас скрипт відновлення, в кінці нього необхідно змінити DNS'и, т. к. у нас майстер це або слэйв — це закріплене в DNS. Ми зараз думаємо про те, щоб використовувати якісь системи типу ZooKeeper для того, щоб автоматично перемикати DNS. Такі плани.

Ця доповідь — розшифровка одного з кращих виступів на конференції розробників високонавантажених систем HighLoad++. Зараз ми активно готуємо конференцію 2016 року — у цьому році HighLoad++ пройде в Сколково, 7 і 8 листопада.

Команда Avito традиційно пропонує дуже сильні виступи, наприклад, в цьому році це будуть:
Також деякі з цих матеріалів використовуються нами в навчальному онлайн-курс по розробці високонавантажених систем HighLoad.Guide — це ланцюжок спеціально підібраних листів, статей, матеріалів, відео. Вже зараз у нашому підручнику понад 30 унікальних матеріалів. Підключайтеся!
Джерело: Хабрахабр

0 коментарів

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