一、背景
天画项目的数据工厂目前在与xxl-job对接自动化数据生成任务,另外我司也在使用该组件做业务,所以想深入了解下XxlJob。在跟进了社区的github等仓库issue发现开发迭代停滞了一段时间,思来想去准备开个下游分支做一些性能优化和特性开发等,于是fork了下源码,将其作为天画社区关于任务调度的组件来使用。
我花了几天阅读源码,同时做了一些简单的调试,发现了一些问题之后着手进行优化,本文将从XxlJob的原理出发深入其性能瓶颈,并提出一些解决方案。相关优化的代码已经上传至gitee.
二、调度流程解读
在进行性能和安全改造之前,需要对xxljob的调度执行原理要非常了解,所以这里我们先简单看一下xxljob的调度流程和线程模型。
2.1 调度流程
- 服务端启动流程
- 客户端启动流程
- 客户端与服务端交互流程
2.2 调度线程模型
通过上面的流程图我们可以梳理出如下服务端和客户端用到的线程和线程池,具体作用通过名称即可了解。
服务端:
- 触发器线程池(fastTriggerPool,slowTriggerPool)
- 注册&停止注册线程池(registryOrRemoveThreadPool)
- 注册监听线程(registryMonitorThread)
- 任务失败监听线程(monitorThread)
- 任务完成回调线程池(callbackThreadPool)
- 任务完成回调监听线程(monitorThread)
- 统计日志线程(logrThread)
- 调度线程(scheduleThread)
- 时间轮线程(ringThread)
客户端:
- http 交互监听线程(thread)
- 注册器线程(registryThread)
- 任务线程(JobThread)
- 任务回调日志线程(triggerCallbackThread)
- 回调日志重试线程(triggerRetryCallbackThread)
2.3 客户端API
这里需要说明的是,在一定程度上客户端(也就是执行器,以下统称为客户端)也会作为服务端接受调度中心(即xxl job server 服务端)的回调,所以这里是一个比较令人迷惑的地方。
这里列出服务端调用客户端的API
127.0.0.1:9999/beat
127.0.0.1:9999/idleBeat
127.0.0.1:9999/run
127.0.0.1:9999/kill
127.0.0.1:9999/log
2.4 服务端API
这里列出客户端调用服务端的API
127.0.0.1:8080/api/callback
127.0.0.1:8080/api/registry
127.0.0.1:8080/api/registryRemove
三、潜在问题
在反复看了xxljob 开源社区gitee/github上的issue之后,我对下面的几个问题比较关注,所以在业余时间中专门调试并且进行了一定的复现,这里简单回顾一下xxljob在高并发调度过程中可能产生的问题。
3.1 重复调度
重复调度的问题其实是偶发问题,问题的现象或者特征就是一个任务在一次调度中重复执行了两次,同时产生了 两条xxljob log,发生时间相同。
3.2 安全问题
xxljob控制台查询客户端日志的时候会返回accessToken,github上的issue讨论已经提供了复现步骤,这里目前作者已经提供升级,但是本文的重点不止于此,因为另外的安全问题就是accessToken在客户端与服务端之间的交互是明文传输的,另外也是服务端与所有客户端共用的,一旦泄露其实会比较危险。
3.3 并发调度变慢
XxlJob的调度在并发变高的时候从日志上可以看出调度会有一些延迟,出现这个问题的原因有以下几个方面:
- 一秒内调度量比较多,对客户端和服务端在一秒内会产生上千次调度
- 调度粒度是线程(JobThread)和单次粒度的,所以一秒内要处理2-3倍的数据库IO和网络IO
- 选用了busyover或者failover的调度策略,这两种需要在调度之前给对应的客户端发送心跳
- 日志处理,这里包括客户端的日志处理和服务端的日志处理,在并发比较高的情况下日志产生的量也比较大,
对调度业务会产生一定的资源占用。
3.4 线程模型的性能瓶颈
这个问题其实与3.3类似,从上面的调度流程图可以看到xxljob其实是对每个xxl_job_info在对应的客户端上构建一个JobThread,如果执行器对应的任务比较多且存在一秒内并发触发的话就可能导致客户端本身出现性能问题。
3.5 回调并发变慢
这个问题也是高并发调度下产生的问题,xxljob在日志处理上有两种方式:一种是在调度job方法内通过XxlJobHelper.log去记录产生的业务日志,这部分是存在于客户端的,另外一种则是调度日志,即对该job在当前调度结果的日志信息推送到服务端做记录或者统计,这部分数据存储在服务端的xxl_job_log表中。
在高并发调度的情况下,客户端的处理存在性能问题,一次回调推送的日志太少,客户端存在积压。
3.6 分页性能问题
在高并发或者大量任务的场景中xxlJob的管理控制台在任务管理和调度日志分页上存在性能问题,可能导致页面加载缓慢同时服务端进程崩溃,进而影响服务运行。
四、优化点
4.1 认证优化
在xxl job的源码中需要借助xxl.job.accessToken来完成客户端与服务端的认证,这个配置在客户端注册到服务端,以及客户端和服务端在相互调用过程中都会进行认证。由于配置是统一的,所以所有客户端都需要配置这个,另外认证过程中是accessToken是明文传输的,所以这种认证机制一定程度上是比较弱的。另外如果改动认证机制的话则客户端和服务端都要改,本次优化则花一定的精力来提升安全性。总结起来就是如下几点:
- 强制安全认证
客户端或者服务端不配置accessToken则启动失败
- 认证加密
客户端与服务端之间的交互均通过加密accessToken的方式
- 弱化accessToken在认证过程中的影响
对于每个客户端虽然都配置同样的accessToken,但是新生成的accessToken(原accessToken+appName)则是通过AES进行加密。
这里我们看一下进行强认证过程的时序图:
下面我们看一下具体实现源码:
客户端:
private String initEmbedServer(String address, String ip,int port, String appname, String accessToken) throws Exception {
// fill ip port
port = port>0?port: NetUtil.findAvailablePort(9999);
ip =(ip!=null&&ip.trim().length()>0)?ip: IpUtil.getIp();// generate address// registry-address:default use address to registry , otherwise use ip:port if address is nullif(address==null|| address.trim().length()==0) {
String ip_port_address = IpUtil.getIpPort(ip, port);
address ="http://{ip_port}/".replace("{ip_port}", ip_port_address);
}
// accessTokenif(accessToken ==null|| accessToken.trim().length()==0) {
throw new IllegalArgumentException("xxl-job accessToken need not null. ");//logger.warn(">>>>>>>>>>> xxl-job accessToken is empty. To ensure system security, please set the accessToken.");
}
// start
embedServer = new EmbedServer();//accessToken加密
String accessCookie &#
版权归原作者 程序男 所有, 如有侵权,请联系我们删除。