Programadores de fontaneros, o la historia de una fuga y las dificultades de lidiar con ella

Era martes 25 de febrero. El difícil lanzamiento de la versión el sábado 22 de febrero ya estaba en el pasado. Parecía que todo lo peor estaba atrás, y nada presagiaba problemas. Pero todo cambió a la vez cuando se produjo un error de monitoreo debido a una pérdida de memoria en el proceso coordinador del servicio de control de acceso.

¿De donde? Los últimos cambios importantes en la base del código del coordinador fueron en la versión anterior hace más de dos meses, y después de eso no sucedió nada notable en la memoria. Pero, desafortunadamente, los horarios de monitoreo fueron inflexibles: la memoria del coordinador obviamente comenzó a gotear en algún lugar, un gran charco hizo alarde en el piso de servicio, lo que significa que el equipo de plomería tenía mucho trabajo por hacer.



Primero hacemos una pequeña digresión. Entre otras cosas, VLSI le permite realizar un seguimiento de las horas de trabajo y controlar el acceso por modelo de rostro, huella digital o tarjetas de acceso. Al mismo tiempo, VLSI se comunica con los controladores de punto final (cerraduras, torniquetes, terminales de acceso, etc.). Un servicio separado se comunica con los dispositivos. Es pasivo, interactúa con dispositivos de control de acceso basados ​​en sus propios protocolos implementados a través de HTTP (S). Está escrito sobre la base de la pila estándar de servicios en nuestra empresa: la base de datos PostgreSQL, Python 3 se usa para la lógica de negocios, extendida con métodos C / C ++ desde nuestra plataforma.

Un nodo de servicio web típico consta de los siguientes procesos:

  • Monitor es el proceso raíz.
  • Un coordinador es un proceso secundario de un monitor.
  • Procesos de trabajo.

Monitor es el primer proceso de servicio web. Su tarea es llamar a fork (), iniciar el proceso del coordinador secundario y monitorear su trabajo. El coordinador es el proceso principal del servicio web, es él quien recibe solicitudes de dispositivos externos, envía respuestas y equilibra la carga. El coordinador envía la solicitud a los procesos de trabajo para su ejecución, ellos la ejecutan, transfieren la respuesta a la memoria compartida e informan al coordinador que la tarea se ha completado y usted puede recoger el resultado.

¿Quién tiene la culpa y qué hacer?


Por lo tanto, el coordinador del servicio de control de acceso difiere de los coordinadores de otros servicios en nuestra empresa por la presencia de un servidor web. Los coordinadores de otros servicios trabajaron sin fugas, por lo que el problema tuvo que buscarse en nuestra configuración. Peor aún, en los bancos de pruebas, la nueva versión existió durante mucho tiempo, y nadie notó problemas de memoria en ellos. Comenzaron a mirar más de cerca y descubrieron que la memoria fluye solo en uno de los soportes, e incluso entonces con un éxito variable, lo que significa que el problema aún no se reproduce tan fácilmente.



¿Qué hacer? ¿Cómo encontrar una razón? En primer lugar, tomamos un volcado de memoria y lo enviamos a especialistas de la plataforma para su análisis. El resultado: no hay nada en el vertedero: no hay razón, no hay indicios en qué dirección mirar más allá. Verificamos los cambios en el código del coordinador de la versión anterior; de repente hicimos algunas ediciones terribles, pero ¿no entendimos esto de inmediato? Pero no, solo se agregaron algunos comentarios al código del coordinador, pero un par de métodos se trasladaron a nuevos archivos, en general, nada criminal.

Comenzaron a mirar a nuestros colegas, desarrolladores del núcleo de nuestro servicio. Negaron con confianza la posibilidad misma de estar involucrados en nuestra desgracia, pero ofrecieron implantar el monitoreo de tracemalloc en el servicio. Apenas dicho que hecho, en la próxima revisión estamos finalizando el servicio, probando rápidamente, lanzando a la batalla.

Y ¿qué vemos? Ahora nuestra memoria fluye no solo rápidamente, sino también muy rápidamente: el crecimiento se ha vuelto exponencial. Atribuimos el primer pico a las fuerzas del mal y los factores que los acompañan, pero el segundo pico, varias horas después del primero, deja en claro que lleva demasiado tiempo esperar a que se libere el próximo hotfix con un comportamiento de emergencia del servicio. Por lo tanto, tomamos los resultados de tracemalloc y aplicamos parches al servicio, retrocediendo las ediciones con monitoreo para regresar al menos al crecimiento lineal.



Parecía que teníamos los resultados del trabajo de tracemalloc en la memoria asignada en Python, ahora los analizaremos y encontraremos al culpable de la fuga, pero no estaba allí: en los datos recopilados no hay picos de 5.5GB que vimos en los gráficos de monitoreo. La memoria máxima utilizada es de solo 250 MB, e incluso traceMalloc consume 130 MB de ellos. Esto es en parte explicable: tracemalloc le permite ver la dinámica de la memoria en Python, pero no sabe acerca de la asignación de memoria en los paquetes C y C ++ que implementa nuestra plataforma. No fue posible encontrar algo interesante en los datos obtenidos, la memoria se asigna en volúmenes aceptables a objetos ordinarios como secuencias, cadenas y diccionarios, en general, nada sospechoso. Luego decidimos eliminar todo lo superfluo de los datos, dejando solo el consumo total de memoria y el tiempo, y visualizar.Aunque la visualización no ayudó a responder las preguntas "qué está sucediendo" y "por qué", con su ayuda vimos una correlación con los datos del monitoreo, lo que significa que definitivamente tenemos un problema en algún lugar y tenemos que buscarlo.



En ese momento, nuestro equipo de fontaneros se quedó sin ideas sobre dónde buscar una fuga. Afortunadamente, el pájaro nos cantó que ocurrió un cambio importante desde la plataforma: la versión de Python cambió de 3.4 a 3.7, y este es un gran campo de búsqueda.

Decidimos buscar problemas relacionados con pérdidas de memoria en Python 3.7 en Internet, porque seguro que alguien ya se encontró con este comportamiento. Aún así, Python 3.7 se publicó hace mucho tiempo, cambiamos a él solo con la actualización actual. Afortunadamente, la respuesta a nuestra pregunta se encontró rápidamente, y también hubo un problema y una solicitud de extracción para solucionar el problema, y ​​ella misma estaba en los cambios realizados por los desarrolladores de Python.
¿Que pasó?

A partir de la versión 3.7, el comportamiento de la clase ThreadingMixIn ha cambiado, de lo que heredamos de nuestro servidor web para procesar cada solicitud en un hilo separado. En la clase ThreadingMixIn, agregaron la entrada de todos los hilos creados en una matriz. Debido a tales cambios, las instancias de clase que manejan conexiones de dispositivos no se liberan después de la finalización, y el recolector de basura en Python no puede borrar la memoria de los hilos gastados. Esto es lo que condujo al crecimiento lineal de la memoria asignada en proporción directa al número de solicitudes a nuestro servidor.

Aquí está, el código insidioso del módulo Python con un gran agujero (el código en Python 3.5 se muestra a la izquierda antes de los cambios, a la derecha, en 3.7, después):



Al descubrir la razón, eliminamos fácilmente la fuga: en nuestra clase de herederos cambiamos el valor de la bandera que devolvió el comportamiento anterior, y eso es todo: ¡una victoria! Las secuencias se crean como antes, sin escribir en la variable de clase, pero observamos una imagen agradable en los gráficos de monitoreo: ¡la fuga se ha solucionado!



Es bueno escribir sobre esto después de una victoria. Probablemente no seamos los primeros en encontrar este problema después de cambiar a Python 3.7, pero lo más probable es que no sea el último. Para nosotros, concluimos que necesitamos:

  • Adopte un enfoque más serio para evaluar las posibles consecuencias de cambios importantes, especialmente si otras decisiones aplicadas dependen de nosotros.
  • En el caso de cambios globales en la plataforma, como, por ejemplo, cambiar la versión de Python, verifique su código en busca de posibles problemas.
  • Responda a cualquier cambio sospechoso en los horarios de monitoreo no solo de los servicios de combate, sino también de los de prueba. A pesar del recolector de basura actual, también hay pérdidas de memoria en Python.
  • Es necesario tener cuidado con las herramientas de análisis de memoria como tracemalloc, ya que su uso incorrecto puede empeorar las cosas.
  • Debe estar preparado para el hecho de que la detección de pérdidas de memoria requerirá paciencia, perseverancia y un poco de trabajo de detective.

Bueno, me gustaría expresar mi gratitud a todos los que ayudaron a hacer frente al trabajo de plomería de emergencia y una vez más a volver a su capacidad de trabajo anterior a nuestro servicio.

All Articles