Hola a tots,
com he estat de vacances la setmana passada no vaig postejar res, intentaré compensar amb dues entrades dedicades a WMI. Cada vegada que explico les excel·lències de WMI a altres desenvolupadors, els dos principals inconvenients que em comenten que tenen en intentar utilitzar-lo són la seva lentitud i com sembla de fàcil algunes vegades que deixi de funcionar. Estic gairebé completament d'acord amb ells però no del tot:) En aquest post em centraré en la primera part, la lentitud. Us donaré una explicació a "grosso modo" de per què pot arribar a ser extremadament lent i intentaré oferir-vos alguns consells per millorar el rendiment. En el següent parlarem de com enfrontar-nos a alguns errors.
Infraestructura
Per entendre per què les consultes poden arribar a ser tan lentes és necessari conèixer una mica la infraestructura. L'arquitectura de WMI bàsicament es compon del servei WMI i el repositori WMI. Una aplicació o script demana informació al servei WMI a través de les API a COM, el servei s'encarrega de recuperar o bé del repositori o bé de demanar la informació als proveïdors WMI també a través de COM (un proveïdor no és més que un objecte COM i un arxiu MOF). Per tant la velocitat de WMI dependrà directament del proveïdor WMI que estiguem consultant. Encara més, com que només la informació estàtica dels objectes s'emmagatzema en el repositori, la resta d'informació s'ha de recuperar dinàmicament del proveïdor al moment de demanar el client, i això ocasiona un major retard. Així que ja sabem que encara utilitzem WQL, un llenguatge semblant a SQL per recuperar informació, no estem llegint d'una base de dades sinó que alguna de la informació es pot estar generant al vol.
Cachejar dades
Algunes de les optimitzacions que podem aplicar són les mateixes que aplicaríem si haguéssim d’accedir a qualsevol altre recurs lent. Per exemple, cachejar els resultats de la consulta si sabem que s'haurà de repetir i no ens importa que no es reflecteixin les últimes actualitzacions. Aquesta tècnica és especialment útil si el que volem fer són consultes i no actualitzacions o execucions de mètodes. A més, guanyarem espai en memòria si extraiem la informació de l'objecte ManagementObject i l'emmagatzemem en les propietats d'un objecte POCO que sigui el que finalment guardarem a la cache.
Consultes sobre la xarxa
Un dels usos més populars de WMI és accedir a la informació d'ordinadors remots així que si ja sabem que les consultes WQL són generalment lentes (ara sabem que perquè normalment s'han de crear les dades al vol) aquest rendiment pot empitjorar encara més si hem d’afegir la latència de la xarxa. En el cas de recuperar dades d'ordinadors remots o referents a la xarxa és molt difícil optimitzar les consultes. En aquests cas hem d'intentar aglutinar totes les dades a recuperar d'un ordinador remot en una única consulta, de manera que es minimitzin els viatges a través de la xarxa. D'altra banda, també és aconsellable, si volem treballar amb un ordinador remot, intentar fer ping primer i analitzar el temps de resposta de manera que sapiguem quina és la causa de la lentitud o puguem ajornar el procés per quan la xarxa estigui menys congestionada. En qualsevol cas, sempre és molt important reduir al mínim indispensable les dades a recuperar.
Camps indexats i enumeracions
Ara anem a entrar més en detall en com implementar una consulta en .Net per intentar optimitzar-la el màxim possible. Per a això tindrem en compte el que hem comentat prèviament que les dades poden estar sent calculades en el moment de realitzar la consulta, de manera que haurem d'evitar sentències del tipus "SELECT *" i minimitzar les dades recuperades, ja que cada camp que afegim pot estar generant-se en aquest instant. Una consulta tipus "SELECT *" és especialment perillosa en WMI ja que les classes solen tenir una gran quantitat de camps amb el que per exemple una consulta a Win32_Process amb un "SELECT *" ens pot retornar una gran quantitat d'informació del processador quan només volíem, per exemple, l'arquitectura.
D'altra banda, també volia dir que hi ha alguns proveïdors que proporcionen camps optimitzats per poder filtrar per ells, millorant el rendiment, amb la qual cosa, en la mesura del possible, hem de consultar sempre la documentació del proveïdor per filtrar per ells en la clàusula WHERE. En qualsevol cas, un bon filtre sempre millorarà el rendiment de la nostra consulta però en el cas dels camps no optimitzats és el motor WMI qui s'encarrega de realitzar un filtrat "a posteriori" un cop calculades les dades pel proveïdor. Un exemple de propietats optimitzades són Drive i Path de la classe CIM_DataFile.
Una altra forma de millorar el rendiment és reutilitzar les connexions WMI que establim, sobretot si és contra ordinadors remots. No obstant això, és molt important no oblidar-se de fer un Dispose dels objectes innecessaris per estalviar memòria.
Finalment volia comentar que és molt important que es configurin les opcions d'enumeració d'una consulta, ja que es poden aconseguir grans millores de rendiment. Per a configurar aquestes opcions s'ha d'utilitzar la classe EnumerationOptions que s'assigna a la propietat Options de la classe ManagementObjectSearcher o bé se li passa al constructor. Per defecte, la consulta permet navegar cap endavant i cap enrere en les dades recuperades però és molt important que si el que volem és recuperar dades el més ràpidament possible, s'utilitzi una mode d'enumeració només cap endavant, només lectura i semi-sincron. Per això establirem les propietats ReturnImmediately a true i Rewind a false. No obstant això, aquestes configuracions no solen reflectir-se en una millora important si no es fan servir per enumerar una classe amb un nombre d'entrades considerable. A més és important recordar que és incompatible utilitzar Rewind a false i utilitzar la propietat Count. Amb el mode semi-sincron el que aconseguim és que la crida retorni immediatament i que els objectes siguin recuperats en segon plà i retornats sota demanda, un cop s'hagin creat.
I ara per fi una mica de codi:
- EnumerationOptions optionsQuery = new EnumerationOptions();
- optionsQuery.DirectRead = true;
- optionsQuery.EnumerateDeep = false;
- optionsQuery.ReturnImmediately = true;
- optionsQuery.Rewindable = false;
- StringBuilder units = new StringBuilder(200); // DriveType 4 is a network drive
- WqlObjectQuery query = new WqlObjectQuery("SELECT Name, ProviderName FROM Win32_LogicalDisk WHERE DriveType=4");
- using (ManagementObjectSearcher diskSearch = new ManagementObjectSearcher(null, query, optionsQuery))
- {
- foreach (ManagementObject disk in diskSearch.Get())
- {
- units.Append(disk["Name"].ToString());
- units.Append("\t");
- units.Append(disk["ProviderName"].ToString());
- units.Append("\n");
- }
- }
Canvi d'enfocament
De vegades l'error està en l'enfocament, potser és millor utilitzar events que consultes? WMI també ens permet instal·lar "eventwatchers". Per exemple, el següent fragment informa de desconnexions de la xarxa.
- static void Test()
- {
- ManagementEventWatcher w = null;
- ManagementOperationObserver observer = new ManagementOperationObserver();
- // Bind to local machine
- ManagementScope scope = new ManagementScope("root\\wmi");
- scope.Options.EnablePrivileges = true; //sets required privilege
- try
- {
- WqlEventQuery q = new WqlEventQuery();
- q.EventClassName = "MSNdis_StatusMediaDisConnect";
- w = new ManagementEventWatcher(scope, q);
- w.EventArrived += new EventArrivedEventHandler(MediaEventArrived);
- w.Start();
- Console.ReadLine(); // block main thread for test purposes
- }
- catch (Exception e)
- {
- Console.WriteLine(e.Message);
- }
-
- finally
- {
- w.Stop();
- }
- }
-
- static void MediaEventArrived(object sender, EventArrivedEventArgs e)
- {
- Console.WriteLine("Event arrived");
- Console.WriteLine(e.NewEvent.Properties["InstanceName"].Value);
- //Get the Event object and display it
- Console.WriteLine(Convert.ToBoolean(e.NewEvent.Properties["Active"].Value) ? "Active" : "Inactive");
- }
Conclusió
Així doncs en conclusió els punts que hem de revisar principalment són:
- Evitar "SELECT *"
- Utilitza camps indexats en la WHERE
- Configurar EnumerationSettings
- Tenir especial cura amb les consultes sobre la xarxa
- Cachejar els resultats
- Analitzar si l'enfocament utilitzat per recuperar informació és el correcte
Més info a:
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
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:
- EnumerationOptions optionsQuery = new EnumerationOptions();
- optionsQuery.DirectRead = true;
- optionsQuery.EnumerateDeep = false;
- optionsQuery.ReturnImmediately = true;
- optionsQuery.Rewindable = false;
- StringBuilder units = new StringBuilder(200); // DriveType 4 is a network drive
- WqlObjectQuery query = new WqlObjectQuery("SELECT Name, ProviderName FROM Win32_LogicalDisk WHERE DriveType=4");
- using (ManagementObjectSearcher diskSearch = new ManagementObjectSearcher(null, query, optionsQuery))
- {
- foreach (ManagementObject disk in diskSearch.Get())
- {
- units.Append(disk["Name"].ToString());
- units.Append("\t");
- units.Append(disk["ProviderName"].ToString());
- units.Append("\n");
- }
- }
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.
- static void Test()
- {
- ManagementEventWatcher w = null;
- ManagementOperationObserver observer = new ManagementOperationObserver();
- // Bind to local machine
- ManagementScope scope = new ManagementScope("root\\wmi");
- scope.Options.EnablePrivileges = true; //sets required privilege
- try
- {
- WqlEventQuery q = new WqlEventQuery();
- q.EventClassName = "MSNdis_StatusMediaDisConnect";
- w = new ManagementEventWatcher(scope, q);
- w.EventArrived += new EventArrivedEventHandler(MediaEventArrived);
- w.Start();
- Console.ReadLine(); // block main thread for test purposes
- }
- catch (Exception e)
- {
- Console.WriteLine(e.Message);
- }
-
- finally
- {
- w.Stop();
- }
- }
-
- static void MediaEventArrived(object sender, EventArrivedEventArgs e)
- {
- Console.WriteLine("Event arrived");
- Console.WriteLine(e.NewEvent.Properties["InstanceName"].Value);
- //Get the Event object and display it
- Console.WriteLine(Convert.ToBoolean(e.NewEvent.Properties["Active"].Value) ? "Active" : "Inactive");
- }
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