内容来源:宜信技术学院第4期技术沙龙-线上直播|宜信微服务任务调度平台建设实践
分享者:宜信高级架构师&开发平台负责人梁鑫
无论是互联网应用还是企业级应用,都充斥着大量的批处理任务,常常需要一些任务调度系统帮助我们解决问题。随着微服务化架构的逐步演进,单体架构逐渐演变为分布式、微服务架构。
在这样的背景下,很多之前的任务调度平台或组件已经不能满足业务系统的需求,于是出现了一些基于分布式的任务调度平台。这些平台各有其特点,但也各有不足之处,比如不支持任务编排、与业务高耦合、不支持跨平台等问题。
按照任务与时间的关系,我们把批处理任务分成三类,飞机型、地铁型、公共汽车型。
飞机型是指每年/月/周/天固定某一时刻执行的任务。这种任务在我们的业务系统中非常常见,比如每天1点要执行一个跑批任务去清理前一天的日志;每月10号要给公司全员发工资,这些都属于飞机型任务。
地铁型是指每隔固定时间执行任务,不可并发。我们也经常遇到这样的批处理任务,第一个任务没有结束,第二个任务是不可以执行的,这就是不可并发。
公共汽车型是指每隔固定时间执行任务,可并发。如果是公共汽车型的任务,前一个任务没有结束,下一个任务也可以按点开始执行。
在跑批任务的过程中会遇到以下问题:
遗忘,忘记了还在运行的定时任务。在我们公司发生过一个这样的案例,若干年前的一个冬天,我们的一个项目团队用3个月的时间做了一个项目,运行一段时间后发现项目的效果并不是很理想,便将相关的程序都停掉了,却忘了有一个跑批任务的节点还在继续运行,直到两年后,这个节点产生的日志把磁盘填满了,触发了监控报警,我们才发现。
单点,就是没有热备,跑批任务是一个单点运行的定时任务,出了故障需要转入手工处理。
依赖,利用时间差来处理依赖反复造成问题数据。大家知道项目有的时候是需要有依赖关系的。比如某个项目的跑批流程A和跑批流程B存在先后次序,项目组设置跑批流程A在凌晨2点运行,跑批流程B在凌晨4点运行,从时间上保证先后次序,万一跑批流程A执行时间过长,超过2小时,就会导致数据出现问题,需要手工处理出现问题的数据。
前文提到任务之间是有关系的,那到底存在哪些关系呢?我认为主要有以下3种:
串行,存在先后关系的两个任务。即任务B在任务A后执行,要先执行任务A之后再执行任务B。
并行,可以并发执行的两个任务。比如任务B和C都要在任务A之后执行,而任务A执行完成后,任务B和C可以同时执行,那B和C就是并行关系。
分支,根据前置任务的返回结果进行判断,不同的结果执行不同的后续任务。比如返回0的时候,执行任务A,返回1的时候执行任务B,这是一种分支的情况。
基于上述的几种关系,我们在建设任务调度平台的时候会思考以下两个方面:
平台化。项目团队总是希望把更多的精力投入到业务开发中,希望把其它与业务开发无关的事情尽可能地放到架构团队。他们希望有一个执行任务的平台,仅仅需要把编写好的业务逻辑放到这个平台就可以了,这个平台会完成所有的工作,项目组只需要关心业务逻辑。
微服务。为了更好地满足项目的需求,我们希望能把任务的业务逻辑和任务的编排调度区隔开来,采用注册和发现机制来建设任务调度平台,与业务相关的部分交给项目团队处理,把其他的部分交给任务平台来处理。
除了上述两个方面的考虑以外,我们还需要思考以下八个因素。
任务编排。多个业务之间的定时任务存在流程次序,前面提到任务之间有并行的关系、有串行的关系,还有分支的关系,我们希望平台能有相应的编排功能去处理和支持这些任务。
任务分片。对于一个大型任务,需要分片并行执行。
跨平台。除了使用 Java 技术栈(SpringBoot、Spring等)的项目之外,还要能够支持使用其他语言的应用。
无侵入。业务不希望与调度高耦合,只关注业务的执行逻辑,希望平台对业务本身代码是无侵入的,将影响降到最低。
高可用/故障转移。调度系统自身必须保证高可用,不能有单点,任务执行过程中遇到问题有补偿措施,能够平滑处理,减少人工介入。
可视化。任务调度的操作提供可视化页面,方便使用。
实时监控。平台要有实时监控系统,实时获取任务的执行状态。
动态编辑。业务的任务时钟参数可能变动,在可视化的基础上,对所有任务执行的操作都实时反映到业务系统中去,不需要停机部署。
基于以上的背景与考虑,我们建设了微服务任务调度平台SIA-Task。
SIA是“Simple is Awesome”的简称。
SIA-TASK(微服务任务调度平台)是其中的一项重要产品,SIA-Task契合当前微服务架构模式,具有跨平台、可编排、高可用、无侵入、一致性、异步并行、动态扩展、实时监控等特点。
SIA-TASK是任务调度的一体式解决方案,对任务进行元数据采集,然后进行任务可视化编排,最终进行任务调度,并且对任务采取全流程监控,简单易用。对业务完全无侵入,通过简单灵活的配置即可生成符合预期的任务调度模型。
SIA-TASK借鉴微服务的设计思想,获取分布在每个任务执行器上的任务元数据,上传到任务注册中心。利用在线方式进行任务编排,可动态修改任务时钟,采用HTTP作为任务调度协议,统一使用JSON数据格式,由调度中心进行时钟解析,执行任务流程,进行任务通知。
简单介绍一下SIA-TASK的术语。
任务(Task): 基本执行单元,执行器对外暴露的一个HTTP调用接口;
作业(Job): 由一个或者多个存在相互逻辑关系(串行/并行)的任务组成,任务调度中心调度的最小单位;
计划(Plan): 由若干个顺序执行的作业组成,每个作业都有自己的执行周期,计划没有执行周期;
任务调度中心(Scheduler): 根据每个的作业的执行周期进行调度,即按照计划、作业、任务的逻辑进行HTTP请求,它是一个单独的节点;
任务编排中心(Config): 编排中心使用任务来创建计划和作业;
任务执行器(Executer): 接收HTTP请求进行业务逻辑的执行;
Hunter:Spring项目扩展包,负责执行器中的任务抓取,上传注册中心,业务可依赖该组件进行Task编写。
Task是业务执行的基本单元,执行器对外暴露的一个HTTP调用接口。若干个Task构成一个Job,而Plan是由若干个顺序执行的Job构成。
为什么这里需要一个Plan?有的时候两个任务不光有顺序关系(就是A任务执行完之后再执行B任务),还需要满足一定的时间要求,比如上午10点执行任务A,下午2点执行任务B,而且必须保证上午10点任务A按时执行完成。
打个比方,今晚8点有一场足球比赛的直播,如果晚上8点我还不能到家,那我就没办法看直播,而如果今天我下班早,下午6点多就到家,也必须等到8点才能开始看球赛,这就是Plan计划的来源。
SIA-TASK任务调度平台由以下几个部分组成:
任务执行器,就是你的业务代码在哪里,这是属于项目组的。
任务注册中心,我们用的是ZooKeeper。
任务编排中心
持久存储,我们用的是MySQL。
任务调度中心
接下来详细介绍SIA-TASK的运行逻辑。
首先,通过注解抓取任务执行器中的任务上报到任务注册中心。任务执行器在启动的时候,会有一个叫online Task的注解,只要把这个注解放到control代码的方法上,就会自动把HTTP接口抓取出来,然后上报到任务注册中心,这里我们用的是ZooKeeper。
任务编排中心从任务注册中心获取数据进行编排保存入持久化存储。也就是说,相当于在执行器里,把业务调用HTTP接口请求的URL地址、端口等实例抓取出来上传到ZooKeeper里,ZooKeeper就拿到了一个个的任务,ZooKeeper会把任务本身的信息抓取出来放到MySQL里。
这里要区别一下什么是任务,什么是任务实例。任务实例和任务的关系,有点像类和对象的关系,就是一份业务逻辑代码可能部署在多个节点上,也就是说这些节点的业务逻辑代码是一模一样的,在运行阶段抓取的时候会把每个节点上业务逻辑代码都抓取上来,针对这个业务它就是一个任务,但是每一个端口、每个IP地址对应的可能就是一个任务实例。比如高可用热备时,我们会把任务本身的信息经过处理之后保存到持久存储里,而实例本身的信息只会停留在ZooKeeper里。
任务配置中心可以根据ZooKeeper里的信息和MySQL里的信息进行配置,就是根据抓取的任务,给这些Task加时钟、策略,然后编排出Job和Plan,并把现在的这些信息保存到MySQL里。
任务调度中心从持久化存储获取调度信息,知道编排的Job、Plan、时钟、策略等逻辑,任务调度中心按照调度逻辑访问任务执行器,对这些从执行器上抓取来的Task进行调度。
这就是SIA-TASK的运行逻辑,同时我们会把调度日志存到Kafka里。
在暴露成HTTP服务的方法上加入@OnlineTask注解,@OnlineTask会自动抓取方法所在的IP地址、端口、请求路径、请求方法、请求参数格式等信息上传到任务注册中心(zookeeper),并同步把任务信息写入持久化存储中。
单一任务实例必须保持单线程运行,任务调度框架自动拦截@OnlineTask注解进行单线程运行控制,保持在一个任务运行时不会被再次调度。而且整个控制过程对开发者完全无感知。
就是在一个任务实例上,要保证任务在运行的时候是单线程状态。其实这是由用户自己控制的,如果需要是单线程的,这里可以加以控制;如果需要是多线程的,可以不加控制。这个控制并不需要另加代码,只需要在注解上去处理。
SIA-TASK的设计思想是以任务为原子,把多个任务按照执行的关系组合起来形成一个作业(Job)。同时运行时分为任务调度中心和任务编排中心,使得作业的调度和作业的编排分隔开来,互不影响。在我们需要调整作业的流程时,只需要在编排中心进行处理即可。同时编排中心支持任务按照串行、并行、分支等方式组织关系。在相同任务不同任务实例时,也支持多种调度方式进行处理,而且整个的处理编排都是在页面上完成的,这个功能非常好用,这也是SIA-TASK平台的一个亮点。
任务执行过程中出现失败、异常时,可以根据任务定制的策略进行多点重新唤醒任务,保证任务的不间断执行。我们设定了很多策略,比如某个Task出现问题了怎么办?是再唤醒一次?还是不管了?还是人工干预发警报?我们定制了很多策略去处理这些问题。
了解了平台特性,我们来梳理SIA-TASK的技术关键点。
任务流。实现任务与任务之间可配置的流向关系,形成有向无环图(DAG)。 任务流可由定时时间(Cron 表达式)或外部请求(提供 API 地址) 开始,根据 DAG 逻辑执行。
元数据管理。微服务中各个任务元数据的管理同步数据抓取、录入。
智能运维。可视化的任务实时监控,所有监控都是有页面可以看到的;实时预警机制,出现问题的时候,会发送邮件或短信给相关人员告警;半智能化的自主修复,嗅探重试,不需要人工干预。
资源隔离。进程间的资源隔离;进程内的资源隔离,提高系统吞吐,提供稳定性。时钟用的是Core Schedule,一个调度中心对一个项目组用一个Core Schedule,每个项目组在同一个调度的时候,同一个调度器上都是隔离的,一个项目组出问题,不会影响到其他的项目组,这就相当于代表了隔离性负载均衡。
负载均衡。调度中心调度任务的时候,任务的执行周期时间不一样,可能有的任务需要的时间长一点,有的任务需要的时间短一点,调度器的资源也不太一样,有的CPU高一点,有的CPU低一点,那如何保证调度负载均衡?如何保证资源隔离的负载均衡?我们会根据这种任务调度的历史值(任务耗时)以及机器本身性能的值进行考量,使每一个任务调度中心拥有的调度数量差不多、消耗也差不多。这是一种新的负载,而不是简单的流量负载。
任务调度管理首页主要包括三部分:调度器信息、调度次数、对接项目详情。
调度器信息:调度中心调度器的数量。
调度次数:调度中心调度Job的历史累计总数。
对接项目详情:调度中心对接的项目组总数,Job总数。
目前SIA-Task平台上已经接入了51个项目,上面跑的Job数有600多个,今年上线的版本,Job已经跑了3000多万次。
调度器上有几个值需要了解一下,每台调度器都有三个指标。
Job上限值:所能负载的Job动态阈值;
Job运行数量:该调度器当前运行的Job数量;
Job预警值:当调度器运行的Job数超过预警值时,会发邮件通知管理员。
关于调度器有几个 信息需要了解,如图所示,点击某个调度器(柱状图),会显示该调度器所抢占的Job详情列表:
JobKey:所配置的Job名称,每个Job都有自己的名字。
类型:配置Job的定时任务类型,分为Cron与fixRate两类。
Job类型值:如果是Cron表达式,6位时间戳怎么写;如果是fixRate,那就是需要间隔多少时间。
预警邮箱:该Job配置的预警邮箱。
描述信息:描述该Job的功能信息,便于管理员能够迅速发现某台调度器所抢占的Job详情。
调度器包括工作调度器、下线调度器、离线调度器、白名单。
工作调度器:这类调度器具有抢占和调度Job的能力。对某调度器进行下线操作,它会立即失去抢占Job的能力,已经抢占的Job执行完毕后会自动释放,进而被其他调度器抢占,调度器下线后会进入下线调度器列表中;工作调度器列表提供下线以及批量下线的功能。简单来说,工作调度器就是正在工作中的调度器。
下线调度器:这类调度器进程仍然存活,但失去了抢占Job与参与调度的能力。对这类调度器执行上线操作,会进入工作调度器列表,且开始具有抢占和调度Job的能力;下线调度器列表提供上线及批量上线的功能。就是说,下线调度器依然活着,只是不再参与抢占Job,之前已经有的Job还是会继续执行完成,如果点击上线就重新具备抢占Job的能力,变成工作调度器。
离线调度器:这类调度器进程不再存活,当下线调度器进程死亡后,会自动进入离线调度器列表,这类调度器进程重新启动后,会自动进入下线调度器列表;离线调度器列表也提供删除及批量删除的功能。离线调度器一般都是出现问题了,可能是进程挂掉了,也可能是网络故障了。
白名单:将某个IP加入白名单之后,它具有调用所有执行器实例的权限;白名单列表提供批量删除的功能,删除该IP后自动失去该权限。
上图所示是SIA-TASK的调度监控页面,分着的一块一块区域属于不同项目组。目前SIA-Task接入了51个项目,准备中的有500多个,正在运行的有25个。
有的Job执行非常快,几秒钟就执行完了,有的Job执行非常慢,需要很长的时间,我们在状态抓取的时候,只能抓取到时间长的Job,这些被抓取的Job显示为正在运行,而时间短的捕捉不到,但它们都处于执行状态,这些没有被抓取到的Job就显示为准备中。
可能有的Job这段时间不需要运行,可以手动停止,剩下的就是异常停止的Job,需要发送邮件告警。
我们也提供了检索的能力,可以接受不同项目组登录查询自己的项目运行状态。
Task管理界面中,Task按项目组分组显示,主要提供Task的配置、修改与删除等功能。Task包含两部分:一部分Task使用了sia-Task-hunter组件,通过标准注解实现Task的自动抓取,这类Task不允许修改;另外一部分Task是由用户手动添加的,我知道访问的URL和HTTP地址,手动添加进来,这部分Task支持跨平台的抓取,而且可以修改和删除。
一个Task管理包含以下几个部分内容:项目名称、应用名称、任务名称、机器地址、描述、以及查看/修改/连通性测试等操作。同一个Task名称,不同的机器地址,代表一个任务和不同的任务实例。
前面介绍了一个Job由若干个Task组成,图中每一个不同的列代表项目名称,点击下拉列表可以显示所有的项目,可以进行过滤、添加、状态查看等操作。
其中状态操作可以手工执行,可以停止或激活Job,Job配置好之后属于未激活的状态,需要激活一下。还可以修改Job里的信息,配置Job等。 如何添加Job?假如我要添加一个Cron表达式类型的Job,需要添加哪些内容呢?
因为Job是Cron表达式类型的,首先我需要输入六位表达式内容,还要添加一个预警邮箱,再描述这个Job,每个Job都有一个key,最后还需要添加Job_key。这样一个新的Job就添加好了。
回过头来看,添加Job需要配置Task信息,这是一个比较复杂的过程。一个Job由若干个Task组成,我们可以用拖拉拽的方式根据Task之间的关系确定形成组成Job的所有Task的顺序关系。还可以以不同颜色代表不同项目进行区分,当然只有管理员才有权限看到所有项目,各个项目的负责人只能看到自己所属项目的状态。
上传Task的时候会带一些参数,所以还涉及到参数的处理,比如参数类型、参数值、过期时间等。重点聊聊过期时间。
通过HTTP方式调用会遇到一个问题:到底Task什么时间会执行完成。为解决这个问题,就需要设一个Task的过期时间,只要过期时间一到,就会转入其他策略,比如放弃或人工处理等。因为作为异步调用,不可能无休止地等待客户端返回结果。
当然也可能存在一种情况:我得到的结果是超时了,实际上任务是在正确执行,而且再过一段时间给我返回结果了。我们曾经设计了一种队列补偿机制来处理这个问题,但是好像意义不大。当然,这只是一种可能,平台上线至今没有出现过。
目前平台的Task_选取实例策略包括两种:
随机,从可选的列表中,随机选择实例,即IP+端口;
固定IP,指定实例,随后需要从可选列表中人工指定实例。
平台支持四种Task_调用失败策略:
STOP,停止策略,调用失败则整个Job停止,不再执行后续Task;
IGNORE,忽略策略,调用失败则跳过该Task,继续执行后续Task;
TRANSFER,转移策略,选取该Task的其他实例执行,如果依然失败,则使用停止策略;
MULTI_CALLS_TRANSFER,多次调用再转移策略,重复调用该Task多次,如果依然失败,则使用转移策略。
日志管理提供了Job的运行日志相关信息,按项目组分组显示,一条Job日志的关键元素包含:
执行状态:表示该Job执行结果;
执行时间:表示调度器调度Job的时间;
执行完成时间:表示Job执行完成的时间;
调度信息:表示执行Job的调度器实例;
执行信息:Job执行的具体信息,并且已实现Job与所引用的Task的执行日志信息的关联,日志默认保存七天。
SIA-TASK作为SIA团队的一个重要产品,在公司接入了数十个项目,运行着数百个Job,经受住了稳定性的考验。
SIA-TASK微服务调度平台于5月已经开源,开源地址:https://github.com/siaorg/sia-Task,感兴趣的同学可以登录查看详细介绍。
分享者:梁鑫
◆ ◆ ◆ ◆ ◆
发现文章有错误、对内容有疑问,都可以通过关注宜信技术学院微信公众号(CE_TECH),在后台留言给我们。我们每周会挑选出一位热心小伙伴,送上一份精美的小礼品。快来扫码关注我们吧!