Чому може знадобитися напівсинхронна реплікація?

Всім привіт. На зв'язку Владислав Родін. В даний час я викладаю на порталі OTUS курси, присвячені архітектурі ПЗ та архітектурі ПЗ, схильним до високого навантаження. Напередодні старту нового потоку курсу «Архітектор високих навантажень» я вирішив написати невеликий авторський матеріал, яким я хочу поділитися з вами.

Чому може знадобитися напівсинхронна реплікація?

Запровадження

Через те, що на HDD може виконуватися лише близько 400-700 операцій на секунду (що незрівнянно з типовими rps'ами, що припадають на високонавантажену систему), класична дискова база даних є вузьким шийкою архітектури. Тому необхідно приділити окрему увагу патернам масштабування даного сховища.

На даний момент є 2 патерни масштабування бази: реплікація та шардування. Шардування дозволяє масштабувати операцію запису, і, як наслідок, знижувати rps на запис, що припадає на один сервер кластера. Реплікація дозволяє робити те саме, але з операціями читання. Саме цьому патерну і присвячено цю статтю.

Реплікація

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

Незважаючи на простоту, існує кілька варіантів класифікації різних реалізацій даної схеми:

  • За ролями в кластері (master-master або master-slave)
  • По об'єктах, що пересилаються (row-based, statement-based або mixed)
  • За механізмом синхронізації нід

Сьогодні ми розберемося саме з 3 пунктом.

Як відбувається коміт транзакції

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

  1. Запис транзакції до журналу бази даних.
  2. Застосування транзакції у движку бази даних.
  3. Повернення підтвердження клієнту про успішне застосування транзакції.

У різних базах у даному алгоритмі можуть виникати нюанси: наприклад, у движку InnoDB бази MySQL є 2 журнали: один для реплікації (binary log), а інший для підтримки ACID (undo/redo log), тоді як у PostgreSQL є один журнал, що виконує обидві функції (write ahead log = WAL). Але вище представлено саме загальну концепцію, яка дозволяє такі нюанси не враховувати.

Синхронна (sync) реплікація

Давайте додамо в алгоритм коміту транзакції логіку щодо реплікування отриманих змін:

  1. Запис транзакції до журналу бази даних.
  2. Застосування транзакції у движку бази даних.
  3. Надсилання даних на всі репліки.
  4. Отримання підтвердження від усіх реплік про виконання ними транзакції.
  5. Повернення підтвердження клієнту про успішне застосування транзакції.

При цьому підході ми отримуємо низку недоліків:

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

Якщо з 1-м пунктом все більш-менш ясно, то причини 2-го пункту варто пояснити. Якщо за синхронної реплікації ми отримаємо відповідь хоча б від однієї ноди, ми відкочуємо транзакцію. Таким чином, збільшуючи кількість нід у кластері, ви збільшуєте ймовірність того, що операція запису зірветься.

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

Асинхронна (async) реплікація

Давайте видозмінимо попередній алгоритм. Дані на репліки ми надсилатимемо «колись потім», і «колись потім» зміни будуть на репліках застосовані:

  1. Запис транзакції до журналу бази даних.
  2. Застосування транзакції у движку бази даних.
  3. Повернення підтвердження клієнту про успішне застосування транзакції.
  4. Надсилання даних на репліки та застосування змін ними.

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

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

Напівсинхронна (semisync) реплікація

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

Спробуємо об'єднати 2 попередні підходи. Не довго триматимемо клієнта, але вимагатимемо, щоб дані зареплікувалися:

  1. Запис транзакції до журналу бази даних.
  2. Застосування транзакції у движку бази даних.
  3. Надсилання даних на репліки.
  4. Отримання підтвердження від репліки про отримання змін (застосовані вони будуть «колись потім»).
  5. Повернення підтвердження клієнту про успішне застосування транзакції.

Зверніть увагу на те, що при такому алгоритмі втрата транзакції відбувається тільки у разі падіння як ноди, що приймає зміни, так і репліки ноди. Імовірність такого збою визнається малою, і ці ризики приймаються.

Але за даного підходу можливий ризик фантомних читань. Уявімо такий сценарій: на кроці 4 ми не отримали підтвердження від жодної репліки. Ми повинні цю транзакцію відкочувати, а клієнту підтвердження не повертати. Оскільки дані були застосовані на кроці 2, між закінченням кроку 2 і відкатом транзакції виникає тимчасовий проміжок, протягом якого паралельні транзакції можуть побачити ті зміни, яких у базі бути не повинно.

Lose-less semisync реплікація

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

  1. Запис транзакції до журналу бази даних.
  2. Надсилання даних репліки.
  3. Отримання підтвердження від репліки про отримання змін (застосовані вони будуть «колись потім»).
  4. Застосування транзакції у движку бази даних.
  5. Повернення підтвердження клієнту про успішне застосування транзакції.

Тепер ми комітуємо зміни, лише якщо вони зареплікувалися.

Висновок

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

На цьому все. До зустрічі на курсі!

Джерело: habr.com

Додати коментар або відгук