Масштабирование по требованию: как Serverless легко справляется с пиками трафика

Масштабирование по требованию: как Serverless легко справляется с пиками трафика

В предыдущей статье мы обсудили две процессные модели для Serverless: выполнение до завершения (run-to-completion) и долгоживущие процессы (long-running processes). Ключевое различие между ними заключается в том, завершается ли экземпляр функции сразу после выполнения. Кроме того, мы рассмотрели два сценария: оркестрацию данных и оркестрацию сервисов.

Возможно, вы задаётесь вопросом, можно ли реализовать эти сценарии с помощью долгоживущих процессов. Ответ — да, но важно отметить, что выполнение до завершения является наиболее «чистой» формой Serverless. Так в чём же тут логика?

Чтобы полностью это осознать, нам нужно ввести ключевую концепцию эволюции архитектуры сложных интернет-приложений: масштабирование (scaling), которая и является темой этой статьи.

Представьте, что 200 пользователей одновременно обращаются к index.html домашней страницы вашего веб-приложения, разработанного локально. Что произойдёт с вашим локальным экземпляром веб-сервера?

Опишем состояние вашего ПК. Во-первых, между клиентами и вашим ПК устанавливается 200 TCP/IP-соединений, с которыми он едва справляется. Затем все 200 клиентов одновременно отправляют HTTP-запросы «GET /». Главный процесс вашего веб-сервера создаёт параллельно «количество ядер CPU минус 1» дочерних процессов для обработки этих запросов. Обратите внимание: мы вычитаем одно ядро, чтобы зарезервировать его для главного процесса.

Например, на 4-ядерном процессоре создаётся три дочерних процесса, которые обрабатывают три клиентских запроса параллельно, а остальные запросы помещаются в очередь. Дочерние процессы начинают обрабатывать запросы «GET /», сопоставляют правила маршрутизации, входят в соответствующую функцию управления и возвращают index.html клиентам. Как только дочерний процесс отправляет файл index.html, главный процесс переиспользует его и создаёт новый для обработки следующего запроса — и так до тех пор, пока все запросы не будут обработаны.

Поняв это, ответ на следующий вопрос становится простым. Как повысить скорость обработки очереди клиентов?

Вертикальное масштабирование vs. горизонтальное масштабирование

Очевидное решение — увеличить количество ядер CPU. Этого можно добиться, улучшив конфигурацию одной машины, например, с 4 до 8 ядер, что даст 7 параллельных дочерних процессов.

Помимо прямого увеличения ядер CPU, можно добавить больше машин (каждая с 4 ядрами). Распределив по 500 клиентов на каждую из двух машин, мы также увеличим количество параллельных дочерних процессов до 6.

Увеличение или уменьшение производительности одной машины называется вертикальным масштабированием (vertical scaling), которое часто сопровождается резким ростом затрат при повышении производительности. Поэтому к такому подходу нужно подходить с осторожностью. Увеличение или уменьшение количества машин — это горизонтальное масштабирование (horizontal scaling), более экономически эффективный подход и наш метод масштабирования по умолчанию.

Теперь добавим немного сложности. Хотя index.html — это один файл, как быть с данными? При любом масштабировании — вертикальном или горизонтальном — нам нужно перезапускать машины. В нашем примере со списком задач данные хранятся в памяти и сбрасываются при каждом перезапуске. Как же сохранить данные при масштабировании?

Состояние vs. отсутствие состояния

Узлы в сетевой топологии можно разделить на сохраняющие состояние (stateful) и не сохраняющие состояние (stateless) в зависимости от того, хранят ли они состояние. Узлы с состоянием сохраняют его, то есть хранят данные. Поэтому они требуют особого внимания: стабильности и устойчивости к частым изменениям. Например, базы данных обычно используют структуру «ведущий-ведомый», что позволяет немедленно переключиться на ведомый узел в случае проблем с ведущим, обеспечивая непрерывную доступность сервиса.

Узлы без состояния, напротив, не хранят никакого состояния или хранят только временные ненадёжные данные. Из-за отсутствия состояния такие узлы могут горизонтально масштабироваться для обработки высокой нагрузки и масштабироваться до нуля при отсутствии трафика (знакомо?). Узлы с состоянием так делать не могут. В сценариях со значительными колебаниями трафика между пиковыми и непиковыми часами нам нужно проектировать узлы с состоянием так, чтобы они выдерживали пиковую нагрузку, но при этом поддерживать эксплуатационные расходы даже в периоды низкой активности.

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

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

Однако в Serverless, даже если мы храним значения в главном процессе долгоживущего процесса, облачный провайдер всё равно может его перезабрать. Даже при использовании зарезервированных экземпляров данные в памяти масштабированных узлов остаются изолированными.

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

Отделив хранение данных от узла главного процесса и обеспечив, чтобы главный процесс не сохранял данные, мы делаем наше приложение не сохраняющим состояние. Мы храним данные в отдельном узле базы данных с состоянием. Этот пример превращается в сценарий долгоживущего Serverless, описанный в предыдущей статье, где мы подключаемся к базе данных при запуске главного процесса и получаем доступ к данным через дочерние процессы. Однако у этого подхода есть существенный недостаток: он напрямую увеличивает время холодного старта. Есть ли лучшее решение?

Рассмотрим альтернативный подход к сохранению данных. Зачем нам подключаться к базе данных самостоятельно? Наши операции CRUD (создание, чтение, обновление, удаление) над данными по сути представляют собой использование дочерними процессами TCP-соединения, установленного главным процессом, отправку запросов к базе данных и получение данных. Представьте, что мы могли бы отправлять инструкции базе данных с помощью HTTP-запросов, таких как POST, DELETE, PUT и GET. Разве это не позволило бы нам использовать концепции оркестрации данных и сервисов из предыдущего урока?

Что такое BaaS

Действительно, вся эта подготовка подводит нас к главному герою сегодняшнего обсуждения: BaaS-ификация (BaaSification). Операции с интерфейсами данных POST, DELETE, PUT и GET соответствуют семантическим HTTP-методам RESTful API. Взяв MySQL в качестве примера, POST соответствует команде CREATE, DELETE — DELETE, PUT — UPDATE, а GET — SELECT. Это семантическое взаимно-однозначное соответствие позволяет нам естественным образом перевести операции MySQL в операции RESTful API.

Традиционные подходы к работе с базами данных, благодаря повторному использованию TCP-соединений и низким накладным расходам на связь, выполняют те же операции быстрее по сравнению с HTTP. Serverless может напрямую подключаться к базам данных, но подключение к традиционным базам данных с использованием IP-адресов часто оказывается сложным в облачных средах с сегментацией VPC. Поэтому для подключения Serverless к базам данных мы обычно полагаемся на сервисы BaaS, предоставляемые облачными провайдерами, хотя многие BaaS-сервисы пока недостаточно зрелы.

Развивая эту мысль: если Serverless не подходит для узлов с состоянием, почему бы не вынести все операции с состоянием в виде интерфейсов данных? Это позволит нашим Serverless-функциям использовать подход оркестрации данных, описанный в предыдущем уроке, и достичь свободного масштабирования.

Итог

Причина, по которой модель выполнения до завершения считается более «чистой», чем модель долгоживущих процессов, заключается в том, что последняя может вводить в заблуждение, побуждая нас относиться к ней как к PaaS и использовать как узел с состоянием для постоянного хранения данных. Однако в Serverless, даже с долгоживущими процессами, облачные провайдеры всё равно могут перезабрать наши экземпляры функций.

Точно так же, как в нашем примере с хранением данных в памяти, когда данные сбрасывались при каждом перезапуске, переходя к мышлению оркестрации данных и преобразуя операции внутренней базы данных в интерфейсы данных, мы можем выгрузить хранение данных в Serverless на внутренние приложения и взаимодействовать с ними с помощью оркестрации данных, как объяснялось в предыдущем уроке. Однако нам нужно не просто создавать интерфейсы данных для внутренних приложений. Нам необходимо принять BaaS-ификацию, освободив внутренних разработчиков от забот об эксплуатации серверов во время разработки.

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

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

Когда наши Serverless-функции обращаются к узлам с состоянием, предпочтительно, чтобы эти узлы предоставляли интерфейсы данных вместо того, чтобы полагаться исключительно на команды базы данных, поскольку подключения к базе данных создают дополнительные накладные расходы для Serverless-функций. Более того, чтобы упростить разработку для внутренних разработчиков, мы должны стремиться к BaaS-ификации узлов с состоянием. Мы углубимся в BaaS-ификацию в следующих статьях.

Novita AI — это универсальная облачная платформа, которая расширяет ваши AI-амбиции. Интегрированные API, Serverless, GPU Instance — экономически эффективные инструменты, которые вам нужны. Избавьтесь от инфраструктуры, начните бесплатно и воплощайте своё AI-видение в реальность.

Рекомендуемое чтение

Unveiling the Revolution: Exploring the World of Serverless Computing

Serverless Analysis, Starting From Data Models