Питання на співбесіді з Архітектура
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
100 питань
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
Шардинг - розподіл даних однієї логічної бази між кількома фізичними серверами за ключем шардингу. Кожен сервер (шард) зберігає частину рядків і обслуговує частину навантаження - зокрема записів, які реплікація не масштабує.
Стратегії вибору шарду:
- за діапазоном (користувачі 1-1 млн на шарді A) - просто, але нерівномірно: нові активні користувачі всі на останньому шарді;
- за хешем ключа - рівномірно, але діапазонні запити йдуть на всі шарди; додавання шарду переміщує багато даних (зменшує це консистентне хешування);
- за довідником - таблиця відповідності «ключ → шард»: гнучко, але довідник стає ще одним критичним компонентом;
- за орендарем (tenant) - кожен клієнт SaaS на своєму шарді: природна межа, дані клієнта разом.
Чому шардинг - останній засіб:
- запити між шардами:
JOIN, агрегати, сортування по всіх даних стають розподіленими - їх збирає застосунок; - транзакції між шардами - без звичних гарантій ACID; потрібні саги чи двофазний коміт;
- унікальність і ідентифікатори - автоінкремент на кожному шарді дасть дублікати; потрібні UUID/Snowflake;
- перебалансування - додати шард означає перенести частину даних під навантаженням;
- гарячі шарди - великий клієнт чи популярний ключ перевантажує один сервер;
- операційна складність - бекапи, міграції схеми, моніторинг на N серверах;
- вибір ключа майже незворотний - помилка дорого коштує через роки.
Що спробувати раніше:
- індекси, оптимізація запитів, усунення N+1;
- вертикальне масштабування бази (сучасні сервери - сотні ядер і терабайти пам'яті);
- кеш і репліки для читання;
- секціонування (partitioning) великих таблиць усередині однієї бази;
- винесення окремих доменів у власні бази (функціональне розділення) - лог подій, аналітика, пошук;
- архівація старих даних.
Коли шардинг виправданий: записи чи обсяг даних справді перевищують можливості найпотужнішого сервера, або вимоги до ізоляції клієнтів (SaaS з великими орендарями). Альтернатива - розподілені бази з вбудованим шардингом (Vitess, Citus, CockroachDB), що беруть частину складності на себе.
Класична задача системного дизайну. Важливо не одне «правильне» рішення, а структура міркувань: вимоги → оцінки → API → дані → вузькі місця.
1. Вимоги.
- функціональні: створити коротке посилання, перенаправити за ним, (опційно) власні аліаси, термін дії, статистика переходів;
- нефункціональні: переходи дуже швидкі й доступні; читань набагато більше, ніж записів; коди непередбачувані (не можна перебрати чужі посилання).
2. Оцінки (див. попередні розрахунки): ~10 записів і ~1000-5000 читань на секунду, ~1 ТБ за 5 років.
3. API:
POST /api/links {"url": "https://..."} → {"code": "aB3xK9q"}
GET /aB3xK9q → 301/302 Location: https://...
301 чи 302: 301 кешують браузери - менше навантаження, але зникає статистика повторних переходів. Для аналітики - 302.
4. Генерація коду - найцікавіша частина:
- хеш URL (перші символи base62 від SHA-256) - однакові URL дають однаковий код, але можливі колізії - потрібна перевірка;
- лічильник + base62 - унікально без колізій; 7 символів base62 = 62^7 ≈ 3,5 трлн кодів. Але послідовні коди передбачувані - перемішати (бієкція, шифрування лічильника);
- випадковий код + перевірка унікальності - простіше, при великій заповненості зростає ймовірність колізії.
5. Сховище: таблиця code → url з унікальним індексом на code. Пошук за ключем - ідеальний випадок для будь-якої бази; масштаб невеликий для шардингу.
6. Масштабування читань: кеш (Redis) перед базою: популярні посилання становлять малу частку, і кеш з LRU покриє більшість переходів. Плюс CDN/edge для редиректів.
7. Статистика переходів: не писати в базу синхронно на кожен перехід - події в чергу чи потік, агрегування пакетами.
8. Безпека й зловживання: перевірка URL на фішинг і шкідливі сайти, ліміти створення, заборона внутрішніх адрес.
Чого чекає інтерв'юер: уточнення вимог, обґрунтування вибору генерації кодів, розуміння, що основне навантаження - читання, і що кеш вирішує його дешевше за шардинг.
SLI (service level indicator) - вимірювана характеристика якості сервісу з погляду користувача:
- доступність: частка успішних запитів (не
5xx); - затримка: частка запитів, швидших за 300 мс;
- свіжість даних: частка оновлень, що з'явилися протягом хвилини.
SLO (service level objective) - цільове значення SLI за період:
- «99,9% запитів до API успішні за 30 днів»;
- «95% сторінок відкриваються швидше за 500 мс».
SLA (agreement) - договірне зобов'язання перед клієнтами з наслідками (компенсаціями). SLA зазвичай м'якший за внутрішній SLO, щоб мати запас.
Бюджет помилок - допустима «ненадійність», що випливає з SLO:
SLO 99,9% за 30 днів → 0,1% невдалих запитів
→ ~43 хвилини повної недоступності на місяць
Як бюджет помилок керує рішеннями:
- бюджет є - команда може ризикувати: часті релізи, експерименти, міграції;
- бюджет вичерпано - пріоритет на стабільність: заморожування ризикованих змін, виправлення причин збоїв, автоматизація відкатів.
Це прибирає вічну суперечку «розробка хоче швидше, експлуатація хоче стабільніше» - є об'єктивне число.
Чому не 100%:
- кожна наступна «дев'ятка» коштує в рази дорожче (резервування, кілька регіонів, черговість);
- користувач не помітить різниці між 99,99% і 100%, бо його мережа, провайдер і пристрій надійні менше;
- 100% означає «ніколи нічого не змінювати».
Практичні поради:
- SLI - з погляду користувача, а не серверів: «процесор на 40%» - не SLI, «90% запитів швидші за 200 мс» - SLI;
- перцентилі, а не середні: середня затримка ховає повільний «хвіст», який бачать реальні користувачі;
- кілька SLO на ключові сценарії (вхід, оформлення замовлення, пошук), а не один на весь сервіс;
- сповіщення за швидкістю витрачання бюджету (burn rate), а не за кожен окремий збій - менше хибних тривог.
Для невеликого проєкту досить почати з одного SLO доступності й одного затримки для головних сторінок - і вимірювати їх (Pulse, APM, uptime-моніторинг).
Докладніше в документації: Google SRE Book: Service Level Objectives
Каскадна відмова - збій одного компонента поширюється на інші й валить усю систему. Типовий сценарій:
- сервіс рекомендацій сповільнився (відповідає за 30 секунд замість 50 мс);
- процеси PHP, що чекають на нього, не звільняються;
- пул процесів вичерпано - уся сторінка товару, а за нею й увесь сайт перестає відповідати;
- користувачі оновлюють сторінку - навантаження зростає, повторні спроби добивають систему.
Причина - не відмова рекомендацій, а те, що від них синхронно залежала критична частина.
Плавна деградація - при збої некритичної частини система продовжує виконувати головне, з урізаною функціональністю:
- рекомендації недоступні - сторінка товару без блоку рекомендацій;
- пошук перевантажений - простий пошук за назвою замість повнотекстового;
- платіжний провайдер не відповідає - замовлення приймається зі статусом «очікує оплати».
Механізми:
1. Тайм-аути на всі зовнішні виклики - короткі, розраховані на нормальну відповідь, а не на «стандартні 30 секунд»:
Http::timeout(2)->connectTimeout(1)->get($url);
2. Запобіжник (circuit breaker) - після серії помилок перестати викликати сервіс на якийсь час і одразу повертати запасний варіант. Сервіс отримує час відновитися, а запити - швидку відповідь замість очікування.
3. Запасні відповіді (fallback) - кешоване значення, порожній блок, спрощена версія.
4. Ізоляція ресурсів (bulkheads) - окремі пули процесів, черги й з'єднання для різних залежностей: проблеми з однією не займають ресурси інших.
5. Обмеження й скидання навантаження (load shedding) - при перевантаженні відмовляти частині запитів одразу (503), щоб решта обслуговувалася нормально, - замість того щоб повільно обслуговувати всіх.
6. Повтори з експоненційною затримкою й розкидом - і з обмеженою кількістю; бездумні повтори множать навантаження на сервіс, що й так не справляється.
7. Асинхронність - некритичне (аналітика, листи, синхронізації) у черги, щоб збій отримувача не впливав на відповідь користувачу.
8. Прапорці функцій - швидко вимкнути важку функцію під час інциденту без деплою.
Перевірка: навмисно «вимикати» залежності в тестовому середовищі (chaos testing) і дивитися, що відбувається з головними сценаріями.
Докладніше в документації: Google SRE Book: каскадні відмови
Позиція «фреймворк - деталь» (чиста архітектура): бізнес-логіка не повинна знати про Laravel. Ніяких моделей Eloquent, фасадів, хелперів у ядрі - лише чисті PHP-класи, інтерфейси й DTO, а фреймворк лише доставляє запити й зберігає дані.
Позиція Laravel-спільноти: фреймворк - не деталь, а інструмент продуктивності. Eloquent, фасади, черги, події дають швидкість розробки саме завдяки тісній інтеграції. Абстрагування від них - це подвоєння коду заради заміни, яка ніколи не станеться.
Що коштує повна ізоляція:
- окремі доменні сутності й Eloquent-моделі + відображення між ними;
- репозиторії-обгортки над Eloquent з інтерфейсами;
- DTO на кожній межі;
- втрата зручностей (зв'язки, ліниве завантаження, події моделей,
toResource()); - новим людям складніше: «Laravel-проєкт» не схожий на Laravel.
Що вона дає:
- бізнес-правила тестуються миттєво, без бази й контейнера;
- складна предметна область (фінанси, страхування, логістика) не тоне в інфраструктурному коді;
- довговічність: ядро переживає великі оновлення фреймворку.
Прагматичний середній шлях, що працює в більшості проєктів:
- дії (actions) чи сервіси для сценаріїв - бізнес-логіка не в контролерах, моделях чи Livewire-компонентах;
- Eloquent лишається доменною моделлю - Active Record з методами домену (
$order->cancel()), скоупами й кастами; - ізолювати зовнішні системи, а не власну базу: інтерфейси для платежів, SMS, сторонніх API - тут заміна й фейки реально потрібні;
- value objects і енуми для доменних понять (гроші, статуси, діапазони дат);
- arch-тести на напрям залежностей (домен не імпортує
Illuminate\Http); - фасади - за потреби, з розумінням, що в тестах вони підміняються (
Mail::fake()).
Критерій вибору: складність предметної області порівняно з технічною. CRUD-адмінка, каталог, блог - Laravel-шлях. Складні правила з багатьма інваріантами й довгим життям - більше ізоляції, хоча б для ядра цієї частини (модульний моноліт, де лише складний модуль має «чисту» архітектуру).
Найгірший варіант - половинчастий: інтерфейси репозиторіїв, що повертають моделі Eloquent, і «сервіси», що лише проксюють виклики. Складність є, а користі немає.
Еволюційна архітектура (Ніл Форд, Ребекка Парсонс, Патрік Куа) - підхід, за яким архітектуру не проєктують раз і назавжди, а постійно змінюють разом із системою. Ключове питання - як змінювати, не втрачаючи важливих властивостей (продуктивності, безпеки, модульності).
Функція придатності (fitness function) - автоматична перевірка конкретної архітектурної властивості. Термін запозичено з еволюційних алгоритмів, де функція придатності оцінює, наскільки рішення близьке до мети.
Приклади функцій придатності:
- модульність: arch-тести Pest - «модуль Billing не залежить від внутрішніх класів Catalog», «домен не імпортує HTTP-шар»;
- продуктивність: тест, що головна сторінка робить не більше 15 SQL-запитів (
expectsDatabaseQueryCount); бюджет часу відповіді в навантажувальних тестах; - розмір збірки фронтенду: CI падає, якщо JavaScript-бандл перевищив 250 КБ;
- безпека:
composer auditбез вразливостей високого рівня, заборонені функції черезarch()->preset()->security(); - якість: рівень PHPStan, покриття критичних модулів тестами;
- операційні: моніторинг SLO в продакшені - «функція придатності», що працює постійно.
Види:
- атомарні (одна властивість) і цілісні (поєднання - продуктивність разом із безпекою);
- тригерні (запускаються в CI на кожну зміну) і безперервні (моніторинг у продакшені);
- статичні (правила коду) і динамічні (вимірювання під навантаженням).
Чому це важливо: архітектура деградує поступово - кожен окремий pull request «трохи» порушує межі, і через рік модульний моноліт стає великою грудкою бруду. Функції придатності роблять архітектурні рішення виконуваними: порушення помітне в момент внесення, а не на ретроспективі.
Інші принципи еволюційної архітектури:
- останній відповідальний момент для рішень - вирішувати, коли знань достатньо, але до того, як відкладання стане дорожчим;
- зменшення зв'язності - чим менше залежностей, тим легше змінювати частини незалежно;
- малі інкрементальні зміни з можливістю відкату - замість великих переписувань.
Як почати: обрати 2-3 властивості, порушення яких найболючіші для проєкту (часто - межі модулів і N+1), і автоматизувати їх перевірку в CI.
Докладніше в документації: Building Evolutionary Architectures
Роберт Мартін запропонував метрики, що перетворюють розмови про «зв'язність модулів» на числа.
Аферентна зв'язність (Ca) - скільки інших модулів залежать від цього. Висока Ca - модуль важливий для багатьох; його зміна зачіпає багатьох.
Еферентна зв'язність (Ce) - від скількох модулів залежить цей. Висока Ce - модуль залежить від багатьох; зміни в будь-якому з них можуть його зламати.
Нестабільність:
I = Ce / (Ca + Ce) від 0 до 1
- I ≈ 0 - стабільний модуль: від нього багато залежать, він сам ні від кого. Змінювати його дорого (багато споживачів). Приклад: спільні value objects, базові інтерфейси, ядро предметної області;
- I ≈ 1 - нестабільний: ні від нього ніхто, а він від багатьох. Змінювати легко й безпечно. Приклад: контролери, консольні команди, UI.
Принцип стабільних залежностей: залежності мають іти в напрямку стабільності - нестабільні модулі залежать від стабільних, а не навпаки. Якщо стабільний модуль (від якого залежить половина системи) залежить від нестабільного, кожна зміна нестабільного «трусить» усю систему.
Абстрактність (A) - частка абстрактних класів та інтерфейсів у модулі. Принцип стабільних абстракцій: стабільні модулі мають бути абстрактними (щоб їх можна було розширювати без зміни), нестабільні - конкретними.
Головна послідовність: ідеально A + I ≈ 1. Відхилення показує проблемні зони:
- «зона болю» (стабільний і конкретний: I ≈ 0, A ≈ 0) - від модуля всі залежать, але його неможливо розширити без зміни. Типово для «утилітних» класів і перевантажених базових моделей;
- «зона марності» (нестабільний і абстрактний) - інтерфейси, які ніхто не використовує.
Як застосувати в PHP-проєкті:
- інструменти: PhpMetrics, Deptrac (ще й перевіряє дозволені залежності між шарами), pdepend;
- дивитися на тенденції між релізами, а не на абсолютні значення;
- для модульного моноліту - головні метрики на рівні модулів (
Billing,Catalog), а не окремих класів.
Застереження: метрики - сигнал для розмови, а не мета. Оптимізувати «I» заради числа - створити штучні інтерфейси. Корисне питання: «чому від цього модуля залежить усе?» - і чи має так бути.
У великій команді один архітектор, через якого проходять усі рішення, стає вузьким місцем: рішення чекають тижнями, а ухвалюються далеко від людей, що знають деталі. У маленькій - навпаки, рішення ухвалюються «мимохідь», і через рік ніхто не пам'ятає чому.
Поділ рішень за вартістю зміни:
- оборотні («двосторонні двері») - легко змінити: структура класів усередині модуля, вибір бібліотеки для внутрішньої задачі. Ухвалюються швидко, тими, хто робить роботу;
- необоротні («односторонні двері») - дорого змінити: формат публічного API, основна база, розбиття на сервіси, модель даних, автентифікація. Тут варто зупинитися, зібрати думки й записати рішення.
Процес порад (advice process) - підхід, який описує Андрю Гармел-Ло в статті на сайті Мартіна Фаулера:
- будь-хто може ухвалити архітектурне рішення, але спершу мусить порадитися з тими, кого воно зачепить, і з тими, хто має досвід у цій галузі;
- порада не є вето - відповідальність за рішення лишається на тому, хто його ухвалює;
- рішення фіксується в ADR - разом із контекстом, альтернативами й отриманими порадами;
- архітектурний форум (регулярна зустріч) - місце, де обговорюють поточні рішення й діляться ними, а не орган затвердження.
Інструменти, що підтримують такий підхід:
- ADR - пам'ять про рішення і їхні причини;
- принципи архітектури - кілька загальноприйнятих правил («межі модулів не перетинаємо напряму», «нові інтеграції - через черги»), щоб більшість рішень можна було ухвалити самостійно, звіряючись із ними;
- технологічний радар - перелік технологій зі статусами «використовуємо», «пробуємо», «не використовуємо»;
- функції придатності - автоматична перевірка того, що вже вирішено.
Чого уникати:
- рішення без обговорення з тими, хто житиме з наслідками (експлуатація, безпека, інші команди);
- комітет, що затверджує все - повільно й знімає відповідальність з авторів;
- рішення без запису - через рік їх «виправляють» люди, які не знають контексту.
На співбесіді важливо показати, що архітектура - не лише технічні знання, а й процес: як збирати інформацію, зважувати компроміси, документувати й поширювати рішення.
Докладніше в документації: Martin Fowler: Scaling the Practice of Architecture, Conversationally
Питання з реальних технічних співбесід - 100 питань у 7 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії