Administrar los recursos para tus contenedores en Kubernetes
Cuando empiezas a trabajar en serio con Kubernetes, necesitas poner orden a cuánto y cómo se aprovecha tu clúster. Sobre todo si el mismo se comparte entre diferentes aplicaciones y/o servicios, donde tienes que evitar que “unos se coman a otros”, en cuanto a recursos se refiere 🙂 En este artículo quiero hablarte de cómo Kubernetes permite establecer cuántos recursos necesita un pod, límites, valores por defectos y cuotas a través de diferentes objetos.
Requests
Cuando creas un pod es posible, y más que recomendable, establecer la cantidad de recursos mínimos que necesita para funcionar. La sección requests, o peticiones, dentro del apartado resources define por cada contenedor dentro de un pod cuánta memoria y cuánta CPU va a necesitar como mínimo para poder ejecutarse:

En este caso se han establecido de CPU 500m y de memoria 10Mi. Quizás la unidad en la que se define la CPU a utilizar te parezca extraña porque se mide en unidades de CPU, esto es: una CPU en Kubernetes equivale a un core virtual. Las peticiones pueden ser fraccionadas, es decir que puedes especificar menos de una CPU, como es el caso. En el ejemplo se establece que necesitas 0.5, o 500m, lo cual se lee como “quinietas milicpu”, o lo que es lo mismo: la mitad de una CPU.
Por otro lado, para la memoria la unidad de medida son los bytes, por lo que ya es más familiar. Sin embargo, en el ejemplo he utilizado la cantidad 10Mi, la cual no es lo mismo que 10MB, y creo que también es importante que lo tengas en cuenta. 10Mi se lee como “diez Mebibytes” y son 10.48576MB. Más información aquí.
Cómo saber cuántos recursos tienen tus nodos
Si estás trabajando con un Kubernetes gestionado en la nube puedes saber qué características has elegido para tus nodos a través del portal del proveedor. Sin embargo, la forma de saber la capacidad exacta de los mismos es a través de describe:

Entre toda la información que devuelve, verás que existen dos secciones llamadas Capacity y Allocatable:

En el caso de mi nodo puedes ver que tiene 2 CPUs y 7113828Ki (7284.55MB ó 7.2GB) de memoria en el apartado Capacity. Esta sección representa la cantidad total de recursos que tiene un nodo, lo cual no significa que todo esté disponible. Estos es así porque, evidentemente, el nodo tiene otros recursos del sistema que también consumen recursos.
Es por ello que existe otra sección llamada Allocatable donde puedes ver el espacio real que queda disponible para tus pods. El Scheduler, que es la pieza encargada dentro de Kubernetes en decidir dónde irán los recursos que queremos desplegar, utiliza este apartado, entre otras cosas, como parte de su decisión para valorar si nuestro nuevo recurso tiene cabida en un nodo u otro. Para saber cuántos pods caben dentro de un nodo, el Scheduler sumará todas las requests que se han definido y comprobará si no sobrepasan los valores de esta sección. De hecho, puede ocurrir que queramos desplegar un pod como el anterior, solicitando 1 CPU, y que no tengamos nodos con espacio suficiente y se quede en estado Pending. Si esto ocurre, podrás ver a través del comando describe, sobre el pod, que el problema viene de que el Scheduler no ha encontrado un nodo donde colocarlo:

Debemos evitar esta situación en la medida de lo posible.
También puede ser útil saber cómo están tus nodos gastando sus recursos. Con el mismo comando describe, puedes ver también una sección llamada Non-terminated Pods donde se ve el número total de pods en dicho nodo, así como la cantidad de recursos que tienen solicitados cada uno:

Limits
Especificar los requests en un pod te asegura de que cada contenedor tiene el mínimo que necesita para funcionar. Sin embargo, también es posible establecer el máximo a través de la sección limits:

Estos límites se configuran en el mismo sitio que los requests y tienen el mismo formato. Es importante limitar sobre todo la memoria que un pod puede llegar a utilizar ya que, a diferencia de la CPU, esta es más difícil de liberar. Si no limitamos la memoria, un pod podría adueñarse de toda la que está disponible y afectar así a otros pods en el nodo, e incluso a los nuevos que se instalen, ya que el Scheduler asigna pods en base al espacio disponible teniendo en cuenta la sección Allocatable del nodo, pero no en base a la memoria que se está usando.
Nota: Si solo se especifican los límites, Kubernetes tomará estos también como valor para los requests.
Cuando un proceso dentro de un contenedor intenta usar una cantidad mayor de la memoria que tiene permitida se mata al proceso. De hecho, pasa al estado Terminated y la razón es OOMKilled (OOM proviene de Out Of Memory). Si el pod tiene establecida la política de reinicio en Always o OnFailure el proceso se reiniciará de manera inmediata y es posible que inicialmente no te des cuenta de que esto ha ocurrido. Sin embargo, si sigue necesitando más memoria de la que tiene permitida se reiniciará una y otra vez y el proceso de reinicio será cada vez más lento, ya que Kubernetes establece unos retrasos cuando los reinicios son muy seguidos, hasta llegar a 5 minutos, o lo que es lo mismo: tu contenedor estará 5 minutos sin dar servicio, intentará funcionar de nuevo y, si vuelve a caer, otros 5 minutos más esperando a ser reiniciado y así indefinidamente. Por ello también es importante que los límites no sean demasiado bajos para evitar esta situación.
Por último, es importante tener en cuenta que los recursos que tiene un nodo para alojar pods no son una restricción para establecer los límites. Es decir que la suma de todos los límites establecidos podrían llegar a exceder el 100% de la capacidad del nodo. Esto tiene una consecuencia importante, ya que si el nodo llega a estar al 100% de uso en algún momento ciertos contenedores podrían ser matados/reiniciados para liberar recursos.
Kubernetes mata tus pods cuando se queda sin recursos
Como un nodo puede tener pods que suman más del 100% de su capacidad, en cuanto a límites se refiere, esto significa que en algún momento podría no tener recursos disponibles para todos los pods a la vez. Imagina que tienes dos pods, donde uno de ellos está utilizando más memoria de la que debe y de repente un segundo pod necesita utilizar dicha memoria, pero el nodo no puede proporcionársela. En este caso uno de los dos debe morir ¿Pero cuál? Llegado el caso Kubernetes necesita conocer sus prioridades 🙂 Para ello, categoriza a los pods en tres clases llamadas Quality of Service (QoS):
- BestEffort: Son aquellos a los cuales no les hemos asignado ni requests ni limits, por los que serán los primeros en morir si fuera necesario.
- Guaranteed: Son aquellos pods que sus requests son iguales a sus límites. Para estos se garantiza la supervivencia.
- Burstable: Aquellos entre medias del BestEffort y Guaranteed. Los límites del contenedor no se corresponden con los requests y/o no todos los contenedores dentro del pod tienen establecidos estos valores.
Para verlo con un ejemplo, voy a utilizar esta definición de pod:

En este caso el resultado será BestEffort porque no tiene especificado ni los requests ni los limits. Puedes verlo haciendo un describe del pod, en el campo QoS Class:

Si “queremos” que el pod entre en la categoría Burstable lo conseguiríamos poniendo solo los requests, pero no los limits, o bien poniendo unos límites que no coincidan con los requests.

Por último, para que un pod sea bueno bueno, y garantizarnos que no estará entre las prioridades de Kubernetes a la hora de liberar espacio, lo mejor es que establezcamos los límites. Como te decía antes, si solamente añadimos estos, igualará los requests a los mismos y tendremos nuestro pod categorizado como Guaranteed.

Como te puedes imaginar, es muy importante definir estos valores ya que de ellos dependen la vida de tus pods en un momento crítico, si el sistema se ve comprometido.
Configurar requests y limits por defecto en un namespace
Para obligar a que tu clúster siga esta buena práctica, es posible establecer a nivel de namespace los limits y requests por defecto a través de un objeto llamado LimitRange, así como los mínimos y máximos permitidos. De esta manera evitamos que los usuarios del clúster den de alta recursos sin estos valores:

De hecho, puedes ver que es posible establecer límites a nivel de pod, de contenedor e incluso otros tipos, como el tamaño del almacenamiento, entre otros. Puedes consultar los límites para un namespace a través del siguiente comando:

Si creas un pod sin estos valores se asignarán de manera automática los elegidos por defecto. Además, si intentas crear un pod que no encaja con los mínimos y máximos elegidos Kubernetes no lo creará y lanzará un error:

Esto es genial porque es una de las medidas para evitar que un pod no se quede en estado Pending por no disponer de recursos.
Establecer cuotas
Cuando varios usuarios o equipos comparten un clúster con un número fijo de nodos es probable que quieras controlar que nadie se pase consumiendo recursos, perjudicando de este modo al resto. Con LimitRange solamente indicamos el valor por defecto y el máximo y mínimo que se puede establecer a un pod/contenedor, pero siempre que se cumplan dichos valores dejaría crearlo, incluso si nos estamos aprovechando de recursos que son compartidos con otros equipos/aplicaciones. Para controlar esto tenemos otro objeto llamado ResourceQuota.

Gracias a él restringimos cuántos recursos puede usar el equipo o aplicación asignado a un namespace:

Si te fijas en la salida anterior, si ResourceQuota se aplica a posteriori en un namespace con unos valores por debajo de lo ya consumido este no corregirá este desfase de ningún modo. Sin embargo, si intentas crear un nuevo pod y este no tiene espacio dentro del namespace, en lugar de desplegarse y quedarse en estado Pending, o adueñarse de recursos de otra aplicación, se denegará directamente su creación:

Con todas estas herramientas deberías de tener tus recursos bajo control 🙂
¡Saludos!