Situación inicial
En aplicaciones Python con múltiples procesos, es común enfrentar situaciones donde:
- Un proceso queda hangueado sin continuar su ejecución
- No se genera ninguna excepción ni mensaje de error
- Los logs tradicionales no proporcionan información útil
- Necesitamos identificar exactamente dónde está el bloqueo
En el ecosistema Java, herramientas como jstack y jmap permiten analizra el estado de los procesos. Para Python existe una alternativa similar llamada pystack.
Caso práctico
El problema identificado fue: una tarea que esperaba respuesta de un servicio HTTP sin configurar timeout, provocando que el proceso quedara en espera indefinida ante respuestas lentas del servidor.
Instalación de pystack
Requisitos previos
yum install gdb
pip install pystack-debugger
Instalación en entornos sin acceso externo
Para servidores en redes aisladas, se puede realizar una instalación manual:
# Descargar los paquetes necesarios
pip download pystack-debugger
# Instalar dependencias desde servidor interno
wget -c -t 10 http://servidor-interno:8080/packages/click-8.1.7-py2.py3-none-any.whl
wget -c -t 10 http://servidor-interno:8080/packages/pystack_debugger-0.9.0-py2-none-any.whl
# Instalar en orden
pip install ./click-8.1.7-py2.py3-none-any.whl
pip install ./pystack_debugger-0.9.0-py2-none-any.whl
Utilización de pystack
Análisis de hilos
Para obtener un dump de los hilos activos en un proceso:
pystack 22374
Resultado esperado:
pystack 22374
Dumping Threads....
File "/usr/lib64/python2.7/threading.py", line 784, in __bootstrap
self.__bootstrap_inner()
File "/usr/lib64/python2.7/threading.py", line 811, in __bootstrap_inner
self.run()
File "/usr/lib64/python2.7/threading.py", line 764, in run
self.__target(*self.__args, **self.__kwargs)
File "/proyectos/servicio/procesamiento/tareas/_base_job.py", line 157, in wait
time.sleep(self.watch_dog_duration)
---------------
File "/proyectos/servicio/ejecutor/tareas_ejecutor.py", line 17, in <module>
proceso.ejecutar()
File "/proyectos/servicio/procesamiento/tareas/_base_job.py", line 175, in ejecutar
self.job_execute()
File "/proyectos/servicio/procesamiento/tareas/_base_job.py", line 521, in job_execute
(codigo_respuesta, salida, error) = helpers.ejecutar_shell("bash " + self.nombre_script)
File "/proyectos/servicio/procesamiento/utils/helpers.py", line 218, in ejecutar_shell
stdout, stderr = p.communicate()
File "/usr/lib64/python2.7/subprocess.py", line 800, in communicate
return self._communicate(input)
File "/usr/lib64/python2.7/subprocess.py", line 1401, in _communicate
stdout, stderr = self._communicate_with_poll(input)
File "/usr/lib64/python2.7/subprocess.py", line 1455, in _communicate_with_poll
ready = poller.poll()
File "<string>", line 1, in <module>
File "<string>", line 1, in <module>
Análisis de greenlets
Para aplicaciones que utilizan la biblioteca greenlet (común en frameworks asíncronos):
pystack 22374 --include-greenlet
Este comando muestra tanto los hilos del sistema como los greenlets activos:
pystack 22374 --include-greenlet
Dumping Threads....
File "/usr/lib64/python2.7/threading.py", line 784, in __bootstrap
self.__bootstrap_inner()
File "/usr/lib64/python2.7/threading.py", line 811, in __bootstrap_inner
self.run()
File "/usr/lib64/python2.7/threading.py", line 764, in run
self.__target(*self.__args, **self.__kwargs)
File "/proyectos/servicio/procesamiento/tareas/_base_job.py", line 157, in wait
time.sleep(self.watch_dog_duration)
---------------
File "/proyectos/servicio/ejecutor/tareas_ejecutor.py", line 17, in <module>
proceso.ejecutar()
File "/proyectos/servicio/procesamiento/tareas/_base_job.py", line 175, in ejecutar
self.job_execute()
File "/proyectos/servicio/procesamiento/tareas/_base_job.py", line 521, in job_execute
(codigo_respuesta, salida, error) = helpers.ejecutar_shell("bash " + self.nombre_script)
File "/proyectos/servicio/procesamiento/utils/helpers.py", line 218, in ejecutar_shell
stdout, stderr = p.communicate()
File "/usr/lib64/python2.7/subprocess.py", line 800, in communicate
return self._communicate(input)
File "/usr/lib64/python2.7/subprocess.py", line 1401, in _communicate
stdout, stderr = self._communicate_with_poll(input)
File "/usr/lib64/python2.7/subprocess.py", line 1455, in _communicate_with_poll
ready = poller.poll()
File "<string>", line 1, in <module>
File "<string>", line 1, in <module>
Dumping Greenlets....
File "/proyectos/servicio/ejecutor/tareas_ejecutor.py", line 17, in <module>
proceso.ejecutar()
File "/proyectos/servicio/procesamiento/tareas/_base_job.py", line 175, in ejecutar
self.job_execute()
File "/proyectos/servicio/procesamiento/tareas/_base_job.py", line 521, in job_execute
(codigo_respuesta, salida, error) = helpers.ejecutar_shell("bash " + self.nombre_script)
File "/proyectos/servicio/procesamiento/utils/helpers.py", line 218, in ejecutar_shell
stdout, stderr = p.communicate()
File "/usr/lib64/python2.7/subprocess.py", line 800, in communicate
return self._communicate(input)
File "/usr/lib64/python2.7/subprocess.py", line 1401, in _communicate
stdout, stderr = self._communicate_with_poll(input)
File "/usr/lib64/python2.7/subprocess.py", line 1455, in _communicate_with_poll
ready = poller.poll()
File "<string>", line 1, in <module>
File "<string>", line 1, in <module>
File "<string>", line 1, in <genexpr>
Interpretación de resultados
El análisis revela que el proceso está bloqueado en subprocess.communicate(), esperando que un comando shell finalize su ejecución. Este comportamiento es típico cuando:
- Se llama a un servicio externo sin timeout configurado
- Un proceso hijo queda en estado zombie
- Existe un deadlock en la comunicación entre procesos
Recomendaciones
Para evitar este tipo de bloqueos:
- Siempre configurar timeouts en llamadas a servicios externos
- Utilizar mecanismos de timeout en operaciones de subprocess
- Implementar watchdog que termine procesos que excedan tiempo límite
- Monitorear procseos con herramientas como pystack de forma periódica