AR-обробка відео на 11 мікросервісах: Docker, RabbitMQ і GPU, який постійно падав

💡 Усі статті, обговорення, новини про DevOps — в одному місці. Приєднуйтесь до DevOps спільноти!

Ідея здавалась простою, завантажуєш відео і отримуєш відео з AR-ефектами. Фари автомобіля підсвічуються, об’єкти розпізнаються, 3D-моделі накладаються в правильній перспективі. Але швидко стало зрозуміло: кожен компонент вимагає свого інструменту.

Звісно ж, хотілось Node.js для всього, але з OpenCV там тре заморочитись, а от в Python OpenCV легко і навчання моделей там також майже з коробки і знайомий з цим вже якісь час. FFmpeg, API і рендеринг зроблю на Node.js. TensorFlow.js буде з GPU в окремому контейнері з CUDA. MongoDB для 3D-активів. RabbitMQ щоб ці всі сервіси спілкувалися асинхронно. Все це в одному процесі не живе тож звідси випливає мікросервісна архітектура. Та давайте усе по порядку.

У цій статті я розповім про реальну архітектуру цікавого проєкту. Не «hello world», а систему з 11 сервісів, спільними Docker volumes, чергами повідомлень і GPU-контейнером, яка ну ніяк не хотіла стартувати.

High-level architecture of the AR video processing pipeline

Pipeline обробки:

1️⃣ користувач завантажує відео

2️⃣ FFmpeg розбиває його на кадри

3️⃣ OpenCV аналізує геометрію сцени

4️⃣ TensorFlow виконує детекцію об’єктів

5️⃣ Three.js рендерить 3D-ефекти

6️⃣ Composite накладає AR на кадри

7️⃣ FFmpeg збирає фінальне відео

Інфраструктура:

  • RabbitMQ — асинхронний конвеєр
  • Redis — кеш статусів
  • MongoDB — 3D-активи
  • Shared volume — зберігання кадрів

GitHub: Код проєкту: github.com/Gdymora/ar-microservices

Частина 1. Архітектура: 11 сервісів і чому саме так

Сервіси і їх стек

Система складається з 11 Docker-контейнерів, кожен зі своєю відповідальністю:

Сервіс

Порт

Стек і функція

Nginx

:8888

Reverse proxy, балансування, SSL-термінація

API Gateway

:3000

Node.js + Express. Маршрутизація, Swagger, JWT

Video Service

:4000

Node.js + fluent-ffmpeg. Розбиття на кадри, збірка відео

OpenCV Service

:5000

Python + OpenCV. AR-маркери, геометрія сцени, матриці проєкції

TensorFlow Service

:6000

Node.js + TF.js + NVIDIA GPU. Детекція об’єктів, аналіз сцени

Render Service

:7000

Node.js + Three.js. 3D-рендеринг: CPU / GPU / WebGL рендерери

Composite Service

:8000

Node.js + Canvas. Фінальне накладання AR на кадри

Asset Service

:27017

MongoDB. 3D-моделі, текстури, конфігурації AR-ефектів

Redis

:6379

In-memory кеш статусів обробки та проміжних результатів

RabbitMQ

:5672

Брокер повідомлень. Асинхронна комунікація між сервісами

Model Training

profile

Python + TensorFlow. Навчання моделей. Docker profile: training

Схема взаємодії

Потік даних рухається по конвеєру де кожен сервіс обробляє кадр і передає результат наступному через RabbitMQ:

The system is organized as an asynchronous processing pipeline where services communicate through RabbitMQ while sharing frame data via a shared Docker volume.

Паралельно: Asset Service (MongoDB) надає 3D-моделі і текстури, Redis кешує статуси обробки.

Чому не монолітний підхід

Перша версія з одним процесом Node.js, який намагався робити все. Проблеми стали очевидні одразу:

  • OpenCV на Python, а не Node.js. Нативні bindings для Node.js нестабільні і відстають від Python-версії за функціоналом.
  • TensorFlow.js + GPU вимагає CUDA-залежностей. Тягнути їх у кожен сервіс , ну це явно безглуздо.
  • FFmpeg — CPU-hungry операція. Коли він розбиває відео на кадри, він забирає все CPU і гальмує TF-інференс.
  • Незалежне масштабування: 1000 кадрів у черзі — масштабуємо тільки TF і OpenCV, а не всю систему.
  • docker compose up —scale opencv-service=3 — і все, горизонтальне масштабування без жодних змін у коді.

Принцип: Мікросервіси тут наслідок реальних технічних обмежень, а не данина моді. Тож, Python майже всюди, Node.js там де потрібен async I/O.

Частина 2. Docker Compose і спільний volume

Спільний volume ar-storage, як ключове рішення

Всі сервіси монтують один volume ar-storage. Це критично: сервіси не передають кадри через повідомлення (це сотні MB), а лише шляхи до файлів:

volumes:
  ar-storage:   # спільне сховище для всіх сервісів
  mongo-data:   # персистентні дані MongoDB
 
# Кожен сервіс монтує:
volumes:
  - ar-storage:/storage
 
# Структура /storage/:
# uploads/{videoId}/            ← завантажені відео
# temp/{videoId}/               ← кадри після FFmpeg
# processed/{videoId}/frames/   ← оброблені кадри
# processed/{videoId}/tensorflow/  ← результати TF
# processed/{videoId}/metadata/    ← JSON метадані
# output/{videoId}.mp4             ← готове відео

Важливо: Не передавайте Base64-encoded кадри через RabbitMQ це катастрофа для пам’яті і пропускної здатності. Shared volume з шляхом у повідомленні, на мою думку правильний підхід.

Healthcheck для RabbitMQ — обов’язковий

depends_on без condition: service_healthy не гарантує що сервіс готовий, а говорить про те, що контейнер запустився. RabbitMQ підіймається ~5 секунд після старту контейнера. Без healthcheck всі сервіси падали при старті:

rabbitmq:
  image: rabbitmq:3-management-alpine
  healthcheck:
    test: ["CMD", "rabbitmqctl", "status"]
    interval: 5s
    timeout: 10s
    retries: 5
 
video-service:
  depends_on:
    rabbitmq:
      condition: service_healthy  # чекаємо поки RabbitMQ готовий

TensorFlow Service: GPU + graceful degradation

Ключові налаштування сервісу, щоб він працював і з GPU і без нього:

tensorflow-service:
  environment:
    - NVIDIA_VISIBLE_DEVICES=all
    - NVIDIA_DRIVER_CAPABILITIES=compute,utility
    - TF_FORCE_GPU_ALLOW_GROWTH=true
    - TF_FORCE_ENABLE_CPU_NOAVX=1  # для CPU без AVX-інструкцій
    - TF_CPP_MIN_LOG_LEVEL=0        # детальне логування
  deploy:
    resources:
      reservations:
        devices:
          - driver: nvidia
            count: 1
            capabilities: [gpu]
            options:
              failonmissingcuda: false  # не падати без GPU

Model Training, як окремий Docker profile

Навчання моделі не запускається разом з основним стеком, а тільки коли нам потрібно (для цього використовуємо наступну конфігурацію):

model-training-service:
  profiles:
    - training   # запускається тільки з --profile training
 
model-testing-service:
  profiles:
    - testing
 
# Запуск навчання:
docker compose --profile training up model-training-service
 
# Тестування моделі:
docker compose --profile testing up model-testing-service
 
# Звичайний старт системи (без training/testing):
docker compose up -d

Це дає можливість навчати нову версію моделі не зупиняючи основну систему, і деплоїти її через Model Manager без рестарту контейнера.

Частина 3. Конвеєр обробки через RabbitMQ

П’ять черг — п’ять стадій

Черга RabbitMQ

Відправник

Отримувач

video-frames

Video Service

OpenCV Service

tensorflow-processing

OpenCV Service

TensorFlow Service

render-processing

TensorFlow Service

Render Service

composite-processing

Render Service

Composite Service

video-frames-done

Composite Service

Video Service (збірка)

Кожен сервіс в нас це незалежний consumer черги. Повідомлення містить тільки метадані, не сам кадр:

// Приклад повідомлення у черзі tensorflow-processing:
{
  videoId:          "acc5b6c2-ddcb-4b5a-97ea-eb96468909b7",
  frameIndex:       42,
  framePath:        "/storage/temp/{videoId}/frame_042.png",
  opencvOutputPath: "/storage/processed/{videoId}/opencv/frame_042.png",
  opencvMetadata: {
    markers:          [...],       // AR-маркери знайдені OpenCV
    projectionMatrix: [...],       // матриця проєкції камери
    sceneGeometry:    { ... }      // геометрія сцени
  }
}

TensorFlow Service: основний orchestrator

TensorFlow Service тут найскладніший компонент. Він не просто запускає інференс, а координує весь потік між сервісами:

// tensorflow-service/src/services/tensorflow-service.js
class TensorFlowService {
  async initialize() {
    await this.objectDetector.initialize();  // завантаження моделі
    await this.rabbitmqService.connect();
    await this.startProcessing();
  }
  async processFrame(frameInfo) {
    // якщо OpenCV обробив — беремо його результат, інакше оригінал
    const inputPath = frameInfo.opencvOutputPath || frameInfo.framePath;
    const { tensor } = await this.imageProcessor.preprocessImage(inputPath);
    const detections  = await this.objectDetector.detectObjects(tensor);
    const sceneContext = this.analyzeSceneContext(detections);
    // → тип сцени: "traffic", "crowd" або "mixed"
    // зберігаємо анотоване зображення і JSON метадані
    const annotatedPath = await this.imageProcessor
      .visualizeDetections(inputPath, detections, outputDir);
    await fs.writeFile(metadataPath, JSON.stringify({
      detections, sceneContext, frameIndex: frameInfo.frameIndex
    }));
    // передаємо далі у render-processing
    await this.rabbitmqService.publish("render-processing", {
      ...frameInfo,
      tensorflowOutputPath: annotatedPath,
      tensorflowMetadata: { detections, sceneContext }
    });
  }
}

Render Service: фабрика рендерерів

Render Service вибирає рендерер залежно від доступних ресурсів — GPU, WebGL або CPU:

// render-service/src/core/renderer-factory.js
class RendererFactory {
  create(config) {
    if (config.gpu.cudaAvailable)   return new GpuRenderer(config);
    if (config.gpu.webglAvailable)  return new WebGLRenderer(config);
    if (config.rendering.svgEnabled) return new SvgRenderer(config);
    return new CpuRenderer(config);  // завжди є fallback
  }
}

Чотири рендерери (cpu-renderer.js, gpu-renderer.js, svg-renderer.js, webgl-renderer.js) дають системі можливість запуститися на будь-якому залізі. На dev-машині без GPU це CPU рендерер. У продакшні це CUDA або WebGL.

Частина 4. Найбільший головний біль: NVIDIA GPU в Docker

Проблема 1: відсутність AVX інструкцій

TensorFlow.js вимагає AVX-інструкцій процесора. На деяких серверних конфігураціях або у VM вони вимкнені. Як наслідок контейнер падає при старті з Illegal instruction. Рішення одне — змінна середовища:

environment:
  - TF_FORCE_ENABLE_CPU_NOAVX=1

Проблема 2: NVIDIA Container Toolkit vs nvidia-docker2

Це різні речі, і плутанина між ними це джерело більшості помилок. nvidia-docker2 застаріло, сучасний підхід це Container Toolkit:

# Крок 1: встановлення NVIDIA Container Toolkit
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
  | sudo gpg --dearmor \
  -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list \
  | sed 's#deb https://#deb [signed-by=...keyring.gpg] https://#g' \
  | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
# Крок 2: конфігурація Docker runtime
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
# Крок 3: перевірка
docker info | grep -i nvidia
# Повинно вивести: Runtimes: io.containerd.runc.v2 nvidia runc
# Крок 4: тест
docker run --rm --gpus all nvidia/cuda:12.4.0-runtime-ubuntu22.04 nvidia-smi

Типова помилка: could not select device driver with capabilities: [[compute utility]] — Docker не бачить NVIDIA runtime. docker info | grep nvidia — якщо порожньо, Container Toolkit не налаштований.

Проблема 3: версії CUDA

Версія CUDA в образі має бути не новіша ніж версія драйвера на хості. Перевірити максимальну підтримувану версію:

# Показує максимальну версію CUDA для вашого драйвера
nvidia-smi
# Рядок "CUDA Version: 12.4" → можна використовувати образи до 12.4
# Список доступних образів:
# https://hub.docker.com/r/nvidia/cuda/tags
# Тест конкретної версії:
docker run --rm --gpus all nvidia/cuda:12.4.0-runtime-ubuntu22.04 nvidia-smi

Корисні команди для дебагу GPU

# GPU всередині контейнера
docker exec -it ar-microservices-tensorflow-service-1 nvidia-smi
# Чи завантажився TF.js з GPU
docker compose exec tensorflow-service node -e \
  "require('@tensorflow/tfjs-node-gpu'); console.log('GPU OK')"
# Перевірка OOM-кілера (якщо контейнер несподівано падає)
sudo journalctl -u docker.service | grep -i oom
# Пам'ять GPU
docker exec -it ar-microservices-tensorflow-service-1 \
  nvidia-smi --query-gpu=memory.used,memory.free --format=csv

Частина 5. Навчання моделі детекції фар

COCO-SSD vs кастомна модель

Стандартна COCO-SSD модель розпізнає car, але не headlight. Є два шляхи:

  • Шлях А: @tensorflow-models/coco-ssd — швидко, API простий, але ви отримаєте лише bbox автомобіля. Render Service мусить сам розраховувати позицію фар евристично.
  • Шлях Б: кастомна модель на датасеті car-lights-sharif з Kaggle — складніше, але розпізнає фари напряму.

💡 Компроміс: COCO-SSD як проміжний варіант і це ефективна стратегія. Запускаєте систему, перевіряєте конвеєр, потім підключаєте кастомну модель.

Model Training Service: структура

Сервіс навчання — окремий Docker-контейнер з Python та TensorFlow:

model-training-service/
  src/
    train.py              ← головний скрипт навчання
    convert.py            ← конвертація у TF.js формат
    utils/
      data_loader.py      ← завантаження TFRecord датасетів
      model_config.py     ← гіперпараметри
    models/
      headlight_detector/
        model.py          ← архітектура моделі
        config.json       ← конфігурація
# Датасет (монтується у volume):
car-lights-sharif/
  train/  *.tfrecord   ← 80% даних
  valid/  *.tfrecord   ← 10%
  test/   *.tfrecord   ← 10%
# Результат навчання:
models/headlight_detector/
  saved_model/   ← для Python-сервісу
  web_model/     ← для Node.js TF.js сервісу

ModelManager: гаряче оновлення моделі

TensorFlow Service слідкує за файлами моделі і перезавантажує її без рестарту контейнера:

// tensorflow-service/src/services/model-manager.js
class ModelManager {
  async loadModel(modelType, config) {
    const model = new HeadlightDetector(config);
    await model.initialize();
    this.models[modelType] = model;
    if (config.watchForChanges && config.modelPath) {
      const modelDir = path.dirname(config.modelPath);
      fs.watch(modelDir, async (eventType, filename) => {
        if (filename?.endsWith('.json')) {
          console.log(`Model ${filename} changed, reloading...`);
          await this.reloadModel(modelType, config);
        }
      });
    }
    return model;
  }
}

Це дає zero-downtime деплой нової версії моделі: навчили → конвертували → скопіювали у web_model/ → Model Manager підхопив автоматично.

Частина 6. Дебаг і моніторинг

Основні команди

# Логи конкретного сервісу
docker logs ar-microservices-tensorflow-service-1 --follow
docker compose logs video-service
docker compose logs opencv-service
# Статус черг RabbitMQ
# http://localhost:15672  логін: ar_user / ar_password
# Черги: video-frames, tensorflow-processing, render-processing...
# Файли у volume
docker exec -it ar-microservices-tensorflow-service-1 find /storage -name '*.png' | wc -l
docker exec -it ar-microservices-tensorflow-service-1 du -sh /storage/*
# Скопіювати результати локально
docker cp ar-microservices-render-service-1:/storage/processed/ ./temp

Типові проблеми і рішення

Кадри розбиті, але черга порожня

Симптом: Video Service розбив відео на кадри, але черга tensorflow-processing порожня.

Причина найчастіше — OpenCV Service не підключився до RabbitMQ при старті (залежність без healthcheck).

docker logs ar-microservices-opencv-service-1
# Шукаємо 'Connection refused'
docker-compose restart opencv-service
# Перевіряємо чи FFmpeg завершив роботу:
docker exec ar-microservices-video-service-1 ps aux | grep ffmpeg

Контейнер падає без помилки

Швидше за все, OOM-кілер. TensorFlow і рендеринг потребують пам’яті:

sudo journalctl -u docker.service | grep -i oom
# Встановлюємо ліміти:
deploy:
  resources:
    limits:
      memory: 2G
    reservations:
      memory: 512M

Повне очищення і ребілд

# Зупинити і видалити volumes (УВАГА: видалить дані MongoDB!)
docker compose down -v
# Очистити кеш білдів
docker builder prune -f
# Видалити образи проєкту
docker images | grep -E '(tensorflow|opencv|video-service|render|composite)' \
  | awk '{print $3}' | xargs docker rmi -f
# Ребілд конкретного сервісу без кешу
docker compose build --no-cache tensorflow-service
docker compose up -d tensorflow-service

Висновки

Що реально виніс з цього проєкту

  • Мікросервіси це не архітектурна мода. Для системи, де Python-компонент, GPU-контейнер і навантажений CPU, і FFmpeg мають жити поряд, як на мене, це єдиний розумний вибір.
  • RabbitMQ healthcheck — перша і найчастіша помилка у всіх хто бере цей стек. Без condition: service_healthy сервіси стартують раніше за брокер.
  • Shared volume замість передачі файлів у повідомленнях. Передавати Base64-кадри через RabbitMQ — катастрофа для пам’яті.
  • NVIDIA Container Toolkit ≠ nvidia-docker2. nvidia-docker2 архівовано. Сучасний підхід — тільки Toolkit + nvidia-ctk runtime configure.
  • failonmissingcuda: false — must have. Дозволяє запускати і тестувати систему без GPU.
  • Docker profiles для ML-сервісів. Model training не повинен конкурувати за GPU з inference.
  • RendererFactory з чотирма рендерерами — CUDA/WebGL/SVG/CPU. Система запускається на будь-якому залізі, просто з різною швидкістю.
  • ModelManager з fs.watch() — zero-downtime деплой нової версії моделі без рестарту сервісу.

Що в планах

  • Замінити поллінг статусу відео на WebSocket — фронтенд отримує оновлення в реальному часі.
  • Kubernetes конфігурація для продакшн-деплою з автомасштабуванням.
  • Навчити кастомну модель для точнішої детекції фар при різному освітленні.
  • Замінити headless Three.js на Babylon.js — стабільніша підтримка headless WebGL.

Корисні ресурси

Є питання по архітектурі або проблеми з GPU — пишіть у коментарі.

Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.

👍ПодобаєтьсяСподобалось6
До обраногоВ обраному3
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Мікросервіси — не єдиний варіант для таких задач.

Якщо основний бекенд на Java, Python/JS можна виконувати прямо всередині JVM через GraalVM без винесення в окремі сервіси (без мережевих викликів, простіший деплой, менше інфраструктури).

Зробив під це невелику лібу github.com/ih0r-d/polyglot-platform (уніфікований API для запуску Python/JS з Java + контроль execution).

Звісно, це не silver bullet і залежить від конкретних бібліотек (особливо якщо є native залежності), але як альтернатива частині мікросервісів — цілком робочий підхід.

Підписатись на коментарі