Проектирование serverless функций

от автора

Serverless функции — отличная возможность не думать о масштабировании, легко деплоить, а также использовать free tier для своих пет-проектов. В своей практике я часто использую этот подход и хочу поделиться опытом, когда это действительно удобно, а когда лучше посмотреть в сторону других решений.

Если у нас простая задача, например отправлять уведомления по вызову и событиям, то в целом проблем нет (только нюансы реализации). Но если мы хотим один или несколько микросервисов, или целое приложение разделить на serverless функции — тут начинаются интересные вещи. Нужно так спланировать свое приложение, чтобы оно было разделено по функциям, при этом его легко можно было масштабировать и расширять.

В этой статье хочу разобрать, каким образом проектировать и делить приложение, когда этот подход имеет смысл, а когда нет. За последние годы я реализовал несколько крупных проектов на serverless архитектуре, и постараюсь поделиться основными принципами и подводными камнями, с которыми столкнулся.

Основные концепции

Для того чтобы начать проектировать, нужно понимать основные ограничения и особенности функций. Расскажу про самые важные моменты, которые влияют на архитектуру:

  1. Отсутствие состояния У функции не должно быть состояния. Как правило, провайдер предоставляет папку /tmp или аналогичную, но она требуется только для временного сохранения чего-то во время выполнения функции. На практике это означает, что все важные данные нужно хранить во внешних сервисах.

  2. Время выполнения Функция ограничена по времени выполнения (у разных провайдеров разное) — в среднем 10 минут, после чего наступает таймаут. Поэтому если вам нужно пару часов что-то перемалывать, для этого кейса стоит взять виртуалку/железо, при этом все остальное вы спокойно можете использовать в функциях.

Кстати, недавно начали появляться так называемые «долгоживущие функции», для них например у Яндекса таймаут 1 час, но я пока не вижу выгоды от их использования. Если у вас есть интересные кейсы — поделитесь в комментариях.

  1. Стоимость выполнения Исходя из ограничения по времени и особенностей оплаты за функции — чем дольше выполняется функция и больше потребляет оперативной памяти, тем больше вы заплатите. Это основные параметры тарификации — память и время обработки вызовов.

  2. Принцип единой точки входа В идеале каждая функция должна иметь одну точку входа и выполнять одно логическое действие. Хотя само действие может быть комплексным, важно чтобы оно представляло собой единую логическую операцию.

Хороший пример — телеграм-бот. У него может быть только одна точка входа — webhook для обработки обновлений. И хотя бот обрабатывает разные типы сообщений и команд, это всё части одной логической операции — обработки обновления от Telegram. По требованиям Telegram API мы не можем разделить этот функционал на отдельные serverless функции в рамках одного бота.

  1. Способы вызова Основные способы вызова функции зависят от провайдера, как правило — запрос или тригеры (например передача сообщений из очереди для обработки). Проектировать приложение стоит из этих двух возможностей.

Практический кейс

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

Функциональные требования:

  • Обработка обращений через бот:

    • Создание нового обращения

    • Просмотр статуса текущих обращений

    • Возможность добавить сообщение к существующему обращению

    • Получение уведомлений об изменении статуса обращения

    • Возможность оценить качество поддержки после решения

    • Просмотр истории обращений

  • Работа с обращениями в таск-трекере:

    • Автоматическое создание тикета при поступлении обращения

    • Отправка ответов клиенту через бот

    • Изменение статуса обращения

    • Назначение приоритета

    • Категоризация обращений

  • Рассылки и уведомления:

    • Массовая рассылка системных уведомлений всем пользователям бота

    • Автоматические уведомления при изменении статуса обращения

    • Публикация новостей сервиса

    • Информирование о плановых работах

  • Аналитика:

    • Количество обращений в разрезе каналов и категорий

    • Среднее время решения обращений по специалистам

    • Средний рейтинг удовлетворенности по специалистам

    • Статистика по типам обращений

    • Пиковые нагрузки по времени суток/дням недели

    • Процент просроченных обращений

Нефункциональные требования:

  • Масштабируемость:

    • Обработка растущего количества обращений

    • Поддержка лимитов мессенджера при массовых рассылках

  • Расширяемость:

    • Возможность добавления новых мессенджеров

    • Подключение дополнительных метрик для анализа

Проектирование

После определения требований давайте спроектируем нашу систему, используя 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/