Устойчивая система доставки сообщений в мессенджеры
Как построить мультиканальную систему доставки: архитектура, очереди, 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. Адаптеры нормализуют различия, предоставляя единый интерфейс для отправки:
| Аспект | MAX | Telegram | VK |
|---|---|---|---|
| Endpoint | api.max.ru/bot/v1 | api.telegram.org/bot | api.vk.com/method |
| Авторизация | Bearer token | Токен в URL | access_token в параметрах |
| ID получателя | chat_id (число) | chat_id (число) | user_id + random_id |
| Rate limit | ~30/s | ~30/s | 20/s |
| Суточный лимит | Без лимита | Без лимита | 5 000 (рассылка) |
| Retry-стратегия | Expo backoff | Retry-After header | error_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 (основной) | MAX | 152-ФЗ, высокая открываемость, нет суточного лимита |
| 2 | Telegram | Быстрая доставка, высокая вовлечённость |
| 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, отправьте себе тестовое сообщение и уже потом решайте, нужны ли другие каналы.