martes, 15 de mayo de 2018

Kubernetes [Best Practices ] Solicitudes de recursos y límites

En Google Cloud Español estamos  pendientes de  Sandeep Dinesh y sus publicaciones sobre kubernetes, esta semana nos habla de la gestion de los Recursos y sus limites con kubernetes.

Cuando en Kubernetes se programa un Pod, es importante que los contenedores tengan recursos suficientes para que puedan trabajar. Si se programa una aplicación que necesita grandes requerimientos  en un nodo con recursos limitados, es posible que el nodo se quede sin memoria o recursos de la CPU y que las cosas dejen de funcionar.

También es posible que las aplicaciones tomen más recursos de los que deberían. Esto podría deberse a que un equipo genere más réplicas de las necesarias para reducir artificialmente la latencia (¡es más fácil generar más copias que hacer que su código sea más eficiente!), A un cambio de configuración incorrecto que hace que un programa salga de controlar y usar el 100% de la CPU disponible. Independientemente de si el problema está causado por un mal desarrollador, código incorrecto o mala suerte, lo importante es que no se pierda el control. 



Solicitudes y límites


Las solicitudes y los límites son los mecanismos que Kubernetes usa para controlar recursos como la CPU y la memoria. Las solicitudes son lo que el contenedor está garantizado. Si un contenedor solicita un recurso, Kubernetes solo lo programará en un nodo que pueda darle ese recurso. Los límites, por otro lado, aseguran que un contenedor nunca supere un cierto valor. El contenedor solo puede subir hasta el límite y luego está restringido.

Es importante recordar que el límite nunca puede ser más bajo que la solicitud. Si prueba esto, Kubernetes arrojará un error y no le permitirá ejecutar el contenedor.

Las solicitudes y los límites son por contenedor. Los pod's pueden conetner un solo contenedor, pero es común pod's con varios contenedores. Cada contenedor del Pod's obtiene su propio límite y solicitud individual, pero como los Pods siempre se programan como un grupo, debe agregar los límites y las solicitudes de cada contenedor para obtener un valor agregado para el Pod. Para controlar qué solicitudes y límites puede tener un contenedor, puede establecer cuotas en el nivel del contenedor y en el namespaces.

Configuraciones del contenedor


Hay dos tipos de recursos: CPU y memoria. El planificador de Kubernetes los usa para averiguar dónde ejecutar tus pods. Los documentos para estos recursos. Si está ejecutando en Google Kubernetes Engine, el namespace "default"  ya tiene algunas solicitudes y límites configurados.

 

Estas configuraciones predeterminadas son correctas para "Hello World", pero es importante cambiarlas para adaptarlas a su aplicación. Una especificación típica de Pod para recursos podría verse más o menos así. Este pod tiene dos contenedores:

containers: 
- name: container1
  image: busybox
  resources:
    request:
      memory: "32Mi"
      cpu: "200m"
    limits:
      memory: "64Mi"
      cpu: "250m"
- name: container2
  image: busybox
  resources:
    request:
      memory: "96Mi"
      cpu: "300m"
    limits:
      memory: "192Mi"
      cpu: "750m"

Cada contenedor en el Pod puede establecer sus propias solicitudes y límites, y todos son aditivos. Por lo tanto, en el ejemplo anterior, el Pod tiene una solicitud total de 500 mCPU y 128 MiB de memoria, y un límite total de 1 CPU y 256Mib de memoria.

CPU


Los recursos de CPU  se definen en milicores. Si su contenedor necesita dos núcleos completos para ejecutar, pondría el valor "2000m". Si su contenedor solo necesita ¼ de un núcleo, pondría un valor de "250 m". Una cosa a tener en cuenta acerca de las solicitudes de CPU es que si ingresas un valor mayor que el recuento de núcleos de tu nodo más grande, tu pod nunca se programará.

Digamos que tiene un pod que necesita cuatro núcleos, pero su clúster de Kubernetes está compuesto por máquinas virtuales de doble núcleo: ¡su módulo nunca se programará! A menos que su aplicación esté diseñada específicamente para aprovechar múltiples núcleos, generalmente es una buena práctica mantener la solicitud de CPU en '1' o menos y ejecutar más réplicas para escalar.

Esto le da al sistema más flexibilidad y confiabilidad. Cuando se trata de límites de CPU, las cosas se ponen interesantes. La CPU se considera un recurso "comprimible". Si su aplicación comienza a alcanzar los límites de su CPU, Kubernetes comienza a estrangular su contenedor. Esto significa que la CPU será artificialmente restringida, ¡lo que le dará a tu aplicación un peor rendimiento! Sin embargo, no se dará por terminado ni se desalojará.

Puede usar un health check Liveness para asegurarse de que el rendimiento no se haya visto afectado. Los recursos de memoria de memoria se definen en bytes. Normalmente, le da un valor mebibyte a la memoria (esto es básicamente lo mismo que un megabyte), pero puede dar cualquier cosa, desde bytes hasta petabytes.

Memoria


Al igual que la CPU, si pones una solicitud de memoria que es más grande que la cantidad de memoria en tus nodos, el pod nunca se programará. A diferencia de los recursos de la CPU, la memoria no se puede comprimir. Debido a que no hay forma de reducir el uso de memoria, si un contenedor sobrepasa su límite de memoria, terminará. Si su pod está gestionado por un "Deployment", "StatefulSet", "DaemonSe"t u otro tipo de controlador, el controlador se ejecutara solo si es posible.

Nodos


Es importante recordar que no puede establecer solicitudes que son más grandes que los recursos proporcionados por sus nodos. Por ejemplo, si tiene un clúster de máquinas de doble núcleo, ¡nunca se programará un Pod con una solicitud de 2.5 núcleos! Aquí puede encontrar los recursos totales para las VM de Kubernetes Engine.

Configuraciones de Namespace


En un mundo ideal, la configuración de contenedores de Kubernetes sería lo suficientemente buena para encargarse de todo, pero el mundo es un lugar oscuro y terrible. Las personas pueden olvidarse fácilmente de establecer los recursos, o un equipo  puede establecer las solicitudes y límites muy altos y ocupar más de su parte en del clúster.

Para evitar estos escenarios, puede configurar ResourceQuotas y LimitRanges a nivel de Namespaces.

ResourceQuotas


Después de crear espacios de nombres, puede bloquearlos utilizando ResourceQuotas. Las ResourceQuotas son muy potentes, pero veamos cómo se pueden usar para restringir el uso de recursos de CPU y memoria. Una cuota para los recursos puede ser similar a esto:
 

apiVersion: v1 
Kind: ResourceQuota
metadata:
  name: demo
spec:
  hard:
    requests.cpu: "500m"
    requests.memory: "100MIB"
    limits.cpu: "700m"
    limits.memory: "500Mib"

Examinando  este ejemplo, puede ver que hay cuatro secciones. Configurar cada una de estas secciones que es opcional.

requests.cpu es el máximo de solicitudes combinadas de CPU en millicores para todos los contenedores en el Namespace. En el ejemplo anterior, puede tener 50 contenedores con solicitudes de 10m, cinco contenedores con solicitudes de 100m o incluso un contenedor con una solicitud de 500m. ¡Siempre y cuando la CPU total solicitada en Namespace sea inferior a 500m!

requests.memory es el máximo de solicitudes combinadas de memoria para todos los contenedores en el Namespace. En el ejemplo anterior, puede tener 50 contenedores con solicitudes de 2MiB, cinco contenedores con solicitudes de CPU de 20MiB o incluso un solo contenedor con una solicitud de 100Mib. ¡Siempre y cuando la Memoria total solicitada en Namespace sea inferior a 100MiB!

limits.cpu es el límite de CPU máximo combinado para todos los contenedores en Namespace. Es como requests.cpu pero para el límite.  

limits.memory es el límite máximo de Memoria combinada para todos los contenedores en Namespace. Es solo como request.memory pero para el límite. Si está utilizando un Namespaces de producción y desarrollo (a diferencia de un Namespace por equipo o servicio), un patrón común es no poner cuotas en el Namespace de producción y cuotas estrictas en Namespace de desarrollo. Esto permite que la producción tome todos los recursos que necesita en caso de un pico en el tráfico.

LimitRange


También puedes crear un LimitRange en el Namespace. A diferencia de una Cuota, que considera el Namespace como un todo, un LimitRange se aplica a un contenedor individual.

Esto puede ayudar a evitar que las personas creen contenedores súper pequeños o súper grandes dentro Namespace. Un LimitRange podría tener un aspecto similar al siguiente:

apiVersion: v1 
Kind: LimitRange
metadata:
 name: demo
spec:
 limits:
-default:
   cpu: 600m
   memory: 100Mib
 defaultRequest:
   cpu: 100m
   memory: 50Mib
 max:
   cpu: 1000m
   memory: 200Mib
 min:
   cpu: 10m
   memory: 10Mib
 type: Container

Examinando este ejemplo, puede ver que hay cuatro secciones. De nuevo, configurar cada una de estas secciones es opcional.

La sección default establece los límites predeterminados para un contenedor en un pod. Si establece estos valores en limitRange, a los contenedores que no los establezcan explícitamente se les asignarán los valores predeterminados.

La sección defaultRequest establece las solicitudes predeterminadas para un contenedor en un pod. Si establece estos valores en limitRange, a los contenedores que no los establezcan explícitamente se les asignarán los valores predeterminados.

La sección max establecerá los límites máximos que un contenedor en un Pod puede establecer. La sección predeterminada no puede ser más alta que este valor. Del mismo modo, los límites establecidos en un contenedor no pueden ser superiores a este valor. Es importante tener en cuenta que si se establece este valor y la sección predeterminada no, todos los contenedores que no establezcan explícitamente estos valores recibirán los valores máximos como límite.

La sección min establece las Solicitudes mínimas que un contenedor en un Pod puede establecer. La sección de solicitud predeterminada no puede ser menor que este valor. Del mismo modo, las solicitudes establecidas en un contenedor tampoco pueden ser inferiores a este valor. Es importante tener en cuenta que si se establece este valor y la sección defaultRequest no lo está, el valor mínimo también se convierte en el valor predeterminado de solicitud.

El lifecycle de un Pod en Kubernetes


Al final del día, el planificador Kubernetes utiliza estas solicitudes de recursos para ejecutar tus cargas de trabajo Es importante entender cómo funciona esto para que pueda ajustar sus contenedores correctamente.

Digamos que quieres ejecutar un Pod en tu Cluster. Asumiendo que las especificaciones del Pod son válidas, el planificador de Kubernetes usará el balanceo de carga round-robin para elegir un Nodo para ejecutar su carga de trabajo.

Nota: La excepción a esto es si usa un nodeSelector o mecanismo similar para forzar a Kubernetes a programar su Pod en un lugar específico. Las verificaciones de recursos aún ocurren cuando usa un nodeSelector, pero Kubernetes solo verificará los nodos que tengan la etiqueta requerida.

Kubernetes luego verifica si el Nodo tiene suficientes recursos para cumplir con las solicitudes de recursos en los contenedores del Pod. Si no lo hace, pasa al siguiente nodo. Si ninguno de los Nodos en el sistema tiene recursos para completar las solicitudes, los Pods entran en un estado "pendiente".

Al utilizar las características de Kubernetes Engine, como el Node Autoscaler, Kubernetes Engine puede detectar automáticamente este estado y crear más Nodos automáticamente. Si hay un exceso de capacidad, el escalador automático también puede escalar y eliminar nodos para ahorrar dinero.

Pero, ¿qué pasa con los límites? Como sabe, los límites pueden ser más altos que las solicitudes. ¿Qué sucede si tiene un Nodo donde la suma de todos los Límites del contenedor es en realidad más alta que los recursos disponibles en la máquina? En este punto, Kubernetes entra en algo llamado "estado comprometido".

Aquí es donde las cosas se ponen interesantes. Debido a que la CPU se puede comprimir, Kubernetes se asegurará de que sus contenedores obtengan la CPU que solicitaron y estrangulará el resto. La memoria no se puede comprimir, por lo que Kubernetes necesita comenzar a tomar decisiones sobre qué contenedores terminar si el Nodo se queda sin memoria.

Imaginemos un escenario en el que tenemos una máquina que se está quedando sin memoria. ¿Qué hará Kubernetes?

Nota: Lo siguiente es cierto para Kubernetes 1.9 y superior. En versiones anteriores, utiliza un proceso ligeramente diferente. Vea este documento para una explicación detallada.

Kubernetes busca Pods que estén usando más recursos de los que solicitaron. Si los contenedores de su Pod no tienen solicitudes, entonces, de forma predeterminada, están utilizando más de lo solicitado, por lo que estos son los principales candidatos para la terminación.

Otros candidatos principales son los contenedores que han revisado su solicitud, pero aún están por debajo de su límite. Si Kubernetes encuentra varios pods que han revisado sus solicitudes, los clasificará según la prioridad del Pod y terminará primero con los pods de prioridad más baja.

Si todos los Pods tienen la misma prioridad, Kubernetes termina el Pod que es más sobre su solicitud.

En escenarios muy raros, Kubernetes podría verse obligado a terminar Pods que todavía están dentro de sus solicitudes. Esto puede suceder cuando los componentes críticos del sistema, como el kubelet o el docker, comienzan a tomar más recursos de los que estaban reservados para ellos.

Conclusión


Si bien su clúster de Kubernetes podría funcionar bien sin establecer límites y solicitudes de recursos, comenzará a tener problemas de estabilidad a medida que crezcan sus equipos y proyectos. ¡Agregar solicitudes y límites a sus Pods y Namespaces solo requiere un pequeño esfuerzo adicional, y puede evitar que sufra muchos dolores de cabeza más adelante!