Middle: питання на співбесіді з теми «Продакшен і оркестрація»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
5 питань
Обидва - оркестратори: керують контейнерами на кластері серверів за описом бажаного стану. Різниця - у складності й можливостях.
Docker Swarm - вбудований у Docker Engine:
docker swarm init
docker stack deploy -c compose.yaml myapp
docker service scale myapp_app=4
- сервіс - опис того, що має працювати (образ, кількість реплік, порти, ресурси);
- задача (task) - конкретний контейнер сервісу на конкретному вузлі;
- стек - набір сервісів з файлу у форматі Compose (з розділом
deploy); - вбудовані: overlay-мережі між вузлами, балансування (routing mesh), секрети, поступові оновлення з відкатом.
Kubernetes - окрема платформа зі своїми поняттями:
- Pod - найменша одиниця: один чи кілька контейнерів зі спільною мережею;
- Deployment - бажана кількість однакових pod-ів і стратегія оновлення;
- Service - стабільна адреса й балансування до pod-ів;
- Ingress / Gateway - вхід HTTP-трафіку ззовні;
- ConfigMap, Secret, томи (PersistentVolume), горизонтальне автомасштабування, оператори для баз даних і черг.
Порівняння:
| Swarm | Kubernetes | |
|---|---|---|
| поріг входу | низький: знайомий формат Compose | високий: багато понять і YAML |
| встановлення | одна команда | керований сервіс чи складне налаштування |
| екосистема | обмежена | величезна (Helm, оператори, моніторинг) |
| автомасштабування | немає вбудованого | є (HPA, кластерні автоскейлери) |
| популярність, вакансії | невелика | стандарт індустрії |
Коли достатньо Swarm: кілька серверів, кілька сервісів, невелика команда без окремого DevOps, потреба в простих поступових оновленнях і відмовостійкості. Багато PaaS для самостійного хостингу (наприклад, Dokploy) використовують Swarm під капотом.
Коли Kubernetes: десятки сервісів, кілька команд, автомасштабування, складні мережеві й безпекові політики, керований сервіс у хмарі - і готовність інвестувати в його знання.
Помилка вибору: Kubernetes для одного Laravel-застосунку з базою - складність, яка забирає більше часу, ніж дає користі.
Найпростіший деплой - docker compose pull && docker compose up -d - зупиняє старий контейнер і лише потім запускає новий. Між цими моментами сайт недоступний, а якщо новий контейнер не стартує - недоступний довго.
Принципи деплою без простою:
1. Спершу запустити нове, потім зупинити старе (start-first). Обидві версії якийсь час працюють паралельно.
2. Трафік на новий контейнер - лише коли він готовий. Перевірка стану (healthcheck) підтверджує, що застосунок справді відповідає, а не просто процес запущено:
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost/up"]
interval: 10s
timeout: 3s
retries: 3
start_period: 20s
У Laravel 11+ маршрут /up налаштовано за замовчуванням.
3. Старий контейнер завершує поточні запити перед зупинкою (graceful shutdown): отримує SIGTERM, перестає приймати нові запити, дообробляє поточні, і лише потім зупиняється. Балансувальник має встигнути прибрати його з пулу.
4. Автоматичний відкат, якщо нова версія не пройшла перевірку стану.
Як це реалізують:
- Docker Swarm:
deploy:
replicas: 2
update_config:
parallelism: 1
order: start-first
failure_action: rollback
Розділ update_config діє саме в режимі Swarm (docker stack deploy); звичайний docker compose up його не використовує і просто перестворює контейнери.
- Kubernetes - стратегія
RollingUpdateу Deployment з readiness-перевірками; - Kamal - запускає новий контейнер, перевіряє його й перемикає трафік через свій проксі (
kamal-proxy); - власний скрипт з Compose - запустити новий контейнер під іншою назвою, дочекатися здорового стану, перемкнути зворотний проксі (Caddy, Traefik, Nginx), зупинити старий.
Пастки, через які «без простою» не виходить:
- міграції бази, несумісні зі старою версією коду: під час переходу працюють обидві версії. Видалення чи перейменування колонки ламає стару. Потрібні сумісні в обидва боки міграції (спершу додати, потім - окремим релізом - прибрати);
- черги: воркери зі старим кодом обробляють задачі нового формату -
queue:restartі сумісний формат задач; - сесії й кеш у пам'яті контейнера - губляться при заміні.
Перевірки стану відповідають на різні питання, і оркестратор реагує на них по-різному.
Liveness - «процес живий чи завис?»
Якщо перевірка не проходить кілька разів поспіль, контейнер перезапускається. Призначення - виходити з безвихідних станів: взаємоблокування, нескінченний цикл, вичерпані воркери.
Readiness - «чи готовий приймати трафік прямо зараз?»
Якщо не проходить - контейнер прибирається з балансування, але не перезапускається. Призначення - не слати запити туди, де їх не оброблять: застосунок ще прогрівається, тимчасово перевантажений, втратив з'єднання з залежністю.
Startup - «чи завершився запуск?»
Поки вона не пройшла, інші перевірки не виконуються. Для застосунків з довгим стартом: без неї liveness-перевірка може «вбивати» контейнер, що просто повільно запускається.
Що буде, якщо переплутати:
- liveness перевіряє базу даних. База на хвилину недоступна - усі контейнери застосунку провалюють liveness і одночасно перезапускаються. Після відновлення бази - шторм перезапусків і холодний старт усього сервісу. Залежності - у readiness, а liveness має перевіряти лише сам процес;
- немає readiness - трафік іде на контейнер, який ще прогріває кеш чи виконує міграції: користувачі отримують помилки після кожного деплою;
- занадто суворі пороги (тайм-аут 1 секунда під навантаженням) - здорові контейнери перезапускаються саме тоді, коли найбільше потрібні;
- важка перевірка (повний запит до бази, рендер сторінки) на кожні кілька секунд - зайве навантаження.
Як це виглядає в Docker: HEALTHCHECK у Dockerfile чи healthcheck у Compose дає один статус (healthy/unhealthy). Сам Docker не перезапускає нездоровий контейнер - на статус реагують Swarm, Compose з depends_on: condition: service_healthy чи зовнішні інструменти.
Для Laravel:
- liveness - легкий ендпойнт без звернень до залежностей (маршрут
/upпідходить, якщо не перевантажений подіями); - readiness - окремий ендпойнт, що перевіряє базу, Redis, наявність закешованої конфігурації;
- черги й планувальник - окремі перевірки: воркер без HTTP перевіряють командою.
Поширена помилка - міграції в команді старту контейнера:
CMD php artisan migrate --force && php-fpm
Проблеми:
- кілька реплік стартують одночасно - і одночасно запускають ті самі міграції. Конфлікти, блокування, наполовину застосовані зміни;
- падіння міграції - контейнер не стартує, оркестратор перезапускає його знову й знову;
- масштабування (новий контейнер через годину) теж запускає міграції;
- воркери черг і планувальник з того самого образу - теж мігрують.
Правильні варіанти:
1. Окремий крок деплою перед оновленням застосунку - одноразовий контейнер з тим самим образом:
docker run --rm --env-file .env ghcr.io/acme/app:sha-a1b2c3d php artisan migrate --force
У Kubernetes - Job перед оновленням Deployment; у Kamal - хук перед деплоєм; у CI - окремий крок пайплайну.
2. Якщо міграції все ж запускаються зі старту контейнера - з блокуванням:
php artisan migrate --force --isolated
--isolated бере атомарне блокування через кеш - міграції виконає лише один процес, інші пропустять. Потрібен спільний драйвер кешу з підтримкою блокувань (Redis, база даних).
Сумісність версій - найважливіше. Під час поступового деплою старий і новий код працюють одночасно з уже оновленою базою. Тому міграції мають бути сумісними з попередньою версією:
- додати колонку - безпечно (nullable чи зі значенням за замовчуванням);
- перейменувати чи видалити колонку - у кілька релізів: додати нову, писати в обидві, перенести дані, перевести читання, і лише потім видалити стару;
- важкі зміни великих таблиць (індекси, зміна типу) - без довгих блокувань (онлайн-DDL,
CREATE INDEX CONCURRENTLYу PostgreSQL), інакше застосунок «зависне» на час міграції.
Відкат: відкат коду не відкочує базу. План має враховувати, що попередня версія коду працюватиме з новою схемою - ще одна причина для сумісних міграцій.
Резервна копія перед міграцією на продакшені - обов'язковий крок для ризикованих змін.
Горизонтальне масштабування - запустити кілька однакових контейнерів і розподіляти між ними запити. Працює, лише якщо контейнер без стану (stateless): будь-який запит може обробити будь-яка копія.
Що заважає масштабуванню і куди це винести:
| Стан у контейнері | Куди винести |
|---|---|
| сесії у файлах | Redis чи база даних (SESSION_DRIVER=redis) |
| кеш у файлах | Redis, Memcached |
завантажені файли в storage/app |
об'єктне сховище (S3, R2) чи спільний том |
черги в синхронному режимі чи database на SQLite |
Redis чи база даних, окремі воркери |
| планувальник у кожному контейнері | окремий контейнер чи onOneServer() |
| блокування у файлах | блокування через Redis/базу |
Типові наслідки, якщо цього не зробити:
- користувача «розлогінює» при кожному запиті - сесія на іншій копії;
- завантажений аватар то є, то немає;
- запланована задача виконується стільки разів, скільки контейнерів (листи - вчетверо);
- кеш скидається лише на одній копії.
Масштабування в різних середовищах:
docker compose up -d --scale app=4 # кілька копій на одному сервері
docker service scale myapp_app=4 # Swarm
kubectl scale deployment app --replicas=4 # Kubernetes
На одному сервері кілька копій мають сенс, лише якщо одна копія не використовує всі ядра; справжня користь - копії на різних серверах за балансувальником.
Що ще враховувати:
- з'єднання з базою: кожна копія тримає свій пул - 10 копій по 20 процесів PHP-FPM = 200 з'єднань. База може не витримати раніше, ніж закінчаться ресурси застосунку;
- база - не масштабується так само: вузьке місце переміщується туди - репліки для читання, кеш, оптимізація запитів;
- довірені проксі - за балансувальником застосунок має бачити справжні IP і протокол;
- одноразові задачі при старті (міграції, прогрів кешу) не повинні виконуватися кожною копією.
Для Laravel контейнер з php-fpm чи FrankenPHP, сесіями й кешем у Redis, файлами в S3 і окремими контейнерами для воркерів і планувальника - класична конфігурація, що масштабується.