Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Senior: питання на співбесіді з теми «Розподілені системи»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

4 питання

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

Приклад - оформлення замовлення:

1. Замовлення: створити (статус pending)       компенсація: скасувати
2. Склад: зарезервувати товар                   компенсація: зняти резерв
3. Платежі: списати кошти                       компенсація: повернути кошти
4. Замовлення: підтвердити

Якщо оплата не пройшла (крок 3) - знімається резерв (компенсація 2) і скасовується замовлення (компенсація 1).

Компенсація - не відкат. Інші частини системи вже могли побачити проміжні стани (резерв товару, замовлення pending). Компенсація - нова бізнес-операція («повернути кошти»), а не магічне повернення в минуле.

Хореографія - кожен сервіс реагує на події інших:

OrderCreated → Склад резервує → StockReserved → Платежі списують → PaymentCompleted → Замовлення підтверджує
  • плюси: немає центрального координатора, сервіси слабо зв'язані, просто для 2-3 кроків;
  • мінуси: логіку процесу ніде не видно цілком - вона розподілена по слухачах; важко відповісти «на якому кроці зараз замовлення?»; ризик циклічних залежностей подій.

Оркестрація - окремий оркестратор (сервіс чи об'єкт процесу) керує кроками, надсилаючи команди й чекаючи відповідей:

Оркестратор: «Склад, зарезервуй» → OK → «Платежі, спиши» → помилка → «Склад, зніми резерв» → «Замовлення, скасуй»
  • плюси: процес описаний в одному місці, стан саги зберігається явно, легко моніторити й змінювати;
  • мінуси: оркестратор - додатковий компонент і ризик «знати забагато».

Що обов'язково для саг:

  • ідемпотентність кроків і компенсацій - повідомлення можуть повторюватися;
  • збереження стану саги для відновлення після збою;
  • семантичні блокування - статус «pending», щоб інші процеси не працювали з незавершеними даними;
  • компенсації, що теж можуть не вдатися - повтори й ручне втручання як останній рубіж;
  • відсутність ізоляції: інші бачать проміжні стани - бізнес має погодитися з цим.

Інструменти: у межах Laravel - ланцюжки й пакети джоб (Bus::chain, Bus::batch) і явна модель стану процесу; для складних процесів - рушії робочих процесів (Temporal тощо).

Правило вибору: хореографія - для простих процесів з кількома кроками; оркестрація - для складних, де важлива видимість і керування процесом.

Докладніше в документації: Microservices.io: Saga

Двофазний коміт (2PC) - протокол атомарної транзакції на кількох учасниках (базах, сервісах) під керуванням координатора.

Фаза 1 - підготовка (prepare): координатор питає кожного учасника «чи можеш закомітити?». Учасник виконує зміни, блокує ресурси, записує все необхідне для коміту й відповідає «готовий» чи «ні».

Фаза 2 - коміт: якщо всі відповіли «готовий», координатор надсилає «комітьте»; якщо хоч один «ні» - «відкотіть».

Результат - усі учасники або закомітили, або відкотили.

Чому в мікросервісах його уникають:

1. Блокування й доступність. Між фазами учасники тримають блокування і чекають рішення координатора. Якщо координатор упав після фази 1, учасники в стані «готовий» не можуть ні закомітити, ні відкотити самостійно - ресурси заблоковані, доки координатор не відновиться. Одна несправна ланка зупиняє всіх.

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

3. Зв'язаність. Усі учасники мають підтримувати той самий протокол (XA) і бути доступні одночасно. Це суперечить ідеї незалежних сервісів з різними технологіями й сховищами.

4. Підтримка. Брокери повідомлень, NoSQL-бази, зовнішні API (платіжні системи) зазвичай не беруть участі в 2PC. Транзакцію «база + Kafka + Stripe» двофазним комітом не зробити.

5. Теорема CAP на практиці: при розриві мережі 2PC жертвує доступністю заради узгодженості - для більшості вебсистем це неприйнятна ціна.

Що використовують натомість:

  • саги з компенсуючими діями;
  • Transactional Outbox для атомарного «зміна + подія»;
  • ідемпотентні споживачі і кінцева узгодженість;
  • перегляд меж: якщо операція постійно вимагає атомарності між двома сервісами - можливо, це один сервіс (чи один агрегат), розрізаний неправильно.

Де 2PC (і схожі протоколи) все ж живуть: усередині розподілених баз даних (Spanner, CockroachDB використовують його варіанти з консенсусом), у класичних корпоративних системах з XA-транзакціями між кількома базами одного постачальника. На рівні архітектури вебсервісів - майже ніколи.

Для співбесіди: важливо не лише описати фази, а й пояснити проблему блокування при збої координатора - саме вона робить 2PC непридатним для слабко зв'язаних систем.

Докладніше в документації: Patterns of Distributed Systems: Two-Phase Commit

CAP (Ерік Брюер): розподілена система з реплікацією даних не може одночасно гарантувати всі три властивості:

  • C - узгодженість (consistency): кожне читання бачить останній запис (лінеаризовність);
  • A - доступність (availability): кожен запит до робочого вузла отримує відповідь;
  • P - стійкість до розділення мережі (partition tolerance): система працює, коли частина вузлів не може зв'язатися з іншими.

Як розуміти правильно. «Вибрати два з трьох» - спрощення. Розділення мережі в реальних системах трапляються, тож P не обирають - з ним живуть. Справжній вибір - що робити під час розділення:

  • CP: відмовити частині запитів (повернути помилку), але не віддати застарілі дані - так поводяться системи на консенсусі (etcd, ZooKeeper), основна база з синхронною реплікацією;
  • AP: відповідати всім, але дані на різних вузлах можуть тимчасово розходитися - Cassandra, DynamoDB у режимі кінцевої узгодженості, DNS.

PACELC (Даніель Абаді) доповнює CAP тим, що відбувається без збою:

if Partition → вибір між Availability і Consistency
Else         → вибір між Latency і Consistency

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

Як це допомагає на практиці:

  • Postgres/MySQL з асинхронними репліками: читання з репліки - швидко, але можливо застаріле (PA/EL-поведінка для читань з реплік). Звідси «прилипання» читань до основного сервера після запису;
  • Redis як кеш - типово жертвує узгодженістю заради швидкості;
  • синхронна реплікація для фінансових даних - узгодженість ціною затримки й доступності;
  • налаштовувані системи (Cassandra, DynamoDB) дозволяють обирати рівень узгодженості на кожен запит.

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

Обмеження CAP: теорема говорить про дуже конкретну модель (лінеаризовність, повна доступність) - реальні системи мають багато проміжних рівнів узгодженості (читання своїх записів, монотонні читання, причинна узгодженість). CAP - інструмент мислення, а не класифікатор баз даних.

Докладніше в документації: PACELC

Інтуїтивно здається: у кожної події є мітка часу, тож порядок подій очевидний. У розподіленій системі це не так.

Чому годинник ненадійний:

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

Типова помилка:

Сервіс A о 10:00:00.120: ProfileUpdated(name="Оля")
Сервіс B о 10:00:00.100: ProfileUpdated(name="Олена")   ← годинник B відстає на 50 мс

Правило «перемагає пізніша мітка часу» (last write wins) тихо втрачає справжню останню зміну.

Що використовують замість фізичного часу:

1. Версії сутності - лічильник, що збільшується з кожною зміною в джерелі істини:

CustomerRenamed { customer_id: 7, version: 15, name: "Оля" }

Споживач застосовує подію, лише якщо version більша за збережену, - запізнілі й дубльовані події ігноруються.

2. Логічні годинники Лампорта - лічильник, який кожен вузол збільшує при події й синхронізує з отриманими повідомленнями (max(local, received) + 1). Дають порядок, узгоджений з причинністю: якщо подія A спричинила B, лічильник A менший.

3. Векторні годинники - виявляють конкурентні зміни (жодна не спричинила іншу) - тоді конфлікт треба розв'язати явно, а не мовчки перезаписати.

4. Гібридні логічні годинники (HLC) - поєднують фізичний час із логічним лічильником; використовуються в розподілених базах (CockroachDB).

5. Порядок у брокері повідомлень: Kafka гарантує порядок у межах партиції - події однієї сутності публікують з тим самим ключем (order_id), і вони обробляються послідовно.

6. Одне джерело істини для порядку - послідовність у базі власника даних (автоінкремент, sequence) чи запис усіх змін однієї сутності через один сервіс.

Практичні правила:

  • мітки часу - для показу людям і приблизного аналізу, а не для вирішення конфліктів;
  • для конкурентних змін однієї сутності - оптимістичне блокування з версією (WHERE version = ?);
  • події, що мають застосовуватися в порядку, - з ключем партиціювання за сутністю й номером версії.

Докладніше в документації: Patterns of Distributed Systems: Lamport Clock