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:
- 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.
- 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.