QA Interview Hub · База знань · Тести
System Design
Основи бекенду: LB, кеш, БД, черги
Клієнт-сервер і load balancer
Базова схема, яку треба вміти намалювати і пояснити:
Client -> DNS -> Load Balancer -> [App 1, App 2, App 3] -> Cache
| |
+------> DB <-----+
+------> Queue -> Workers
Load balancer розподіляє запити між кількома екземплярами сервісу. Алгоритми: round robin, least connections, хеш від IP або ключа. LB також робить health checks і перестає слати трафік на нездоровий екземпляр.
Stateless сервіс не зберігає стан клієнта в пам'яті. Сесія лежить у токені або в спільному сховищі. Тоді будь-який екземпляр може обробити будь-який запит, і сервіс легко масштабується.
Sticky sessions прив'язують клієнта до одного екземпляра. Це простіше для стейтфул-логіки, але при падінні екземпляра сесія губиться, і навантаження розподіляється гірше.
Що тут тестувати QA:
- Сценарій, коли один екземпляр падає посеред потоку: чи продовжує користувач роботу.
- Послідовні запити потрапляють на різні екземпляри: чи немає стану в пам'яті, який "губиться". Типовий баг: перший запит створив чернетку платежу в пам'яті екземпляра 1, другий потрапив на екземпляр 2 і отримав 404.
- Rolling deploy: під час оновлення частина екземплярів має нову версію, частина стару. Чи сумісні вони між собою.
- Таймаути LB: довгий запит (звіт, експорт) може обірватися на LB раніше, ніж сервіс відповість.
На співбесіді: Load balancer розподіляє трафік і прибирає нездорові екземпляри, а сервіси за ним мають бути stateless. Я окремо тестую падіння екземпляра посеред сценарію і змішані версії під час rolling deploy, бо там часто ховаються баги.
Кеш: де він стоїть і що в ньому ламається
Кеш зберігає часто читані дані у швидкому сховищі (наприклад, Redis), щоб не ходити в БД щоразу.
Основні патерни:
- Cache-aside: сервіс шукає в кеші, якщо немає - читає з БД і кладе в кеш. Найпоширеніший.
- Write-through: запис іде в кеш і в БД одночасно.
- TTL: запис у кеші живе обмежений час і потім зникає.
Найскладніше - інвалідація. Коли дані змінилися в БД, кеш може ще віддавати старе значення. Для каталогу товарів 30 секунд застарілої ціни часто допустимо. Для балансу рахунку - ні.
Що може зламатися і що тестувати:
- Застарілі дані. Після переказу баланс у UI старий, бо читається з кешу. Тест: змінити дані і перевірити, що читання повертає нове значення в межах обіцяного часу.
- Неправильний ключ кешу. Ключ не враховує користувача або валюту - і один користувач бачить дані іншого. Це вже інцидент безпеки.
- Cache stampede. Популярний ключ протух, і тисячі запитів одночасно йдуть у БД. Перевіряється навантажувальним тестом.
- Кеш недоступний. Сервіс має працювати повільніше, але працювати, а не падати.
Для тестів кеш створює ще одну проблему: тест читає дані, які інший тест або попередній прогін поклав у кеш. Тому в тестових середовищах корисно мати спосіб скинути кеш або короткі TTL.
На співбесіді: Кеш пришвидшує читання, але дає ризик застарілих даних, тому я спершу питаю, яка затримка допустима для кожного типу даних. Окремо перевіряю ключі кешу, бо помилка в ключі може показати користувачу чужі дані, і поведінку сервісу при недоступному кеші.
База даних: індекси, реплікація, шардинг
Індекс - структура, яка дозволяє швидко знайти рядки без повного перегляду таблиці. Індекс прискорює читання, але сповільнює запис і займає місце. Для QA це важливо в двох місцях: повільні запити на великих обсягах даних (на staging з 1000 рядків все швидко, у проді з 50 млн - ні) і унікальні індекси як захист від дублів.
CREATE UNIQUE INDEX ux_transfers_idem
ON transfers (client_id, idempotency_key);
Такий індекс гарантує на рівні БД, що один і той самий запит переказу не буде записаний двічі.
Реплікація - копії БД. Primary приймає записи, репліки віддають читання. Реплікація зазвичай асинхронна, тому є replication lag: щойно записані дані можуть ще не бути на репліці. Типовий баг: користувач створив переказ, сторінка історії читає з репліки, і переказу там ще немає. Тестувати: читання одразу після запису, поведінку під штучною затримкою реплікації.
Шардинг - розподіл даних між кількома БД за ключем, наприклад за user_id. Кожен шард зберігає частину даних. Складності: запити через кілька шардів, транзакції між шардами (переказ між користувачами на різних шардах), перебалансування. Для тестування важливо мати дані на різних шардах, а не тільки на одному.
Транзакції і ізоляція. Переказ - це списання і зарахування, які мають відбутися разом. Конкурентні перекази з одного рахунку можуть дати від'ємний баланс, якщо немає блокування або правильного рівня ізоляції. Це перевіряється паралельними запитами в тесті.
На співбесіді: Індекси прискорюють читання і можуть гарантувати унікальність, реплікація дає масштаб читання, але створює replication lag, шардинг розподіляє дані за ключем і ускладнює операції між шардами. Як QA я тестую читання після запису, перекази між шардами і конкурентні списання з одного рахунку.
Черги повідомлень і гарантії доставки
Черга (Kafka, RabbitMQ, SQS) розв'язує сервіси: producer кладе повідомлення, consumer обробляє його пізніше. Це дає стійкість до піків і незалежність сервісів, але робить систему асинхронною.
Гарантії доставки:
- At-most-once: повідомлення може загубитися, але не продублюється.
- At-least-once: повідомлення не загубиться, але може прийти двічі. Найпоширеніший варіант.
- Exactly-once: важко досягти в усій системі. На практиці роблять at-least-once плюс ідемпотентний consumer.
З at-least-once випливає головне правило: consumer має бути ідемпотентним. Якщо повідомлення "зарахувати 30 на рахунок" прийде двічі, гроші мають зарахуватися один раз. Зазвичай consumer зберігає id оброблених повідомлень і пропускає дублікати.
Інші поняття:
- DLQ (dead letter queue) - куди йдуть повідомлення, які не вдалося обробити після N спроб. Їх не можна просто ігнорувати: у фінтеху це завислі гроші.
- Порядок. У Kafka порядок гарантується тільки в межах партиції. Якщо події одного рахунку йдуть у різні партиції, "списання" може обробитися раніше за "поповнення".
- Consumer lag - скільки повідомлень чекає обробки. Метрика для моніторингу і для тестів під навантаженням.
Що тестувати: дубль повідомлення, повідомлення не в тому порядку, падіння consumer посеред обробки, отруйне повідомлення, яке має піти в DLQ, а не блокувати чергу.
На співбесіді: У більшості систем черга дає at-least-once, тому consumer має бути ідемпотентним. Я тестую дублікати, порушення порядку, падіння consumer посеред обробки і перевіряю, що невалідні повідомлення йдуть у DLQ, а не зависають.