任务和调度:理解批量处理的关键设计
ss5723
8年前
<p>一、背景</p> <p>1.1.什么是批量处理</p> <p>1.2.批量处理拥有广泛的使用场景</p> <p>1.3.批量处理需要良好的架构设计</p> <p>二、批量处理中的关键设计</p> <p>2.1从SpringBatch看批量任务设计模式</p> <p>2.2任务调度设计</p> <p>三、总结</p> <p><strong>一、背景</strong></p> <h2> </h2> <p>1.1.什么是批量处理</p> <p>维基百科给批量处理的定义是指在没有人工干预的情况下,由一个计算机程序基于一份批量的输入执行一系列的任务的一种处理模式。这句话可能有点拗口,简单来说,批量处理是一种处理模式,这种模式在进行数据处理时,输入数据一般包含多条,处理过程中一般没有人工交互。而另一种主流的处理模式,联机处理与批量处理的最主要区别就是,联机处理中一般一条输入数据就产生一次处理过程,然后直接将结果反馈给调用方。批量处理曾经在早期的计算机处理模式中占据统治地位。</p> <p>1.2.批量处理拥有广泛的使用场景</p> <p>我们先来看下批量处理的特点:</p> <ul> <li> <p>批量处理单次执行就可以处理大量数据,而联机处理中单次执行一般只能处理少量数据。</p> </li> <li> <p>批量处理每次需要处理大量的数据,执行时间将较长,而联机系统需要实时、快速响应调用方的请求。</p> </li> <li> <p>批量处理不需要维持与调用方的连接,执行结果一般通过报告等形式通知调用方,资源利用率比较高。</p> </li> <li> <p>批量处理可以选择将处理时间放在计算资源不那么紧张的时间段,更好的利用系统资源。</p> </li> </ul> <p>从批量处理的特点我们可以看到,在实时性、交互性要求不高,同时待处理的数据量比较大的场景下,就可以考虑使用批量处理的模式。而实际各种业务系统中通常都会存在大量适合使用或者正在使用批量处理的场景,常见的如银行的对账、网银的批量待发工资、日志系统中批量备份日志等。我们大家可能都会有从支付宝里提现至银行卡的经历,通常提现并不是实时的,支付宝会给你一个deadline,这中间支付宝与银行之间数据对账就是采用批量处理完成的。</p> <h2> </h2> <p>1.3.批量处理需要良好的架构设计</p> <p>在最简单的批量处理场景下,我们可以通过编写脚本,在类Unix系统中通过cron程序定时启动执行。但是这种模式仅仅适合单机处理的情况,没有分布式处理的能力,同时也没有办法进行统一的监控管理。在实际使用时,可能同时存在数量巨大的批量任务,如何管理与调度这些任务将是个巨大的挑战。设计良好的批量处理框架可以简化批量任务开发过程,减少配置时间,提高整体稳定性。笔者曾经参与过某银行BPM系统批量处理框架的设计,一开始设计比较简单,在各个服务器部署批量脚本,基于cron执行,通过数据库进行结果统计,在项目上线初始阶段,由于批量任务比较少,所做的工作也比较简单,该设计能够基本满足需求,但是随着项目上线后,批量任务越来越多,场景越来越复杂(比如需要支持数据库服务器HA切换时批量任务不重复执行),原有设计已经越来越力不从心,最后只有推倒重新设计,费时又费力,由此可见一个好的批量处理框架设计是多么的重要。本文将通过分析批量处理中的两个关键环节, 结合一些开源的批量处理框架,来聊一聊如何更好地进行批量处理型架构的设计。</p> <p> </p> <p><strong>二、批量处理中的关键设计</strong></p> <p>批量处理中两个关键环节是 <strong> 批量任务设计 </strong> 和 <strong> 任务调度设计 </strong> :</p> <p>批量任务设计 :统一规定了作业的定义、编排、执行等过程,良好的作业模型可以隐藏了内部复杂性,简化具体作业开发难度,更好的支持调度过程。</p> <p>任务调度设计 :通俗的说调度就是控制作业在什么时候由那些资源(节点、线程等)去执行,同时还包含作业执行失败后的处理等内容。</p> <p>2.1从SpringBatch看批量任务设计模式</p> <p>2.1.1传统批量作业结构</p> <p>我们首先来看一下过去几十年间已经被广泛使用的批量作业结构:</p> <p><img src="https://simg.open-open.com/show/9331ff56ae5c8dc914ca3dd067a743f2.png"></p> <p>图1 批量作业结构</p> <p>这个架构图非常简单,传递了批量作业中最重要的几个领域概念:</p> <ul> <li> <p>JobLauncher :该领域对象是Job的启动器,其作用就是启动Job。</p> </li> <li> <p>Job :定义,配置批处理任务的领域对象,该对象的作用,是做Step的容器,配置该批处理任务需要的Step,以及他们之间的逻辑关系。</p> </li> <li> <p>Step :定义批处理任务中一个对立的逻辑任务处理单元。基本上的业务逻辑处理代码都是封装在Step中的,这种形式定义了一个Step的流程必须是“ItemReader- ItemProcessor(可选)-ItemWriter”。</p> </li> <li> <p>JobRepository :该领域对象会为Job的运行数据提供一种持久化机制,为所有的Job提供CRUD的操作接口,并为所有的操作提供事务支持。</p> </li> </ul> <p>在这种设计模式下,任务的定义执行过程变得非常清晰,使用这只需要关注于每个Step中的具体业务实现即可,通过简单的配置就能完成任务的设计。</p> <p>2.1.2. SpringBatch中的任务设计模式:</p> <p>传统批量作业结构在好几代平台和编程语言中已经被证明为非常合理和有效。著名Java开源批处理框架SpringBatch就是实现了这种作业结构,不过除此之外,SpringBatch还加入了自身一些设计:</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/98f472b5a8ba2fe1b6e320e84c1bab0b.png"></p> <p style="text-align: center;">图2 SpringBatch作业模型</p> <p>上图展现了SpringBatch中的几个概念模型:</p> <ul> <li> <p>JobInstance :该领域概念和Job的关系与Java中实例和类的关系一样,Job定义了一个工作流程, JobInstance就是该工作流程的一个具体实例。一个Job可以有多个JobInstance,多个JobInstance之间的区分就要靠另外一个领域概念JobParameters了。</p> </li> <li> <p>JobParameters :是一组可以贯穿整个Job的运行时配置参数。不同的配置将产生不同的JobInstance,如果你是使用相同的JobParameters运行同一个Job,那么这次运行会重用上一次创建的JobInstance。</p> </li> <li> <p>JobExecution :该领域概念表示JobInstance的一次运行,JobInstance运行时可能会成功或者失败。每一次JobInstance的运行都会产生一个JobExecution。同一个JobInstance(JobParameters相同)可以多次运行,这样该JobInstance将对应多个Jobexecution。JobExecution记录了一个JobInstance在一次运行时的发生的所有事情,因此,一个JobExecution需要包含很多的属性,并且需要持久化,这样才能很好的支撑Restart等Spring Batch特性。</p> </li> <li> <p>StepExecution : 类似于JobExecution,该领域对象表示Step的一次运行。Step是Job的一部分,因此一个StepExecution会关联到一个Jobexecution。另外,该对象还会存储很多与该次StepExecution运行相关的所有数据,因此该对象也有很多的属性,并且需要持久化以支持一些Spring Batch的特性。</p> </li> </ul> <p>同时,为了提高作业运行时效率, SpringBatch中还同时提供了几种并行处理方案:</p> <ul> <li> <p>多线程处理 ,一个Step的处理过程可以配置一个包含多个线程资源的线程池处理。</p> </li> <li> <p>并行Step处理 ,根据任务的特点,可以将任务中的多个不同的Step进行分组,形成多个流,这多个流可以并行处理。</p> </li> <li> <p>Step远程分片处理 ,下图为Step远程分片模型:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/a2c1f18cabc830f62333a3a7f9bd8a59.png"></p> </li> </ul> <p style="text-align:center">图3 远程分片模型</p> <p> 在远程分片模型中,某一个Step中由Master节点去读取数据,但是处理的过程,由Master分配给多个Slaves去处理,在这种模型中,Master节点的读取能力不能成为整个Step的瓶颈。</p> <ul> <li> <p>Step分区处理 ,这种模式跟远程分片处理过程很类似,不同是,分区处理中Master节点不负责读取数据,而是由该Step中的各个分区独立去读取和处理,当然这种模式下如何将数据进行合适的分区很重要,并不是所有Step都适合这种模式去处理。</p> </li> </ul> <p>2.1.3 SpringBatch的不足</p> <p>可以看到SpringBatch中提供了一套非常完善的批量任务设计模式,但是SpringBatch也有不足的地方:</p> <ul> <li> <p>SpringBatch本身不提供调度的能力,调度依赖于quartz,quartz虽然可以进行集群调度作业,一个节点挂了可以将任务漂移给其他节点执行从而避免单点故障,但是不支持分布式作业,一旦达到单机处理极限也会存在问题。</p> </li> <li> <p>SpringBatch中虽然提供了一些并行处理方案,但是分片、多线程这些方案都非常依赖于任务配置,没有提供一种自动化的机制去灵活地进行资源的调度。</p> </li> </ul> <p>2.2任务调度设计</p> <p>2.2.1两种调度模式</p> <p>常见分布式调度系统在设计上主要有 中心化 和 去中心化 两种模式:</p> <p>2.2.1.1. <strong>中心化的调度模式</strong></p> <p>下图为中心化的调度模式结构图:</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/98f472b5a8ba2fe1b6e320e84c1bab0b.png"></p> <p style="text-align: center;">图4中心化的调度模型</p> <p>如上图所示,在中心化的调度模式下,一般都有一个Leader节点用来负责拉取任务的调度信息,然后向各个Follower节点分派任务,由Follower节点完成任务的执行。同时为了保证整个系统的高可用,Leader节点一般会采取主备模式,当一个Leader节点失效时,备用节点会接管Leader节点工作。</p> <p>2.2.1.2. <strong> 去中心化的调度模式 </strong></p> <p>下面再来看一下去中心化模式:</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/d47fef37d8bf2bf7f40578fd56f29e3d.png"></p> <p style="text-align: center;">图5去中心化的调度模型</p> <p>在去中心化的调度模式下,没有调度中心节点这个概念,所有节点都是工作节点,节点之间通过注册中心进行分布式协调,但是在这种模式下,一般会有一个主节点用于处理一些集中式任务,如分片,清理运行时信息等,并无调度功能,定时调度都是由作业节点自己触发执行。</p> <p>下面对比一下两种调度模式各自的优缺点:</p> <table cellspacing="0"> <tbody> <tr> <td> </td> <td> <p>中心化</p> </td> <td> <p>去中心化</p> </td> </tr> <tr> <td> <p>实现难度</p> </td> <td> <p>高</p> </td> <td> <p>低</p> </td> </tr> <tr> <td> <p>部署难度</p> </td> <td> <p>高</p> </td> <td> <p>低</p> </td> </tr> <tr> <td> <p>触发时间统一控制</p> </td> <td> <p>可以</p> </td> <td> <p>不可以</p> </td> </tr> <tr> <td> <p>触发延迟</p> </td> <td> <p>有</p> </td> <td> <p>无</p> </td> </tr> <tr> <td> <p>异构语言支持</p> </td> <td> <p>容易</p> </td> <td> <p>困难</p> </td> </tr> </tbody> </table> <p>表1 中心化和去中心化调度比较</p> <p>2.2.2. TBSchedule 中的调度设计</p> <p>TBSchedule是由Taobao开源的一款非常优秀的高性能分布式调度框架,TBSchedule的使用非常广泛,目前被应用于淘宝、京东、国美、等很多互联网企业的调度系统。TBSchedule有如下特点:</p> <ul> <li> <p>支持集群、分布式</p> </li> <li> <p>灵活的任务分片</p> </li> <li> <p>动态的服务扩容和资源回收</p> </li> <li> <p>任务监控支持</p> </li> </ul> <p>TBSchedule支持Cluster,可以宿主在多台服务器多个线程组并行进行任务调度,或者说可以将一个大的任务拆成多个小任务分配到不同的服务器。</p> <p>TBSchedule的分布式机制是通过Sharding方式实现的,比如可以按所有数据的ID按10取模分片、按月份分片等等,根据不同的需求,不同的场景由客户端配置分片规则。TBSchedule的宿主服务器可以进行动态扩容和资源回收,这个特点主要是因为它后端依赖的ZooKeeper,这里的ZooKeeper对于TBSchedule来说是一个NoSQL,用于存储策略、任务、心跳信息数据,它的数据结构类似文件系统的目录结构,它的节点有临时节点、持久节点之分。调度引擎上线后,随着业务量数据量的增多,当前Cluster可能不能满足目前的处理需求,那么就需要增加服务器数量,一个新的服务器上线后会在ZooKeeper中创建一个代表当前服务器的一个唯一性路径(临时节点),并且新上线的服务器会和ZooKeeper保持长连接,当通信断开后,节点会自动摘除。下图为TBSchedule的基本结构图,从图上可以到,TBSchedule从整体上来说遵循的是去中心化的调度模式,每个节点都可以从ZooKeeper中拉取任务去执行。</p> <p style="text-align: center;"><img src="https://simg.open-open.com/show/04782c3c64475c22337f46096e7ac05b.jpg"></p> <p style="text-align: center;">图6 TBSchedule结构图</p> <p>TBSchedule提供了两个核心组件 ScheduleServer 、 TBScheduleManagerFactory。</p> <p>ScheduleServer即任务处理器,的主要作用是任务和策略的管理、任务采集和执行,由一组工作线程组成,这组工作线程是基于队列实现的,进行任务抓取和任务处理。每个任务处理器和ZooKeeper有一个心跳通信连接,用于检测Server的状态和进行任务动态分配。</p> <p>调度服务器TBScheduleManagerFactory的主要工作ZooKeeper连接参数配置和ZooKeeper的初始化、调度管理。</p> <p>TBSchedule中的用户目标任务是通过实现任务接口实现的,任务接口中包含selectTasks和execute两个方法,分别对应任务的采集和执行过程。</p> <p>2.2.3. TBSchedule 的不足</p> <p>尽管TBSchedule已经很优秀,尤其是资源调度这块,但是TBSchedule 也有不足的地方:</p> <ul> <li> <p>TBSchedule中对于批量任务开发的指导比较欠缺,这点SpringBatch中做的很好。</p> </li> <li> <p>TBSchedule中任务执行是相互独立的,而在在实际使用场景中很多任务执行必须依赖于另一个任务,甚至可能多个任务之间都有相互关系,形成任务流这种形式,任务流需要以可视化的方式进行编排和执行时的管理,TBSchedule没有提供这种能力。</p> </li> <li> <p>目前TBSchedule中如果上线一个新服务器,需要通过手动的方式去启动,没有结合这几年流行的容器技术,实现服务资源的动态伸缩。</p> </li> </ul> <p><strong>四、总结</strong></p> <p>随着计算机技术的发展,在C/S和B/S软件体系结构中,联机处理模式已经慢慢成为最主要的数据处理模式,尽管如此,批量处理作为一种古老的处理模式,任然以其高吞吐、高性能的特性占据着一席之地。</p> <p>本文从批量处理的概念出发,结合开源批量框架SpringBatch和TBSchedule,简要介绍了批处理型服务架构的设计。可以看到目前虽然有很多已经被广泛使用的批量处理框架,但是还是存在着很多不完善的地方。</p> <p>冰冻三尺非一日之寒,任何事物的发展和完善都不是一朝一夕的事,对于批量处理框架设计而言也是如此。 我们自己在设计时可以考虑站在巨人的肩膀上,借鉴成熟的框架设计,同时结合具体的业务场景,加入符合需求的功能特性,完善出功能强大、运行稳定和易于使用的批量处理框架。</p> <p> </p> <p>来自:http://mp.weixin.qq.com/s?__biz=MzI5MDEzMzg5Nw==&mid=2660393382&idx=1&sn=05b3924639374a16b1cf2660658cfc1e&scene=2&srcid=0822QUrxOT8v2IpTJSqgYoFU&from=timeline&isappinstalled=0</p> <p> </p>