Назад к блогу

9 микросервисов Tirebase: что делает каждый и зачем он отдельный

Реальная архитектура Tirebase — от API-агрегатора до маркетплейсов, CRM, системы уведомлений и Telegram-ботов. Каждый сервис решает конкретную боль.

9 микросервисов Tirebase: что делает каждый и зачем он отдельный

Tirebase — IT-платформа для шинного бизнеса: пять физических точек в Петербурге, десяток поставщиков, присутствие на трёх маркетплейсах. Я отвечаю за всю техническую часть — от инфраструктуры до кода. Сейчас это экосистема из девяти сервисов, каждый из которых появился не из архитектурного перфекционизма, а потому что без него что-то конкретное не работало.

Общая картина

Прежде чем разбирать каждый сервис — вот как всё связано:

                          ┌─────────────────────────────────────┐
                          │          Маркетплейсы               │
                          │   Avito    Ozon (x3)   Wildberries  │
                          └──────▲──────▲──────────▲────────────┘
                                 │      │          │
                          XML    │  YML/API  WB SDK│
                          feeds  │      │          │
                          ┌──────┴──────┴──────────┴────────────┐
                          │     AUTOLOAD ENGINE                  │
                          │  (autoload.tirebase.ru)              │
                          │  FastAPI + RabbitMQ Worker            │
                          │  Pricing Engine │ Feed Generator      │
                          └──────┬──────────┬───────────────────┘
                                 │          │
                     REST API    │          │  RabbitMQ
                                 │          │  (feed.*.generate)
         ┌───────────────────────┤          │
         │                       │          │
         ▼                       ▼          ▼
  ┌──────────────┐   ┌──────────────┐  ┌──────────┐
  │  API          │   │  ERP (1C)    │  │ RabbitMQ │
  │ api.tirebase  │   │  + PIM       │  │          │
  │  GET /tires   │   │              │  └────┬─────┘
  │  GET /wheels  │   └──────┬───────┘       │
  └──────┬────────┘          │               │
         │                   │               ▼
         │              REST │       ┌───────────────┐
         ▼                   │       │  EVC           │
  ┌──────────────┐           │       │ evc.tirebase   │
  │  SEARCH       │          │       │ FastStream +   │
  │ search.tirebase│◄────────┘       │ WebSocket      │
  │ Vue 3 + TS    │                  └───────┬────────┘
  │ PWA + Dexie   │                          │
  └──────┬────────┘                  WS Push │
         │                                   │
         │ Cart → CRM                        ▼
         │                           ┌───────────────┐
         ▼                           │  SEARCH / TS   │
  ┌──────────────┐                   │  (браузер)     │
  │  OCRM         │                  └────────────────┘
  │ ocrm.tirebase │
  │ CRM + Avito   │   ┌──────────┐   ┌──────────┐   ┌──────────┐
  │ б/у шины      │   │  BSCAN   │   │   PDF    │   │   BOT    │
  └───────────────┘   │  CRPT    │   │  HTML→PDF│   │ Telegram │
                      │ DataMatrix│   │ port 8888│   │ pyTBA    │
                      └──────────┘   └──────────┘   └──────────┘

Каждый прямоугольник — это отдельный деплой, отдельный репозиторий, часто — отдельная база данных. Связи между ними — REST API, RabbitMQ и WebSocket. Никакого общего монолита.

API: фундамент всего

api.tirebase.ru — самый первый сервис. REST API, который отдаёт актуальные данные по шинам и дискам. GET /tires с фильтрами по бренду, модели, ширине, высоте, диаметру, сезону, шипам, RunFlat, грузовым, индексам нагрузки и скорости. GET /wheels — аналогично для дисков. Плюс /tires/params и /wheels/params — фотографии, описания, характеристики.

Это тонкий слой поверх PostgreSQL, который знает структуру шинных данных. Все остальные сервисы ходят сюда за товарными данными. Если API лежит — лежит всё.

# Типичный ответ API
{
    "title": "Continental PremiumContact 7 205/55 R16 91V",
    "brand": "Continental",
    "model": "PremiumContact 7",
    "width": 205, "height": 55, "diam": 16,
    "season": "summer", "spike": false, "runflat": false,
    "quantity": 48,
    "storehouses": ["Московский пр.", "Лиговский пр."],
    "rrc_price": 8900, "opt_price": 6200, "retail_price": 7800
}

Почему отдельный сервис, а не часть Autoload Engine? Потому что API должен быть стабильным и быстрым. Он обслуживает Search, Bot, внешних партнёров. Autoload Engine при этом постоянно эволюционирует — новые маркетплейсы, новые алгоритмы. Если бы API жил внутри Autoload, каждый деплой с новой фичей для Ozon мог бы уронить поиск для менеджеров.

Autoload Engine: мозг маркетплейсов

autoload.tirebase.ru — самый сложный сервис в экосистеме. Python 3.12, FastAPI, SQLAlchemy 2.0 async, PostgreSQL через asyncpg, Redis 7.2, RabbitMQ. Управляет присутствием на трёх маркетплейсах: Avito, Ozon, Wildberries.

Две ключевые подсистемы работают как отдельные процессы. API Server (main.py) обрабатывает HTTP-запросы — управление товарами, настройки магазинов, мониторинг. Feed Worker (worker.py) слушает очереди RabbitMQ и генерирует фиды для маркетплейсов.

Зачем два процесса? Feed generation — CPU-bound операция. Генерация XML-фида для Avito на 5000 товаров занимает 10-15 секунд. Если делать это в том же процессе, что обрабатывает API-запросы, latency улетает. RabbitMQ развязал руки — API ставит задачу в очередь (feed.avito.generate, feed.ozon.generate), worker подхватывает когда может.

Pricing Engine

Ценообразование на маркетплейсах — не просто «оптовая + наценка». Трёхуровневый алгоритм:

def calculate_marketplace_price(opt_price: Decimal, category: str) -> PriceResult:
    # Уровень 1: Базовая наценка 18%
    base_price = opt_price * Decimal("1.18")

    # Уровень 2: Маржинальный буфер
    # +5% но не менее 500 руб для шин, 1000 руб для дисков
    margin = max(base_price * Decimal("0.05"),
                 Decimal("500") if category == "tire" else Decimal("1000"))
    sell_price = base_price + margin

    # Уровень 3: "Старая цена" — маркетплейс покажет перечёркнутую цену
    old_price = sell_price * Decimal("1.06")

    return PriceResult(
        price=round_to_market(sell_price),
        old_price=round_to_market(old_price),
    )

def round_to_market(price: Decimal) -> int:
    """8743 → 8790. Цена на 90 выглядит 'рыночной'."""
    return int(price) - int(price) % 100 + 90

Без минимального буфера дешёвые шины (бюджетные по 2000 руб) продавались бы с маржой 60 рублей — меньше стоимости упаковки. Округление к 90 — психология, 8743 выглядит «случайной», 8790 — «рыночной». Каждое число в алгоритме пришло из реального опыта торговли.

Умный выбор поставщика

Одна шина может быть у пяти поставщиков по разным ценам. Самая дешёвая — не всегда лучшая:

async def select_best_supplier(offers: list[SupplierOffer], min_stock: int = 16):
    # Минимальный остаток 16 штук — при меньшем высок шанс out-of-stock
    viable = [o for o in offers if o.stock >= min_stock]

    # Цена на 30%+ ниже средней — подозрительно (ошибка в прайсе)
    avg_price = sum(o.price for o in viable) / len(viable)
    viable = [o for o in viable if abs(o.price - avg_price) / avg_price < 0.30]

    # Надёжный поставщик с ценой чуть выше лучше ненадёжного с минимумом
    viable.sort(key=lambda o: float(o.price) * (1.1 - o.reliability_score * 0.1))
    return viable[0] if viable else None

Число 16 — эмпирическое. Если у поставщика 4 шины на складе, к моменту подтверждения заказа их купит кто-то другой. Reliability scoring — рейтинг по истории поставок: поставщик, который 3 раза из 10 сорвал сроки, проигрывает тому, кто на 3% дороже, но надёжен.

Фиды: три маркетплейса — три разных мира

Avito — XML-фиды, по одному на каждый из пяти магазинов. Менеджеры назначаются через itertools.cycle, описания рандомизируются из пула 15+ шаблонов — чтобы объявления выглядели «живыми», а не массовой публикацией. Avito понижает в выдаче аккаунты, которые выглядят как боты.

Ozon — три аккаунта, батчевый импорт по 100 позиций. Rate limit 50 req/min, решается простым asyncio.sleep(1.2) — пробовал token bucket, sliding window, но для batch import раз в 20 минут простой sleep надёжнее. offer_id через MD5 от brand:model:size — стабильный идентификатор, без него Ozon дважды создавал дубликаты.

Wildberries — пришлось написать свой SDK (wbapi/). Их API документация… неполная. TireCard — 11+ обязательных характеристик. Cursor pagination, batch sync по 1000 позиций, background upload медиа с прогрессом через Redis.

Кеширование и расписание

Три уровня: TTLCache в памяти (микросекунды), Redis через fastapi-cache2 (1-2 мс), готовые XML/YML на файловой системе. Nginx отдаёт файлы третьего уровня напрямую — Python не просыпается при каждом запросе маркетплейса.

Три шедулера: FeedScheduler — фиды раз в час, OzonFeedScheduler — каждые 20 минут (Ozon жёстче штрафует за неактуальные остатки), CacheScheduler — полный прогрев в 9:00 и 21:00 по Москве.

Search: рабочий инструмент менеджеров

search.tirebase.ru — Vue 3 + TypeScript + Vite + Tailwind CSS. PWA, работает офлайн. Версия 2.8.5 — это реально зрелый продукт, который каждый день используют менеджеры в пяти точках.

Три роли: менеджер по продажам, контент-менеджер, администратор. Менеджер ищет шины для клиента — мгновенная фильтрация по бренду, модели, сезону, размеру, поставщику. Три режима отображения: плоская таблица, группировка по бренду, подбор разноширинной комплектации (staggered fitment — когда передние и задние колёса разного размера, типичный запрос для BMW и Mercedes).

Фильтр по автомобилю — ввёл марку и модель, получил подходящие размеры, включая разноширинные конфигурации. Это экономит менеджеру минуты на каждом клиенте.

Offline-first архитектура была осознанным решением. Интернет в торговых точках бывает нестабильным. PWA с IndexedDB через Dexie — локальный кеш данных, фоновое обновление каждые 5 минут. Менеджер открывает приложение и сразу видит актуальный ассортимент, даже если WiFi только что отвалился.

Корзина с созданием заказа интегрирована с CRM. Менеджер набрал позиции — заказ уходит в систему. Excel-экспорт с rate limiting — чтобы никто не выгрузил весь прайс-лист одной кнопкой. Есть управление маркетплейсами: синхронизация Wildberries, подготовка данных для Ozon и Яндекс.Маркета.

EVC: нервная система

evc.tirebase.ru — Event Communication service. Python 3.13, FastAPI, FastStream + RabbitMQ, PostgreSQL, Redis. Микросервис, который я сначала думал не делать, а потом не мог без него жить.

Задача: когда Autoload Engine обновил цены на Wildberries, менеджер в Search должен увидеть уведомление. Когда CRM получила новый заказ — менеджер нужной точки должен узнать мгновенно. Когда поставщик обновил прайс — контент-менеджер должен увидеть это сейчас, а не через 5 минут при следующем обновлении.

Архитектура:

Autoload/CRM/другие сервисы

    RabbitMQ events.topic


  Notification Processor

  direct.user exchange


   WebSocket → браузер

Любой сервис кидает событие в RabbitMQ topic exchange. EVC подхватывает, определяет, кому отправить, и пушит через WebSocket. JWT-авторизация через ERP API с кешированием в Redis на 24 часа — не дёргаем ERP на каждый WebSocket connect.

Kubernetes-ready health checks — потому что это единственный сервис, который я планирую вынести в Kubernetes, если нагрузка вырастет. Остальные пока живут в Docker Compose. Не уверен, что Kubernetes здесь вообще нужен при текущих объёмах, но хочется быть готовым.

TS: шиномонтаж как POS-терминал

ts.tirebase.ru — система управления заказами шиномонтажа. Vue 3 + TypeScript + Pinia + Tailwind CSS. Интерфейс в стиле POS-терминала, оптимизированный под тач-скрин.

Менеджер шиномонтажа выбирает категории услуг, добавляет сервисы в корзину, выбирает рабочий пост (у нас несколько подъёмников). Управление сменами — кто сегодня работает, какая выручка по сменам. Backend — crm.tireshop.ru.

Почему отдельный сервис? Потому что шиномонтаж и торговля шинами — это разные бизнес-процессы с разными ролями, разным UX и разными циклами релизов. Менеджер шиномонтажа не должен видеть маркетплейсы, менеджер продаж не должен видеть подъёмники.

BSCAN: маркировка товаров

bscan.tireshop.ru — интеграция с системой «Честный знак» (CRPT). FastAPI, работа с DataMatrix-кодами. Генерация, валидация, проверка статусов через API CRPT. Поиск данных по ИНН через DADATA. Интеграция с PIM-системой.

Маркировка шин стала обязательной. Каждая шина должна иметь DataMatrix-код, его нужно проверить при приёмке, обновить статус при продаже. BSCAN автоматизирует весь цикл. Без него менеджеры вводили бы коды вручную на сайте CRPT — по 30 секунд на шину.

PDF: генерация документов

pdf.tirebase.ru — микросервис с одним эндпоинтом. POST /generate-pdf/ принимает HTML, возвращает PDF. Работает как systemd-сервис на порту 8888.

Зачем отдельный сервис? Генерация PDF — ресурсоёмкая операция, которая использует headless-браузер. Если бы это жило в API или в CRM, пиковая нагрузка на генерацию счетов могла бы замедлить всё остальное. Отдельный сервис на отдельном порту — изолированная нагрузка. Любой другой сервис может вызвать его через HTTP. Накладные, коммерческие предложения, акты — всё генерируется здесь.

BOT: Telegram-бот для клиентов

bot.tiredrop — Telegram-бот для поиска шин. pyTelegramBotAPI + Flask + SQLAlchemy + PostgreSQL. Клиент пишет размер «205/50/16» — бот возвращает предложения с ценами. Фильтры по сезону, ценовому диапазону, грузовые шины. Inline-кнопки, пагинация.

Этот бот появился из конкретной потребности: мелкие клиенты не хотят звонить, не хотят заходить на сайт. Они хотят написать размер в Telegram и получить ответ. Бот ходит в API за данными и отдаёт их в удобном формате.

OCRM: CRM для б/у шин

ocrm.tirebase.ru — FastAPI, PostgreSQL, Redis, Alembic. REST API для управления объявлениями б/у шин на Avito. Отдельная подсистема, потому что б/у шины — специфический бизнес-процесс. Другие данные (фотографии реального товара, не каталожные), другие правила ценообразования (оценка состояния), другие требования к объявлениям.

Как всё это общается

Три типа коммуникации:

REST API (синхронный). Search вызывает API для получения товаров. Autoload вызывает ERP для получения прайсов. Любой сервис вызывает PDF для генерации документов. Простая HTTP-связь, которую легко отлаживать и мониторить.

RabbitMQ (асинхронный). Autoload ставит задачи на генерацию фидов. EVC получает доменные события. Это development decoupling — отправитель не знает и не заботится, кто обработает сообщение.

WebSocket (real-time). EVC пушит уведомления в браузеры менеджеров. Единственный stateful протокол в системе, и он изолирован в одном сервисе.

Почему FastAPI, а не Django

Ни в одном из сервисов нет Django. Причина простая: ни одному не нужна админка. Каждый сервис — чистый API или SPA. FastAPI даёт async из коробки, автоматическую документацию через Pydantic, минимальный overhead. Когда у тебя десяток сервисов, каждый лишний мегабайт RAM и миллисекунда latency множатся.

SQLAlchemy 2.0 async с asyncpg — потому что ORM нужен (шинные данные — это десятки связанных таблиц), но синхронный ORM в async-мире — это боль. Redis — для кеширования и для pub/sub. RabbitMQ — для надёжной доставки задач.

Почему Vue 3 на фронтенде

Search и TS — оба на Vue 3 + TypeScript + Tailwind CSS. Composition API с <script setup> — компактный, типизированный, легко переиспользуемый код. Pinia для состояния. Tailwind — потому что при двух фронтенд-приложениях писать кастомный CSS бессмысленно.

PWA на Search — потому что менеджеры в торговых точках работают с планшетов и телефонов. IndexedDB через Dexie — потому что нужен офлайн. Vite — потому что Webpack в 2025 году — это мазохизм.

Аутентификация: единая точка входа

Все сервисы используют гибридную схему ERP + JWT. Авторизация происходит через ERP API (1C/Custom). JWT-токен с TTL 12 часов. OAuth2 Password Flow. Один логин — доступ ко всем системам. EVC кеширует JWT-валидацию в Redis на 24 часа, чтобы не нагружать ERP при каждом WebSocket heartbeat.

Что бы я сделал по-другому

Общий API Gateway. Сейчас сервисы ходят друг к другу напрямую. При девяти сервисах это управляемо, но уже на грани. Traefik или HAProxy с единой точкой маршрутизации — следующий шаг.

Централизованное логирование раньше. Первые полгода я grep’ил логи по SSH на разных серверах. structlog + агрегация нужны с первого дня.

Контракты между сервисами. OpenAPI-спеки генерируются автоматически, но никто не проверяет обратную совместимость. Contract testing — то, что нужно внедрить.

Главный вывод

Каждый сервис в этой экосистеме появился потому, что конкретная проблема не решалась в рамках существующих компонентов. API отделился от Autoload, потому что нужна стабильность. EVC отделился, потому что WebSocket — stateful, а всё остальное — stateless. BSCAN отделился, потому что маркировка — регуляторное требование с собственным жизненным циклом.

Микросервисы — это не архитектурное решение, которое принимается один раз. Это процесс, в котором система постепенно декомпозируется по мере роста бизнес-требований. Каждый раз, когда я добавлял новый сервис, я сначала пробовал решить задачу в существующем. И только когда это создавало больше проблем, чем решало, выделял отдельный компонент.

Девять сервисов в продакшене — это девять деплоев, девять мониторингов, девять потенциальных точек отказа. Но это и девять независимых частей, каждая из которых может упасть, обновиться или масштабироваться, не затрагивая остальные. Для бизнеса с пятью точками, тремя маркетплейсами и десятком поставщиков — это работает. Хотя, честно говоря, иногда задаюсь вопросом, не стоило ли объединить OCRM и Autoload Engine — у них много общего кода по работе с Avito.