La configuración de Threading que podemos realizar en el fichero Machine.Config, puede resultarnos de gran ayuda para enfrentarnos ante problemas de rendimiento en IIS (incluyendo IIS Deadlocks), y en consecuencia, de SharePoint.
Se trata de una configuración no demasiado conocida, y de la cual tenemos disponible alguna KB, entradas en los Blogs de MSDN y Technet, así como otras referencias variadas en la Web. Un buen ejemplo, serían los siguientes enlaces:
Básicamente se trata de reducir las peticiones (Requests) que se quedan encoladas (y en consecuencia, su tiempo de espera), para de este modo, aumentar la concurrencia del sistema, y con ello, aumentar el número de peticiones por segundo. Esta es una forma de conseguir mejorar el rendimiento de ASP.NET, consiguiendo una mayor concurrencia de nuestros servidores (mayor rendimiento), y minimizando los IIS DeadLocks.
Además, el problema de los IIS Deadlocks puede producir un Reciclado del App Pool, que puede tardar más de lo habitual, y al final, entre unas cosas y otras nos encontramos con una indisponibilidad de varios minutos (bienvenido al mundo de las SLAs y las penalizaciones ;-).
Un caso práctico: configurando Threads en el Machine.Config para SharePoint 2007 (ASP.NET 2.0 en IIS6)
Vamos a ver un caso práctico paso a paso de un SharePoint 2007 x86 sobre una Máquina Virtual con 4 Cores ejecutando Windows Server 2003 R2 SP2 x86 (es decir, IIS6). Lo primero es localizar el fichero Machine.Config, que encontraremos en ruta en la que tenemos instalado en .Net Framework, en nuestro caso particular C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG, aunque también podría ser C:\WINDOWS\Microsoft.NET\Framework64\v2.0.50727\CONFIG para una máquina x64. Haremos una Copia de Seguridad del Machine.Config, para seguidamente editarlo.
Necesitamos configurar varios valores en el fichero Machine.Config, que por defecto no están, por lo que deberemos añadirlos en la mayoría de los casos, y configurarlos como deseemos. A continuación ponemos un ejemplo de una configuración genérica recomendada, la cual, puede ser o no la mejor opción para nuestros servidores (no hay que cegarse, estas recomendaciones no tienen por qué ser la mejor opción en todos los casos, se debe analizar cada caso por separado):
- system.web\processModel, atributo maxWorkerThreads. Controla el número máximo de Worker Threads en el Thread Pool. Su valor por defecto es 2, mientras que el valor recomendado es 100. El valor que especifiquemos, será implícitamente multiplicado por el número de CPUs.
- system.web\processModel, atributo maxIoThreads. Controla el número máximo de I/O Threads en el Thread Pool. Su valor por defecto es 2, mientras que el valor recomendado es 100. El valor que especifiquemos, será implícitamente multiplicado por el número de CPUs.
- system.web\processModel, atributo minWorkerThreads. Controla el número mínimo de Worker Threads que deben estar disponibles en el Thread Pool. En aquellas ocasiones, en las que de repente es necesario disponer de una gran cantidad de Threads, puede ser interesante establecer esta propiedad, ya que de lo contrario el sistema podría no ser capaz de conseguir crear todos estos Threads en un tiempo aceptable, produciéndose algunas esperas. Su valor por defecto depende de la versión de Net Framework, mientras que el valor recomendado es maxWorkerThreads/2, es decir, 50.
- system.web\processModel, atributo minIoThreads. Controla el número mínimo de I/O Threads que deben estar disponibles en el Thread Pool. En aquellas ocasiones, en las que de repente es necesario disponer de una gran cantidad de Threads, puede ser interesante establecer esta propiedad, ya que de lo contrario el sistema podría no ser capaz de conseguir crear todos estos Threads en un tiempo aceptable, produciéndose algunas esperas. Su valor por defecto depende de la versión de Net Framework, mientras que el valor recomendado es maxIoThreads/2, es decir, 50.
- system.web\httpRuntime, atributo minFreeThreads. Determina cuantos Worker Threads y Completion Port Threads deben estar disponibles para poder iniciar una petición (Request) remota. Si no hay suficientes Threads, la petición será encolada. Su valor por defecto es 8, mientras que el valor recomendado es 88*N, donde N es el número de CPUs. En nuestro caso, tenemos 4 CPUs, por lo que el valor recomendado sería 352.
- system.web\httpRuntime, atributo minLocalRequestFreeThreads. Determina cuantos Worker Threads y Completion Port Threads deben estar disponibles para poder iniciar una petición (Request) local. Si no hay suficientes Threads, la petición será encolada. Su valor por defecto es 4, mientras que el valor recomendado es 76*N, donde N es el número de CPUs. En nuestro caso, tenemos 4 CPUs, por lo que el valor recomendado sería 304.
- system.net\connectionManagement, atributo maxconnection. Controla el número máximo de conexiones salientes HTTP que se pueden iniciar desde un cliente, teniendo en cuenta, que en esta ocasión el cliente es el propio ASP.NET. Su valor por defecto es 2, mientras que el valor recomendado es 12*N, donde N es el número de CPUs. En nuestro caso, tenemos 4 CPUs, por lo que el valor recomendado sería 48. Sin embargo, en nuestro caso de ejemplo vamos a especificar un valor de 100.
Hay que tener en cuenta que ASP.NET en teoría no ejecutará más del siguiente número de peticiones (Requests) en el mismo tiempo: (maxWorkerThreads*number of CPUs)-minFreeThreads. Por lo tanto, al utilizar esta configuración, se podrá ejecutar un máximo de 12 peticiones (Requests) por CPU. Con todo esto, deseamos realizar la siguiente configuración en el fichero Machine.Config.
<system.net>
<connectionManagement>
<add address="*" maxconnection="100" />
</connectionManagement>
</system.net>
<system.web>
<processModel autoConfig="true" maxWorkerThreads = "100" maxIoThreads = "100"
minWorkerThreads = "50" minIoThreads = "50" />
<httpRuntime minFreeThreads="352" minLocalRequestFreeThreads="304" />
</system.web>
Por lo tanto, modificaremos el fichero Machine.Config con estos valores, y guardaremos los cambios. No hace falta hacer IISRESET para que aplique, otra cosa, es que nos quedemos más tranquilos haciéndolo.
Y hemos acabado.
Monitorización básica: Prueba y Error
Si lo deseamos, podemos probar diferentes combinaciones de los anteriores parámetros del Machine.Config, y a la vez hacer Pruebas de Carga de nuestro sistema con las diferentes configuraciones, del mismo modo, que podemos monitorizar el sistema real antes de cambiar dichos parámetros, y monitorizarlo también después de cambiarlos, y así poder ver las diferencias.
Podemos realizar las pruebas de carga con diferentes herramientas, como el Microsoft WCAT (Web Capacity Analysis Tool) que podemos encontrar disponible en el Kit de Recursos del IIS, o también con Visual Studio. En cualquier caso, podremos monitorizar algunos contadores de rendimiento, como por ejemplo:
- ASP.NET Apps v2.0.50727\Requests Executing
- ASP.NET v2.0.50727\Requests Queued
- ASP.NET v2.0.50727\Requests Current
A continuación se muestra un ejemplo de monitorización durante una pequeña prueba de carga con Microsoft WCAT sobre un MOSS 2007 x86, tomando los anteriores contadores, y algún que otro contador más.
Los resultamos que obtendremos pueden depender del tipo de actividad, es decir, no es lo mismo peticiones largas que consuman poca CPU, que peticiones cortas que consuman mucha CPU. En función de qué tipo de peticiones ejecute nuestro sistema, puede interesarnos realizar una configuración u otra del Machine.Config.
¿Y qué más podemos hacer?
Aumentar el número de CPUs, o bien, el número de servidores de nuestra Granja (suponiendo que tenemos algún tipo de balanceo como NLB). Estaba claro. El tamaño importa.
Al menos, desde mi desconocimiento. Es decir, en las pruebas que he estado realizando, jugando con las diferentes configuraciones de Threading del Machine.Config en ASP.NET v2.0 sobre IIS6 con un SharePoint 2007 SP3, y realizando diversas pruebas de carga con Microsoft WCAT (Web Capacity Analysis Tool), he observado:
- VM Win 2K3 x86 con 1 CPU. A partir de las 24 peticiones (Requests) concurrentes (contador Requests Executing), se empiezan a encolar (contador Requests Queued). WCAT sólo es capaz de alcanzar aproximadamente 1200 peticiones a la Home en 30 segundos.
- VM Win 2K3 x86 con 2 CPUs. A partir de las 48 peticiones (Requests) concurrentes (contador Requests Executing), se empiezan a encolar (contador Requests Queued). WCAT sólo es capaz de alcanzar aproximadamente 2400 peticiones a la Home en 30 segundos.
- Servidor físico Win 2K3 x86 con 8 CPUs. A partir de las 192 peticiones (Requests) concurrentes (contador Requests Executing), se empiezan a encolar (contador Requests Queued). No quiero mostrar los resultados de WCAT, ya que se trata de una Granja totalmente distinta a la anterior, y considero que no son datos comparables.
Curioso, ¿verdad?
Despedida y Cierre
En cualquier caso, este tipo de configuraciones resultan algo abstractas. A través del presente artículo, he intentado dejar un resumen junto con un muestrario de enlaces a otras entradas en la Web, para facilitar su comprensión, pero no deja de ser un tema algo complejo y abstracto, que en muchas ocasiones, lo acabaremos resolviendo por prueba y error.
Poco más por hoy. Como siempre, confío que la lectura resulte de interés.