Більше розробників повинні знати це про бази даних

Прим. перев.: Jaana Dogan - досвідчений інженер з Google, яка в даний момент займається питаннями спостереження production-сервісів компанії, написаних на Go. У цій статті, що здобула велику популярність у англомовної аудиторії, вона в 17 пунктах зібрала важливі технічні деталі, що стосуються СУБД (а іноді розподілених систем в цілому), які корисно враховувати розробникам великих/вимогливих додатків.

Більше розробників повинні знати це про бази даних

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

  1. Вам пощастило, якщо 99,999% часу не є джерелом проблем.
  2. ACID має на увазі безліч різних речей.
  3. Кожна база має свої механізми забезпечення узгодженості та ізоляції.
  4. Оптимістичне блокування приходить на допомогу, коли складно утримувати звичайну.
  5. Існують й інші аномалії крім «брудних» читань та втрати даних.
  6. База даних та користувач не завжди приходять до єдиної думки щодо порядку дій.
  7. Шардинг на рівні програми можна винести за межі програми.
  8. Автоінкрементування може бути небезпечним.
  9. Застарілі дані можуть бути корисні і не потребувати блокування.
  10. Спотворення типові для будь-яких джерел часу.
  11. У затримки безліч значень.
  12. Вимоги щодо продуктивності слід оцінювати для конкретної транзакції.
  13. Вкладені транзакції можуть бути небезпечними.
  14. Транзакції не повинні бути прив'язані до стану програми.
  15. Планувальники запитів можуть багато розповісти про бази даних.
  16. Онлайн-міграція складна, але можлива.
  17. Значне збільшення бази даних тягне у себе зростання непередбачуваності.

Хочу висловити вдячність Emmanuel Odeke, Rein Henrichs та іншим за їхній зворотний зв'язок за ранньою версією цієї статті.

Вам пощастило, якщо 99,999% часу мережа не є джерелом проблем

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

Маючи показник доступності 99,999% для Spanner (глобально розподіленої бази даних Google), Google стверджує, що тільки 7,6% проблем пов'язані з мережею. При цьому компанія називає свою спеціалізовану мережу "головною опорою" високої доступності. Дослідження Bailis та Kingsbury, проведене в 2014 році, кидає виклик одному зпомилок про розподілені обчислення», які Peter Deutsch сформулював у 1994 році. Чи справді мережа надійна?

Повноцінних досліджень поза межами компаній-гігантів, проведених для широкого інтернету, просто не існує. Також недостатньо даних від основних гравців у тому, який відсоток проблем їхніх клієнтів пов'язані з мережею. Ми добре обізнані про перебої в мережевому стеку великих хмарних провайдерів, здатних вивести з ладу цілий шматок Інтернету на кілька годин просто тому, що це найрезонансніші події, що стосуються великої кількості людей і компаній. Перебої в роботі мережі можуть бути джерелом проблем у набагато більшій кількості випадків, навіть якщо не всі ці випадки потрапляють до центру загальної уваги. Клієнти хмарних послуг також нічого не знають про причини проблем. У разі збою практично неможливо пов'язати його з помилкою мережі на стороні постачальника послуг. Для них сторонні послуги – це чорні ящики. Неможливо оцінити вплив, не будучи великим постачальником послуг.

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

ACID має на увазі безліч різних речей

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

Коли я тільки-но прийшла в галузь, наш техлід міркував про те, наскільки актуальна концепція ACID. Задля справедливості слід зазначити, що ACID вважається зразковим описом, а не суворим стандартом реалізації. Сьогодні я знаходжу його переважно корисним, оскільки він порушує певну категорію питань (і пропонує спектр можливих рішень).

Не будь-яка СУБД відповідає вимогам ACID; при цьому підтримують ACID реалізації баз по-різному розуміють набір вимог. Одна з причин різнорідної реалізації ACID пов'язана з великою кількістю компромісів, на які доводиться йти задля впровадження вимог ACID. Творці можуть представляти свої бази даних як ACID-сумісні, при цьому інтерпретація прикордонних випадків може кардинально відрізнятися, як і механізм обробки «неймовірних» подій. Принаймні розробники можуть на високому рівні розібратися в тонкощах реалізації баз, щоб отримати правильне уявлення про їх особливі режими та компроміси під час проектування.

Дебати про те, наскільки MongoDB відповідає вимогам ACID, не припиняються навіть після виходу четвертої версії. MongoDB довгий час не підтримувала журналування, хоча за умовчанням фіксація даних на диск здійснювалася не частіше ніж раз на 60 секунд. Подайте наступний сценарій: програма проводить два записи (w1 і w2). MongoDB успішно зберігає w1, але w2 втрачається через апаратний збій.

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

Фіксація на диск – дорогий процес. Уникаючи частих фіксацій, розробники покращують показники запису, жертвуючи надійністю. На сьогодні MongoDB підтримує журналування, проте «брудні» записи все ще можуть позначитися на цілісності даних, оскільки за замовчуванням журнали фіксуються кожні 100 мс. Тобто схожий сценарій все ще можливий для журналів та змін, представлених у них, хоча ризик набагато нижчий.

Кожна база має свої механізми забезпечення узгодженості та ізоляції.

З-поміж вимог ACID узгодженість та ізоляція можуть похвалитися найбільшою кількістю різних реалізацій, оскільки спектр компромісів ширший. Слід сказати, що узгодженість та ізоляція — досить затратні функції. Вони вимагають координації та посилюють конкуренцію задля узгодженості даних. Складність проблеми значно зростає за необхідності горизонтально масштабувати базу кількох ЦОД (особливо якщо вони перебувають у різних географічних регіонах). Досягти високого рівня узгодженості дуже складно, оскільки принагідно знижується доступність і збільшується сегментація мережі. Для більш загального пояснення цього феномену раджу звернутись до теоремі САР. Варто також відзначити, що програми здатні справлятися з невеликою неузгодженістю, а програмісти можуть досить добре представляти нюанси проблеми, щоб реалізувати додаткову логіку в додатку, що дозволяє справлятися з неузгодженістю, не покладаючись на БД в цьому питанні.

СУБД часто надають різні рівні ізоляції. Розробники додатків можуть вибрати найбільш ефективний з них, ґрунтуючись на своїх уподобаннях. Низька ізоляція дозволяє збільшити швидкість, водночас підвищуючи ризик виникнення гонки даних (data race). Висока ізоляція знижує цю ймовірність, але уповільнює роботу і може призвести до конкуренції, яка призведе до таких гальм у базі, що почнуться збої.

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

Стандарт SQL визначає лише чотири рівні ізоляції, хоча в теорії та на практиці їх кількість значно більша. Jepson.io пропонує чудовий огляд існуючих моделей паралелізму. Наприклад, Google Spanner гарантує зовнішню серіалізацію із синхронізацією годинника, і хоча це більш суворий шар ізоляції, він не визначений у стандартних шарах ізоляції.

У стандарті SQL згадуються такі рівні ізоляції:

  • Serializable (Найжорсткіший і витратний): серіалізується виконання має ефект, аналогічний деякому послідовному виконанню транзакцій. Послідовне виконання означає, що кожна наступна транзакція починається тільки після завершення попередньої. Слід зазначити, що рівень Serializable часто реалізується у вигляді так званої snapshot-ізоляції (наприклад, Oracle) через відмінностей в інтерпретації, хоча сама snapshot-ізоляція не представлена ​​в стандарті SQL.
  • Repeatable reads: незафіксовані записи в поточній транзакції доступні для поточної транзакції, але зміни, зроблені іншими транзакціями (наприклад, нові рядки), невидимі.
  • Read committed: незафіксовані дані недоступні для транзакцій. У цьому випадку транзакції можуть бачити лише зафіксовані дані, при цьому можуть відбуватися фантомні читання. Якщо транзакція вставить і зафіксує нові рядки, поточна транзакція зможе побачити їх при запиті.
  • Read uncommitted (найменше суворий і витратний рівень): «брудне» читання припустимо, транзакції можуть бачити незафіксовані зміни, внесені іншими транзакціями. На практиці цей рівень може стати в нагоді для приблизних оцінок, наприклад, для запитів COUNT(*) на таблиці.

Рівень Serializable мінімізує ризик виникнення гонок даних, при цьому найбільш витратний у реалізації та призводить до найвищого конкурентного навантаження на систему. Інші рівні ізоляції реалізувати легше, проте зростає можливість гонок даних. Деякі СУБД дозволяють задати власний рівень ізольованості, в інших є виражені переваги і підтримуються не всі рівні.

Підтримка рівнів ізольованості часто рекламується у тій чи іншій СУБД, проте лише ретельне вивчення її поведінки дозволяє прояснити, що відбувається.

Більше розробників повинні знати це про бази даних
Огляд аномалій паралелізму на різних рівнях ізоляції для різних СУБД

Martin Kleppmann у своєму проекті Ермітаж проводить порівняння різних рівнів ізоляції, розповідає про аномалії паралелізму і про те, чи здатна база даних дотримуватись певного рівня ізоляції. Дослідження Kleppmann'а показують, наскільки по-різному розробники баз даних уявляють рівні ізоляції.

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

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

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

UPDATE products
SET name = 'Telegraph receiver', version = 2
WHERE id = 1 AND version = 1

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

Існують й інші аномалії крім «брудних» читань та втрати даних

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

Одним із прикладів таких аномалій є спотворення запису (write skews). Спотворення складно виявити, оскільки зазвичай їх активно не шукають. Вони пов'язані не з «брудними» читаннями чи втратою даних, і з порушеннями логічних обмежень, накладених на дані.

Наприклад, давайте розглянемо програми для моніторингу, які потребують, щоб один оператор постійно був on-call:

BEGIN tx1;                      BEGIN tx2;
SELECT COUNT(*)
FROM operators
WHERE oncall = true;
0                               SELECT COUNT(*)
                                FROM operators
                                WHERE oncall = TRUE;
                                0
UPDATE operators                UPDATE operators
SET oncall = TRUE               SET oncall = TRUE
WHERE userId = 4;               WHERE userId = 2;
COMMIT tx1;                     COMMIT tx2;

У ситуації вище виникне спотворення запису, якщо обидві транзакції успішно зафіксовано. Хоча «брудних» читань чи втрати даних не відбулося, цілісність даних виявилася порушеною: тепер дві людини одночасно вважаються on-call.

Серіалізована ізоляція, будова схеми або обмеження БД можуть сприяти усуненню спотворень запису. Розробники повинні вміти виявляти такі аномалії під час розробки, щоб уникнути їх у production. При цьому спотворення запису дуже важко шукати в кодовій базі. Особливо у великих системах, коли різні команди розробників відповідають за реалізацію функцій на основі тих самих таблиць і не узгоджують між собою особливості доступу до даних.

База даних та користувач не завжди приходять до єдиної думки щодо порядку дій

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

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

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

result1 = T1() // реальні результати - це promises
result2 = T2()

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

Шардинг на рівні програми можна винести за межі програми

Шардинг - це спосіб горизонтального поділу бази даних. Деякі бази вміють автоматично розбивати дані горизонтально, тоді як інші цього не вміють, або це вдається не надто добре. Коли архітектори даних/розробники мають можливість спрогнозувати, як саме здійснюватиметься доступ до даних, вони можуть створити горизонтальні партиції в просторі користувача замість того, щоб делегувати цю роботу БД. Цей процес називається "шардинг на рівні програми" (application-level sharding).

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

Більше розробників повинні знати це про бази даних
Приклад архітектури, в якій сервери програми відокремлені від сервісу шардингу

Винесення шардингу в окремий сервіс розширює можливості використання різних стратегій шардингу без необхідності заново деплоїти програми. Вітес — приклад такої системи шардингу лише на рівні докладання. Vitess забезпечує горизонтальний шардинг для MySQL та дозволяє клієнтам підключатися до нього через протокол MySQL. Система сегментує дані по різних вузлах MySQL, які нічого не знають один про одного.

Автоінкрементування може бути небезпечним

AUTOINCREMENT – поширений спосіб генерації первинних ключів. Нерідко трапляються випадки, коли БД використовуються як генератори ID, а в базі є таблиці, призначені для генерації ідентифікаторів. Існує кілька причин, з яких генерація первинних ключів за допомогою auto-incrementing'а далека від ідеалу:

  • У розподіленій базі даних auto-incrementing є серйозною проблемою. Для створення ID необхідне глобальне блокування. Натомість можна згенерувати UUID: для цього не потрібно взаємодія різних вузлів бази. Auto-incrementing з блокуванням може призвести до конкуренції та значно знизити продуктивність при вставках у розподілених ситуаціях. Деяким СУБД (наприклад, MySQL) може бути потрібне особливе налаштування і більш пильну увагу, щоб правильним чином організувати реплікацію майстер-майстер. А при конфігуруванні легко помилитись, що призведе до збоїв запису.
  • У деяких баз алгоритми партикування засновані на первинних ключах. Послідовні ID можуть призвести до непередбачуваного виникнення гарячих точок і підвищеного навантаження на деякі партиції, в той час як інші простоюватимуть.
  • Первинний ключ — це найшвидший спосіб доступу до рядка бази даних. За наявності кращих способів ідентифікації записів, послідовні ID можуть перетворити найважливіший стовпець у таблицях на марний, наповнений безглуздими значеннями. Тому за можливості, будь ласка, вибирайте глобально унікальний та природний первинний ключ (наприклад, ім'я користувача).

Перш ніж визначитися з підходом, розгляньте вплив auto-increment'ованих ID та UUID на індексування, партикування та шардинг.

Застарілі дані можуть бути корисними і не вимагати блокування

Управління паралельним доступом за допомогою багатоверсійності (MVCC) реалізує багато вимог до узгодженості, які коротко обговорювалися вище. Деякі бази даних (наприклад, Postgres, Spanner) за допомогою MVCC «годують» транзакції знімки (snapshot'и) — старіші версії бази даних. Транзакції зі snapshot'ами можна серіалізувати для забезпечення узгодженості. При читанні зі старого snapshot'а зчитуються застарілі дані.

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

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

Більше розробників повинні знати це про бази даних
Сервер програми зчитує з локальної репліки дані, застарілі на 5 секунд, навіть якщо їхня найсвіжіша версія доступна з іншого боку Тихого океану

СУБД автоматично зачищають старі версії і в деяких випадках дозволяють робити це на запит. Наприклад, Postgres дозволяє користувачам робити VACUUM на запит, а також періодично проводить цю операцію в автоматичному режимі. Spanner запускає збирач сміття, щоб позбутися знімків старше однієї години.

Спотворення схильні до будь-яких джерел часу

Найпоширеніший секрет в інформатиці полягає в тому, що всі тимчасові API брешуть. Насправді наші машини не знають точний час. Комп'ютери містять кварцові кристали, що генерують коливання, які використовуються для часу. Однак вони недостатньо точні та можуть випереджати/відставати від точного часу. Зсув може досягати 20 секунд на добу. Тому час на наших комп'ютерах необхідно періодично синхронізувати із мережним.

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

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

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

У Google TrueTime реалізовано зовсім інший підхід. Більшість людей вважають, що прогрес Google у цьому напрямі пояснюється банальним переходом на атомний та GPS-годинник, але це лише частина загальної картини. Ось як влаштований TrueTime:

  • TrueTime використовує два різні джерела: GPS і атомний годинник. Цього годинника некорелюючі точки відмови (failure modes) [детальніше див. на 5 сторінці тут - прим. перев.), тому їхнє спільне використання підвищує надійність.
  • У TrueTime незвичайний API. Він повертає час у вигляді інтервалу із закладеною похибкою виміру та невизначеністю. Реальний момент часу знаходиться десь між верхньою та нижньою межами інтервалу. Spanner, розподілена база даних від Google, просто чекає моменту, коли можна з упевненістю сказати, що поточний час вийшов за межі інтервалу. Цей метод привносить певну затримку в систему, якщо невизначеність на майстрах висока, але забезпечує коректність навіть у глобально розподіленої ситуації.

Більше розробників повинні знати це про бази даних
Компоненти Spanner використовують TrueTime, де TT.now() повертає інтервал, тому Spanner просто спить до моменту, коли можна напевно сказати, що поточний час перевалило через певну позначку

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

У затримки безліч значень

Запитавши десяток фахівців про те, що таке затримка, ви, напевно, отримаєте різні відповіді. У СУБД latency часто називається "затримкою бази даних", і вона відрізняється про ту, яку сприймає клієнт. Справа в тому, що клієнт спостерігає суму мережевої затримки та затримки БД. Здатність виділяти тип затримки критично важлива при налагодженні проблем, що наростають. Під час збирання та відображення метрик завжди намагайтеся стежити за обома типами.

Вимоги щодо продуктивності слід оцінювати для конкретної транзакції

Іноді характеристики продуктивності СУБД та її обмеження вказуються у вигляді пропускної спроможності write/read та затримки. Це дозволяє отримати загальне уявлення про ключові параметри системи, проте при оцінці продуктивності нової СУБД набагато більш всеосяжний підхід полягає в роздільній оцінці критичних операцій (для кожного запиту та/або транзакції). Приклади:

  • Пропускна здатність запису та затримка при вставці нового рядка в таблицю Х (з 50 млн. рядків) із заданими обмеженнями та заповненням рядків у зв'язаних таблицях.
  • Затримка при виведенні друзів друзів якогось користувача, коли середня кількість друзів дорівнює 500.
  • Затримка при вилученні топ-100 записів з історії користувача, коли він підписаний на 500 інших користувачів із Х записів на годину.

Оцінка та експериментування можуть включати подібні критичні випадки, доки ви не будете впевнені, що БД задовольняє вимогам до продуктивності. Аналогічне емпіричне правило також враховує цю розбивку при збиранні latency-метрик та визначенні SLO.

Пам'ятайте про високу потужність (cardinality) під час збору метрик для кожної операції. Використовуйте логи, збір подій або розподілене трасування для отримання налагоджувальних даних з високою потужністю. У статті "Want to Debug Latency?» Ви можете ознайомитись з методологіями налагодження затримок.

Вкладені транзакції можуть бути небезпечними

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

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

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

with newTransaction():
   Accounts.create("609-543-222")
   with newTransaction():
       Accounts.create("775-988-322")
       throw Rollback();

Яким буде результат наведеного вище коду? Чи відкотить він обидві транзакції, чи лише внутрішню? Що станеться, якщо ми покладатимемося на кілька рівнів бібліотек, які інкапсулюють створення транзакцій від нас? Чи зможемо ми виявляти та покращувати такі випадки?

Уявіть, що шар даних із кількома операціями (наприклад, newAccount) вже реалізовано у своїх транзакціях. Що станеться, якщо запустити їх у рамках бізнес-логіки вищого рівня, що працює у рамках власної транзакції? Якими в цьому випадку будуть ізоляція та узгодженість?

function newAccount(id string) {
  with newTransaction():
      Accounts.create(id)
}

Замість того, щоб шукати відповіді на такі нескінченні питання, краще уникайте вкладених транзакцій. Адже ваш шар даних може проводити операції високого рівня без створення власних транзакцій. Крім того, бізнес-логіка сама здатна ініціювати транзакцію, виконувати операції над нею, фіксувати чи переривати транзакцію.

function newAccount(id string) {
   Accounts.create(id)
}
// In main application:
with newTransaction():
   // Read some data from database for configuration.
   // Generate an ID from the ID service.
   Accounts.create(id)
   Uploads.create(id) // create upload queue for the user.

Транзакції не повинні бути прив'язані до стану програми

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

var seq int64
with newTransaction():
    newSeq := atomic.Increment(&seq)
    Entries.query(newSeq)
    // Other operations...

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

Планувальники запитів можуть багато розповісти про БД

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

SELECT * FROM articles where author = "rakyll" order by title;

Результати можна отримати двома способами:

  • Повне сканування таблиці: можна дивитися на кожен запис у таблиці та повертати статті з іменем автора, що збігається, а потім упорядкувати їх.
  • Індексне сканування: можна використовувати індекс, щоб знайти відповідні ідентифікатори, отримати ці рядки, а потім упорядкувати їх.

Завдання планувальника запитів у тому, щоб визначити, яка стратегія є найкращою. Варто врахувати, що планувальники запитів мають лише обмежені можливості прогнозування. Це може призводити до невдалих рішень. Адміністратори БД або розробники можуть використовувати їх для діагностики та тонкого настроювання неефективних запитів. Нові версії СУБД можуть налаштовувати планувальників запитів, а самодіагностика здатна допомогти при оновленні БД, якщо нова версія призводить до проблем з продуктивністю. Логи повільних запитів, повідомлення про проблеми із затримкою чи статистика часу виконання здатні допомогти з виявленням запитів, які потребують оптимізації.

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

Онлайн-міграція складна, але можлива

Онлайн-міграція, "жива" міграція або міграція в реальному часі означають перехід від однієї бази даних до іншої без простоїв та порушень коректності даних. Живу міграцію легше провести, якщо перехід відбувається у межах однієї й тієї ж СУБД/движка. Ситуація ускладнюється, коли необхідний переїзд на нову СУБД з іншою продуктивністю та вимогами до схеми.

Існують різні моделі онлайн-міграції. Ось одна з них:

  • Увімкніть подвійний запис в обидві бази. Нова база цьому етапі немає всіх даних, лише приймає нові дані. Переконавшись, можна переходити до наступного кроку.
  • Увімкніть читання з обох баз даних.
  • Налаштуйте систему так, щоб читання та запис насамперед проводилися з новою базою.
  • Зупиніть запис у стару базу, при цьому продовжуючи зчитувати дані. На цьому етапі нова БД, як і раніше, позбавлена ​​частини даних. Їх слід скопіювати зі старої бази.
  • Стара база доступна лише для читання. Скопіюйте відсутні дані зі старої БД в нову. Після завершення міграції переключіть шляхи на нову базу, а стару зупиніть та видаліть із системи.

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

Значне збільшення бази даних тягне за собою зростання непередбачуваності

Зростання БД призводить до непередбачуваних проблем, пов'язаних із її масштабом. Чим більше ми знаємо про внутрішній пристрій БД, тим краще можемо спрогнозувати, як вона масштабуватиметься. При цьому деякі моменти все ж таки неможливо передбачити.
Зі зростанням бази колишні припущення та очікування щодо обсягу даних та вимог до пропускної спроможності мережі можуть застаріти. Саме тоді постає питання про серйозний перегляд схеми, широкомасштабні поліпшення в експлуатації, переосмислення розгортань чи міграцію на інші СУБД, щоб уникнути потенційних проблем.

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

...

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

PS

Читайте також у нашому блозі:

Джерело: habr.com

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