Practice of thread pool dynamic configuration

Posted by lJesterl on Sat, 06 Nov 2021 18:03:38 +0100

        Recently, during the test, due to business needs, the concurrency of the system needs to be controlled, because the previous method was thread pool and multithreading to process tasks. However, it is troublesome to publish the version after each adjustment. Therefore, a dynamic thread pool is used to dynamically modify the number of threads, so as to control concurrent processing. Refer to meituan's technical articles and introduce the main technical points.

1. Thread pool Basics

The code of thread pool is shown in the figure. These parameters are required during creation. Please briefly introduce the meaning of each parameter.

1.corePoolSize: the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set

(number and size of core threads: whether they are idle or not after they are created. The thread pool needs to maintain the number of threads with corePoolSize, unless allowCoreThreadTimeOut is set.)

2.maximumPoolSize: the maximum number of threads to allow in the pool.

(maximum number of threads: a maximum of maximumPoolSize threads are allowed to be created in the thread pool.)

3.keepAliveTime: when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.

(survival time: if the threads exceeding the number of core threads have not received new tasks after keepAliveTime, they will be recycled.)

4.unit: the time unit for the {@code keepAliveTime} argument

(the time unit of keepAliveTime.)

5.workQueue: the queue to use for holding tasks before they are executed. This queue will hold only the {@code Runnable} tasks submitted by the {@code execute} method.

(queue for storing tasks to be executed: when the number of tasks submitted exceeds the number of core threads, the tasks submitted are stored here. It is only used to store Runnable tasks submitted by the execute method.)

6.threadFactory: the factory to use when the executor creates a new thread.

(thread Engineering: used to create a thread factory. For example, the thread name can be customized. When analyzing the virtual machine stack, you can know where the thread comes from by looking at the name, so you won't be confused.)

7.handler : the handler to use when execution is blocked because the thread bounds and queue capacities are reached.

(reject policy: when the queue is full of tasks and the threads with the maximum number of threads are working, the task thread pool that continues to submit cannot be processed. What kind of reject policy should be implemented.)

It must be clear that maxzsize+queueSize is the maximum number of tasks that can be processed. When the task queue is full, the redundant threads in maxSize will be triggered to process.

2. Why dynamic threading

Recently, I found that thread pools are generally used when processing tasks concurrently, but if   IO intensive tasks or   CPU intensive tasks   Set some parameters according to the online algorithm. Although it is OK, it is not controllable. In case the peak comes or the downstream service cannot be processed, it needs to be increased or reduced. coreSize, do you have to change the code release every time? Therefore, dynamic configuration becomes very important to make the processing power of the program controllable.

After careful evaluation, we still can't guarantee that the appropriate parameters can be calculated at one time. Can we reduce the cost of modifying thread pool parameters, so that at least we can adjust quickly in case of failure, so as to shorten the time of failure recovery? Based on this consideration, whether we can migrate the parameters of the thread pool from the code to the distributed configuration center to realize the dynamic configuration and immediate effectiveness of the thread pool parameters. The comparison of the parameter modification process before and after the dynamic thread pool parameters is as follows.

 

  Direct code:

@Component
public class DynamicExcutorFactory {
    private static final Logger log = LoggerFactory.getLogger(DynamicExcutorFactory.class);

    private static ThreadPoolExecutor executor;
    private static final String CORE_SIZE = "task.coresize";
    private static final String MAX_SIZE = "task.maxsize";

    public DynamicExcutorFactory() {
        Config config = ConfigService.getConfig("application");
        int corePoolSize = config.getIntProperty(CORE_SIZE,10);
        int maximumPoolSize = config.getIntProperty(MAX_SIZE,20);
        init(corePoolSize,maximumPoolSize);
    }

    /**
     * initialization
     */
    private void  init(int corePoolSize,int maximumPoolSize) {
        log.info("init core:{},max:{}",corePoolSize,maximumPoolSize);
        if (executor == null) {
            synchronized (DynamicExcutorFactory.class) {
                executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize,60L, TimeUnit.SECONDS,
                        new LinkedBlockingQueue<>(50), new ThreadFactoryBuilder().setNameFormat("my-thread-%d").build(), new ThreadPoolExecutor.CallerRunsPolicy());
            }
        }
    }
    /**
     * monitor
     */
    @ApolloConfigChangeListener
    private void listen(ConfigChangeEvent changeEvent) {
        if (changeEvent.isChanged(CORE_SIZE)) {
            ConfigChange change = changeEvent.getChange(CORE_SIZE);
            String newValue = change.getNewValue();
            refreshThreadPool(CORE_SIZE, newValue);
            log.info("Core thread changes key={},oldValue={},newValue={},changeType={}", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType());
        }
        if (changeEvent.isChanged(MAX_SIZE)) {
            ConfigChange change = changeEvent.getChange(MAX_SIZE);
            String newValue = change.getNewValue();
            refreshThreadPool(MAX_SIZE, newValue);
            log.info("The maximum number of threads has changed key={},oldValue={},newValue={},changeType={}", change.getPropertyName(), change.getOldValue(), change.getNewValue(), change.getChangeType());
        }

    }

    /**
     * Refresh thread pool
     */
    private void refreshThreadPool(String key, String newValue) {
        if (executor == null) {
            return;
        }
        if (CORE_SIZE.equals(key)) {
            executor.setCorePoolSize(Integer.parseInt(newValue));
            log.info("Modify the number of core threads key={},value={}", key, newValue);
        }
        if (MAX_SIZE.equals(key)) {
            executor.setMaximumPoolSize(Integer.parseInt(newValue));
            log.info("Modify the maximum number of threads key={},value={}", key, newValue);
        }
    }

    public ThreadPoolExecutor getExecutor(){
        return executor;
    }
}

Just write a method to test
 

private void process(List<Long> list) {
        List<List<Long>> parts = Lists.partition(list,3);
        if (CollectionUtils.isNotEmpty(parts)) {
            CountDownLatch latch = new CountDownLatch(parts.size());
            LOGGER.info("parts.size={}", parts.size());
            executor = excutorFactory.getExecutor();
            for (List<Long> part : parts) {
                executor.submit(() -> {
                    try {
                        System.out.println("this is a thread.");
                        Thread.sleep(2000);
                    }catch (Exception e){
                        LOGGER.error("error:",e);
                    } finally {
                        latch.countDown();
                    }
                });
            }
            try {
                latch.await();
            } catch (InterruptedException interruptedException){
                LOGGER.error("InterruptedException error:",interruptedException);
                Thread.currentThread().interrupt();
            } catch (Exception e) {
                LOGGER.error("ConutDownLaunch error:", e);
            }
        }
    }

In this way, every time you adjust the coreSize in Apollo, it will be monitored, and then the coreSize will be dynamically added,

Because the thread pool provides the set method, we can change it dynamically.

1. The first is parameter validity verification.

2. Then overwrite the original value with the passed in value.

3. Judge whether the number of working threads is greater than the maximum number of threads. If greater than, initiate an interrupt request for idle threads.

PS: the maximum coreSize set cannot exceed maxsize. It does not take effect even if it exceeds. The maximum number of threads is also maxsize. If you want to expand the, you can also increase the maxsize.

In this method, we can see that if the number of worker threads is greater than the maximum number of threads, the number of worker threads will be reduced by one, and then null will be returned.

Therefore, the actual process of this place should be to create a new worker thread, and then add one to the number of worker threads. Run the created worker thread to get the task. If the number of working threads is greater than the maximum number of threads, subtract one from the number of working threads. Return null, that is, no task was obtained. Clean up the task and the process ends.

In this way, the number of working threads actually executing tasks has not changed, that is, the maximum number of threads.

When you decrease, you will find that the number of coreSize decreases, but the number of active threads does not change. This is because the maxSize is not reduced. If you want to change it, reduce the maxSize and the number of active threads will be reduced.

However, if you want to dynamically configure the size of the Queue, you can't set it because the capacity is final. Cannot be modified. At this time, you can copy a code, customize a Queue, set the capacity to non final, and add a set method.

After copy modification:

 

So far, we can make a dynamic thread pool. These are the practices of meituan. Just make a configuration page like meituan.

3. Check and fill the gaps in relevant knowledge

There are also some knowledge points posted below in the query data. You can check whether you know and fill in the gaps:

Question 1: are there any threads in the thread pool after it is created? If not, do you know how to preheat the thread pool?

Answer: after the thread pool is created, if there are no tasks coming, there will be no threads in it. If preheating is required, the following two methods can be called:

Start all:

Start only one:

Question 2: will the number of core threads be recycled? What settings are needed?

Answer: the number of core threads will not be recycled by default. If you need to recycle the number of core threads, you need to call the following method:

 

allowCoreThreadTimeOut this value defaults to false.

Topics: Java thread pool apollo