/3GB, memoria de kernel y un sábado de locos

Si no considero las 3 horas que dormí del viernes al sábado, podría decir que llevo casi 48 horas despierto, o que llevaba hasta hoy en la mañana ya que pude dormir 5 horas más de 6 a 11 am (hoy es domingo en la tarde). ¿Cómo tanto?. La primera trasnochada fue por una fiesta de la oficina. La segunda, por un visita fugaz a un cliente en apuros. Mi día sábado fue extenuante y estresante para mi familia, cumpleaños, visitas, compra de pasajes, arreglo de taxis, reservas hoteleras, información adicional, ajustar otros viajes, una locura.


Menos detalles aburridos y vamos al caso


El escenario era similar al descrito a continuación:




  1. Aplicaciones COM+ configuradas por defecto, salvo que algunas de ellas estaban configuradas con diferentes identidades (usuario que será utilizado para ejecutar la aplicación Com+). Las opciones disponibles para identidad son Interactive, Local Service, Network Service o uno específico.



  2. Después de un par de días funcionando, cualquier aplicación COM+ que no estuviese funcionando y era requerida, fallaría al levantarse



  3. Si la aplicación no había sido detenida durante todo el período de tiempo, seguiría funcionando sin problemas, aún cuando otras aplicaciones no pudiesen levantarse.



  4. Si esta aplicación dejaba de ser requerida por más de 3 minutos (valor por defecto) y era bajada, un nuevo requerimiento trataría de levantarla y fallaría.


Claramente no estábamos en presencia de algún problema de código ya que  en ese caso, habría fallado siempre. Y más aún, nadie había hecho ningún cambio en los componentes en bastante tiempo.


Investigación del problema


Como en cualquier actividad de resolución de problemas, lo PRIMERO que hay que mirar es el event viewer o visor de sucesos (en español).


Dos tipos de eventos llamaban la atención sutilmente. No habían muchos registros de ellos, pero estaban en el momento adecuado, en el lugar adecuado.


Uno de los eventos estaba en el mismo momento cuando empezaban los problemas. El mensaje era el siguiente.




  • Failed to create a desktop due to desktop heap exhaustion


Adicionalmente, otros tres eventos habían ocurrido uno de estos días, los que terminaron de aclarar la situación. Este evento el 2019:




  • Event ID 2019: “The server was unable to allocate from the system nonpaged pool because the pool was empty.”


Serie de preguntas y respuestas


Ingeniero, es decir, yo [:)]: ¿Configuraron /3GB en boot.ini?
Contraparte: Si
Ingeniero: ¿y configuraron userva?
Contraparte: No
Ingeniero: mmm…interesante

Ingeniero: ¿Cómo andan las PTEs?
Ingeniero: perfmon -> Memory -> Free System Page Table Entries (PTEs) –> cercano a 2.000 … ouch


Conclusión


El Kernel se quedó sin memoria.


¿Ahh?…bajémoslo a la tierra.


En una arquitectura de 32 bits, la memoria virtual es un recurso medianamente escaso. No vamos a hablar de la cantidad memoria física del servidor. Si bien, mientras más memoria mejor, el problema a describir ahora no depende totalmente de la cantidad de memoria del servidor, ya que o bien tengas 2 o 20 GB, se presentará igual.


Con 32 bits, la memoria virtual, que tampoco tiene que ver con la memoria paginada en disco, está restringida a 4GB por aplicación. El valor 4GB se obtiene al elevar 2 a 32, o 2^32.


De esos 4 GB virtuales, 2 GB son usados por un proceso y los otros 2 por el kernel. Cada aplicación tendrá sus 2 GB virtuales y compartirán los 2 GB de kernel. Debido a esto último, algunos de ustedes recordarán haber visto en Task manager que un proceso nunca pasa de 1,7 o 1,8 GB. Bueno, la explicación es esa. Una aplicación no puede usar más de 2 GB de memoria virtual.


Existe un switch para que sí lo haga, pero con su consecuencia. Este switch se configura en el archivo boot.ini, y corresponde a la opción /3GB.


El impacto de la aplicación de éste es que del espacio virtual de 4 GB, 1 GB pasa de memoria de kernel a memoria de proceso de usuario. Entonces ahora un proceso de usuario puede llegar a usar 3 GB y el kernel queda drásticamente reducido a 1 GB.


SQL Server, Exchange, COM+ y los Application pool de IIS son algunos de los procesos que tomarán ventaja de este cambio y podrán llegar a 2,7 u 2,8 GB de memoria usada, lo que representa un incremento del 50%, y que tiende a ayudar bastante en algunos casos, pero en otros, produce problemas si no se usa adecuadamente (como éste).


¿El problema?


El kernel necesita memoria para poder funcionar, y 1 GB virtual no es suficiente en algunos casos. En el kernel se cargan drivers, el manejador de memoria, de procesos, controladores gráficos, el HAL, manejo de plug & play y otras tareas.


Además, el kernel maneja dos pool de memoria, uno llamado paginable y el otros no paginable. En ingles, paged pool y nonpaged pool respectivamente. ¿Recuerdan el evento 2019? Vuelvan a leer la descripción.


Al aplicar el switch /3GB, el tamaño de estos pools se reduce a la mitad, pudiendo entonces en determinados casos agotarse y poner en riesgo el servidor. Si el kernel no es capaz de obtener memoria de estos pools, varios problemas pueden ocurrir, a saber:




  • Problemas de interfaz grafica, en donde ésta no responde adecuadamente



  • Problemas de funcionamiento de algunos procesos, para nuestro caso, las aplicaciones COM+ no levantan.



  • Falla en procesar requerimientos vía red


Al aplicar /3GB se reduce también la cantidad de PTEs disponibles, lo que generará problemas con los requerimientos de IO, entre otros.


Identidades de aplicaciones


Complementando lo anterior, si recuerdan, en un inicio hablamos de las identidades de las aplicaciones COM+.


Este tópico no lo comprendo a cabalidad, pero puedo decir (con temor a equivocarme) que cada aplicación que es iniciada con un usuario diferente, requerirá que el sistema operativo cargue ciertos componentes para el usuario (desktop). Estos componentes incluyen:




  • Menús del sistema



  • Ventanas



  • Otra información.


Entonces, si tengo aplicaciones COM+ configuradas con X usuarios, podría llegar a cargar X desktops. ¿De donde se obtiene la memoria para cargar estos desktops? De la memoria del kernel.


Como sé que la información que acabo de entregar del tópico de Desktops es bastante limitada, prefiero incluir un link donde esta MUCHO mejor descrita. Aunque está en inglés, no está difícil. El link es http://blogs.msdn.com/ntdebugging/archive/2007/01/04/desktop-heap-overview.aspx.


Solución


Si tu aplicación no hará uso de 3GB, no incluyas la opción. No será necesaria.


Si tienes alguno de los servicios que mencioné antes y necesitas usarlo, deberás entonces usar además la opción userva en el archivo boot.ini.


Userva se utiliza para “mover” un poco de memoria de proceso de usuario a kernel. Realmente no mueve nada, pero el efecto final es que en vez de tener 3GB de usuario y 1GB de kernel, puedes configurar 2,7 GB y 1,3GB respectivamente, lo que ayudará a mitigar el problema, y en la gran mayoría de los casos, desaparecerá totalmente.


El KB referenciado al final muestra como configurarlo. Si bien dice que se deberá probar entre 2900 y 3030, si es necesario para tu sistema, podrás llegar a un valor menor. Mayores valores no tienen sentido.


Si estas usando SQL Server, utiliza mejor AWE por sobre 3GB. No expondrás el kernel a estos problemas.


Adicionalmente, estos links te podrán proveer mas información:


http://technet.microsoft.com/en-us/library/7a44b064-8872-4edf-aac7-36b2a17f662a.aspx
http://support.microsoft.com/kb/316739


Este otro está curioso: http://xentelworker.blogspot.com/2005/10/i-open-100-explorer-windows-and-my.html


Saludos,
Patrick.

Concatenación ultra rápida en Visual Basic 6.0 (ejercicio mental)

Después de una agradable tarde de relajo en el hotel Hilton de Sao Paulo, he decidido “jugar” un rato en el computador y echar a andar el cerebro.


Aunque el desarrollo utilizado Visual Basic 6.0 es cada día menos, decidí inventar un algoritmo (o función) para concatenar string que fuese más rápida que las que he visto en internet. Honestamente no he recorrido todos los sitios web disponibles, pero me basé en algunos códigos encontrados en algunos sitios, a saber:



El primero está muy bueno porque implementa una clase StringBuilder con funciones similares a las de su homónimo en .Net. El segundo, a pesar de ser más rápido que primero cuando el número de elementos a concatenar es alto, es muy engorroso y cualquiera que lo quiera usar, deberá tomarse un tiempo para poder implementarlo cómodamente.


Bueno, como mencionaba, mi intención para esta tarde era tratar de crear algo que corra más rápido, y así hacer funcionar las neuronas que a veces se nos atrofian.


Revisemos las implementaciones sugeridas en ambos posts, limitado a la parte importante de la concatenación, para no tener páginas y páginas de código.


Foros del web.






Class StringBuilder
    Private Sub Class_Initialize()
        growthRate = 50
        itemCount = 0
        ReDim arr(growthRate)
    End Sub
   
    Public Sub Append(ByVal strValue)
        If itemCount > UBound(arr) Then
            ReDim Preserve arr(UBound(arr) + growthRate)
        End If
        arr(itemCount) = strValue
        itemCount = itemCount + 1
    End Sub

    Public Function ToString()
        ToString = Join(arr, “”)
    End Function
End Class


La base del funcionamiento de este algoritmo es la utilización de un arreglo de strings donde en vez de concatenar, los strings se van agregando en el arreglo. Posteriormente la función ToString junta todos los strings del arreglo en uno solo. Simple, elegante y funcional. Notable.


MSDN






Sub Concat(Dest As String, Source As String)
    Dim L As Long
    L = Len(Source)
    If (ccOffset + L) >= Len(Dest) Then
        If L > ccIncrement Then
            Dest = Dest & Space$(L)
        Else
            Dest = Dest & Space$(ccIncrement)
        End If
    End If
    Mid$(Dest, ccOffset + 1, L) = Source
    ccOffset = ccOffset + L
End Sub
Sub MidConcat(ByVal LoopCount As Long)
    Dim BigStr As String, I As Long
    ccOffset = 0
    For I = 1 To LoopCount
        Concat BigStr, ConcatStr
    Next I
    BigStr = Left$(BigStr, ccOffset)
End Sub


La base del funcionamiento de este algoritmo es la utilización de un buffer donde se va copiando el contenido a concatenar. Este buffer se agranda a medida que se necesita agregar más información. Es bastante interesante, pero muy engorroso.


Desafío


El desafío que me planteé es realizar un algoritmo que fuese más rápido que ambos. Para hacerlo hice una lista con las ventajas y desventajas de cada uno. En este caso, las ventajas no me interesaban mucho ya que no tenia como mejorarlas, pero si mantenerlas en mi implementación, y me interesé en analizar las desventajas.


Entonces, la principal desventaja de cada uno es:




  • Foros del web: si la concatenación es con muchos strings, el arreglo crece bastante. Esto hará que la concatenación final no sea óptima. Además, agrandar el arreglo muchas veces también tiene su penalidad.



  • MSDN: cuando se llena el buffer, hace concatenación de strings para agrandarlo.


Entonces, ¿cómo mejorarlo?. Como me interesa la elegancia del primero, utilicé la misma idea, pero en vez de tener un arreglo de muchos strings pequeños, implementé un arreglo de buffers más grandes, en donde se va copiando el contenido con Mid$, sin necesidad de concatenar. La concatenación se hace al final, pero de sólo una cantidad de buffers mucho menor a que si fuesen strings chicos.


Con este algoritmo:



  1. Evito tener un arreglo de muchos elementos que luego hay que concatenar.

  2. Evito la concatenación de strings para agrandar el buffer, ya que cuando se acaba el buffer actual, agrego un nuevo elemento al arreglo de buffers.

Código de mi súper ultra rápida concatenación de strings.






Dim oArrBlocks() As String
Dim cBlocksUsados As Long
Dim iOffset As Long ‘mantiene la posición del último carácter copiado en el último buffer del arreglo
Const cNuevosBlocks As Long = 10
Const cNuevosBlocksLength As Long = 1000

Public Sub Class_Initialize()
    cBlocksUsados = 0 ‘Se define el primer bloque a llenar
    ReDim oArrBlocks(cNuevosBlocks) ‘se crea el arreglo de bloques
    oArrBlocks(0) = Space$(cNuevosBlocksLength) ‘Se llena el primero de espacios
    iOffset = 0
End Sub

Public Sub Append(ByVal sAppend As String)
    Dim iTemp As Long
    Dim iLargoCopiado As Long
    Dim iLargoTexto As Long
   
    iLargoTexto = Len(sAppend)
   
    If (iLargoTexto > 0) Then
        ‘revisar si cabe en el bloque que se está usando como buffer
        If (iLargoTexto + iOffset <= cNuevosBlocksLength) Then
            Mid$(oArrBlocks(cBlocksUsados), iOffset + 1, iLargoTexto) = sAppend
            iOffset = iOffset + iLargoTexto
        Else ‘ si no, hacer ajustes y particionar
            ‘Se copia lo que cabe en el bloque actual
            iLargoCopiado = cNuevosBlocksLength – iOffset
            If (iLargoCopiado > 0) Then
                Mid$(oArrBlocks(cBlocksUsados), iOffset + 1, iLargoCopiado) = Left$(sAppend, iLargoCopiado)
            End If
            ‘se crea un nuevo bloque y se copia el resto ahí.
            ‘Hay espacios disponibles en el arreglo de bloques?
            iTemp = UBound(oArrBlocks)
            If (iTemp = cBlocksUsados) Then
                iTemp = iTemp + cNuevosBlocks
                ReDim Preserve oArrBlocks(iTemp)
            End If
            cBlocksUsados = cBlocksUsados + 1
            iOffset = 0
            oArrBlocks(cBlocksUsados) = Space$(cNuevosBlocksLength)
            ‘Se copia el resto en el nuevo bloque
            iLargoCopiado = iLargoTexto – iLargoCopiado
            Mid$(oArrBlocks(cBlocksUsados), iOffset + 1, iLargoCopiado) = Right$(sAppend, iLargoCopiado)
            iOffset = iLargoCopiado
        End If
    End If
End Sub

Public Function ToString() As String
    oArrBlocks(cBlocksUsados) = Left$(oArrBlocks(cBlocksUsados), iOffset)
    ToString = Join$(oArrBlocks, “”)
End Function


¿Y los resultados?


Para concatenaciones pequeñas de caracteres, cualquiera de los tres es bastante eficiente. Sin embargo, cuando la cantidad de elementos a concatenar supera varias decenas de miles, se empieza a notar la diferencia. La unidad de medición para la prueba son los ticks, tomados antes y después de cada simulación. Para ser justos con la concatenación bruta (str = str & str2), en los otros algoritmos se incluyó el tiempo de creación y destrucción de objetos en cada una de las pruebas pertinentes. Cada prueba se realizó 5 veces, ingresando en la próxima tabla, los valores promedio.











































Algoritmo\Cantidad strings 1.000 5.000 10.000 25.000 50.000 100.000
Concatenación bruta 6 216 1.436 38.003 166.180 *
Foros del web 3 6,4 9,4 50 124,75 400
MSDN 0 3 15,4 37,4 105,75 356,2
Mi algoritmo 0 3 3 9,6 27,25 53

Para la prueba de 100.000 concatenaciones decidí excluir la concatenación bruta porque probablemente todavía estaría corriendo. [:)]


El siguiente gráfico muestra una curva con los resultados para los 3 algoritmos. Nuevamente no incluí la concatenación bruta ya que el grafico tendría una sola línea visible. Los otros algoritmos se dibujarían como líneas planas casi pegados a cero.



Conclusión


Sin lugar a dudas, el algoritmo logrado, que combina lo mejor y corrige lo peor de los dos anteriores, posee un mejor rendimiento, en especial, cuando la cantidad de elementos a concatenar es bastante. Para concatenaciones breves, cualquiera de los tres es igual de bueno.


Lamentablemente, hoy en día, este algoritmo no será muy utilizado ya que como comenté al inicio, ya casi no se hace desarrollo sobre Visual Basic 6.0 y para .net existe el objeto StringBuilder.


Como consuelo personal, rescato el ejercicio mental realizado hoy y los resultados obtenidos. Da gusto saber que aún “la neurona” responde.


desde Sao Paulo
Patrick.

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.