Notas de estudio de Kubernetes: mejores prácticas para desarrollar aplicaciones (1) 20230311

1. Todos los recursos de kubernetes

Los siguientes son los diversos componentes de kubernetes utilizados en una aplicación típica

Un manifiesto de aplicación típico contiene uno o más objetos Deployment y StatefulSet . Estos objetos contienen plantillas de pod para uno o más contenedores, cada contenedor tiene una sonda de actividad y sondas de preparación para los servicios proporcionados por el contenedor (si corresponde). Un pod que proporciona un servicio se expone a sí mismo a través de uno o más servicios. Cuando sea necesario acceder a estos servicios desde el clúster, configure estos servicios como servicios de tipo LoadBalancer o NodePort, o abra los servicios a través de recursos de Ingress

Las plantillas de pod (archivos de configuración a partir de los cuales se crean los pods) suelen hacer referencia a dos tipos de secretos . Uno se usa cuando se extraen imágenes de un repositorio espejo privado; el otro lo usan directamente los procesos que se ejecutan en pods. Las credenciales de privacidad en sí no suelen formar parte del manifiesto de la aplicación, ya que no las configura el desarrollador de la aplicación, sino el equipo de operaciones. Las credenciales secretas generalmente se asignan a una cuenta de servicio, que luego se asigna a cada pod individual.

Una aplicación también contiene uno o más objetos ConfigMap , que pueden usarse para inicializar variables de entorno o montarse como volúmenes configMap en pods. Algunos pods usan volúmenes adicionales, como los volúmenes emptyDir o gitRepo. Un pod que requiere almacenamiento persistente requiere un volumen persistenteVolumeClaim . PersistentVolumeClaim también forma parte de un manifiesto de aplicación, y el administrador del sistema crea por adelantado la StorageClass a la que hace referencia PersistentVolumeClaim.

En algunos casos, una aplicación también necesita usar tareas (jobs) o cronjobs (cronJobs). Los DaemonSets no suelen formar parte de la implementación de aplicaciones, pero los administradores del sistema suelen crearlos para ejecutar los servicios del sistema en todos o en algunos nodos. El desarrollador puede incluir el escalador automático de módulos horizontales en el manifiesto de la aplicación o el equipo de operación y mantenimiento puede agregarlo al sistema más adelante. El administrador del clúster también crea objetos LimitRange y ResourceQuota para controlar el uso de recursos informáticos de cada pod y de todos los pods (como un todo).

Una vez implementada la aplicación, varios controladores de kubernetes crearán automáticamente otros objetos. Estos incluyen el objeto de punto final de servicio (punto final) creado por el controlador de punto final (controlador de punto final), el objeto ReplicaSet creado por el controlador de implementación (controlador de implementación) y el objeto de pod real creado por ReplicaSet (o trabajo, CronJob, StatefulSet, DaemonSet ).

Los recursos suelen estar organizados por una o más etiquetas. Esto se aplica no solo a los pods, sino también a otros recursos. Además de las etiquetas, la mayoría de los recursos también contienen una anotación que describe el recurso, enumera la información de contacto de la persona o el equipo responsable de modificar el recurso o proporciona metadatos adicionales para otras herramientas utilizadas por los administradores.

Pod es el centro de todos los recursos y sin duda es el recurso más importante de kubernetes, después de todo, cada una de tus aplicaciones se ejecuta en pods.

2. Comprender el ciclo de vida de la vaina

Un pod se puede comparar con una máquina virtual que ejecuta una sola aplicación, pero aún existen diferencias significativas. Un ejemplo es que la aplicación que se ejecuta en el pod puede cerrarse en cualquier momento, porque Kubernetes necesita programar el pod en otro nodo, o solicitar Retracción.

  1. La aplicación en el pod puede eliminarse o reprogramarse

Las aplicaciones que se ejecutan en máquinas virtuales rara vez se migran de una máquina a otra. Cuando un operador de migración migra una aplicación, puede reconfigurar la aplicación y verificar manualmente que la aplicación funcione correctamente en la nueva ubicación.

Con kubernetes, las aplicaciones se pueden migrar automáticamente con mayor frecuencia sin intervención manual, lo que significa que nadie configurará las aplicaciones para garantizar que se ejecuten con normalidad después de la migración.

Se esperan cambios en la ip local y el nombre de host

Cuando un pod se elimina y se ejecuta en otro lugar (un pod nuevo reemplaza al pod anterior, el pod anterior no se migró), no solo tiene una nueva dirección IP sino también un nuevo nombre y nombre de host. La mayoría de las aplicaciones sin estado pueden manejar este escenario sin efectos adversos. Pero los servicios con estado generalmente no funcionan. Una aplicación con estado puede ejecutarse a través de un StatefulSet, y StatefulSet garantizará que la aplicación debe poder hacer frente a este cambio después de que se programe en un nuevo nodo y se inicie. Por lo tanto, los desarrolladores de aplicaciones no deben confiar en las direcciones IP de los miembros para establecer relaciones entre sí en un clúster. Además, si usa nombres de host para construir relaciones, debe usar StatefulSet.

Se espera que los datos escritos en el disco desaparezcan

En el caso de una aplicación que escribe datos en el disco, esos datos pueden perderse cuando la aplicación se inicia en un nuevo módulo, a menos que adjunte almacenamiento persistente a la ruta de escritura de la aplicación. La pérdida de datos está garantizada cuando se reprograman los pods. Pero incluso en ausencia de programación, los archivos escritos en el disco se perderán. Incluso durante la vida útil de un solo pod, los archivos escritos en el disco por las aplicaciones del pod se pierden.

Supongamos que hay una aplicación cuyo proceso de inicio es relativamente fácil de usar y requiere muchas operaciones informáticas. Para que la aplicación sea más rápida en los inicios posteriores, los desarrolladores generalmente almacenan en caché algunos resultados de los cálculos durante el proceso de inicio en el disco. . Dado que las aplicaciones en kubernetes se ejecutan en contenedores de forma predeterminada, estos archivos se escribirán en el sistema de archivos del contenedor. Si el contenedor se reinicia en este momento, estos archivos se perderán. Porque se utilizará una nueva capa de escritura cuando se inicie un nuevo contenedor.

Un solo contenedor puede reiniciarse por varios motivos, como un bloqueo del proceso, una sonda de inventario que devuelve un error o el nodo se está quedando sin memoria y OOMKiller elimina el proceso. Cuando sucede lo anterior, las cápsulas siguen siendo las mismas, pero los contenedores son completamente nuevos. Kubelet no ejecutará un contenedor varias veces, pero volverá a crear un contenedor.

Utilice volúmenes de almacenamiento para persistir datos entre contenedores

Cuando se reinicia el contenedor del pod, para no perder datos, se requiere un volumen a nivel de pod. Debido a que la existencia y destrucción de volúmenes es coherente con el ciclo de vida del módulo, los nuevos contenedores podrán reutilizar los datos escritos en volúmenes por contenedores anteriores.

A veces es una buena idea usar volúmenes de almacenamiento para almacenar en contenedores, pero no siempre, ¿qué sucede si el proceso recién creado falla nuevamente debido a la corrupción de datos? Esto provocará un bloqueo continuo del bucle (el pod indicará el estado CrashLoopBackOff). Si no se utilizan volúmenes de almacenamiento, los nuevos contenedores se iniciarán desde cero y lo más probable es que no se bloqueen. El uso de volúmenes de almacenamiento para almacenar datos en contenedores es un arma de doble filo.

2. Reprogramar pods muertos o parcialmente muertos

Si los contenedores de un pod siguen fallando, el kubelet los seguirá reiniciando. El intervalo entre cada reinicio aumenta exponencialmente hasta llegar a los 5 minutos. Durante este intervalo de 5 minutos, los pods esencialmente murieron porque sus procesos de contenedores no se estaban ejecutando.

Para ser justos, si se trata de un pod con varios contenedores, algunos de ellos pueden estar funcionando normalmente, por lo que el pod solo está parcialmente muerto, pero si el pod contiene solo un contenedor, entonces el pod está completamente muerto e inútil, porque hay no hay procesos ejecutándose en él.

Uno puede preguntarse por qué estos pods no se eliminan o reprograman automáticamente a pesar de que son parte de un ReplicaSet o un controlador similar. Es posible que espere poder eliminar el pod y reiniciar un pod que se ejecuta correctamente en otros nodos, después de que el contenedor se haya bloqueado debido a un problema relacionado con el nodo que no ocurre en otros nodos. Desafortunadamente, este no es el caso, al ReplicaSet en sí mismo no le importa si el pod está en un estado inactivo, solo le importa si la cantidad de pods coincide con la cantidad esperada de réplicas.

3. Inicie los pods en un orden fijo

Otra diferencia entre las aplicaciones que se ejecutan en pods y las aplicaciones que se ejecutan manualmente es que los operadores conocen las dependencias entre las aplicaciones cuando las implementan manualmente, de modo que pueden iniciar las aplicaciones en orden.

Cómo se inician los pods

Al usar kubernetes para ejecutar múltiples aplicaciones de pod, kubernetes no tiene un método integrado para ejecutar primero algunos pods y luego esperar a que estos pods se ejecuten correctamente antes de ejecutar otros pods. Por supuesto, puede publicar primero la configuración de una aplicación y luego publicar la configuración de la segunda aplicación después de iniciar el pod. Pero todo el sistema generalmente se define en un solo archivo yaml o json, que contiene definiciones de múltiples pods, servicios u otros objetos.

El servidor api de kubernetes procesa los objetos en el orden definido por los archivos yaml y json. Pero eso solo significa que están en orden cuando se escriben en etcd. No hay garantía de que los pods comiencen en este orden.

Pero puede evitar que un contenedor principal se inicie hasta que se cumplan sus condiciones previas. Esto se logra a través de un contenedor llamado init contenido en el pod.

Introducción al contenedor init

El contenedor init se puede usar para inicializar pods

Un pod puede tener cualquier cantidad de contenedores de inicio. Los contenedores init se ejecutan secuencialmente. Y solo cuando se ejecuta el último contenedor de inicio, se iniciará el contenedor principal. Dicho esto, el contenedor init también se puede usar para retrasar el inicio del contenedor principal del pod. Por ejemplo, hasta que se cumpla una determinada condición, el contenedor init puede esperar hasta que se inicie el servicio del que depende el contenedor principal y pueda proporcionar servicios. Cuando el servicio se inicia y puede proporcionar servicios, finaliza la ejecución del contenedor init. Entonces se puede iniciar el contenedor principal. De esta manera, el contenedor principal no lo usa antes de que el servicio del que depende esté listo.

Agregue el contenedor init al pod

El contenedor de inicio se puede definir en el archivo de especificaciones del pod como el contenedor principal, pero a través del campo spec.initContainers.

Fortun-cliente.yaml

especificación:
initContainers:
-name:init
image:busybox
command:
-sh
-c
-'while true;do echo "esperando que aparezca el servicio de fortuna...";
wget http://fortune -q -T 1 -o /dev/null > /dev/null 2>/dev/null &&break;sleep 1;hecho; echo "¡El servicio está activo! Iniciando contenedor principal".'

Al implementar este pod, solo se iniciará el contenedor de inicio del pod, que se puede mostrar con el comando kubectl get po para ver el estado del pod.

$ kubectl obtener po

Puede ver los registros del contenedor init a través de los registros de kubectl

$kubectl registra el cliente de la fortuna -c init

Mejores prácticas para manejar múltiples dependencias dentro de un pod

Considere usar una sonda de preparación. Si una aplicación no funciona en ausencia de una de sus dependencias, debe ser notificado a través de su sonda de preparación, para que kubernetes también sepa que la aplicación no está lista. El motivo de esto no es solo porque la sonda de preparación recibe la señal de que la aplicación de la ciudad de Huizu se convierte en un punto final de servicio, sino también porque el controlador de implementación usará la sonda de preparación de la aplicación cuando realice actualizaciones, de modo que se pueda evitar la versión incorrecta.

4. Agregue ganchos de ciclo de vida

Los pods también permiten definir dos tipos de enlaces de ciclo de vida

  • Gancho posterior al inicio

  • Gancho de parada previa

Estos enlaces de ciclo de vida se especifican por contenedor, a diferencia de los contenedores init, que se aplican a todo el pod. Estos ganchos, como sugiere su nombre, se ejecutan después de que se inicie el contenedor y antes de que se detenga.

Los ganchos de ciclo de vida son similares a las sondas de inventario y las sondas de preparación, ambas pueden

  • Ejecutar un comando dentro del contenedor.

  • Enviar una solicitud HTTP GET a una URL

Use ganchos de ciclo de vida posteriores al inicio

Los ganchos posteriores al inicio se ejecutan inmediatamente después de que se inicia el proceso principal del contenedor. Se puede usar para hacer un trabajo adicional cuando se inicia la aplicación. Development y puede agregar estas operaciones al código de la aplicación, pero si ejecuta una aplicación desarrollada por otros, la mayoría de ellos no desea modificar su código fuente. El enlace posterior al inicio le permite ejecutar algunas funciones adicionales sin cambiar el Solicitud Orden. Estos comandos pueden incluir la señalización a oyentes externos de que la aplicación se ha detenido o la inicialización de la aplicación para que pueda ejecutarse sin problemas.

Este gancho se ejecuta en paralelo con el proceso principal. El nombre del enlace puede ser engañoso, porque no espera hasta que el proceso principal se haya iniciado por completo antes de ejecutarse.

Aunque el enlace se ejecuta de forma asíncrona, afecta al contenedor de dos formas. Antes de que se ejecute el enlace, el contenedor permanecerá en estado de espera debido a ContainerCreating, por lo que el estado del pod será Pendiente en lugar de En ejecución. Si el enlace falla o devuelve un código de estado distinto de cero, el contenedor principal se eliminará.

Un manifiesto de pod que contiene un enlace posterior al inicio: post-start-hook.yaml

apiVersion:v1
kind:pod
metadata:
name:pod-with-poststart-hook
spec:
containers:
-image:luksa/kubia
name:kubia
lifecycle:
postStart:
exec:
comando:
-sh
- -c
-"echo 'hook fallará con código de salida 15';sleep 5;salida 15"

Como en el ejemplo anterior, los comandos echo, sleep y exit se ejecutan junto con el proceso principal del contenedor cuando se crea el contenedor. Por lo general, no ejecutamos comandos como este, sino que los ejecutamos a través de scripts de shell o ejecutables binarios almacenados en la imagen del contenedor.

Deje que el ciclo de vida se enganche antes de usar detener

Ejecutado en ganchos de parada previa inmediatamente antes de que se termine el contenedor. Cuando es necesario terminar un contenedor, kubelet ejecutará el gancho previo a la parada cuando el enlace previo a la parada esté configurado, y solo enviará la señal SIGTERM al proceso del contenedor después de que se ejecute el programa gancho (si el proceso no termina correctamente , será asesinado)

El gancho de parada previa se puede usar para activar el contenedor para que se apague correctamente cuando el contenedor no se cierra correctamente después de recibir la señal SIGTERM. Estos ganchos también pueden realizar operaciones arbitrarias antes de que finalice el contenedor y no requieren que la aplicación implemente estas operaciones internamente.

Configurar un enlace previo a la parada en el manifiesto del pod es similar a agregar un método de enlace posterior al inicio. El siguiente es un enlace previo a la parada que ejecuta una solicitud HTTP GET.

fragmento de código pre-stop-hoop-httpget.yaml

ciclo de vida:
preStop:
httpGet:
puerto: 8080
ruta: apagado

Como se definió anteriormente, el gancho previo a la detención ejecuta la solicitud HTTP GET a http://pod_IP :8080/shutdown inmediatamente cuando el kubelet comienza a detener el contenedor. También puede configurar el host del esquema (HTTP o HTTPS)

Nota: De forma predeterminada, el valor de host es la dirección IP del pod. Asegúrese de que la solicitud no se envíe a localhost, porque localhost significa nodo, no pod

A diferencia del enlace posterior al inicio, el contenedor finalizará independientemente de si la ejecución del enlace es exitosa o no. Ni un código de estado de error devuelto por HTTP ni un código de salida distinto de cero devuelto por un enlace basado en comandos evitará que el contenedor finalice. Si falla la ejecución del enlace previo a la detención, verá una alarma FailedPreStopHook en los eventos del pod.

Los ganchos de ciclo de vida son para contenedores, no para vainas

Los ganchos de ciclo de vida son para contenedores, no para vainas. El enlace de parada previa no debe usarse para ejecutar operaciones que deben realizarse cuando finaliza el pod. El motivo es que el enlace de predetención solo investigará antes de que se termine el contenedor (lo más probable es que se deba a un sondeo de actividad fallido). Este proceso ocurre varias veces durante el ciclo de vida del pod, no solo cuando se apaga el pod.

5. Comprender el apagado del módulo

El apagado del pod se desencadena por la eliminación del objeto del pod por parte del servidor API. Después de recibir la solicitud HTTP DELETE, el servidor API no elimina el objeto del pod, sino que establece un valor de marca de tiempo de eliminación para el pod. Los pods con deletionTimestamp comienzan a detenerse.

Cuando el kubelet se da cuenta de que se debe terminar un pod, comienza a terminar cada contenedor en el pod. El kubelet le dará a cada contenedor una cierta cantidad de tiempo para detenerse con gracia. Este tiempo se denomina Período de gracia de terminación y cada módulo se puede configurar individualmente. Una vez que comienza el proceso de finalización, el temporizador comienza a funcionar y los siguientes eventos se ejecutan en orden:

1) Ejecute el gancho de parada previa (si está configurado) y luego espere a que se complete

2) Enviar una señal SIGTERM al proceso principal del contenedor

3) Espere a que el contenedor se apague correctamente o espere a que expire el período de gracia de terminación

4) Si el proceso del contenedor principal no se cierra correctamente, use la señal sigkill para terminar el proceso por la fuerza.

Especificar un período de gracia de terminación

Establezca a través del campo spec.terminationGracePeriodPeriods de la especificación del pod. De forma predeterminada, el valor es 30, lo que significa que el contenedor tendrá 30 segundos para finalizar correctamente antes de verse obligado a finalizar.

Mejora: el período de gracia de terminación debe establecerse lo suficientemente largo para que su proceso de contenedor pueda completar la limpieza dentro de este período de tiempo.

Al eliminar un pod, el período de gracia de terminación especificado en la especificación del pod también puede ser anulado por:

$kubectl eliminar po mypod --período de gracia=5

El comando anterior esperará 5 segundos para que kuectl permita que el módulo se apague solo. Cuando se detengan todos los contenedores de pod, el kubelet notificará al servidor API y, finalmente, los recursos de pod se eliminarán. Es posible obligar al servidor API a eliminar los recursos del pod inmediatamente sin esperar confirmación. Se puede lograr configurando el período de gracia en 0 y luego agregando una opción --force

$kubectl eliminar po mypod --grace-period=0 --force

Cuando use las opciones anteriores, debe prestar atención, especialmente para los pods StatefulSet, el controlador StatefulSet tendrá mucho cuidado para evitar ejecutar dos instancias del mismo pod al mismo tiempo (dos pods tienen el mismo número de serie, nombre y montaje). el mismo volumen persistente). La eliminación forzada de un pod hace que el controlador cree un pod de reemplazo sin esperar a que los contenedores en el pod eliminado terminen de apagarse. En otras palabras, es posible que se estén ejecutando dos instancias del mismo pod al mismo tiempo, lo que hace que el servicio de clúster con estado se comporte de manera anormal. Solo elimine a la fuerza un pod con estado si se confirma que el pod ya no se está ejecutando o no puede comunicarse con otros miembros del clúster (esto puede confirmarse por la falla de conexión de red del nodo que aloja el pod y no se puede volver a conectar).

Manejar adecuadamente las operaciones de cierre de contenedores en la aplicación

Las aplicaciones deben responder a la señal SIGTERM iniciando un proceso de apagado y terminar cuando el proceso se complete. Además de manejar la señal SIGTERM, también debería ser posible enganchar antes de la parada para ser notificado de la parada. En ambos casos, la aplicación solo tiene una cantidad fija de tiempo para terminar limpiamente.

Pero, ¿qué sucede si es imposible predecir cuánto tiempo tardará la aplicación en finalizar limpiamente? Si la aplicación es un almacenamiento de datos distribuido, una de las instancias del módulo se eliminará y luego se cerrará durante la reducción.Durante el proceso de cierre, el módulo debe migrar sus datos a otros módulos supervivientes para asegurarse de que los datos no se pierdan. , entonces este pod debería comenzar a migrar datos cuando recibe una señal de finalización (ya sea a través de una señal SIGTERM o un gancho previo a la parada)?

La respuesta es no, este enfoque no se recomienda por las siguientes razones:

  • La terminación de un contenedor no significa necesariamente que todo el pod esté terminado

  • No hay garantía de que este proceso de apagado se pueda ejecutar antes de que se elimine el proceso.

Reemplace los procesos de apagado críticos con pods que se enfocan en los procesos de apagado

Una solución es hacer que la aplicación (al recibir una señal de finalización) cree un nuevo recurso de trabajo que ejecutará un nuevo pod cuyo único trabajo es migrar datos del pod eliminado al pod sobreviviente. Pero no puede garantizar que la aplicación pueda crear con éxito el objeto jod cada vez ¿Qué pasa si el nodo falla cuando la aplicación está a punto de crear un trabajo?

Una solución razonable a este problema es tener un pod dedicado que se ejecute de forma continua para comprobar continuamente si hay datos huérfanos. Cuando el pod encuentra datos huérfanos, puede migrarlos a los pods supervivientes. Por supuesto, no tiene que ser un pod que se ejecute continuamente, también puede usar los recursos de CronJob para ejecutar este pod periódicamente.

StatefulSet también tiene problemas. Por ejemplo, la reducción de StatefulSet hará que PersistentVolumeClaim esté en un estado aislado, lo que hará que los datos almacenados en PersistentVolumeClaim queden varados. Por supuesto, en el proceso de expansión posterior, PersistentVolume se adjuntará a la nueva instancia de pod, pero ¿qué pasa si esta operación de expansión nunca ocurre (o sucederá después de mucho tiempo)? . Por lo tanto, al usar StatefulSet, es posible que desee ejecutar un pod para la migración de datos. Para evitar la migración de datos durante el proceso de actualización de la aplicación, los pods especialmente utilizados para la migración de datos se pueden configurar con un tiempo de espera antes de la migración de datos, de modo que los pods con estado tener tiempo para empezar. ponerse de pie.

Supongo que te gusta

Origin blog.csdn.net/wwxsoft/article/details/129462532
Recomendado
Clasificación