December 2007 - Artículos
En una entrada anterior hablábamos del Query Governor, ese recurso tan interesante como desconocido que nos permitía impedir la ejecución de consultas demasiado pesadas contra un servidor SQL Server. Aunque esta solución resulta interesante en muchos escenarios, en otros impedir la ejecución de la consulta completa puede ser poco operativo o incluso inviable. Como ya adelantamos en aquella entrada, una de las grandes novedades de SQL Server 2008 viene a ayudarnos precisamente en estos escenarios: el Resource Governor.
Como es evidente, para probar las técnicas de éste artículo necesitaremos tener acceso a un servidor SQL Server 2008. Tenemos dos opciones: o bien nos descargamos la última CTP y la instalamos en nuestra máquina (o mejor aún, una máquina virtual, por si acaso! xD) o bien tiramos de un Virtual Lab como éstos (más fácil no nos lo pueden poner).
Introducción al Resource Governor
Como ya comente anteriormente, la idea detrás del Resource Governor es poder controlar consultas determinadas y poder adjudicarles una cantidad específica de recursos. Para lograrlo, todas las conexiones que se realizan contra el SQL Server pasan por un clasificador, que se encarga de asignar un grupo a cada sesión en base a una función de clasificación. A estos grupos les podemos asignar un pool , que se encarga de otorgar y limitar los recursos (CPU y memoria).
Veámoslo de con un pequeño diagrama:
(Atiende que diagrama.. [cariñoso homenaje a los chicos de Muchachada Nui xD])
Ahora vamos a definir cada uno de los conceptos en negrita con un poco mas de calma :) Estos son, por tanto, los 0tres conceptos fundamentales:
Los Resource Pools, o simplemente pools, son un contenedor que representa un conjunto de los recursos del servidor. A día de hoy solo soporta solamente dos recursos: Memoria y CPU.
Cada recurso puede especificar dos valores porcentuales: Mínimo y Máximo.
El valor mínimo indica que como mínimo las consultas que se ejecuten dentro de ese pool dispondrán de ese porcentaje de memoria. La conclusión evidente es que la suma de los valores mínimos de todos los pools no puede superar 100. Se trata pues de un factor que nos permite garantizar unos recursos mínimos para la ejecución de las consultas que vayan a este pool.
El valor máximo, por otra parte, indica el limite a partir del cual no se otorgaran mas recursos a una consulta. Es un factor limitante, que nos permite preservar recursos del sistema impidiendo que las consultas que vayan a este pool excedan estos limites.
Hay dos pools especiales o particulares:
- Por una parte tenemos el Pool Interno (Internal Pool), que representa los recursos consumidos por el propio SQL Server. No hay restricciones a los recursos consumidos dentro de este pool y, de echo, éste puede hacer presión sobre el resto de pools, incluso aunque esto signifique violar los limites establecidos por ellos.
- El otro pool especial es el Pool por Defecto (Defaul Pool), que es el primer pool 'de usuario' que se crea. Éste puede ser alterado, pero no puede ser eliminado.
Los workload groups (o grupos) son contenedores para sesiones que cumplen unos mismos criterios. Como explicamos arriba, y veremos en detalle más abajo, cuando se realiza una petición de sesión contra el servidor se le asigna a un grupo en función de unas reglas de clasificación.
Estos grupos nos permiten monitorizar fácilmente todos los recursos que consume un mismo tipo de consultas, así como aplicar políticas a todas a la vez. También nos facilitan el mover todas las sesiones de un grupo de un pool a otro.
Tenemos dos grupos por defecto:
- El Grupo Interno (Internal Group) esta dedicado a contener las sesiones del propio SQL Server. No podemos cambiar nada que este dentro de este grupo, pero si podemos monitorizar su consumo de recursos.
- El Grupo por Defecto (Default Group), al que van todas las sesiones que no encajen con reglas de clasificación que los muevan a otros grupos.
Evidentemente, a partir de aquí podemos crear todos los grupos que necesitemos a voluntad. Por ejemplo, un grupo para sesiones de nuestro ERP, otro grupo para Reporting, otro exclusivo para el administrador...
Y finalmente llegamos al final... ¿o al principio? En realidad el clasificador es lo primero qué entra en juego, cronológicamente hablando, desde que se lanza la consulta hasta que ésta es procesada, pero parece que tiene más sentido explicar antes lo que es un pool y un grupo, por lo que he dejado el clasificador para el final.
Un clasificador no es mas que un mecanismo que emplea un conjunto de reglas suministradas por el sistema, o bien definidas por el usuario como UDFs, de modo que se permita un mapeo entre una sesión y un grupo.
... y ahora, al turrón! (que para algo es navidad...)
Vamos a hacer un escenario muy simple. ¿Por que muy simple? por mientras escribo esto son las 6.54am del día de Nochebuena (navidad ya!) y no es plan... además, el blog es mío y pongo los escenarios que me apetece :)
Escenario: Queremos limitar las consultas que vengan desde el SqlCmd, de modo que en tal caso obtengan un máximo de un 10% de CPU y un 5% de memoria del sistema. El resto de consultas no deben verse afectadas.
Lo primero que haremos será crear un pool donde limitaremos los recursos. Dentro de este pool será donde se ejecutarán las sesiones que se conecten a través del SqlCmd.
CREATE RESOURCE POOL poolSqlCmd
WITH
(
MAX_CPU_PERCENT=10,
MAX_MEMORY_PERCENT=5
);
Como visteis, muy sencillito todo. Podríamos haber especificado recursos mínimos, pero para este ejemplo no tiene sentido.
Ahora vamos a crear un grupo, que será donde almacenemos todas las sesiones que se conecten desde el SqlCmd. Lo haremos con el siguiente código:
CREATE WORKLOAD GROUP groupLimitado
WITH
(
IMPORTANCE=MEDIUM ,
REQUEST_MAX_MEMORY_GRANT_PERCENT=75,
REQUEST_MAX_CPU_TIME_SEC=600,
MAX_DOP=4,
GROUP_MAX_REQUESTS=10
)
USING poolSqlCmd;
Como podéis ver, las sesiones que entren en este grupo tendrán ciertas propiedades en común, que definimos a nivel de grupo:
- Por una parte tenemos su importancia, que es simplemente un mecanismo de prioridades. En este caso hemos establecido MEDIUM, que es el valor por defecto.
- También hemos especificado que no vamos a permitir que entre ninguna sesión cuyos requisitos de memoria sean mayores a un 75% de la memoria máxima asignada al grupo.
- Tampoco entrara ninguna sesión cuya petición de tiempo de CPU sea mayor a 600 segundos. (Esto es similar al Query Governor que vimos en el otro artículo)
- El grado máximo de paralelismo que permitiremos dentro del grupo es de 4. Esta limitación puede ser muy útil en entornos multiprocesador complejos.
- El último parámetro indica que como mucho procesaremos 10 peticiones a la vez, el resto no entraran al grupo hasta que alguna del grupo se libere.
Finalmente, gracias a la cláusula using, vinculamos el grupo a su pool por defecto, que será el pool que creamos previamente.
¡Ferpecto! Ahora solo nos queda vincular las sesiones entrantes a nuestro grupo si es necesario. La regla que habíamos establecido era la siguiente: capamos las consultas que vengan de SqlCmd, y dejamos las demás como estaban. Ok, nos creamos una función que devuelva el nombre del grupo en función del nombre de la aplicación que crea la conexión... algo como esto, vaya...
CREATE FUNCTION fnuClasificador() RETURNS SYSNAME
WITH SCHEMABINDING
AS
BEGIN
DECLARE @grupo SYSNAME
IF (APP_NAME() LIKE 'SQLCMD')
SET @grupo= 'groupLimitado'
ELSE
SET @grupo= 'default'
RETURN @grupo
END;
... y por último, le decimos al Resource Governor que a partir de ahora va a emplear nuestra función como función de clasificación:
ALTER RESOURCE GOVERNOR WITH (CLASSIFIER_FUNCTION=dbo.fnuClasificador);
Finalmente aplicamos los cambios con un RECONFIGURE, que se encargará de hacer que la nueva función de clasificación se empiece a utilizar.
ALTER RESOURCE GOVERNOR RECONFIGURE;
Ahora es cuando podemos probar los resultados. ¿Como? Pues por ejemplo, con la consulta generadora de números que vimos en el post acerca del Query Governor. Estos sin los resultados que he obtenido en mi máquina lanzando la consulta desde el Management Studio y desde el SqlCmd:
| Cliente |
Duración |
| Management Studio |
27.1 s. |
| SqlCmd |
53.5 s. |
La razón por la que la ejecución desde SqlCmd es más lenta es bien simple: hemos limitado su uso de recursos, forzándola a ejecutarse en el pool poolSqlCmd, con lo que no puede rendir lo mismo que la sesión que lanzamos desde el Management Studio, y que se ejecuta en el pool por defecto!
Por último, ¡no os olvides de eliminar nuestra configuración de prueba! Podéis hacerlo con un script como el siguiente, que elimina la función de clasificación, el grupo y finalmente notificar al Resource Governor para que utilice la nueva configuración:
USE master
GO
ALTER RESOURCE GOVERNOR WITH (CLASSIFIER_FUNCTION = NULL);
DROP WORKLOAD GROUP groupLimitado
GO
ALTER RESOURCE GOVERNOR RECONFIGURE;
Conclusiones:
Como hemos visto, el Resource Governor es un mecanismo sencillo y extremadamente versátil para la gestión de recurso de nuestros servidores. Nos permiten ajustar la carga en función del origen o las intenciones de la consulta, de modo que podemos establecer prioridades sobre los recursos de un modo muy sencillo. Además, resulta casi trivial modificar este balance de recursos a lo largo del día mediante scripts gestionados por el Agent que nos cambien la función de clasificación.
Aprovecho para recordaros que podéis obtener información adicional sobre el Resource Governor desde su página en los books online de SQL Server 2008; eso si, no olvidéis que se trata de documentación no definitiva, y así seguirá siendo hasta que el producto no esté en la calle.
Espero y deseo que este post os haya resultado de interés y os emocione al menos una décima parte de lo que a mi me emociona esta característica :)
Feliz Navidad a todos, y Keep Rockin'!!!
Rock Tip:
Si cuando comentamos el Query Governor decíamos que este no era suficiente, y hacíamos alusión al tema 'Not Enough' de Van Halen, en el caso del Resource Governor parece apropiado hacer referencia al temazo 'Everything' de Hardline, el grupo de los hermanos Gioely con Neal Schon a la guitarra. Un grupo bastante desconocido fuera del ámbito del hard rock, pero que estoy seguro de que agradará a más de un profano. Rock clasico, baladones y medios tiempos fantásticos con una voz grandiosa a cargo de Johnny Gioely, y la legendaria guitarra de Neal Schon. Discos como el Double Eclipse deberían ser oídos por todo amante de la música.
Aunque bien pensado, quizá everything tampoco sea muy apropiado... y antes de que me acusen de conformista o condescendiente con todos los productos de Microsoft, aclararé que espero ver en el futuro la posibilidad de limitar recursos de I/O a parte de CPU y memoria, pero bueno, me apetecía poner un tema de los Hardline y este pegaba más o menos :)
Creo que a veces todos nos hemos sentido atraídos o fascinados por una característica o producto que parece no levantar pasiones a nadie más. Intentamos convencer a nuestros amigos, compañeros y clientes de sus bondades, pero finalmente hemos de admitir que somos los únicos en comprenderlo o valorarlo (o en estar terriblemente ciegos y no ver que el producto apesta xD).
Eso es lo que me ha sucedido con SQL Server Notification Services. Lo he presentado en la práctica totalidad de las sesiones que he impartido sobre SQL Server 2005, he realizado las demos, y creía que los asistentes veían su utilidad... pero no debió ser así, ya que aparentemente no ha tenido muy buena acogida. Por ello, Microsoft ha anunciado su discontinuidad en SQL Server 2008.
Por supuesto, Microsoft seguirá soportando Notification Services de SQL Server 2005, al menos hasta que el soporte de SQL Server 2005 finalice, por lo que no debe alarmaros la noticia si sois alguno de los clientes que lo emplean actualmente en producción.
No es que fuera mi parte favorita del SQL Server, pero le tenía cariño :) Pero, como dicen por ahí, a Rey muerto Rey puesto: tengo tantos 'juguetes' nuevos para jugar con SQL Server 2008 que en en breves me olvidaré del ya ol' good NS :)
Rock Tip:
Para hoy, 'Love Lies', temazo del primer disco de los alemanes Casanova. La letra habla de cuando un amor se acaba por la ausencia de alguna de las partes, y como os digo, es un poco lo que sucede en este caso...
No os puedo poner esta vez un link a un vídeo, ni a la letra, ni a biografía del grupo... porque no los localizo! Como podéis ver, el disco es bastante complicado de conseguir, y ciertamente no se trata de un grupo muy conocido, pero si os gustan los medios tiempos en la onda de Europe, Bon Jovi y demás, no dejéis de buscarlo para pegarle un orejazo.
Hace tiempo publiqué un post dedicado a mis amigos los DBAs, pero desde entonces no he dedicado ningún articulo dedicado en exclusiva a tareas de administración en entornos SQL Server. Esta carencia de posts no se debe a una aversión ciega a los DBAs por mi parte; de echo, les tengo un respeto enorme. Yo no podría sentarme día a día a realizar operaciones críticas en un servidor y aguantar la presión (principalmente porque soy muy torpe y despistado, como mi amigo el pezuñas, y si el se equivoca de botón, yo también puedo hacerlo).
Hoy voy a tratar de redimirme, de aportar algo a la comunidad de DBAs en su continua batalla con los usuarios... hoy quería hablaros de una herramienta muy poco conocida dentro de SQL Server: el Query Governor (música de suspense aqui, por favor...), que no es más que un mecanismo de control de ejecución de consultas pesadas. ¿Para que nos sirve esto? Veamos pues...
Un Escenario...
Imaginemos un escenario como el siguiente: tenemos un entorno transaccional tradicional que es atacado por una aplicación. Tanto la aplicación como la base de datos están bien dimensionadas para el uso diario y rutinario. Sin embargo, un buen día se decide implementar funcionalidad de reporting sobre la misma base de datos, ya que no hay presupuesto/tiempo/conocimientos para implementar un sistema de reporting paralelo. Se puede dar el caso que ciertas peticiones de informes se realicen sin problemas, mientras que algunos informes concretos, debido a la pesadez de las consultas subyacentes, nos dejen temblando el servidor, afectándonos al rendimiento de la operativa principal del sistema.
Si nos encontramos con este escenario, una posible solución es notificar a todos los usuarios que retrasen la ejecución de estos informes para horas de baja actividad del sistema... *carcajadas* ... Sabemos que esa no es una opción válida para un administrador; sabemos como son los usuarios, les conocemos bien :)
Una solución fantástica sería poder limitar el consumo de CPU y de memoria que puede emplea una consulta o grupo de consultas para su ejecución. ¡La buena noticia es que esa solución existe! La mala noticia es que no os voy a hablar de ella por ahora ;) Se trata de mi característica favorita de SQL Server 2008, conocida como el Resource Governor, y os hablaré de ella en un post próximo.
No obstante, antes quería escribir este post, que se basa en una solución intermedia, que consiste en cancelar la ejecución de una consulta cuyo consumo de CPU exceda un umbral predefinido. Esta solución no es tan elegante como la anterior, pero la ventaja es que la tenemos disponible en SQL Server 2000 y SQL Server 2005, con lo que podemos aplicarla a entornos actuales de producción.
Ahora que tenemos claro un escenario de ejemplo y la herramienta que vamos a utilizar, podemos pasar a describir al...
Query Governor
Siempre que pienso en el Query Governor me viene a la mente el retrato del Gobernador de Barbados, e historias de piratas, corsarios y la buena vida en Port Royal vienen a mi mente... pero lamentablemente la realidad es mucho más aburrida :(
El Query Governor, como hemos venido comentando, nos permite definir un umbral de tiempo de ejecución para nuestras consultas. Esto es, si la estimación del tiempo que va a durar una consulta es superior al valor definido como limite de tiempo en el Query Governor, esta consulta no llegará a comenzar su ejecución.
Para ver el valor actual, lo que debemos hacer es consultar el valor de los parámetros de configuración. Podemos hacerlo mediante la interfaz de usuario del SQL Server Management Studio, o mejor aún, mediante el procedimiento almacenado sp_configure. Como se trata de una opción avanzada, antes de poder visualizarla debemos habilitar las opciones avanzadas de configuración:
sp_configure 'show advanced options', 1
GO
RECONFIGURE
GO
sp_configure
Esto nos debería devolver la lista con todos los parámetros de configuración, entre los que se encontrará el query governor cost limit, que en mi caso vale 0 (el valor por defecto, que quiere decir que el Query Governor no está en uso).
Si alteramos el valor, podemos establecer el número de segundos máximos que otorgamos a la ejecución de cualquier consulta sobre toda la instancia. Podemos también establecer este parámetro a nivel de conexión, con el parametro SET query_governor_cost_limit, pero siendo realista ¿que cliente se va a poner restricciones a si mismo? :)
Vamos a escribir una consulta de ejemplo que resulte bastante pesada, por ejemplo la siguiente:
CREATE DATABASE PruebaQueryGovernor
GO
USE PruebaQueryGovernor
GO
CREATE TABLE dbo.Numeros
(
Numero INT IDENTITY(1,1) PRIMARY KEY CLUSTERED
)
GO
WHILE COALESCE(SCOPE_IDENTITY(), 0) <= 100000
BEGIN
INSERT dbo.Numeros DEFAULT VALUES
END
SELECT * FROM dbo.Numeros
Como se puede apreciar en el código, nos limitamos a crear una tabla auxiliar de números (viva la numeración!), en éste caso los primeros 100.000 números. La construcción de esta tabla tarda poco mas de 6 minutos en mi portátil, y bajo la configuración por defecto se ejecuta sin ningún problema. Vamos ahora a establecer un valor de umbral de tiempo de ejecución de, por ejemplo, 100 segundos:
sp_configure 'query governor cost limit', 100
RECONFIGURE
Ahora, si eliminamos la base de datos de prueba y volvemos a crear al tabla y a intentar poblarla, veremos que no se llega a realizar ninguna inserción, pues SQL Server determina que la consulta es demasiado pesada y la cancela antes de su ejecución. Ain't it fun? :)
Conclusiones
Hemos repasado el Query Governor al que tanto provecho le podemos sacar en nuestros despliegues actuales sobre SQL Server 2000 y SQL Server 2005, y de paso, he dejado caer un poquito de información acerca del Resource Governor de SQL Server 2008 para que la curiosidad os pueda... quiero creer que lo suficiente como para que todos juntos os estéis descargando como locos la última CTP de SQL Server 2008 para probar el Resource Governor, los servidores se colapsen, salga en los telediarios y en el programa de Ana Rosa y en el Sé lo que Hicisteis.... pero bueno, me conformaré con que al menos os despierte el interés suficiente como para leer el siguiente artículo :)
Rock Tip:
Como hemos visto en las conclusiones, el Query Governor de SQL Server 2005 es muy práctico, pero aún así it's not enough. 'Not Enough' es también el titulo de una balada/medio tiempo de Van Halen... bastante ñoña, todo hay que decirlo, pero no por ñoño, menos temazo... ojo... que en los Rock Tips del tio Doval solo aparecen temazos!.
En serio, si no la conocéis os recomiendo que le peguéis un orejazo; aunque no seas muy 'rockeros', este tema merece la pena :)
Voy a comenzar algunas entradas en el blog relativas a depuración de aplicaciones Win32, por lo que parece interesante sentar antes unas bases teóricas sobre arquitectura de memoria y procesos de Windows NT. Esto, por otra parte, es muy útil a la hora de planificar el despliegue y configuración de aplicaciones de servidor, como puede ser SQL Server, por lo que no solo recomiendo la lectura a los interesados en el desarrollo de software.
Me plantee escribir la introducción desde cero, pero me acordé que hace cosa de un año escribí un artículo similar para la revista de los clubes .NET, de modo que saltándome a la torera los derechos de autor (que para algo el artículo es mío XD) y manteniéndome fiel a mi principio de Supreme Laziness, he decidido hacer un copy&paste sin remordimientos.
Espero que os guste!
Beware the Natives!
Un vistazo a la arquitectura de memoria y procesos de Windows NT®
Los que me conocéis sabéis que siento verdadera pasión por la plataforma .NET, pasión compartida, supongo, con todos los que estáis leyendo esta revista. De hecho, mis últimos temas de conversación favoritos para esas tardes de cervezas con los colegas son el recolector de basura y los objetos lázaro, el namespace System.Transactions y las plantas rodadoras.
Sin embargo hoy me siento rebelde y provocador, y me gustaría que me acompañarais en una breve excursión. Imaginaos a vosotros mismos en un bote en un río (ejem, no seguiré por ahí, que no estamos para pagar derechos de autor a los de Liverpool). Partiremos de nuestro hábitat natural, el mundo administrado, la tierra de Heljsberg y Wiltamuth, para adentrarnos en las oscuras y desconocidas tierras nativas (nunca mejor dicho) de Cutler, el núcleo de Windows NT® (2000, 20003, XP).
Muchas veces tengo la impresión de que en nuestro entorno, entre los apasionados de .NET, existe una especie de corriente autárquica; parece que no queremos saber nada de ése mundo exterior. Parecemos creer que nos podemos olvidar de sistema operativo subyacente (ya sea Windows, GNU/Linux, etc…) y permanecer en nuestra burbuja administrada. Sin embargo, veo constantemente ejemplos en la vida real de problemas de rendimiento o fallos de aplicaciones que, o bien se derivan de un desconocimiento de lo que hay por debajo de la plataforma, o bien podrían ser detectados y depurados mucho más fácilmente de conocer estas nociones básicas. Por eso creo importante dedicar unas pocas palabras a sentar al menos un conocimiento básico sobre la arquitectura de los sistemas Windows NT®, y hoy me centrare principalmente su gestión de memoria y de procesos.
NOTA: Antes de comenzar, me gustaría aclarar que todo lo que os voy a comentar de aquí en adelante es cierto en entornos Windows de 32 bits, ya que en los 64 bits la historia cambia bastante.
Programas, Procesos, hilos, y la madre que los parió…
Como ya sabemos, las aplicaciones que ejecutamos en nuestra máquina se convierten en procesos. Aunque a primera vista parece que un proceso es lo mismo que un programa, esto no es del todo cierto; podríamos decir que un programa es una secuencia estática de instrucciones, y el proceso es una estructura dinámica, creada por el loader sistema operativo a la hora de cargar un programa en memoria para comenzar su ejecución.
Estos procesos contienen, entre otras cosas, un espacio de memoria virtual privado (del que hablaremos más adelante), el código y datos del ejecutable inicial, así como uno o más hilos de ejecución.
Estos hilos son los que el sistema operativo planifica para su ejecución; podríamos considerarlos como la unidad mínima de planificación. Así que ya sabéis, a partir de ahora si alguien os vuelve a decir que el sistema operativo está ejecutando un proceso, podéis aclararle que lo que se ejecutan son los diferentes hilos del proceso, y de paso seguro que os ganáis un Certificado de Súper Gafotas.
Os adjunto la salida generada por la herramienta gratuita PsList de SysInternals (www.sysinternals.com) sobre uno de los procesos de mi máquina (un notepad.exe en éste caso). Como podéis ver, el proceso está compuesto por cinco hilos de ejecución diferentes, de los cuales podemos ver su identificador (TID), su prioridad, número de cambios de contexto, estado y estadísticas de tiempo de ejecución.
Como ya sabéis, Notepad.exe es una aplicación nativa; os propongo como ejercicio observar cómo se comporta un PsList sobre una aplicación administrada con múltiples hilos de ejecución usando System.Threading.
Espacio de Memoria Virtual
Ahí va una afirmación sorprendente: todos los procesos que se ejecutan en nuestras máquinas Windows disponen para 2 Gb de espacio de memoria virtual para ellos solitos, que se llama espacio de memoria virtual privado. Esto es así independientemente de que nuestra máquina tenga 128 Mb o 16 Gb de RAM, y es independiente de la cantidad de memoria que tengamos establecida en los archivos de paginación. Es más, si tenemos en nuestro sistema 10 procesos, entre todos tendremos un total de 20 Gb de espacio de memoria virtual privado.
Evidentemente toda magia tiene su explicación. No hay manera en la cual podamos tener persistidos en memoria datos que ocupen más que la cantidad total de memoria física disponible (RAM + Archivos de Paginación), por tanto el sistema operativo dispone de mecanismos que mapean las direcciones del espacio de memoria virtual con ubicaciones físicas de memoria, dándose la posibilidad de que la misma dirección de memoria dentro del espacio de direcciones virtual en dos procesos diferentes apunte a dos posiciones de memoria física diferentes.
Cada proceso, por tanto, tiene 2 Gb de memoria virtual donde ubicará sus recursos, que principalmente serán los siguientes:
- Imagen del código de la aplicación
- Imagen de todos los módulos (.dlls) empleados por la aplicación
- Uno o más heaps
- Una pila (stack) por cada hilo de ejecución (otra pila se crea automáticamente dentro del área de memoria destinada al Kernel, que comentaremos después)
- En caso de ser una aplicación .NET, un heap adicional llamado Managed Heap, que es donde se realizan todas las reservas de memoria y por tanto, el ámbito donde opera el recolector de basura.
La figura que se muestra a continuación representa el espacio de memoria privado de una hipotética aplicación administrada compuesta de un ejecutable (demo1.exe), con dos librerías enlazadas (lib1.dll y lib2.dll), dos hilos de ejecución y un heap administrado.
Este heap administrado se comporta de manera diferente si estamos en un entorno de servidor o en un Workstation. En el primer caso, el managed heap se compone de segmentos de 64 Mb, con un managed heap en cada procesador del sistema. En el caso de tratase de una estación de trabajo, se trata de segmentos de 16 Mb. ¿Cómo podemos determinar que versión del recolector de basura estamos empleando? Para ello solo debemos comprobar si estamos empleando la mscorwks.dll (Workstation) o la mscorsrv.dll (Servidor).
Como apreciamos en la figura, los diferentes hilos de un mismo proceso comparten el mismo espacio de memoria virtual privada, ya que sus diferentes pilas están en el mismo área de 2 Gb. De éste modo los diferentes hilos pueden acceder a la misma memoria. Esto no ocurre a la hora de comunicarse con otros procesos, para lo cual hay que emplear otros mecanismos de los que hablaremos en otra entrega de ésta serie.
A todo esto hay que sumarle un área inmutable (común para todos los procesos) de otros 2 Gb dentro del espacio de memoria virtual que almacena estructuras internas del sistema operativo, como las Page Table Entries, handles y demás. Es la zona de memoria para el Kernel, y se puede ver en la siguiente figura.
La figura sirve de sumario de la estructura de procesos y memoria en un entorno Windows en su configuración por defecto. Y digo en su configuración por defecto porque este balance “2 Gb para proceso de usuario / 2 Gb para Kernel” puede ser alterado mediante los siguientes parámetros suministrados al kernel en el fichero boot.ini:
Mediante este switch indicamos al sistema operativo que queremos asignar más memoria a las aplicaciones y menos al sistema operativo, de modo que el particionado se hará dejando 1 Gb para el Kernel y 3 Gb para el modo usuario.
Para que nuestras aplicaciones nativas puedan beneficiarse de éste incremento en su espacio de direcciones virtual, deberemos compilarlas con la opción /LargeAdressAware. En el caso de nuestras aplicaciones .NET, si bien el CLR respeta este flag, el compilador de C# no tiene opción para establecerlo, por lo que debemos editar la cabecera PE del ensamblado usando editbin:
“editbin /largeadressaware <myapp.exe>”
Usando el switch /3Gb en conjunción con /UserVA=xxxx podemos hacer una ajuste fino de la cantidad de memoria que queremos asignar al proceso de usuario, estableciendo un valor en megas entre 2000 y 3000. Esta opción solo está disponible en Windows 2003 Server y Windows XP.
¿Y por qué me cuentas todo eso?
Un ejemplo sencillo, pero que me encanta exponer cuando comento estos temas, es un servidor de FTP. Cuando propongo el diseño de un servidor de FTP, la gente se suele sentir atraída por la idea de usar un hilo de escucha de peticiones, más otros n hilos, uno por cada cliente que se conecta al servidor de FTP. El hilo principal haría un spawn de un nuevo hilo de ejecución con cada conexión entrante, y cuando ésta conexión se cerrara, se haría lo propio con el hilo principal.
Suena bastante lógico, pero si pensamos un poco en ello, y a la vista de lo que hemos comentado antes, podemos ver que ésta solución no escala precisamente bien. El tamaño por defecto para cada pila es de 512 Kb, y considerando que necesitamos dos pilas por cada hilo, la aplicación consumiría 1 Mb por cada conexión solo para sus pilas.
¿Quiere esto decir que no debemos emplear múltiples hilos a la hora de diseñar nuestras aplicaciones? Evidentemente no es así, pero hay que tener un cuidado especial a la hora de diseñar nuestras aplicaciones multi-hilo para asegurarnos que escalen correctamente. Para mejorar la escalabilidad se recomienda emplear un Pool de hilos, como el ThreadPool de System.Threading o el QueueUserWorkItem de la API de Windows 2000, y que por cuestiones de espacio y ámbito no trataré en éste artículo.
En la práctica también podemos ver ejemplos de problemas de fragmentación de memoria (muy típicos en despliegues de IIS), bloqueos completos de un servidor por agotamiento de PTEs, y un largo etcétera de problemas que un conocimiento superficial del sistema operativo subyacente nos puede ayudar a detectar y tratar rápidamente.
Despedida
Hasta aquí hemos llegado en esta breve introducción a la arquitectura de procesos y memoria de los sistemas Windows NT®. En un futuro artículo exploraremos ese espacio entre el código administrado y el código nativo, chapotearemos en ese rio entre P/Invokes y Marhallings (y de paso, haremos nuestros pinitos con WinDbg) para comunicar una aplicación .NET con una aplicación Win32 existente. Si os resulta atractivo, ¡ya sabéis donde podréis encontrarlo!
Referencias Bibliográficas:
- Microsoft Windows Internals, 4th Ed. [ M. Russinovich / D. Solomon], Microsoft Press, ISBN: 0-7356-1917-4
Rock Tip:
En el caso de hoy, y debido a mi infracción flagrante de los derechos de autor de mi artículo en student.NET, he decidido hacer alusión no ya a una canción, sino a un disco; se trata de 'Against the Law', de los heavys cristianos Stryper. Es una banda curiosa, famosos por la increible voz del guitarrista/vocalista Michael Sweet, por los peinados cardados que caracterizaban a estas bandas en los años 80 y, como no, por esas mallas y toreras amarillas y negras que se atrevían a lucir en sus conciertos!! Algún día conseguiré yo unas iguales! :)
Os voy a dejar una enlace a uno de sus temas más clásicos, 'Calling on You'. Yo adoro a este grupo, pero pinchad bajo vuestra propia responsabilidad :)
El otro día llego a mi buzón de entrada un correo de un compañero con el subject "DNN Panic!". El correo hacia referencia, entre otras cosas, a un mensaje de advertencia que aparecía en el errorlog de un SQL Server 2005 Express que estaba sirviendo a un portal DotNetNuke. El mensaje en cuestión aparecía cada dos o tres minutos, y tenía el siguiente aspecto:
SQL Server has encountered 1 occurrence(s) of cachestore flush for the 'Object Plans' cachestore (part of plan cache) due to some database maintenance or reconfigure operations.
Como debería ser bien sabido por todos los que tenemos que trabajar con o contra un SQL Server, cuando se realizan ciertas operaciones en la base de datos se puede forzar una limpieza de la caché de planes de ejecución (similar a cuando se ejecuta un DBCC FREEPROCACHE, etc...). En principio, salvo casos muy puntuales, no es buena idea, porque al eliminarse la caché, todos los procedimientos almacenados se han de reconstruir; por tanto, las siguientes veces que se invoquen, se perderá cierto tiempo en la recompilación y almacenado de ese plan. Lo mismo es aplicable a las otras cachés que existen en SQL Server.
El mensaje de error que hemos visto es un nuevo mensaje introducido en el SP2, para intentar localizar que posible plan de mantenimiento o tarea esta haciendo limpiando la caché de los planes (o de otro tipo de cachés).
Las operaciones que pueden producir esta limpieza de planes son las siguientes:
- Comprobación de consistencia mediante DBCC CHECKDB (Solo en SQL Server 2005 RTM o SP1.. en SP2 no se limpian por defecto)
- Si la base de datos tiene habilitado el autoclose, cada vez que se hace un shutdown de la base de datos se limpia la caché.
- Poner la base de datos online u offline
- Poner la base de datos en modo readonly o writeonly
- Cambiar el nombre de la base de datos, cambiar collation, etc...
- Hacer un restore de la base de datos
- Hacer un attach de la base de datos
- Evidentmente, hacer un DBCC FREEPROCCACHE o un DBCC FREESYSTEMCACHE
En el caso que nos ocupaba (nuestro DNN Panic!! ;) ) se daba el caso más común, es decir, que el autoclose de las bases de datos estaba habilitado. Este es el comportamiento por defecto en la versión Express de SQL Server, ya que facilita el despliegue y movimiento de los ficheros de datos. No obstante, cada vez que se hace un shutdown de la base de datos, se obliga a limpiar las cachés, lo que trae consigo dos consecuencias:
- Aparece el molesto mensaje en el errorlog cada vez que se hace un autoclose. Esto es inocuo, excepto porque 'ensucia' el log y luego se complica la tarea de buscar errores o avisos mas severos.
- Como se ha explicado, se refleja el que ha habido una limpieza de cachés, con el problema de rendimiento que puede suponer sobre la aplicación debido a la necesidad de creación de nuevo de los planes de ejecución y la población de otras cachés.
Conclusión:
Si tenéis alguna aplicación funcionando en SQL Server Express 2005, aplicad el SP2 y comprobad si os aparecen estos mensajes en el log. Si es así, comprobad si realmente es necesario que tengáis el autoclose establecido, y en caso de que no lo sea, estableced el autoclose a false para evitar perder las cachés.
Os ahorraréis mensajes que ensucian el log y de paso se deberían apreciar mejoras de rendimiento... no obstante, el rendimiento en un SQL Server es algo muy complejo, así que mejor lo dejamos para otra serie de artículos.
Rock Tip:
A este post le he adjudicado el temazo 'I Wonder', de los hardrockeros suizos Gotthard. Muchas veces nos quedamos mirando a un mensaje de error o advertencia y no podemos evitar pensar 'I Wonder... que narices querrá decirme esto...' :)
Dudo mucho que los chicos de Gotthard hayan dedicado su tema a todas esas preguntas que nos hacemos cuando vemos un mensaje extraño en el log de SQL Server; aún así, para mi su 'Lipservice' fue uno de los mejores discos de 2005 y por tanto, aprovecho para recomendarlo a los más duros, pero con corazoncito, de la comunidad geek ;)
Hace mucho que no estoy presente en geeks.ms, muy a mi pesar. Los que me conocéis personalmente sabéis que he estado pasando una época un poco dura, pero lo importante es que la normalidad ha vuelto y espero que aquellos que os atreváis a leerme tengais mucho material mío de aquí en adelante.
Durante estos meses muchas cosas han pasado; geeks.ms no ha dejado de crecer a un ritmo espectacular, Visual Studio 2008 ha llegado, ya nadie se maravilla con LINQ, tenemos a tiro de piedra SQL Server 2008 (que para mi y para muchos siempre será Katmai), Windows Server 2008... si bien durante estos meses he seguido con la antena puesta y trabajando con todas las betas que pude obtener, no puedo evitar que ahora, en el momento de retomar este blog, sienta un poco ese vertigo del que Jorge nos hablaba hacia poco.
Tengo ya algunos artículos en mente; estos días seré bastante oportunista, y aprovecharé la cercanía de Katmai para hablar escribir sobre el que sin duda es uno de mis productos favoritos de los chicos de Redmond. También tengo algunos artículos pendiente, como cierta sesión de WinDbg + winmine.exe en tierras Gallegas, o una serie de artículos sobre depuración post-mortem básica.
Por último, me gustaría disculparme si hay algún comentario, duda o cuestión realizada en alguno de los posts anteriores que no he podido responder durante estos meses anteriores. Si es así, no dudes en replanteármelas en los posts apropiados o en enviarme un mensaje privado!
Rock Tip:
Siguiendo la costumbre que tenía en el blog, el titulo de cada articulo irá precedido del nombre de una canción que de algún modo tiene relación con el post actual. En este caso, y en lo que constituye un penoso ejercicio de SSP (Shameful Self Promotion xD) el tema elegido es 'Leave the Storm Behind' de Angel Divine.
¿Que quienes son Angel Divine? Pues bueno, eran mi grupo de juventud y años mozos, de ahí lo del SSP :P Aquel tema se compuso en una época mala del grupo, y representaba las ganas y el empuje para dejar los malos rollos atrás y tirar adelante con lo que nos gustaba. Es exactamente eso lo que siento estos días, y por ello no podía titular a este post de otro modo.