Serverless функции - отличная возможность не думать о масштабировании, легко деплоить, а также использовать free tier для своих пет-проектов. В своей практике я часто использую этот подход и хочу поделиться опытом, когда это действительно удобно, а когда лучше посмотреть в сторону других решений.
Если у нас простая задача, например отправлять уведомления по вызову и событиям, то в целом проблем нет (только нюансы реализации). Но если мы хотим один или несколько микросервисов, или целое приложение разделить на serverless функции - тут начинаются интересные вещи. Нужно так спланировать свое приложение, чтобы оно было разделено по функциям, при этом его легко можно было масштабировать и расширять.
В этой статье хочу разобрать, каким образом проектировать и делить приложение, когда этот подход имеет смысл, а когда нет. За последние годы я реализовал несколько крупных проектов на serverless архитектуре, и постараюсь поделиться основными принципами и подводными камнями, с которыми столкнулся.
Основные концепции
Для того чтобы начать проектировать, нужно понимать основные ограничения и особенности функций. Расскажу про самые важные моменты, которые влияют на архитектуру:
Отсутствие состояния У функции не должно быть состояния. Как правило, провайдер предоставляет папку /tmp или аналогичную, но она требуется только для временного сохранения чего-то во время выполнения функции. На практике это означает, что все важные данные нужно хранить во внешних сервисах.
Время выполнения Функция ограничена по времени выполнения (у разных провайдеров разное) - в среднем 10 минут, после чего наступает таймаут. Поэтому если вам нужно пару часов что-то перемалывать, для этого кейса стоит взять виртуалку/железо, при этом все остальное вы спокойно можете использовать в функциях.
Кстати, недавно начали появляться так называемые "долгоживущие функции", для них например у Яндекса таймаут 1 час, но я пока не вижу выгоды от их использования. Если у вас есть интересные кейсы - поделитесь в комментариях.
Стоимость выполнения Исходя из ограничения по времени и особенностей оплаты за функции - чем дольше выполняется функция и больше потребляет оперативной памяти, тем больше вы заплатите. Это основные параметры тарификации - память и время обработки вызовов.
Принцип единой точки входа В идеале каждая функция должна иметь одну точку входа и выполнять одно логическое действие. Хотя само действие может быть комплексным, важно чтобы оно представляло собой единую логическую операцию.
Хороший пример - телеграм-бот. У него может быть только одна точка входа - webhook для обработки обновлений. И хотя бот обрабатывает разные типы сообщений и команд, это всё части одной логической операции - обработки обновления от Telegram. По требованиям Telegram API мы не можем разделить этот функционал на отдельные serverless функции в рамках одного бота.
Способы вызова Основные способы вызова функции зависят от провайдера, как правило - запрос или тригеры (например передача сообщений из очереди для обработки). Проектировать приложение стоит из этих двух возможностей.
Практический кейс
Давайте, для того чтобы рассматривать архитектуру предметно, сформируем требования к приложению которое будет проектировать. Это приложение для организации технической поддержки пользователей.
Функциональные требования:
Обработка обращений через бот:
Создание нового обращения
Просмотр статуса текущих обращений
Возможность добавить сообщение к существующему обращению
Получение уведомлений об изменении статуса обращения
Возможность оценить качество поддержки после решения
Просмотр истории обращений
Работа с обращениями в таск-трекере:
Автоматическое создание тикета при поступлении обращения
Отправка ответов клиенту через бот
Изменение статуса обращения
Назначение приоритета
Категоризация обращений
Рассылки и уведомления:
Массовая рассылка системных уведомлений всем пользователям бота
Автоматические уведомления при изменении статуса обращения
Публикация новостей сервиса
Информирование о плановых работах
Аналитика:
Количество обращений в разрезе каналов и категорий
Среднее время решения обращений по специалистам
Средний рейтинг удовлетворенности по специалистам
Статистика по типам обращений
Пиковые нагрузки по времени суток/дням недели
Процент просроченных обращений
Нефункциональные требования:
Масштабируемость:
Обработка растущего количества обращений
Поддержка лимитов мессенджера при массовых рассылках
Расширяемость:
Возможность добавления новых мессенджеров
Подключение дополнительных метрик для анализа
Проектирование
После определения требований давайте спроектируем нашу систему, используя serverless подход. Прежде чем углубляться в детали реализации, рассмотрим основных пользователей системы и их взаимодействие:
Клиенты обращаются в поддержку через ботов в мессенджерах, создают обращения и получают ответы
Специалисты поддержки работают с обращениями через таск-трекер, отвечают клиентам и меняют статусы тикетов
Администраторы системы управляют массовыми рассылками и анализируют эффективность работы поддержки через дашборды

В качестве языка разработки выберем Go - он отлично подходит для serverless архитектуры. Go обеспечит хорошую производительность, а главное - позволяет создавать компактные бинарные файлы, которые быстро загружаются и не требуют внешних зависимостей. Это особенно важно для serverless функций, где время холодного старта напрямую влияет на отзывчивость приложения.
Начнем с базы данных. Нам понадобится реляционная БД для хранения информации о пользователях: их идентификаторы в разных системах, права доступа, настройки уведомлений. Для хранения соответствия между чатами и тикетами использовать отдельную таблицу не требуется - эту информацию будем хранить в дополнительных полях тикета (тип мессенджера, id/nickname пользователя и id беседы).
Теперь о точках входа в систему. У нас два канала коммуникации - Telegram и VK. Создадим отдельные функции для каждого мессенджера с собственными эндпоинтами в API Gateway. Такой подход дает нам несколько преимуществ:
Изоляция: проблемы с одним ботом не влияют на работу другого
Простота масштабирования: для добавления нового мессенджера достаточно создать новую функцию по аналогии с существующими
Независимая настройка ресурсов: каждую функцию можно оптимизировать под специфику конкретного мессенджера
Для работы ботов нам понадобится хранить состояние диалогов. Например, когда пользователь создает обращение, мы собираем информацию в несколько шагов: тема, описание, тип. Кроме того, имеет смысл кэшировать часто запрашиваемые данные - список активных обращений пользователя. Для этих целей используем Redis.

Трекер:
Нам нужно обрабатывать следующие типы событий из трекера:
Создание нового комментария
Изменение статуса тикета
Для этого создадим две функции: TrackerHandler и TrackerWorker. TrackerHandler принимает входящие API запросы от трекера, валидирует их и складывает в очередь сообщений. В трекере запрос отправляется за счет триггера по заданным условиям. TrackerWorker забирает события из очереди и выполняет необходимые действия - определяет канал коммуникации из данных тикета и отправляет уведомление в соответствующий мессенджер.
Казалось бы, можно было обойтись одной функцией - получили событие и сразу отправили уведомление. Однако такой подход имеет несколько недостатков. Во-первых, отправка сообщений в мессенджеры может занять время или завершиться с ошибкой из-за недоступности API. В этом случае трекер не получит успешный ответ и будет пытаться повторить запрос, что может привести к дублированию уведомлений. Во-вторых, при большом количестве одновременных событий (например, массовое изменение статусов тикетов) мы можем превысить ограничения API мессенджеров. Разделение на две функции с очередью между ними решает эти проблемы - мы гарантируем доставку уведомлений за счет механизма повторных попыток в очереди и можем контролировать скорость отправки сообщений.

Рассылки:
С массовыми рассылками ситуация интереснее. Основная проблема здесь - ограничения API мессенджеров. Например, Telegram позволяет отправлять не более 30 сообщений в секунду, поэтому для надежности будем использовать лимит в 20 сообщений. Для этого сделаем две функции:
NotificationAPI - функция, которая принимает POST запрос с данными для рассылки (текст сообщения и критерии выборки получателей). Она получает список пользователей согласно критериям и, учитывая ограничения Telegram в 30 сообщений в секунду и стандартный таймаут serverless функций в 5 минут, разбивает получателей на батчи по 6000 сообщений (20 сообщений в секунду * 300 секунд). Каждый такой батч отправляется в очередь как отдельное задание.
NotificationWorker - это рабочая функция, которая просто получает готовый батч из очереди и последовательно отправляет сообщения пользователям с учетом ограничений API мессенджера. При ошибках отправки сообщение может вернуться в очередь для повторной попытки.

Аналитика:
Сбор аналитики в этой системе реализуется достаточно прямолинейно. Создаем функцию, которая по таймеру (например, раз в час) запрашивает через API новые тикеты и обновления по существующим, затем складывает эти данные в ClickHouse. Для начальной загрузки исторических данных предусмотрим скрипт в процессе деплоя - это избавит от необходимости отдельной инициализации и сделает развертывание системы более удобным.

Такая архитектура позволяет эффективно масштабировать систему под нагрузкой, легко добавлять новые каналы коммуникации, новые сервисы и собирать подробную аналитику. А еще это достаточно выгодно.
Если тема действительно интересна и эта статья наберёт больше 20 лайков, обязательно напишу отдельный материал о технической реализации!
Если остались вопросы или хотите обсудить тему подробнее — пишите в комментариях и подписывайтесь на мой канал в телеграмме.