Como regla general, siempre existe la necesidad de proporcionar un conjunto de recursos dedicado a cualquier aplicación para su funcionamiento correcto y estable. Pero, ¿qué pasa si varias aplicaciones funcionan con las mismas capacidades a la vez? ¿Cómo proporcionar los recursos mínimos necesarios para cada uno de ellos? ¿Cómo puedo limitar el consumo de recursos? ¿Cómo distribuir correctamente la carga entre nodos? ¿Cómo garantizar el mecanismo de escala horizontal en caso de aumento de carga en la aplicación?

Debe comenzar con qué tipos básicos de recursos existen en el sistema: esto, por supuesto, el tiempo de procesador y la RAM. En los manifiestos de k8, estos tipos de recursos se miden en las siguientes unidades:
- CPU - en los núcleos
- RAM - en bytes
Además, para cada recurso existe la oportunidad de establecer dos tipos de requisitos: solicitudes y límites . Solicitudes: describe los requisitos mínimos para que los recursos libres del nodo ejecuten el contenedor (y el hogar como un todo), mientras que limit establece un límite estricto en los recursos disponibles para el contenedor.
Es importante comprender que en el manifiesto no es necesario definir explícitamente ambos tipos, y el comportamiento será el siguiente:
- Si solo los límites del recurso se establecen explícitamente, las solicitudes de este recurso toman automáticamente un valor igual a los límites (esto se puede verificar llamando a describir entidades). Es decir de hecho, la operación del contenedor estará limitada por la misma cantidad de recursos que requiere para ejecutarse.
- Si solo las solicitudes se establecen explícitamente para un recurso, entonces no se establecen restricciones sobre este recurso, es decir el contenedor está limitado solo por los recursos del nodo en sí.
También es posible configurar la gestión de recursos no solo a nivel de un contenedor específico, sino también a nivel de espacio de nombres utilizando las siguientes entidades:
- LimitRange : describe la política de restricción a nivel de contenedor / hogar en ns y es necesaria para describir las restricciones predeterminadas en el contenedor / hogar, así como para evitar la creación de contenedores / hogares obviamente gordos (o viceversa), limite su número y determine la posible diferencia entre los límites y solicitudes
- ResourceQuotas : describe la política de restricción en general para todos los contenedores en ns y se usa, por regla general, para diferenciar recursos entre entornos (útil cuando los entornos no están delimitados rígidamente a nivel de nodos)
Los siguientes son ejemplos de manifiestos donde se establecen límites de recursos:
En el nivel de contenedor específico:
containers: - name: app-nginx image: nginx resources: requests: memory: 1Gi limits: cpu: 200m
Es decir en este caso, para iniciar un contenedor con nginx, necesitará al menos la presencia de 1G OP y 0.2 CPU libres en el nodo, mientras que el contenedor máximo puede consumir 0.2 CPU y todos los OP disponibles en el nodo.
En el nivel entero ns:
apiVersion: v1 kind: ResourceQuota metadata: name: nxs-test spec: hard: requests.cpu: 300m requests.memory: 1Gi limits.cpu: 700m limits.memory: 2Gi
Es decir la suma de todos los contenedores de solicitud en los ns predeterminados no puede exceder los 300 m para la CPU y 1G para el OP, y la suma de todos los límites es 700 m para la CPU y 2G para el OP.
Restricciones predeterminadas para contenedores en ns:
apiVersion: v1 kind: LimitRange metadata: name: nxs-limit-per-container spec: limits: - type: Container defaultRequest: cpu: 100m memory: 1Gi default: cpu: 1 memory: 2Gi min: cpu: 50m memory: 500Mi max: cpu: 2 memory: 4Gi
Es decir en el espacio de nombres predeterminado para todos los contenedores, por defecto, la solicitud se establecerá en 100 m para la CPU y 1G para el OP, límite - 1 CPU y 2G. Al mismo tiempo, también se estableció una restricción sobre los posibles valores en la solicitud / límite para la CPU (50m <x <2) y RAM (500M <x <4G).
Limitaciones en el nivel de hogar ns:
apiVersion: v1 kind: LimitRange metadata: name: nxs-limit-pod spec: limits: - type: Pod max: cpu: 4 memory: 1Gi
Es decir para cada hogar en el ns predeterminado, se establecerá un límite de 4 vCPU y 1G.
Ahora me gustaría decirle qué ventajas puede brindarnos la instalación de estas restricciones.
El mecanismo de equilibrio de carga entre nodos.
Como saben, el componente k8s, como el planificador , que funciona de acuerdo con cierto algoritmo, es responsable de la distribución de los hogares sobre los nodos. Este algoritmo en el proceso de elegir el nodo óptimo para ejecutar pasa por dos etapas:
- Filtrado
- Ranking
Es decir de acuerdo con la política descrita, los nodos se seleccionan inicialmente en los que se puede iniciar un hogar en función de un conjunto de predicados (incluido si el nodo tiene suficientes recursos para ejecutar un hogar - PodFitsResources), y luego se otorgan puntos para cada uno de estos nodos, de acuerdo con las prioridades (incluyendo, cuantos más recursos libres tenga un nodo, más puntos se le asignan, LeastResourceAllocation / LeastRequestedPriority / BalancedResourceAllocation) y se ejecuta en el nodo con más puntos (si varios nodos satisfacen esta condición a la vez, se selecciona uno aleatorio).
Al mismo tiempo, debe comprender que el planificador, al evaluar los recursos disponibles del nodo, se centra en los datos almacenados en etcd, es decir, por la cantidad del recurso solicitado / límite de cada pod que se ejecuta en este nodo, pero no por el consumo real de recursos. Esta información se puede obtener en la salida del kubectl describe node $NODE
, por ejemplo:
Aquí vemos todos los pods que se ejecutan en un nodo en particular, así como los recursos que cada uno de ellos solicita. Y así es como se ven los registros del planificador al iniciar el pod cronjob-cron-events-1573793820-xt6q9 (esta información aparece en el registro del planificador al establecer el décimo nivel de inicio de sesión en los argumentos del comando de inicio --v = 10):
gaviota ancha I1115 07:57:21.637791 1 scheduling_queue.go:908] About to try and schedule pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 I1115 07:57:21.637804 1 scheduler.go:453] Attempting to schedule pod: nxs-stage/cronjob-cron-events-1573793820-xt6q9 I1115 07:57:21.638285 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s5 is allowed, Node is running only 16 out of 110 Pods. I1115 07:57:21.638300 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s6 is allowed, Node is running only 20 out of 110 Pods. I1115 07:57:21.638322 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s3 is allowed, Node is running only 20 out of 110 Pods. I1115 07:57:21.638322 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s4 is allowed, Node is running only 17 out of 110 Pods. I1115 07:57:21.638334 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s10 is allowed, Node is running only 16 out of 110 Pods. I1115 07:57:21.638365 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s12 is allowed, Node is running only 9 out of 110 Pods. I1115 07:57:21.638334 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s11 is allowed, Node is running only 11 out of 110 Pods. I1115 07:57:21.638385 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s1 is allowed, Node is running only 19 out of 110 Pods. I1115 07:57:21.638402 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s2 is allowed, Node is running only 21 out of 110 Pods. I1115 07:57:21.638383 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s9 is allowed, Node is running only 16 out of 110 Pods. I1115 07:57:21.638335 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s8 is allowed, Node is running only 18 out of 110 Pods. I1115 07:57:21.638408 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s13 is allowed, Node is running only 8 out of 110 Pods. I1115 07:57:21.638478 1 predicates.go:1369] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s10 is allowed, existing pods anti-affinity terms satisfied. I1115 07:57:21.638505 1 predicates.go:1369] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s8 is allowed, existing pods anti-affinity terms satisfied. I1115 07:57:21.638577 1 predicates.go:1369] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s9 is allowed, existing pods anti-affinity terms satisfied. I1115 07:57:21.638583 1 predicates.go:829] Schedule Pod nxs-stage/cronjob-cron-events-1573793820-xt6q9 on Node nxs-k8s-s7 is allowed, Node is running only 25 out of 110 Pods. I1115 07:57:21.638932 1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s10: BalancedResourceAllocation, capacity 39900 millicores 66620178432 memory bytes, total request 2343 millicores 9640186880 memory bytes, score 9 I1115 07:57:21.638946 1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s10: LeastResourceAllocation, capacity 39900 millicores 66620178432 memory bytes, total request 2343 millicores 9640186880 memory bytes, score 8 I1115 07:57:21.638961 1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s9: BalancedResourceAllocation, capacity 39900 millicores 66620170240 memory bytes, total request 4107 millicores 11307422720 memory bytes, score 9 I1115 07:57:21.638971 1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s8: BalancedResourceAllocation, capacity 39900 millicores 66620178432 memory bytes, total request 5847 millicores 24333637120 memory bytes, score 7 I1115 07:57:21.638975 1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s9: LeastResourceAllocation, capacity 39900 millicores 66620170240 memory bytes, total request 4107 millicores 11307422720 memory bytes, score 8 I1115 07:57:21.638990 1 resource_allocation.go:78] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s8: LeastResourceAllocation, capacity 39900 millicores 66620178432 memory bytes, total request 5847 millicores 24333637120 memory bytes, score 7 I1115 07:57:21.639022 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s10: TaintTolerationPriority, Score: (10) I1115 07:57:21.639030 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s8: TaintTolerationPriority, Score: (10) I1115 07:57:21.639034 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s9: TaintTolerationPriority, Score: (10) I1115 07:57:21.639041 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s10: NodeAffinityPriority, Score: (0) I1115 07:57:21.639053 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s8: NodeAffinityPriority, Score: (0) I1115 07:57:21.639059 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s9: NodeAffinityPriority, Score: (0) I1115 07:57:21.639061 1 interpod_affinity.go:237] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s10: InterPodAffinityPriority, Score: (0) I1115 07:57:21.639063 1 selector_spreading.go:146] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s10: SelectorSpreadPriority, Score: (10) I1115 07:57:21.639073 1 interpod_affinity.go:237] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s8: InterPodAffinityPriority, Score: (0) I1115 07:57:21.639077 1 selector_spreading.go:146] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s8: SelectorSpreadPriority, Score: (10) I1115 07:57:21.639085 1 interpod_affinity.go:237] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s9: InterPodAffinityPriority, Score: (0) I1115 07:57:21.639088 1 selector_spreading.go:146] cronjob-cron-events-1573793820-xt6q9 -> nxs-k8s-s9: SelectorSpreadPriority, Score: (10) I1115 07:57:21.639103 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s10: SelectorSpreadPriority, Score: (10) I1115 07:57:21.639109 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s8: SelectorSpreadPriority, Score: (10) I1115 07:57:21.639114 1 generic_scheduler.go:726] cronjob-cron-events-1573793820-xt6q9_nxs-stage -> nxs-k8s-s9: SelectorSpreadPriority, Score: (10) I1115 07:57:21.639127 1 generic_scheduler.go:781] Host nxs-k8s-s10 => Score 100037 I1115 07:57:21.639150 1 generic_scheduler.go:781] Host nxs-k8s-s8 => Score 100034 I1115 07:57:21.639154 1 generic_scheduler.go:781] Host nxs-k8s-s9 => Score 100037 I1115 07:57:21.639267 1 scheduler_binder.go:269] AssumePodVolumes for pod "nxs-stage/cronjob-cron-events-1573793820-xt6q9", node "nxs-k8s-s10" I1115 07:57:21.639286 1 scheduler_binder.go:279] AssumePodVolumes for pod "nxs-stage/cronjob-cron-events-1573793820-xt6q9", node "nxs-k8s-s10": all PVCs bound and nothing to do I1115 07:57:21.639333 1 factory.go:733] Attempting to bind cronjob-cron-events-1573793820-xt6q9 to nxs-k8s-s10
Aquí vemos que inicialmente el planificador realiza el filtrado y forma una lista de 3 nodos en los que es posible ejecutar (nxs-k8s-s8, nxs-k8s-s9, nxs-k8s-s10). Luego calcula los puntos de acuerdo con varios parámetros (incluyendo BalancedResourceAllocation, LeastResourceAllocation) para cada uno de estos nodos para determinar el nodo más adecuado. Al final, se planifica bajo el nodo con la mayor cantidad de puntos (aquí, dos nodos a la vez tienen el mismo número de puntos 100037, por lo que se selecciona uno aleatorio: nxs-k8s-s10).
Conclusión : si los pods funcionan en el nodo para el que no hay restricciones, entonces para k8s (desde el punto de vista del consumo de recursos) esto será equivalente a si dichos pods estuvieran completamente ausentes en este nodo. Por lo tanto, si tiene una vaina condicionalmente con un proceso voraz (por ejemplo, wowza) y no hay restricciones para ello, entonces puede surgir una situación cuando, de hecho, el dado ha consumido todos los recursos del nodo, pero para k8s este nodo se considera descargado y se le otorgará el mismo número de puntos al clasificar (es decir, en puntos con una evaluación de los recursos disponibles), así como un nodo que no tiene campos de trabajo, lo que en última instancia puede conducir a una distribución desigual de la carga entre los nodos.
Desalojo del hogar
Como sabe, a cada uno de los pods se le asigna una de las 3 clases de QoS:
- garantizado : se asigna cuando la solicitud y el límite se establecen para cada contenedor en el hogar para memoria y CPU, y estos valores deben coincidir
- burstable : al menos un contenedor en el hogar tiene solicitud y límite, mientras que solicitud <límite
- mejor esfuerzo : cuando ningún recipiente en el hogar tiene recursos limitados
Al mismo tiempo, cuando hay una escasez de recursos (disco, memoria) en el nodo, kubelet comienza a clasificar y expulsar los pods de acuerdo con un cierto algoritmo que tiene en cuenta la prioridad del pod y su clase de QoS. Por ejemplo, si estamos hablando de RAM, entonces, según la clase de QoS, los puntos se otorgan de acuerdo con el siguiente principio:
- Garantizado : -998
- Mejor esfuerzo : 1000
- Burstable : min (max (2, 1000 - (1000 * memoryRequestBytes) / machineMemoryCapacityBytes), 999)
Es decir con la misma prioridad, kubelet primero expulsará las vainas con el mejor esfuerzo de la clase de QoS del nodo.
Conclusión : si desea reducir la probabilidad de desalojo del nodo necesario del nodo en caso de recursos insuficientes, entonces, junto con la prioridad, también debe ocuparse de establecer la solicitud / límite para él.
Mecanismo de escalamiento horizontal horizontal de hogar (HPA)
Cuando la tarea es aumentar y disminuir automáticamente el número de pod dependiendo del uso de recursos (sistema - CPU / RAM o usuario - rps), k8 como HPA (Horizontal Pod Autoscaler) pueden ayudar en su solución. El algoritmo de los cuales es el siguiente:
- Se determinan las lecturas actuales del recurso observado (currentMetricValue)
- Se determinan los valores deseados para el recurso (nedMetricValue), que se establecen para los recursos del sistema mediante solicitud
- Se determina el número actual de réplicas (currentReplicas)
- La siguiente fórmula calcula el número deseado de réplicas (réplicas deseadas)
desiredReplicas = [currentReplicas * (currentMetricValue / desiredMetricValue)]
Sin embargo, la escala no ocurrirá cuando el coeficiente (currentMetricValue / deseadoMetricValue) esté cerca de 1 (podemos establecer el error permitido nosotros mismos, por defecto es 0.1).
Considere hpa usando la aplicación de prueba de aplicación (descrita como Implementación), donde es necesario cambiar el número de réplicas, dependiendo del consumo de CPU:
Manifiesto de solicitud
kind: Deployment apiVersion: apps/v1beta2 metadata: name: app-test spec: selector: matchLabels: app: app-test replicas: 2 template: metadata: labels: app: app-test spec: containers: - name: nginx image: registry.nixys.ru/generic-images/nginx imagePullPolicy: Always resources: requests: cpu: 60m ports: - name: http containerPort: 80 - name: nginx-exporter image: nginx/nginx-prometheus-exporter resources: requests: cpu: 30m ports: - name: nginx-exporter containerPort: 9113 args: - -nginx.scrape-uri - http://127.0.0.1:80/nginx-status
Es decir vemos que debajo de la aplicación se inicia inicialmente en dos instancias, cada una de las cuales contiene dos contenedores nginx y nginx-exporter, para cada una de las cuales se dan solicitudes para la CPU.
Manifiesto de HPA
apiVersion: autoscaling/v2beta2 kind: HorizontalPodAutoscaler metadata: name: app-test-hpa spec: maxReplicas: 10 minReplicas: 2 scaleTargetRef: apiVersion: extensions/v1beta1 kind: Deployment name: app-test metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 30
Es decir creamos un hpa que supervisará la prueba de implementación de la aplicación y ajustará el número de hogares con la aplicación en función del indicador de la CPU (esperamos que el hogar consuma el 30% de la CPU solicitada por este), mientras que el número de réplicas está en el rango de 2-10.
Ahora, consideraremos el mecanismo de operación hpa si aplicamos una carga a uno de los hogares:
Total tenemos lo siguiente:
- Valor deseado (deseadoMetricValue): de acuerdo con la configuración de hpa, tenemos un 30%
- Valor actual (currentMetricValue): para el cálculo, el controlador-administrador calcula el valor promedio del consumo de recursos en%, es decir condicionalmente hace lo siguiente:
- Obtiene los valores absolutos de las métricas de hogar del servidor de métricas, es decir 101m y 4m
- Calcula el valor absoluto promedio, es decir (101m + 4m) / 2 = 53m
- Obtiene el valor absoluto para el consumo de recursos deseado (para esto, se suma la solicitud de todos los contenedores) 60m + 30m = 90m
- Calcula el porcentaje promedio de consumo de CPU en relación con el hogar de solicitud, es decir 53m / 90m * 100% = 59%
Ahora tenemos todo lo necesario para determinar si es necesario cambiar el número de réplicas, para esto calculamos el coeficiente:
ratio = 59% / 30% = 1.96
Es decir el número de réplicas debe aumentarse ~ 2 veces y completar [2 * 1.96] = 4.
Conclusión: Como puede ver, para que este mecanismo funcione, un requisito previo es incluir la disponibilidad de solicitudes para todos los contenedores en el hogar observado.
El mecanismo de autoescalado horizontal de nodos (Cluster Autoscaler)
Para neutralizar el impacto negativo en el sistema durante ráfagas de carga, la presencia de un hpa sintonizado no es suficiente. Por ejemplo, de acuerdo con la configuración en el administrador del controlador hpa, decide que la cantidad de réplicas debe aumentarse 2 veces, sin embargo, no hay recursos libres en los nodos para ejecutar tal cantidad de pods (es decir, el nodo no puede proporcionar los recursos solicitados para las solicitudes de pod) y estos pods entrar en el estado pendiente.
En este caso, si el proveedor tiene el IaaS / PaaS apropiado (por ejemplo, GKE / GCE, AKS, EKS, etc.), una herramienta como Node Autoscaler puede ayudarnos. Le permite establecer el número máximo y mínimo de nodos en el clúster y ajustar automáticamente el número actual de nodos (accediendo a la API del proveedor de la nube para ordenar / eliminar nodos) cuando hay escasez de recursos en el clúster y los pods no se pueden programar (en el estado Pendiente).
Conclusión: para poder escalar automáticamente los nodos, es necesario especificar solicitudes en los contenedores de hogares para que k8s pueda evaluar correctamente la carga de nodos y, en consecuencia, informar que no hay recursos en el clúster para iniciar el próximo hogar.
Conclusión
Cabe señalar que establecer límites de recursos para el contenedor no es un requisito previo para el lanzamiento exitoso de la aplicación, pero es mejor hacerlo por las siguientes razones:
- Para una operación más precisa del planificador en términos de equilibrio de carga entre nodos k8s
- Para reducir la probabilidad de un evento de desalojo del hogar
- Para hogares de aplicación de escala automática horizontal (HPA)
- Para el autoescalado horizontal de nodos (Cluster Autoscaling) para proveedores en la nube
Lea también otros artículos en nuestro blog: