For a while, a dynamic thread pool was created, and the source code was put on Github, which suddenly caught fire

Keywords: github Spring REST Java

From: ape world, address:
https://juejin.im/post/5ee9a2e7e51d457423261adb

Explain the background

Thread pool is still used in daily work. When asynchronous and batch processing tasks are needed, we will define a thread pool to handle them.

There are some problems in the process of using thread pool. Here is a brief introduction of some problems encountered before.

Scenario 1: realize some batch data processing functions. At the beginning, the number of core threads in the thread pool is set to be relatively small. If you want to adjust, you can only restart the application after the modification.

Scenario 2: there is a task processing application that receives MQ messages for task processing, and the queue of thread pool also allows to cache a certain number of tasks. When the task processing is very slow, it is not very convenient to see how many tasks have not been processed. At that time, in order to be fast and convenient, a thread was directly started to cycle the print thread pool queue size.

Just before I had a thread pool application in the official account,
mp.weixin.qq.com/s/tIWAocevZ…) , I think their idea is very good, that is, there is no open source, so I took the time to add a component of dynamic thread pool in my open source project Kitty, which supports Cat monitoring, dynamic change of core parameters, task accumulation alarm, etc. Today, I'd like to share with you the way of implementation.

Project source address:
https://github.com/yinjihuan/kitty[1]

How to use

Add dependency

The components that depend on the thread pool are not published by Kitty at present. You need to download the source code install locally or privately.

<dependency>
    <groupId>com.cxytiandi</groupId>
    <artifactId>kitty-spring-cloud-starter-dynamic-thread-pool</artifactId>
</dependency>

Add configuration

Then configure the thread pool information in Nacos, which integrates Nacos. It is recommended to create a separate thread pool configuration file for an application. For example, this is called dataId
kitty-cloud-thread-pool.properties , group is BIZ_GROUP.

The contents are as follows:

kitty.threadpools.nacosDataId=kitty-cloud-thread-pool.properties
kitty.threadpools.nacosGroup=BIZ_GROUP
kitty.threadpools.accessToken=ae6eb1e9e6964d686d2f2e8127d0ce5b31097ba23deee6e4f833bc0a77d5b71d
kitty.threadpools.secret=SEC6ec6e31d1aa1bdb2f7fd5eb5934504ce09b65f6bdc398d00ba73a9857372de00
kitty.threadpools.owner=Yin Jihuan
kitty.threadpools.executors[0].threadPoolName=TestThreadPoolExecutor
kitty.threadpools.executors[0].corePoolSize=4
kitty.threadpools.executors[0].maximumPoolSize=4
kitty.threadpools.executors[0].queueCapacity=5
kitty.threadpools.executors[0].queueCapacityThreshold=5
kitty.threadpools.executors[1].threadPoolName=TestThreadPoolExecutor2
kitty.threadpools.executors[1].corePoolSize=2
kitty.threadpools.executors[1].maximumPoolSize=4

nacosDataId,nacosGroup

When listening to configuration modification, you need to know which DataId to listen to. The value is the DataId of the current configuration.

accessToken,secret

Verification information of nail robot, used for alarm.

owner

The person in charge of this application will be displayed in the alarm message.

threadPoolName

The name of the thread pool. You need to pay attention to it when using it.

The rest of the configuration will not be introduced one by one, which is consistent with the internal parameters of the thread pool, and some can be seen from the source code.

Injection use

@Autowired
private DynamicThreadPoolManager dynamicThreadPoolManager;
dynamicThreadPoolManager.getThreadPoolExecutor("TestThreadPoolExecutor").execute(() -> {
    log.info("Use of thread pool");
    try {
        Thread.sleep(30000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}, "getArticle");

Get the thread pool object through getThreadPoolExecutor method of DynamicThreadPoolManager, and then pass in Runnable, Callable, etc. The second parameter is the name of the task. The reason to extend a parameter is that if the task is not identified, the task cannot be distinguished.

By default, this thread pool component integrates Cat dotting and sets the name to view the monitoring data related to this task on Cat.

Extended functions

Task performance monitoring

In Cat's Transaction report, the type is the name of the thread pool.

 

Details are displayed with the name of the task.

 

Dynamic modification of core parameters

At present, core parameters only support the modification of corePoolSize, maximumPoolSize, queueCapacity (the queue type is LinkedBlockingDeque, which can be modified), rejectedExecutionType, keepAliveTime, unit.

Generally, corePoolSize, maximumPoolSize, queueCapacity are the most frequently changed dynamically.

If it needs to be changed, you can directly modify the corresponding configuration value in Nacos. The client will listen for the configuration change, and then synchronously modify the parameters of the first thread pool.

Queue capacity alarm

queueCapacityThreshold is the threshold value of queue capacity alarm. If the number of tasks in the queue exceeds queueCapacityThreshold, an alarm will be given.

 

Reject times alarm

When the queue capacity is full, the new tasks will choose the corresponding processing method according to the rejection policy set by the user. If the AbortPolicy policy policy is adopted, an alarm will also be given. It's equivalent to that consumers are overloaded.

 

Thread pool operation

The bottom layer is connected to Cat, so the running data of the thread is reported to Cat. We can view this information in Cat.

 

If you want to show it on your own platform, I have exposed the / Actor / thread pool endpoint here, and you can pull data by yourself.

{
	threadPools: [{
		threadPoolName: "TestThreadPoolExecutor",
		activeCount: 0,
		keepAliveTime: 0,
		largestPoolSize: 4,
		fair: false,
		queueCapacity: 5,
		queueCapacityThreshold: 2,
		rejectCount: 0,
		waitTaskCount: 0,
		taskCount: 5,
		unit: "MILLISECONDS",
		rejectedExecutionType: "AbortPolicy",
		corePoolSize: 4,
		queueType: "LinkedBlockingQueue",
		completedTaskCount: 5,
		maximumPoolSize: 4
	}, {
		threadPoolName: "TestThreadPoolExecutor2",
		activeCount: 0,
		keepAliveTime: 0,
		largestPoolSize: 0,
		fair: false,
		queueCapacity: 2147483647,
		queueCapacityThreshold: 2147483647,
		rejectCount: 0,
		waitTaskCount: 0,
		taskCount: 0,
		unit: "MILLISECONDS",
		rejectedExecutionType: "AbortPolicy",
		corePoolSize: 2,
		queueType: "LinkedBlockingQueue",
		completedTaskCount: 0,
		maximumPoolSize: 4
	}]
}

Custom reject policy

Usually, we can use code to create a thread pool to customize the rejection policy, which can be passed in when constructing a thread pool object. Since the creation of thread pool is encapsulated, we can only configure the name of rejection policy in Nacos to use the corresponding policy. By default, four kinds of CallerRunsPolicy, AbortPolicy, DiscardPolicy and DiscardOldestPolicy can be configured.

If you want to customize it, it is also supported. The definition method is the same as before, as follows:

@Slf4j
public class MyRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        log.info("Come in.........");
    }
}

To make this policy effective, the SPI method is used. You need to create a META-INF folder under resources, then create a services folder, and then create a service folder
java.util.concurrent.RejectedExecutionHandler, which contains the full path of the class you defined.

 

Custom alarm mode

By default, the alarm mode of the nail robot is integrated internally. You can turn it off if you don't want to use it. Or connect the alarm information to your monitoring platform.

If there is no alarm platform, new alarm methods can be implemented in the project, such as SMS.

Just implement the ThreadPoolAlarmNotify class.

/**
 * Custom SMS alert notification
 *
 * @By Yin Jihuan
 * @Personal wechat
 * @WeChat official account
 * @GitHub https://github.com/yinjihuan
 * @About the author http://cxytiandi.com/about
 * @Time: 22:26, May 27, 2020
 */
@Slf4j
@Component
public class ThreadPoolSmsAlarmNotify implements ThreadPoolAlarmNotify {
    @Override
    public void alarmNotify(AlarmMessage alarmMessage) {
        log.info(alarmMessage.toString());
    }
}

code implementation

I won't talk about it in detail. The source code is in
https://github.com/yinjihuan/kitty/tree/master/kitty-dynamic-thread-pool[2] It's not complicated to see it by yourself.

Create thread pool

Create a thread pool according to the configuration. The ThreadPoolExecutor is customized because it needs to be embedded in Cat.

/**
 * Create thread pool
 * @param threadPoolProperties
 */
private void createThreadPoolExecutor(DynamicThreadPoolProperties threadPoolProperties) {
    threadPoolProperties.getExecutors().forEach(executor -> {
        KittyThreadPoolExecutor threadPoolExecutor = new KittyThreadPoolExecutor(
                executor.getCorePoolSize(),
                executor.getMaximumPoolSize(),
                executor.getKeepAliveTime(),
                executor.getUnit(),
                getBlockingQueue(executor.getQueueType(), executor.getQueueCapacity(), executor.isFair()),
                new KittyThreadFactory(executor.getThreadPoolName()),
                getRejectedExecutionHandler(executor.getRejectedExecutionType(), executor.getThreadPoolName()), executor.getThreadPoolName());
        threadPoolExecutorMap.put(executor.getThreadPoolName(), threadPoolExecutor);
    });
}

Refresh thread pool

First of all, you need to monitor the modification of Nacos.

/**
 * Listening configuration modification, spring cloud Alibaba version 2.1.0 does not support @ NacosConfigListener listening
 */
public void initConfigUpdateListener(DynamicThreadPoolProperties dynamicThreadPoolProperties) {
    ConfigService configService = nacosConfigProperties.configServiceInstance();
    try {
        configService.addListener(dynamicThreadPoolProperties.getNacosDataId(), dynamicThreadPoolProperties.getNacosGroup(), new AbstractListener() {
            @Override
            public void receiveConfigInfo(String configInfo) {
                new Thread(() -> refreshThreadPoolExecutor()).start();
                log.info("Thread pool configuration changed, refresh completed");
            }
        });
    } catch (NacosException e) {
        log.error("Nacos Configuration listening exception", e);
    }
}

Then refresh the parameter information of the thread pool. Since the configuration has not been refreshed at this time when the listening event is triggered, I waited for 1 second for the configuration to complete the refresh and then take the value directly from the configuration.

Although a little frustrating, it can still be used. In fact, the better way is to parse the configInfo of receiveConfigInfo. configInfo is the whole configuration content after the change. Because it's not easy to parse it into a property file, I didn't do it. I'll change it later.

/**
 * Refresh thread pool
 */
private void refreshThreadPoolExecutor() {
    try {
        // Wait for configuration refresh to complete
        Thread.sleep(1000);
    } catch (InterruptedException e) {
    }
    dynamicThreadPoolProperties.getExecutors().forEach(executor -> {
        ThreadPoolExecutor threadPoolExecutor = threadPoolExecutorMap.get(executor.getThreadPoolName());
        threadPoolExecutor.setCorePoolSize(executor.getCorePoolSize());
        threadPoolExecutor.setMaximumPoolSize(executor.getMaximumPoolSize());
        threadPoolExecutor.setKeepAliveTime(executor.getKeepAliveTime(), executor.getUnit());
        threadPoolExecutor.setRejectedExecutionHandler(getRejectedExecutionHandler(executor.getRejectedExecutionType(), executor.getThreadPoolName()));
        BlockingQueue<Runnable> queue = threadPoolExecutor.getQueue();
        if (queue instanceof ResizableCapacityLinkedBlockIngQueue) {
            ((ResizableCapacityLinkedBlockIngQueue<Runnable>) queue).setCapacity(executor.getQueueCapacity());
        }
    });
}

Other refreshes are provided by the thread pool itself. It should be noted that the refresh of the thread pool queue size only supports the LinkedBlockingQueue queue. Because the LinkedBlockingQueue size is not allowed to be modified, according to the idea provided in the article of the United States group, a user-defined queue that can be modified is actually the LinkedBlockingQueue Copy a copy of the code of. Just change it.

Report operation information to Cat

The code for uploading data to Cat's Heartbeat report is as follows. It is mainly Cat itself that provides the extended ability. You only need to call the following method to report data at a fixed time.

public void registerStatusExtension(ThreadPoolProperties prop, KittyThreadPoolExecutor executor) {
    StatusExtensionRegister.getInstance().register(new StatusExtension() {
        @Override
        public String getId() {
            return "thread.pool.info." + prop.getThreadPoolName();
        }
        @Override
        public String getDescription() {
            return "Thread pool monitoring";
        }
        @Override
        public Map<String, String> getProperties() {
            AtomicLong rejectCount = getRejectCount(prop.getThreadPoolName());
            Map<String, String> pool = new HashMap<>();
            pool.put("activeCount", String.valueOf(executor.getActiveCount()));
            pool.put("completedTaskCount", String.valueOf(executor.getCompletedTaskCount()));
            pool.put("largestPoolSize", String.valueOf(executor.getLargestPoolSize()));
            pool.put("taskCount", String.valueOf(executor.getTaskCount()));
            pool.put("rejectCount", String.valueOf(rejectCount == null ? 0 : rejectCount.get()));
            pool.put("waitTaskCount", String.valueOf(executor.getQueue().size()));
            return pool;
        }
    });
}

Define thread pool endpoint

The configuration and operation of the thread pool can be exposed by customizing the endpoint, and the external monitoring system can pull data for corresponding processing.

@Endpoint(id = "thread-pool")
public class ThreadPoolEndpoint {
    @Autowired
    private DynamicThreadPoolManager dynamicThreadPoolManager;
    @Autowired
    private DynamicThreadPoolProperties dynamicThreadPoolProperties;
    @ReadOperation
    public Map<String, Object> threadPools() {
        Map<String, Object> data = new HashMap<>();
        List<Map> threadPools = new ArrayList<>();
        dynamicThreadPoolProperties.getExecutors().forEach(prop -> {
            KittyThreadPoolExecutor executor = dynamicThreadPoolManager.getThreadPoolExecutor(prop.getThreadPoolName());
            AtomicLong rejectCount = dynamicThreadPoolManager.getRejectCount(prop.getThreadPoolName());
            Map<String, Object> pool = new HashMap<>();
            Map config = JSONObject.parseObject(JSONObject.toJSONString(prop), Map.class);
            pool.putAll(config);
            pool.put("activeCount", executor.getActiveCount());
            pool.put("completedTaskCount", executor.getCompletedTaskCount());
            pool.put("largestPoolSize", executor.getLargestPoolSize());
            pool.put("taskCount", executor.getTaskCount());
            pool.put("rejectCount", rejectCount == null ? 0 : rejectCount.get());
            pool.put("waitTaskCount", executor.getQueue().size());
            threadPools.add(pool);
        });
        data.put("threadPools", threadPools);
        return data;
    }
}

Cat monitors the execution time of threads in the thread pool

Originally, monitoring was placed in the execute and submit methods of KittyThreadPoolExecutor. After the test, we found that there was a problem. The data did exist on Cat, but the execution time was 1 millisecond, that is, it didn't work.

Needless to say, we all know that because threads are executed separately later, it is meaningless to bury a point where tasks are added.

Later, I came up with a way to realize the function of embedding point, which is to use the two methods of beforeExecute and afterExecute provided by thread pool, which will trigger before and after thread execution.

@Override
protected void beforeExecute(Thread t, Runnable r) {
    String threadName = Thread.currentThread().getName();
    Transaction transaction = Cat.newTransaction(threadPoolName, runnableNameMap.get(r.getClass().getSimpleName()));
    transactionMap.put(threadName, transaction);
    super.beforeExecute(t, r);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
    super.afterExecute(r, t);
    String threadName = Thread.currentThread().getName();
    Transaction transaction = transactionMap.get(threadName);
    transaction.setStatus(Message.SUCCESS);
    if (t != null) {
        Cat.logError(t);
        transaction.setStatus(t);
    }
    transaction.complete();
    transactionMap.remove(threadName);
}

You can read the following code yourself, and this article is over here. If you feel that this article is still good remember to forward next Oh! Thank you very much.

Posted by knetcozd on Wed, 17 Jun 2020 20:45:16 -0700