“Kubernetes aumentó 10 veces la latencia”: ¿quién tiene la culpa de esto?

Nota. traducir: Este artículo, escrito por Galo Navarro, que ocupa el puesto de Ingeniero Principal de Software en la empresa europea Adevinta, es una “investigación” fascinante e instructiva en el campo de las operaciones de infraestructura. Su título original fue ligeramente ampliado en la traducción por una razón que el autor explica al principio.

“Kubernetes aumentó 10 veces la latencia”: ¿quién tiene la culpa de esto?

Nota del autor: Se parece a esta publicación atraído mucha más atención de la esperada. Todavía recibo comentarios enojados de que el título del artículo es engañoso y que algunos lectores están entristecidos. Entiendo las razones de lo que está sucediendo, por eso, a pesar del riesgo de arruinar toda la intriga, quiero contarles de inmediato de qué trata este artículo. Una cosa curiosa que he visto a medida que los equipos migran a Kubernetes es que cada vez que surge un problema (como un aumento de la latencia después de una migración), lo primero que se culpa es a Kubernetes, pero luego resulta que el orquestador no está realmente dispuesto a hacerlo. culpa. Este artículo habla de uno de esos casos. Su nombre repite la exclamación de uno de nuestros desarrolladores (luego veréis que Kubernetes no tiene nada que ver). Aquí no encontrará ninguna revelación sorprendente sobre Kubernetes, pero puede esperar un par de buenas lecciones sobre sistemas complejos.

Hace un par de semanas, mi equipo estaba migrando un único microservicio a una plataforma central que incluía CI/CD, un tiempo de ejecución basado en Kubernetes, métricas y otras ventajas. La medida fue a modo de prueba: teníamos previsto tomarla como base y transferir aproximadamente 150 servicios más en los próximos meses. Todos ellos son responsables del funcionamiento de algunas de las mayores plataformas online de España (Infojobs, Fotocasa, etc.).

Después de implementar la aplicación en Kubernetes y redirigir parte del tráfico hacia ella, nos esperaba una sorpresa alarmante. Demora (estado latente) Las solicitudes en Kubernetes fueron 10 veces mayores que en EC2. En general, era necesario encontrar una solución a este problema o abandonar la migración del microservicio (y, posiblemente, de todo el proyecto).

¿Por qué la latencia es mucho mayor en Kubernetes que en EC2?

Para encontrar el cuello de botella, recopilamos métricas a lo largo de toda la ruta de la solicitud. Nuestra arquitectura es simple: una puerta de enlace API (Zuul) envía solicitudes a instancias de microservicios en EC2 o Kubernetes. En Kubernetes usamos NGINX Ingress Controller y los backends son objetos ordinarios como Despliegue con una aplicación JVM en la plataforma Spring.

                                  EC2
                            +---------------+
                            |  +---------+  |
                            |  |         |  |
                       +-------> BACKEND |  |
                       |    |  |         |  |
                       |    |  +---------+  |                   
                       |    +---------------+
             +------+  |
Public       |      |  |
      -------> ZUUL +--+
traffic      |      |  |              Kubernetes
             +------+  |    +-----------------------------+
                       |    |  +-------+      +---------+ |
                       |    |  |       |  xx  |         | |
                       +-------> NGINX +------> BACKEND | |
                            |  |       |  xx  |         | |
                            |  +-------+      +---------+ |
                            +-----------------------------+

El problema parecía estar relacionado con la latencia inicial en el backend (marqué el área del problema en el gráfico como "xx"). En EC2, la respuesta de la aplicación tardó unos 20 ms. En Kubernetes, la latencia aumentó a 100-200 ms.

Rápidamente descartamos a los posibles sospechosos relacionados con el cambio de tiempo de ejecución. La versión de JVM sigue siendo la misma. Los problemas de contenedorización tampoco tuvieron nada que ver: la aplicación ya se estaba ejecutando correctamente en contenedores en EC2. ¿Cargando? Pero observamos latencias altas incluso con 1 solicitud por segundo. También se podrían descuidar las pausas para la recogida de basura.

Uno de nuestros administradores de Kubernetes se preguntó si la aplicación tenía dependencias externas porque las consultas de DNS habían causado problemas similares en el pasado.

Hipótesis 1: resolución de nombres DNS

Para cada solicitud, nuestra aplicación accede a una instancia de AWS Elasticsearch de una a tres veces en un dominio como elastic.spain.adevinta.com. Dentro de nuestros contenedores hay una concha, así podremos comprobar si realmente la búsqueda de un dominio lleva mucho tiempo.

Consultas DNS desde el contenedor:

[root@be-851c76f696-alf8z /]# while true; do dig "elastic.spain.adevinta.com" | grep time; sleep 2; done
;; Query time: 22 msec
;; Query time: 22 msec
;; Query time: 29 msec
;; Query time: 21 msec
;; Query time: 28 msec
;; Query time: 43 msec
;; Query time: 39 msec

Solicitudes similares de una de las instancias EC2 donde se ejecuta la aplicación:

bash-4.4# while true; do dig "elastic.spain.adevinta.com" | grep time; sleep 2; done
;; Query time: 77 msec
;; Query time: 0 msec
;; Query time: 0 msec
;; Query time: 0 msec
;; Query time: 0 msec

Teniendo en cuenta que la búsqueda tomó aproximadamente 30 ms, quedó claro que la resolución DNS al acceder a Elasticsearch de hecho contribuía al aumento de la latencia.

Sin embargo, esto resultó extraño por dos razones:

  1. Ya tenemos un montón de aplicaciones de Kubernetes que interactúan con los recursos de AWS sin sufrir una alta latencia. Cualquiera sea el motivo, se relaciona específicamente con este caso.
  2. Sabemos que la JVM realiza el almacenamiento en caché de DNS en memoria. En nuestras imágenes, el valor TTL está escrito en $JAVA_HOME/jre/lib/security/java.security y establecido en 10 segundos: networkaddress.cache.ttl = 10. En otras palabras, la JVM debería almacenar en caché todas las consultas de DNS durante 10 segundos.

Para confirmar la primera hipótesis, decidimos dejar de llamar a DNS por un tiempo y ver si el problema desaparecía. Primero, decidimos reconfigurar la aplicación para que se comunicara directamente con Elasticsearch mediante una dirección IP, en lugar de mediante un nombre de dominio. Esto requeriría cambios de código y una nueva implementación, por lo que simplemente asignamos el dominio a su dirección IP en /etc/hosts:

34.55.5.111 elastic.spain.adevinta.com

Ahora el contenedor recibió una IP casi instantáneamente. Esto resultó en cierta mejora, pero estábamos sólo un poco más cerca de los niveles de latencia esperados. Aunque la resolución de DNS tomó mucho tiempo, la verdadera razón aún se nos escapaba.

Diagnóstico vía red

Decidimos analizar el tráfico desde el contenedor utilizando tcpdumppara ver qué está pasando exactamente en la red:

[root@be-851c76f696-alf8z /]# tcpdump -leni any -w capture.pcap

Luego enviamos varias solicitudes y descargamos su captura (kubectl cp my-service:/capture.pcap capture.pcap) para un análisis más detallado en Wireshark.

No había nada sospechoso en las consultas de DNS (excepto por una pequeña cosa de la que hablaré más adelante). Pero había ciertas rarezas en la forma en que nuestro servicio manejaba cada solicitud. A continuación se muestra una captura de pantalla de la captura que muestra la aceptación de la solicitud antes de que comience la respuesta:

“Kubernetes aumentó 10 veces la latencia”: ¿quién tiene la culpa de esto?

Los números de paquete se muestran en la primera columna. Para mayor claridad, he codificado por colores los diferentes flujos TCP.

La secuencia verde que comienza con el paquete 328 muestra cómo el cliente (172.17.22.150) estableció una conexión TCP con el contenedor (172.17.36.147). Después del apretón de manos inicial (328-330), el paquete 331 trajo HTTP GET /v1/.. — una solicitud entrante a nuestro servicio. Todo el proceso tomó 1 ms.

La secuencia gris (del paquete 339) muestra que nuestro servicio envió una solicitud HTTP a la instancia de Elasticsearch (no hay protocolo de enlace TCP porque está utilizando una conexión existente). Esto tomó 18 ms.

Hasta ahora todo está bien y los tiempos corresponden aproximadamente a los retrasos esperados (20-30 ms medidos desde el cliente).

Sin embargo, la sección azul tarda 86 ms. ¿Qué está pasando en él? Con el paquete 333, nuestro servicio envió una solicitud HTTP GET a /latest/meta-data/iam/security-credentials, e inmediatamente después, a través de la misma conexión TCP, otra solicitud GET a /latest/meta-data/iam/security-credentials/arn:...

Descubrimos que esto se repetía con cada solicitud a lo largo del seguimiento. De hecho, la resolución de DNS es un poco más lenta en nuestros contenedores (la explicación de este fenómeno es bastante interesante, pero la guardaré para un artículo aparte). Resultó que la causa de los largos retrasos fueron las llamadas al servicio de metadatos de instancia de AWS en cada solicitud.

Hipótesis 2: llamadas innecesarias a AWS

Ambos puntos finales pertenecen a API de metadatos de instancia de AWS. Nuestro microservicio utiliza este servicio mientras ejecuta Elasticsearch. Ambas convocatorias forman parte del proceso de autorización básico. El punto final al que se accede en la primera solicitud emite el rol de IAM asociado con la instancia.

/ # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
arn:aws:iam::<account_id>:role/some_role

La segunda solicitud solicita permisos temporales al segundo punto final para esta instancia:

/ # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/arn:aws:iam::<account_id>:role/some_role`
{
    "Code" : "Success",
    "LastUpdated" : "2012-04-26T16:39:16Z",
    "Type" : "AWS-HMAC",
    "AccessKeyId" : "ASIAIOSFODNN7EXAMPLE",
    "SecretAccessKey" : "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    "Token" : "token",
    "Expiration" : "2017-05-17T15:09:54Z"
}

El cliente puede utilizarlos por un corto período de tiempo y deberá obtener periódicamente nuevos certificados (antes de que sean Expiration). El modelo es simple: AWS rota las claves temporales con frecuencia por razones de seguridad, pero los clientes pueden almacenarlas en caché durante unos minutos para compensar la penalización de rendimiento asociada con la obtención de nuevos certificados.

El SDK de Java de AWS debería asumir la responsabilidad de organizar este proceso, pero por alguna razón esto no sucede.

Después de buscar problemas en GitHub, encontramos un problema #1921. Ella nos ayudó a determinar la dirección en la que “excavar” más.

El AWS SDK actualiza los certificados cuando se produce una de las siguientes condiciones:

  • Fecha de caducidad (Expiration) Caer en EXPIRATION_THRESHOLD, codificado en 15 minutos.
  • Ha pasado más tiempo desde el último intento de renovación de certificados REFRESH_THRESHOLD, codificado durante 60 minutos.

Para ver la fecha de vencimiento real de los certificados que recibimos, ejecutamos los comandos cURL anteriores tanto desde el contenedor como desde la instancia EC2. El período de validez del certificado recibido del contenedor resultó ser mucho más corto: exactamente 15 minutos.

Ahora todo ha quedado claro: para la primera solicitud, nuestro servicio recibió certificados temporales. Dado que no eran válidos durante más de 15 minutos, el SDK de AWS decidiría actualizarlos en una solicitud posterior. Y esto sucedió con cada solicitud.

¿Por qué se ha acortado el período de validez de los certificados?

Los metadatos de instancias de AWS están diseñados para funcionar con instancias EC2, no con Kubernetes. Por otro lado, no queríamos cambiar la interfaz de la aplicación. Para esto utilizamos kiam - una herramienta que, utilizando agentes en cada nodo de Kubernetes, permite a los usuarios (ingenieros que implementan aplicaciones en un clúster) asignar roles de IAM a contenedores en pods como si fueran instancias EC2. KIAM intercepta llamadas al servicio de metadatos de instancia de AWS y las procesa desde su caché, habiéndolas recibido previamente de AWS. Desde el punto de vista de la aplicación, nada cambia.

KIAM suministra certificados a corto plazo a los pods. Esto tiene sentido considerando que la vida útil promedio de un pod es más corta que la de una instancia EC2. Período de validez predeterminado para los certificados igual a los mismos 15 minutos.

Como resultado, si superpone ambos valores predeterminados uno encima del otro, surge un problema. Cada certificado proporcionado a una aplicación caduca después de 15 minutos. Sin embargo, AWS Java SDK fuerza la renovación de cualquier certificado al que le queden menos de 15 minutos antes de su fecha de vencimiento.

Como resultado, el certificado temporal se ve obligado a renovarse con cada solicitud, lo que implica un par de llamadas a la API de AWS y resulta en un aumento significativo de la latencia. En AWS Java SDK encontramos solicitud de función, que menciona un problema similar.

La solución resultó ser sencilla. Simplemente reconfiguramos KIAM para solicitar certificados con un período de validez más largo. Una vez que esto sucedió, las solicitudes comenzaron a fluir sin la participación del servicio de metadatos de AWS y la latencia cayó a niveles incluso más bajos que en EC2.

Hallazgos

Según nuestra experiencia con las migraciones, una de las fuentes más comunes de problemas no son los errores en Kubernetes u otros elementos de la plataforma. Tampoco aborda ninguna falla fundamental en los microservicios que estamos trasladando. Los problemas a menudo surgen simplemente porque juntamos diferentes elementos.

Mezclamos sistemas complejos que nunca antes habían interactuado entre sí, esperando que juntos formen un sistema único y más grande. Por desgracia, cuantos más elementos, más margen de error y mayor será la entropía.

En nuestro caso, la alta latencia no fue el resultado de errores o malas decisiones en Kubernetes, KIAM, AWS Java SDK o nuestro microservicio. Fue el resultado de combinar dos configuraciones predeterminadas independientes: una en KIAM y la otra en AWS Java SDK. Tomados por separado, ambos parámetros tienen sentido: la política de renovación de certificados activa en AWS Java SDK y el corto período de validez de los certificados en KAIM. Pero cuando los juntas, los resultados se vuelven impredecibles. Dos soluciones lógicas e independientes no tienen por qué tener sentido cuando se combinan.

PD del traductor

Puede obtener más información sobre la arquitectura de la utilidad KIAM para integrar AWS IAM con Kubernetes en este artículo de sus creadores.

Lea también en nuestro blog:

Fuente: habr.com

Añadir un comentario