Red de conocimiento informático - Espacio del host - Por qué necesitamos pausar los contenedores

Por qué necesitamos pausar los contenedores

Contenedor de pausa universal

Cuando usamos docker ps para ver los nodos del clúster de kubernetes, encontraremos que algunos contenedores llamados "pausa" se están ejecutando en los nodos.

¿Qué son estos contenedores "pausados"? ¿Por qué hay tantos contenedores suspendidos? ¿Qué está sucediendo?

Para responder a estas preguntas, necesitamos revisar cómo se crean estos pods en Kubernetes, específicamente en el entorno de ejecución de Docker/Containerd. Si aún no entiendes cómo funciona, consulta mi artículo anterior ¿Qué son exactamente los Kubernetes Pods?

Docker admite la implementación de software en forma de contenedores, y los contenedores también son una excelente manera de implementar software individual. Sin embargo, este método resulta muy inconveniente cuando queremos ejecutar varios módulos de un software al mismo tiempo. Esto suele ocurrir cuando los desarrolladores crean varias imágenes de Docker y luego necesitan usar módulos de monitoreo para iniciar y administrar múltiples procesos. En un entorno de producción, este enfoque sería más eficiente si estas aplicaciones se implementaran como un conjunto de contenedores y se separaran entre sí, permitiendo que cada grupo de contenedores disfrutara de un único entorno.

En respuesta a esta situación, Kubernetes propuso el concepto abstracto de pod, que oculta la complejidad de los indicadores en Docker, así como la complejidad de administrar los contenedores de Docker, los volúmenes compartidos de Docker y otros recursos de Docker. . También oculta las diferencias en los entornos de ejecución de diferentes contenedores.

En principio, todo lo que hay que hacer es crear un contenedor principal para configurar Docker para gestionar el uso compartido entre el grupo de contenedores. El contenedor principal necesita saber exactamente cómo crear contenedores que compartan el entorno de ejecución y, al mismo tiempo, gestionar el ciclo de vida de esos contenedores. Para implementar esta idea de contenedores principales, en Kubernetes se utiliza un contenedor en pausa como contenedor principal para todos los contenedores de un pod. Pausar un contenedor tiene dos funciones principales: primero, proporciona la base para todo el espacio de nombres de Linux del pod. En segundo lugar, habilita el espacio de nombres PID, actúa como un proceso PID 1 en cada pod y recicla procesos zombies.

En Linux, cuando ejecutamos un nuevo proceso, el proceso heredará el espacio de nombres del proceso padre. Cree un nuevo espacio de nombres "dejando de compartir" el espacio de nombres del proceso principal, ejecutando así el proceso en el nuevo espacio de nombres.

Después de que el proceso se esté ejecutando, podemos agregar otros procesos al espacio de nombres del proceso para formar un pod, o podemos usar setns en el mismo pod que solo compartan este espacio de nombres. El siguiente ejemplo ilustra cómo podemos crear un pod usando contenedores pausados ​​y disfrutando de espacios de nombres.

Primero, creamos un contenedor de pausa.

Luego podemos ejecutar otros contenedores para componer nuestro pod. Primero ejecute nginx y cree un proxy para localhost:2368.

Tenga en cuenta que también asignamos el puerto local 8080 al puerto 80 del contenedor pausado, no al puerto 80 del contenedor nginx. Esto se debe a que el contenedor pausado inicializa el espacio de nombres de la red y el contenedor nginx se unirá a ese espacio de nombres.

Luego, creamos el contenedor fantasma, que es un programa de blogs que actuará como nuestro servidor.

Al hacer lo anterior, tenemos acceso a todo lo que se ha hecho por nosotros.

En Linux, los procesos con un proceso padre forman una estructura de árbol en el mismo espacio de nombres PID. En esta estructura familiar, el proceso en el nodo raíz no tiene proceso padre; es el proceso inicial con PID 1.

Un proceso puede crear otros procesos usando las llamadas al sistema fork y exec, y el proceso que usa la llamada al sistema fork se convierte en el proceso padre del nuevo proceso.

fork se usa para crear otra copia del proceso actual, mientras que exec se usa para ejecutar un nuevo proceso para reemplazar el proceso actual, donde el PID del nuevo proceso es el mismo que el proceso reemplazado (para ejecutar una aplicación completamente independiente, usted deben tener los mismos procesos con el mismo PID). (Para ejecutar una aplicación completamente independiente, debe ejecutar las llamadas al sistema fork y exec, usar fork para crear un proceso hijo con un nuevo PID para el proceso actual y luego, cuando el proceso hijo detecte si es un proceso hijo , ejecute exec y él mismo Reemplace con el proceso que desea ejecutar, la mayoría de los lenguajes proporcionan funciones que implementan este método). Cada proceso tiene una entrada en la tabla de procesos del sistema. Registra información sobre el estado del proceso y los códigos de salida. Cuando un proceso hijo termina de ejecutarse, su entrada en la tabla de procesos permanece, pero sólo si el proceso padre ha obtenido su código de salida mediante la llamada al sistema de espera. Este proceso se llama proceso zombie de reciclaje.

Los procesos zombies son procesos que han dejado de ejecutarse pero tienen entradas en la tabla de procesos que aún existen porque el proceso principal no ha sido liberado. La razón principal por la que el proceso principal no se libera es que no se libera llamando a la llamada al sistema de espera. Técnicamente, cada proceso se convierte en un "zombi" durante un corto período de tiempo cuando finaliza, pero estos procesos zombies viven mucho más tiempo.

Cuando el proceso padre no llama a la llamada al sistema de espera después de que finaliza el proceso hijo, aparecerá un proceso zombie que sobrevive más tiempo. Una situación es que el proceso padre está mal escrito y simplemente ignora la llamada de espera, o el proceso padre muere antes que el proceso hijo y el nuevo proceso padre no llama a esperar. Por ejemplo, el proceso init "adopta" el proceso hijo y se convierte en su padre. Esto significa que ahora, cuando el proceso hijo sale, el nuevo proceso padre (init) debe llamar a esperar para obtener el código de salida; de lo contrario, su entrada en la tabla de procesos permanecerá para siempre y se convertirá en un zombi.

En el contenedor, cada espacio de nombres PID debe tener un proceso como proceso de inicio. Cada contenedor en Docker generalmente tiene su propio espacio de nombres PID y el proceso de punto de entrada es el proceso de inicio. Sin embargo, en un pod de Kubernetes, podemos tener un contenedor ejecutándose en el espacio de nombres de otro contenedor. En este caso, un contenedor debe asumir el rol del proceso de inicio y el otro contenedor se agrega al espacio de nombres como hijo del proceso de inicio.

En el siguiente ejemplo, agregaré el contenedor fantasma al espacio de nombres PID del contenedor nginx.

En este ejemplo, nginx asumirá el rol de PID 1 y el fantasma se agregará como un proceso hijo de nginx. Básicamente, esto no es un problema, pero técnicamente nginx ahora debe ser responsable de administrar cada proceso hijo. Por ejemplo, si Ghost bifurca un proceso hijo o usa exec para ejecutar un proceso hijo, pero Ghost falla antes de que finalice el proceso hijo, nginx tratará estos procesos hijos fantasma como sus propios procesos hijos. Sin embargo, nginx no fue diseñado originalmente para poder ejecutarse como proceso inicial y obtener zombies. Para resolver este problema, dentro de un pod de Kubernetes, los contenedores se ejecutan esencialmente de la misma manera que antes, pero se crea un contenedor en pausa especial para cada pod. Este contenedor de pausa ejecuta un proceso muy simple que no realiza ninguna funcionalidad y esencialmente duerme para siempre (consulte la llamada de pausa() a continuación). Es tan simple que puedo incluir el código fuente completo aquí:

Como se muestra arriba, hace más que simplemente hibernar. Una de sus funciones importantes es actuar como PID 1 en el pod y llamar a esperar para detectar el proceso zombie cuando está aislado por el proceso padre (ver sigreap). De esta manera, no tenemos procesos zombis que se acumulan en el espacio de nombres PID del pod de Kubernetes.

Vale la pena señalar que Kubernetes ha pasado por múltiples iteraciones del espacio de nombres PID, ****.

Pausar el contenedor puede ayudarlo a reciclar procesos zombies si el disfrute del espacio de nombres PID está habilitado; esta configuración actualmente solo está disponible en Kubernetes 1.7+. En los entornos de ejecución Docker 1.13.1+ y Kubernetes 1.7, esta opción está activada de forma predeterminada. También puede desactivarlo usando el indicador kubelet (--docker-disable-share-pid=true). Pero en Kubernetes 1.8, esta configuración se cambió nuevamente y ahora está deshabilitada de manera predeterminada a menos que la habilite usando el indicador kubelet (--docker-disable-share-pid=false). Consulte la discusión sobre cómo agregar ***share*** en el espacio de nombres PID en esta edición de GitHub.

Si el uso compartido del espacio de nombres PID *** no está habilitado, entonces cada contenedor en el pod de Kubernetes tiene su propio PID 1 y cada contenedor debe detectar el proceso zombie por sí solo. Muchas veces, esto no es un problema porque la aplicación no genera otros procesos, pero los procesos zombies que ocupan memoria es un problema que a menudo se pasa por alto. Entonces, dado que el uso compartido del espacio de nombres PID nos permite enviar señales entre contenedores en el mismo pod, me gustaría mucho que el uso compartido del espacio de nombres PID se convierta en el valor predeterminado en Kubernetes.