Debido a la ignorancia del error del grupo de subprocesos de Java, un programador fue sacrificado al cielo

Usaremos varias técnicas de agrupación para almacenar en caché objetos con sobrecarga de alto rendimiento , como agrupaciones de subprocesos, agrupaciones de conexiones y agrupaciones de memoria.
Su principio es crear algunos objetos de antemano en el grupo, sacarlos directamente cuando se usan y devolverlos para su reutilización cuando se agotan. También ajustan el número de objetos en caché en el grupo a través de estrategias para lograr una escalabilidad dinámica.

Debido a que la creación de subprocesos es más cara, las tareas cortas y rápidas generalmente se consideran procesadas por el grupo de subprocesos en lugar de crear subprocesos directamente.

Declarar manualmente el grupo de subprocesos

La Executorsclase de herramienta del JDK define muchos métodos convenientes para crear rápidamente un grupo de subprocesos.

Pero Ali tiene algo que decir:

echemos un vistazo a los casos de negligencia que dijo que son realmente tan graves.

newFixedThreadPool puede OOM

Escribimos un fragmento de código de prueba para inicializar un FixedThreadPool de un solo subproceso y enviamos tareas al grupo de subprocesos 100 millones de veces en un bucle. Cada tarea creará una cadena relativamente grande y dormirá durante una hora:

Poco después de ejecutar el programa, apareció el siguiente OOM en el registro:

Exception in thread "http-nio-45678-ClientPoller" 
	java.lang.OutOfMemoryError: GC overhead limit exceeded

  • newFixedThreadPoolLa cola de trabajo del grupo de subprocesos directamente nuevo a LinkedBlockingQueue
  • Pero su constructor predeterminado es una Integer.MAX_VALUEcola de longitud, por lo que la cola se llena pronto

Aunque el newFixedThreadPoolnúmero de subprocesos de trabajo se puede arreglar, la cola de tareas es casi ilimitada. Si hay más tareas y una ejecución lenta, la cola se acumulará rápidamente y la memoria insuficiente puede conducir fácilmente a OOM.

newCachedThreadPool causa OOM

[11:30:30.487] [http-nio-45678-exec-1] [ERROR] [.a.c.c.C.[.[.[/].[dispatcherServlet]:175 ] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: unable to create new native thread] with root cause
java.lang.OutOfMemoryError: unable to create new native thread 

El registro muestra que OOM se debe a que no se pueden crear subprocesos. El número máximo de subprocesos en un grupo de subprocesos como newCachedThreadPool es Integer.MAX_VALUE, que puede considerarse ilimitado, y su cola de trabajo SynchronousQueue es una cola de bloqueo sin espacio de almacenamiento.
Entonces, siempre que haya una solicitud, se debe encontrar un hilo de trabajo para su procesamiento, y se creará uno nuevo si no hay un hilo libre actualmente.

Dado que nuestra tarea tarda 1 hora en completarse, se creará una gran cantidad de subprocesos cuando ingrese una gran cantidad de tareas. Sabemos que los subprocesos deben asignar una cierta cantidad de espacio de memoria como pilas de subprocesos, como 1 MB, por lo que la creación ilimitada de subprocesos conducirá inevitablemente a OOM:

public static ExecutorService newCachedThreadPool() {
    
    
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());

Los estudiantes de desarrollo realmente conocían el principio de estos dos grupos de subprocesos durante la entrevista, pero tuvieron un golpe de suerte y sintieron que el solo hecho de usar el grupo de subprocesos para realizar tareas ligeras no causaría retrasos en la cola ni abriría una gran cantidad de subprocesos.

Caso

Después de que el usuario se registra, llamamos a un servicio externo para enviar SMS. Cuando la interfaz de SMS es normal, puede responder en 100ms. El volumen registrado de TPS 100 y CachedThreadPool puede satisfacer de manera estable la demanda mientras ocupa unos 10 hilos. En cierto momento, el servicio de SMS externo no está disponible y el tiempo de espera para llamar a este servicio es extremadamente largo. Por ejemplo, en 1 minuto, pueden ingresar 6000 usuarios en 1 minuto y se generarán 6000 tareas de SMS, lo que requiere 6000 hilos. ¿Cuánto tiempo se tarda en causar OOM porque no se puede crear el hilo?

Por lo tanto, Ali no recomienda usar los dos métodos convenientes de creación de grupos de subprocesos proporcionados por los Ejecutores:

  • Debe evaluar varios parámetros básicos del grupo de subprocesos de acuerdo con sus propios escenarios y simultaneidad, incluido el número de subprocesos principales, el número máximo de subprocesos, la estrategia de reciclaje de subprocesos, el tipo de cola de trabajo y la estrategia de rechazo para garantizar que el comportamiento de trabajo del grupo de subprocesos cumpla con los requisitos. Ambos necesitan configurar una cola de trabajo limitada y un número controlable de subprocesos
  • En cualquier momento, debe especificar un nombre significativo para el grupo de subprocesos personalizados para facilitar la resolución de problemas. Cuando hay problemas como un fuerte aumento en el número de subprocesos, interbloqueos de subprocesos, subprocesos que ocupan una gran cantidad de CPU y excepciones de ejecución de subprocesos, la pila de subprocesos a menudo se toma. En este punto, un nombre de hilo significativo puede facilitar la ubicación del problema.

Además de declarar manualmente el grupo de subprocesos, se recomienda utilizar algunos métodos de supervisión para observar el estado del grupo de subprocesos. El componente del grupo de subprocesos tiende a ser laborioso y oscurecido. A menos que haya una estrategia de rechazo, ninguna excepción será presionada. Si puede observar el retraso de la cola del grupo de subprocesos o la rápida expansión del número de subprocesos de antemano, a menudo puede encontrar y resolver el problema temprano.

Gestión de subprocesos del grupo de subprocesos

  • El siguiente método realiza el monitoreo más simple

Personalice un grupo de subprocesos y construya una fábrica de subprocesos con la ayuda del método ThreadFactoryBuilder de la biblioteca de clases Jodd para realizar la denominación personalizada de los subprocesos del grupo de subprocesos.

Luego, escribimos un fragmento de código de prueba para observar la estrategia de administración del grupo de subprocesos. La lógica del código de prueba es enviar tareas al grupo de subprocesos en un intervalo de 1 segundo cada vez, y repetir 20 veces. Cada tarea tarda 10 segundos en completarse. El código es el siguiente:

  • Encontré el registro de falla de envío, el registro es así

Comportamiento predeterminado del grupo de subprocesos

  • Los subprocesos de CorePoolSize no se inicializarán, los subprocesos de trabajo se crearán solo cuando lleguen las tareas
  • Cuando el subproceso principal está lleno, el grupo de subprocesos no se expandirá inmediatamente, pero la tarea se apilará en la cola de trabajo
  • Una vez que la cola de trabajo esté llena, expanda el grupo de subprocesos hasta que el número de subprocesos alcance el tamaño de grupo máximo
  • Si la cola está llena y alcanza el subproceso máximo, aún quedan tareas por procesar de acuerdo con la política de rechazo
  • Cuando el número de subprocesos es mayor que el número de subprocesos centrales, todavía no hay tareas que procesar después de que los subprocesos esperan a keepAliveTime, y los subprocesos se reducen al número de subprocesos principales.

Comprender esta estrategia nos ayuda a establecer los parámetros de inicialización adecuados para el grupo de subprocesos en función de los requisitos de planificación de capacidad reales. También puede cambiar estos comportamientos laborales predeterminados a través de algunos medios, como:

  • Llame al método prestartAllCoreThreads inmediatamente después de que se declare el grupo de subprocesos para iniciar todos los subprocesos principales
  • Pase true a allowCoreThreadTimeOut para permitir que el grupo de subprocesos también recicle los subprocesos centrales cuando está inactivo

El grupo de subprocesos de Java primero usa la cola de trabajo para almacenar tareas que son demasiado tarde para procesar y luego expande el grupo de subprocesos cuando está lleno. Cuando la cola de trabajo se establece en un tamaño grande (la clase de herramienta predeterminada), el parámetro de número máximo de subprocesos no tiene sentido, porque la cola es difícil de llenar o puede ser OOM cuando está llena, y no hay posibilidad de expandir el grupo de subprocesos.

¿Se puede dar prioridad al grupo de subprocesos para abrir más subprocesos y a la cola como solución de seguimiento?
Por ejemplo, las tareas en el caso se ejecutan muy lentamente y toman 10 segundos. Si el grupo de subprocesos se puede expandir a los 5 subprocesos más grandes primero, estas tareas eventualmente se pueden completar y la tarea lenta no será demasiado tarde para procesar debido a la expansión tardía del grupo de subprocesos.

Idea de realización

La realización se basa básicamente en los siguientes dos problemas:

  • El grupo de subprocesos se expandirá cuando la cola de trabajo esté llena y no se pueda poner en cola. ¿Se puede reescribir la oferta de la cola para crear artificialmente la ilusión de que la cola está llena?
  • Después de piratear la cola, es probable que la estrategia de rechazo se active después de que se alcance el subproceso máximo. ¿Se puede implementar un controlador de estrategia de rechazo personalizado y luego la tarea se insertará en la cola en este momento?

Tomcat ha implementado un grupo de subprocesos "elásticos" similar.
Asegúrese de confirmar si el grupo de subprocesos en sí es un
entorno de producción de proyectos reutilizados . La cantidad de subprocesos ocasionalmente se alarma, superando los 2000. Después de recibir la alarma, verifique la supervisión y descubra que la cantidad de subprocesos instantáneos es relativamente grande, pero caerá después de un tiempo, y la cantidad de subprocesos fluctuará Muy potente, pero el tráfico de la aplicación no ha cambiado mucho.

Para localizar el problema, tome la pila de subprocesos cuando la cantidad de subprocesos sea alta y descubra que hay más de 1000 grupos de subprocesos personalizados en la memoria. En términos generales, los grupos de subprocesos deben multiplexarse ​​y los grupos de subprocesos dentro de 5 pueden considerarse normales, pero más de 1000 grupos de subprocesos definitivamente no son normales.

No vi el grupo de subprocesos declarado en el código del proyecto. Busqué la palabra clave de ejecución y
la ubiqué . Resultó que el código comercial llamó a una biblioteca de clases para obtener el grupo de subprocesos, similar a lo siguiente: llame al método getThreadPool de ThreadPoolHelper para obtener el grupo de subprocesos y luego envíe varios Las tareas se procesan en el grupo de subprocesos y no hay ninguna excepción.

Pero el método getThreadPool en realidad usa Executors.newCachedThreadPool cada vez para crear un grupo de subprocesos.

newCachedThreadPool creará tantos subprocesos como sea necesario. Una operación comercial del código comercial enviará varias tareas lentas al grupo de subprocesos, de modo que se abrirán varios subprocesos cuando se ejecute una operación comercial. Si la cantidad de operaciones comerciales simultáneas es grande, es posible iniciar miles de subprocesos a la vez.

Entonces, ¿por qué podemos ver que la cantidad de subprocesos caerá en el monitoreo sin OOM?
El número de subprocesos principales de newCachedThreadPool es 0 y keepAliveTime es 60, es decir, todos los subprocesos se pueden reciclar después de 60 segundos.

reparar

Utilice un campo estático para almacenar la referencia del grupo de subprocesos, y el código que devuelve el grupo de subprocesos puede devolver directamente este campo estático.

Considere el uso mixto de grupos de subprocesos

El significado del grupo de subprocesos es reutilización, ¿significa esto que el programa siempre debe usar un grupo de subprocesos?
No es. Para especificar los parámetros centrales del grupo de subprocesos de acuerdo con la prioridad de la tarea, incluido el número de subprocesos, la estrategia de reciclaje y la cola de tareas.

Caso

El código comercial usa el grupo de subprocesos para procesar algunos datos en la memoria de forma asincrónica, pero el monitoreo encontró que el procesamiento es muy lento. Todo el proceso de procesamiento es que el cálculo en la memoria no involucra operaciones de E / S. También requiere varios segundos de tiempo de procesamiento, y el uso de la CPU de la aplicación no es Muy alto.
La investigación final descubrió que el grupo de subprocesos utilizado por el código comercial también fue utilizado por una tarea de procesamiento por lotes de archivos en segundo plano.

Simular el procesamiento por lotes de archivos. Después de que se inicia el programa, la lógica de bucle sin fin se inicia a través de un hilo y las tareas se envían continuamente al grupo de hilos. La lógica de la tarea es escribir una gran cantidad de datos en un archivo:


Como puede imaginar, las tareas de 2 subprocesos en este grupo de subprocesos son bastante pesadas. A través del log impreso por el método printStats, observamos la carga del grupo de subprocesos:

Los dos subprocesos del grupo de subprocesos siempre están activos y la cola está básicamente llena. Debido a que la política de procesamiento de rechazo de CallerRunsPolicy está habilitada, cuando el hilo está lleno y la cola está llena, la tarea se ejecutará en el hilo que envió la tarea o en el hilo que llamó al método de ejecución. Es decir, la tarea enviada al grupo de hilos debe procesarse de forma asincrónica.
Si usa CallerRunsPolicy, es posible que las tareas asincrónicas se conviertan en ejecución sincrónica. Esto también se puede ver en la cuarta línea del registro. Por eso esta estrategia de rechazo es especial.

No sé por qué los estudiantes que escribieron el código establecieron esta estrategia. Quizás se encontró que el grupo de subprocesos era anormal porque la tarea no se pudo procesar y el grupo de subprocesos no quería que el grupo de subprocesos descartara la tarea, así que finalmente elegí esta estrategia de rechazo. En cualquier caso, estos registros son suficientes para mostrar que el grupo de subprocesos está saturado.
Es difícil para el código empresarial reutilizar tales grupos de subprocesos para cálculos de memoria.

  • Envíe una tarea simple al grupo de subprocesos
  • La prueba de presión simple TPS es 85, bajo rendimiento

El problema no es tan sencillo. El grupo de subprocesos original que ejecuta las tareas de E / S usa CallerRunsPolicy, por lo que el grupo de subprocesos se usa directamente para cálculos asincrónicos. Cuando el grupo de subprocesos está saturado, las tareas de cálculo se ejecutarán en el subproceso de Tomcat que ejecuta la solicitud web, lo que afectará aún más a otros procesos sincrónicos Thread, e incluso hacer que toda la aplicación se bloquee.

Reparar

Utilice un grupo de subprocesos independiente para realizar esas "tareas informáticas".
El código de simulación realiza una operación de suspensión, que no es una operación vinculada a la CPU, pero es más similar a una operación vinculada a E / S. Si el número de subprocesos en el grupo de subprocesos se establece demasiado pequeño, limitará el rendimiento:

  • Use un grupo de subprocesos separado para modificar el código y luego probar el rendimiento, TPS aumentó a 1683

Se puede ver que el problema de reutilizar ciegamente grupos de subprocesos y mezclarlos es que los atributos del grupo de subprocesos definidos por otros pueden no ser adecuados para su tarea, y la mezcla interferirá entre sí.
Por ejemplo, a menudo usamos tecnología de virtualización para lograr el aislamiento de recursos, en lugar de permitir que todas las aplicaciones usen directamente máquinas físicas.

Mezcla de grupos de subprocesos: flujo paralelo en Java 8

Es conveniente procesar los elementos de la colección en paralelo, compartiendo el mismo ForkJoinPool, y el paralelismo predeterminado es el número de núcleos de CPU -1 . Para las tareas vinculadas a la CPU, esta configuración es más apropiada, pero si la operación de recopilación implica operaciones de E / S síncronas (como operaciones de base de datos, llamadas de servicio externo, etc.), se recomienda personalizar un ForkJoinPool (o un grupo de subprocesos ordinario).

referencia

  • "Manual de desarrollo de Java de Alibaba"

Supongo que te gusta

Origin blog.csdn.net/qq_33589510/article/details/109549716
Recomendado
Clasificación