Saturday 3 September 2011

Optimizando consultas WMI

Hola a todos,

como he estado de vacaciones la semana pasada no posteé nada, intentaré compensar con dos entradas dedicadas a WMI. Cada vez que cuento las excelencias de WMI a otros desarrolladores, los dos principales inconvenientes que me comentan que tienen al intentar utilizarlo son su lentitud y lo fácil que parece algunas veces que deje de funcionar. Estoy casi completamente de acuerdo con ellos pero no del todo :) En este post me centraré en la primera parte, la lentitud. Os daré una explicación a "grosso modo" de por qué puede llegar a ser extremadamente lento e intentaré ofreceros algunos consejos para mejorar el rendimiento. En el siguiente hablaremos de como enfrentarnos a algunos errores.

 

Infraestructura

Para entender por qué las consultas pueden llegar a ser tan lentas es necesario conocer un poco la infraestructura. La arquitectura de WMI básicamente se compone del servicio WMI y el repositorio WMI. Una aplicación o script pide información al servicio WMI a través de las API en COM, el servicio se encarga de recuperarla o bien del repositorio o bien de pedir la información a los proveedores WMI también a través de COM (un proveedor no es más que un objeto COM y un archivo MOF). Por lo tanto la velocidad de WMI dependerá directamente del proveedor WMI que estemos consultando. Más aún, como solo la información estática de los objetos se almacena en el repositorio, el resto de información debe recuperarse dinámicamente del proveedor al momento de pedirla el cliente, lo que ocasiona un mayor retardo. Así que ya sabemos que aunque utilicemos WQL, un lenguaje parecido a SQL para recuperar información, no estamos leyendo de una base de datos si no que alguna de la información se puede estar generando al vuelo.

 

Cachear datos

Algunas de las optimizaciones que podemos aplicar son las mismas que aplicaríamos si tuviéramos que acceder a cualquier otro recurso lento. Por ejemplo cachear los resultados de la consulta si sabemos que se tendrá que repetir y no nos importa que no se reflejen las últimas actualizaciones. Esta técnica es especialmente útil si lo que queremos hacer son consultas y no actualizaciones o ejecución de métodos. Además, ganaremos espacio en memoria si extraemos la información del objeto ManagementObject y la almacenamos en las propiedades de un objeto POCO que sea el que finalmente guardaremos en la cache.

 

Consultas sobre la red

Uno de los usos más populares de WMI es acceder a la información de ordenadores remotos así que sí ya sabemos que las consultas WQL son generalmente lentas (ahora sabemos que porque normalmente deben crearse los datos al vuelo) este rendimiento puede empeorar todavía más si tenemos que añadir la latencia de la red. En el caso de recuperar datos de ordenadores remotos o referentes a la red es muy difícil optimizar las consultas. En estos caso hemos de intentar aglutinar todos los datos a recuperar de un ordenador remoto en una única consulta, de manera que se minimicen los viajes a través de la red. Por otra parte, también es aconsejable si queremos trabajar con un ordenador remoto intentar hacer ping primero y analizar el tiempo de respuesta de manera que sepamos cual es la causa de la lentitud o podamos aplazar el proceso para cuando la red esté menos congestionada. En cualquier caso, siempre es muy importante reducir al mínimo indispensable los datos a recuperar.

 

Campos indexados y enumeraciones

Ahora vamos a entrar más en detalle en como implementar una consulta en .Net para intentar optimizarla lo máximo posible. Para ello vamos a tener en cuenta lo que hemos comentado previamente de que los datos pueden estar siendo calculados en el momento de realizar la consulta, con lo que tendremos que evitar sentencias del tipo "SELECT *" y minimizar los datos recuperados, puesto que cada campo que añadamos puede estar generándose en ese instante. Una consulta tipo "SELECT *" es especialmente peligrosa en WMI ya que las clases suelen tener una gran cantidad de campos con lo que por ejemplo una consulta a Win32_Process con un "SELECT *" nos puede retornar una gran cantidad de información del procesador cuando solo queríamos , por ejemplo, la arquitectura.

 

Por otra parte, también quería mencionar que hay algunos proveedores que proporcionan campos optimizados para poder filtrar por ellos, mejorando el rendimiento, con lo cual, dentro de lo posible, debemos siempre consultar la documentación del proveedor para filtrar por ellos en la clausula WHERE. En cualquier caso, un buen filtro siempre mejorará el rendimiento de nuestra consulta pero en el caso de los campos no optimizados es el motor WMI quien se encarga de realizar un filtrado "a posteriori" una vez calculados los datos por el proveedor. Un ejemplo de propiedades optimizadas son Drive y Path de la clase CIM_DataFile.

 

Otra forma de mejorar el rendimiento es reutilizar las conexiones WMI que establezcamos, sobre todo si es contra ordenadores remotos. No obstante, es muy importante no olvidarse de hacer un Dispose de los objetos innecesarios para ahorrar memoria.

 

Por último quería hacer hincapié en que se configuren las opciones de enumeración de una consulta, ya que se pueden conseguir grandes mejoras de rendimiento. Para configurar estas opciones se debe utilizar la clase EnumerationOptions que se asigna a la propiedad Options de la clase ManagementObjectSearcher o bien se le pasa en el constructor. Por defecto, la consulta permite navegar hacia delante y atrás en los datos recuperados pero es muy importante que si lo que deseamos es recuperar datos lo más rápidamente posible, se utilice un modo de enumeración solo hacia delante, solo lectura y semi-síncrona. Para ello estableceremos las propiedades ReturnImmediately a true y Rewindable a false. No obstante, estas configuraciones no suelen reflejarse en una mejora importante a menos que se usen para enumerar una clase con un número de entradas considerable. Además es importante recordar que es incompatible utilizar Rewindable a false y utilizar la propiedad Count. Con el modo semi-síncrono lo que logramos es que la llamada retorne inmediatamente y que los objetos sean recuperados en segundo plano y retornados bajo demanda, una vez se hayan creado.

 

Y ahora por fin algo de código:

  1. EnumerationOptions optionsQuery = new EnumerationOptions();
  2. optionsQuery.DirectRead = true;
  3. optionsQuery.EnumerateDeep = false;
  4. optionsQuery.ReturnImmediately = true;
  5. optionsQuery.Rewindable = false;            
  6. StringBuilder units = new StringBuilder(200); // DriveType 4 is a network drive
  7. WqlObjectQuery query = new WqlObjectQuery("SELECT Name, ProviderName FROM Win32_LogicalDisk WHERE DriveType=4");
  8. using (ManagementObjectSearcher diskSearch = new ManagementObjectSearcher(null, query, optionsQuery))
  9. {
  10.     foreach (ManagementObject disk in diskSearch.Get())
  11.     {
  12.         units.Append(disk["Name"].ToString());
  13.         units.Append("\t");
  14.         units.Append(disk["ProviderName"].ToString());
  15.         units.Append("\n");
  16.     }
  17. }

Cambio de enfoque

A veces el error está en el enfoque, ¿quizás es mejor utilizar eventos que consultas? WMI también nos permite instalar “eventwatchers”. Por ejemplo, el siguiente fragmento informa de desconexiones de la red.

 

  1. static void Test()
  2. {
  3.     ManagementEventWatcher w = null;
  4.     ManagementOperationObserver observer = new ManagementOperationObserver();
  5.     // Bind to local machine
  6.     ManagementScope scope = new ManagementScope("root\\wmi");
  7.     scope.Options.EnablePrivileges = true; //sets required privilege
  8.     try
  9.     {
  10.         WqlEventQuery q = new WqlEventQuery();
  11.         q.EventClassName = "MSNdis_StatusMediaDisConnect";
  12.         w = new ManagementEventWatcher(scope, q);
  13.         w.EventArrived += new EventArrivedEventHandler(MediaEventArrived);
  14.         w.Start();
  15.         Console.ReadLine(); // block main thread for test purposes
  16.     }
  17.     catch (Exception e)
  18.     {
  19.         Console.WriteLine(e.Message);
  20.     }
  21.  
  22.     finally
  23.     {
  24.         w.Stop();
  25.     }
  26. }
  27.  
  28. static void MediaEventArrived(object sender, EventArrivedEventArgs e)
  29. {
  30.     Console.WriteLine("Event arrived");
  31.     Console.WriteLine(e.NewEvent.Properties["InstanceName"].Value);
  32.     //Get the Event object and display it
  33.     Console.WriteLine(Convert.ToBoolean(e.NewEvent.Properties["Active"].Value) ? "Active" : "Inactive");
  34. }

 

Conclusión

Así pues en conclusión los puntos que tenemos que revisar principalmente son:

  • Evitar "SELECT *"

  • Usar campos indexados en la WHERE

  • Configurar EnumerationSettings

  • Tener especial cuidado con las consultas sobre la red

  • Cachear los resultados

  • Analizar si el enfoque utilizado para recuperar información es el correcto

Más info en:

Secrets of Windows Management Instrumentation - Troubleshooting and Tips

Optimizing WMI query performances - avoid the nasty ‘select *’

How to make forward-only, read-only WMI queries in C#?

Calling a Method - Semisynchronous

1 comment:

Anonymous said...

Se pueden crear campos calculados en las consultas sql a WMI.. por ejemplo select CAST(FreeSpace AS bigint)/1024 from Win32_LogicalDisk where deviceid='d:'