Чому Systemd не підходить для контейнерів?
Контейнери засновані на принципі “один процес = один контейнер”. Systemd – це система управління множинними процесами, що суперечить контейнерній філософії. Використання systemd всередині контейнера вважається антипатерном.
Проблеми systemd в контейнерах:
- Потребує привілейованого режиму
- Порушує принцип immutable infrastructure
- Ускладнює debugging та логування
- Конфліктує з оркестраторами (Docker Compose, Kubernetes)
- Збільшує розмір образу та час запуску
Контейнерний підхід: окремі сервіси
Базова архітектура
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# docker-compose.yml version: '3.8' services: # Веб-сервер Laravel app: build: context: . dockerfile: Dockerfile image: laravel-app:local # Тегування для кешування ports: - "80:80" environment: QUEUE_CONNECTION: redis depends_on: - redis - db volumes: - storage:/var/www/storage # Воркер для обробки черг queue-worker: build: . command: php artisan queue:work --sleep=3 --tries=3 --max-time=3600 restart: unless-stopped environment: QUEUE_CONNECTION: redis depends_on: - redis - db volumes: - storage:/var/www/storage # Планувальник завдань scheduler: build: . command: php artisan schedule:work restart: unless-stopped depends_on: - db volumes: - storage:/var/www/storage # База даних db: image: mysql:8 environment: MYSQL_DATABASE: laravel MYSQL_USER: laravel MYSQL_PASSWORD: secret MYSQL_ROOT_PASSWORD: secret volumes: - mysql_data:/var/lib/mysql # Redis для черг redis: image: redis:7-alpine restart: unless-stopped volumes: - redis_data:/data command: redis-server --appendonly yes volumes: storage: mysql_data: redis_data: |
Конфігурації для різних середовищ
Development (локальна розробка)
|
1 2 3 4 5 6 7 8 9 10 11 |
# docker-compose.dev.yml services: app: build: context: . dockerfile: Dockerfile.dev image: laravel-app:dev volumes: - .:/var/www # Монтувати код для live reload - storage:/var/www/storage |
Staging/Production
|
1 2 3 4 5 6 7 |
# docker-compose.prod.yml services: app: image: registry.example.com/laravel-app:${VERSION} # Без build - використовуємо готовий образ # Без volume mount коду - образ містить весь код |
CI/CD пайплайн
|
1 2 3 4 5 6 7 8 9 10 |
#!/bin/bash # deploy.sh # Збірка з унікальним тегом docker build -t myapp:${CI_COMMIT_SHA} . docker push myapp:${CI_COMMIT_SHA} # Деплой з конкретною версією VERSION=${CI_COMMIT_SHA} docker-compose -f docker-compose.prod.yml up -d |
Переваги такого підходу:
- Незалежне масштабування – можна збільшити тільки queue воркери
- Ізоляція збоїв – падіння воркера не впливає на веб-додаток
- Простота управління – кожен сервіс має одну відповідальність
- Легке деплоювання – оркестратор управляє lifecycle
Вибір черги: Redis vs RabbitMQ vs Cloud
Redis – для більшості випадків
|
1 2 3 4 5 6 7 |
redis: image: redis:7-alpine restart: unless-stopped volumes: - redis_data:/data command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru |
Переваги Redis:
- Швидкість (в пам’яті)
- Простота налаштування
- Laravel має нативну підтримку
- Мінімальні ресурси
Недоліки Redis:
- Може втратити дані при збої
- Обмежений функціонал routing
- Немає built-in clustering
RabbitMQ – для критичних систем
|
1 2 3 4 5 6 7 8 9 10 11 12 |
rabbitmq: image: rabbitmq:3-management-alpine restart: unless-stopped environment: RABBITMQ_DEFAULT_USER: laravel RABBITMQ_DEFAULT_PASS: secret RABBITMQ_DEFAULT_VHOST: laravel ports: - "15672:15672" # Management UI volumes: - rabbitmq_data:/var/lib/rabbitmq |
Налаштування в Laravel:
|
1 2 |
composer require vladimir-yuldashev/laravel-queue-rabbitmq |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// config/queue.php 'connections' => [ 'rabbitmq' => [ 'driver' => 'rabbitmq', 'queue' => env('RABBITMQ_QUEUE', 'default'), 'connection' => PhpAmqpLib\Connection\AMQPLazyConnection::class, 'hosts' => [ [ 'host' => env('RABBITMQ_HOST', '127.0.0.1'), 'port' => env('RABBITMQ_PORT', 5672), 'user' => env('RABBITMQ_USER', 'guest'), 'password' => env('RABBITMQ_PASSWORD', 'guest'), 'vhost' => env('RABBITMQ_VHOST', '/'), ], ], 'options' => [ 'ssl_options' => [ 'cafile' => env('RABBITMQ_SSL_CAFILE', null), 'local_cert' => env('RABBITMQ_SSL_LOCALCERT', null), 'local_key' => env('RABBITMQ_SSL_LOCALKEY', null), 'verify_peer' => env('RABBITMQ_SSL_VERIFY_PEER', true), 'passphrase' => env('RABBITMQ_SSL_PASSPHRASE', null), ], ], ], ], |
Переваги RabbitMQ:
- Гарантована доставка повідомлень
- Складні routing schemes
- Clustering та high availability
- Детальний моніторинг через web UI
Cloud черги – Amazon SQS
|
1 2 3 4 5 6 7 8 9 |
# docker-compose.yml (без власної черги) services: app: environment: QUEUE_CONNECTION: sqs AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} SQS_PREFIX: https://sqs.us-east-1.amazonaws.com/123456789 |
Переваги SQS:
- Managed сервіс (без обслуговування)
- Автоматичне масштабування
- Висока доступність
- Інтеграція з AWS ecosystem
Масштабування воркерів
Множинні типи черг
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
services: # Високий пріоритет - швидкі завдання queue-high: build: . command: php artisan queue:work --queue=high --sleep=1 --memory=128 restart: unless-stopped deploy: replicas: 2 # Звичайні завдання queue-default: build: . command: php artisan queue:work --queue=default --sleep=3 --memory=256 restart: unless-stopped deploy: replicas: 3 # Низький пріоритет - довгі завдання queue-low: build: . command: php artisan queue:work --queue=low --sleep=10 --timeout=1800 --memory=512 restart: unless-stopped deploy: replicas: 1 |
Горизонтальне автоскейлінг
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# Для Docker Swarm queue-worker: deploy: replicas: 2 update_config: parallelism: 1 delay: 10s restart_policy: condition: on-failure delay: 5s max_attempts: 3 resources: limits: cpus: '0.50' memory: 512M reservations: cpus: '0.25' memory: 256M |
Dockerfile для queue воркерів
Оптимізований Dockerfile
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
FROM php:8.2-cli-alpine # Встановити необхідні розширення RUN apk add --no-cache \ $PHPIZE_DEPS \ mysql-client \ && docker-php-ext-install pdo pdo_mysql \ && pecl install redis \ && docker-php-ext-enable redis # Встановити Composer COPY --from=composer:latest /usr/bin/composer /usr/bin/composer # Створити користувача RUN addgroup -g 1000 www && \ adduser -u 1000 -G www -s /bin/sh -D www WORKDIR /var/www # Копіювати залежності окремо для кешування COPY --chown=www:www composer.json composer.lock ./ RUN composer install --no-scripts --no-autoloader --prefer-dist # Копіювати код COPY --chown=www:www . . # Завершити встановлення Composer RUN composer dump-autoload --optimize # Налаштувати права RUN mkdir -p storage/logs storage/framework/cache storage/framework/sessions storage/framework/views \ && chown -R www:www storage bootstrap/cache \ && chmod -R 775 storage bootstrap/cache USER www # За замовчуванням запускати queue worker CMD ["php", "artisan", "queue:work", "--verbose", "--tries=3", "--timeout=90"] |
Multi-stage build для production
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# Build stage FROM php:8.2-cli-alpine AS builder WORKDIR /var/www COPY composer.json composer.lock ./ RUN composer install --no-dev --optimize-autoloader --no-scripts COPY . . RUN composer dump-autoload --optimize # Production stage FROM php:8.2-cli-alpine AS production RUN apk add --no-cache mysql-client \ && docker-php-ext-install pdo pdo_mysql \ && addgroup -g 1000 www \ && adduser -u 1000 -G www -s /bin/sh -D www WORKDIR /var/www # Копіювати з build stage COPY --from=builder --chown=www:www /var/www . # Встановити права RUN mkdir -p storage/logs storage/framework/cache \ && chown -R www:www storage bootstrap/cache \ && chmod -R 775 storage bootstrap/cache USER www HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD php artisan queue:monitor || exit 1 CMD ["php", "artisan", "queue:work", "--verbose", "--tries=3", "--max-time=3600"] |
Kubernetes для production
Deployment конфігурація
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
apiVersion: apps/v1 kind: Deployment metadata: name: laravel-queue labels: app: laravel-queue spec: replicas: 3 selector: matchLabels: app: laravel-queue template: metadata: labels: app: laravel-queue spec: containers: - name: queue-worker image: your-registry/laravel-app:latest command: ["php", "artisan", "queue:work"] env: - name: QUEUE_CONNECTION value: "redis" - name: REDIS_HOST value: "redis-service" resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" livenessProbe: exec: command: - php - artisan - queue:monitor initialDelaySeconds: 30 periodSeconds: 60 readinessProbe: exec: command: - php - artisan - tinker - --execute="echo 'ready';" initialDelaySeconds: 10 periodSeconds: 30 --- # Horizontal Pod Autoscaler apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: laravel-queue-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: laravel-queue minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 |
Моніторинг та логування
Prometheus metrics для черг
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// app/Console/Commands/QueueMetrics.php use Prometheus\CollectorRegistry; use Prometheus\RenderTextFormat; class QueueMetrics extends Command { public function handle() { $registry = new CollectorRegistry(new RedisAdapter()); $queueSize = $registry->getOrRegisterGauge('laravel', 'queue_size', 'Queue size'); $queueSize->set(Queue::size(), ['queue' => 'default']); $failedJobs = $registry->getOrRegisterGauge('laravel', 'failed_jobs', 'Failed jobs count'); $failedJobs->set(DB::table('failed_jobs')->count()); $renderer = new RenderTextFormat(); echo $renderer->render($registry->getMetricFamilySamples()); } } |
Structured logging
|
1 2 3 4 5 6 7 8 9 10 11 12 |
services: queue-worker: build: . command: php artisan queue:work --verbose environment: LOG_CHANNEL: stderr logging: driver: "json-file" options: max-size: "10m" max-file: "3" |
ELK Stack для логів
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.14.0 environment: - discovery.type=single-node logstash: image: docker.elastic.co/logstash/logstash:7.14.0 volumes: - ./logstash/pipeline:/usr/share/logstash/pipeline kibana: image: docker.elastic.co/kibana/kibana:7.14.0 ports: - "5601:5601" depends_on: - elasticsearch |
Кращі практики та troubleshooting
Health checks
|
1 2 3 4 |
# В Dockerfile HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD php artisan queue:monitor --queue=default || exit 1 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// artisan команда для health check class QueueMonitor extends Command { public function handle() { $size = Queue::size(); $failed = DB::table('failed_jobs')->where('created_at', '>', now()->subHour())->count(); if ($failed > 10) { $this->error("Too many failed jobs: {$failed}"); return 1; } $this->info("Queue healthy. Size: {$size}, Failed: {$failed}"); return 0; } } |
Graceful shutdown
|
1 2 3 4 5 6 7 8 9 10 |
// config/queue.php 'connections' => [ 'redis' => [ // ... 'block_for' => null, 'after_commit' => false, 'pcntl_timeout' => 60, ], ], |
|
1 2 3 4 5 6 7 8 9 |
# В контейнері обробляти SIGTERM trap 'kill -TERM $PID' TERM INT php artisan queue:work & PID=$! wait $PID trap - TERM INT wait $PID EXIT_STATUS=$? |
Ресурсні обмеження
|
1 2 3 4 5 6 7 8 9 10 11 |
services: queue-worker: deploy: resources: limits: cpus: '0.5' memory: 512M reservations: cpus: '0.25' memory: 256M |
Порівняння підходів
| Підхід | Переваги | Недоліки | Підходить для |
|---|---|---|---|
| Systemd на сервері | Простота для монолітних додатків | Важко масштабувати, single point of failure | Невеликі додатки, legacy системи |
| Docker Compose | Швидке налаштування, легкий development | Обмежене масштабування | Development, staging, невеликий production |
| Kubernetes | Автоскейлінг, high availability | Складність налаштування | Enterprise, high-load системи |
| Cloud черги (SQS) | Zero maintenance, автоскейлінг | Vendor lock-in, додаткові витрати | Production з непередбачуваним навантаженням |
Міграція з Systemd на контейнери
Поетапний план:
- Аналіз поточних черг – які типи завдань, навантаження
- Вибір черги – Redis/RabbitMQ/SQS
- Контейнеризація – створити Dockerfile та docker-compose
- Тестування – порівняти performance з поточною системою
- Поступовий rollout – перенести частину трафіку
- Моніторинг – налаштувати метрики та алерти
- Повний перехід – вимкнути старі systemd сервіси
Checklist для переходу:
- [ ] Queue drivers працюють в контейнерах
- [ ] Налаштовано persistent volumes для даних
- [ ] Health checks для всіх воркерів
- [ ] Логування централізовано
- [ ] Metrics відправляються в monitoring
- [ ] Backup стратегія для черг
- [ ] Disaster recovery процедури
- [ ] Load testing пройшов успішно
Висновок
Контейнерний підхід до Laravel черг дає значно більше переваг ніж традиційний Systemd:
- Масштабованість – кожен тип воркера масштабується незалежно
- Надійність – ізоляція збоїв та автоматичне відновлення
- Простота управління – декларативна конфігурація
- Переносимість – однакова поведінка на dev/staging/production
- Екосистема – інтеграція з современними DevOps інструментами
Для нових проектів рекомендується одразу використовувати контейнерний підхід, а існуючі системи поступово мігрувати з монолітних серверів на мікросервісну архітектуру.