Net Core (Linux) Error: System.IO .IOException: The configured user limit on the number of inotify instances has been reached

Buenas! Andaba yo preparando unas demos donde tenía varios contenedores ejecutándose en un Kubernetes, usando netcore y Linux. Todo funcionaba (más o menos) bien, hasta que de golpe y porrazo los contenedores empezaron a fallar:

System.IO.IOException: The configured user limit (1024) on the number of inotify instances has been reached

Este error apareció cuando escalé el número de contenedores y se daba en los nuevos contenedores creados (los iniciados seguían funcionando). ¿Qué podía estar sucendiendo?

Bueno, los que llevéis un tiempo en el mundo del desarrollo, seguro que habéis desarrollado una especie de sexto sentido para detectar errores: en muchos casos seguro que sois capaces de determinar más o menos donde puede estar el error. No siempre se acierta, por supuesto, pero en este caso tuve una ligera intiución que se demostró verdadera.

Como digo, era un código para unas demos, lo que significa que… bueno, a veces uno hace cosas que no deberían hacerse. Y eso es lo que ocurría en este caso. Tenía dos contenedores distintos que se ejecutaban. Uno era un contenedor con una Azure Function en netcore, el otro era una API, también en Net Core. En la AF tenía el siguiente código:

var config = new ConfigurationBuilder()
.SetBasePath(ctx.FunctionAppDirectory)
.AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();

Este código (donde ctx es el ExecutionContext) permite usar un IConfiguration para acceder a la configuración pasada a la función.

Por su parte en la web api, tenía el código «estándard» de inicialización:

public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

Pero (ya sabéis, es código para una demo xD), hacía algo que NO debe hacerse. Tenía un servicio (StorageService) declarado como transient en el sistema de DI. Este servicio era inyectado a un controlador de la API. Hasta ahí nada raro o incorrecto. La mala práctica estaba en el constructor del servicio StorageService:

public StorageClient(IConfiguration config)
{
    _constr = config["StorageConnectionString"];
}

Le inyectaba IConfiguration al servicioNO HAGÁIS NUNCA ESO. Es una mala práctica reconocida (un servicio no debería depender de toda la configuración, solo de aquellas partes que le afectan). Net Core ofrece la interfaz IOptions<T> para esos casos. Bueno… lo que decía de «código demo», para ahorrarme dos líneas y una clase extra inyecté IConfiguration directamente… Y eso es probable que contribuyese al error.

Vayamos ahora al error: se queja de que el número máximo de inotify ha sido alcanzado. Bien, en Linux un inotify es un objeto del kernel que se usa para monitorizar cambios en ficheros del disco. Como todos los objetos del kernel hay un límite (en este caso 1024).

Seguramente sabes que los contenedores permiten ejecutar tu aplicación en un entorno aislado, pero también debes (o deberías) saber que los contenedores no virtualizan: el kernel es compartido. Eso significa que es posible «auto ataques de DoS entre contenedores». Si el límite de instancias de inotify es 1024, este límite es a nivel de kernel (por cada usuario). Si un contenedor usa 1023 inotify, queda uno solo para todos los demás contenedores. Por eso, los nuevos contenedores fallaban al iniciarse: los inotify estaban agotados, y este es (por el momento) un recurso «global» compartido entre todos los contenedores. Ten presente siempre eso: el kernel se comparte y algunos de sus objetos tienen límites globales.

La pregunta, de qué causaba que se agotasen 1024 inotify con apenas 7-8 contenedores corriendo, tiene que ver con esa línea:

AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)

Este reloadOnChange: true, es el culpable. Ese parámetro habilita la reconfiguración automática si el fichero se modifica (algo que, dicho sea de paso me da igual, ya que los contenedores los configuro con variables de entorno). Me dirás «Ya, pero en la web api NO usas esa línea«. Falso: Esa lína está metida dentro del método WebHost.CreateDefaultBuilder.

Quizá haya algún error en netcore que hace que no se liberen los inotify o quizá eso es complicado/imposible de hacer automáticamente (hay una issue donde se discute eso). Pero parece que inyectar/crear múltiples veces un IConfiguration creado con reloadOnChange:true te llevará al desastre.

Honestamente, en mi caso yo tenía dos casos donde lo hacía:

  • La Azure function (cada invocación crea un IConfiguration)
  • La Web Api (se inyectaba IConfiguration en un controlador), que se llamaba varias veces por segundo.

No sé si la causa es la AF, la Web Api o ambos (me inclino a pensar que la AF, pero hay gente que dice que el error se les da solo con una web api, así que pueden ser ambos los causantes del error).

Lo solucione obviamente haciendo:

  1. No inyectando IConfiguration si no usando IOptions<T> en la web api
  2. Usando reloadOnChanges:false en la Azure Function

Tras esos dos cambios, parece ser que todo está funcionando. Si el error se me reproduce (ya informaré si se da el caso), eso significa que cualquier webapi puede generar ese error (usar IOptions<T> no lo solventa) y que deberíamos dejar de usar WebHost.CreateDefaultBuilder, o si lo usamos, modificar el builder para quitar las entradas de configuración creadas con reloadOnChange:true.

En cualquier caso recordad: Incluso sin problemas de inotify, es una mala práctica inyectar IConfiguration directamente 🙂

Saludos!

 

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *