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

Питання на співбесіді з Архітектура

Питання з реальних співбесід з відповідями: 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 - інструмент мислення, а не класифікатор баз даних.

Докладніше в документації: 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

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

Стратегії вибору шарду:

  • за діапазоном (користувачі 1-1 млн на шарді A) - просто, але нерівномірно: нові активні користувачі всі на останньому шарді;
  • за хешем ключа - рівномірно, але діапазонні запити йдуть на всі шарди; додавання шарду переміщує багато даних (зменшує це консистентне хешування);
  • за довідником - таблиця відповідності «ключ → шард»: гнучко, але довідник стає ще одним критичним компонентом;
  • за орендарем (tenant) - кожен клієнт SaaS на своєму шарді: природна межа, дані клієнта разом.

Чому шардинг - останній засіб:

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

Що спробувати раніше:

  1. індекси, оптимізація запитів, усунення N+1;
  2. вертикальне масштабування бази (сучасні сервери - сотні ядер і терабайти пам'яті);
  3. кеш і репліки для читання;
  4. секціонування (partitioning) великих таблиць усередині однієї бази;
  5. винесення окремих доменів у власні бази (функціональне розділення) - лог подій, аналітика, пошук;
  6. архівація старих даних.

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

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

Класична задача системного дизайну. Важливо не одне «правильне» рішення, а структура міркувань: вимоги → оцінки → 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 на фішинг і шкідливі сайти, ліміти створення, заборона внутрішніх адрес.

Чого чекає інтерв'юер: уточнення вимог, обґрунтування вибору генерації кодів, розуміння, що основне навантаження - читання, і що кеш вирішує його дешевше за шардинг.

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

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

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

  1. сервіс рекомендацій сповільнився (відповідає за 30 секунд замість 50 мс);
  2. процеси PHP, що чекають на нього, не звільняються;
  3. пул процесів вичерпано - уся сторінка товару, а за нею й увесь сайт перестає відповідати;
  4. користувачі оновлюють сторінку - навантаження зростає, повторні спроби добивають систему.

Причина - не відмова рекомендацій, а те, що від них синхронно залежала критична частина.

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

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

Механізми:

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, і «сервіси», що лише проксюють виклики. Складність є, а користі немає.

Докладніше в документації: Laravel: сервіс-контейнер

Еволюційна архітектура (Ніл Форд, Ребекка Парсонс, Патрік Куа) - підхід, за яким архітектуру не проєктують раз і назавжди, а постійно змінюють разом із системою. Ключове питання - як змінювати, не втрачаючи важливих властивостей (продуктивності, безпеки, модульності).

Функція придатності (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» заради числа - створити штучні інтерфейси. Корисне питання: «чому від цього модуля залежить усе?» - і чи має так бути.

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

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

Поділ рішень за вартістю зміни:

  • оборотні («двосторонні двері») - легко змінити: структура класів усередині модуля, вибір бібліотеки для внутрішньої задачі. Ухвалюються швидко, тими, хто робить роботу;
  • необоротні («односторонні двері») - дорого змінити: формат публічного API, основна база, розбиття на сервіси, модель даних, автентифікація. Тут варто зупинитися, зібрати думки й записати рішення.

Процес порад (advice process) - підхід, який описує Андрю Гармел-Ло в статті на сайті Мартіна Фаулера:

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

Інструменти, що підтримують такий підхід:

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

Чого уникати:

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

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

Докладніше в документації: Martin Fowler: Scaling the Practice of Architecture, Conversationally

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

Рівні
Junior 35 Middle 35 Senior 30

Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії