Haga los cálculos: escalado de aplicaciones de microservicios con orquestadores
Publicado: 2022-03-11No es una sorpresa que la arquitectura de aplicaciones de microservicios continúe invadiendo el diseño de software. Es mucho más conveniente distribuir la carga, crear implementaciones de alta disponibilidad y administrar actualizaciones mientras se facilita el desarrollo y la administración del equipo.
Pero la historia seguramente no es la misma sin los orquestadores de contenedores.
Es fácil querer usar todas sus funciones clave, especialmente el escalado automático. Qué bendición es ver las implementaciones de contenedores que fluctúan durante todo el día, con un tamaño suave para manejar la carga actual, liberando nuestro tiempo para otras tareas. Estamos orgullosos de lo que muestran nuestras herramientas de monitoreo de contenedores; mientras tanto, acabamos de configurar un par de ajustes: ¡sí, eso es (casi) todo lo que se necesitó para crear la magia!
Eso no quiere decir que no haya motivos para estar orgullosos de esto: estamos seguros de que nuestros usuarios están teniendo una buena experiencia y que no estamos desperdiciando dinero con una infraestructura de gran tamaño. ¡Esto ya es bastante considerable!
Y, por supuesto, ¡qué viaje fue llegar allí! Porque incluso si al final no hay que configurar muchas configuraciones, es mucho más complicado de lo que normalmente pensamos antes de que podamos comenzar. Número mínimo/máximo de réplicas, umbrales de escala ascendente/descendente, períodos de sincronización, retrasos de enfriamiento: todas esas configuraciones están muy vinculadas. La modificación de uno probablemente afectará a otro, pero aún debe organizar una combinación equilibrada que se adapte tanto a su aplicación/implementación como a su infraestructura. Y sin embargo, no encontrarás ningún libro de cocina ni ninguna fórmula mágica en Internet, ya que depende mucho de tus necesidades.
La mayoría de nosotros primero los establecemos en valores "aleatorios" o predeterminados que luego ajustamos de acuerdo con lo que encontramos mientras monitoreamos. Eso me hizo pensar: ¿Qué pasaría si pudiéramos establecer un procedimiento más “matemático” que nos ayude a encontrar la combinación ganadora?
Cálculo de parámetros de orquestación de contenedores
Cuando pensamos en microservicios de escalado automático para una aplicación, en realidad buscamos mejorar dos puntos principales:
- Asegurarse de que la implementación pueda escalar rápidamente en el caso de un aumento rápido de la carga (para que los usuarios no enfrenten tiempos de espera o HTTP 500)
- Reducir el costo de la infraestructura (es decir, evitar que las instancias se sobrecarguen)
Básicamente, esto significa optimizar los umbrales del software del contenedor para escalar hacia arriba y hacia abajo. (El algoritmo de Kubernetes tiene un solo parámetro para los dos).
Mostraré más adelante que todos los parámetros relacionados con la instancia están vinculados al umbral de escalado superior. Este es el más difícil de calcular, de ahí este artículo.
Nota: con respecto a los parámetros que se establecen en todo el clúster, no tengo ningún buen procedimiento para ellos, pero al final de este artículo, presentaré una pieza de software (una página web estática) que los tiene en cuenta al calcular los parámetros de escalado automático de una instancia. De esa manera, podrá variar sus valores para considerar su impacto.
Cálculo del umbral de ampliación
Para que este método funcione, debe asegurarse de que su aplicación cumpla con los siguientes requisitos:
- La carga debe distribuirse uniformemente en cada instancia de su aplicación (de forma rotativa)
- Los tiempos de solicitud deben ser más cortos que el intervalo de verificación de carga de su clúster de contenedores.
- Debe considerar ejecutar el procedimiento en una gran cantidad de usuarios (definido más adelante).
La razón principal de esas condiciones proviene del hecho de que el algoritmo no calcula la carga por usuario sino como una distribución (explicado más adelante).
Obteniendo todo Gaussiano
Primero tenemos que formular una definición para un aumento rápido de la carga o, en otras palabras, el peor de los casos. Para mí, una buena forma de traducirlo es: tener una gran cantidad de usuarios realizando acciones que consumen recursos en un corto período de tiempo , y siempre existe la posibilidad de que suceda mientras otro grupo de usuarios o servicios están realizando otras tareas. Entonces, comencemos con esta definición e intentemos extraer algunas matemáticas. (Prepara tu aspirina.)
Introduciendo algunas variables:
- $N_{u}$, el “gran número de usuarios”
- $L_{u}(t)$, la carga generada por un solo usuario que realiza la "operación que consume recursos" ($t=0$ señala el momento en que el usuario inicia la operación)
- $L_{tot}(t)$, la carga total (generada por todos los usuarios)
- $T_{tot}$, el “período corto de tiempo”
En el mundo matemático, hablando de un gran número de usuarios realizando lo mismo al mismo tiempo, la distribución de los usuarios en el tiempo sigue una distribución gaussiana (o normal), cuya fórmula es:
\[G(t) = \frac{1}{\sigma \sqrt{2 \pi}} e^{\frac{-(t-\mu)^2}{2 \sigma^2}}\]Aquí:
- µ es el valor esperado
- σ es la desviación estándar
Y se grafica de la siguiente manera (con $µ=0$):
Probablemente le recuerde algunas clases que ha tomado, nada nuevo. Sin embargo, aquí nos enfrentamos a nuestro primer problema: para ser matemáticamente precisos, tendríamos que considerar un rango de tiempo de $-\infty$ a $+\infty$, que obviamente no se puede calcular.
Pero mirando el gráfico, notamos que los valores fuera del intervalo $[-3σ, 3σ]$ están muy cerca de cero y no varían mucho, por lo que su efecto es realmente insignificante y se puede dejar de lado. Esto es más cierto, ya que nuestro objetivo es probar la ampliación de nuestra aplicación, por lo que buscamos variaciones de un gran número de usuarios.
Además, dado que el intervalo $[-3σ, 3σ]$ contiene el 99,7 por ciento de nuestros usuarios, está lo suficientemente cerca del total para trabajar en él, y solo necesitamos multiplicar $N_{u}$ por 1,003 para compensar la diferencia. Seleccionando este intervalo nos da $µ=3σ$ (ya que vamos a trabajar desde $t=0$).
En cuanto a la correspondencia con $T_{tot}$, elegir que sea igual a $6σ$ ($[-3σ, 3σ]$) no será una buena aproximación, ya que el 95,4 por ciento de los usuarios se encuentran en el intervalo $[- 2σ, 2σ]$, que dura $4σ$. Por lo tanto, elegir $T_{tot}$ para que sea igual a $6σ$ agregará la mitad del tiempo para solo el 4,3 por ciento de los usuarios, lo que no es realmente representativo. Entonces elegimos tomar $T_{tot}=4σ$, y podemos deducir:
\(σ=\frac{T_{total}}{4}\) y \(µ=\frac{3}{4} * T_{total}\)
¿Se sacaron esos valores de un sombrero? Si. Pero este es su propósito, y esto no afectará el procedimiento matemático. Esas constantes son para nosotros y definen nociones relacionadas con nuestra hipótesis. Esto solo significa que ahora que los tenemos configurados, nuestro peor escenario puede traducirse como:
La carga generada por el 99,7 por ciento de $N{u}$, realizando una operación de consumo $L{u}(t)$ y donde el 95,4 por ciento de ellos lo están haciendo dentro de la duración $T{tot}$.
(Esto es algo que vale la pena recordar al usar la aplicación web).
Inyectando los resultados anteriores en la función de distribución de usuarios (Gaussiana), podemos simplificar la ecuación de la siguiente manera:
\[G(t) = \frac{4 N_{u}}{T_{total} \sqrt{2 \pi}} e^\frac{-(4t-3T_{total})^2}{T_{total }^2}\]De ahora en adelante, teniendo $σ$ y $µ$ definidos, estaremos trabajando en el intervalo $t \in [0, \frac{3}{2}T_{tot}]$ (duración $6σ$).

¿Cuál es la carga total de usuarios?
El segundo paso en el escalado automático de microservicios es calcular $L_{tot}(t)$.
Dado que $G(t)$ es una distribución , para recuperar el número de usuarios en un momento determinado, tenemos que calcular su integral (o usar su función de distribución acumulativa). Pero dado que no todos los usuarios inician sus operaciones al mismo tiempo, sería un verdadero lío intentar introducir $L_{u}(t)$ y reducir la ecuación a una fórmula utilizable.
Entonces, para hacer esto más fácil, usaremos una suma de Riemann, que es una forma matemática de aproximar una integral usando una suma finita de formas pequeñas (aquí usaremos rectángulos). Cuantas más formas (subdivisiones), más preciso será el resultado. Otro beneficio de usar subdivisiones proviene del hecho de que podemos considerar que todos los usuarios dentro de una subdivisión han iniciado sus operaciones al mismo tiempo.
Volviendo a la suma de Riemann, tiene la siguiente propiedad que se conecta con las integrales:
\[\int_{a}^{b} f( x )dx = \lim_{n \rightarrow \infty } \sum_{k=1}^{n} ( x_{k} - x_{k-1} ) f( x_{k} )\]Con $x_k$ definido de la siguiente manera:
\[x_{ k } = a + k\frac{ b - a }{ n }, 0 \leq k \leq n\]Esto es cierto donde:
- $n$ es el número de subdivisiones.
- $a$ es el límite inferior, aquí 0.
- $b$ es el límite superior, aquí $\frac{3}{2}*T_{tot}$.
- $f$ es la función, aquí $G$, para aproximar su área.
Nota: El número de usuarios presentes en una subdivisión no es un número entero. Esta es la razón de dos de los requisitos previos: tener una gran cantidad de usuarios (para que la parte decimal no sea demasiado impactante) y la necesidad de que la carga se distribuya uniformemente en cada instancia.
También tenga en cuenta que podemos ver la forma rectangular de la subdivisión en el lado derecho de la definición de la suma de Riemann.
Ahora que tenemos la fórmula de suma de Riemann, podemos decir que el valor de carga en el momento $t$ es la suma del número de usuarios de cada subdivisión multiplicado por la función de carga de usuarios en su momento correspondiente . Esto se puede escribir como:
\[L_{ tot }( t ) = \lim_{n \rightarrow \infty} \sum_{ k=1 }^{ n } ( x_{k} - x_{k-1} )G( x_{k} ) L_{ u }( t - x_{k} )\]Después de reemplazar variables y simplificar la fórmula, esto se convierte en:
\[L_{ tot }( t ) = \frac{6 N_{u}}{\sqrt{2 \pi}} \lim_{n \rightarrow \infty} \sum_{ k=1 }^{ n } (\ fracción{1}{n}) e^{-{(\frac{6k}{n} - 3)^{2}}} L_{ u }( t - k \frac{3 T_{tot}}{2n } )\]¡Y listo ! ¡Creamos la función de carga!
Encontrar el umbral de escalamiento vertical
Para terminar, solo necesitamos ejecutar un algoritmo de dicotomía que varía el umbral para encontrar el valor más alto donde la carga por instancia nunca supera su límite máximo en toda la función de carga. (Esto es lo que hace la aplicación).
Deducción de otros parámetros de orquestación
Tan pronto como haya encontrado su umbral de ampliación ($S_{up}$), otros parámetros son muy fáciles de calcular.
Desde $S_{up}$ sabrá su número máximo de instancias. (También puede buscar la carga máxima en su función de carga y dividirla por la carga máxima por instancia, redondeada).
El número mínimo ($N_{min}$) de instancias debe definirse de acuerdo con su infraestructura. (Recomendaría tener un mínimo de una réplica por AZ). Pero también debe tener en cuenta la función de carga: como una función gaussiana aumenta con bastante rapidez, la distribución de carga es más intensa (por réplica) al principio, por lo que Es posible que desee aumentar el número mínimo de réplicas para amortiguar este efecto. (Es muy probable que esto aumente sus $S_{up}$).
Finalmente, una vez que haya definido el número mínimo de réplicas, puede calcular el umbral de reducción ($S_{down}$) teniendo en cuenta lo siguiente: Dado que la reducción de una única réplica no tiene más efecto en otras instancias que cuando se reduce de $N_{min}+1$ a $N_{min}$, debemos asegurarnos de que el umbral de ampliación no se active justo después de la reducción. Si está permitido, esto tendrá un efecto yo-yo. En otras palabras:
\[( N_{ min } + 1) S_{ abajo } < N_{ min }S_{ arriba }\]O:
\[S_{ abajo } < \frac{N_{ min }}{N_{min}+1}S_{ arriba }\]Además, podemos admitir que cuanto más tiempo esté configurado su clúster para esperar antes de reducirse, más seguro será configurar $S_{down}$ más cerca del límite superior. Una vez más, tendrás que encontrar el equilibrio que más te convenga.
Tenga en cuenta que al usar el sistema de orquestación Mesosphere Marathon con su escalador automático, la cantidad máxima de instancias que se pueden eliminar a la vez de la reducción está vinculada a AS_AUTOSCALE_MULTIPLIER
($A_{mult}$), lo que implica:
¿Qué pasa con la función de carga de usuarios?
Sí, eso es un pequeño problema, y no es el más fácil de resolver matemáticamente, si es que es posible.
Para solucionar este problema, la idea es ejecutar una sola instancia de su aplicación y aumentar la cantidad de usuarios que realizan la misma tarea repetidamente hasta que la carga del servidor alcance el máximo asignado (pero no por encima). Luego divida por el número de usuarios y calcule el tiempo promedio de la solicitud. Repita este procedimiento con cada acción que desee integrar en su función de carga de usuarios, agregue algo de tiempo y listo.
Soy consciente de que este procedimiento implica considerar que cada solicitud de usuario tiene una carga constante sobre su procesamiento (lo que obviamente es incorrecto), pero la masa de usuarios creará este efecto ya que cada uno de ellos no está en el mismo paso de procesamiento al mismo tiempo. . Así que supongo que esta es una aproximación aceptable, pero una vez más insinúa que estás tratando con una gran cantidad de usuarios.
También puede probar con otros métodos, como gráficos de llamas de CPU. Pero creo que será muy difícil crear una fórmula precisa que vincule las acciones de los usuarios con el consumo de recursos.
Presentamos la app-autoscaling-calculator
Y ahora, para la pequeña aplicación web mencionada en todo momento: toma como entrada su función de carga, la configuración de su orquestador de contenedores y algunos otros parámetros generales y devuelve el umbral de escalamiento vertical y otras cifras relacionadas con la instancia.
El proyecto está alojado en GitHub, pero también tiene disponible una versión en vivo.
Este es el resultado proporcionado por la aplicación web, ejecutado con los datos de prueba (en Kubernetes):
Microservicios escalables: No más torpezas en la oscuridad
Cuando se trata de arquitecturas de aplicaciones de microservicios, la implementación de contenedores se convierte en un punto central de toda la infraestructura. Y cuanto mejor estén configurados el orquestador y los contenedores, más fluido será el tiempo de ejecución.
Aquellos de nosotros en el campo de los servicios DevOps siempre estamos buscando mejores formas de ajustar los parámetros de orquestación para nuestras aplicaciones. ¡Tomemos un enfoque más matemático para el escalado automático de microservicios!