Configurando threads en machine.config

La configuración de threads y conexiones de asp.net es un tópico oscuro. De algunos libros o KBs se puede obtener información, pero a mi entender, ninguna de ellas explica claramente cómo deben configurarse las opciones disponibles.


Las opciones que hago mención son las que se encuentran en el archivo machine.config, dentro de las siguientes secciones:




  • system.net/connectionManagement, atributo maxconnection



  • system.web/httpRuntime, atributo minFreeThreads



  • system.web/httpRuntime, atributo minLocalRequestFreeThreads



  • system.web/httpRuntime, atributo aspRequestQueueLimit



  • system.web/processModel, atributo maxWorkerThreads



  • system.web/processModel, atributo maxIoThreads



  • system.web/processModel, atributo minWorkerThreads



  • system.web/processModel, atributo minIoThreads



  • system.web/processModel, atributo requestQueueLimit


Estas opciones mencionadas son esencialmente para 1.1 ya que para 2.0 se pueden auto-configurar algunas (processModel). Mi experiencia en un caso específico no fue satisfactoria con la auto-configuración, así que optamos por la configuración manual.


La documentación existente hoy no es aclaratoria. Un libro que me gusta usar bastante es Improving .net Application Performance and Scalability, y a pesar de que tiene algunas secciones dedicadas a esta configuración, personalmente no me gusta la propuesta realizada porque no explica algunos detalles importantes ni tampoco te guía de forma correcta a encontrar tu propia configuración.


Detalles del caso


Los requerimientos eran bien simples. “Quiero que mi servidor procese la mayor cantidad de requerimientos que pueda, y tenga un tiempo de respuesta aceptable”. Seguramente este es el requerimientos tradicional de cualquier persona, y hay varios puntos que son totalmente subjetivos, pero hicimos el mejor esfuerzo por hacer pedazos el servidor con requerimientos.


Configuramos los valores sugeridos en el libro antes mencionado, y que son los mismos mencionados en este KB. Estos los despliego en la siguiente imagen, capturada desde el archivo scale.pdf (libro) disponible para bajar en éste link. Además vienen los valores por defecto.



Imagen obtenida de la página 280 del libro mencionado


Como el servidor web que estábamos probando tenía 4 CPUS, los valores aplicados fueron 48, 100, 100, 352 y 304 respectivamente.


Con la excitación de haber realizado la configuración correcta, procedimos a hacer las pruebas de carga, con 100 usuarios simultáneos, desde 2 servidores al mismo tiempo (200 usuarios en total).


Resultados


Los requerimientos se empezaron a encolar y después de algunos segundos empezaron a ser rechazados por el servidor, el cual consumía no más allá de un 3% de la CPU.


El escenario anterior está documentado en varias partes. Un uso de CPU muy bajo y encolamiento de requerimientos, entonces debes modificar tu servidor para que procese más requerimientos.


Ya se lo que hay que hacer. Ahora, ¿cómo lo hago?


Las páginas 445, 446 y otras del libro sugieren modificar modificar algunos valores y monitorear, si tendrás olas de cargas, etc. Es en este punto donde la documentación no es suficiente.


Con esto no quiero decir que mi explicación va a ser mejor que la del libro o que haya que copiar los valores que nosotros definimos para este servidor. Bajo ningún criterio se deberán copiar ciegamente los valores que nosotros aplicamos. Primero, se deberá lograr total entendimiento de los parámetros, la aplicación que se está sirviendo y las implicancias de los cambios que se hacen, para luego, de acuerdo a un estudio y pruebas de carga, se determinen cuales son los adecuados para tu ambiente. Me interesa mostrar una forma de llegar a los valores para lograr un proceso repetible.


En nuestra prueba de carga, el contador de rendimiento Requests Executing del objeto ASP.NET Apps v1.1.4322 mostraba 48 constantemente, mientras los encolados (Requests Queued de ASP.NET v1.1.4322) crecía impetuosamente. Luego empezó a crecer Requests Rejected del contador ASP.NET v1.1.4322.


Nota 1: Recordemos que los requerimientos empiezan a ser rechazados cuando la cantidad de requerimientos ejecutándose (Requests Current) sobrepasa el valor de system.web/processModel/requestQueueLimit. Los requerimientos ejecutándose (Requests Current) corresponden a la suma de los ejecutando, encolados y en espera de ser enviados al cliente. Esto significa que los requerimientos serán rechazados antes de que se llene la cola.


Nota 2: Existe otra cola más específica para cada aplicación o directorio web. El largo de ésta se define con system.web/httpRuntime/aspRequestQueueLimit, y su valor debiera ser menor comparado con system.web/processModel/requestQueueLimit ya que esta otra es una cola de uso general del Worker Process.


Bueno, teníamos 48 requerimientos ejecutándose y varias decenas encolándose. Se esperaba recibir 100 requerimientos por segundo. A ese ritmo, nuestro destino se veía muy lejos y con nubes negras encima [li]. ¿Dónde estaba el problema?


Análisis


Los valores sugeridos en el libro y el KB definen, utilizando un criterio basado en condiciones generales, que la cantidad de requerimientos a procesar por CPU será 12. Esto se refleja en varios atributos. Vamos por parte. Se hará referencia a las páginas del libro donde se definen.




  • maxconnection: valor sugerido, 12*#CPU. Este atributo limita la cantidad de conexiones a recibir (página 445). Es decir, si he definido 48, no puedo esperar a recibir más de 48, por ende, termino encolando y luego rechazando requerimientos.



  • maxIoThreads: valor sugerido, 100. Corresponde a la cantidad de threads disponibles para operaciones de I/O (disco, red, etc.), definido en la página 280. Este contador se multiplica automáticamente por la cantidad de CPUs del servidor. Entonces, potencialmente tenemos 400 threads disponibles para procesar requerimientos de I/O, pero ya sabemos que más de 48 no pasarán, y no todos son de I/O, así que serán menos.



  • maxWorkerThreads: valor sugerido, 100. Lo mismo de el atributo maxIoThreads, pero para threads que procesaran requerimientos y trabajarán dentro del proceso. Seguimos limitados por 48.



  • minFreeThreads: valor sugerido, 88*#CPUs. Este atributo representa la cantidad de threads que quiero que estén libres. ¿De dónde sale el 88?. Es la resta del máximo disponible menos la cantidad de requerimientos que quiero procesar por CPU, que es 12. Si la matemática no falla, 100-12 es 88. [:)]. Como dice la página 282, esta opción efectivamente limita la cantidad de requerimientos a procesar a 12, si maxWorkerThreads es 100.



  • minLocalRequestFreeThreads: valor sugerido, 76*#CPUs. Como el nombre lo dice, limita la cantidad de threads disponibles para requerimientos locales, dejando sólo 12 disponibles, ya que como las matemáticas se me dan hoy, 88-12=76 [:)].


Otros atributos interesantes.




  • minWorkerThreads: valor sugerido, maxWorkerThreads/2. Este atributo define la cantidad de threads que estarán disponibles en caso de una ola de requerimientos, lo que en el libro se llama Burst Load, explicado en la página 282. Al igual que su contraparte max, es implícitamente multiplicado por la cantidad de CPUs.



  • minIoThreads: no hay un valor sugerido, pero se puede suponer un valor similar a maxIoThreads/2. Se utiliza para soportar olas de requerimientos. Al igual que su contraparte max, es implícitamente multiplicado por la cantidad de CPUs.



  • requestQueueLimit: valor por defecto, 5000. Definir a criterio. Por lo general, el valor por defecto será suficiente.



  • aspRequestQueueLimit: valor por defecto, 100.


Durante las pruebas, rara vez lo contadores de threads del pool de aplicación (w3wp.exe) sobrepasaron los 80. Consideremos los 48 trabajando, mas los del garbage collector (1*#CPUs), el finalizador, el thread de compresión http, RPC local, etc., y otros más de control.


Nuestros requerimientos fueron rechazados cuando sobrepasamos los 100 de aspRequestQueueLimit. El uso del procesador no superaba el 3%.


Ajustes


Como podrán suponer, severos ajustes se debieron realizar para permitir procesar más requerimientos.


El cliente decidió soportar 1.000 requerimientos procesando al mismo tiempo. Un gran número. Eso no significa que el servidor recibirá 1.000 requerimientos por segundo, sino que podrá procesar del orden de 1.000 requerimientos al mismo tiempo. La duración de cada requerimiento impactará en la cantidad que se ejecuten concurrentemente. Como mencioné antes, el cliente esperaba recibir 100 requerimientos por segundo.


Siguiendo esta definición, los contadores se redefinieron con los siguiente valores. Explicaremos el por qué de algunos de ellos, y algunos puntos en contra que se deben considerar.


Si no has leído el resto del post, te solicito que lo hagas. Copiar estos valores sin entender qué hacen, podrá hacer que tu servidor colapse.




  • maxconnection = 1000. Si no se permite tamaña cantidad de conexiones, no tendremos los requerimientos.



  • maxWorkerThreads = 250. La formula dice que el contador se multiplica por #CPU, entonces, para poder procesar tantos requerimientos, se necesitan muchos threads..



  • maxIoThreads = 250. Seguimos la misma formula.



  • minFreeThreads = 100. No existe formula que aplicar acá. ¿Cuántos threads se quieren libres siempre? ¿10%, 5%? definir a criterio.



  • minLocalRequestFreeThreads = 50. Si no voy a procesar requerimientos locales, ¿para qué quiero reservar threads?. Este valor debió ser más pequeño.



  • appRequestQueueLimit = 1000. Si se van a procesar 1.000 requerimientos concurrentes, y se espera una tasa de 100 requerimientos por segundo, de nada sirve una cola de 100 registros. Se quedará corta en algunos minutos o segundos.



  • minWorkerThreads = 80. Si se hubiese aplicado la regla, se debió haber definido 125, para tener 500 threads disponibles. No se justifica. (ver consideraciones más abajo)



  • minIoThreads = 100. Igual que minWorkerThreads, si se hubiese aplicado la regla, se debió haber definido 125, para tener 500 threads de I/O disponibles. No se justifica. ¿por que no definimos el mismo valor que minWorkerThreads?. Nadie sabe. [;)].


Resultados obtenidos


Los resultados obtenidos fueron los esperados. Aunque no se pudo hacer que el servidor consumiera más del 15% del procesador, se logró procesar una tasa promedio de 84 requerimientos por segundo, con un máximo de 252 requerimientos en un segundo. Los requerimientos concurrentes promediaron 700 con un máximo de 970.


Se llegó a 970 ya que se aumentó la carga hasta 500 conexiones simultáneas por cada máquina de prueba (2 máquinas). Esos pobres servidores colapsaron ejecutando el script de [ACT].


El punto lamentable de la prueba fue darse cuenta de que el servidor de datos no fue capaz de responder a la demanda. El DBA tendrá que entretenerse un rato afinando consultas. Esto se tradujo en que algunos requerimientos demoraran más tiempo del esperado.


Consideraciones importantes


Aunque ya lo he dicho varias veces, te ruego no copiar los valores entregados aquí ya que son para un caso específico y condiciones especiales. Este es un servidor de servicios web, en donde cada uno de los servicios web se ejecutan en unas decenas de milisegundos (si el servidor de datos tiene poca carga). Si el servidor sirviese páginas ASPX con lógica más compleja que una simple consulta a la base de datos o si sirviese archivos de gran tamaño, la historia sería diferente y esta configuración seguramente haría sucumbir al servidor.


El tener en un momento 970 threads ejecutándose generará una alta tasa de context switch, lo que impactará el rendimiento de cualquier servidor. Nuevamente recalco que las condiciones especiales de este caso permitían definir tal cantidad de threads. Además, cada thread podría llegar a consumir 1 MB de memoria, aunque por lo general no debiera consumir más de 256K. Esto hará que se consuman cerca de 250 MB de memoria sólo por los threads. También es posible que se generen bloqueos entre los threads en dependencia de como esté desarrollada la aplicación. Es precisamente esto último lo que está detallado en el KB que mencioné mas arriba.


Todo esto debe tenerse en mente al configurar el archivo machine.config.


Saludos,
Patrick.

7 Replies to “Configurando threads en machine.config”

  1. Muy buena explicacion sobre la distribucion de carga en servidores, como explicas no debe ser mandatorio ni la verdad absoluta, pero ayudaria bastante en ambientes productivos.
    Bueno ya cuando se realizo la liberacion de carga, segun lo que explicaste… por parte del servidor web o negocio.., el encolamiento estaba en otro lado (como siempre pasa hacia… la base de datos).

    Saludos.. muy buen articulo..

  2. Amigo Carlos,

    tu lo haz dicho. Cada vez que se “destapa” un lado, se tapa otro. Después que ajusten la base de datos se podrá ver si los WS son el cuello de botella, y se repetirá el ciclo.

    Saludos,

    Patrick

  3. muy bueno
    me pregunto que tan dificil seria crear una aplicacion que nos diera los valores optimos para un servidor, algo que fuera incrementando los valores de estos parametros hasta llegar al balance deseado por el usuario y que fuera mostrando uso de memoria, # de threads, etc…

    side note: este post no salio en full feed, no se si todavia estes haciendo ajustes

    salu2

  4. Eber,

    a mi juicio, los valores óptimos dependen de tantos “grados de libertad” que lo que para unos usuarios es correcto, para otros puede ser nefasto.

    Toma este mismo caso. Si se copian estos valores para un servidor de páginas aspx, las cuales son “pesadas”, con viewstate, javascript, grandes listados, etc, seguramente el 95% del hardware “normal” colapsará en minutos. En este caso, como son web services livianos que solo realizan “selects” a la base de datos, y con respuestas breves, si funcionan.

    Creo que lo que se quiso hacer con la opción <processModel autoConfig=”true”/> del framework 2.0 fue eso mismo, pero a nosotros no nos funcionó y aún no se por qué. Tengo mi hipótesis, pero mientras no la pruebe, prefiero no comentar nada.

    Saludos y gracias por tus palabras.

    Patrick

Leave a Reply

Your email address will not be published. Required fields are marked *