Una de mis preguntas favoritas en las entrevistas es “¿Qué te dicen palabras como async y await ?” porque abre la oportunidad de tener una conversación interesante con el entrevistado… O no, porque se habla de este tema. En mi opinión, es sumamente importante entender por qué usamos esta técnica.
Siento que muchos desarrolladores prefieren confiar en la declaración “es la mejor práctica” y usar métodos asincrónicos ciegamente.
Este artículo muestra la diferencia entre métodos asincrónicos y sincrónicos en la práctica.
Ejecutaré un benchmark de la siguiente manera. Dos instancias de Locust independientes se ejecutan en dos máquinas. Las instancias de Locust simulan un usuario que hace lo siguiente:
En segundo plano, cada App Service se conecta a su propia base de datos y ejecuta una consulta SELECT que tarda cinco segundos y devuelve algunas filas de datos. Consulta el código del controlador a continuación para obtener referencias. Usaré Dapper para realizar una llamada a la base de datos. Me gustaría llamar tu atención sobre el hecho de que el punto final asincrónico también llama a la base de datos de forma asincrónica ( QueryAsync<T> ).
Vale la pena agregar que implemento el mismo código en ambos servicios de aplicación.
Durante la prueba, la cantidad de usuarios crece de manera uniforme hasta alcanzar la cantidad objetivo ( Número de usuarios ). La velocidad de crecimiento se controla mediante un parámetro de tasa de generación (cantidad de usuarios únicos que se unen por segundo): cuanto mayor sea la cantidad, más rápido se agregarán los usuarios. La tasa de generación se establece en 10 usuarios/s para todos los experimentos.
Todos los experimentos están limitados a 15 minutos.
Puede encontrar detalles de configuración de la máquina en la sección Detalles técnicos del artículo.
Las líneas rojas se refieren al punto final asincrónico y las líneas azules, al sincrónico, respectivamente.
Eso es todo en cuanto a la teoría. Empecemos.
Podemos ver que ambos puntos finales funcionan de manera similar: manejan alrededor de 750 solicitudes por minuto con un tiempo de respuesta medio de 5200 ms.
El gráfico más fascinante de este experimento es una tendencia de subprocesos. Se pueden ver números significativamente más altos para el punto final sincrónico (un gráfico azul): ¡más de 100 subprocesos!
Sin embargo, esto es lo esperado y coincide con la teoría: cuando llega una solicitud y la aplicación realiza una llamada a la base de datos, el hilo se bloquea porque tiene que esperar a que se complete un viaje de ida y vuelta. Por lo tanto, cuando llega otra solicitud, la aplicación tiene que generar un nuevo hilo para manejarla.
El gráfico rojo (el recuento de subprocesos del punto final asincrónico) demuestra un comportamiento diferente. Cuando llega una solicitud y la aplicación realiza una llamada a la base de datos, el subproceso vuelve a un grupo de subprocesos en lugar de bloquearse. Por lo tanto, cuando llega otra solicitud, este subproceso libre se reutiliza. A pesar de que las solicitudes entrantes aumentan, la aplicación no requiere ningún subproceso nuevo, por lo que su recuento sigue siendo el mismo.
Vale la pena mencionar la tercera métrica: el tiempo de respuesta medio . Ambos puntos finales mostraron el mismo resultado: 5200 ms. Por lo tanto, no hay diferencia en términos de rendimiento.
Ahora es el momento de levantar las estacas.
Duplicamos la carga. El punto final asincrónico maneja esta tarea con éxito: su tasa de solicitudes por minuto ronda las 1500. El hermano sincrónico finalmente alcanzó una cantidad comparable de 1410. Pero si observa el gráfico a continuación, ¡verá que tardó 10 minutos!
El motivo es que el punto final sincrónico reacciona a la llegada de un nuevo usuario creando otro hilo, pero los usuarios se agregan al sistema (solo para recordarle que la tasa de generación es de 10 usuarios/s) más rápido de lo que el servidor web puede adaptarse. Es por eso que puso en cola tantas solicitudes desde el principio.
Como era de esperar, la métrica de recuento de subprocesos sigue rondando los 34 para el punto final asincrónico, mientras que aumentó de 102 a 155 para el sincrónico. El tiempo de respuesta medio se degradó de manera similar a la tasa de solicitudes por minuto : el tiempo de respuesta sincrónico era mucho mayor al comienzo del experimento. Si hubiera mantenido la prueba durante 24 horas, los números medios se habrían vuelto iguales.
El tercer experimento tiene como objetivo comprobar las tendencias reveladas durante el segundo: podemos ver una mayor degradación del punto final sincrónico.
El uso de operaciones asincrónicas en lugar de sincrónicas no mejora directamente el rendimiento ni la experiencia del usuario. En primer lugar, mejora la estabilidad y la previsibilidad bajo presión. En otras palabras, aumenta el umbral de carga para que el sistema pueda procesar más antes de degradarse.
Para lograr el resultado de prueba más limpio, debería haber ejecutado pruebas desde 2 máquinas virtuales ubicadas en la misma red donde se encuentran los servicios de aplicaciones de destino.
Sin embargo, supuse que un retraso en la red afectaría a ambas aplicaciones de una manera más o menos similar. Por lo tanto, no puede poner en peligro el objetivo principal: comparar cómo se comportan los métodos asincrónicos y sincrónicos.
¿Qué hice para forzar al punto final sincrónico a funcionar casi como asincrónico y trazar el gráfico a continuación (las condiciones del experimento son las mismas que en el tercero: 200 usuarios)?