Когда бессерверная система встречается с традиционными реляционными базами данных

FaaS, или функции как сервис, — это концепция, о которой вы, скорее всего, слышали недавно, поскольку ее популярность стремительно возросла за последние несколько лет.

По мере того как все больше людей начинают рассматривать и опробовать модели Serverless в своих проектах, будь то с помощью AWS Lambda, Azure Functions или любого другого доступного провайдера, мы начали видеть, какие удивительные возможности они открывают.

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

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

Что такое Serverless и почему он популярен?

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

Смещение ответственности

Бессерверная модель, несмотря на свое название, все равно должна включать в себя сервер. Разница заключается в том, кто должен управлять этим сервером.

Основным преимуществом архитектуры Serverless является то, что нам, разработчикам и DevOps-инженерам, не нужно беспокоиться о поддержании фактического сервера, на котором работает наш код. Провайдер Serverless, которого вы используете, автоматически запускает и управляет контейнером для вашего приложения.

Потенциально дешевле

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

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

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

Последний плюс — это, безусловно, один из основных моментов Serverless, который делает его переломным и представляет собой другой класс моделей развертывания.

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

Это имеет огромное значение, поскольку позволяет обрабатывать значительный объем трафика эффективно и экономично.

Здесь же кроется одна из основных проблем Serverless по отношению к традиционным реляционным базам данных, таким как MySQL или Postgres.

Где возникают проблемы

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

Существует множество различных архитектур и моделей, но мы рассмотрим три основных, которые чаще всего встречаются в природе:

  1. Монолиты
  2. Микросервисы (распределенные системы)
  3. Бессерверные

Монолиты

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

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

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

Микросервисы

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

Serverless

Теперь мы переходим к обсуждаемой теме — бессерверной модели. В бессерверной модели вы можете иметь множество различных бессерверных функций, выполняемых в контейнерах и автомасштабируемых в соответствии с вашими потребностями.

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

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

Почему это может стать проблемой

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

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

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

Чтобы представить это в перспективе, давайте подумаем о некоторых цифрах. Допустим, мы настроили пул соединений в бессерверной функции со следующими параметрами:

"pool": {
   "min": 0,
   "max": 15,
   "idle": 10000
}
Войти в полноэкранный режим Выход из полноэкранного режима

Это сообщит нашему экземпляру функции, что он должен открыть 15 соединений и закрыть все соединения, которые простаивали в течение 10 секунд.

Функциональные экземпляры Соединения
1 15
5 ~75
10 ~150
20 ~300
50 ~750

Теперь, помня об этом, давайте посмотрим на лимиты соединений для некоторых классов экземпляров AWS. connection_limit для MySQL-Flavored RDS определяется расчетом: {DBInstanceClassMemory/12582880}.

Если мы выберем класс экземпляра db.m5.xlarge, который имеет 16 Гб памяти, это будет равно 1 365 максимальным соединениям. (Имейте в виду, что мы не хотим приближаться к пределу по соображениям производительности).

Более подробную информацию о лимитах подключений AWS можно найти в документации здесь.

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

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

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

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

Потенциальные решения на основе кода

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

Снижение лимита пула соединений

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

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

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

"pool": {
   "min": 0,
   "max": 1,
   "idle": 10000
}
Вход в полноэкранный режим Выйти из полноэкранного режима

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

Регулировка времени простоя

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

  1. Будет запускаться все больше и больше экземпляров, каждый из которых открывает соединение с базой данных.
  2. По мере завершения процессов экземпляры будут оставаться открытыми в течение некоторого времени, возможно, удерживая свое соединение
  3. Когда соединение окончательно удаляется из пула, это не обязательно означает, что оно сразу же становится доступным. Механизм базы данных все еще должен обработать закрытие соединения.

Из-за совокупности этих проблем соединения будут продолжать расти и расти при получении огромного количества быстрых запросов, пока база данных не достигнет своего максимума и не закупорится.

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

"pool": {
   "min": 0,
   "max": 1,
   "idle": 5000
}
Вход в полноэкранный режим Выход из полноэкранного режима

ВНИМАНИЕ: Хотя снижение времени простоя может существенно помочь при длительных периодах интенсивного трафика, слишком сильное снижение может быть негативным фактором. Фактически, это может значительно ухудшить доступность вашего соединения.

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

Сервер базы данных, скорее всего, еще не закрыл соединение, так что в этом случае вы, по сути, удваиваете количество соединений!

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

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

Нахождение хорошего баланса является ключевым моментом.

Предупреждение: Ручное закрытие соединений

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

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

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

Возьмем, к примеру, этот код:

module.exports.handler = (event, context, callback) => {
  var client = mysql.createConnection({
    /* your connection info */
  })

  client.connect()

  /* Your business logic */

  client.end()
}
Войти в полноэкранный режим Выйти из полноэкранного режима

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

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

Потенциальные решения на основе инфраструктуры

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

Прокси-сервер базы данных

Одним из возможных решений проблемы пула соединений является прокси-сервер базы данных, например AWS RDS Proxy или Prisma Data Proxy от Prisma.

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

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

Кэширование

Последнее потенциальное решение, которое я упомяну, — это кэширование. Эффективное использование таких инструментов, как ElastiCache от AWS, может позволить некоторым запросам вообще не обращаться к базе данных.

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

Это приведет к тому, что некоторые, а потенциально и многие, выполнения вашей функции, которые в противном случае использовали бы соединение, не будут использоваться вообще! В долгосрочной перспективе, при большой нагрузке, это может иметь огромное значение.

Заключение

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

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

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

Надеюсь, это было полезно для понимания представленной проблемы и того, как мы можем ее избежать! Большое спасибо за то, что следили за нами!

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *