paint-brush
Una introducción al modelo Spring WebFlux Threadingby@vladimirf
13,753
13,753

Una introducción al modelo Spring WebFlux Threading

pring WebFlux es un marco web reactivo sin bloqueo que utiliza la biblioteca Reactor para implementar la programación reactiva en Java. El modelo de subprocesos de WebFlux es diferente del modelo tradicional de subprocesos por solicitud que se utiliza en muchos marcos web síncronos. WebFlux utiliza un modelo basado en eventos sin bloqueo, donde una pequeña cantidad de subprocesos puede manejar una gran cantidad de solicitudes. Esto permite que el subproceso avance para manejar otras solicitudes mientras las tareas se ejecutan en segundo plano. El uso de un programador paralelo puede mejorar el rendimiento y la escalabilidad al permitir que varias tareas se ejecuten simultáneamente en diferentes subprocesos.
featured image - Una introducción al modelo Spring WebFlux Threading
Vladimir Filipchenko HackerNoon profile picture
0-item

Spring WebFlux es un marco web reactivo y sin bloqueo para crear aplicaciones web modernas y escalables en Java. Es parte de Spring Framework y utiliza la biblioteca Reactor para implementar la programación reactiva en Java.


Con WebFlux, puede crear aplicaciones web escalables y de alto rendimiento que pueden manejar una gran cantidad de solicitudes simultáneas y flujos de datos. Admite una amplia gama de casos de uso, desde API REST simples hasta transmisión de datos en tiempo real y eventos enviados por el servidor.


Spring WebFlux proporciona un modelo de programación basado en flujos reactivos, que le permite componer operaciones asincrónicas y sin bloqueo en una canalización de etapas de procesamiento de datos. También proporciona un amplio conjunto de funciones y herramientas para crear aplicaciones web reactivas, incluida la compatibilidad con el acceso a datos reactivos, la seguridad reactiva y las pruebas reactivas.


Del documento oficial de Spring :

El término "reactivo" se refiere a los modelos de programación que se crean para reaccionar al cambio: componentes de red que reaccionan a eventos de E/S, controladores de interfaz de usuario que reaccionan a eventos del mouse y otros. En ese sentido, el no bloqueo es reactivo, porque, en lugar de estar bloqueados, ahora estamos en el modo de reaccionar a las notificaciones a medida que se completan las operaciones o los datos están disponibles.

Modelo de roscado

Una de las características principales de la programación reactiva es su modelo de subprocesos, que es diferente del modelo tradicional de subprocesos por solicitud que se utiliza en muchos marcos web síncronos.


En el modelo tradicional, se crea un nuevo subproceso para manejar cada solicitud entrante y ese subproceso se bloquea hasta que se procesa la solicitud. Esto puede generar problemas de escalabilidad cuando se manejan grandes volúmenes de solicitudes, ya que la cantidad de subprocesos necesarios para manejar las solicitudes puede volverse muy grande y el cambio de contexto de subprocesos puede convertirse en un cuello de botella.


Por el contrario, WebFlux utiliza un modelo sin bloqueo, basado en eventos, donde una pequeña cantidad de subprocesos puede manejar una gran cantidad de solicitudes. Cuando llega una solicitud, uno de los subprocesos disponibles la maneja, y luego delega el procesamiento real a un conjunto de tareas asincrónicas. Estas tareas se ejecutan sin bloqueo, lo que permite que el subproceso avance para manejar otras solicitudes mientras las tareas se ejecutan en segundo plano.


En Spring WebFlux (y en los servidores sin bloqueo en general), se supone que las aplicaciones no se bloquean. Por lo tanto, los servidores sin bloqueo utilizan un pequeño grupo de subprocesos de tamaño fijo (trabajadores de bucle de eventos) para manejar las solicitudes.


El modelo de subprocesamiento simplificado de un contenedor de Servlet clásico se ve así:

Si bien el procesamiento de solicitudes de WebFlux es ligeramente diferente:

Bajo el capó

Avancemos y veamos qué hay detrás de la brillante teoría.

Necesitamos una aplicación bastante minimalista generada por Spring Initializr . El código está disponible en el repositorio de GitHub .


Todos los temas relacionados con subprocesos dependen mucho de la CPU. Por lo general, la cantidad de subprocesos de procesamiento que manejan las solicitudes está relacionada con la cantidad de núcleos de CPU . Con fines educativos, puede manipular fácilmente el recuento de subprocesos en un grupo al limitar las CPU al ejecutar el contenedor Docker:

 docker run --cpus=1 -d --rm --name webflux-threading -p 8081:8080 local/webflux-threading

Si aún ve más de un subproceso en un grupo, está bien. Puede haber valores predeterminados establecidos por WebFlux.

Nuestra aplicación es un simple adivino. Al llamar /karma endpoint obtendrá 5 registros con balanceAdjustment . Cada ajuste es un número entero que representa un karma que se te ha dado. Sí, somos muy generosos porque la aplicación genera solo números positivos. ¡Ya no hay mala suerte!

Procesamiento por defecto

Comencemos con un ejemplo muy básico. El siguiente método de controlador devuelve un flujo que contiene 5 elementos de karma.


 @GetMapping("/karma") public Flux<Karma> karma() { return prepareKarma() .map(Karma::new) .log(); } private Flux<Integer> prepareKarma() { Random random = new Random(); return Flux.fromStream( Stream.generate(() -> random.nextInt(10)) .limit(5)); }


El método log es una cosa crucial aquí. Observa todas las señales de Reactive Streams y las rastrea en registros bajo el nivel INFO.


La salida de registros en curl localhost:8081/karma es la siguiente:


Como podemos ver, el procesamiento está ocurriendo en el grupo de subprocesos de IO. El nombre del subproceso ctor-http-nio-2 significa reactor-http-nio-2 . Las tareas se ejecutaron inmediatamente en un subproceso que las envió. Reactor no vio ninguna instrucción para programarlos en otro grupo.

Retraso y procesamiento paralelo

La siguiente operación retrasará la emisión de cada elemento en 100 ms (también conocido como emulación de base de datos)


 @GetMapping("/delayedKarma") public Flux<Karma> delayedKarma() { return karma() .delayElements(Duration.ofMillis(100)); }


No necesitamos agregar el método log aquí porque ya se declaró en la llamada karma() original.


En los logs podemos ver la siguiente imagen:


Esta vez, solo se recibió el primer elemento en el subproceso IO reactor-http-nio-4 . El procesamiento de los 4 restantes se dedicó a un grupo de subprocesos parallel .


Javadoc de delayElements confirma esto:

Las señales se retrasan y continúan en el programador predeterminado paralelo


Puede lograr el mismo efecto sin demora especificando .subscribeOn(Schedulers.parallel()) en cualquier parte de la cadena de llamadas.


El uso del programador parallel puede mejorar el rendimiento y la escalabilidad al permitir que varias tareas se ejecuten simultáneamente en diferentes subprocesos, lo que puede utilizar mejor los recursos de la CPU y manejar una gran cantidad de solicitudes simultáneas.


Sin embargo, también puede aumentar la complejidad del código y el uso de la memoria, y potencialmente provocar el agotamiento del grupo de subprocesos si se supera la cantidad máxima de subprocesos de trabajo. Por lo tanto, la decisión de utilizar un grupo de subprocesos parallel debe basarse en los requisitos específicos y las ventajas y desventajas de la aplicación.


subcadena

Ahora echemos un vistazo a un ejemplo más complejo. El código sigue siendo bastante simple y directo, pero el resultado es mucho más interesante.


Vamos a usar un flatMap y hacer que un adivino sea más justo . Para cada instancia de Karma, multiplicará el ajuste original por 10 y generará los ajustes opuestos, creando efectivamente una transacción equilibrada que compensa la original.


 @GetMapping("/fairKarma") public Flux<Karma> fairKarma() { return delayedKarma() .flatMap(this::makeFair); } private Flux<Karma> makeFair(Karma original) { return Flux.just(new Karma(original.balanceAdjustment() * 10), new Karma(original.balanceAdjustment() * -10)) .subscribeOn(Schedulers.boundedElastic()) .log(); }


Como puede ver, el flujo makeFair's debe estar suscrito a un grupo de subprocesos boundedElastic . Veamos lo que tenemos en los registros para los dos primeros Karmas:


  1. Reactor suscribe el primer elemento con balanceAdjustment=9 en el subproceso IO


  2. Luego, el boundedElastic funciona en la equidad de Karma emitiendo ajustes 90 y -90 en boundedElastic-1


  3. Los elementos posteriores al primero se suscriben en un grupo de subprocesos paralelos (porque todavía tenemos delayedElements en la cadena)


¿ Qué es un programador boundedElastic ?

Es un grupo de subprocesos que ajusta dinámicamente la cantidad de subprocesos de trabajo en función de la carga de trabajo. Está optimizado para tareas vinculadas a E/S, como consultas de bases de datos y solicitudes de red, y está diseñado para manejar una gran cantidad de tareas de corta duración sin crear demasiados subprocesos ni desperdiciar recursos.


De forma predeterminada, el grupo de subprocesos boundedElastic tiene un tamaño máximo de la cantidad de procesadores disponibles multiplicado por 10, pero puede configurarlo para usar un tamaño máximo diferente si es necesario.


Mediante el uso de un grupo de subprocesos asincrónicos boundedElastic , puede descargar tareas para separar subprocesos y liberar el subproceso principal para manejar otras solicitudes. La naturaleza limitada del grupo de subprocesos puede evitar la inanición de subprocesos y el uso excesivo de recursos, mientras que la elasticidad del grupo le permite ajustar la cantidad de subprocesos de trabajo dinámicamente en función de la carga de trabajo.


Otros tipos de grupos de subprocesos

Hay dos tipos más de grupos proporcionados por la clase Scheduler lista para usar, como:


  • single : este es un contexto de ejecución serializado de un solo subproceso que está diseñado para la ejecución síncrona. Es útil cuando necesita asegurarse de que una tarea se ejecute en orden y que no se ejecuten dos tareas al mismo tiempo.


  • immediate : esta es una implementación trivial y sin operaciones de un programador que ejecuta tareas inmediatamente en el subproceso de llamada sin ningún cambio de subproceso.


Conclusión

El modelo de subprocesamiento en Spring WebFlux está diseñado para que no bloquee y sea asincrónico, lo que permite el manejo eficiente de una gran cantidad de solicitudes con un uso mínimo de recursos. En lugar de depender de subprocesos dedicados por conexión, WebFlux utiliza una pequeña cantidad de subprocesos de bucle de eventos para manejar las solicitudes entrantes y distribuir el trabajo a los subprocesos de trabajo de varios grupos de subprocesos.


Sin embargo, es importante elegir el grupo de subprocesos correcto para su caso de uso para evitar la escasez de subprocesos y garantizar un uso eficiente de los recursos del sistema.