19 de marzo de 2024
StatefulSets, gestión optimizada de aplicaciones con estado
¿Qué es un StatefulSet?
Podemos definirlo como el objeto API de carga de trabajo y administración de aplicaciones con estado o también conocidas como “stateful applications”. Este gestiona la implementación y el escalado de un conjunto de pods. Ofreciendo así garantías sobre el orden y singularidad delos pods que gestiona.
A semejanza con los deployments, un StatefulSet administra pods basados en especificaciones de contenedor idénticas entre sí, pero a diferencia de los deployments un StatefulSet mantiene una identidad fija para cada uno de sus pods. Es decir, estos pods aunque se hayan creado a partir de las mismas especificaciones no son intercambiables entre ellos, poseen un identificador persistente que se mantiene ante cualquier reprogramación ((re)scheduling).
Si deseamos utilizar volúmenes de almacenamiento para lograr persistencia en nuestra carga de trabajo, los StatefulSets pueden formar parte de una buena solución. Aunque estos pods que pertenecen a un Statefulset son susceptibles a fallos, sus dentificadores persistentes nos facilitarán la unión de los volúmenes ya existentes con los nuevos pods que sustituirán a los pods fallidos.
¿Cuándo utilizar un StatefulSet?
Los StatefulSets son especialmente útiles en los siguientes casos o requerimientos.
- Identificadores de red únicos y estables.
- Almacenamiento estable y persistente.
- Despliegues y escalados ordenados y sin afectación.
- Actualizaciones automáticas, continuas y ordenadas.
Cuando nos referimos a aplicaciones que no requieren de un identificador persistente, implementación ordenada, eliminaciones o escalados.
Debemos, en ese caso, implementar nuestra aplicación mediante objetos de carga de trabajo que proporcione un conjunto de réplicas sin estado. Por ejemplo, un ReplicaSet o Deployment que sería más eficaz dado tal caso.
Limitaciones a tener en cuenta
- El almacenamiento para un pod determinado debe ser aprovisionado mediante un “PersistentVolume” o volumen persistente en función de la clase de almacenamiento solicitada, o bien, previamente de mano del administrador.
- Eliminar y/o reducir un StatefulSet no eliminará los volúmenes asociados a este. Debido a que siempre tratamos de garantizar la seguridad y persistencia de los datos, normalmente más valiosos que una eliminación automática de todos los recursos asociados al StatefulSet.
- Actualmente, los StatefulSets necesitan un servicio “Headless” o sin cabeza que debe ser el responsable de la identidad de red de los pods desplegados. En este caso debemos crear este servicio por nuestro lado.
- Los StatefulSets no ofrecen ninguna garantía en términos de finalización de los pods a causa de la eliminación del StatefulSet. Para poder garantizar una finalización ordenada y sin afectación de los pods debemos utilizar la posibilidad de escalar el StatefulSet a 0 antes de eliminarlo.
- Cuando trabajamos con actualizaciones continuas (rolling updates) sobre las políticas de administración de pods predeterminadas (OrderedReady), es posible recibir un estado de interrupción o rotura que requiera intervención manual para su reparación.
Componentes de un Kubernetes StatefulSet
Analicemos el siguiente ejemplo de un StatefulSet.
El siguiente ejemplo ha sido extraído de documentación oficial de Kubernetes y por lo tanto no revela ningún tipo de configuración o dato sensible.
Observaciones:
- Línea 1-13: El servicio “headless“ o sin cabeza, llamado “nginx” y utilizado para controlar el dominio de la red. Podemos abrir un puerto específico y asignarle un alias.
- Línea 15-49: El StatefulSet llamado “web” tiene una “spec“ (especificación) en la línea 24 que nos indica el número de réplicas del contenedor de “nginx” que serán lanzadas en pods únicos.
- Línea 41-49: El “volumeClaimTemplate” nos facilitará el almacenamiento estable(persistente) utilizando un “PersistentVolume” aprovisionado por el “PersistentVolume Provisioner”.
El nombre del objeto “StatefulSet” debe ser un nombre válido para el sub-dominio especificado en el DNS.
- Línea 20-22: Debemos especificar una etiqueta, pero teniendo en cuenta que deberá ser la misma que la etiqueta especificada en el campo “app” en la línea 29. No etiquetar correctamente el campo del “Pod selector” resultará en un error de validación durante el proceso de creación del StatefulSet.
- Línea 25: Podemos especificar “minReadySeconds“ dentro de “specs” (especificaciones) el cual es un campo opcional y determina los segundos que deben pasar desde la creación de un nuevo pod corriendo y en estado listo o “ready” sin que ninguno de sus contenedores se rompan para ser considerado en estado disponible o “available”. Esta opción es especialmente útil cuando aplicamos actualizaciones continuas (rolling updates) para comprobar el progreso de dicha actualización.
Identidad del pod
Los pods que pertenecen a aun StatefulSet poseen un identificador único compuesto por un ordinal (número ordinal), una identidad de red persistente y almacenamiento persistente también. La identidad o identificador se asocia al pod sin importar en qué nodo se programe((re)schedule).
Índice ordinal
Para un StatefulSet con n replicas, a cada pod le será asignado un número entero ordinal desde 0 hasta n-1, único en su conjunto.
Identificador Persistente de Red
Cada pod de un StatefulSet deriva su nombre del nombre del StatefulSet y del ordinal del pod. El patrón para el nombre del host(pod) construido es $(statefulset name)-$(ordinal).
El StatefulSet puede utilizar un servicio "headless" o sin cabeza para controlar el dominio de sus pods. Dicho dominio tiene la siguiente nomenclatura $(nombre del servicio).$(namespace).svc.cluster.local, donde “cluster.local” es el dominio del cluster.
A medida que se crea cada pod, obtenemos un sub-dominio DNS coincidente, con la siguiente forma; $(podname).$(dominio del servicio), donde el dominio del servicio se define mediante el campo “serviceName” en el StatefulSet.
Dependiendo de la configuración del DNS en su cluster, es posible que no pueda buscar el nombre de DNS para un pod recién ejecutado. Este comportamiento ocurre cuando otros clientes en el cluster ya enviaron consultas a ese nombre de host del pod antes de que se creara. El almacenamiento de caché en negativo significa que los resultados de búsquedas fallidas anteriores se recuerdan y se reutilizan, incluso después de haber ejecutado el pod.
Para que este problema no ocurra y no tener unos segundos de caída o de errores después de crear un pod podemos utilizar alguna de las soluciones descritas a continuación.
- Lanzar las consultas directamente a través de la API de Kubernetes (por ejemplo, utilizando un reloj) en lugar de confiar en las búsquedas de DNS.
- Reducir el tiempo de almacenamiento en caché en su proveedor de DNS en Kubernetes (normalmente configurar el “configmap” para CoreDNS, normalmente con valor en 30s).
"Recordar que como se ha mencionado en el apartado de limitaciones, nosotros mismos somos responsables de crear el servicio headless(sin cabeza) responsable de la identidad de red de los pods."
A continuación, algunos ejemplos de cómo afecta el Dominio del cluster, el nombre del servicio y el nombre del StatefulSet al DNS de los pods.
Almacenamiento persistente
Para cada entrada “VolumeClaimTemplate” definida en un StatefulSet, cada pod recibe un “PersistentVolumeClaim”. En el ejemplo de cada “nginx” planteado anteriormente, cada pod recibe un solo PersistentVolume con StorageClass “my-storage-class“ y un 1Gib de almacenamiento aprovisionado.
Cuando un pod se reprograma ((re)schedule) en un nodo, sus “VolumeMounts” montan los “PersistentVolumes” asociados con sus respectivos “PersistentVolumesClaims”.
Pod Name label (Etiqueta de nombre del pod)
Cuando el “Controller” del StatefulSet crea un pod, añade una etiqueta, “statefulset.kubernetes.io/pod-name”, esta se establece en el nombre del pod. Esta misma etiqueta nos permite asociar un servicio a un pod especifico en el StatefulSet.
Garantías de Despliegue y Escalado
Si tenemos un StatefulSet con n réplicas, cuando los pods están siendo desplegados, lo hacen de forma secuencial en el siguiente orden {0..N-1}.
Cuando los pods están siendo eliminados, lo hacen de forma reversa es decir del {N-1..0}.
Antes de que una operación de escalado se aplique a un pod, todos los predecesores de este deben estar en estado “Running” and “Ready”.
Antes de que un pod sea terminado todos sus sucesores deben cerrarse por completo.
Nunca debemos especificar en “pod.Spec.TerminationGracePeriodSeconds” un valor de 0. Esta practica es insegura y nunca recomendada.
Estrategias de Actualización
El campo .spec.updateStrategy nos permite configurar o desactivar actualizaciones continuas (rolling updates) automáticas para los contenedores, etiquetas, recursos requests y limits y también annotations para los pods en un StatefulSet. Tenemos dos posibles valores.
OnDelete: Cuando establecemos el tipo de “updateStrategy” a “OnDelete”, el controller del StatefulSet automáticamente no actualizará los pods que pertenezcan a los pods. Los usuarios deberemos eliminar manualmente los pods para forzar al controller a crear nuevos pods que reflejen las modificaciones realizadas al template del StatefulSet.
RollingUpdate: Este valor implementa las actualizaciones automáticas, por defecto este es el valor establecido para la estrategia de actualización.
Rolling Updates
Cuando la estrategia de actualizaciones (spec.updateStrategy.type) de un StatefulSet se establece en RollingUpdate, el controlador (controller) de StatefulSet eliminará y volverá a crear cada pod en el StatefulSet. Se procede en el mismo orden que cuando terminamos pods (de ordinal más grande al más pequeño). Actualizando cada pod de uno en uno.
El controlplane de kubernetes espera hasta que un pod actualizado esté en ejecución y listo antes de actualizar a su predecesor. El controlplane respeta el valor que le hayamos asignado a “spec.minReadySeconds” des de que el pod cambia su estado a “listo” antes de continuar actualizando el resto de pods.
Particiones
La estrategia de actualización “rolling update” nos permite hacer la actualización de forma parcial, si especificamos una partición (spec.updaterStrategy.rollingUpdate.partition) todos los pods que tengan un ordinal mayor al valor de la partición sera actualizado cuando el template (spec.template) del StatefulSet se modifique.
Los pods que se posean un ordinal menor al valor de la partición no serán actualizados y aunque se eliminen y se vuelvan a desplegar lo harán con la versión anterior del template del StatefulSet. Si el valor de la partición es mayor al número de replicas (spec.replicas) ningún pod será actualizado.
Retroceso forzado
Cuando utilizamos la estrategia de actualización por defecto “OrderedReady”, es posible entrar en un estado de fallo en que se necesite intervención manual para repararlo. Si actualizamos el template de un pod y nuestra configuración nunca convierte su estado en “running” o “ready” el StatefulSet dejará de actualizarse y quedará a la espera.
Este estado no es suficiente para poder revertir el pod template a una buena configuración. Debido a un problema conocido, el StatefulSet continuará esperando al pod roto que su estado cambie a “ready“, cosa que nunca pasará, antes de intentar revertir hacia la configuración funcional.
Una vez revertido el template, debemos eliminar los pods del StatefulSet que se hayan intentado desplegar con la configuración errónea. Una vez hecho esto el mismo StatefulSet empezará a recrear los pods utilizando la template tirada hacia atrás hacia la configuración correcta.
Replicas
El campo opcional “spec.replicas” especifica el número deseado de pods, el valor por defecto es 1.
Si hemos de escalar automáticamente un despliegue podemos utilizar directamente el comando kubectl o bien modificar el fichero “.yml” y volver a desplegar el StatefulSet. Con el comando kubectl:
"kubectl scale statefulset statefulset –replicas=X"
Si tenemos un “HorizontalPodAtuoscaler” o una api similar para escalar automáticamente no debemos especificar este campo ya que el control plane de kubernetes actuará por nosotros.
Share