Обычно в статьях про архитектуру авторы с умным видом показывают красивые схемы, рассказывая, как они с первого дня всё спроектировали по канонам чистого кода и микросервисов.
Мы будем честны: наша архитектура родилась из боли, хаоса и горящих дедлайнов.
Это история о том, как у нас исторически сложились три совершенно разных AI-сервиса, каждый из которых жил в своей параллельной вселенной. И о том, как одна безобидная просьба бизнеса заставила нас снести всё до основания, чтобы пересобрать этот «франкенштейн» в единую, монолитную по духу, но микросервисную по факту платформу на одном сервере с K3s и двумя NVIDIA H100.
Своим опытом разработки и интеграции AI-сервисов поделился Богдан Александрук — специалист по обработке данных отдела разработки интеллектуальных систем.
Усаживайтесь поудобнее, это будет одна большая история великого слияния.
Эпоха удельных княжеств
Всё началось с того, что ИИ стал модным. Бизнес приносил новые и новые идеи каждые пару месяцев. Так у нас родились три MVP:
- agent-kp — генератор коммерческих предложений.
- vks-api — транскрибатор созвонов (загружаешь аудио, получаешь протокол встречи).
- docpro — умный обработчик и анализатор документов.
Поскольку делали их в разное время (и иногда разные люди), каждый сервис представлял собой суверенное государство:
- Хранение файлов: agent-kp хранил сгенерированные файлы прямо на локальном диске в Docker-контейнере (да, при рестарте пода всё превращалось в тыкву). vks-api складывал аудио в свой отдельный S3, а docpro вообще держал всё в памяти и базе данных.
- Авторизация: В одном сервисе был хардкод-токен, во втором прикрутили базовый JWT, в третьем — Basic Auth.
- Уведомления: agent-kp слал письма на почту, когда КП было готово. vks-api пытался держать WebSocket, который отваливался каждые пять минут.
Все жили счастливо, пока сервисами пользовались разные отделы. Но затем настал день X.
«Простая фича», сломавшая хребет архитектуре
К нам пришел продакт-менеджер и сказал:
«Ребята, у нас же всё готово! Давайте сделаем так: менеджер загружает запись встречи с клиентом в vks-api, мы делаем транскрипцию, и на основе этого текста agent-kp АВТОМАТИЧЕСКИ генерирует коммерческое предложение и присылает уведомление!»
Звучит логично? Для бизнеса — да. Для нас это означало катастрофу.
Чтобы реализовать этот флоу, vks-api должен был как-то авторизоваться в agent-kp, скачать свой файл из S3, перегнать его по HTTP в agent-kp, чтобы тот сохранил его на свой локальный диск. А потом agent-kp должен был как-то уведомить фронтенд vks-api, что всё готово.
Мы попытались написать скрипт-интеграцию (point-to-point). Получился спагетти-монстр. Сервисы начали дергать API друг друга, падать по таймаутам, токены протухали, а если в процессе падала сеть — генерация КП зависала навсегда, но GPU при этом продолжала молотить на 100%, сжигая ресурсы.
Стало ясно: у нас нет трёх сервисов. У нас есть три кучи легаси, которые дерутся за две карточки H100. Нам нужна была Платформа.
Великое слияние (Хирургия наживую)
Мы остановили продуктовую разработку и начали резать. Концепция была простой: бизнес-сервисы должны стать «глупыми». Они не должны знать, как хранить файлы, как проверять токены и как слать уведомления. Всё это должно стать общим ядром — platform-core.
Шаг 1: Убиваем локальные диски (Workspace Service)
Первым делом мы вырвали с корнем работу с файлами из всех трёх сервисов.
Мы создали Workspace Service — единую точку работы с документами (поверх нормального MinIO и PostgreSQL).
Теперь, когда vks-api загружает аудио, он просто отдаёт его в ядро по gRPC и получает document_id (UUID).
Когда нужно сгенерировать КП, он больше не пересылает мегабайты текста по HTTP. Он просто отправляет в agent-kp сообщение: "Сделай КП на основе документа uuid-1234". agent-kp сам идёт по gRPC в Workspace Service и забирает нужные данные.
Файлы перестали дублироваться. Исчезли проблемы с дисками. Появились единые рабочие пространства (воркспейсы) для пользователей.
Шаг 2: Единая шина событий вместо франкенштейна нотификаций
Помните, как каждый сервис слал уведомления по-своему? Мы снесли всё это и написали единый Event Service.
Мы заставили все бизнес-сервисы общаться с миром через один унифицированный gRPC-клиент (lazy singleton, чтобы не падать при рестартах).
Теперь, когда agent-kp заканчивает работу, его код выглядит так:
# Один вызов, который делает всё
await EventClient.get().emit(
user_id=user_id,
action="workflow_complete",
message=f"КП «{doc_name}» сформировано",
category="result" # ⬅ Вся магия здесь
)
Смотрите, что происходит под капотом, когда ядро видит category="result":
- Оно сохраняет лог в PostgreSQL для аналитики.
- Оно АВТОМАТИЧЕСКИ генерирует WebSocket-ивент agent-kp.completed.
- Оно пушит этот ивент в единый Redis Pub/Sub.
- Единственный Notification Service ловит его и зажигает красный бейдж 🔴 в UI пользователя.
Больше никаких отваливающихся сокетов в каждом MVP. Один сокет на всю платформу.
Шаг 3: Выгоняем авторизацию за дверь кластера
Чтобы сервисы могли общаться между собой от имени пользователя, нам нужен был единый формат аутентификации.
Мы снесли хардкод-токены и JWT-валидаторы из бизнес-кода. Поставили на вход Traefik и OAuth2-Proxy.
Теперь разработчику docpro вообще без разницы на авторизацию. Если запрос дошел до его FastAPI-ручки, значит, Traefik уже проверил cookie, сходил в Keycloak, убедился, что права есть, и проксировал запрос, добавив безопасный заголовок:
X-Company-User-Id: ivan.petrov
Внутри кластера сервисы ходят друг к другу по внутреннему DNS K8s вообще без токенов. Доверенный периметр.
Шаг 4: Разнимаем драку за GPU (Temporal)
Когда все сервисы объединились и начали активно делиться файлами, они стали запускать AI-задачи одновременно. И наши H100 предсказуемо захлебнулись по памяти (CUDA Out of Memory).
Чтобы они не передрались, мы ввели диктатуру — оркестратор Temporal.
Мы запретили vks-api и agent-kp напрямую стучаться в vLLM. Теперь они просто кладут задачи в единую gpu-queue. Temporal Worker, зная физические лимиты видеокарты, берет задачи строго по очереди.
Да, пользователю иногда приходится подождать лишнюю минуту. Но зато сервер работает со 100% стабильностью, а если внутри инференса случается сбой, Temporal молча перезапускает Activity, и пользователь даже не замечает, что что-то шло не так.
Прекрасный лебедь (Итоги)
Процесс слияния был болезненным. Пришлось переписать сотни строк кода, сбросить старые базы и перевести пользователей на новый единый UI (который, к слову, собирается динамически через наш App Registry, опрашивающий K8s-аннотации).
Но результат стоил каждой пролитой капли пота. Что мы имеем сейчас:
- Из трёх сервисов получился один организм. Это больше не разрозненные утилиты. Менеджер может загрузить договор в воркспейс, docpro вытащит из него суть, а agent-kp нажмет на эту суть и сделает коммерческое. Всё это — через единый интерфейс, с единой авторизацией и единым хранилищем.
- Скорость разработки взлетела в космос. Когда бизнесу понадобился четвертый AI-сервис, мы написали его за 2 дня. Разработчику нужно было написать только промпт и бизнес-логику. Файлы лежат в ядре, квоты считает ядро, уведомления шлет ядро.
- Стабильность H100. Благодаря Temporal мы выжимаем из наших 160 ГБ видеопамяти максимум, не боясь поймать OOM.
Иногда, чтобы сделать шаг вперед, нужно признать, что твои MVP превратились в костыльного монстра, взять топор и снести их до фундамента. Зато теперь наш кластер на K3s — это настоящая enterprise-платформа, которая готова к любым фантазиям бизнеса.
Если у вас есть вопросы по тому, как мы выносили файлы в единый Workspace, настраивали связку Traefik + OAuth2-Proxy или мирили сервисы в очередях Temporal — задавайте в комментариях, будем рады обсудить!