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 лайков, обязательно напишу отдельный материал о технической реализации!
Если остались вопросы или хотите обсудить тему подробнее — пишите в комментариях и подписывайтесь на мой канал в телеграмме.
ссылка на оригинал статьи https://habr.com/ru/articles/866640/
Добавить комментарий