Cómo tirar abajo tu servidor web

Si hace unos días contaba mis andanzas con hebras y lambdas y los problemas que había tenido con ello, hoy le toca el turno a otro error tonto que ha estado tirando abajo uno de los servidores web de producción durante unos días.

En mi trabajo del Mundo Real ™ tenemos varias aplicaciones de escritorio que interactúan con servidores web y llevábamos unos días en los que, de vez en cuando, el proceso w3wp.exe, es decir, el IIS, empezaba a consumir el 100% de CPU haciendo que el servidor dejara de responder adecuadamente.

Para averiguar qué petición era la que provocaba este consumo de CPU, usamos la herramienta de Procesos de Trabajo que se incluye en el administrador de IIS y cuyo manejo explican en este post. Con ella es muy fácil ver, entre otras cosas, el consumo de CPU de cada pool de aplicaciones e incluso los detalles de la petición que están generando esa carga de trabajo.

El problema en cuestión venía de una página muy sencilla desarrollada con ASP.NET usando WebForms, que básicamente se limita a obtener unos datos enviados a través de una petición POST y guardarlos en una base de datos.

El código que se usaba para obtener los datos enviados por el cliente era éste:

var buffer = new byte[Request.ContentLength];
var read = 0;

while (read < Request.ContentLength)
    read += Request.InputStream.Read(buffer, read, Request.ContentLength - read);

return Request.ContentEncoding.GetString(buffer);

Nada extraño. Se crea un buffer del tamaño que nos indican en el Request y vamos leyendo del InputStream hasta que hayamos leído todo lo que tenemos que leer. Fácil, ¿no? Pues sí, fácil y erróneo.

El problema es que si por algún motivo ContentLength no concuerda con la longitud del contenido de la petición, por ejemplo porque el cliente lo haya escrito mal o porque parte de los datos se hayan perdido por el camino, llegará un momento en que la llamada a InputStream.Read devolverá siempre 0 porque no hay nada más que leer y tendremos un precioso bucle infinito que consumirá el 100% de nuestra CPU.

Para solucionarlo, una opción es intentar leer de una sóla vez todo el Stream usando Read(buffer, 0, Request.ContentLength) y asumir que, si no conseguimos leerlo del tirón, algo va mal.

Otra opción, algo más fea, es añadir un timeout al bucle. No sé si ASPNET me garantiza que toda la información del Request está disponible desde el primer momento o si el Stream se va llenando poco a poco, así que por si acaso decidimos implementar la segunda opción y añadir un timeout:

const long TIMEOUT = 10*1000;

var start = Environment.TickCount;

var buffer = new byte[Request.ContentLength];
var read = 0;
while (read < Request.ContentLength && Environment.TickCount - start <= TIMEOUT)
    read += Request.InputStream.Read(buffer, read, Request.ContentLength - read);

return Request.ContentEncoding.GetString(buffer);

De esta forma, nuestro bucle "infinito" se reduce en el peor de los casos a 10 segundos, que para el tráfico que tiene este servidor es algo aceptable y evitamos el problema.

Moraleja

Nunca confíes en los datos de entrada que llegan de otro sistema, ni siquiera (o especialmente) si lo has programado tú.

Esa máxima es fácil de recordar cuando se trata de parámetros más visibles, como datos de formularios, URLs, etc., pero la verdad es que considerar ContentLength como un parámetro de entrada es algo que se te puede pasar fácilmente.

Me resulta extraño que la configuración por defecto de IIS no se defienda de este tipo de cosas, limitando el tiempo máximo de ejecución por petición o algo así. Sí, sé que en este caso es culpa mía y no del IIS, pero muchos de los parámetros por defecto del IIS están pensados para evitar que un error tire abajo la máquina (por ejemplo, la limitación del tamaño de peticiones).

Lo mejor de toda esta historia es que me ha permitido conocer esa pequeña joya de la administración del IIS que es el gestor de Procesos de Trabajo. Hay que ser positivos.

3 comentarios en “Cómo tirar abajo tu servidor web

  1. Me dio mucha risa la frase «Nunca confíes en los datos de entrada que llegan de otro sistema, ni siquiera (o especialmente) si lo has programado tú.»

    Siempre he desconfiado de los datos de entrada del usuario, pero jamás me había puesto a pensar que nuestro mayor enemigo muchas veces radica en nosotros mismos.

  2. Soy nuevo en esto y tengo ese problema con el IIS donde coloco esto:

    const long TIMEOUT = 10*1000;

    var start = Environment.TickCount;

    var buffer = new byte[Request.ContentLength];
    var read = 0;
    while (read < Request.ContentLength && Environment.TickCount – start <= TIMEOUT)
    read += Request.InputStream.Read(buffer, read, Request.ContentLength – read);

    return Request.ContentEncoding.GetString(buffer);

Comentarios cerrados.