Surviving the Night

El blog de Pablo Doval sobre .NET, SQL, WinDbg...

The Hunter: Cazando bloqueos e interbloqueos

Después de una semana bastante dura, he podido sacar un rato para escribiros sobre un tema realmente interesante y con mucha relevancia desde el punto de vista del rendimiento y estabilidad de nuestras aplicaciones: la correcta utilización de las diversas primitivas de sincronización en nuestro código.

Bueno… en realidad os estoy engañando un poco ;) Hoy me voy a centrar en el viejo y venerable lock, y en algunas peculiaridades suyas. Quizá algún día escriba mas entradas sobre otras primitivas de sincronización, o quizá no… por ello, y si os interesan estos aspectos de la programación concurrente, permitidme que os recomiende el excelente libro de Joe Duffy.

Me vais a permitir que emplee como excusa para introducir el tema un escenario que me encontré esta semana en un cliente nuestro….

El Problema

Este cliente solicito nuestros servicios por problemas masivos de estabilidad en sus aplicación, concretamente en los servicios WCF que atacan los clientes. Durante la investigación comprobamos que teníamos muchos frentes abiertos: pérdidas de memoria, problemas con el ODP de Oracle, etc. Pero uno de los principales problemas se manifestaban a través de bloqueos masivos, que provocaban que la aplicación dejara de dar servicio durante unos minutos, a pesar de hallarse en una situación de bajo uso de CPU.

Procedimos a capturar un volcado de memoria del proceso w3wp asociado al servicio que mostraba el problema de rendimiento, a través de adplus.vbs, y se pudo comprobar rápidamente que había hilos bloqueados.

Empecé pidiéndole a tándem WinDbg + SOS.dll que me mostraran un volcado de todos los hilos administrados (.NET) de la aplicación en el momento de la captura del volcado, con el comando ~*e !ClrStack, y pude comprobar que había múltiples hilos con el mismo call stack:

OS Thread Id: 0x82c (81)
Child-SP         RetAddr          Call Site
0000000005a0edf0 00000642801b10c3 X.Sesion.SesionServidor.setValorConexion(System.String) 0000000005a0ee40 00000642801b0fcd X.CredencialServidor.asignarIdSesion(System.String)
0000000005a0ee70 00000642801b0de1 X.SessionInfoInterceptor.OnReceive(System.ServiceModel.Channels.Message ByRef)
0000000005a0eeb0 00000642801b0d03 X.InspectingChannelBase`1[[System.__Canon, mscorlib]].OnReceive(System.ServiceModel.Channels.Message ByRef)
[ – Corto aqui para ahorrar espacio -- ]
0000000005a0f480 000006427f67d4a2 System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32, UInt32, System.Threading.NativeOverlapped*)

Mucha casualidad sería que en el momento de capturar el volcado de memoria realmente 11 hilos estuvieran ejecutando el mismo método - setValorConexion - por lo que la intuición que teníamos acerca de un problema de bloqueos iba materializándose.

Hicimos una rápida comprobación mediante el comando !syncblk de las sos.dll, que nos revelo la presencia de un bloqueo:

0:000> !syncblk
Index         SyncBlock MonitorHeld Recursion Owning Thread Info          SyncBlock Owner
   40 00000000001a3a60           13         1 000000000d99a370   748  65   0000000160058648 System.RuntimeType
-----------------------------
Total           901
CCW             2
RCW             54
ComClassFactory 0
Free            787

La línea marcada en rojo nos indica que tenemos un bloqueo sobre un objeto de tipo System.RuntimeType y que el padre del bloqueo es el hilo 65. Comprobamos a continuación los detalles del objeto de sincronización del bloqueo:

0:000> !do 0000000160058648
Name: System.RuntimeType
MethodTable: 00000642788c5790
EEClass: 00000642788c56d0
Size: 40(0x28) bytes
Type MethodTable: 000006428023ef40
Type Name: X.Comun.Seguridad.Sesion.SesionServidor

Vemos que, aparentemente, la intención del desarrollador de la aplicación era escribir un bloqueo sobre una parte del código que afectara solo a la sesión actual, pero ¿no os llama algo la atención?

Fijaros en que el bloqueo no se obtuvo sobre un objeto de tipo X.Comun.Seguridad.Sesion.SesionServidor, sino sobre un System.RuntimeType. Si pensamos a nivel de código C#, lo más posible es que le usuario haya introducido el siguiente código para obtener el bloqueo:

lock (typeof(SesionServidor))
{
   // Hacer algo...
}

O bien algo como lo siguiente:

lock (this.GetType())
{
   // Hacer algo...
}

Esto explica el porque vemos un bloque sobre un System.RuntimeType, y más importante aún… explica la razón de los bloqueos. Pero antes de desvelar el misterio, es hora de….

Un poquito de teoría

DISCLAIMER: No puedo evitarlo, me gustan los internals y el saber el porque de las cosas ;) Aun así, entiendo que no todo el mundo es igual, por lo que si quieres saltarte esta parte lo entenderé perfectamente. Lo que si te recomendaría es que te leyeras las conclusiones, ya que ahí resumo las recomendaciones mas importantes que se derivan de lo que voy a explicar aquí.

El establecer estos bloqueos sobre un objeto de tipo System.RuntimeType o System.Type debe ser considerado, no solo como una mala practica, sino directamente como un bug. Hay dos motivos principales por los que esto se puede considerar un bug de consideración:

  1. Al emplear un objeto publico como objeto de sincronización, la aplicación se está exponiendo a que cualquier usuario o desarrollador emplee ese mismo tipo para adquirir un bloqueo, provocando en ese momento el interbloqueo.
  2. Los tipos System.RuntimeType y System.Type son tipos Marshall-By-Bleed, lo que quiere decir que se comparten incluso a través de la frontera de los AppDomains. Si cualquier otro hilo, en cualquier otro proceso del sistema (ya sea desarrollado por nosotros o por terceros) adquiere un bloqueo sobre cualquier tipo a través de un typeof() o de un GetType(), se podrá producir esta situación de interbloqueo.

El segundo punto es particularmente importante, ya que implica que las posibilidades de que ocurra un interbloqueo son mucho más elevadas, al tener que tomar en consideración los bloqueos que todas las aplicaciones administradas que se estén ejecutando en la máquina vayan a tomar, hayan sido desarrolladas por nosotros o no. Además, su localización y depuración va a resultar extremadamente costosa, debido a que algunos participantes del bloqueo podrían no aparecer en los volcados de memoria, al estar en otro proceso diferente.

Volviendo a nuestra Historia

Cuando comprobamos que había un bloqueo sobre un System.RuntimeType, decidimos hacer una revisión sobre el código de nuestro cliente, y pudimos comprobar como había cientos de ocurrencias, literalmente, de bloqueos sobre typeof() y GetType().

Evidentemente esto incrementa las posibilidades de interbloqueo de una manera salvaje, y sobre todo, no controlada, ya que nos pone a merced de otros desarrollos que se ejecuten en el mismo servidor.

¿Cual es la solución? Pues generalmente es revertir al viejo y clásico mecanismo de bloquear sobre un object, que sea un atributo de clase privado y estático para hacerlo robusto a hilos. Es el viejo sistema, si… y el que deberemos emplear en el 99,9% de los casos. :)

Conclusiones

Me gustaría terminar con una serie de recomendaciones:

  • Nunca bloquees por un objeto que no sea estático:

Eso solo serviría para proteger los miembros de esa instancia concreta, y rara vez es esto lo que deseamos con un lock.

  • Nunca hagas un lock sobre un objeto publico:

Como se explico previamente, el lock sobre el objeto publico significa que otra parte de la aplicación puede hacer un lock sobre el mismo objeto y ocasionar un interbloqueo.

  • Sobre todo, la mas importante de todas… Nunca hagas un lock sobre un System.RuntimeType o System.Type:

Ya sabéis, si leísteis la parte teórica, que estos tipos son Marshal-by-bleed, lo que significa que se comparten entre dominios de aplicación diferentes. El riesgo de interbloqueo es enorme, y no puedo pensar en un solo escenario en que tenga sentido hacer ese bloqueo.

NOTA: Evidentemente, todos mis nunca son matizables xDD

NOTA 2: Os dejo, que me voy a ver a Medina Azahara :P

Rock Tip:

El Rock Tip de esta entrada le corresponde a The Hunter, un grandísimo tema de Dokken, grupo que, si la memoria no me traiciona, ¡nunca había aparecido en mi blog! Para intentar resarcirme de esta afrenta a los dioses del metal he decidido incluirlos en esta entrada por los siguientes motivos:

  • The Hunter me pareció una metáfora bastante acertada al trabajo realizado para localizar los problemas de interbloqueos, y para el trabajo del DOT en general :)
  • El disco (perdón, ¡discazo!) en el que este tema vio la luz se llama Under Lock and Key…. under lock…. ehh.. bueno, dejadlo, me hizo gracia en su momento xD

Lo dicho, grupo muy recomendable; tremenda pareja hacían el vocalista Don Dokken y el guitarrista George Lynch. No os perdáis temas como ‘In my dreams’, ‘Alone Again’ o ‘Dream Warriors’, que sin duda están entre lo mejorcito que nos trajo el hard rock/AOR ochentero americano.

Posted: 7/2/2009 17:53 por Pablo Alvarez | con 15 comment(s) |
Archivado en: ,,

Comentarios

Iván González ha opinado:

Post genial!

# February 7, 2009 7:48 PM

Pablo Alvarez ha opinado:

¡Gracias tio! :) La verdad es que es una pena que, por falta de tiempo, he estado dejando de escribir algunas aventurillas interesantes, pero bueno, sabes bien que mi proposito para este año es darle mas caña al blog. ¡Comentarios de animo como el tuyo ayudan a conseguir este objetivo! :)

Un abrazote!

# February 8, 2009 1:53 AM

Octavio Hernández ha opinado:

Pablito,

Como siempre, ¡excelente!

Lo curioso sobre este tema es que "al principio de los tiempos" el propio Microsoft recomendaba utilizar lock(typeof(X)) para los bloqueos. Creo que el gran Jeffrey Richter fue el primero que llamó la atención sobre los problemas de esa técnica aquí:

msdn.microsoft.com/.../cc188793.aspx

Yo todavía a cada rato me encuentro por ahí a alguien que piensa que esa es la manera recomendadada... Y claro, seguirá habiendo un montón de código heredado que lo utilice.

Abrazo - Octavio

# February 8, 2009 2:09 PM

JuanK ha opinado:

Muy bien, de verdad todo programador deberia tener buenos conocimeintos en los aspectos internos de trabajos del framework y del sistema operativo porque solemos ahcer muchas burradas.

Gracias por el aporte.

# February 8, 2009 6:28 PM

Pablo Alvarez ha opinado:

Gracias a los tres por los comentarios!!

@Octavio: el articulo que me comentas no lo conocia, y me parece genial!! Lo que, teniendo en cuenta el autor, no es nada raro por otra parte :) Eso si, hecho en falta una mencion a los problemas de los Marshal-by-bleed, que no los toca.

Me quedo con una frase de su articulo:

"I'm going to explain the motivation for why SyncBlocks and Monitors were designed the way they are and how they work. Then, at the end of this column, I'm going to explain why this design is HORRIBLE and show you how to use this mechanism in a good, safe fashion."

Tremendo este señor :)

Muchas gracias por el aporte, Octavio!!!

Un abrazo!!

# February 8, 2009 8:51 PM

Luis Ruiz Pavón ha opinado:

Te doy mi mas sincera enhorabuena por tus post!!! y lo que es más por compartir este tipo de información que a más de uno le evitará romperse el culo (Como dice Heinze :))

Eres un crack!!!

Salu2

# February 10, 2009 9:22 AM

Pablo Alvarez ha opinado:

@Luis: Gracias!!! así da gusto escribirlos, la verdad es que, como ya dije muchas veces, debido a la naturaleza 'friki' de los posts de WinDbg, depuracion, internals, etc.. pensaba que tendrian menos aceptacion. Me alegro de haberme equivocado! ;)

Un saludo!

# February 10, 2009 9:36 AM

Pablo Alvarez ha opinado:

@espinete: gracias!!! Respecto a seguir con los articulos, todo es cuestion de tiempo :) el día a día me mata, y las madrugadas no dan para tanto!

Pero si todo va bien, esta noche publicare un post sobre algo que me estoy encontrando justo ahora en un cliente... creo que ser´ça interesante!

Un saludo!

# February 10, 2009 6:10 PM

Pablo Alvarez ha opinado:

@Trovitas:

"Tendré que aprender a utilizar windbg o existe alguna herramienta más visual que pueda ayudarme ?"

Yo te propongo que lo enfoques desde otro punto de vista... en lugar del 'tendré', piensa en que esta es una ocasion perfecta para empezar a aprender a usar WinDbg; lo que tienes delante de ti es una oportunidad.

No es tan dificil, tienes mi correo para las dudas que te puedan surgir.

Lo que yo haria sería adjuntar en vivo el WinDbg en el mismo momento que se quede colgada la aplicacion, si necesidad de capturar un volcado, ya que lo puedes reproducir en tu propia maquina.

Puedes ejecutar un !analyze -v, que te hace un pequeño informe donde, en muchas ocasiones, veras rapidamente la causa del problema.

Para cualquier cosa no dudes en escribirme. Animo y suerte!

# February 11, 2009 4:47 PM

Octavio Hernández ha opinado:

Pablito:

Sigue habiendo páginas en MSDN que recomiendan bloquear sobre typeof(X):

msdn.microsoft.com/.../aa664735(VS.71).aspx

Slds - Octavio

# February 12, 2009 9:15 AM
Deja tu comentario

(requerido) 

(requerido) 

(opcional)

(requerido)