APP下载

大型数据场景中定时任务系统研究

2018-12-22尹隆波

电脑知识与技术 2018年33期
关键词:负载均衡

尹隆波

摘要:大型数据运用场景中存在数据进行关联或更新操作的需求,在实际应用中通过单独部署定时任务系统周期性去统计或更新数据。由于庞大而复杂的数据,定时任务系统中的任务运行起来耗时很长而且会过多地消耗系统的资源,对系统的性能造成很大的影响。另外,复杂的定时任务系统中出现问题很难去排查并进行分析。基于以上问题,论文通过合理的利用多线程技术、采用增量更新数据、合理設置不同子定时任务的运行时间和周期来提高系统处理速度,并且采用扩展ThreadPoolExecutor统计线程运行情况、给每个线程设置有意义的名称以方便系统中的问题排查分析。

关键词: 高并发;定时任务;线程池;负载均衡;JAVA

中图分类号:TP311 文献标识码:A 文章编号:1009-3044(2018)33-0025-03

大型数据运用场景中,通常需要展示一个对象关联的其他类型对象的数量,并且其关联的其他类型数据出于不断地动态地增加中。通常巨量的对象数据存储在如Solr、ElaticSearch等非关系型数据库中。非关系型数据中,多个表之间没有提供直接的关联操作。运行单独的定时统计任务来计算对象关联的其他类型对象数量以及动态增加新关联的对象,是处理上述问题的一种实用解决方案。一方面,定时任务能够及时统计或更新关联数据满足客户的展示需要;另一方面,利用定时任务进行统计,能够大幅度减少页面过程中后台程序复杂计算的时间,使得页面加载的时长控制在用户接受范围内。

基于Java 开发的Web系统,可以采用JDK提供的Shedular框架运行定时任务,使用简单并且高效。为了提高定时任务的运行效率、以及方便问题定位分析,论文将根据实际应用中的解决办法从这两个目标进行阐述。

1 提高系统处理速度

大型数据运用场景中,通常定时任务系统需要处理的数据量巨大,为了提高定时任务系统的处理速度,可以从以下三方面考虑。一是利用多线程技术进行并发处理;二是采用增量更新数据的策略;三是合理地设置定时任务数量,以及合理安排多个子定时任务的运行时间和运行周期。

1.1 多线程并发处理

为了提高系统数据处理的速度,在多CPU的服务器上,可以采用多线程技术充分利用多个CPU的计算能力。通常开发者可以将一个定时任务,拆分成可以独立运行的若干个细小子任务,然后为每一个子任务单独创建个线程运行。如果拆分的子定时任务数量过多,系统中运行的线程数量也会过多,由于每个线程的创建需要一定的系统资源和时间开销,大量的线程会降低系统的响应速度和性能。因此,需要合理地设置系统中运行的线程数量。简单来说,Java并发有两种类型:计算密集型并发和IO密集型并发。

1) 计算密集型并发

计算密集型,从名称的定义上来看,就是一个应用需要数量庞大的CPU资源进行数据计算操作。进入21世纪后,信息技术步入了多核CPU服务器时代,让每一个CPU核心都参与数据计算,将多核CPU的性能充分利用起来,这样才能最高效率地使用服务器硬件资源。在装载成百上千CPU的服务器配置上运行单线程任务,是对宝贵的硬件资源的最大浪费。对于计算密集型的应用,主要是靠CPU的运算能力来工作,消耗CPU资源,大部分时间用来运行计算、逻辑判断等CPU动作,比如计算圆周率、对视频进行高清解码等,通常这些应用访问I/O设备频率较低。为了发挥CPU密集并发应用的性能优势,需要尽量避免过多的线程频繁地进行上下文切换,一种比较比合理的方案是:线程数量 = CPU核数 + 1。

另外一种较为理想的方法是:线程数量 = CPU核数 * 2。除了考虑CPU的内核数量,另外还要参考JDK的使用版本,以及CPU的硬件配置(现在部分服务器的CPU支持超线程机制)。JDK 1.8后,JAVA默认支持并行计算,因此,基于JDK1.8或更高的JAVA版本,计算密集型应用的合理方案是:线程数 = CPU核数 * 2。可以通过JDK提供的接口从运行的JVM环境中获取服务器的CPU数量。

2) IO密集型并发

顾名思义,IO密集型的应用,大部分的时间在等待IO读写操作上,而CPU占用率并不高。目前大部分软件项目都是Web应用,一方面涉及大量而频繁的网络信息传输,另一方面,程序应用与数据库系统、与缓存间的数据交互也需要大量的IO读写。一旦发生IO读写操作,线程会一直等待IO操作的运行,直到IO操作完成、数据准备好后,线程才会继续执行后续步骤。通常IO读写操作时间较长(主要受到磁盘读写速度和网络传输带宽的限制),这时线程处于等待状态,而CPU处于空闲状态, 此时可以运行其他非等待状态的线程,处理其他的工作,从而提高系统的并发量。因此对于IO密集型的应用,应该将同时运行的线程数量设置大些。

但同时运行的线程数量也不能设置过大,线程数量应当每个子任务处于阻塞状态的时间比例相关。假设每个子任务平均的50%时间处于阻塞状态,那么定时任务系统同时运行的线程数量应当是服务器CPU核数的两倍。换而言之,应用中的线程数量应当同CPU的数量成正比,而和每个子任务IO的平均阻塞时间占比率成反比。基于CPU和阻塞时间占比,开发人员可以计算出程序所需的线程数。对于IO密集型应用,目前归纳出了一套公式:线程数量=CPU核数/(1-阻塞系数)。

公式中的阻塞系数一般在0.8和0.9之间取值,也可以是0.8或者0.9。基于上述公式,对于一个密集型应用系统,假设服务器CPU数量为16,则其较为合适的线程数量应当是80到160之间,但这也不是绝对的,可以根据系统的实际情况以及运行状况来合理调整。

1.2 增量更新数据

在一些大数据运用场景中,上游应用中的信息数据每时每刻都在发生变化,而依赖这些数据的下游系统需要运行定时任务系统来周期性刷新这些变化的数据。当数据量小时候,可以采用简单粗暴的方式—每次任务进行全量的数据更新。但随着系统业务的快速增长,应用中的数据量成几何方式增长时,定时任务系统中的工作将极为耗时并且占用大量IO硬件资源。这种方式下,每个周期都需要从数据库服务器上通过IO读写或同上游进行数据的网络传输,获得全量的数据。当传输的数据量过大时,IO操作耗费的时间会占比很大,并大量占用系统的IO资源,而IO能力通常是系统的瓶颈所在。

此时,可以通过采购更多以及性能更好的硬件资源来处理不断快速增长的数据。通常,这是这是一种不太可取的奢侈方案。不过可以转换一种思路,将全量更新机制替换为增量更新。虽然数据量总量十分庞大,但一段时间内发生变化的数据量一般是有限的,定时任务系统在每个周期内只需增量处理周期内更新的数据即可,这样可以很大程度上降低定时任务系统的IO时间开销,并节省宝贵的IO资源。

增量更新的基础是全量数据,最初可以采用某种方式先把全量数据拷贝存入业务系统。然后运行定时任务去周期性更新一段时间内增加或变化的数据。增量更新,一般是读取某个时刻(更新时间)或者检查点(checkpoint)之后的信息数据来处理,而不是粗暴的读取全量数据。虽然增量更新会造成一定的额外空间开销,需要对每条数据记录采用时间或checkpoint标识和记录更新点,但从整体上考虑,这是十分必要的。

1.3 合理设置定时任务数量,合理安排运行时间

大型数据运用场景中,需要进行统计或增量更新的对象类型繁多,定时任务处理的事情庞大而复杂。这种场景下,应当避免将所有的功能逻辑放在一个大而全的定时任务中,而是合理地拆分一个大而全的定时任务,将定时任务细分成若干个独立、互不影响的小的定时任务,每个定时任务处理同一种或几种相互关联类型的数据。一方面可以提高系统并行的处理速度,另一方面,可以在不同的时间段内运行合适的定时任务从而实现时间概念上的负载均衡。

将大的任务细分后,一些任务执行周期较短的,如两三个小时左右的,可以考虑放在系统业务不繁忙的时候触发,如凌晨两三点,避免与正在运行的关键业务系统竞争IO、CPU等资源。另外,尽量将不同定时任务的运行时间错开,降低一段时间内的系统负荷,尤其是业务繁忙的时间段。同时,需要合理地考虑每个子定时任务运行的周期。首先,任务周期需要超过通常情况下该任务运行的最长时间;其次,定时任务的周期需要满足大部分数据及时性的要求。

2 方便问题定位分析

复杂而庞大的定时任务系统中,开发人员经常要使用JDK提供的ThreadPoolExecutor框架类创建线程池来执行大量的定时任务,通过线程池的并发特性来提高定时任务系统的吞吐量。在程序开发过程中,合理地运用线程池技术有以下三方面优点。一是:降低系统资源的消耗。通过循环利用已经创建的线程减少重新构造线程和销毁线程造成的资源消耗。二:提高系统的响应速度。新的任务提交后,新任务直接利用已存在的线程就可以立刻执行。三:提高线程的可管理性。使用线程池技术可以对线程这一稀缺资源进行统一分配、调优和监控。

JDK提供了线程池技术,为线程创建、销毁的开销问题和系统资源不足问题提供了很好的解决方案, 能有效地提高系统的响应速度和服务系统的整体性能。但默认的线程池框架在線程运行情况统计、线程名称展示存在不尽人意的地方。基于Thread PoolExecutor类存在的这两个不足,论文提出了相应的解决办法。

2.1 扩展Thread Pool Executor统计运行情况

线程池使用不当也会使服务器资源枯竭,导致异常情况的发生,比如固定线程池的阻塞队列任务数量过多、内存溢出、系统假死等问题。如果在定时任务系统中大量地使用线程池,则很有必要对运行的线程池进行必要的监控,以便系统出现问题时,可以根据线程池的运行状况快速定位问题原因,并分析问题。因此,定时任务系统中需要一种简单而有效的监控方案来监控线程池的使用情况,比如完成任务数量、线程池大小、每个线程的运行时间线程池中运行的线程数量等信息。

基于以上的功能要求,在定时任务系统中可以对ThreadPoolExecutor类进行一定的扩展,在线程池中实现简单的监控功能,并实时将线程池运行状况信息打印到日志中,方便技术人员进行问题排查、系统调优。具体的思路如下:通过StatThreadPoolExecutor类继承ThreadPoolExecutor类来自定义线程池,重写默认线程池的shutdown、shutdownNow、beforeExecute和afterExecute方法来统计线程池的执行情况,这几个方法是ThreadPoolExecutor类预留给程序开发人员进行扩展的方法,具体如下:

shutdown方法:线程池延迟关闭时(这一方法需要等待线程池里的所有任务都执行完毕),统计已执行完毕的任务数量、正在运行的任务数量、还未执行的任务数量。shutdownNow方法:线程池立即关闭时(这一方法无需等待线程池里的所有任务都执行完毕),统计已执行完毕的任务数量、正在运行的任务数量、还未执行的任务数量。beforeExecute(Thread t, Runnable r)方法:添加在任务执行之前,记录任务运行开始的时间,通过Map保存每个任务运行的开始时间,以任务的hashCode为key值,开始时间为value值。afterExecute(Runnable r, Throwable t)方法:任务执行之后,计算任务结束时间。统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、已完成任务数量、任务总数、队列里缓存的任务数量、池中存在的最大线程数、最大允许的线程数、线程空闲时间、线程池是否已经关闭、线程池是否已经终止信息。

在StatThreadPoolExecutor类构造方法中,需要继承ThreadPoolExecutor的构造方法,但可以另外增加一个参数,用来传入线程池名称参数,该参数主要是用来给线程池定义一个与业务相关并有具体意义的线程池名字,方便进行系统的运维工作人员排查线上问题。

并发系统会依赖beforeExecute和afterExecute这两个方法统计的信息。有了这些线程池运行状态信息之后,开发人员可以根据业务情况和统计的线程池的信息合理地调整线程池大小,并根据每个任务耗时长短对任务进行性能分析。同时,可以发现执行时间过长的线程,然后可以根据具体程序分析这些任务耗时较多的原因,对定时任务程序进行优化,从而提高相应的子定时系统的执行速度。在生产环境中,应当尽量避免使用shutdown和shutdownNow方法,因为调用这两个关闭线程池的方法之后,线程池会被关闭,不再接收新的任务。定时任务中需要判断线程池内的线程是否已全部执行完毕,在线程数量固定的情况下可以使用并发工具包中的CountDownLatch来进行判断,在线程数量不固定的情况下,CountDownLatch就不再适用。这种场景下,可以通过判断线程池中的等待队列、和运行状态为active的线程数量来确定线程池中所有线程是否已执行完毕。

2.2 定制线程名称

Web项目中使用ThreadPoolExecutor进行多线程开发,使用起来很方便,但当运维的工作人员查看堆栈信息或者调试性能的时候,看到的线程名称默认都是pool-1-thread-1\2\3\4之类。如果系统中用到了多个线程池,排查系统故障时就无法区分是哪个线程产生的问题。使用JAVA默认的线程池技术,线程的每一次运行都是不同的执行结果,如果在排查问题时要想区分每一个线程,应当给每一个运行的线程任务定义有意义的名称,另外线程的名字应当在线程启动之前进行定义。

在ThreadPoolExecutor中所有的线程都是以pool-开头的,因为线程池工厂中对线程设置的名字是固定格式的。如果需要修改默认的线程名称,需要扩展自行定义并线程工厂ThreadFactory,在定制的线程工厂中重新设置线程运行时的名称。还有一种方法,线程在被调用后才会执行run方法,run方法的执行表示这个任务真正被线程运行了,这时线程的名称也就确定了,所以可以在任务线程的run方法中第一句添加定义线程名称的语句。

3 总结

大数据运用场景的定时任务系统中,使用多线程并发技术能大幅度提高系统的处理速度。在使用多线程机制的过程中,需要判断系统各个模块是IO密集型还是CPU密集型,然后合理地设置同时运行的线程数目。在巨量的信息数据系统中,粗暴地使用全量数据来更新对定时任务系统来说是个很难完成的任务,可以通过增加时间戳的方式,在每个周期内增量更新一段时间内的关联数据。复杂的定时任务系统中一般拆分成多个子定时任务,应该合理地错开不同定时任务的运行时间以及运行周期,从而有效地降低系統的负载。

合理地运用线程池技术可以降低系统资源的消耗、提高系统的响应速度和提高线程的可管理性。但默认的线程池框架没有提供线程运行情况统计、线程名称展示的功能。通过扩展线程池ThreadPoolExecutor,开发人员可以获取线程池的运行调度内部细节,对并发运行的定时任务系统进行故障排查、问题分析很有帮助。此外,开发过程中应当给每一个运行的线程任务定义有意义的名称,以便在排查问题时很轻易地区分每一个线程。

参考文献:

[1] 任冬艳, 廖建新, 王纯. 基于 JDK1. 5 的定时任务执行方案的改进[J]. 计算机系统应用, 2008(2):115-118.

[2] 丰洪才, 邓华来, 刘年波. 定时任务在智能数码监控系统中的应用[J]. 武汉工业学院学报, 2004, 23(3): 1-3.

[3] 王华, 马亮, 顾明. 线程池技术研究与应用[J].计算机应用研究,2005,22(11):141-142.

[4] 吴健. 基于并发控制机制的 Web 系统的开发技术研究[D]. 云南大学, 2015.

[5] 吴高阳. 基于 NIO 的 Java 高性能网络技术的研究与应用[D]. 西安: 西安电子科技大学, 2014.

[6] 周博. WEB 服务器负载均衡系统设计与实现[D]. 电子科技大学, 2014.

[7] 杨开杰, 刘秋菊, 徐汀荣. 线程池的多线程并发控制技术研究[J]. 计算机应用与软件, 2010 (1): 168-170.

[8]唐颖峰, 陈世平. 一种面向分布式数据流的闭频繁模式挖掘方法[J]. 计算机应用研究, 2015, 32(12):3560-3564.

[9] 王有为, 王伟平, 孟丹. 基于统计方法的 Hive 数据仓库查询优化实现[J]. 计算机研究与发展, 2015, 52(6): 1452-1462.

[10] 麻双克, 周兰凤. 云计算环境下基于优先级的 IO 和网络密集型应用调度策略[J]. 上海理工大学学报, 2017, 39(5): 505-510.

【通联编辑:唐一东】

猜你喜欢

负载均衡
异构环境下改进的LATE调度算法