Depuración de aplicaciones Golang muy cargadas o cómo buscamos un problema en Kubernetes que no estaba allí

En el mundo moderno de las nubes de Kubernetes, de una forma u otra, uno tiene que enfrentarse a errores de software que usted o su colega no han cometido, pero tendrá que resolverlos. Este artículo puede ayudar a un recién llegado al mundo de Golang y Kubernetes a comprender algunas formas de depurar su propio software y el extranjero.

imagen

Mi nombre es Viktor Yagofarov, estoy desarrollando la nube de Kubernetes en DomKlik, y hoy quiero hablar sobre cómo resolvimos el problema con uno de los componentes clave de nuestro clúster de producción k8s (Kubernetes).

En nuestro grupo de combate (al momento de escribir):

  • Se lanzaron 1890 pods y 577 servicios (el número de microservicios reales también se encuentra en la región de esta figura)
  • Los controladores de Ingress sirven aproximadamente 6k RPS y aproximadamente la misma cantidad pasa Ingress directamente a hostPort .


Problema


Hace unos meses, nuestros pods comenzaron a experimentar un problema con la resolución de nombres DNS. El hecho es que DNS funciona principalmente sobre UDP, y en el kernel de Linux hay algunos problemas con conntrack y UDP. DNAT al acceder a las direcciones de servicio de k8s El servicio solo exacerba el problema con las carreras de conntrack . Vale la pena agregar que en nuestro clúster en el momento del problema había aproximadamente 40k RPS hacia los servidores DNS, CoreDNS.

imagen

Se decidió utilizar el servidor DNS de almacenamiento en caché local NodeLocal DNS (nodelocaldns) especialmente creado por la comunidad en cada nodo de trabajo del clúster, que todavía está en beta y está diseñado para resolver todos los problemas. En resumen: elimine UDP cuando se conecte al clúster DNS, elimine NAT, agregue una capa de caché adicional.

En la primera iteración de la implementación nodelocaldns, utilizamos la versión 1.15.4 (que no debe confundirse con la versión del cubo ), que vino con «kubernetes-installer» Kubespray : estamos hablando de nuestra empresa Fork fork de Southbridge.

Casi inmediatamente después de la introducción, comenzaron los problemas: la memoria fluyó y el hogar se reinició de acuerdo con los límites de memoria (OOM-Kill). En el momento de reiniciar esto, se perdió todo el tráfico en el host, ya que en todos los pods /etc/resolv.conf apuntaba exactamente a la dirección IP de nodelocaldns.

Esta situación definitivamente no se adaptaba a todos, y nuestro equipo de OPS tomó una serie de medidas para eliminarla.

Como yo mismo soy nuevo en Golang, estaba muy interesado en hacer todo este camino y familiarizarme con las aplicaciones de depuración en este maravilloso lenguaje de programación.

Estamos buscando una solucion


¡Entonces vamos!

La versión 1.15.7 se descargó al clúster de desarrollo , que ya se considera beta, y no alfa como 1.15.4, pero la doncella no tiene ese tráfico en DNS (40k RPS). Es triste.

En el proceso, desanudamos nodelocaldns de Kubespray y escribimos un gráfico especial de Helm para un despliegue más conveniente. Al mismo tiempo, escribieron un libro de jugadas para Kubespray, que le permite cambiar la configuración de kubelet sin digerir el estado completo del clúster por hora; además, esto se puede hacer puntualmente (verificando primero un pequeño número de nodos).

A continuación, implementamos la versión de nodelocaldns 1.15.7 para prod. La situación, por desgracia, se repitió. El recuerdo fluía.

El repositorio oficial de nodelocaldns tenía una versión etiquetada con 1.15. 8, pero por alguna razón no pude hacer que Docker tirara de esta versión y pensé que todavía no había recopilado la imagen oficial de Docker, por lo que esta versión no debería usarse. Este es un punto importante, y volveremos a él.

Depuración: etapa 1


Durante mucho tiempo no pude encontrar la forma de ensamblar mi versión de nodelocaldns en principio, ya que el Makefile del nabo se estrelló con errores incomprensibles dentro de la imagen del acoplador, y realmente no entendí cómo construir astutamente un proyecto Go con un proveedor de go , que se resolvió de manera extraña en directorios de inmediato para varias opciones de servidor DNS diferentes. El caso es que comencé a aprender Go cuando ya aparecían las versiones de dependencia normales .

Pavel Selivanov me ayudó mucho con el problema.pauljamm, por lo cual muchas gracias a él. Me las arreglé para armar mi versión.

A continuación, atornillamos el perfilador pprof , probamos el ensamblaje en la doncella y lo desplegamos en el producto.

Un colega del equipo de Chat realmente ayudó a comprender la creación de perfiles para que pueda aferrarse convenientemente a la utilidad pprof a través de la URL de la CLI y estudiar la memoria y procesar hilos utilizando los menús interactivos en el navegador, por lo que muchas gracias a él también.

A primera vista, según el resultado del generador de perfiles, el proceso funcionaba bien: la mayor parte de la memoria se asignaba a la pila y, al parecer, las rutinas Go la usaban constantemente.

Pero en algún momento se hizo evidente que los hogares "malos" de los nodos locales tenían demasiados hilos activos en comparación con los hogares "sanos". Y los hilos no desaparecieron en ningún lado, sino que continuaron colgados en la memoria. En este momento, se confirmó el presentimiento de Pavel Selivanov de que "los hilos fluyen".

imagen

Depuración: Etapa 2


Se volvió interesante por qué sucede esto (los hilos están fluyendo), y la siguiente etapa en el estudio del proceso de nodelocaldns ha comenzado.

Estático analizador de código staticcheck mostró que hay algunos problemas solo en la etapa de creación de un hilo en la biblioteca , que se utiliza en nodelocaldns (que inkluda CoreDNS, que inkluda nodelocaldns'om). Según tengo entendido, en algunos lugares no se transmite un puntero a la estructura , sino una copia de sus valores .

Se decidió hacer un núcleo del proceso "malo" usando la utilidad gcore y ver qué había dentro.

Atrapado en coredump con la herramienta dlv tipo gdbMe di cuenta de su poder, pero me di cuenta de que buscaría una razón de esta manera durante mucho tiempo. Por lo tanto, cargué coredump en el Goland IDE y analicé el estado de la memoria del proceso.

Depuración: Etapa 3


Fue muy interesante estudiar la estructura del programa, ver el código que los crea. En aproximadamente 10 minutos quedó claro que muchas rutinas de inicio crean algún tipo de estructura para las conexiones TCP, las marcan como falsas y nunca las eliminan (¿recuerdan unos 40k RPS?).

imagen

imagen

En las capturas de pantalla, puede ver la parte problemática del código y la estructura que no se borró cuando se cerró la sesión UDP.

Además, de coredump, el culpable de tal cantidad de RPS se hizo conocido por las direcciones IP en estas estructuras (gracias por ayudar a encontrar un cuello de botella en nuestro clúster :).

Decisión


Durante la lucha contra este problema, descubrí con la ayuda de colegas de la comunidad de Kubernetes que la imagen oficial de Docker de nodelocaldns 1.15.8 todavía existe (y en realidad tengo las manos torcidas y de alguna manera hice un tirón incorrecto de la ventana acoplable, o WIFI fue travieso en tirar momento).

En esta versión, las versiones de las bibliotecas que usa están muy "molestas": específicamente, el "culpable" "se apnalizó" ¡unas 20 versiones más!

Además, la nueva versión ya tiene soporte para la creación de perfiles a través de pprof y está habilitada a través de Configmap, no necesita volver a ensamblar nada.

Se descargó una nueva versión primero en dev y luego en prod.
III ... Victoria !
El proceso comenzó a devolver su memoria al sistema y los problemas se detuvieron.

En el gráfico a continuación puede ver la imagen: "DNS del fumador vs. DNS de una persona sana ".

imagen

recomendaciones


La conclusión es simple: verifique dos veces lo que está haciendo y no desdeñe la ayuda de la comunidad. Como resultado, pasamos más tiempo del problema durante varios días de lo que pudimos, pero recibimos una operación a prueba de fallas de DNS en contenedores. Gracias por leer hasta este punto :)

Enlaces útiles:

1. www.freecodecamp.org/news/how-i-investigated-memory-leaks-in-go-using-pprof-on-a-large-codebase-4bec4325e192
2 . habr.com/en/company/roistat/blog/413175
3. rakyll.org

All Articles