Por qué tu servicio en Rust no pierde memoria: el verdadero culpable es el asignador de glibc

Fuentes: Your Rust Service Isn't Leaking — It Could Be the Allocator

Un servicio en Rust basado en Tokio, desplegado en Kubernetes sobre Ubuntu, experimentaba un consumo de memoria residente (RSS) que se mantenía plano cerca del límite del contenedor tras procesar ráfagas de aproximadamente 100.000 eventos cada 3-4 horas, sin retornar al sistema operativo. Las pruebas con la herramienta dhat demostraron que la aplicación liberaba la práctica totalidad de la memoria del montículo (heap): el pico fue de 1.455.866.178 bytes (1,4 GB) y al finalizar quedaban solo 10.798 bytes. La discrepancia entre la liberación lógica y la memoria reportada por Kubernetes reside en el comportamiento interno del asignador de glibc, ptmalloc. Este gestiona la memoria mediante arenas asociadas a hilos, respaldadas por submontículos creados con mmap, y asigna dentro de esas arenas todas las solicitudes por debajo del umbral dinámico de mmap (entre 128 KB y 32 MB en sistemas de 64 bits). Cuando los hilos de Tokio procesan eventos concurrentes, las asignaciones de distintas tareas se intercalan en las arenas sin fronteras claras. Aunque una tarea finalice y libere su bloque, el sistema operativo solo puede recuperar páginas contiguas al final del montículo, de modo que una asignación viva en mitad de la arena actúa como un tope que retiene todas las páginas inferiores. Además, ptmalloc almacena los bloques liberados en cachés por hilo (tcache) o en bins de la arena para reutilizarlos, lo que impide la consolidación y el recorte del montículo. Con la concurrencia limitada por un semáforo, las ráfagas siguientes reutilizan esos bloques y submontículos ya mapeados, produciendo un RSS estable y alto en lugar de crecer. Como prueba, invocar malloc_trim(0) al final de cada ráfaga devolvió la memoria a la línea base de forma inmediata. En producción, el equipo sustituyó glibc por jemalloc, cuyas arenas segregadas por clases de tamaño y basadas en mmap liberan las páginas no utilizadas de manera más eficaz, y la memoria descendió tras cada ráfaga. El caso ilustra que en Rust las fugas de memoria suelen ser aparentes y se originan en la política de retención del asignador del sistema, no en el código de la aplicación.