57 питань

Питання на співбесіді з System design

Питання рівня Middle

  1. 1. Тобі дали 45 хвилин і задачу «спроєктуй сервіс сповіщень». Як ти розподілиш час і з чого почнеш? Які помилки найчастіше валять кандидатів на system design?

    Відповідь

    Перші 5 хвилин вимоги. Функціональні: які канали (push, email, in-app), чи потрібна історія, чи є розклад. Нефункціональні: скільки користувачів, допустима затримка доставки, чи можна втратити сповіщення, чи важливий порядок. І явно кажу, що поза межами: наприклад, шаблони листів не проєктуємо. Далі 5 хвилин на оцінку: DAU, сповіщень на користувача на добу, середній і піковий QPS, обсяг зберігання на рік. Цифри вирішують архітектуру: 50 сповіщень на секунду живуть в одному Postgres з BullMQ, 50 тисяч уже потребують партиціонованого брокера. Потім 10 хвилин API і висока схема: хто генерує подію, черга, воркери на кожен канал, сховище статусів, провайдери APNs і FCM.

    Додаткове питання: Інтервʼюер каже: «трафік виріс у 10 разів». Що в твоїй схемі ламається першим і як ти це визначив?

  2. 2. Оціни на серветці: 10 млн DAU, кожен робить 50 запитів на добу, 10 % запитів це записи по 1 КБ, середня відповідь 5 КБ. Порахуй середній і піковий QPS, обсяг даних на рік і вихідний трафік. Скільки Node процесів і серверів тобі потрібно?

    Відповідь

    Запити: 10 млн × 50 = 500 млн на добу, тобто в середньому близько 5 800 rps, а з піком утричі близько 17 тисяч. Записи 10 %: 580 у середньому і 1 700 у піку. Обсяг: 50 млн записів по 1 КБ це 50 ГБ на добу, близько 18 ТБ на рік, а з індексами і двома репліками понад 50 ТБ. Одна база без партиціонування й архівації старих даних не проживе року, а бекапи такого обсягу треба планувати окремо. Вихідний трафік: 17 тисяч rps × 5 КБ це близько 85 МБ/с, тобто сотні Мбіт/с у піку. Кількість процесів беру з виміряного rps одного Node процесу (скажімо, 1 000 rps) при завантаженості 60 %: близько 30 процесів, це 2 або 3 сервери по 16 ядер з запасом N+1.

    Додаткове питання: Як зміниться оцінка, якщо 80 % читань можна віддавати з кешу з hit rate 90 %?

  3. 3. Ендпоінт збирає профіль користувача: 40 послідовних GET у Redis і 15 послідовних запитів у Postgres. Локально p50 8 мс. У проді Redis і Postgres переїхали в сусідній датацентр, і p50 став 400 мс, хоча CPU низький. Поясни через цифри, які має знати кожен інженер, і виправ.

    Відповідь

    Класична таблиця порядків величин: звернення до памʼяті близько 100 нс, round trip у межах одного датацентру близько 0,5 мс, між датацентрами в одному регіоні кілька мілісекунд, між континентами близько 150 мс. Читання 1 МБ з памʼяті це сотні мікросекунд, з диска десятки мілісекунд. Тут 55 послідовних звернень. Локально RTT це десятки мікросекунд, тому 8 мс. Між датацентрами RTT, скажімо, 7 мс: 55 × 7 ≈ 385 мс, плюс сама робота. CPU низький, бо процес майже весь час чекає на мережу. Вертикальне масштабування тут нічого не дасть.

    Додаткове питання: Що зміниться, якщо між сервісами додати ще й TLS без повторного використання зʼєднань?

  4. 4. WebSocket чат за Nginx. Скарги: зʼєднання рве рівно щохвилини, коли в чаті тихо. А після кожного деплою один із трьох Node процесів тримає 80 % усіх зʼєднань. Поясни обидва ефекти і полагодь конфіг.

    Відповідь

    Перший ефект. proxy_read_timeout за замовчуванням 60 секунд: якщо від бекенду нічого не прийшло за цей час, Nginx закриває зʼєднання. Тихий чат якраз так виглядає. Лікую з двох боків: сервер шле ping кожні 25 або 30 секунд (у ws через ws.ping, Socket.IO робить це сам через pingInterval), а proxy_read_timeout і proxy_send_timeout ставлю, наприклад, 1h. Ping потрібен і сам по собі: він знаходить мертві зʼєднання, коли клієнт зник без FIN. У конфігу ще бракує proxy_http_version 1.1, без нього Nginx іде до бекенду по HTTP/1.0 і upgrade не працює. При rolling деплої процеси перезапускаються по черзі. Клієнти першого процесу перепідключаються до двох живих, потім клієнти другого до живих і так далі.

    Додаткове питання: Що зміниться, якщо перед Nginx стоїть Cloudflare або інший CDN? Які в нього обмеження на WebSocket?

  5. 5. Дашборд показує середній час відповіді 45 мс, а користувачі скаржаться на повільність. Чому середнє тут бреше? Що означають p50, p95, p99? Чому сторінка, яка робить 50 викликів до бекендів, повільна частіше, ніж каже p99 кожного бекенда?

    Відповідь

    Латентність не має нормального розподілу. Більшість запитів швидкі, але є довгий хвіст: GC пауза, холодний кеш, очікування зʼєднання з пулу, блокування в базі. Середнє 45 мс може означати, що 98 % запитів по 20 мс, а 2 % по 1,3 секунди. Користувач, який клікає 50 разів за сесію, майже напевно ці 1,3 секунди побачить. pN означає, що N % запитів швидші за це значення. p50 це медіана, типовий досвід. p95 і p99 описують хвіст: p99 це кожен сотий запит. Для активного користувача кожен сотий запит трапляється кілька разів за сесію, тому SLO пишуть на p95 або p99, а не на середнє. Якщо сторінка робить 50 незалежних викликів і чекає на всі, то вона повільна, коли хоча б один із них потрапив у хвіст.

    Додаткове питання: Як зменшити хвіст латентності, якщо причина в одному повільному бекенді з 50?

  6. 6. API тримає 3000 rps, середня латентність 40 мс, кожен запит робить два запити в Postgres по 3 мс. Скільки запитів одночасно в роботі? Який сумарний пул зʼєднань потрібен? Що станеться, коли запити в базу сповільняться до 100 мс кожен?

    Відповідь

    Закон Літтла: середня кількість запитів у системі дорівнює інтенсивності надходження, помноженій на середній час перебування, L = λ × W. Тут 3000 × 0,04 = 120 запитів одночасно в роботі. Закон працює для будь-якої стабільної системи, незалежно від розподілу. Той самий закон для бази: кожен запит тримає зʼєднання 2 × 3 = 6 мс, отже в середньому зайнято 3000 × 0,006 = 18 зʼєднань. Пул беру з запасом на нерівномірність, наприклад 40 сумарно. Якщо процесів 12, це по 3 або 4 на процес, через PgBouncer у transaction mode. Більше зʼєднань не прискорить: у Postgres кожне це процес, і понад кілька десятків активних на сервер з 16 ядрами вони конкурують між собою.

    Додаткове питання: Як за допомогою того самого закону оцінити, скільки VU потрібно k6 для 3000 rps?

  7. 7. Розпродаж: трафік утричі вищий за місткість API. Rate limiter за ключем клієнта стоїть, і жоден клієнт не перевищує своєї квоти, але латентність у всіх 20 секунд, а успішних відповідей за секунду менше, ніж у звичайний день. Чому так і що ти додаси?

    Відповідь

    Rate limiting захищає від одного клієнта, що перевищує квоту, а тут перевантаження сумарне, і limiter його не бачить. Сервіс приймає все, черги ростуть, кожен запит чекає довше за таймаут клієнта, тож сервер витрачає ресурс на відповіді, які вже ніхто не чекає, а клієнти ще й повторюють запити: корисна пропускна здатність падає. Потрібен load shedding: обмежити кількість одночасних запитів або довжину черги і відкидати зайве одразу дешевою відповіддю 503 з Retry-After, краще за пріоритетами (оплата важливіша за рекомендації) і з урахуванням дедлайну запиту. 429 означає «ти, конкретний клієнт, перевищив квоту», 503 означає «сервіс перевантажений», і клієнти мають реагувати на обидва backoff, а не миттєвим повтором.

    Додаткове питання: За яким сигналом сервіс має вирішувати, що час відкидати запити: CPU, латентність чи щось інше?

  8. 8. Кейс: сервіс коротких посилань. 100 млн нових посилань на місяць, читань у 100 разів більше, ніж записів, посилання зберігаються 5 років. Які вимоги уточниш першими? Порахуй QPS запису і редиректів, середній і піковий, обсяг сховища і скільки символів base62 треба в коді.

    Відповідь

    Функціональні: створити коротке посилання з довгого, редирект, опційно власний alias і термін дії, статистика переходів, видалення власником. Нефункціональні: редирект швидкий (p99 до 20 мс у ДЦ) і майже завжди доступний, коди не мають бути передбачуваними, якщо посилання приватні. Редирект важливіший за аналітику: якщо статистика відстане на хвилину, ніхто не помітить, а якщо редирект ляже, ламаються всі вже розіслані листи і QR коди. Запис: 100 000 000 / 2 592 000 с ≈ 40 за секунду, пік ×5 ≈ 200. Читання в 100 разів більше: ≈ 4000 rps у середньому, 15 000 або 20 000 у піку. Для Node це кілька процесів, а не кластер, але 20 000 точкових SELECT за секунду без кешу база вже відчує. За 5 років 100 млн × 60 = 6 млрд записів.

    Додаткове питання: Як згенерувати унікальний 7-символьний код без колізій, коли пишуть 10 інстансів API одночасно?

  9. 9. Кейс: домашня стрічка соцмережі. 20 млн DAU, кожен відкриває стрічку 10 разів на добу і публікує 1 пост на 5 днів, у середньому 200 підписок. Уточни вимоги і порахуй QPS читання і запису, скільки вставок у стрічки дає fan-out on write і скільки памʼяті займуть стрічки в Redis.

    Відповідь

    Вимоги: хронологічна чи ранжована стрічка, допустима затримка появи поста (кілька секунд нормально), видалення, нескінченний скрол, читання важливіше за миттєвість. Читання: 200 млн на добу це близько 2300 rps, у піку 8000 або 9000. Пости: 4 млн на добу, близько 46 за секунду. Fan-out on write розкладає кожен пост у 200 стрічок: 800 млн вставок на добу, близько 9300 за секунду в середньому і 30 000 у піку, а акаунт з 10 млн підписників одним постом дає 10 млн вставок. У стрічці лише post_id і score, обрізана до 500 елементів, це приблизно 30 КБ на користувача. Для всіх 20 млн це 600 ГБ, тому стрічки тримають лише для активних користувачів, близько 250 ГБ на кілька шардів Redis. Головна проблема тут не читання, а множення запису і хвіст великих акаунтів.

    Додаткове питання: Як зміниться дизайн, якщо стрічка ранжується моделлю, а не за часом?

  10. 10. Після створення оголошення користувача перекидає на список Мої оголошення, а нового оголошення там немає: проєкція в Elasticsearch відстає на 1-3 секунди. Користувачі тиснуть ще раз і створюють дублікати. Які є варіанти виправлення, і що з них ти обереш?

    Відповідь

    Проблема в тому, що UI вдає синхронну систему поверх асинхронної. Варіантів кілька, зазвичай їх комбінують. Команда повертає створений обʼєкт, клієнт додає його в список локально. Найдешевше і в більшості випадків досить. Команда повертає позицію: id запису в outbox або версію. Проєктор зберігає останню застосовану позицію. Запит списку передає min_position, і API чекає до 2 секунд, поки проєкція її досягне, або віддає відповідь з прапором stale.

    Додаткове питання: Проєктор упав на одній поганій події і стоїть годину. Як зробити, щоб він не блокував усі інші?

  11. 11. Тімлід хоче перевести весь новий сервіс замовлень на event sourcing, бо буде аудит і можна відмотати час. Що ти скажеш? Які реальні проблеми команда отримає в перші пів року?

    Відповідь

    Спершу питаю, яку задачу розвʼязуємо. Аудит і історію змін можна отримати значно дешевше: таблиця audit_log, тригер на зміну, або доменні події в outbox поруч зі звичайним станом. Відмотати час потрібно рідко, а коли потрібно, вистачає історії змін одного замовлення. Що команда отримає за пів року. Кожен список і пошук потребує проєкції, бо по журналу не зробиш SELECT * FROM orders WHERE status = 'paid'. Значить проєктори, їх затримка, read-your-writes в UI, моніторинг lag. Схема подій змінюється з кожною фічею, тому upcaster-и і правила версіонування з першого місяця.

    Додаткове питання: Яку структуру audit_log ти зробиш, щоб з неї можна було відновити стан замовлення на дату?

  12. 12. Сервіс api на Node викликає recommendations по gRPC через звичайний Kubernetes Service. Recommendations масштабували з 2 до 8 pod, але навантаження лишилось на двох старих, нові майже простоюють. Чому? Як виправити?

    Відповідь

    kube-proxy балансує зʼєднання, а не запити: pod обирається один раз при встановленні TCP-зʼєднання. gRPC працює поверх HTTP/2 і мультиплексує всі виклики в одне довге зʼєднання. Клієнт api підʼєднався, коли було 2 pod, і відтоді всі його виклики йдуть туди. Нові 6 pod порожні, поки зʼєднання не обірветься. З HTTP/1.1 і keep-alive буває те саме, але там зʼєднань більше, тому помітно менше. Варіант перший: client-side балансування. Headless Service з clusterIP: None, DNS повертає IP усіх pod. Клієнт тримає зʼєднання з кожним pod і розподіляє виклики.

    Додаткове питання: У Nest gRPC клієнт створюється через ClientsModule. Куди передати service config з round_robin?

  13. 13. Між сервісами вже є e2e тести на staging, які ганяють усі сервіси разом 40 хвилин і часто флейкають. Що дає consumer-driven contract testing і як працює Pact? Чого він не перевіряє?

    Відповідь

    E2E на спільному staging перевіряють усе одразу, тому вони повільні, флейкі і погано локалізують помилку: впав тест оформлення, а зламати його міг будь-хто з 8 сервісів. Contract testing перевіряє окремо кожну пару consumer і provider. Consumer, наприклад orders, у своєму тесті описує взаємодію: стан user 42 exists, запит GET /users/42, очікувана відповідь з полями id і email, де значення задано matcher like, тобто перевіряється тип, а не конкретне значення. Pact піднімає mock server, справжній HTTP клієнт orders ходить у нього, і якщо клієнт робить саме такі запити, генерується pact файл у JSON. Він публікується в Pact Broker з версією, зазвичай git sha. Provider users у своєму CI піднімає себе, бере з брокера контракти, де він provider, готує дані через provider states, наприклад handler для user 42 exists вставляє рядок у тестову базу, і програє запити, порівнюючи відповіді з очікуваннями.

    Додаткове питання: Як тестувати контракт подій у Kafka, де немає запиту і відповіді?

  14. 14. Під час міграції команда зробила так: моноліт при зміні товару пише в свою базу і одразу робить HTTP POST у новий сервіс каталогу. Через місяць у новому сервісі 0,3% товарів з іншими цінами. Чому так стається, хоча помилок майже немає? Як виправити і розбіжність, і сам підхід?

    Відповідь

    Це dual write: два записи в дві системи без спільної транзакції, тому будь-яке падіння між ними дає розбіжність. Моноліт закомітив ціну, а POST упав за таймаутом: у каталозі стара ціна. Або POST пройшов, а транзакція моноліту відкотилась: у каталозі ціна, якої не було. Є й гонки: два адміни змінили ціну майже одночасно, у базі останнім закомітився B, а запити дійшли в порядку B, A. Таймаут теж не означає відмову, і повтор без версії може перезаписати новіше старим. Виправляти треба підхід: синхронізація з журналу, тобто CDC по WAL або outbox у тій самій транзакції, з ключем product_id і умовним записом за версією. Наявні розбіжності лікує reconciliation job, що порівнює бази по product_id і переписує розбіжні з джерела правди.

    Додаткове питання: Як зробити початкове заповнення нової бази, щоб не загубити зміни, які стались під час копіювання?

  15. 15. Consumer надсилає вітальний лист, а потім комітить offset. Після падіння пода частина користувачів отримала два листи. Інша команда комітить offset до обробки і час від часу губить події. Яку гарантію доставки дає кожен варіант і що ти обереш для листів, а що для нарахування бонусів?

    Відповідь

    Коміт після обробки дає at-least-once: якщо процес упав між відправкою листа і комітом, після рестарту повідомлення прийде знову, звідси дублікати. Коміт до обробки дає at-most-once: offset уже зсунутий, а робота не зроблена, тож подія губиться. Гарантію визначає не брокер, а порядок «зробив роботу» і «підтвердив» плюс те, чи є побічний ефект ідемпотентним. Для листів достатньо at-least-once з дедуплікацією за id події, бо дублікат неприємний, але не критичний. Для бонусів потрібне at-least-once плюс ідемпотентний запис: унікальний ключ за id події в тій самій транзакції, що й нарахування, тоді повтор нічого не змінює і виходить ефективно exactly-once. At-most-once доречний там, де втрата дешевша за дубль: метрики, телеметрія, статус «онлайн».

    Додаткове питання: Як зробити дедуплікацію, якщо побічний ефект це виклик зовнішнього API без ключа ідемпотентності?

  16. 16. Redis перезапустився на 20 секунд. 1000 воркерів повторюють підключення щосекунди, і коли Redis піднімається, він знову падає під хвилею зʼєднань, і так кілька разів. Що змінюєш у логіці повторів? Запиши формулу затримки.

    Відповідь

    Фіксований інтервал синхронізує клієнтів: усі втратили зʼєднання одночасно і повторюють одночасно, тож сервер отримує хвилі по 1000 зʼєднань саме тоді, коли він найслабший. Експоненційний backoff розріджує спроби в часі, а jitter розсинхронізує клієнтів між собою, без нього хвилі лишаються, лише рідшими. Формула full jitter: sleep = random(0, min(cap, base * 2 ** attempt)), де cap обмежує максимальну паузу, наприклад 30 секунд, щоб після довгого збою клієнт не чекав годину. Додатково обмежити кількість спроб або загальний дедлайн і повторювати лише тимчасові помилки.

    Додаткове питання: Як уникнути такої самої хвилі, коли після деплою одночасно стартують сотні подів?

  17. 17. Інтеграція з платіжним партнером повторює будь-яку помилку 5 разів з паузою 1 секунда: 400 валідації, 401, 429, таймаут і 503. Виклик це POST /charges без ключа ідемпотентності з таймаутом 30 секунд. Коли партнер почав гальмувати, зʼявились подвійні списання, а наш сервіс ліг. Що тут не так і як переписати?

    Відповідь

    Повторювати можна лише тимчасові помилки: таймаути, скинуті зʼєднання, 502, 503, 504 і 429 з урахуванням Retry-After; 400, 401, 403, 404 і 422 постійні, і повтор лише додає навантаження. Таймаут на неідемпотентному POST неоднозначний: запит міг пройти, тож повтор без ключа ідемпотентності дає подвійне списання, і потрібен Idempotency-Key, однаковий для всіх спроб. Фіксована пауза без jitter синхронізує повтори, а 5 спроб по 30 секунд тримають запит і зʼєднання до 2,5 хвилини, тож пул і памʼять вичерпуються. Потрібні короткий таймаут, загальний дедлайн, backoff з jitter і circuit breaker або retry budget, щоб під час деградації партнера не множити запити.

    Додаткове питання: Як реалізувати retry budget у Node сервісі і яку метрику моніторити?

  18. 18. Під час аудиту зʼясувалось, що в DLQ сервісу повернень лежать 12 тисяч повідомлень за квартал, про які ніхто не знав, а клієнти так і не отримали гроші. Як має бути влаштований процес навколо DLQ, щоб вона не ставала кладовищем?

    Відповідь

    DLQ потрібна, щоб повідомлення, яке не вдалось обробити після N спроб (maxReceiveCount у redrive policy SQS, dead letter exchange у RabbitMQ, список failed після attempts у BullMQ), не блокувало чергу. Але DLQ без процесу просто ховає збої. Потрібно: алерт на ненульову глибину DLQ і вік найстаршого повідомлення, а не лише на помилки в логах; власник, який розбирає її в межах SLA; разом з повідомленням зберігати причину, стек, кількість спроб, час і версію коду; розділяти постійні помилки (валідація, відсутній запис) і тимчасові, бо постійні не треба ганяти по колу. І мати перевірений redrive з обмеженою швидкістю та ідемпотентного consumer, щоб повторна обробка була безпечною.

    Додаткове питання: Як consumer у коді відрізняє постійну помилку від тимчасової і що робить з кожною?

  19. 19. Consumer RabbitMQ постійно перезапускається, CPU 100%, черга не рухається, хоча більшість повідомлень нормальні. В коді при будь-якій помилці робиться channel.nack(msg, false, true). Що це таке і як це лікувати? А як схожа ситуація виглядає в Kafka?

    Відповідь

    Це poison message: одне повідомлення, наприклад з поламаним JSON, падає завжди. requeue=true кладе його назад у голову черги, тому consumer миттєво отримує його знову. Документація RabbitMQ попереджає про такий цикл requeue/redelivery: він їсть CPU і мережу. Якщо падає весь процес, то зʼєднання рветься і повідомлення теж повертається. Обробник не має падати: валідація схеми до обробки, try/catch навколо. Для постійних помилок nack(msg, false, false), щоб пішло в DLX. Quorum queue має delivery-limit: з RabbitMQ 4.0 він за замовчуванням 20, після чого повідомлення видаляється або йде в DLX, тому DLX слід налаштувати. Classic queues цього захисту не мають.

    Додаткове питання: Як consumer у Kafka дізнається, що це вже N-та спроба, якщо брокер цього не рахує?

  20. 20. Події замовлення created, paid, shipped ідуть через SQS standard. Поки consumer був один, усе працювало; після масштабування до чотирьох зʼявились замовлення, де paid оброблено раніше за created, і обробник падає. Чому і як отримати порядок у межах одного замовлення без глобального порядку?

    Відповідь

    SQS standard взагалі не гарантує порядку, а кілька consumers паралельно обробляють сусідні повідомлення, тож з одним consumer порядок був збігом. Глобальний порядок не потрібен і дорогий, потрібен порядок за ключем: SQS FIFO з MessageGroupId = id замовлення (усередині групи суворий порядок, групи обробляються паралельно), Kafka з ключем повідомлення = id замовлення (одна партиція, один consumer у групі), у RabbitMQ consistent hash exchange або окрема черга на шард. Навіть так обробник варто робити стійким до порядку: зберігати версію чи послідовний номер, відкидати застарілі події і відкладати ті, для яких ще немає попереднього стану, бо повтори і DLQ порядок однаково порушують.

    Додаткове питання: Що станеться з порядком у групі, якщо одне повідомлення потрапить у DLQ?

  21. 21. Consumer SQS генерує PDF-звіти і надсилає їх поштою. Частина клієнтів отримує той самий звіт двічі або тричі. Генерація триває до 90 секунд, visibility timeout стоїть за замовчуванням. Поясни, що відбувається, і полагодь.

    Відповідь

    Отримане повідомлення не видаляється, а лише стає невидимим на visibility timeout, за замовчуванням 30 секунд. Якщо обробка триває довше і DeleteMessage не викликано вчасно, повідомлення знову зʼявляється в черзі, його бере інший consumer, і звіт генерується вдруге. Виправлення: visibility timeout із запасом над реальним часом обробки (для Lambda AWS радить ставити його щонайменше вшестеро більшим за timeout функції), а для довгих задач продовжувати його через ChangeMessageVisibility як heartbeat. І однаково зробити обробку ідемпотентною, наприклад записувати id відправленого звіту, бо SQS standard дає at-least-once і дублікати можливі навіть без цієї помилки.

    Додаткове питання: Що зміниться, якщо consumer це Lambda з batch size 10 і одне повідомлення в пачці падає?

  22. 22. Топік має 6 партицій, lag росте. Команда збільшила deployment consumer з 6 до 12 подів, але lag не зменшився, а половина подів нічого не робить. Чому, що реально допоможе, і що буде, коли той самий топік почне читати ще один сервіс?

    Відповідь

    У межах однієї consumer group кожна партиція призначається рівно одному consumer, тож паралелізм обмежений кількістю партицій: шість партицій означають максимум шість активних consumers, решта простоюють і чекають rebalance. Допомогти може: збільшити кількість партицій (але це змінює відображення ключів на партиції і порядок для нових повідомлень), прискорити обробку одного повідомлення (батчі, менше синхронних викликів) або паралелити обробку всередині consumer за ключем, зберігаючи порядок у межах ключа і комітячи offset лише за обробленими повідомленнями. Інший сервіс читає топік своєю consumer group з власними offset і отримує всі повідомлення незалежно від першої групи, бо Kafka не видаляє прочитане.

    Додаткове питання: Як паралелити обробку всередині одного consumer і не закомітити offset повідомлення, яке ще не оброблено?

  23. 23. Після перезапуску consumer частина подій не оброблена, хоча lag показував 0. В інший раз події оброблені двічі. Налаштування стандартні, у коді окремий пул обробляє повідомлення асинхронно. Поясни обидва випадки. Як це виправити?

    Відповідь

    enable.auto.commit за замовчуванням true: offset періодично комітиться у фоні. Він не знає, чи завершив твій код обробку. Втрата: pool.submit повертається одразу, обробник вважається завершеним, offset закомічений. Процес упав із задачами в пулі, а після перезапуску читання продовжиться за ними. Lag показував 0, бо це про закомічений offset, а не про закінчену роботу. Дублі: обробка виконалась, але commit ще не відбувся. Процес упав, і після перезапуску consumer прочитав ті самі записи знову.

    Додаткове питання: Якщо параметр auto.offset.reset=latest, що станеться з новою consumer group при першому запуску?

  24. 24. Consumer обробляє пачку записів 10 хвилин. Постійно відбуваються rebalance, та сама пачка обробляється знову і знову, lag росте. Heartbeat при цьому в порядку. Що сталось і як виправити?

    Відповідь

    Є два окремих механізми. Heartbeat іде у фоновому потоці і доводить, що процес живий (session.timeout.ms, за замовчуванням 45 с). max.poll.interval.ms, за замовчуванням 300000 мс, це максимальна пауза між викликами poll(). Вона доводить, що consumer рухається. Обробка 10 хвилин перевищує 5 хвилин, тому consumer вважається завислим і виходить з групи. Партиції отримує інший, і він починає з останнього закоміченого offset, тобто з тієї самої пачки. Перший закінчує і намагається закомітити, але партиції вже не його. Це нескінченний цикл, а lag росте.

    Додаткове питання: Що змінює новий протокол group.protocol=consumer порівняно з classic?

  25. 25. Подія order.created у RabbitMQ має доходити і в білінг, і в сервіс листів. Розробник публікує в default exchange з routing key billing, і листи ніхто не отримує. А коли чергу білінгу видалили на час обслуговування, повідомлення тихо зникали. Поясни обидві проблеми і полагодь топологію.

    Відповідь

    Default exchange це direct exchange, де routing key дорівнює імені черги, тож повідомлення потрапляє рівно в чергу billing: продюсер фактично знає споживача, і другу чергу так не додати. Правильно: продюсер публікує в іменований exchange, наприклад topic orders з ключем order.created, а кожен сервіс має свою чергу з binding на потрібний ключ, тоді новий споживач додається без змін у продюсері (fanout, якщо фільтрувати не треба). Друга проблема: якщо жодна черга не підходить під маршрут, повідомлення без прапорця mandatory просто відкидається. Захист: durable черги, які не видаляють на обслуговування, alternate exchange для немаршрутизованих повідомлень або mandatory з обробкою return, і publisher confirms.

    Додаткове питання: Сервіс листів лежав тиждень. Що треба мати в топології, щоб він після запуску отримав пропущені події?

  26. 26. Consumer RabbitMQ з автоматичним ack. При деплої губляться повідомлення. Перейшли на ручний ack, тепер один consumer забирає всі 200 тисяч повідомлень і падає з OOM, а інші простоюють. Ще один інцидент: повідомлення зникли при рестарті брокера. Поясни всі три проблеми.

    Відповідь

    Брокер вважає повідомлення обробленим одразу після відправки. Усе, що consumer тримає в памʼяті, губиться при падінні. Рішення: ручний ack після обробки, це at-least-once. Unacked повідомлення при розриві зʼєднання повертається в чергу. Значення 0 означає без ліміту, тому брокер проштовхує все, що є, у сокет першого consumer. Він буферизує все в памʼяті. Рішення: basic.qos, prefetch 10 до 100 залежно від тривалості задачі.

    Додаткове питання: Як підібрати значення prefetch, якщо задачі тривають то 10 мс, то 10 с?

  27. 27. Потрібні повтори із затримкою 1, 5 і 30 хвилин у RabbitMQ, а після цього parking lot. Брокер не має простої опції "повтори через 5 хвилин" у класичних чергах. Як це побудувати? Які є пастки?

    Відповідь

    Схема на TTL і DLX. Основна черга orders має dead-letter-exchange на retry exchange. Є три черги без consumers: orders.retry.1m, 5m, 30m, у кожної свій x-message-ttl і dead-letter-exchange назад на основний exchange. Коли обробка не вдалась, consumer робить nack без requeue або публікує в потрібну retry чергу. Повідомлення лежить протягом TTL, а потім повертається в основну чергу. Лічильник спроб: заголовок x-death заповнює брокер при dead-lettering, або власний заголовок x-attempt. Після третьої спроби повідомлення йде в orders.parking, там без TTL, з алертом і ручним розбором.

    Додаткове питання: Чим опублікувати в retry-чергу надійніше: nack чи publish з confirm, потім ack?

  28. 28. Диск брокерів Kafka заповнений на 90 %. Ти зменшив retention.ms топіку на 2 TB з 7 днів до години, але використання диска годинами майже не змінюється. Чому і що робити?

    Відповідь

    Retention видаляє дані цілими сегментами, а не окремими повідомленнями, і лише закриті сегменти: активний сегмент, у який іде запис, не видаляється, доки не закриється за segment.bytes (за замовчуванням 1 GB) або segment.ms (за замовчуванням 7 днів). Сегмент видаляється, коли найновіший запис у ньому старший за retention, а перевірка йде періодично (log.retention.check.interval.ms, 5 хвилин). На партиціях з повільним записом активний сегмент може містити майже тиждень даних. Що робити: тимчасово зменшити segment.ms або segment.bytes, щоб сегменти закривались, дочекатись перевірки, а на майбутнє обмежувати топік ще й обсягом (retention.bytes) і мати алерт на заповнення диска.

    Додаткове питання: Як видалити дані одного користувача з compacted топіку на вимогу GDPR і коли consumers перестануть їх бачити?

  29. 29. Треба обрати брокер для двох потоків: задачі на генерацію мініатюр і події замовлень, які читають пʼять сервісів, причому новий сервіс аналітики хоче перечитати останні 30 днів. Для кожного потоку RabbitMQ чи SQS, чи Kafka, і чому?

    Відповідь

    Мініатюри це класична черга задач: кожну задачу виконує один воркер, потрібні підтвердження по одному повідомленню, повтор окремої задачі, DLQ і масштабування простим додаванням воркерів, тож RabbitMQ, SQS або BullMQ. Після ack повідомлення з черги зникає, і повторно прочитати його не можна. Події замовлень краще в Kafka: це лог, повідомлення лишаються після читання на час retention, кожен сервіс читає своєю consumer group зі своїм offset, а новий сервіс може почати з offset місячної давнини і зробити replay. Ціна логу: паралелізм обмежений партиціями, повтор одного повідомлення без блокування партиції доводиться будувати самому, а операційно Kafka складніша.

    Додаткове питання: Що робити, якщо подія замовлення в Kafka не обробляється через баг, а блокувати партицію не можна?

  30. 30. Після 20 хвилин простою consumers у черзі 500 тисяч задач. Нові приходять зі швидкістю 800 на секунду, один воркер обробляє 50 на секунду, зараз 20 воркерів. Скільки часу займе розбір черги? Скільки воркерів треба, щоб розібрати за 10 хвилин? Що ще треба перевірити, перш ніж додавати воркери?

    Відповідь

    Арифметика. 20 воркерів по 50 це 1000 на секунду. Нові приходять зі швидкістю 800, тож чергу зменшують лише 200 на секунду. 500000 / 200 = 2500 с, близько 42 хвилин. Весь цей час нові задачі теж чекають, бо стоять у кінці черги. Щоб розібрати за 10 хвилин (600 с), чистий розбір має бути 500000 / 600, близько 833 на секунду. Разом з потоком 800 це 1633 на секунду, тобто 33 воркери. Закон Літтла: L = λ W. Кількість у системі дорівнює швидкості надходження, помноженій на середній час перебування.

    Додаткове питання: Як налаштувати автоскейлінг воркерів за глибиною черги, щоб він не розгойдувався?

  31. 31. Consumer Redis Streams упав посеред обробки і перезапустився. Він читає XREADGROUP GROUP g c1 STREAMS orders >, і 300 повідомлень, які він обробляв у момент падіння, так і лишились необробленими. Чому і як їх підібрати?

    Відповідь

    XREADGROUP з ID > віддає лише нові повідомлення, які ще нікому в групі не доставлялись. Доставлені, але не підтверджені через XACK, лишаються в pending entries list (PEL) групи, закріплені за конкретним consumer, і самі не повертаються. Підібрати їх так: після рестарту той самий consumer спершу читає з ID 0 і отримує свої pending, а повідомлення мертвих consumers забирають через XPENDING і XCLAIM або XAUTOCLAIM з мінімальним часом простою (Redis 6.2+). Лічильник доставок у PEL дозволяє після N спроб переносити повідомлення в окремий stream як DLQ. Обробка має бути ідемпотентною, бо частину повідомлень буде оброблено вдруге.

    Додаткове питання: Як не дати stream рости безкінечно і не втратити непідтверджені повідомлення?

Питання рівня Senior

  1. 1. Зараз пік 2000 rps на трьох серверах по 16 ядер, CPU 45 %. Маркетинг обіцяє вчетверо більший пік через два місяці. Нові сервери постачальник везе три тижні, хмари немає. Як спланувати місткість і що зробити до дня Х?

    Відповідь

    Спершу не множу 45 % на чотири: CPU не росте лінійно, і вузьке місце майже ніколи не там, де здається. Роблю тест на одному сервері з даними, подібними до проду, з реальною сумішшю ендпоінтів, і піднімаю rps кроками, поки p99 не вийде за SLO. Нехай перегин 1400 rps, робоча межа 70 % від нього, тобто близько 1000 rps на сервер. Окремо тестую спільні залежності на цільових 8000 rps: Postgres, Redis, PgBouncer, мережа, зовнішній API з лімітом. Розрахунок: 8000 / 1000 = 8 серверів, плюс запас на N+1 і деплой, разом близько 10, тобто треба ще 7. Замовляю одразу після тесту, щоб лишився час на налаштування, або орендую dedicated сервери на місяць. План Б перевіряю заздалегідь: microcaching, вимкнення важких функцій, load shedding. За тиждень до піку роблю генеральну репетицію на повну потужність.

    Додаткове питання: Як перевірити, що план Б справді спрацює, не чекаючи реального піку?

  2. 2. Стек: мобільний клієнт з таймаутом 10 с, Nginx, Node, Postgres. Коли база гальмує, у Node тисячі запитів у роботі, памʼять росте, а в Postgres висять запити, які виконуються вже 5 хвилин. Ось налаштування. Що не так з таймаутами і як вибудувати бюджет?

    Відповідь

    Правило: кожен внутрішній таймаут коротший за зовнішній. Інакше клієнт уже пішов, а система продовжує робити його роботу і тримати ресурси. Клієнт чекає 10 с і ретраїть. Nginx чекає upstream 60 с. Node: server.requestTimeout за замовчуванням 300 секунд. Пул node-postgres з connectionTimeoutMillis 0 чекає вільне зʼєднання вічно. statement_timeout 0 у Postgres означає без ліміту. Коли база гальмує, кожен ретрай клієнта додає новий запит, старі не скасовуються, в Node тисячі промісів чекають пул, а в базі висять запити, результат яких нікому не потрібен.

    Додаткове питання: Як передати дедлайн через ланцюг з трьох сервісів, щоб останній знав, скільки часу в нього лишилось?

  3. 3. Реалізуй token bucket на Redis для 12 Node процесів на трьох серверах. Чому не GET і SET з коду? Чий годинник використовуєш? Що з Redis Cluster, з TTL ключів і що робить API, коли Redis недоступний?

    Відповідь

    GET, обчислення в Node і SET це гонка: два процеси одночасно читають 1 токен, обидва дозволяють запит, обидва пишуть 0. Під навантаженням ліміт пропускає більше, ніж задано. Тому вся логіка в одному Lua скрипті, Redis виконує його атомарно. Усередині: redis.call('TIME') дає поточний час Redis, HMGET tokens і ts, поповнення за минулий час з обмеженням ємністю, якщо токенів не менше 1, віднімаю 1, HSET нових значень, PEXPIRE на час повного поповнення (ємність / швидкість, з запасом). Повертаю allowed, залишок і через скільки мілісекунд зʼявиться токен, це йде в Retry-After і заголовки RateLimit. Годинник саме Redis, бо на трьох серверах час розходиться на десятки мілісекунд, і процес з годинником, що поспішає, нараховує зайві токени. Скрипт завантажую один раз через SCRIPT LOAD і викликаю EVALSHA, у Redis 7 зручніше FUNCTION LOAD і FCALL.

    Додаткове питання: Користувач з 50 тисяч rps робить один ключ гарячим. Як зменшити навантаження на Redis, не втративши ліміт?

  4. 4. B2B API з 300 клієнтами. Один великий клієнт запускає імпорт і забирає 70 % потужності, решта отримують таймаути, хоча кожен у межах своєї квоти. Як спроєктуєш ліміти, щоб система була справедливою? Що робиш з лімітом за IP?

    Відповідь

    Перше: квоти за tenant, а не за IP. Ключ це API key або organization_id, ліміти залежать від тарифу і записані в конфігу клієнта. IP лишаю лише для анонімних ендпоінтів: за NAT і CGNAT тисячі людей мають один IPv4, а в IPv6 одна людина має цілу /64 мережу, тому ліміт там рахую за префіксом /64. Друге: рахую не лише кількість запитів. Імпорт робить запити, кожен з яких у 50 разів дорожчий за GET. Тому кожен ендпоінт має вартість у токенах (GET 1, пошук 5, імпорт пачки 50), і token bucket списує вартість. Додаю ліміт конкурентності на tenant: не більше 10 запитів одночасно в роботі, бо саме конкурентні важкі запити забирають пул бази.

    Додаткове питання: Як зробити справедливий розподіл у BullMQ, якщо в одній черзі задачі від 300 клієнтів?

  5. 5. Design: чат для 5 млн DAU, у піку онлайн 20 %, кожен користувач надсилає 40 повідомлень на добу, середня розмова має 5 учасників. Оціни кількість одночасних WebSocket зʼєднань, gateway вузлів, повідомлень і доставок на секунду, обсяг зберігання. Що з цього найдорожче?

    Відповідь

    Зʼєднання: 5 млн × 20 % = 1 млн одночасних WebSocket. Скільки тримає один Node процес, міряю, але орієнтир 40 або 50 тисяч при тюнінгу дескрипторів і TLS, термінованому на балансувальнику. Памʼять на зʼєднання з буферами порядку 20 або 50 КБ, тобто 1 або 2 ГБ на процес. Виходить 20 або 25 процесів, з запасом на втрату сервера і деплой 30, тобто 3 або 4 сервери по 16 ядер. Повідомлення: 5 млн × 40 = 200 млн на добу, в середньому 2300 на секунду. Пік беру вчетверо: близько 9 тисяч на секунду записів. Для однієї бази на запис це вже серйозно, але реально з партиціонуванням або Cassandra подібним сховищем.

    Додаткове питання: Як ти розбʼєш систему на сервіси, щоб деплой бізнес логіки не рвав мільйон зʼєднань?

  6. 6. Після деплою gateway вузлів 300 тисяч клієнтів перепідключились протягом кількох секунд. Сервіс автентифікації ліг, Postgres отримав сплеск запитів історії, частина gateway перевантажена, частина порожня, і система не могла піднятися 15 хвилин. Розбери причини і що зміниш у клієнті, gateway і деплої.

    Відповідь

    Деплой закрив зʼєднання одночасно, і кожен клієнт перепідключився одразу. Кожне перепідключення це TLS handshake, перевірка токена, запис у реєстр присутності, запит історії з last_seq. 300 тисяч цього за кілька секунд кладе автентифікацію і базу. Клієнти отримують помилку і знову миттєво пробують, тобто система сама себе ддосить і не може вийти з цього стану. Exponential backoff з full jitter: затримка випадкова від 0 до min(cap, base × 2^n), наприклад base 1 с, cap 60 с. Перша спроба після штатного закриття теж із випадковою затримкою в кілька секунд. Сервер може в кадрі закриття передати підказку reconnect_after. При SIGTERM вузол знімається з балансувальника і закриває свої 40 тисяч зʼєднань поступово, протягом 2 або 5 хвилин, а не одразу.

    Додаткове питання: Як зробити деплой логіки gateway взагалі без розриву зʼєднань?

  7. 7. Спроєктуй API і модель даних сервісу коротких посилань. Як генеруватимеш код: хеш від URL, випадковий рядок чи лічильник? Що робиш з колізіями і як 10 інстансів API отримують унікальні id, не звертаючись до спільного сховища на кожен запис?

    Відповідь

    API: POST створює посилання і повертає код, GET /:code віддає редирект, DELETE доступний власнику. Редирект роблю 302, бо 301 браузер кешує назавжди: аналітика не побачить повторних переходів, а ціль посилання вже не змінити. Модель: links(code primary key, long_url, owner_id, created_at, expires_at), унікальність коду тримає primary key, а не SELECT перед INSERT, бо між ними є гонка. Код від хешу URL або випадковий дає колізії (при 6 млрд записів їх будуть тисячі), тому INSERT ... ON CONFLICT DO NOTHING і повтор із сіллю. Надійніше лічильник з діапазонами: кожен інстанс забирає з Postgres sequence блок на 10 000 id і роздає їх з пам'яті, колізій немає за побудовою, а дірки в id нікому не заважають. Щоб коди не були передбачуваними, id перед base62 перемішують бієктивною функцією із секретом.

    Додаткове питання: Як зробити так, щоб той самий довгий URL від того самого користувача завжди давав той самий код?

  8. 8. Після завантаження файл треба перевірити антивірусом, для картинок зробити три мініатюри, для відео превʼю. ClamAV сканує десятки МБ/с на ядро, ffmpeg на відео працює хвилинами. Спроєктуй цей конвеєр і віддачу: де черги, як не дати відео заблокувати картинки, коли файл стає доступним і як віддати 20 Гбіт/с скачувань.

    Відповідь

    Complete фіксує status = uploaded і пише file.uploaded в outbox, relay публікує в exchange files у RabbitMQ, а від нього йдуть окремі черги: files.scan для всіх, files.image і files.video після скану. Окремі черги й пули воркерів потрібні, бо в спільній черзі 50 відео по 10 хвилин займуть усіх воркерів, а картинки по 200 мс чекатимуть годину. Воркер стрімить обʼєкт з MinIO у clamd командою INSTREAM без запису на диск, а ядра для скану рахують від пікового трафіку з запасом на архіви. Чистий файл отримує status = ready, інфікований іде в quarantine бакет і status = rejected. Результати пишуться ідемпотентно за fileId, retry з backoff і DLQ. Віддача іде через підписаний URL, CDN або кеш Nginx перед MinIO, з незмінними ключами, довгим Cache-Control і Range.

    Додаткове питання: Як віддавати приватні файли через CDN так, щоб кеш працював, а чужий користувач не скачав файл за скопійованим URL?

  9. 9. Порівняй три способи зберігати індекс підказок: completion suggester в Elasticsearch, поле з edge_ngram аналізатором і префіксний індекс у Redis (sorted set з топ-10 на кожен префікс). Опиши модель даних кожного, API ендпоінта і що обереш для 2 млн товарів і 10 млн популярних запитів.

    Відповідь

    Completion suggester в Elasticsearch будує FST у пам'яті, префіксний пошук займає 1 або 2 мс, є weight і fuzzy, але пошук лише з початку рядка, а вага статична, тож оновлення популярності потребує переіндексації. Поле з edge_ngram аналізатором пише префікси кожного слова, тому працює з фільтрами й function_score за продажами та in_stock, але індекс більший, а запит повільніший. Префіксний індекс у Redis тримає sorted set з топ-10 на кожен префікс: читання менше 1 мс, але пам'ять росте з кількістю префіксів, перебудова офлайн і немає опечаток. API це GET /suggest з q і limit та Cache-Control: public, max-age=300 для коротких префіксів, щоб кешували Nginx або CDN. Для 10 млн популярних запитів беру Redis або completion, а для 2 млн товарів з фільтром наявності edge_ngram.

    Додаткове питання: Як оновлювати популярність підказок, не переіндексовуючи все щогодини?

  10. 10. Продаж за категоріями: «фан зона, 20 000 квитків» без вибору місця. Перша версія тримає залишок в одному рядку: UPDATE inventory SET left_count = left_count, 1 WHERE category_id = $1 AND left_count > 0. На старті продажу p99 росте до 8 секунд, пул зʼєднань вичерпано, а CPU бази низький. Чому і як прибрати це вузьке місце?

    Відповідь

    Усі покупці категорії оновлюють один рядок, і Postgres серіалізує їх через row lock: якщо транзакція з round trip триває 20 або 50 мс, рядок пропускає лише десятки або сотні оновлень за секунду. Решта стоїть у черзі, тримаючи зʼєднання з пулу, тому пул вичерпується, а CPU низький, бо всі чекають. У pg_stat_activity це видно як сесії з wait_event_type Lock. Перше, що роблю: скорочую транзакцію, жодних зовнішніх викликів між BEGIN і COMMIT. Друге: прибираю єдиний рядок, або створюю окремі рядки квитків і беру вільний через FOR UPDATE SKIP LOCKED, або ділю залишок на десятки бакетів. Третє: ставлю перед базою лічильник у Redis, щоб розпродано відповідати без звернення в Postgres, а керований вхід обмежує кількість одночасних транзакцій на категорію.

    Додаткове питання: Як перевірити під навантаженням, що overselling справді нема, а не просто не трапився в цьому прогоні?

  11. 11. Спроєктуй модель double-entry ledger і API переказу між користувачами. Які таблиці, як гарантуєш, що сума проводок дорівнює нулю, як зберігаєш баланс і як Idempotency-Key захищає від подвійного переказу, коли клієнт повторив запит після таймауту?

    Відповідь

    Таблиці. accounts(id, owner_id, currency, type, balance bigint). Типи: рахунок користувача, рахунок комісій, технічний рахунок провайдера, через який гроші входять і виходять. transactions(id, type, status, idempotency_key, created_at, metadata). entries(id, transaction_id, account_id, amount bigint, currency, created_at), де amount зі знаком: мінус списання, плюс зарахування. Переказ 100 грн з комісією 2 грн: 10200 копійок списання з А, 10000 зарахування на Б, 200 на рахунок комісій. Сума 0. Інваріант перевіряється в коді перед вставкою і constraint trigger у базі, а нічна задача рахує SUM(amount) по всьому ledger. UPDATE і DELETE на entries заборонені правами ролі застосунку. Помилкову операцію виправляє нова транзакція з протилежними проводками і посиланням на оригінал.

    Додаткове питання: Що робити з рахунком, на який іде 500 зарахувань за секунду, наприклад з рахунком комісій?

  12. 12. Спроєктуй модель даних і механізм вибору задач для планувальника, де працюють 4 інстанси сервісу. Порівняй два підходи: лідер (через advisory lock у Postgres або lease у Redis) вибирає задачі і роздає, проти всі воркери самі беруть задачі через FOR UPDATE SKIP LOCKED. Як задача не губиться, якщо воркер упав посеред виконання?

    Відповідь

    Модель. jobs(id bigint, user_id, kind, payload jsonb, run_at timestamptz, tz text, rrule text, status, attempts, locked_by, locked_until, created_at). Статуси scheduled, running, done, failed. Індекс CREATE INDEX ON jobs (run_at) WHERE status = 'scheduled': частковий, тому маленький і без мільйонів виконаних. Кожен воркер у циклі: UPDATE jobs SET status = 'running', locked_by = $1, locked_until = now() + interval '2 minutes', attempts = attempts + 1 WHERE id IN (SELECT id FROM jobs WHERE status = 'scheduled' AND run_at <= now() ORDER BY run_at LIMIT 200 FOR UPDATE SKIP LOCKED) RETURNING *. Конкуренти пропускають заблоковані рядки, тож 4 воркери беруть різні батчі без жодної координації. Транзакція коротка, лише захоплення. Саме виконання йде після COMMIT, а не всередині відкритої транзакції.

    Додаткове питання: Чим поганий node-cron або @nestjs/schedule у кожному інстансі для цієї задачі?

  13. 13. У піку 150 000 подій за секунду ClickHouse встигає приймати лише 100 000, consumer lag у Kafka росте на мільйони, а один ingest інстанс тим часом падає з OOM, бо producer буферизує в памʼяті. Опиши backpressure від сховища до клієнта: що робить кожен шар і що свідомо відкидаєш.

    Відповідь

    Ідея: у кожного шару обмежений буфер, і коли він повний, сигнал «повільніше» іде вгору, а не памʼять росте до OOM. Kafka тут амортизатор: якщо пік 150 000 триває годину, а ClickHouse бере 100 000, lag виросте на 180 млн подій, і за кілька годин після піку consumer їх наздожене. Це нормальний режим, поки retention 72 години. Алерт ставимо на lag у часі (вік найстарішого непрочитаного повідомлення), а не в кількості повідомлень. Consumer при великому lag збільшує батч до 200 000 рядків, бо ClickHouse великі батчі пише ефективніше, і додає екземпляри в групу до кількості партицій. Відповідати на lag дрібнішими і частішими insert не можна: це Too many parts і ще повільніше. OOM означає необмежений буфер: send без await, проміси накопичуються, а Kafka в цей момент відповідає повільно, наприклад через переобрання лідера партиції.

    Додаткове питання: Як не дати одному галасливому сервісу, що раптом шле вдесятеро більше логів, зʼїсти всю місткість конвеєра?

  14. 14. Bare-metal кластер на шість вузлів. О 9:00 трафік різко зростає, HPA хоче 30 подів API, але 10 з них висять у Pending, а латентність перші 4 хвилини сплеску вдесятеро вища. Чому HPA спрацював так пізно і так мало, і що ти зміниш без хмари й автоматичного додавання вузлів?

    Відповідь

    HPA працює циклом: раз на 15 секунд читає метрики з metrics-server, які самі приходять із затримкою в десятки секунд, рахує бажану кількість як поточні поди, помножені на відношення поточної метрики до цільової, а новий под ще має стягнути образ, стартувати і пройти readiness. Тому реакція на сплеск це хвилини, а не секунди. Pending означає, що scheduler не знаходить вузол, де вистачає requests: на bare-metal немає Cluster Autoscaler, тож стеля це сумарні requests шести вузлів. Що зробити: тримати запас (вищий мінімум реплік перед відомим піком або за розкладом, нижчий target утилізації), пришвидшити старт (менший образ, startupProbe замість великої затримки), задати behavior для швидкого scale-up і повільного scale-down, тримати поди-заглушки з низьким PriorityClass, які витісняються під реальне навантаження, і вмикати load shedding, поки місткість наздоганяє.

    Додаткове питання: На яку метрику масштабувати Node API замість CPU і чому?

  15. 15. Бізнесу потрібен щоденний звіт: виручка за регіонами з розбивкою по категоріях товарів і сегментах клієнтів. Дані лежать у трьох сервісах: orders, catalog, customers, у кожного своя база. Як побудувати звіт? Чому не варто просто дати BI read-only репліки і робити join там?

    Відповідь

    Read-only репліки з join у BI працюють рівно до першої зміни схеми в одному з сервісів: звіт ламається, а власник сервісу навіть не знає, що його таблицю хтось читає. Це той самий shared database, тільки споживачем стала аналітика. API composition: окремий сервіс викликає три API і зʼєднує дані в памʼяті. Для сторінки замовлення з 10 рядками нормально. Для звіту по 5 млн замовлень це пагінація по трьох сервісах, навантаження на прод і години роботи. Правильний шлях: окреме аналітичне сховище, наприклад ClickHouse або окремий Postgres, яке наповнюють самі сервіси. Варіант перший: доменні події OrderPaid, ProductCategorized, CustomerSegmentChanged, кожен сервіс публікує через outbox, а ingestion складає їх у таблиці фактів і вимірів.

    Додаткове питання: Один сервіс тиждень відправляв події з неправильною валютою. Як виправити дані у сховищі?

  16. 16. Сервіс orders тримає копію назви і ціни товару, яку отримує з події ProductUpdated від catalog через Kafka. Після інциденту частина карток показує стару ціну, хоча в catalog нова. Логи consumer чисті. Назви три причини, як таке буває, і як побудувати копію, щоб вона сходилась сама.

    Відповідь

    Перша причина: порядок. Без ключа події одного товару потрапляють у різні partition, або consumer обробляє батч паралельно, і стара подія перезаписує нову. Друга: втрата, бо autoCommit комітить offset незалежно від запису в базу, або catalog пише в базу і в Kafka без outbox. Третя: повтор старої події після нової, наприклад після reset offset або redrive з DLQ. Логи чисті, бо кожна обробка формально успішна. Щоб копія сходилась, ключ повідомлення productId, у події монотонна version і умовний UPDATE ... WHERE version менша, а подія несе повний стан, а не дельту. Публікація з catalog іде через outbox, а шлях відновлення це compacted topic або повний reload.

    Додаткове питання: Як ти в проді помітиш, що копія розійшлась з оригіналом, раніше, ніж поскаржиться клієнт?

  17. 17. Оркестратор saga оформлення замовлення тримає стан у памʼяті процесу. Під час деплою pod перезапустився: оплата пройшла, резерв на складі висить, доставка не створена. Як спроєктувати оркестратор, щоб він переживав падіння? Що і коли писати в базу?

    Відповідь

    Стан saga це дані, а не змінні в памʼяті. Таблиця saga_instances(id, type, state, payload jsonb, version, deadline_at, updated_at). Кожен перехід в одній транзакції: UPDATE стану з WHERE version = $old і INSERT команди наступного кроку в outbox. Relay публікує команду в брокер. Якщо процес упав після COMMIT, команда все одно вийде. Якщо до COMMIT, нічого не сталось, і перехід повториться. Відповіді учасників приходять подіями з sagaId.

    Додаткове питання: Компенсація RefundPayment теж падає: провайдер дві години повертає 500. Що робить saga?

  18. 18. Saga переказу: списати з рахунку A, зарахувати на B. Між кроками інший запит читає баланс A і дозволяє ще одне списання, а saga потім компенсується. Чому так стається? Які контрзаходи проти відсутньої ізоляції в saga ти знаєш, і як семантичне блокування виглядає на практиці?

    Відповідь

    Кожен крок saga це окрема локальна транзакція, яка комітиться одразу. Тому saga дає атомарність через компенсації, узгодженість і довговічність, але не ізоляцію. Між кроками інші запити бачать проміжний стан: це dirty read, якщо saga потім відкотиться, і lost update, якщо хтось перезапише те, що saga згодом компенсує. Семантичне блокування: крок не просто змінює дані, а позначає, що запис у процесі. Списання з A не зменшує balance напряму, а створює hold: рядок у holds зі статусом PENDING і сумою. Доступний баланс рахується як balance мінус сума активних hold. Інший запит бачить зарезервовані гроші й не дозволяє витратити їх двічі.

    Додаткове питання: Hold висить 3 дні, бо saga загубилась. Як ти це помітиш і хто його зніме?

  19. 19. Подія OrderPlaced v1 мала поле amount у гривнях як число з плаваючою точкою. Тепер потрібні валюта і копійки: amount як обʼєкт з value і currency. У журналі 40 млн старих подій. Як змінити схему подій? Чому не можна просто зробити UPDATE старих подій?

    Відповідь

    Подія фіксує факт, який вже стався, і на неї спираються проєкції, аудит і, можливо, інші сервіси, які отримали її через Kafka. UPDATE 40 млн рядків ламає незмінність журналу: аудит більше не доводить, що було, а споживачі, які вже обробили v1, мають іншу історію, ніж база. До того ж це велика міграція без можливості відкату. Нове опційне поле можна додати без версії: старі події його не мають, обробник ставить дефолт. Змінити тип, одиниці чи сенс поля можна лише через нову версію: schema_version у metadata або новий тип події. Округлення роблю явно, Math.round(amount * 100), бо в double 0.1 + 0.2 не дорівнює 0.3. Обробники і проєкції знають лише останню версію, а все старе живе в одному місці.

    Додаткове питання: Як тестувати upcaster, щоб не зламати відновлення агрегатів зі старими подіями?

  20. 20. Трейсинг 100% запитів при 3000 rps коштує дорожче за весь кластер. Як налаштувати sampling? Чим head-based відрізняється від tail-based? Як не втратити трейси саме тих запитів, що впали?

    Відповідь

    Head-based sampling: рішення приймає перший сервіс у момент створення трейсу. У Node OTEL_TRACES_SAMPLER=parentbased_traceidratio і OTEL_TRACES_SAMPLER_ARG=0.05, тобто 5%. Рішення записується в trace-flags, останнє поле traceparent: 01 означає sampled. Parent-based sampler у наступних сервісах не вирішує заново, а слідує прапору батька. Якщо кожен сервіс семплує незалежно по 5%, повних трейсів через 4 сервіси майже не буде. Мінус head-based: рішення приймається до того, як відомо, чи запит упаде, тому 95% помилок лишаються без трейсу. Tail-based: сервіси шлють усі спани в OpenTelemetry Collector, а processor tail_sampling чекає decision_wait, наприклад 10 секунд, і вирішує за політиками: всі трейси зі статусом ERROR, всі довші за 1 секунду і 1% решти.

    Додаткове питання: Клієнт скаржиться на конкретний запит, а трейс міг не потрапити у вибірку. Як його знайти?

  21. 21. Вендор обіцяє "exactly-once delivery". Що ти запитаєш? Що насправді дають Kafka transactions? Чому це не вирішує проблему подвійного списання в Stripe?

    Відповідь

    Першим запитаю: де саме межі цієї гарантії? Consumer не може відрізнити "виконав, але ack загубився" від "не виконав". Тому гарантовано є лише at-least-once, а exactly-once processing досягається дедуплікацією: повтори можливі, але ефект один. Це називають effectively once. У Kafka idempotent producer прибирає дублікати при повторних спробах запису в лог, він увімкнений за замовчуванням. Транзакції дозволяють атомарно записати вихідні повідомлення разом з offset, а consumer з isolation.level=read_committed не бачить скасованого. Це справжня гарантія, але для потоку read-process-write всередині Kafka.

    Додаткове питання: Якщо вихід consumer це запис у Postgres, як отримати effectively once без Kafka transactions?

  22. 22. Спроєктуй ідемпотентний POST /payments. Клієнт при таймауті повторює запит. Опиши схему таблиці, обробку двох паралельних запитів з одним ключем, запит з тим самим ключем, але іншим тілом, і термін зберігання ключів.

    Відповідь

    Таблиця idempotency_keys(client_id, key, request_hash, status, response_code, response_body, created_at) з унікальним індексом (client_id, key). Перша вставка з ON CONFLICT DO NOTHING вирішує, хто виконує операцію. Якщо рядок вставлено, ми перші й виконуємо платіж. Якщо ключ уже був, дивимось на нього: інший request_hash дає 422, завершений статус повертає збережену відповідь разом з помилкою, а виконання, що ще триває, дає 409 з Retry-After. Так паралельний запит не виконує роботу вдруге. Ключі живуть обмежений час, наприклад добу, і потребують очищення. Окремо треба продумати падіння після виклику провайдера, але до COMMIT: передавати провайдеру той самий ключ.

    Додаткове питання: Що робити, якщо процес упав після виклику платіжного провайдера, але до COMMIT?

  23. 23. Після збою в DLQ накопичилось 50 тисяч повідомлень за 6 годин. Баг виправили, натиснули redrive. Через 5 хвилин база лягла, частина повідомлень знову в DLQ, а деякі дані перезаписані старими значеннями. Що пішло не так і як треба було зробити?

    Відповідь

    Перша: redrive на повній швидкості. Це 50 тисяч задач одразу, хоча в нормі вони розтягнуті на 6 годин. Система отримала пік, якого раніше не бачила. Частина знову впала і пішла в DLQ, і це лише збільшило навантаження. Події в DLQ старі, а основний потік тим часом обробляв нові. Коли стару подію застосували пізніше, вона перезаписала новіше значення. Виправлення у самому consumer: застосовувати подію, лише якщо вона новіша.

    Додаткове питання: Як реалізувати обмежений redrive у BullMQ, якщо задачі лежать у failed set?

  24. 24. Спроєктуй relay для outbox на Postgres. Polling чи CDC? Як запустити кілька реплік relay без подвійної публікації? Як зберегти порядок за aggregate? Що робити з ростом таблиці?

    Відповідь

    Polling раз на 100 до 1000 мс простий і працює будь-де, але дає затримку і навантаження на базу. CDC через Debezium читає WAL з мінімальною затримкою, але потребує окремої інфраструктури і керування replication slot, а неактивний slot затримує очищення WAL. Для початку беру polling. Кілька реплік: або SELECT ... FOR UPDATE SKIP LOCKED у транзакції (паралельно, але порядок між пачками не гарантований), або одна активна репліка через advisory lock, що простіше і зберігає порядок. Порядок за aggregate забезпечує ключ партиції в брокері. Подвійна публікація можлива завжди, тому в подію кладу стабільний event_id, а consumer дедуплікує. Таблицю чистять порціями або партиціонують за часом, індекс частковий WHERE sent_at IS NULL, а вік найстарішого ненадісланого рядка є метрикою затримки.

    Додаткове питання: Чому курсор "id > останній прочитаний" небезпечний у polling?

  25. 25. Топік: replication.factor=3, min.insync.replicas=1, producer з acks=1. Після падіння брокера зникли повідомлення, на які producer отримав успіх. Як саме це сталось? Яку конфігурацію ти поставиш і що вона коштує, коли падають один і два брокери?

    Відповідь

    З acks=1 лідер партиції відповідає producer одразу після запису у свій лог. Followers тягнуть дані асинхронно. Якщо лідер упав до того, як follower скопіював запис, новим лідером стає follower з ISR, у якого цих записів немає. Producer отримав успіх, а дані зникли. Kafka не робить fsync на кожен запис (flush.messages за замовчуванням вимкнено), тож навіть рестарт того самого брокера не рятує: на стійкість відповідає реплікація. Друга пастка: min.insync.replicas=1. Навіть з acks=all, якщо обидва followers відстали більше ніж на replica.lag.time.max.ms (30 с) і випали з ISR, лідер сам собі ISR, і acks=all фактично стає acks=1.

    Додаткове питання: Чому min.insync.replicas задають на топіку, а не на producer, і що станеться з producer, у якого acks=1, на такому топіку?

  26. 26. Мобільний клієнт робить 3 спроби, API gateway 3 спроби, сервіс замовлень 3 спроби до сервісу оплат, а фонова задача BullMQ має attempts: 5. Що станеться, коли сервіс оплат почне відповідати повільно? Як правильно розподілити retry між шарами?

    Відповідь

    Повтори множаться по шарах: одна дія користувача дає 3 × 3 × 3 = 27 запитів до оплат, а задача BullMQ ще ×5. Саме коли оплати гальмують, на них іде в десятки разів більше запитів, черги ростуть, і виникає metastable failure: навіть після зникнення причини retry storm тримає систему лежачою. Правильно повторювати на одному шарі, найближчому до збою або на самому краю, а проміжні шари одразу повертають помилку. Додають retry budget, наприклад повтори не більше 10 % від запитів, і deadline propagation, щоб не повторювати, коли часу не лишилось. Повторюють лише тимчасові помилки та ідемпотентні запити, а circuit breaker швидко відсікає збійну залежність. Фонові задачі можуть повторювати довше, але з exponential backoff, jitter і обмеженням concurrency.

    Додаткове питання: Як перевірити в навантажувальному тесті, що система відновлюється сама після 2 хвилин збою залежності?

Тренуй ці питання

Вчи картки, відповідай текстом або голосом і отримуй оцінку AI. Потрібен вхід в акаунт.

Відповідай уголос і отримуй оцінку

Тут питання з еталонними відповідями. У застосунку ShortLesson ти відповідаєш сам, текстом або голосом, а AI оцінює відповідь за критеріями і ставить додаткове питання, як на справжній співбесіді.

Застосунок незабаром з'явиться в магазинах.

Інші теми

Розділи платформи