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

Если остались вопросы или хотите обсудить тему подробнее — пишите в комментариях и подписывайтесь на мой канал в телеграмме.

Комментарии (10)


  1. alhimik45
    14.12.2024 21:36

    Из неочевидных подводных камней: проверьте максимальные таймауты который разрешает ваш ApiGateway. У нативного амазоновского жётский лимит в 29 секунд и порой то холодный старт подлагал, то внешнее апи медленнее ответило и упс, ваш клиент уже получил gateway timeout.

    В том же AWS как вариант можно использовать Application Load Balancer, который не имеет таких лимитов как Api Gateway (как не имеет и его фичей), но при этом вы всё ещё ограничены упомянутым в статье таймаутом на время выполнения функции, что может быть стоппером для fully serverless архитектуры, если вы обращаетесь к какому-нибудь легаси у которого единственный вид взаимодействия - синхронные http вызовы по 30 минут.


    1. Apokalepsis Автор
      14.12.2024 21:36

      Спасибо за совет! Заглянул в Яндексовский - 5 минут. Основной плюс использования, если не большие личные проекты - есть free tier - 100к запросов.

      Я стараюсь делать функции так, что бы они отвечали меньше двух минут. Если больше, тогда можно сделать как у меня в одном из примеров - выносить в очередь. Тригер по очереди не имеет такого лимита.


  1. YegorP
    14.12.2024 21:36

    Большой головняк с тем, что драйверы для взаимодествия с БД плохо оптимизированы под жизненный "ритм" функций в serverless. Например, монговский пакет для Node.js долго подключается и создаёт большой пул соединений. Это ок для классических архитектур, но в serverless надо наоборот быстро стартовать и не держать слишком большой пул, потому что параллельной обарботки запросов одним и тем же пулом соединений всё равно не будет. И вот из-за этого начинаются пляски с provisioned concurrency, тюнингом настроек, чтением исходников драйверов и т.п.


    1. ednersky
      14.12.2024 21:36

      полагаю тут для баз, вроде PG, балансеры/баунсеры становятся must have.

      для постгри это pgbouncer.

      и режим работы ни в коем случае не tcp session, а только потранзакционный переключатель. как-то так


    1. Apokalepsis Автор
      14.12.2024 21:36

      Да, это нужно иметь ввиду и очень сильно зависит от языка/библиотеки. Как выше написали, можно использовать внешние пулы соединений. Еще хороший вариант - ориентироваться на serverless БД которые предоставляет провайдер, с которым вы работаете.


  1. ednersky
    14.12.2024 21:36

    меня всегда больше всего волновал вопрос: "а как тестировать в CI вот это вот всё?"

    и кроме варианта в CI создать свой namespace функций, баз данных, очередей итп и после на этом всём запустить набор тестов - кроме такого варианта в голову не приходит ничего.

    а как Вы такое тестируете?


    1. Apokalepsis Автор
      14.12.2024 21:36

      Ответ тянет на статью, очень многое зависит от самого приложения и языка. Если коротко: unit на моках, если часть компонентов можно аналогично поднять у себя (не специфичный для облака продукт, например PostgreSQL) - тогда можно поднимать гонять в docker в рамках CI. По интеграционным тестам - большинство облачных провайдеров поддерживают версионирование функций. Можно деплоить тестовую версию функции параллельно с основной и гонять тесты на ней, направляя трафик через отдельные endpoint. БД и остальные компоненты в этом случае можно создавать под тесты - благо есть terraform и API провайдера.


      1. ednersky
        14.12.2024 21:36

        Ответ тянет на статью

        меня вопрос serverless занимает давно, но именно из-за того, что никто не написал такую статью я делаю заключение, что ситуация здесь: "каждый ваяет что-то на коленке", а значит "и мне тоже придётся".

        Поэтому я так и не удосужился попробовать.


        1. Apokalepsis Автор
          14.12.2024 21:36

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

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

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


          1. ednersky
            14.12.2024 21:36

            Если статья наберет достаточно лайков и я пойму что это действительно будет интересно многим, то я напишу

            ну я поставил плюсиков сколько мог, да ещё и подписался.

            конечно, один в поле не воин, но сделал что мог :)

            буду ждать :)