AR-обробка відео на 11 мікросервісах: Docker, RabbitMQ і GPU, який постійно падав
Ідея здавалась простою, завантажуєш відео і отримуєш відео з 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.
Корисні ресурси
- NVIDIA Container Toolkit: docs.nvidia.com/datacenter/cloud-native/container-toolkit
- RabbitMQ best practices: rabbitmq.com/docs/production-checklist
- TensorFlow.js Node GPU: github.com/tensorflow/tfjs/tree/master/tfjs-node
- Датасет для навчання: kaggle.com → car-lights-sharif
- Код проєкту: github.com/Gdymora/ar-microservices
Є питання по архітектурі або проблеми з GPU — пишіть у коментарі.
Сподобалась стаття? Підписуйтесь на автора, щоб отримувати сповіщення про нові публікації на пошту.
1 коментар
Додати коментар Підписатись на коментаріВідписатись від коментарів