基于事件驱动的任务分布式调度消费方案

1. 技术背景

事件驱动架构(Event-Driven Architecture, EDA)是一种用于设计应用的软件架构和模型。EDA是一种以事件为媒介,实现组件或服务之间最大松耦合的方式。面向服务驱动框架(Service-Oriented Architecture)是以接口为媒介,实现调用接口者和接口实现者之间的解耦,但是这种解耦程度不是很高,如果接口发生变化,双方代码都需要变动,而事件驱动则是调用者和被调用者互相不知道对方,两者只和中间消息队列耦合。事件驱动架构可以最大程度减少耦合度,因此是现代化分布式应用架构的理想之选。

事件是指系统硬件或软件的状态出现任何重大改变。事件与事件通知不同,后者是指系统发送的消息或通知,用于告知系统的其他部分有相应的事件发生。而事件的来源可以是内部或外部输入。事件可以来自用户(例如点击鼠标或按键)、外部源(例如传感器输出)或系统(例如加载程序)。

事件驱动架构由事件发起者和事件使用者组成。事件的发起者会检测或感知事件,并以消息的形式来表示事件。它并不知道事件的使用者或事件引起的结果。 检测到事件后,系统会通过事件通道从事件发起者传输给事件使用者,而事件处理平台则会在该通道中以异步方式处理事件。事件发生时,需要通知事件使用者。他们可能会处理事件,也可能只是受事件的影响。 事件处理平台将对事件做出正确响应,并将活动下发给相应的事件使用者。通过这种下发活动,就可以看到事件的结果。

事件驱动架构可以基于发布/订阅模型事件流模型:发布/订阅模型是基于事件流订阅的消息传递基础架构,对于该模型而言,在事件发生或公布之后,系统会将相应的消息发送给需要通知的订阅用户;事件流模型会把事件写入日志,事件使用者无需订阅事件流。相反,它们可以从流的任何部分读取并随时加入流。

事件流有几种不同的类型:

  • 事件流处理使用诸如 Apache Kafka 等数据流平台来提取事件并处理或转换事件流。事件流处理可用于检测事件流中有用的模式。
  • 简单事件处理是指事件立即在事件使用者中触发操作。
  • 复杂事件处理则需要事件使用者处理一系列事件以检测模式。

2. 现有技术方案

XXL-JOB是一个开源的轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展、开箱即用。骑在分布式任务调度的主要功能点有:

  • 支持同一服务多个实例的任务存统一协调;
  • 定时任务的执行支持高可用、监控运维、故障告警;
  • 具有统一管理解密,并可以追踪各个服务节点定时任务的运行情况,以及任务属性信息,例如任务所属服务、所属责任人等。
输入图片说明

XXL-JOB将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求;将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑;因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性。自2015年开源以来,XXL-JOB已接入数百家公司的线上产品线,接入场景涉及电商业务,O2O业务和大数据作业等。

3. 现有方案的缺点

  1. XXL-JOB的作为分布式任务调度平台,其作为面向服务驱动(Service-Oriented Architecture)的架构,以接口的形式提供服务,不支持事件驱动的模式,需要任务的发起者显示调用来发起任务。换而言之,其只解决了任务执行的问题,但是不能管理任务的触发,事件和被触发的任务的对应关系,需要业务方自己管理。
  2. XXL-JOB的任务调度通过获取数据库锁的方式来保证集群中执行任务的唯一性,如果在短任务居多的情况下,随着集群内节点数量的增加,数据库锁的竞争将会剧烈上升,进而成为性能瓶颈。
  3. XXL-JOB所接收的任务都会先直接写入数据库,然后通过抢夺数据库锁的形式决定其执行的节点。这种模式在面临大数据的情况下,会造成数据写入排队,从而影响服务的吞吐量。
  4. XXL-JOB虽然支持水平扩容,但是随着集群内节点数量越多,其抢锁的竞争越剧烈,所以其弹性伸缩的能力有边界限制,不能无限扩容。

4. 发明目的

    为了将事件的触发和任务的执行完全解耦,提高分布式任务调度系统的吞吐量,并强化其弹性伸缩的能力,本方案基于发布/订阅模型的事件驱动架构,来专注处理事件触发的任务流程,即一个事件到来,要触发哪些不同的响应的任务,保证这些任务被记录下来不丢失。而具体的任务执行过程,可以利用各种框架和工具,也包括上文提到的xxl-job框架。

5. 方案设计

本方案基于事件驱动架构,引入发布/订阅模型消息队列(MQ),将事件与任务的触发关系映射为主题消息的订阅关系,从而利用消息队列的来提升系统的吞吐量和可伸缩性。方案重点内容如下:

  1. 将事件看为消息,被触发的任务被看作消息的订阅者,不同类型的事件被发送到各自对应的消息主题,通过MQ的传输给不同订阅者消费,从而触发任务的执行。

  2. 消费者集群内每一个实例节点都会在消息主题下注册一个专属的消息分区(partition),当任务调度系统决定让一个节点处理一个事件时,就会把该事件以MQ消息的形式编码并放入该节点对应的分区中,等待消费节点来获取消息。

  3. 任务调度系统会根据消费节点各自分区内的消息挤压的情况,指派合适节点消费任务,避免节点过渡繁忙,实现任务消费的负载均衡,同时避免抢锁。

  4. 消费实例节点接收到任务信息后进行持久化处理,将任务信息和状态保存到数据库中。待任务处理器执行完任务后,数据库中任务的状态将被设置为完成。

  5. 如果任务执行失败,消费节点将会根据失败策略尝试重试或者忽略任务。

  6. 消费集群依赖消息队列和心跳来保证集群的弹性伸缩:

    • 每个消费节点都会定期上报心跳,并保存在缓存服务器上(如redis);

    • 当有节点出现无法及时更新心跳时,缓存服务器保存的心跳数据就会过期,从而触发该节点待执行的任务回重新分配;

    • 新实例上线时,依赖消息队列的消费者组再平衡(Consumer Rebalance)机制,会被自动分配新自动任务,无需多加干预。

  7. 消费者实例重启后,会先执行任务队列的初始化,从数据库中加载分配给自身但是未执行完毕的任务,并优先执行这些任务,保证任务不会丢失。

各模块具体功能点如下:

  1. 任务发布模块

    • 任务的发布基于事件驱动,将任务事件(TaskEvent)以MQ消息编码并发布到消息队列上。
    • 事件消费者集群通过关注消息队列主题来实现对于任务的触发:
      • 每一个消费实例节点都会在消息队列上创建一个对应的partition,用于存放待消费的消息事件;
      • 当消费实例节点成功接收到事件之后,会计算触发的任务负载量并记录到缓存服务器。
    • 任务调度器根据缓存服务器上每个实例节点partition的负载情况,选择合适的消费者来接收事件并执行任务。因此在发送事件的时刻已经决定了执行任务的实例,避免抢锁。
  2. 任务处理流程以及任务处理器模块

    • 消费实例节点接收到任务事件(消息)之后,首先对消息解密还原为事件,再将事件持久化到数据库,最后才向消息队里确认消费成功。如果事件持久化失败,消息队列则会重新发送事件。这样的机制保障事件一定可以发送成功。

    • 任务在数据库中所记录生命周期状态共有如下5种:

      • 待执行,即todo状态,为持久化时的默认值;
      • 实例处理中 ,即doing状态
      • 执行完毕,即done状态;
      • 执行失败,即failed状态;
      • 重新调度中 ,即resechedule状态。
    • 在持久化成功之后,消费实例节点会向缓存服务器上报有新任务到来,并更新其对应partition的任务负载情况;

    • 任务处理器模块中记录着任务事件(Event)类型和事件处理方案(EventHandler)的对应关系,事件处理方案会发起对应的任务执行,并将任务状态设置为doing。一种事件可以同时对应多种处理方案,从而触发多个任务。

    • 如果事件触发的任务执行成功,则任务状态将会被设置为done

    • 本方案对于任务的具体执行方式没有特别的限制,可以是本地的线程池,也可以是分布式的任务执行框架(如xxl-job)。

  3. 失败处理器

    • 如果任务失败,失败处理器模块会根据事件(Event)对应的失败策略来应对
    • 失败策略可以定义重试次数和时间间隔,或者直接忽略失败结果;
    • 如果任务重试达到最大次数而没有成功,其状态被设置为failed
  4. 服务集群管理模块

    • 每个集群的节点都向缓存服务器定期刷新心跳数据(如实例的IP地址);
    • 集群管理模块会监听心跳数据过期的事件
      • 如果实例发送故障问题没有及时刷新心跳,必然会造成心跳数据过期;
      • 集群管理模监模块听到心跳过期事件之后,会触发对于问题实例节点未完成的任务再分配,即更新任务的状态为resechedule,并重新通过任务发布器发布到消息队列上。
      • 问题实例节点在缓存服务上的partition信息会被删除,防止新的任务被分配给问题实例节点。
  5. 初始化模块

    • 消费实例节点启动或者重启的时,任务队列会从数据库中加载分配给自己的但是还没有执行的任务,并优先执行这些任务,保证任务的不丢失。
image.png

6. 增益效果

本方案基于发布/订阅模型的事件驱动架构,将事件的触发和任务的执行完全解耦,充分提高了分布式任务调度系统的吞吐量,并强化其弹性伸缩的能力,具体优化点如下:

  1. 基于消息队列特性,任务事件以消息的方式被合理地消费,不会有事件丢失或者重复消费的问题;
  2. 采用事件驱动框架,事件的触发和任务的执行完全解耦,更加便于维护代码和业务逻辑;
  3. 基于发布/订阅模型,服务集群具有高可伸缩性 :
    • 新实例节点上线,消息队列自动分配新任务;
    • 实例节点出现问题后,将不会再被分配新的任务,其所分配任务回重新分配;
  4. 本方案因在消息发出时已经决定了任务的消费者,所以避免了基于数据库的抢锁,没有扩容限制和性能瓶颈,对于突发的海量请求有更好吞吐量。
  5. 消费实例节点高可用,即使短暂重启或者网络不可达,其未执行完毕的任务可以重新执行。

你可能感兴趣的:(基于事件驱动的任务分布式调度消费方案)