Инфраструктура
13 мин·

Устойчивая система доставки сообщений в мессенджеры

Как построить мультиканальную систему доставки: архитектура, очереди, failover и мониторинг для Telegram, VK и MAX.

Зачем нужна мультиканальность

Зависимость от одного мессенджера — критический риск для бизнеса. Telegram может заблокировать бота за превышение лимитов, VK ограничивает рассылки до 5 000 сообщений в сутки, а любой канал может испытывать временные сбои.

Мультиканальная доставка решает три ключевые задачи:

  • Надёжность — если один канал недоступен, сообщение доставляется через другой
  • Охват — пользователь получает сообщение в том мессенджере, где он активен (VK, Telegram или MAX)
  • Оптимизация — можно выбирать канал с лучшей открываемостью или наименьшей стоимостью для конкретного сегмента

Компании с мультиканальной доставкой показывают на 35–40% более высокий процент доставленных уведомлений по сравнению с моноканальным подходом.

Архитектура системы доставки

Типичная мультиканальная система состоит из нескольких слоёв. Вот общая архитектура:

Рендер схемы...

Каждый компонент отвечает за свою задачу: API Gateway принимает запросы, роутер выбирает канал, очереди буферизуют нагрузку, адаптеры нормализуют запросы под конкретный API мессенджера.

Слой маршрутизации сообщений

Роутер — центральный компонент системы. Он принимает решение, в какой канал отправить сообщение, на основе нескольких факторов:

  • Предпочтение пользователя — в каком мессенджере пользователь зарегистрирован (MAX, Telegram, VK)
  • Приоритет канала — заданный бизнес-логикой порядок каналов (например, MAX → Telegram → VK)
  • Доступность канала — проверка health-check каждого канала (circuit breaker)
  • Тип сообщения — транзакционные уведомления маршрутизируются через самый надёжный канал, маркетинговые — через самый дешёвый
  • Лимиты канала — если лимит VK (5 000/сутки) исчерпан, сообщение перенаправляется в MAX или Telegram
// TypeScript — пример маршрутизатора каналов
interface ChannelRouter {
  route(message: OutgoingMessage): ChannelDecision;
}

interface OutgoingMessage {
  recipientId: string;
  text: string;
  channels: ChannelType[];
  backupStrategy: 'priority' | 'round-robin' | 'cheapest';
  messageType: 'transactional' | 'marketing' | 'service';
}

type ChannelType = 'maxbot' | 'telegrambot' | 'vk';

interface ChannelDecision {
  primary: ChannelType;
  backups: ChannelType[];
  reason: string;
}

class PriorityRouter implements ChannelRouter {
  private healthCheck: Map<ChannelType, boolean>;
  private dailyUsage: Map<ChannelType, number>;
  private dailyLimits: Map<ChannelType, number> = new Map([
    ['maxbot', Infinity],      // MAX Bot — без суточного лимита
    ['telegrambot', Infinity], // Telegram Bot — без суточного лимита
    ['vk', 5000],           // VK — 5000 сообщений/сутки
  ]);

  route(message: OutgoingMessage): ChannelDecision {
    const available = message.channels.filter(ch =>
      this.healthCheck.get(ch) &&
      (this.dailyUsage.get(ch) ?? 0) < (this.dailyLimits.get(ch) ?? 0)
    );

    if (available.length === 0) {
      throw new Error('No available channels');
    }

    // MAX в приоритете для транзакционных (152-ФЗ + высокая доставляемость)
    if (message.messageType === 'transactional') {
      const preferred = ['maxbot', 'telegrambot', 'vk'] as ChannelType[];
      const primary = preferred.find(ch => available.includes(ch))!;
      return {
        primary,
        backups: available.filter(ch => ch !== primary),
        reason: `Transactional: ${primary} selected (152-FZ compliant)`,
      };
    }

    // Для маркетинговых — по приоритету из конфига
    const [primary, ...backups] = available;
    return { primary, backups, reason: 'Priority-based routing' };
  }
}

Адаптеры каналов: Telegram, VK, MAX

Каждый мессенджер имеет уникальный API. Адаптеры нормализуют различия, предоставляя единый интерфейс для отправки:

АспектMAXTelegramVK
Endpointapi.max.ru/bot/v1api.telegram.org/botapi.vk.com/method
АвторизацияBearer tokenТокен в URLaccess_token в параметрах
ID получателяchat_id (число)chat_id (число)user_id + random_id
Rate limit~30/s~30/s20/s
Суточный лимитБез лимитаБез лимита5 000 (рассылка)
Retry-стратегияExpo backoffRetry-After headererror_code 6 → пауза

Адаптер инкапсулирует все эти различия. Для бизнес-логики всё выглядит как один вызов send(recipient, message).

Единая система очередей

Очереди — обязательный компонент для массовой отправки. Они решают несколько задач:

  • Буферизация — принимают сообщения мгновенно, отправляют с контролем rate limit каждого канала
  • Приоритизация — транзакционные уведомления обрабатываются первыми, маркетинговые — в фоне
  • Retry — при ошибке сообщение возвращается в очередь с увеличенным delay (exponential backoff)
  • DLQ (Dead Letter Queue) — сообщения, которые не удалось доставить после N попыток, попадают в DLQ для анализа
// Конфигурация очередей (пример для Redis + BullMQ)
import { Queue, Worker } from 'bullmq';

// Отдельная очередь для каждого канала
const maxQueue = new Queue('channel:maxbot', {
  defaultJobOptions: {
    attempts: 5,
    backoff: { type: 'exponential', delay: 1000 },
    removeOnComplete: 1000,
    removeOnFail: false, // сохраняем в DLQ
  }
});

const telegramQueue = new Queue('channel:telegram', {
  defaultJobOptions: {
    attempts: 5,
    backoff: { type: 'exponential', delay: 1000 },
  }
});

const vkQueue = new Queue('channel:vk', {
  defaultJobOptions: {
    attempts: 3,
    backoff: { type: 'exponential', delay: 2000 },
  }
});

// Worker с контролем rate limit
const maxWorker = new Worker('channel:maxbot', async (job) => {
  const { recipientId, text, buttons } = job.data;
  await maxAdapter.send(recipientId, text, buttons);
}, {
  limiter: { max: 25, duration: 1000 }, // 25 msg/s (с запасом)
  concurrency: 5,
});

// При исчерпании retry → failover
maxWorker.on('failed', async (job, err) => {
  if (job.attemptsMade >= job.opts.attempts) {
    // Перенаправляем в резервный канал
    const backup = job.data.backups?.[0];
    if (backup === 'telegrambot') {
      await telegramQueue.add('backup', job.data);
    } else if (backup === 'vk') {
      await vkQueue.add('backup', job.data);
    }
  }
});

Стратегии failover

Failover — переключение на резервный канал при недоступности основного. Существует несколько стратегий:

  • Sequential — каналы перебираются по порядку: MAX Bot → Telegram Bot → VK. Если MAX вернул ошибку, пробуем Telegram, затем VK
  • Parallel with first-win — сообщение отправляется во все каналы одновременно, первый успешный ответ фиксируется. Дорого, но быстро
  • Circuit Breaker — если канал вернул N ошибок подряд, он «размыкается» и все сообщения сразу идут в резервный канал. Через timeout канал проверяется снова
  • Weighted — нагрузка распределяется по каналам с весами. Неработающий канал получает вес 0

На практике чаще используют такой приоритет каналов для failover:

ПриоритетКаналОбоснование
1 (основной)MAX152-ФЗ, высокая открываемость, нет суточного лимита
2TelegramБыстрая доставка, высокая вовлечённость
3 (резерв)VKШирокий охват, но суточный лимит 5 000

Подробнее о построении отказоустойчивости для отдельных каналов: «Отказоустойчивая система доставки для Telegram».

Подтверждение доставки и трекинг

Мультиканальная система должна точно знать статус каждого сообщения. Это сложнее, чем кажется, потому что каналы предоставляют разный уровень обратной связи:

  • MAX — возвращает message_id при успехе, поддерживает callback о прочтении
  • Telegram — возвращает полный объект Message с метаданными. Нет нативного «прочитано», но можно отслеживать callback-нажатия
  • VK — возвращает message_id. Callback API позволяет получить событие message_read

Для унификации статусов используйте конечный автомат:

// Статусы доставки сообщения
type DeliveryStatus =
  | 'queued'      // В очереди
  | 'sending'     // Отправляется
  | 'sent'        // Отправлено (API вернул успех)
  | 'delivered'   // Доставлено (если канал поддерживает)
  | 'read'        // Прочитано (если канал поддерживает)
  | 'failed'      // Ошибка отправки
  | 'backup'      // Перенаправлено в другой канал
  | 'dlq';        // В Dead Letter Queue

// Пример записи в БД
interface MessageRecord {
  id: string;
  recipientId: string;
  text: string;
  primaryChannel: ChannelType;
  actualChannel: ChannelType | null;
  status: DeliveryStatus;
  attempts: number;
  externalMessageId: string | null; // ID в мессенджере
  createdAt: Date;
  sentAt: Date | null;
  deliveredAt: Date | null;
  readAt: Date | null;
  error: string | null;
}

Мониторинг и алертинг

Без мониторинга мультиканальная система — «чёрный ящик». Минимальный набор показателей:

  • Delivery Rate — процент успешно доставленных сообщений по каналу. Норма: >98% для MAX/Telegram, >95% для VK
  • Latency P99 — время от постановки в очередь до подтверждения отправки. Норма: <5с для MAX/Telegram, <10с для VK
  • Queue Depth — количество сообщений в очереди. Резкий рост означает проблему с каналом
  • Error Rate — процент ошибок по типам (rate limit, auth, network). Алерт при >5%
  • Процент переключений — процент сообщений, ушедших через резервный канал. Алерт при >10% (проблема с основным каналом)
  • DLQ Size — количество недоставленных сообщений. Алерт при >0
// Статистика мультиканальной доставки — dashboard API
const stats = await fetch('/api/stats/multichannel');
// {
//   "sent": {
//     "maxbot": { "success": 15234 },
//     "telegrambot": { "success": 8921 },
//     "vk": { "success": 4812, "error": 188 }
//   },
//   "latency_p95": { "maxbot": 1.0, "telegrambot": 1.2, "vk": 3.5 },
//   "backup_switches": { "vk_to_maxbot": 312, "telegrambot_to_maxbot": 45 },
//   "queue_depth": { "maxbot": 42, "telegrambot": 18, "vk": 3210 }
// }

Масштабирование системы

При росте объёмов сообщений (100K+/день) система должна масштабироваться горизонтально:

  • Адаптеры — запускаются как отдельные микросервисы, масштабируются независимо. Рабочий темп для MAX-адаптера лучше определять по текущим лимитам платформы и своим нагрузочным тестам
  • Очереди — Redis Cluster или Kafka для высоких объёмов. Partitioning по recipient_id для сохранения порядка
  • Роутер — stateless, масштабируется горизонтально за load balancer
  • База данных — PostgreSQL с partitioning по дате для хранения истории доставки
  • Мониторинг — встроенная аналитика для трейсинга каждого сообщения через всю цепочку

Для 1M+ сообщений в день потребуется: 3+ инстанса адаптеров, Redis Cluster (6 нод), PostgreSQL с партиционированием, выделенный мониторинг.

Как это реализовано в Релая

Всё вышеописанное — это именно то, что Релая предоставляет из коробки. Вам не нужно строить эту инфраструктуру самостоятельно:

  • Единый API — один POST /v1/profiles/{profileId}/integrations/{integration}/messages для всех каналов
  • Встроенный роутер — приоритетная маршрутизация MAX Bot → Telegram Bot → VK с учётом доступности и лимитов
  • Очереди и retry — BullMQ + Redis с экспоненциальным backoff и DLQ для каждого канала
  • Circuit Breaker — автоматическое отключение неработающего канала с периодической проверкой
  • Delivery tracking — статус каждого сообщения в реальном времени через API и webhook-уведомления
  • Встроенная аналитика — статистика и трейсинг из коробки
  • Горизонтальное масштабирование — Docker/Kubernetes-ready

Подробнее о том, почему готовая платформа выгоднее самописной интеграции: «Самописная интеграция или готовая платформа». А о том, как построить единый API для мессенджеров: «Единый API для всех мессенджеров: миф или реальность».

Мультиканальная доставка — не роскошь, а необходимость для бизнеса, отправляющего тысячи уведомлений. Релая берёт на себя всю инфраструктуру: очереди, failover, мониторинг и масштабирование — чтобы вы сосредоточились на продукте, а не на инфраструктуре доставки.

Создайте бесплатный MAX-профиль

Если хочется не просто читать, а сразу проверить сценарий руками: подключите MAX, отправьте себе тестовое сообщение и уже потом решайте, нужны ли другие каналы.