我厂的分布式任务调度平台的私有化之路

1,169 阅读10分钟

一. 背景

清明假期了,总算有一点时间来复盘这几个月的工作和生活了。
距离上一次更新已过了许久。并不是我不想更新,而是年前跳槽到一家Top10的互联网企业了。
一是任务重忙了许多,恨不得有分身术;
二是牛人多压力很大,真希望有时光穿梭之术,让我回炉再造一次!
但总的来说内心很开心,我也很珍惜这个机会。到大厂最直接的就是薪水的较高幅度上涨,另外我觉得最有价值的是接触到了大厂的研发流程,公司内部的CF空间有取之不尽用之不竭的技术文章、方案。这些是公司一路发展以来萃取出来的结晶。我仿佛像一头饿狼一样一有时间就在里面徜跑!
这几个月我的任务之一是调研并给出一套切实可行的分布式任务调度解决方案,因为在我们的SaaS场景和私有化场景下都会用到分布式任务调度。
我一开始比较纳闷,咱这么大的公司难道没有成熟可用的方案么?后来了解到是有的,但在SaaS平台私有化场景下,公司的平台就无法使用。为了避免在私有化部署时对工程有太大的改造,决定自主实现一套。

二. 技术选型

在网上有很多定时任务选型方案,掘金有也有很多大佬在这方面讲述的很全面。我试着以简短的方式集大家之建议,梳理如下比较方案。像Timer、Spring Task不在比较范围之内,因为他们不适合大多数项目的生产环境,且比较简单。

产品QuartzElastic-jobXXL-JOBApache Airflow
调度类型CronCronCron \ 固定速度Cron
工作流不支持不支持不支持支持
HA支持:多节点部署支持:基于zookeeper支持:多节点部署支持
分布式任务静态分片动态分片支持
自定义任务参数不支持支持支持支持
白屏化任务治理执行记录:无;运行大盘:有;运行日志:有;原地重跑:无;重刷数据:无执行记录:有;运行大盘:有;运行日志:有;原地重跑:无;重刷数据:无执行记录:有;运行大盘:有;运行日志:有;原地重跑:有;重刷数据:有
任务类型JavaJava/ShellJava/Shell/Python/PHP/Node.js可通过Operator自定义;自带的主要是大数据和Shell,无Java
报警监控自研邮件自研
使用成本DB;多个ServerDB;zookeeper;console;serverDB;调度中心;执行器DB;Master;Worker;MQ
维护周期近期3年前近期近期
使用难度二开工作量大项目多年未维护,不知有什么坑功能较完善;二开工作量低适用于大数据领域

基于以上选型比较,结合我们团队的需求特点,基于XXL-JOB进行二开是比较适合我们的:其本身定义为轻量级,任务类型丰富,功能比较完善,项目一直在维护,源码相对容易阅读理解。

XXL-JOB的不足

XXL-JOB本身定义为轻量级,轻量级意味着它没有依赖过多的其他jar包和功能中间件。尽量使用Java本身提供的能力来实现功能,比如RPC通信基于java.net.URL;日志输出采用FileOutputStream;鉴权采用accessToken字符串匹配等。轻量是轻量了,但性能和安全性方面有比较明显的缺陷。最起码不符合我们的要求。
XXL-JOB将调度和执行分为两个项目,这是它最棒的特色之一,但调度中心需要独立部署,对于我们而言,我们希望它能作为平台通用能力之一集成在通用能力平台上,但又希望能够最小化改造管理页面,避免投入过多的人力资源;而且在私有化场景下需要支持功能定制化能力,因此,对于调度中心我们需要将其改造为嵌入式调度模块,具备可插拔能力。

改造方案

当前我们改造的目标是先让其能够融合近我们整个的架构方案,因此对于核心能力层(比如:转发策略,调度策略等)不做大幅改造。

1.嵌入式调度中心

XXL-JOB的调度中心模块是xxl-job-admin。该模块开箱即用。但我需要将其改造为嵌入式模块。

改造目标

其他工程引入改造之后的模块,即可拥有调度中心能力,且不能影响原有工程的任何功能。

实施方案

xxl-job-admin是一个典型的MVC项目。采用的模版引擎是freemaker。这意味着我需要考虑如何处理resources目录下大量的资源文件和配置文件。

image.png

这里有两种方式来处理:
1.资源配置文件迁移
将xxl-job-admin下的资源配置文件迁移到需要集成的项目之下。这样做节省了脑力,不用想方案了,但需要更多的体力。这不符合我厂程序员的身份(咳咳~),这种方案不够优雅。
2.修改maven构建方式
xxl-job-admin是基于maven构建的项目,因此可通过定制pom.xml中的配置来达到我们的目的。pom.xml提供了一个叫做build的标签,该标签定义了如何编译和打包项目,而具体的编译和打包工作交给了内部的plugin来完成。build的plugin默认有以下几种:

plugin功能生命周期阶段
clean plugin清理上一次编译执行的目标文件,即清理target目录clean
resource plugin处理源资源文件和测试源资源文件resources testResources
jar plugin创建jarjar
deploy plugin发布jar到远程maven仓库deploy

如果我们没有任何配置,maven在构建的过程中也会执行默认的plugin。
maven不是这本篇文章的重点,基于以上认知可以知道通过定义<build>节点的<resources>配置,可以处理资源文件。具体配置如下:

<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*.*</include>
            </includes>
            <excludes>
                <exclude>**/*.properties</exclude>
                <exclude>**/*.xml</exclude>
            </excludes>
            <filtering>false</filtering>
        </resource>
    </resources>
</build>

通过以上配置,maven在编译过程中就不会对资源进行过滤了,另外,配置类文件是需要在具体的工程中配置的,因此这里将其排除掉。这样,在编译之后,xxl-job-admin的资源文件就可以被编译到具体工程的资源目录下了。
基于此,嵌入式调度中心成功了第一步。

2.统一用户认证

xxl-job提供了自己的用户体系,拥有注册、登录、基本的权限认证。但对于我们而言,这远远不够。

改造目标

注册、登录需接入集团统一用户认证中心和安全中心,调度中心本身需要完善的RBAC鉴权能力;接口挂到统一网关。

实施方案

这一块的改造主要是因为作为嵌入式模块,其需要对接团队的其他底层能力,而不是“鸡立鹤群”,我们需要把这只“鸡”改造成一只“鹤”才能完全的融入到整个解决方案中。
xxl-job的登录能力的核心在于PermissionInterceptorCookieInterceptor,因此,为了避免大的改造,我的方案如下:
作为独立应用发布:沿用xxl-job的登录、注册模块,但需要对接集团统一用户认证和安全中心;
作为嵌入式使用:使用集团统一的登录、注册模块。
因此,我们需要对登录能力的核心做一些改造。由于涉及到机密问题,这里我就简单的放一下源码:

image.png

3.网关挂载

我们所有的接口都需要经过网关,因此写了一个自动注册网关的工具类,同样的这里就不贴出源码了,思路就是写一个事件监听器,当监听到容器初始化完成之后,扫描嵌入式调度中心下所有的api接口,然后按照一定的规则注册到网关即可。

4.RPC扩展

在实际的业务中会有这样一类需求:某个工程没有定时任务,但又希望能够主动触发其他工程中的定时任务以完成某些初始化或者手动补偿能力。如果不具备这样的能力的话,那么每次都需要去调度中心管理平台去触发。在业务闭环的链路中,节点越少,流程越清晰,因此,我们希望能提供这种主动触发且屏蔽手动触发的情况。因此,在xxl-job-admin中添加了一个rpc包,将一些能力对外开放。

image.png

5.分布式场景优化

xxl-job-admin源码在新增执行器和任务时,并没有对执行器jobHandler做唯一性校验,这会引起两个问题:
1.视觉混淆:产研人员管理任务时,由于名称一样,会对其造成混淆。尽管我们知道在创建时名称的语义应当清晰明确,但程序层面应该要做这种强制性校验; 2.分布式场景下执行器注册问题:在xxl-job-admin源码中,执行器会自动注册到调度中心,其注册逻辑是根据执行器配置的xxl.job.executor.appname来决定注册到哪一个appname上,贴上源码:

image.png xxl-job是通过异步的方式注册执行器的,registryUpdateregistrySave最终执行的是mybatis-mapper中的配置sql,registry_group对应的就是appname。源码如下:

<update id="registryUpdate" >
    UPDATE xxl_job_registry
    SET `update_time` = #{updateTime}
    WHERE `registry_group` = #{registryGroup}
      AND `registry_key` = #{registryKey}
      AND `registry_value` = #{registryValue}
</update>

<insert id="registrySave" >
    INSERT INTO xxl_job_registry( `registry_group` , `registry_key` , `registry_value`, `update_time`)
    VALUES( #{registryGroup}  , #{registryKey} , #{registryValue}, #{updateTime})
</insert>

发现问题了吗?如果appname相同,则执行器都会注册上去。在分布式任务调度场景下,必然会引起不可预测的问题!因此,我对执行器jobHandler做了唯一性校验,从源头避免这种情况发生。(该问题我已经提出issue),改造之后如下:

image.png

6.日志组件优化

xxl-job的日志组件比较简单粗暴,以同步的方式,通过FileInputStream直接append到文件中,查看日志时也是直接通过IO流直接读取。
性能问题暂且不说,由于架构方案有统一的日志管理平台,任何一个模块的日志都需要写入到日志管理平台中,因此xxl-job的日志组件需要进行改造,将其改造为符合我们验收标准的样子。 xxl-job的日志组件在com.xxl.job.core.log路径下,具体源码就不在这里贴出。

7.底层通信

xxl-job的底层rpc通信模块使用了URLnetty,执行器接收调度是用的netty,除此之外,都是用的HttpURLConnection,在xxl-job中使用URL其实问题不大,毕竟本身定位为轻量级,但在大规模分布式任务调度场景下,而且我们基本上是作为嵌入式调度使用,因此对性能更加敏感,基于此考虑,决定使用团队统一的HTTP请求方案。

三.总结

本篇文章大致介绍了我们是如何基于xxl-job进行二次开发将其打造为符合我们团队使用标准的分布式任务调度平台的,限于篇幅,其实还有很多思考的具体细节和一些结论没有说到,这里把一些我认为比较重要的一些点做一下记录,以作备录。
总的来说,xxl-job里面有很多思路值得借鉴,其源码不难理解,是一款很优秀的定时任务开源框架。