Cinco errores al implementar la primera aplicación en Kubernetes

Cinco errores al implementar la primera aplicación en KubernetesFail de Aris Dreamer

Mucha gente piensa que es suficiente transferir la aplicación a Kubernetes (ya sea usando Helm o manualmente), y habrá felicidad. Pero no todo es tan simple.

Equipo Soluciones en la nube Mail.ru tradujo un artículo del ingeniero de DevOps Julian Gindy. Cuenta qué escollos enfrentó su empresa durante el proceso de migración para que no pises el mismo rastrillo.

Paso uno: configurar solicitudes y límites de pods

Comencemos configurando un entorno limpio donde se ejecutarán nuestros pods. Kubernetes es excelente para la programación de pods y la conmutación por error. Pero resultó que el programador a veces no puede colocar un pod si es difícil estimar cuántos recursos necesita para funcionar correctamente. Aquí es donde aparecen las solicitudes de recursos y límites. Hay mucho debate sobre el mejor enfoque para establecer solicitudes y límites. A veces parece que esto es realmente más un arte que una ciencia. Aquí está nuestro enfoque.

Solicitudes de pods es el valor principal que utiliza el programador para colocar el pod de manera óptima.

de Documentación de Kubernetes: el paso de filtro define un conjunto de nodos donde se puede programar un Pod. Por ejemplo, el filtro PodFitsResources verifica si un nodo tiene suficientes recursos para satisfacer solicitudes de recursos específicas de un pod.

Usamos solicitudes de aplicaciones de tal manera que podemos estimar cuántos recursos de hecho La aplicación lo necesita para funcionar correctamente. De esta forma, el programador puede colocar los nodos de manera realista. Inicialmente, queríamos sobreprogramar las solicitudes para asegurar suficientes recursos para cada Pod, pero notamos que el tiempo de programación aumentó significativamente y algunos Pods no estaban completamente programados, como si no hubiera solicitudes de recursos para ellos.

En este caso, el programador a menudo "exprimía" los pods y no podía reprogramarlos porque el plano de control no tenía idea de cuántos recursos necesitaría la aplicación, que es un componente clave del algoritmo de programación.

Límites de pods es un límite más claro para un pod. Representa la cantidad máxima de recursos que el clúster asignará al contenedor.

De nuevo, desde documentación oficial: si un contenedor tiene un límite de memoria de 4 GiB, kubelet (y el tiempo de ejecución del contenedor) lo aplicarán. El tiempo de ejecución evita que el contenedor use más del límite de recursos especificado. Por ejemplo, cuando un proceso en un contenedor intenta utilizar una cantidad de memoria superior a la permitida, el kernel del sistema finaliza el proceso con un error de "memoria insuficiente" (OOM).

Un contenedor siempre puede usar más recursos de los que especifica la solicitud de recursos, pero nunca puede usar más del límite. Este valor es difícil de establecer correctamente, pero es muy importante.

Idealmente, queremos que los requisitos de recursos de un pod cambien durante el ciclo de vida de un proceso sin interferir con otros procesos en el sistema; este es el propósito de establecer límites.

Desafortunadamente, no puedo dar instrucciones específicas sobre qué valores establecer, pero nosotros mismos nos adherimos a las siguientes reglas:

  1. Usando una herramienta de prueba de carga, simulamos un nivel base de tráfico y observamos el uso de los recursos del módulo (memoria y procesador).
  2. Establezca las solicitudes de pod en un valor arbitrariamente bajo (con un límite de recursos de aproximadamente 5 veces el valor de las solicitudes) y observe. Cuando las solicitudes tienen un nivel demasiado bajo, el proceso no puede iniciarse, lo que a menudo provoca errores crípticos en el tiempo de ejecución de Go.

Observo que los límites de recursos más altos dificultan la programación porque el pod necesita un nodo de destino con suficientes recursos disponibles.

Imagine una situación en la que tiene un servidor web ligero con un límite de recursos muy alto, como 4 GB de memoria. Es probable que este proceso deba escalarse horizontalmente, y cada pod nuevo deberá programarse en un nodo con al menos 4 GB de memoria disponible. Si no existe tal nodo, el clúster debe introducir un nuevo nodo para procesar este pod, lo que puede llevar algún tiempo. Es importante lograr una diferencia mínima entre las solicitudes de recursos y los límites para garantizar un escalado rápido y sin problemas.

Paso dos: configure las pruebas de estado de vida y preparación

Este es otro tema sutil que se discute a menudo en la comunidad de Kubernetes. Es importante tener una buena comprensión de las pruebas Liveness y Readiness, ya que proporcionan un mecanismo para el funcionamiento estable del software y minimizan el tiempo de inactividad. Sin embargo, pueden afectar seriamente el rendimiento de su aplicación si no se configuran correctamente. A continuación se muestra un resumen de lo que son ambas muestras.

Vivacidad muestra si el contenedor se está ejecutando. Si falla, el kubelet elimina el contenedor y la política de reinicio se habilita para él. Si el contenedor no está equipado con un Liveness Probe, el estado predeterminado será correcto, como se indica en Documentación de Kubernetes.

Las sondas de actividad deben ser económicas, es decir, no consumir muchos recursos, ya que se ejecutan con frecuencia y deben informar a Kubernetes que la aplicación se está ejecutando.

Si configura la opción para que se ejecute cada segundo, esto agregará 1 solicitud por segundo, así que tenga en cuenta que se requerirán recursos adicionales para procesar este tráfico.

En nuestra empresa, las pruebas de Liveness prueban los componentes principales de una aplicación, incluso si los datos (por ejemplo, de una base de datos remota o caché) no están completamente disponibles.

Hemos configurado un punto final de "salud" en las aplicaciones que simplemente devuelve un código de respuesta 200. Esto es una indicación de que el proceso se está ejecutando y es capaz de manejar solicitudes (pero no tráfico todavía).

Muestra Preparación indica si el contenedor está listo para atender solicitudes. Si la sonda de preparación falla, el controlador de punto final elimina la dirección IP del pod de los puntos finales de todos los servicios que coinciden con el pod. Esto también se indica en la documentación de Kubernetes.

Los sondeos de preparación consumen más recursos, ya que deben llegar al backend de tal manera que muestren que la aplicación está lista para aceptar solicitudes.

Hay mucho debate en la comunidad sobre si acceder a la base de datos directamente. Teniendo en cuenta la sobrecarga (las comprobaciones son frecuentes, pero se pueden controlar), decidimos que para algunas aplicaciones, la preparación para atender el tráfico solo se cuenta después de comprobar que los registros se devuelven desde la base de datos. Las pruebas de preparación bien diseñadas garantizaron una mayor disponibilidad y eliminaron el tiempo de inactividad durante la implementación.

Si decide consultar la base de datos para probar la preparación de su aplicación, asegúrese de que sea lo más económica posible. Tomemos esta consulta:

SELECT small_item FROM table LIMIT 1

Aquí hay un ejemplo de cómo configuramos estos dos valores en Kubernetes:

livenessProbe: 
 httpGet:   
   path: /api/liveness    
   port: http 
readinessProbe:  
 httpGet:    
   path: /api/readiness    
   port: http  periodSeconds: 2

Puede agregar algunas opciones de configuración adicionales:

  • initialDelaySeconds - cuántos segundos pasarán entre el lanzamiento del contenedor y el inicio del lanzamiento de las sondas.
  • periodSeconds — intervalo de espera entre análisis de muestras.
  • timeoutSeconds — el número de segundos después de los cuales el módulo se considera de emergencia. Tiempo de espera habitual.
  • failureThreshold es el número de pruebas fallidas antes de que se envíe una señal de reinicio al módulo.
  • successThreshold es la cantidad de intentos exitosos antes de que el pod pase al estado listo (después de una falla cuando el pod se inicia o se recupera).

Paso tres: Configuración de las políticas de red predeterminadas del pod

Kubernetes tiene una topografía de red "plana", de manera predeterminada, todos los pods se comunican directamente entre sí. En algunos casos esto no es deseable.

Un posible problema de seguridad es que un atacante podría usar una sola aplicación vulnerable para enviar tráfico a todos los pods de la red. Como en muchas áreas de seguridad, aquí se aplica el principio de privilegio mínimo. Idealmente, las políticas de red deberían indicar explícitamente qué conexiones entre pods están permitidas y cuáles no.

Por ejemplo, la siguiente es una política simple que deniega todo el tráfico entrante para un espacio de nombres en particular:

---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:  
 name: default-deny-ingress
spec:  
 podSelector: {}  
 policyTypes:  
   - Ingress

Visualización de esta configuración:

Cinco errores al implementar la primera aplicación en Kubernetes
(https://miro.medium.com/max/875/1*-eiVw43azgzYzyN1th7cZg.gif)
Más detalles aquí.

Paso cuatro: comportamiento personalizado con ganchos y contenedores de inicio

Uno de nuestros principales objetivos era proporcionar implementaciones en Kubernetes sin tiempo de inactividad para los desarrolladores. Esto es difícil porque hay muchas opciones para cerrar aplicaciones y liberar sus recursos usados.

Surgieron dificultades particulares con Nginx. Notamos que al implementar estos Pods en secuencia, las conexiones activas se interrumpieron antes de completarse con éxito.

Después de una extensa investigación en Internet, resultó que Kubernetes no espera a que las conexiones de Nginx se agoten antes de apagar el pod. Con la ayuda del gancho previo a la parada, implementamos la siguiente funcionalidad y eliminamos por completo el tiempo de inactividad:

lifecycle: 
 preStop:
   exec:
     command: ["/usr/local/bin/nginx-killer.sh"]

y aquí nginx-killer.sh:

#!/bin/bash
sleep 3
PID=$(cat /run/nginx.pid)
nginx -s quit
while [ -d /proc/$PID ]; do
   echo "Waiting while shutting down nginx..."
   sleep 10
done

Otro paradigma extremadamente útil es el uso de contenedores init para manejar el lanzamiento de aplicaciones específicas. Esto es especialmente útil si tiene un proceso de migración de base de datos que consume muchos recursos y debe ejecutarse antes de que se inicie la aplicación. También puede especificar un límite de recursos superior para este proceso sin establecer dicho límite para la aplicación principal.

Otro esquema común es acceder a los secretos en el contenedor de inicio, que proporciona estas credenciales al módulo principal, lo que evita el acceso no autorizado a los secretos desde el propio módulo de la aplicación principal.

Como de costumbre, una cita de la documentación.: los contenedores init ejecutan de forma segura el código de usuario o las utilidades que, de otro modo, comprometerían la seguridad de la imagen del contenedor de la aplicación. Al mantener separadas las herramientas innecesarias, limita la superficie de ataque de la imagen del contenedor de la aplicación.

Paso cinco: configuración del kernel

Finalmente, hablemos de una técnica más avanzada.

Kubernetes es una plataforma extremadamente flexible que le permite ejecutar cargas de trabajo como mejor le parezca. Tenemos una serie de aplicaciones altamente eficientes que consumen muchos recursos. Después de realizar pruebas de carga exhaustivas, descubrimos que una de las aplicaciones tenía dificultades para mantenerse al día con la carga de tráfico esperada cuando la configuración predeterminada de Kubernetes estaba en vigor.

Sin embargo, Kubernetes le permite ejecutar un contenedor privilegiado que solo cambia los parámetros del kernel para un pod específico. Esto es lo que usamos para cambiar el número máximo de conexiones abiertas:

initContainers:
  - name: sysctl
     image: alpine:3.10
     securityContext:
         privileged: true
      command: ['sh', '-c', "sysctl -w net.core.somaxconn=32768"]

Esta es una técnica más avanzada que a menudo no es necesaria. Pero si su aplicación tiene dificultades para hacer frente a una carga pesada, puede intentar ajustar algunas de estas configuraciones. Más información sobre este proceso y configuración de diferentes valores, como siempre en la documentación oficial.

en conclusión

Si bien Kubernetes puede parecer una solución lista para usar, hay algunos pasos clave que se deben tomar para que las aplicaciones funcionen sin problemas.

A lo largo de la migración a Kubernetes, es importante seguir el "ciclo de prueba de carga": ejecute la aplicación, pruébela bajo carga, observe las métricas y el comportamiento de escalado, ajuste la configuración en función de estos datos y luego repita este ciclo nuevamente.

Sea realista sobre el tráfico esperado e intente ir más allá para ver qué componentes se rompen primero. Con este enfoque iterativo, solo unas pocas de las recomendaciones enumeradas pueden ser suficientes para lograr el éxito. O puede ser necesaria una personalización más profunda.

Hágase siempre estas preguntas:

  1. ¿Cuántos recursos consumen las aplicaciones y cómo cambiará esta cantidad?
  2. ¿Cuáles son los requisitos reales de escalado? ¿Cuánto tráfico manejará la aplicación en promedio? ¿Qué pasa con el tráfico pico?
  3. ¿Con qué frecuencia será necesario escalar horizontalmente el servicio? ¿Qué tan rápido deben estar en funcionamiento los nuevos pods para recibir tráfico?
  4. ¿Con qué gracia se apagan los pods? ¿Es necesario en absoluto? ¿Es posible lograr la implementación sin tiempo de inactividad?
  5. ¿Cómo minimizar los riesgos de seguridad y limitar el daño de cualquier pod comprometido? ¿Algún servicio tiene permisos o accesos que no necesita?

Kubernetes proporciona una plataforma increíble que le permite utilizar las mejores prácticas para implementar miles de servicios en un clúster. Sin embargo, todas las aplicaciones son diferentes. A veces, la implementación requiere un poco más de trabajo.

Afortunadamente, Kubernetes proporciona la configuración necesaria para lograr todos los objetivos técnicos. Mediante el uso de una combinación de solicitudes y límites de recursos, sondeos Liveness y Readiness, contenedores de inicialización, políticas de red y ajuste de kernel personalizado, puede lograr un alto rendimiento junto con tolerancia a fallas y escalabilidad rápida.

Qué más leer:

  1. Mejores prácticas y mejores prácticas para ejecutar contenedores y Kubernetes en entornos de producción.
  2. Más de 90 herramientas útiles para Kubernetes: implementación, administración, monitoreo, seguridad y más.
  3. Nuestro canal Around Kubernetes en Telegram.

Fuente: habr.com

Añadir un comentario