Посчитайте: масштабирование приложений микросервисов с помощью оркестраторов
Опубликовано: 2022-03-11Неудивительно, что архитектура приложений микросервисов продолжает проникать в дизайн программного обеспечения. Гораздо удобнее распределять нагрузку, создавать высокодоступные развертывания и управлять обновлениями, упрощая при этом разработку и управление командой.
Но без оркестраторов контейнеров дело обстоит иначе.
Легко захотеть использовать все их ключевые функции, особенно автоматическое масштабирование. Какое это благословение — наблюдать, как развертывания контейнеров колеблются в течение всего дня, аккуратно подбирая размер для обработки текущей нагрузки, освобождая наше время для других задач. Мы гордимся тем, что показывают наши инструменты мониторинга контейнеров; тем временем мы только что настроили пару параметров — да, это (почти) все, что потребовалось для создания волшебства!
Это не значит, что нет причин гордиться этим: мы уверены, что наши пользователи получают хороший опыт и что мы не тратим деньги впустую на слишком большую инфраструктуру. Это уже немало!
И, конечно же, какой это был путь, чтобы добраться туда! Потому что даже если в конце нужно настроить не так много параметров, это намного сложнее, чем мы обычно думаем, прежде чем мы сможем начать. Минимальное/максимальное количество реплик, пороги увеличения/уменьшения масштаба, периоды синхронизации, задержки охлаждения — все эти настройки тесно связаны друг с другом. Изменение одного, скорее всего, повлияет на другое, но вам все равно придется организовать сбалансированную комбинацию, которая подойдет как для вашего приложения/развертывания, так и для вашей инфраструктуры. И тем не менее, вы не найдете ни кулинарной книги, ни волшебной формулы в Интернете, так как это сильно зависит от ваших потребностей.
Большинство из нас сначала устанавливают для них «случайные» значения или значения по умолчанию, которые мы потом корректируем в соответствии с тем, что обнаруживаем во время мониторинга. Это заставило меня задуматься: что, если бы мы смогли установить более «математическую» процедуру, которая помогла бы нам найти выигрышную комбинацию?
Расчет параметров оркестрации контейнеров
Когда мы думаем об автоматическом масштабировании микросервисов для приложения, мы на самом деле стремимся улучшить два основных момента:
- Убедитесь, что развертывание может быстро масштабироваться в случае быстрого увеличения нагрузки (чтобы пользователи не сталкивались с тайм-аутами или HTTP 500).
- Снижение стоимости инфраструктуры (т. е. предотвращение недогрузки экземпляров)
В основном это означает оптимизацию пороговых значений программного обеспечения контейнера для увеличения и уменьшения масштаба. (Алгоритм Kubernetes имеет один параметр для двух).
Позже я покажу, что все параметры экземпляра привязаны к порогу повышения масштаба. Это труднее всего вычислить — поэтому и эта статья.
Примечание. Что касается параметров, которые устанавливаются для всего кластера, у меня нет подходящей процедуры для них, но в конце этой статьи я представлю часть программного обеспечения (статическую веб-страницу), которая учитывает их при расчете. параметры автоматического масштабирования экземпляра. Таким образом, вы сможете варьировать их значения, чтобы учитывать их влияние.
Расчет порога масштабирования
Чтобы этот метод работал, вы должны убедиться, что ваше приложение соответствует следующим требованиям:
- Нагрузка должна быть равномерно распределена по каждому экземпляру вашего приложения (в циклическом режиме).
- Время запроса должно быть короче, чем интервал проверки загрузки вашего кластера контейнеров.
- Вы должны рассмотреть возможность запуска процедуры для большого количества пользователей (определенных позже).
Основная причина этих условий заключается в том, что алгоритм рассчитывает нагрузку не на пользователя , а как распределение (поясняется позже).
Получение всех гауссовых
Сначала мы должны сформулировать определение быстрого роста нагрузки или, другими словами, наихудшего сценария. На мой взгляд, хороший способ перевести это так: наличие большого количества пользователей, выполняющих ресурсоемкие действия в течение короткого периода времени , и всегда есть вероятность, что это произойдет, пока другая группа пользователей или сервисов выполняет другие задачи. Итак, давайте начнем с этого определения и попробуем извлечь немного математики. (Приготовьте аспирин.)
Введение некоторых переменных:
- $N_{u}$, «большое количество пользователей»
- $L_{u}(t)$ — нагрузка, создаваемая одним пользователем, выполняющим «ресурсоёмкую операцию» ($t=0$ указывает на момент начала операции пользователем)
- $L_{tot}(t)$, общая нагрузка (генерируемая всеми пользователями)
- $T_{tot}$, «короткий период времени»
В математическом мире, когда речь идет о большом количестве пользователей, выполняющих одно и то же в одно и то же время, распределение пользователей во времени следует гауссовскому (или нормальному) распределению, формула которого такова:
\[G (t) = \ frac {1} {\ sigma \ sqrt {2 \ pi}} e ^ {\ frac {- (t- \ mu) ^ 2} {2 \ sigma ^ 2}} \]Здесь:
- µ - ожидаемое значение
- σ - стандартное отклонение
И это выглядит следующим образом (при $µ=0$):
Вероятно, это напоминает некоторые уроки, которые вы посещали — ничего нового. Однако здесь мы сталкиваемся с первой проблемой: чтобы быть математически точными, нам пришлось бы рассматривать временной диапазон от $-\infty$ до $+\infty$, который, очевидно, не поддается вычислению.
Но глядя на график, мы замечаем, что значения вне интервала $[-3σ, 3σ]$ очень близки к нулю и сильно не меняются, то есть их влияние действительно незначительно и его можно отбросить. Это тем более верно, поскольку наша цель — протестировать масштабирование нашего приложения, поэтому мы ищем варианты для большого количества пользователей.
Кроме того, поскольку интервал $[-3σ, 3σ]$ содержит 99,7% наших пользователей, он достаточно близок к общему количеству, чтобы работать с ним, и нам просто нужно умножить $N_{u}$ на 1,003, чтобы компенсировать разница. Выбор этого интервала дает нам $µ=3σ$ (поскольку мы собираемся работать с $t=0$).
Что касается соответствия $T_{tot}$, выбор его равным $6σ$ ($[-3σ, 3σ]$) не будет хорошим приближением, поскольку 95,4 процента пользователей находятся в интервале $[- 2σ, 2σ]$, который длится $4σ$. Таким образом, выбор значения $T_{tot}$ равным $6σ$ добавит половину времени только для 4,3% пользователей, что на самом деле не является репрезентативным. Таким образом, мы решили взять $T_{tot}=4σ$ и можем сделать вывод:
\(σ=\frac{T_{tot}}{4}\) и \(µ=\frac{3}{4} * T_{tot}\)
Эти ценности просто вытащили из шляпы? да. Но такова их цель, и это не повлияет на математическую процедуру. Эти константы предназначены для нас и определяют понятия, связанные с нашей гипотезой. Это означает только то, что теперь, когда мы их установили, наш наихудший сценарий можно перевести как:
Нагрузка генерируется 99,7% из $N{u}$, выполняющих потребляющую операцию $L{u}(t)$, причем 95,4% из них делают это в течение времени $T{tot}$.
(Об этом стоит помнить при использовании веб-приложения.)
Внедряя предыдущие результаты в функцию распределения пользователей (по Гауссу), мы можем упростить уравнение следующим образом:
\[G(t) = \frac{4 N_{u}}{T_{tot} \sqrt{2 \pi}} e^\frac{-(4t-3T_{tot})^2}{T_{tot }^2}\]С этого момента, определив $σ$ и $µ$, мы будем работать на интервале $t \in [0, \frac{3}{2}T_{tot}]$ (длительностью $6σ$).

Какова общая пользовательская нагрузка?
Второй шаг автоматического масштабирования микросервисов — вычисление $L_{tot}(t)$.
Поскольку $G(t)$ является распределением , чтобы получить количество пользователей в определенный момент времени, мы должны вычислить его интеграл (или использовать его кумулятивную функцию распределения). Но поскольку не все пользователи начинают свои операции в одно и то же время, попытка ввести $L_{u}(t)$ и свести уравнение к пригодной для использования формуле приведет к настоящей путанице.
Поэтому, чтобы упростить задачу, мы будем использовать сумму Римана, которая представляет собой математический способ аппроксимации интеграла с использованием конечной суммы малых форм (здесь мы будем использовать прямоугольники). Чем больше фигур (подразделов), тем точнее результат. Еще одним преимуществом использования подразделений является тот факт, что мы можем считать, что все пользователи в пределах подразделения начали свою работу в одно и то же время.
Возвращаясь к сумме Римана, она обладает следующим свойством, связанным с интегралами:
\[\int_{a}^{b} f( x )dx = \lim_{n \rightarrow \infty } \sum_{k=1}^{n} ( x_{k} - x_{k-1} ) е(х_{к})\]С $x_k$, определенным следующим образом:
\[x_{k} = a + k\frac{b - a}{n}, 0 \leq k \leq n\]Это верно, где:
- $n$ — количество подразделений.
- $a$ — нижняя граница, здесь 0.
- $b$ — верхняя граница, здесь $\frac{3}{2}*T_{tot}$.
- $f$ — это функция — здесь $G$ — для аппроксимации его площади.
Примечание. Количество пользователей, присутствующих в подразделении, не является целым числом. Это причина двух предварительных условий: наличие большого количества пользователей (чтобы десятичная часть не слишком влияла) и необходимость равномерного распределения нагрузки по каждому экземпляру.
Также обратите внимание, что мы можем видеть прямоугольную форму подразделения в правой части определения суммы Римана.
Теперь, когда у нас есть формула суммы Римана, мы можем сказать, что значение нагрузки в момент времени $t$ представляет собой сумму количества пользователей в каждом подразделении, умноженную на функцию загрузки пользователей в соответствующее время . Это можно записать как:
\[L_{tot}(t) = \lim_{n \rightarrow \infty} \sum_{k=1}^{n} (x_{k} - x_{k-1})G(x_{k}) L_{u}(t - x_{k})\]После замены переменных и упрощения формулы получается:
\[L_{ tot }( t ) = \frac{6 N_{u}}{\sqrt{2 \pi}} \lim_{n \rightarrow \infty} \sum_{k=1 }^{ n } (\ frac{1}{n}) e^{-{(\frac{6k}{n} - 3)^{2}}} L_{ u }( t - k \ frac{3 T_{tot}}{2n } )\]И вуаля ! Мы создали функцию загрузки!
Нахождение порога масштабирования
Чтобы закончить, нам просто нужно запустить алгоритм дихотомии, который изменяет порог, чтобы найти самое высокое значение, при котором нагрузка на экземпляр никогда не превышает своего максимального предела по всей функции нагрузки. (Это то, что делает приложение.)
Вывод других параметров оркестровки
Как только вы нашли свой порог масштабирования ($S_{up}$), другие параметры рассчитать довольно просто.
Из $S_{up}$ вы узнаете максимальное количество экземпляров. (Вы также можете найти максимальную нагрузку на функцию загрузки и разделить на максимальную нагрузку на экземпляр, округленную в большую сторону.)
Минимальное количество ($N_{min}$) экземпляров должно быть определено в соответствии с вашей инфраструктурой. (Я бы рекомендовал иметь как минимум одну реплику на зону доступности.) Но также необходимо учитывать функцию нагрузки: поскольку функция Гаусса возрастает довольно быстро, распределение нагрузки в начале более интенсивно (на реплику), поэтому вы может потребоваться увеличить минимальное количество реплик, чтобы смягчить этот эффект. (Это, скорее всего, увеличит ваш $S_{up}$.)
Наконец, после того как вы определили минимальное количество реплик, вы можете рассчитать порог масштабирования ($S_{down}$), учитывая следующее: $N_{min}+1$ до $N_{min}$, мы должны убедиться, что порог масштабирования не сработает сразу после масштабирования. Если это разрешено, это будет иметь эффект йо-йо. Другими словами:
\[( N_{мин.} + 1) S_{вниз} < N_{мин.}S_{вверх}\]Или:
\[S_{вниз} < \frac{N_{мин}}{N_{мин}+1}S_{вверх}\]Кроме того, мы можем признать, что чем дольше ваш кластер настроен на ожидание перед масштабированием, тем безопаснее установить $S_{down}$ ближе к более высокому пределу. Опять же, вам придется найти баланс, который вас устраивает.
Обратите внимание, что при использовании системы оркестровки Mesosphere Marathon с ее автомасштабированием максимальное количество экземпляров, которые могут быть одновременно удалены из масштабирования, привязано к AS_AUTOSCALE_MULTIPLIER
($A_{mult}$), что означает:
Как насчет функции загрузки пользователя?
Да, это небольшая проблема, и не самая простая для математического решения — если это вообще возможно.
Чтобы обойти эту проблему, идея состоит в том, чтобы запустить один экземпляр вашего приложения и многократно увеличивать количество пользователей, выполняющих одну и ту же задачу, пока нагрузка на сервер не достигнет максимума, который был назначен (но не превысит его). Затем разделите на количество пользователей и рассчитайте среднее время запроса. Повторите эту процедуру с каждым действием, которое вы хотите интегрировать в функцию загрузки пользователя, добавьте немного времени, и готово.
Я в курсе, что эта процедура предполагает учет того, что каждый запрос пользователя имеет постоянную нагрузку по его обработке (что заведомо неверно), но масса пользователей будет создавать такой эффект, так как каждый из них не находится на одном и том же шаге обработки в одно и то же время. . Так что я думаю, что это приемлемое приближение, но оно еще раз намекает на то, что вы имеете дело с большим количеством пользователей.
Вы также можете попробовать другие методы, такие как графики пламени процессора. Но я думаю, будет очень сложно создать точную формулу, которая будет связывать действия пользователя с потреблением ресурсов.
Представляем app-autoscaling-calculator
А теперь, для небольшого веб-приложения, упомянутого повсюду: оно принимает в качестве входных данных вашу функцию загрузки, конфигурацию вашего контейнерного оркестратора и некоторые другие общие параметры и возвращает порог масштабирования и другие показатели, связанные с экземпляром.
Проект размещен на GitHub, но также доступна живая версия.
Вот результат, выдаваемый веб-приложением на тестовых данных (в Kubernetes):
Масштабирование микросервисов: больше не нужно возиться в темноте
Когда дело доходит до архитектур приложений микросервисов, развертывание контейнеров становится центральной точкой всей инфраструктуры. И чем лучше настроены оркестратор и контейнеры, тем более плавным будет время выполнения.
Те из нас, кто занимается услугами DevOps, всегда ищут лучшие способы настройки параметров оркестровки для наших приложений. Давайте применим более математический подход к автомасштабированию микросервисов!