Hablando de un parámetro de grupo de subprocesos de Java que casi provoca un accidente en línea

I. Introducción

Recientemente, se ajustó el grupo de subprocesos del servicio Dubbo refactorizado y el subproceso de trabajo usó la estrategia de subprocesos CachedThreadPool. Sin embargo, después de conectarse, el grupo de subprocesos aumentó por completo, lo que casi provocó un accidente en línea.

89d01d99f0a4a06dca7fc71cc65373b3.png

Entonces, este artículo descubre el misterio del grupo de subprocesos.

2. Introducción al conjunto de subprocesos de Dubbo

Código fuente de CachedThreadPool en Dubbo

package org.apache.dubbo.common.threadpool.support.cached;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.threadlocal.NamedInternalThreadFactory;
import org.apache.dubbo.common.threadpool.ThreadPool;
import org.apache.dubbo.common.threadpool.support.AbortPolicyWithReport;

import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import static org.apache.dubbo.common.constants.CommonConstants.ALIVE_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.CORE_THREADS_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.DEFAULT_ALIVE;
import static org.apache.dubbo.common.constants.CommonConstants.DEFAULT_CORE_THREADS;
import static org.apache.dubbo.common.constants.CommonConstants.DEFAULT_QUEUES;
import static org.apache.dubbo.common.constants.CommonConstants.DEFAULT_THREAD_NAME;
import static org.apache.dubbo.common.constants.CommonConstants.QUEUES_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.THREADS_KEY;
import static org.apache.dubbo.common.constants.CommonConstants.THREAD_NAME_KEY;

/**
 * This thread pool is self-tuned. Thread will be recycled after idle for one minute, and new thread will be created for
 * the upcoming request.
 *
 * @see java.util.concurrent.Executors#newCachedThreadPool()
 */
public class CachedThreadPool implements ThreadPool {

    @Override
    public Executor getExecutor(URL url) {
        //1 获取线程名称前缀 如果没有 默认是Dubbo
        String name = url.getParameter(THREAD_NAME_KEY, DEFAULT_THREAD_NAME);
        //2. 获取线程池核心线程数大小
        int cores = url.getParameter(CORE_THREADS_KEY, DEFAULT_CORE_THREADS);
        //3. 获取线程池最大线程数大小,默认整型最大值
        int threads = url.getParameter(THREADS_KEY, Integer.MAX_VALUE);
        //4. 获取线程池队列大小
        int queues = url.getParameter(QUEUES_KEY, DEFAULT_QUEUES);
        //5. 获取线程池多长时间被回收 单位毫秒
        int alive = url.getParameter(ALIVE_KEY, DEFAULT_ALIVE);
        //6. 使用JUC包里的ThreadPoolExecutor创建线程池
        return new ThreadPoolExecutor(cores, threads, alive, TimeUnit.MILLISECONDS,
                queues == 0 ? new SynchronousQueue<Runnable>() :
                        (queues < 0 ? new LinkedBlockingQueue<Runnable>()
                                : new LinkedBlockingQueue<Runnable>(queues)),
                new NamedInternalThreadFactory(name, true), new AbortPolicyWithReport(name, url));
    }
}

Se puede ver que Dubbo utiliza esencialmente ThreadPoolExecutor en el paquete JUC para crear un grupo de subprocesos. El código fuente es el siguiente

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

El diagrama de flujo general es el siguiente:

81889daa32d888b5e8358489a8c00d17.png

1. Cuando el grupo de subprocesos es más pequeño corePoolSize, la nueva tarea creará un nuevo subproceso, incluso si hay subprocesos inactivos en el grupo de subprocesos en este momento.

2. Cuando se alcanza el grupo de subprocesos corePoolSize, la tarea recién enviada se colocará en la cola workQueue, a la espera de que se programe la ejecución de la tarea del grupo de subprocesos.

3. Cuando workQueueesté lleno y maximumPoolSize> corePoolSize, la nueva tarea creará un nuevo hilo para ejecutar la tarea.

4. Cuando el número de tareas enviadas exceda maximumPoolSize, las nuevas tareas enviadas serán RejectedExecutionHandlerprocesadas por .

5. Cuando se exceda el grupo de subprocesos corePoolSize, keepAliveTimecuando se alcance el tiempo de inactividad, cierre el subproceso inactivo.

Además, cuando se establece allowCoreThreadTimeOut(true), corePoolSizeel tiempo de inactividad del subproceso en el grupo de subprocesos keepAliveTimetambién se cerrará.

RejectedExecutionHandler proporciona cuatro estrategias de rechazo por defecto

1. Estrategia AbortPolicy: esta estrategia lanzará directamente una excepción para evitar que el sistema funcione normalmente;

2. Política CallerRunsPolicy: si la cantidad de subprocesos en el grupo de subprocesos alcanza el límite superior, esta política colocará las tareas en la cola de tareas para que se ejecuten en el subproceso de la persona que llama;

3. Política DiscardOledestPolicy: esta política descartará la tarea más antigua en la cola de tareas, es decir, la tarea que se agrega primero en la cola de tareas actual y se ejecutará de inmediato, e intentará enviarla nuevamente.

4. Política DiscardPolicy: esta política descartará silenciosamente las tareas que no se pueden procesar sin ningún procesamiento. Por supuesto, utilizando esta estrategia, se debe permitir la pérdida de tareas en escenarios comerciales;

Vale la pena señalar que la política de rechazo AbortPolicyWithReport en Dubbo en realidad hereda la política ThreadPoolExecutor.AbortPolicy, principalmente imprimiendo información clave e información de pila.

3. Acerca de la configuración del grupo de subprocesos

La configuración del grupo de subprocesos es muy importante, pero a menudo se pasa por alto fácilmente.Si la configuración no es razonable o el número de reutilizaciones del grupo de subprocesos es pequeño, las cuentas se crearán y cancelarán con frecuencia.

  1. ¿Cómo calcular razonablemente el número de subprocesos principales?

Podemos calcular a través del tiempo de respuesta promedio de la interfaz y el QPS que el servicio necesita admitir. Por ejemplo: el RT promedio de nuestra interfaz es 0.005 s, entonces un subproceso de trabajo puede manejar 200 tareas. Si una sola máquina necesita admitir QPS 3W, entonces se puede calcular que la cantidad de subprocesos principales necesarios es 150

Esa es la fórmula: QPS ➗ (1 ➗ RT promedio) = QPS * RT

  1. La anotación @Async que se pasa por alto fácilmente

El grupo de subprocesos predeterminado que usa la anotación @Async en Spring es SimpleAsyncTaskExecutor. De forma predeterminada, si no hay configuración, el grupo de subprocesos no se usa, porque recreará un nuevo subproceso cada vez y no se reutilizará.

Así que recuerda, si usas @Async, debes configurarlo.

@EnableAsync
@Configuration
@Slf4j
public class ThreadPoolConfig {
    private static final int corePoolSize = 100;             // 核心线程数(默认线程数)
    private static final int maxPoolSize = 400;             // 最大线程数
    private static final int keepAliveTime = 60;            // 允许线程空闲时间(单位:默认为秒)
    private static final int queueCapacity = 0;         // 缓冲队列数
    private static final String threadNamePrefix = "Async-Service-"; // 线程池名前缀

    @Bean("taskExecutor") 
    public ThreadPoolTaskExecutor getAsyncExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveTime);
        executor.setThreadNamePrefix(threadNamePrefix);

        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        // 初始化
        executor.initialize();
        return executor;
    }
}

4. ¿Cómo se disparó el grupo de subprocesos?

Configuramos el subproceso de trabajo del servidor Dubbo de la siguiente manera:

corethreads: 150
threads: 800
threadpool: cached
queues: 10

¿Parece razonable? Establecer una pequeña cantidad de colas es para evitar la escasez de subprocesos a corto plazo causada por el jitter. De lo anterior, parece que no hay problema.En términos de volumen de negocios durante el día, la cantidad de subprocesos principales es completamente suficiente (RT<5ms, QPS<1w). Sin embargo, después de conectarse, el grupo de subprocesos se disparó por completo, alcanzando el valor de umbral máximo de 800, y la información de alarma es la siguiente:

org.apache.dubbo.remoting.RemotingException("Server side(IP,20880) thread pool is exhausted, detail msg:Thread pool is EXHAUSTED! Thread Name: DubboServerHandler-IP:20880, Pool Size: 800 (active: 4, core: 300, max: 800, largest: 800), Task: 4101304 (completed: 4101301), Executor status:(isShutdown:false, isTerminated:false, isTerminating:false), in dubbo://IP:20880!"

De lo anterior se puede ver que cuando se alcanza el número máximo de subprocesos, el número de subprocesos activos es muy pequeño, lo cual es completamente inesperado.

5. Simulación de escena

por código fuente

queues == 0 ? new SynchronousQueue<Runnable>() :
                        (queues < 0 ? new LinkedBlockingQueue<Runnable>()
                                : new LinkedBlockingQueue<Runnable>(queues))

Puede observarse que:

Cuando el elemento de la cola es 0, la cola de bloqueo usa SynchronousQueue; cuando el elemento de la cola es menor que 0, se usa la cola de bloqueo ilimitada LinkedBlockingQueue; cuando el elemento de la cola es mayor que 0, se usa la cola limitada LinkedBlockingQueue.

La cantidad de subprocesos centrales y la cantidad máxima de subprocesos definitivamente no serán un problema, así que supongo que hay un problema con la cantidad de colas.

Para reproducir, escribí una simulación de código simple

package com.bytearch.fast.cloud;

import java.util.concurrent.*;

public class TestThreadPool {

    public final static int queueSize = 10;
    public static void main(String[] args) {
        ExecutorService executorService = getThreadPool(queueSize);
        for (int i = 0; i < 100000; i++) {
            int finalI = i;

            try {
                executorService.execute(new Runnable() {
                    @Override
                    public void run() {
                        doSomething(finalI);
                    }
                });
            } catch (Exception e) {
                System.out.println("emsg:" + e.getMessage());
            }
            if (i % 20 == 0) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        System.out.println("all done!");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    public static ExecutorService getThreadPool(int queues) {
        int cores = 150;
        int threads = 800;
        int alive = 60 * 1000;


        return new ThreadPoolExecutor(cores, threads, alive, TimeUnit.MILLISECONDS,
                queues == 0 ? new SynchronousQueue<Runnable>() :
                        (queues < 0 ? new LinkedBlockingQueue<Runnable>()
                                : new LinkedBlockingQueue<Runnable>(queues)));
    }

    public static void doSomething(final int i) {
        try {
            Thread.sleep(5);
            System.out.println("thread:" + Thread.currentThread().getName() +  ", active:" + Thread.activeCount() + ", do:" + i);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Resultados de la simulación:

valor de tamaño de cola Fenómeno
0 sin excepción
10 Ocurrió una excepción de rechazo
100 sin excepción

La excepción es la siguiente:

emsg:Task com.bytearch.fast.cloud.TestThreadPool$1@733aa9d8 rejected from java.util.concurrent.ThreadPoolExecutor@6615435c[Running, pool size = 800, active threads = 32, queued tasks = 9, completed tasks = 89755]
all done!

Obviamente, cuando la simultaneidad es alta, cuando se utiliza la cola limitada LinkedBlockingQueue y el número de colas es relativamente pequeño, el grupo de subprocesos tendrá problemas.

Cambie la configuración de las colas a 0 y conéctese, y volverá a la normalidad.

En cuanto a las razones más profundas, los estudiantes interesados ​​pueden analizarlo en profundidad o comunicarse conmigo en el fondo de la cuenta oficial.

6. Resumen

Esta vez, compartí el principio básico del grupo de subprocesos ThreadPoolExecutor, el método de cálculo de la configuración del grupo de subprocesos y el problema de configuración @Async que se pasa por alto fácilmente.

Además, presenta el extraño problema que encontramos al usar el grupo de subprocesos, un problema de parámetros, que puede tener consecuencias impredecibles.

Espero que el intercambio anterior sea útil para usted.

Supongo que te gusta

Origin blog.csdn.net/weixin_38130500/article/details/120359848
Recomendado
Clasificación