在大多数情况下,线程池的运行状态对于用户来说是一个黑框。运行状态未知,导致生产事故排查困难,线程池参数难以定义。本文围绕线程池监控展开讨论线程。如何监控池,监控指标及监控数据展示存储01如何监控运行数据想象一下,如果要监控线程池的运行数据,你会怎么做?这里有两种大概的思路,在运行时埋点线程池,每次统计运行任务,定时获取线程池的运行数据。这里推荐第二种方式,因为线程池的监控API会通过获取主锁来控制结果的相对准确性,性能相对较差。后面会详细解释为什么叫relative。准确的?因为任务和线程的状态在计算过程中可能会动态变化,所以只能给出一个大概的值,不能保证绝对的准确性。代码privateScheduledThreadPoolExecutorExecutorcollectVesselExecutor;StringcollectVesselTask??Name="client.scheduled.collect.data";collectVesselExecutor=newScheduledThreadPoolExecutor(newInteger(1),ThreadFactoryBuilder.builder().daemon(true).prefix(collectVesselTask??Name).build());//循环调用延迟后initialDelay.scheduleWithFixedDelay每次执行时间为最后一个任务结束时,往后推一个时间间隔一般的线程池有两种创建方式,SpringBean和非SpringBean,假设创建的线程池是由Spring管理的,我们只需要在Spring容器启动成功后,延迟一段时间后开始采集运行数据即可。不管线程池是否由Spring管理,收集的方法公式大致相同,一个取自Spring容器,一个是创建一个线程池,放在自定义容器中。监测指标有哪些?说说Hippo4J目前定义的线程池监控指标,包括但不限于。大家可以讨论业务中使用的监控指标。线程池当前负载:当前线程数/最大线程数线程池峰值负载:当前线程数/最大线程数,运行期间线程池的最大负载数ofcorethreads:threads池中核心线程的数量。最大线程数:线程池限制并发存在的线程数。当前线程数:当前线程池中的线程数。活动线程数:执行任务的线程的大概数量。阻塞队列中的最大线程数:线程池中临时存放任务的容器队列容量:队列中允许的最大元素数队列元素:队列中存储的元素数队列剩余容量:队列中可以存储的元素数线程池任务完成总数:已执行任务的大概总数。拒绝策略执行次数:运行时抛出的拒绝总数。这些指标可以帮助我们解决大部分由线程池引起的问题。然而,事情往往并不完美。当前线程数、活跃线程数、最大线程数、线程池任务完成总数的线程池API会先获取mainLock,然后开始计算。mainLock是线程池的主锁。线程执行,这个锁会用于线程销毁和线程池停止。finalReentrantLockmainLock=this.mainLock;mainLock.lock();try{xxxxx}finally{mainLock.unlock();}如果频繁获取这个锁,会导致原线程池任务执行的性能受到影响。因此,我们应该避免频繁获取这些参数。这也是没有使用线程池任务执行来埋点的最重要原因。03如果监控数据存储上的线程池监控指标只能支持实时查看,无法帮助开发进行日常排查。在大多数场景下,生产问题的发现会被延迟。比如12:30出现问题,13:00业务反馈,为了更好的帮助开发和排查问题,我们需要存储线程池的历史运行数据。说到线程池的历史运行数据的存储,使用时序数据库(TSDB)是最合适的,但是大多数情况下,公司不会为此需求自建或采购时序数据库,所以可以使用折衷方案,比如MySQL、ES等。我们以MySQL为例,his_run_data历史运行数据表,建表语句如下:CREATETABLE`his_run_data`(`thread_pool_id`varchar(56)DEFAULTNULLCOMMENT'线程池ID',`instance_id`varchar(256)DEFAULTNULLCOMMENT'实例ID',`current_load`bigint(20)DEFAULTNULLCOMMENT'当前负载',`peak_load`bigint(20)DEFAULTNULLCOMMENT'峰值负载',`pool_size`bigint(20)DEFAULTNULLCOMMENT'线程数',`active_size`bigint(20)DEFAULTNULLCOMMENT'活动线程数',`queue_capacity`bigint(20)DEFAULTNULLCOMMENT'队列容量',`queue_size`bigint(20)DEFAULTNULLCOMMENT'队列元素',`queue_remaining_capacity`bigint(20)DEFAULTNULLCOMMENT'队列剩余容量y',`completed_task_count`bigint(20)DEFAULTNULLCOMMENT'完成任务数',`reject_count`bigint(20)DEFAULTNULLCOMMENT'拒绝次数',`timestamp`bigint(20)DEFAULTNULLCOMMENT'时间戳',`gmt_create`datetimeDEFAULTNULLCOMMENT'创建时间',`gmt_modified`datetimeDEFAULTNULLCOMMENT'修改时间',PRIMARYKEY(`id`),KEY`idx_group_key`(`tp_id`,`instance_id`)USINGBTREE,KEY`idx_timestamp`(`timestamp`)USINGBTREE)ENGINE=InnoDBAUTO_INCREMENT=1DEFAULTCHARSET=utf8mb4COMMENT='历史运行数据表';可以看到建表语句中有三个关键字段:thread_pool_id:表示当前数据线程池标识instance_id:应用可能部署在集群中,标识集群下唯一的线程池。timestamp:记录线程池运行数据产生时的时间戳。有一个问题。在线线程池不断产生运行数据。表的数据量推到上亿?因为数据是时效性的,经过一定时间后,就不需要占用实时资源了。针对以上问题提供了两种解决方案:假设数据保存1天,如果超过这个时间,直接按照上面的方法删除即可,过期的数据可以保留在备份表中,his_run_data数据可能是删除。有的朋友可能还会担心,数据量太大了查询会不会太慢?我们可以计算一下,假设有100个应用,每个应用部署10个节点。假设数据有效期为1小时,那么输出数据为72万条,即每天1728万条。对于MySQL来说,对于索引的查询在千万级以下的数据是不会有性能瓶颈的。04如何定义公共监督?抽象线程池存储前文提到,线程池收集了各个应用系统中的历史运行数据,是否可以将数据存储和周期删除抽象出来,避免重复工作。如果选择抽象数据存储,客户端节点与服务端的交互如下:客户端定时采集线程池的历史运行数据,将数据打包发送给服务端。服务器端接收客户端上报的数据,并将数据存入数据库中进行持久化存储。定期删除或归档客户端线程池的历史运行数据,服务端对外提供线程池运行图表的数据展示。这里有个小问题。客户端如何打包发送给服务端?定期采集数据后直接上报是否可行?建议将采集和上报两种行为合为一个流程。好的设计应该是职责分离;而且,如果在上报过程中出现网络拥塞等问题,会延迟采集线程的下一次采集结果。我们可以使用多线程生产和消费模型来做。相信刚接触多线程的大家一定学过这个设计//缓冲队列privateBlockingQueue
