Independientemente de si está siguiendo o no, las aplicaciones web de Java utilizan grupos de subprocesos para manejar las solicitudes en mayor o menor medida. Se pueden ignorar los detalles de implementación de los grupos de subprocesos, pero es necesario comprender tarde o temprano en el uso y ajuste de los grupos de subprocesos. Este artículo presenta principalmente el uso de Java Thread Pool y cómo configurar correctamente el grupo de subprocesos.
Solo enhebrado
Comencemos con lo básico. No importa qué servidor o marco de aplicaciones (como Tomcat, Jetty, etc.) se usen, tienen implementaciones básicas similares. La base de un servicio web es un socket, que es responsable de escuchar el puerto, esperar la conexión TCP y aceptar la conexión TCP. Una vez que se acepta la conexión TCP, los datos se pueden leer y enviar desde la conexión TCP recientemente creada.
Para comprender el proceso anterior, no usamos ningún servidor de aplicaciones directamente, sino que creamos un servicio web simple desde cero. Este servicio es un microcosmos de la mayoría de los servidores de aplicaciones. Un simple servicio web de un solo subproceso se ve así:
SERVERSOCK LOYER = new ServerSocket (8080); try {while (true) {socket socket = oyente.accept (); intente {HandLerequest (socket); } catch (ioException e) {E.PrintStackTrace (); }}} Finalmente {oyente.close ();}El código anterior crea un socket de servidor (Serversocket) , escucha al puerto 8080 y luego bucea para verificar el socket para ver si hay una nueva conexión. Una vez que se acepta una nueva conexión, el socket se pasará al método HandleRequest. Este método analiza el flujo de datos en una solicitud HTTP, responde y escribe los datos de respuesta. En este simple ejemplo, el método Handlerequest simplemente implementa la lectura del flujo de datos y devuelve un simple datos de respuesta. En implementaciones generales, este método será mucho más complejo, como leer datos de una base de datos, etc.
Respuesta de cadena estática final = "Http/1.0 200 OK/R/N" + "Tipo de contenido: Text/Plain/R/N" + "/R/N" + "Hello World/R/N"; public static void HandleRequest (Socket Socket) lanza IOException {// Lea la secuencia de entrada y devuelve "200 OK" Try {BufferedReader in = new BufferedReader (new InputStreamReader (Socket.getInputStream ())); log.info (in.readline ()); OutputStream out = Socket.getOutputStream (); out.write (respuesta.getBytes (StandardCharSets.utf_8)); } finalmente {Socket.close (); }}Dado que solo hay un hilo para procesar la solicitud, cada solicitud debe esperar a que se procese la solicitud anterior antes de que pueda responder. Suponiendo que un tiempo de respuesta de solicitud es de 100 milisegundos, el número de respuestas por segundo (TPS) de este servidor es solo 10.
Múltiple
Aunque el método Handlerequest puede bloquear en IO, la CPU aún puede manejar más solicitudes. Pero en un solo caso roscado, esto no se puede hacer. Por lo tanto, la capacidad de procesamiento paralelo del servidor se puede mejorar mediante la creación de métodos de subprocesos múltiples.
Public static class handLerequestrunnable implementos runnable {Final Socket Socket; public HandLerequestrunnable (socket de socket) {this.socket = socket; } public void run () {try {handlerequest (socket); } catch (ioException e) {E.PrintStackTrace (); }}} Serversocket oyente = nuevo Serversocket (8080); try {while (true) {socket socket = oyente.accept (); nuevo hilo (nuevo HandLerequestrunnable (Socket)). Start (); }} Finalmente {oyente.close ();}Aquí, el método Acept () todavía se llama en el hilo principal, pero una vez que se establece la conexión TCP, se creará un nuevo hilo para manejar la nueva solicitud, que es ejecutar el método Handlerequest en el texto anterior en el nuevo hilo.
Al crear un nuevo hilo, el hilo principal puede continuar aceptando nuevas conexiones TCP, y estas solicitudes se pueden procesar en paralelo. Este método se llama "un hilo por solicitud". Por supuesto, hay otras formas de mejorar el rendimiento del procesamiento, como el modelo asincrónico basado en eventos utilizado por Nginx y Node.js, pero no usan grupos de subprocesos y, por lo tanto, no están cubiertos por este artículo.
En cada solicitud de implementación de un hilo, crear una sobrecarga de hilo (y destrucción posterior) es muy costoso porque tanto el JVM como el sistema operativo deben asignar recursos. Además, la implementación anterior también tiene un problema, es decir, el número de subprocesos creados es incontrolable, lo que puede hacer que los recursos del sistema se agoten rápidamente.
Recursos agotados
Cada hilo requiere una cierta cantidad de espacio de memoria de pila. En el JVM más reciente de 64 bits, el tamaño de pila predeterminado es 1024 kb. Si el servidor recibe una gran cantidad de solicitudes, o el método Handlerequest se ejecuta lentamente, el servidor puede bloquearse debido a la creación de una gran cantidad de hilos. Por ejemplo, hay 1000 solicitudes paralelas, y los hilos 1000 creados deben usar 1 GB de memoria JVM como espacio de pila de subprocesos. Además, los objetos creados durante la ejecución del código de cada hilo también se pueden crear en el montón. Si esta situación empeora, excederá la memoria del montón JVM y generará una gran cantidad de operaciones de recolección de basura, lo que eventualmente causará desbordamiento de memoria (OutOfMemoryErrors).
Estos hilos no solo consumen memoria, sino que también usan otros recursos limitados, como manijas de archivos, conexiones de bases de datos, etc. Los subprocesos de creación incontrolables también pueden causar otros tipos de errores y bloqueos. Por lo tanto, una forma importante de evitar el agotamiento de los recursos es evitar estructuras de datos incontrolables.
Por cierto, debido a los problemas de memoria causados por el tamaño de la pila de subprocesos, el tamaño de la pila se puede ajustar a través del interruptor -xss. Después de reducir el tamaño de la pila del hilo, se puede reducir la sobrecarga por hilo, pero se puede elevar el desbordamiento de la pila (stackoverflowerrors). Para aplicaciones generales, el 1024 kb predeterminado es demasiado rico, y puede ser más apropiado reducirlo a 256 kb o 512kb. El valor mínimo permitido en Java es de 160 kb.
Piscina
Para evitar la creación continua de nuevos hilos, puede limitar el límite superior del grupo de hilos utilizando un grupo de hilos simple. La piscina de hilos maneja todos los hilos. Si el número de hilos no ha alcanzado el límite superior, el grupo de hilos crea hilos al límite superior y reutiliza los hilos libres tanto como sea posible.
Serversocket oyente = new Serversocket (8080); ExecutorService Ejecutor = Ejecutors.NewFixedThreadPool (4); Prueba {while (true) {socket socket = oyente.accept (); ejecutor.submit (new HandLerequestrunnable (Socket)); }} Finalmente {oyente.close ();}En este ejemplo, en lugar de crear el hilo directamente, se usa ExecutorService. Envía las tareas que deben ejecutarse (necesitan implementar la interfaz RunNables) al grupo de subprocesos y ejecuta el código usando subprocesos en el grupo de subprocesos. En el ejemplo, se utiliza un grupo de subprocesos de tamaño fijo con una serie de hilos de 4 para procesar todas las solicitudes. Esto limita el número de hilos que manejan las solicitudes y también limita el uso de recursos.
Además de crear un grupo de hilos de tamaño fijo a través del método NewFixedThreadPool, la clase Ejecutores también proporciona el método NewCachedThreadPool. La reutilización de un grupo de subprocesos aún puede conducir a un número incontrolable de hilos, pero usará los hilos inactivos que se han creado antes de la mayor cantidad posible. Por lo general, este tipo de grupo de subprocesos es adecuado para tareas cortas que no están bloqueadas por recursos externos.
Cola de trabajo
Después de usar un grupo de subprocesos de tamaño fijo, si todos los hilos están ocupados, ¿qué sucederá si viene otra solicitud? ThreadPoolExecutor usa una cola para mantener las solicitudes pendientes, y los grupos de subprocesos de tamaño fijo usan listas vinculadas ilimitadas de forma predeterminada. Tenga en cuenta que esto a su vez puede causar problemas de agotamiento de recursos, pero no sucederá siempre que la velocidad de procesamiento de hilo sea mayor que la tasa de crecimiento de la cola. Luego, en el ejemplo anterior, cada solicitud de cola contendrá un socket, que en algunos sistemas operativos consumirá el mango del archivo. Dado que el sistema operativo limita el número de manijas de archivos abridas por el proceso, es mejor limitar el tamaño de la cola de trabajo.
public static EjecutorService NewBoundedFixedThreadPool (int nthreads, int lanza IOException {Serversocket Listener = New Serversocket (8080); EjecutorService Ejecutor = NewBoundedFixedThreadPool (4, 16); intente {while (true) {socket socket = oyente.accept (); ejecutor.submit (new HandLerequestrunnable (Socket)); }} finalmente {oyente.close (); }}Aquí, en lugar de usar directamente el método de ejecutores. NewFixedThreadPool para crear un grupo de subprocesos, construimos el objeto ThreadPoolExecutor nosotros mismos y limitamos la longitud de la cola de trabajo a 16 elementos.
Si todos los hilos están ocupados, la nueva tarea se llenará en la cola. Dado que la cola limita el tamaño a 16 elementos, si se excede este límite, debe ser manejado por el último parámetro al construir el objeto Threadpoolexecutor. En el ejemplo, se usa una expolicia, es decir, cuando la cola alcanza el límite superior, la nueva tarea se descartará. Además de la primera vez, también hay una política de abortes (abortpolicy) y una política de ejecución de la persona que llama (CallerrunSpolicy). El primero lanzará una excepción, mientras que el segundo ejecutará la tarea en el hilo de la persona que llama.
Para las aplicaciones web, la política predeterminada óptima debe ser abandonar o abortar la política y devolver un error al cliente (como un error HTTP 503). Por supuesto, también es posible evitar abandonar las solicitudes de los clientes aumentando la duración de la cola de trabajo, pero las solicitudes de los usuarios generalmente no están dispuestas a esperar mucho tiempo, y esto consumirá más recursos del servidor. El propósito de la cola de trabajo no es responder a las solicitudes del cliente sin límite, sino para suavizar y explotar las solicitudes. Normalmente, la cola de trabajo debe estar vacía.
Ajuste del conteo de hilos
El ejemplo anterior muestra cómo crear y usar un grupo de subprocesos, pero el problema central con el uso de un grupo de subprocesos es cuántos hilos deben usarse. Primero, debemos asegurarnos de que cuando se alcance el límite de hilo, el recurso no se agotará. Los recursos aquí incluyen memoria (montón y pila), número de manijas de archivos abiertos, número de conexiones TCP, número de conexiones de bases de datos remotas y otros recursos limitados. En particular, si las tareas roscadas son computacionalmente intensivas, el número de núcleos de CPU también es una de las limitaciones de recursos. En términos generales, el número de hilos no debe exceder el número de núcleos de CPU.
Dado que la selección del recuento de subprocesos depende del tipo de aplicación, puede tomar muchas pruebas de rendimiento antes de que se puedan obtener los resultados óptimos. Por supuesto, también puede mejorar el rendimiento de su aplicación aumentando la cantidad de recursos. Por ejemplo, modifique el tamaño de la memoria del montón JVM, o modifique el límite superior del mango del archivo del sistema operativo, etc. Entonces, estos ajustes eventualmente alcanzarán el límite superior teórico.
Ley de Little
La ley de Little describe la relación entre tres variables en un sistema estable.
Cuando L representa el número promedio de solicitudes, λ representa la frecuencia de las solicitudes, y W representa el tiempo promedio para responder a la solicitud. Por ejemplo, si el número de solicitudes por segundo es 10 y cada tiempo de procesamiento de solicitudes es de 1 segundo, entonces en cualquier momento se procesan 10 solicitudes. De vuelta a nuestro tema, requiere 10 hilos para procesar. Si el tiempo de procesamiento de una sola solicitud se duplica, el número de subprocesos procesados también se duplicará, convirtiéndose en 20.
Después de comprender el impacto del tiempo de procesamiento en la eficiencia de procesamiento de solicitudes, encontraremos que el límite superior teórico puede no ser el valor óptimo para el tamaño del grupo de subprocesos. El límite superior del grupo de subprocesos también requiere un tiempo de procesamiento de tareas de referencia.
Suponiendo que el JVM puede procesar 1000 tareas en paralelo, si cada tiempo de procesamiento de solicitudes no excede los 30 segundos, entonces en el peor de los casos, como máximo se pueden procesar 33.3 solicitudes por segundo. Sin embargo, si cada solicitud solo toma 500 milisegundos, la solicitud puede procesar 2000 solicitudes por segundo.
Piscina de hilo dividido
En microservicios o arquitecturas orientadas a servicios (SOA), generalmente se requiere acceso a múltiples servicios de backend. Si uno de los servicios se degrada, puede hacer que el grupo de subprocesos se quede sin subprocesos, lo que afecta las solicitudes a otros servicios.
Una forma efectiva de lidiar con la falla del servicio de backend es aislar el grupo de subprocesos utilizado por cada servicio. En este modo, todavía hay un grupo de subprocesos enviado que envía tareas a diferentes grupos de subprocesos de solicitud de back -end. Este grupo de hilos puede no tener carga debido a un backend lento y transferir la carga a una piscina de hilo que solicita un backend lento.
Además, el modo de agrupación de múltiples subprocesos también debe evitar problemas de punto muerto. Si cada hilo está bloqueando mientras espera el resultado de una solicitud sin procesar, se produce un punto muerto. Por lo tanto, en el modo de grupo multiproceso, es necesario comprender las tareas ejecutadas por cada grupo de subprocesos y las dependencias entre ellos, para evitar problemas de punto muerto tanto como sea posible.
Resumir
Incluso si los grupos de subprocesos no se usan directamente en la aplicación, es probable que el servidor o el marco de la aplicación los usen indirectamente en la aplicación. Frameworks como Tomcat, JBoss, Untow, Dropwizard, etc. Todos proporcionan opciones para ajustar los grupos de subprocesos (grupos de subprocesos utilizados por la ejecución de servlet).
Espero que este artículo pueda mejorar su comprensión del grupo de hilos y ayudarlo a aprender.