Hechizo de Threadpool de los demonios Dotnet en Linux / Sudo Null IT News

Todo el mundo ha oído que a veces dotnet en Linux consume más recursos que en Windows. A veces esta diferencia es casi invisible. Pero también sucede que la misma aplicación consume entre 2 y 3 veces más CPU en Linux que en Windows.

Una digresión artística, una broma sobre un tema de actualidad. Para descubrir lo más interesante, leer el texto debajo del corte es completamente opcional.

En el extraño entorno del Immaterium, casi todo se comporta de manera inusual. Muchas leyes de la naturaleza no funcionan. Más precisamente, no funcionan como todo el mundo está acostumbrado. Incluso el tiempo se comporta de manera algo diferente.

Magos Technicus G estaba inmerso en aprender cómo hacer que algoritmos y mecanismos familiares funcionaran en el entorno agresivo de Warp, al menos aceptable para su comprensión habitual.

Sólo los High Tech-Sacerdotes tienen acceso a las antiguas tabletas perf que contienen hechizos misteriosos y prohibidos. Sólo aquellos endurecidos por siglos de esfuerzo intelectual dirigido a la gloria del Omnissiah están lo suficientemente preparados para aprovecharlos. Después de todo, para comprender los hechizos necesitas sumergirte en las profundidades de tu mente, entrar en contacto con la Disformidad y no dejar que te devore.

Después de una semana de profunda meditación, Magos G tuvo una idea: es necesario cambiar el límite de rotación del semáforo injusto.

Hay muchas razones para la diferencia en el consumo de CPU de las aplicaciones dotnet en diferentes sistemas operativos y todas son variadas. En resumen, la implementación de una gran cantidad de primitivas o incluso de grandes piezas de lógica difiere. Y en cada caso concreto puede ocurrir algo diferente.

Algunas de estas diferencias (o incluso “problemas”) se eliminan con el tiempo y, si es posible, las mejora el propio dotnet. Para algunas “características”, aparecen funciones experimentales en ciertas versiones de dotnet que usted mismo puede administrar. A veces, estas funciones se trasladan a nuevas versiones y están habilitadas de forma predeterminada.

No es sorprendente que la mayor parte de la degradación del rendimiento de dotnet en Linux se deba al trabajo asincrónico, en torno al grupo de subprocesos. En cierto punto, los desarrolladores de dotnet incluso reescribieron el código del grupo de subprocesos de código nativo a código C# administrado para que al menos intentara ser similar en diferentes sistemas operativos. Pero las primitivas básicas para el trabajo asincrónico siguen siendo muy diferentes en diferentes sistemas operativos; incluso el conjunto de métodos asincrónicos en las API del sistema operativo difiere. No todos los métodos asíncronos son realmente honestos y asíncronos en todos SO.

Describamos la situación.

Hay un tipo de aplicación que muchas veces “no hace nada”. Están esperando algo, listos para empezar a hacer el trabajo lo más rápido posible tan pronto como aparezca. Necesitan empezar a realizarlo lo más rápido posible. Y no existe un cronograma predeterminado para la realización de tales tareas. Al mismo tiempo, el patrón de aparición de este trabajo es explosivo: si aparece, significa que hay mucho a la vez, para un montón de hilos.

¿Por qué nos interesan? Porque hay muchos de ellos. Por ejemplo, en nuestra empresa hay alrededor de 600 demonios de este tipo en un entorno de prueba en un sistema de prueba. Nos ocuparemos de ellos más a fondo.

También nos interesan porque la transición de Windows a Linux resultó en un consumo total de recursos ligeramente más que duplicado.

¿En qué se gastó la CPU?

Desafortunadamente, en este caso no funcionó ningún método popular y ampliamente disponible para diagnosticar el consumo de CPU. Todas las herramientas mostraron que “toda la CPU se gastó en algún lugar del grupo de subprocesos”. A veces descendiendo a un máximo de tal especificidad: PortableThreadPool.WorkerThread.WorkerThreadStart(). Esto no fue suficiente.

Una herramienta vino al rescate perf. Puede encontrar fácilmente cómo usarlo. Y con dificultad, pero aún se puede analizar una aplicación algo compleja.

No entraremos en detalles sobre el estudio de los artefactos. Pero todo indicaba que la CPU se desperdiciaba en SpinWaits dentro del semáforo.

¿Qué es SpinWait?

Esta pregunta se responde perfectamente con la documentación del sitio web de Microsoft:

SpinWait es un tipo de sincronización liviano que puede usar en escenarios de bajo nivel para evitar los costosos cambios de contexto y transiciones del kernel que se requieren para los eventos del kernel.

¿Cómo funciona SpinWait en tus dedos? Simplemente desperdicia varios ciclos de CPU con trabajo inútil, aproximadamente equivalente a varias decenas de nanosegundos en el tiempo.

Se puede suponer que los autores del grupo de subprocesos consideran trabajar en el método WorkerThreadStart El interior del semáforo tomado es muy corto. Y si el semáforo está actualmente ocupado por alguien, entonces, muy probablemente, solo necesitará omitir algunos ciclos del procesador y el semáforo quedará vacío. Y debería ser mucho más barato y más rápido que caer en una verdadera espera. Porque una espera real generará un rendimiento del subproceso, es decir, realizará un cambio de contexto y devolverá el subproceso al programador de subprocesos. Y esta es una operación muy cara y larga. Por lo general, es mucho más costoso que un par de ciclos de CPU perdidos.

¿Por qué es más caro en Linux?

Y el diablo lo sabe. Simplemente funciona diferente, eso es todo. Ni peor ni mejor, diferente. Y en el buen sentido, el comportamiento de dotnet, el uso de dichas primitivas, debe configurarse de diferentes maneras según el sistema operativo.

¿Qué vamos a hacer?

A juzgar por códigoel semáforo está configurado para que esté limitado a 70 iteraciones de SpinWait de forma predeterminada. Y, he aquí, ¡este valor está configurado mediante una variable de entorno!

¿Qué pasa si este número se reduce? Por ejemplo, ¿escribir 0 allí?

Configurar una variable de entorno DOTNET_ThreadPool_UnfairSemaphoreSpinLimit=0las más de 600 instancias. Liberemos, miremos los gráficos del consumo total de CPU:

¿Es hora de regocijarse?

¿Éxito? ¿Vamos a configurar esta variable de entorno para todas las aplicaciones dotnet en Linux? De ninguna manera.

Teorizando lo que podría salir mal

¿Qué mal podría pasar con lo que ahora nunca hacemos? SpinWaitpero siempre caemos en una honesta ¿Esperar? Esto puede hacer que el rendimiento del grupo de subprocesos disminuya drásticamente.

Es fácil imaginar que en su aplicación tiene un flujo regular y estable de tareas emergentes y ejecutadas muy rápidamente. Y threadpool en el método. WorkerThreadStart a menudo se topa con un semáforo ocupado, espera un poco SpinWait-e, espera el semáforo, toma la tarea y va a ejecutarla. La proporción entre trabajo inútil (SpinWait) y trabajo útil es mínima. El tiempo de “inactividad” (no dedicado a realizar un trabajo útil) es mínimo.

Y si SpinWait-s no están presentes (o su número es pequeño y no basta con esperar el semáforo), nosotros, en teoría, a menudo podemos caer en una espera honesta y hacer un cambio de contexto. Esto consumirá mucho de nuestro tiempo y la proporción de tiempo “inactivo” (pasado sin hacer un trabajo útil) aumentará en relación con el tiempo dedicado a un trabajo útil. Cuanto más cortas sean las Tareas y cuantas más haya, peor será esta relación.

Por lo tanto, no se recomienda tocar la variable sin pensar. DOTNET_ThreadPool_UnfairSemaphoreSpinLimit. Primero evalúe todos los riesgos, estudie cuidadosamente cómo afectará su aplicación y observe atentamente durante un largo período de tiempo.

Conclusiones

  • ThreadPool es una abstracción increíblemente compleja que hace que escribir código “multiproceso” sea fácil y sin esfuerzo. Pero a veces esto tiene un precio enorme.

  • Incluso los desarrolladores de ThreadPool no pueden escribirlo para que sea ideal en todos los casos extremos. En algunas situaciones especiales no funciona “idealmente”.

  • Si su aplicación comenzó a consumir significativamente más CPU al cambiar de Windows a Linux, puede jugar con la variable de entorno DOTNET_ThreadPool_UnfairSemaphoreSpinLimitponiendo números desde 0 hasta el valor que desee allí, mirando el valor predeterminado en el código ThreadPool.

  • Es cierto que esto no ayudará a todas las aplicaciones. Y probablemente incluso a mucha gente le moleste. Después de todo, estos valores predeterminados se eligieron por una razón: deberían ser buenos “en promedio”.

  • Esta característica, sobre la que se puede influir, y la variable de entorno, que se puede cambiar, no son las únicas que tenemos a nuestra disposición en este momento.

  • En cada versión posterior de dotnet, todo puede cambiar por completo y es posible que la variable de entorno ya no se utilice. Lea los registros de cambios.

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *