Senior: питання на співбесіді з теми «Масштабування й продуктивність»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Шардинг - розподіл даних однієї логічної бази між кількома фізичними серверами за ключем шардингу. Кожен сервер (шард) зберігає частину рядків і обслуговує частину навантаження - зокрема записів, які реплікація не масштабує.
Стратегії вибору шарду:
- за діапазоном (користувачі 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: каскадні відмови