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 тощо).
Правило вибору: хореографія - для простих процесів з кількома кроками; оркестрація - для складних, де важлива видимість і керування процесом.
Двофазний коміт (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 - інструмент мислення, а не класифікатор баз даних.
Інтуїтивно здається: у кожної події є мітка часу, тож порядок подій очевидний. У розподіленій системі це не так.
Чому годинник ненадійний:
- годинники різних серверів розходяться - на мілісекунди чи більше навіть з 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