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

Senior: питання на співбесіді з теми «Масштабування й продуктивність»

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

4 питання

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

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

  • за діапазоном (користувачі 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: каскадні відмови