1 puntos por GN⁺ 2024-05-12 | 1 comentarios | Compartir por WhatsApp

Resumen del proceso para resolver una fuga de memoria relacionada con ActiveSupport::Notifications

  • Situación en la que ocurrió la fuga de memoria

    • A partir de cierto momento, el uso de memoria del Dyno web empezó a aumentar de forma anormal
    • Empezó a sonar el pager y apareció una situación que parecía una fuga de memoria
  • Respuesta inmediata

    • En Heroku, si se sospecha de una fuga de memoria, reiniciar el Dyno puede servir como solución temporal
    • Reiniciar siguiendo el ciclo normal de deploy o reiniciar manualmente los Dynos cercanos al límite de memoria
  • Revisión del código sospechoso para identificar la causa

    • Revisar los cambios de código desplegados justo antes del pico de memoria
    • Desplegar uno por uno algunos fragmentos de código sospechosos para comprobar si se produce la fuga de memoria
    • Como no había código que pareciera ser la causa, también se revirtieron cambios en tooling para verificarlo. Aun así, la fuga de memoria continuó
  • Análisis del patrón de aumento de memoria

    • La fuga solo ocurría en los Dynos web. Los Dynos de Sidekiq y Delayed::Job funcionaban con normalidad
    • No todos los Dynos web presentaban fuga siempre. Tras varias horas de uso normal, uno o dos, o incluso todos los Dynos, comenzaban a fugar memoria
    • Se sospechó que la causa no era el volumen de tráfico, sino cierto tipo de tráfico específico
    • La fuga no ocurría en todos los workers de Puma dentro del Dyno; unos pocos workers estaban usando la mayor parte de la memoria total
  • Recolección y análisis del heap dump

    • Se usó rbtrace para recolectar un heap dump del proceso Ruby en el que estaba ocurriendo la fuga
      • Con heroku ps:exec, conectarse por ssh al dyno con la fuga
      • Con el comando ps, seleccionar el proceso worker de Ruby que más memoria estaba usando
      • Adjuntarse a ese pid con rbtrace y comenzar a rastrear la asignación de memoria (ObjectSpace.trace_object_allocations_start)
      • Recolectar el heap dump con ObjectSpace.dump_all. Si el tamaño es grande, comprimir con gzip
      • Traer el archivo dump a la máquina local con heroku ps:copy
    • Se usó reap para visualizar el heap dump como flamegraph
      • Se encontró un Thread que referenciaba 1.9GB de memoria y, debajo de él, un Array que referenciaba 32,067 objetos
    • Se usó sheap para explorar los objetos sospechosos
      • Se descubrió que ese Thread era un worker thread de Puma
      • Un objeto ActiveSupport::SubscriberQueueRegistry estaba referenciando un Hash, y debajo había objetos String y Array
      • En el Array problemático se estaban acumulando más de 32,000 objetos ActiveSupport::Notifications::Event
  • Inferencia sobre la causa

    • Se supuso que los objetos Event de ActiveSupport::Notifications se estaban acumulando incorrectamente en el array #children
    • Se estimó que, si ocurría un error dentro del bloque de ActiveSupport::Notifications.instrument, ese Event no se eliminaba de #children y quedaba ahí, provocando la fuga de memoria
  • Reproducción local

    • Se enviaron solicitudes localmente usando el request path y los parámetros sospechosos encontrados en producción
    • Se confirmó la aparición de URI::InvalidURIError junto con 500 Internal Server Error
    • También se confirmó que el uso de memoria del dyno de producción que recibió esa solicitud aumentaba de forma abrupta
  • Análisis detallado de la causa

    • Existía un bug relacionado con Event#children de ActiveSupport::Notifications que fue corregido en Rails 7.1
    • Además, coincidió con un bug en la gema Bugsnag que, durante el proceso de limpiar la URL de la request, lanzaba URI::InvalidURIError durante URI.parse, y eso detonaba la fuga de memoria
    • Como el error lanzado dentro del bloque de ActiveSupport::Notifications.subscribe no era capturado, ese Event no se eliminaba del array #children y seguía acumulándose, causando la fuga de memoria
  • Solución

    • Corto plazo: actualizar la versión de la gema Bugsnag para que no lance un error incluso si ocurre URI::InvalidURIError
    • Largo plazo: actualizar a Rails 7.x, donde ya está corregido el bug de ActiveSupport::Notifications

Opinión de GN⁺

  • Resulta muy llamativo el proceso para detectar el problema e ir identificando la causa de forma sistemática. También resume muy bien un flujo básico de análisis que vale la pena intentar cuando se sospecha de una fuga de memoria
  • Parece que se están desarrollando activamente varias herramientas open source para recolectar, visualizar y analizar heap dumps de Ruby, como rbtrace, reap y sheap. Incluso fuera de Ruby, parece importante conocer herramientas útiles de análisis de memoria por lenguaje y saber aplicarlas a problemas reales
  • En muchos casos, la causa de una fuga de memoria termina siendo un bug en alguna librería o framework usado por el proyecto, pero normalmente no es fácil tener las condiciones para analizarlo, corregirlo y desplegarlo directamente. Por eso, es importante aplicar cuanto antes una forma de mitigación práctica. Acompañar el bug report con alternativas viables también es una buena estrategia
  • También fue valioso que no se quedara solo en resolver la fuga de memoria, sino que profundizara hasta llegar al root cause del problema. Esa actitud analítica de revisar con cuidado el código interno del framework y rastrear la causa fundamental parece muy necesaria para cualquier desarrollador
  • Al final, la causa de la fuga de memoria estaba en una actualización menor de una librería que al principio no parecía tener ninguna relación. Es un caso que muestra la importancia de la gestión de dependencias y del seguimiento de cambios. Incluso los cambios pequeños requieren analizar su impacto con cuidado y monitorear después del deploy

1 comentarios

 
GN⁺ 2024-05-12
Comentario de Hacker News

Se puede resolver con entrenamiento de ingeniería sin miedo a la gestión manual de memoria

  • Con solo RAII y reglas claras de propiedad, la gestión de memoria es una tarea de ingeniería sencilla
  • Más bien, los frameworks que insisten en el conteo de referencias y los punteros compartidos vuelven más difícil el problema al hacer ambigua la propiedad
  • Si lo creaste, lo liberas; si lo transferiste, dejas de preocuparte: eso es parte de la disciplina de ingeniería
  • Los bugs de memoria no son distintos de los bugs de lógica, así que arreglarlos es lo natural
  • Los recursos del SO (handles, sockets, etc.) también se gestionan manualmente sin administradores automáticos de recursos, así que con la memoria se puede aplicar el mismo enfoque

Un caso de pérdida de 5 millones de dólares por una fuga de memoria

  • Se presenta una anécdota ocurrida en los 90 por un bug de fuga de memoria en un driver de impresora de Solaris
  • En ese tiempo, en un banco se confirmaban transacciones por fax, se imprimían y luego se leían por teléfono a la contraparte mientras se grababa la llamada para obtener confirmación legal
  • Por la fuga de memoria, el driver de la impresora se cayó y no se imprimió la confirmación, así que la operación fue cancelada y se perdieron 5 millones de dólares
  • Al final, por una queja del CEO de Sun, los desarrolladores terminaron corrigiendo el bug

Herramientas para depurar fugas de memoria y formas de resolverlas

  • Con Valgrind se pueden encontrar fugas fácilmente en C
  • Si el diseño está bien hecho, por lo general la asignación y la liberación ocurren en la misma función, así que es fácil corregirlo
  • Se presenta un caso de fuga de memoria en un servidor de anuncios de Yahoo y una solución provisional
  • Se cita una broma del diseñador de PHP para mostrar una actitud más pragmática que perfeccionista
  • En Rails, dicen que lo habitual es resolverlo con hardware para priorizar la productividad

Elogios al estilo de escritura

  • Un comentario dice que la forma de escribir del autor resulta agradable, quizá por los emoticonos o por el formato