承 林 王海宁 高春成
(南瑞集团有限公司 江苏 南京 211000)(北京科东电力控制系统有限责任公司 北京 100192)
随着电力改革的推进,电力交易规模的不断扩大,参与电力市场的各类用户快速增长,电力市场也正在从中长期市场向现货市场逐步扩展。这些都对系统架构、并发响应能力、系统资源分布式调度使用能力、大数据并行计算能力提出了更新更高的要求。因此借助云计算技术、服务化设计思想,对传统单体架构的电力交易系统进行改造已成为一种必然趋势[1-3]。
任务调度[4-5]作为信息系统中最重要的功能之一,是管理信息系统中各种任务优先级安排以及任务执行的中枢。任务调度算法一般分为事件驱动调度算法、时钟驱动调度算法[6]。目前,时钟驱动调度算法在电力交易系统平台[7-8]中有着广泛的应用。例如横向数据同步、纵向数据同步、电力交易日清算、电力交易月结算、交易对账、日志轮转、定期数据统计分析、定期数据校验等功能,这些都属于时钟驱动调度算法的任务调度处理方式。然而在分布式微服务化的信息系统架构中,不仅存在着基于时钟驱动调度算法的业务处理需求,在各微服务之间的数据请求、数据处理的信息交互下,基于事件驱动调度算法的任务调度也将普遍存在。从资源分布、调度算法、任务执行控制、数据一致性等方面看,传统的单体系统架构中任务调度框架已无法适应分布式系统架构中对任务调度的要求。在这种情况下,一些互联网企业基于自身的需求开发出了一系列分布式调度系统,如淘宝网的TBschedule和当当网的Elastic-Job等技术架构,但是这些系统应对业务规模和基础设施与电力交易系统存在的差异较大,关注的问题也往往在负载均衡上。直接将这些系统架构应用于集群规模小,任务绝对量少,策略复杂电力交易系统中并不适合,因此急需一种针对电力交易业务应用特点和分布式系统架构的任务调度解决方案[9-10]。
在传统的单体结构中,单机任务调度获得广泛应用,操作系统和各种语言的调用库,都提供了良好的实现机制。当前基于F5负载均衡的多节点电力交易系统中,仍采用单体调度任务模式,其中:有的调度任务部署在一个节点;有的调度任务部署在多个节点同时重复执行。前者容易出现单点故障,一旦配置调度任务的节点宕机,将会导致整个任务调度的失效;后者不仅增加无效的负载,而且容易出现数据一致性的问题。
通过将函数封外部接口、轮训方式调用或者利用虚拟IP实现主备机,实现一些简单分布式任务调度的能力,本质上仍是将分布式任务调度转化为单机任务调度问题。这不仅容易出现单点故障,而且对复杂业务的调度任务分配也难以应对,调度任务需要手动注册在每一个节点上,配置和维护也十分繁琐。
随着电力交易系统业务的扩展和系统架构向云计算、微服务方向演进,任务调度的场景也将日益复杂化。归纳起来,可以考虑如下三种场景:场景A:任务一执行失败,写入部分数据,任务二读取到任务一产生的脏数据导致不一致。场景B:任务一先于任务二执行,而任务二先于任务一完成,旧数据覆盖新数据同样导致数据不一致问题。场景C:执行任务节点异常,系统未能成功唤起其他节点执行。
在实践中,无论是单机调度还是分布式调度,对于事物的控制通常由业务逻辑本身支持,不同业务调度之间通常是业务数据依赖。场景A和场景B中,数据一致性主要是同一调度任务不同批次之间的数据一致性。对于需用各台机器执行相同的任务,本质上属于单机调度的范畴。对于主控性任务,需要多台机器中选出一台执行的任务。如果只在一台机器上执行,那么此时分布式调度也退化成单机调度。对于场景C在不考虑分布式事物情况下,可以视为主备问题。
解决分布式系统中任务调度问题,通常有调度集中式控制和分布式规划控制式两种。
(1) 集中式控制 是任务的集中触发控制,由独立的控制模块控制,各个节点只提供任务触发的接口。
(2) 分布式控制 有各个节点独立的维护任务触发逻辑,控制中心只起到协调的作用。
集中式控制是出现较早也容易实现的方式,但容易出现单点故障。比如基于虚拟IP进行轮询就是一种简单实现,但虚拟IP失效时会引起任务调度系统整体失效。在单机任务调度应用广泛的Quartz框架也基于数据库行锁机制提供了一套分布式解决方案,但是仍然无法避免单点故障问题。
分布式控制将任务调度控制权分担到各个节点上,来避免单点故障问题。但是这种设计引入了复杂性,需要解决分布式系统中的协调问题。淘宝网的TBschedule和当当网的Elastic-Job也主要是通过引入Zookeeper技术来进行解决。
(1) Zookeeper与Leader选举 Zookeeper[11-14]是一个分布式的协调工具,通过zab算法来解决分布式系统一致性问题。Zookeeper分布式系统中解决统一配置、分布式命名空间、分布式队列、Leader选举等功能。
在分布式系统中,Leader选举是在一个跨越几台机器(节点)的分布式任务中,指定一台机器作为任务组织者,在选举进行之前各个节点并不知道哪台机器将会成为Leader。在Leader选举之后各台机器都将知道集群中唯一的Leader。因为Zookeeper保证节点之间的数据一致性和顺序性,使用Zookeeper可以满足Leader选举的要求。创建一个节点election通知相关机器参与选举,各台机器接到通知后在election节点下方建立顺序临时节点,然后选取序列号最小的节点作为Leader。Leader选举结束后,对Leader进行监听,一旦发现Leader节点被删除,重新发起Leader选举。但当Leader失去时,所有节点就会同时拉取election节点下的所有子节点,来重新进行选举。这就会对Zookeeper集群产生很大的压力。一种改进方法就是:任何节点只监听下一个兄弟节点,一旦出现Leader失效,监听Leader的节点必然成为Leader,因为没有序号比它更小。
(2) 任务分配策略考虑 传统任务分配策略类似操作系统的作业调度,主要解决同构任务在不同节点的分配,关心任务执行的效率和负载均衡问题。比如在分布式计算中,子计算任务的分配策略侧重考虑的是各个节点的CPU、内存等资源如何得到充分的利用以及如何在任务失败后重新分配。
对电力交易系统来说,分配策略的复杂性在于分配策略的多样性。一种任务分配策略是各个节点都需要同时执行的,如定时拉取缓存,访问数据库;另一种是互斥执行的,如定时结算等,这种需要在数个节点中选出一个执行,属于一种互斥性任务。
对于涉及的节点数目较少,不需过分考虑各个节点之间的均衡问题,只保证不出现单点故障的情况,本文采用Leader选举方式解决互斥问题。当Leader失去服务能力时候,进行Leader切换,互斥任务也迁移到新的Leader上,形成主从备份。
对于任务失败的处理策略,各种任务也不同,有些任务需要重复执行,有些需要任务放弃执行。各种任务失败策略以配置形式进行注册,以满足各种任务的需要。
(3) 脑裂问题的预防和解决思路 在实际的生产环境中,网络震荡是随时可能出现的,如果Leader所在机器出现短暂网络中断,集群则会认为Leader已经宕机,从而重新发起Leader选举。旧的Leader本身并不知道集群已经产生了新Leader,这种情况常被称为脑裂。虽然Zookeeper本身保证了脑裂问题不会长期出现,但是需要旧Leader等待集群的一个通知。在(2)中介绍的互斥任务是在Leader上进行执行的,即使短暂脑裂,也可能引起任务重复执行,对于计费、清算这种业务来说这是不可接受。所以Leader需要对网络事件进行监听,一旦产生网络中断,立即释放Leader,同时重新进行选举时候,等待一定的时间间隔,保证失去网络的Leader完成释放。
(4) 任务的触发控制 分布式调度任务触发控制可以分为时钟控制和任务触发两部分。
时钟触发:通过时间满足时钟条件时,激活相关动作。激活条件可以使用类似Crotnab的时间表达式注册于任务调度中心,表达式采用字符形式表达时间条件,每个字符分别表达秒、分钟、小时、日、月、星期、年等。这种表达式可以清晰表达时间条件。
任务控制:任务的方法、类、执行对象通过字符串的形式进行注册,满足时钟条件的时候,通过反射技术进行调用。在Java语言中提供了原生的反射功能支持。另外,SpingTask[15]和Quartz[16]也提供完整时钟机制和反射调用框架[17],并且容易同基于Spring架构的电力交易系统进行集成,降低了实用和开发的难度。
电力市场交易逻辑复杂,调度任务场景类型多,特别是分布式服务化架构的引入,使得电力交易系统对分布式任务调度存在较迫切的应用需求。
本文依据电力交易系统的应用场景特点、现有技术架构特点以及分布式架构的技术要求,并参考互联网行业的成熟解决方案,提出一个基于分布式控制的任务调度解决方案。其架构如图1所示。
图1 分布式调度方案架构图
该方案主要由管理界面、任务调度中心、任务调度客户端三个部分组成。用户界面提供给用户注册、编辑任务功能;任务调度中心对任务分配进行管理;任务调度客户端负责任务触发和执行。
(1) 管理界面 管理界面提供一个可视化的交互平台,提供调度任务注册、暂停、取消等功能,监控任务的执行的结果。
任务注册:任务执行是将所在机器(节点)具体指定的类和方法,以及任务执行的Crontab时间表达式注册于系统。对于注册于多台机器(节点)的任务,还需指定是互斥任务还是并发任务。互斥任务为在满足任务条件时选出一台机器(节点)执行任务,其他机器作为备份;并发任务为这些机器(节点)同时执行的任务。
任务修改:管理界面提供功能启动和暂停任务,可以在任务执行时刻前暂停任务。
任务监控:通过管理界面查看任务的状态、历史执行时间、执行结果、互斥任务显示、执行的实际机器等信息。
(2) 任务调度中心 如图2所示,任务调度中心为调度系统核心,控制各个节点任务的实际执行行为。本文提出的调度中心使用基于Zookeeper的方案,用户通过管理界面注册任务时,是注册在各个机器对应的节点下面,并在该节点下面建立子节点。客户修改任务时,调度中心就更新对应节点下面的内容。管理端删除任务时候,将对应任务节点删除。
图2 任务调度中心
(3) 任务调度客户端 任务调度客户端部署执行的应用,客户端部分负责调度的具体执行,客户端的主要结构有监听器、任务容器、任务调度器,如图3所示。
图3 任务客户端示意图
监听器在客户端所在应用进程初始化开始时对调度中心对应的IP节点开始监听,并从该节点拉取任务数据保存到任务容器中。如果子节点发生增加、删除、修改就要对任务容器中对应的任务信息进行增加、删除、修改,并且通知任务控制器进行相应的处理。
任务容器:用来存储本地任务的具体信息,如任务名称、任务类型、任务调度方法、调度表达式、任务状态等信息。
任务调度器:初始化时任务调度从任务容器获取任务,依据任务的时间表达式来启动任务,并将执行状态写到任务容器中。当监听器监测到新增或者启动事件时,调度器将会从任务容器中取出任务,检查任务状态,如果任务尚未启动就启动它,并更新状态到任务容器;如果任务执行失败,则根据配置判断是否需要重复执行。
如果监听到暂停或者删除事件,将首先修改状态,再将其从任务容器中删除。
如果任务的类型是互斥任务,客户端初始化时,就会发起Leader选举,从多台备用机中选出一台,作为实际执行调度的机器。调度器在加载任务时,如果检查到自身是Leader就正常启动,如果不是Leader就放弃启动。任务启动后Leader选举的状态保持监听,如果监听到丧失Leader权限,就暂停任务,如果监听到获得Leader身份,则重启动调度任务。
(4) 效果监控功能 为了调度任务进行管理和监控,任务的注册、执行、完成或者异常等数据将实时写入消息总线,并通过消息总线同步到数据库中。管理界面通过发布订阅机制与数据总线保持监听,并将接收到的消息实时同步到管理界面。
监控模块对写入消息总线的数据进行过滤,当监控到任务异常消息时触发报警通知管理员处理。由于本文阐释系统不考虑对分布式事物的控制,即使在单机系统的调度中,调度系统本身也不牵涉到回滚等事物操作的,所以在实践中采用报警触发的机制是能满足实际业务需要的。
基于本文提出的方案构建的系统,在实践中运行了三个月,其中普通节点4台,执行主控性任务机器2台,任务执行的成功率为99.98%,未发生因Leader切换失败或任务失效等情况,也未发生因短期脑裂产生任务重复执行的情况。
对于电力交易系统而言,负载性能并不是业务痛点所在。为应对更大规模的集群介入,对该系统进行了压力测试,结果如表1所示。
表1 压力测试结果
本系统采用分布式的技术架构,对于任务调用中心的Zookeeper,可以通过扩充集群来提高负载性能,对于管理端界面,也可以通过负载均衡的手段,实现水平扩展提高吞吐量。
在系统运行的几个月中,除了系统上线引起Leader切换外,因网络问题出现几次意外的切换,但都通过本文的Leader选举改进机制和任务注册管理功能,防止了分布式系统中Leader选举的脑裂问题,从而保护了任务不会因为网络震荡被重复调度执行。
本文提出了一种基于改进式Leader选举的分布式任务调度系统,解决了电力交易系统从单体架构演进到分布式架构中的复杂任务调度问题。在分析了电力交易业务和电力交易系统的基础上,利用改进式Leader选举方式解决了互斥任务的调度问题,提供了可配置的失败任务处理方式。为多样性的电力交易系统提供了灵活的支持,并通过方案验证和压力测试,证明该方案不仅能够满足当前系统的需要,而且在面对更大规模业务需要时,依然能够良好运行。