it编程 > 软件设计 > 架构设计

Go-Job让你的任务调度不再繁琐

89人参与 2024-08-04 架构设计

一、背景

在选择任务调度平台时,团队遇到了一些实际的问题。现有的开源项目如xxl-job、elastic-job,虽然功能强大,但主要是围绕java设计,而我们团队主要使用go语言进行开发。这使得我们在集成和使用这些工具时遇到了诸多不顺。经过深入的调研和讨论,决定开发一个适合go语言的任务调度框架,以满足我们的特定业务需求。于是,go-job应运而生。 为了让大家有个全面的了解,接下来主要探讨它的架构设计和功能特性。 本文的另一亮点是借助gpt生成了一些精美的章节彩色插图。看看大家和gpt是否是"同款"理解!

二、架构设计

go-job的目标是充分利用go语言的优势,提供高性能、易扩展的分布式任务调度解决方案,满足不同业务团队的复杂需求和快速变化的技术环境。就像量脚定制鞋一样,专为go项目量身打造!

整体架构

首先,让我们先认识一些关键术语:

1.jpg

从上图可以看出,整个架构可分为三部分:

任务调度流程如下:

让我们用一个生动的比喻来理解这套架构:

想象一下,你在一个大型积木乐园工作,任务就是搭建各种酷炫的模型。你有一个控制台(web控制台),可以配置不同的积木任务。在这里可以创建新的模型任务、编辑已有的任务、查看模型的历史记录,还能控制谁有权参与搭建。

接着,你有一位超级智能的朋友(调度服务 ),他会根据你设定的规则,定时提醒你什么时候该搭建什么样的模型。每当时间到了,他就会生成一个新的任务,并指派给最适合的搭建员(执行器)。

这些搭建员会认真执行每一个任务,拼接积木块(task)并汇报他们的进展。这样一来,你就可以轻松管理和监督整个搭建过程,而每个模型也会在指定的时间内完成。

虽然这个架构看起来复杂,但每一个组件就像拼接积木一样简单易懂。通过控制台、智能调度和执行器的配合,可以高效、灵活地完成各种任务。

2.jpg

(gpt理解的架构)

服务端设计

说完整体架构和任务调度流程,再来看看服务端设计。go-job的内部运作就像积木拼接。每一块积木都有自己的角色和作用,拼搭在一起形成一个高效的任务调度系统。

服务组成

3.jpg

从上图可以看出,go-job的服务层由三部分组成,它们共同构成了系统的基础:

通过这些模块的协作,系统能够高效地处理和调度任务,确保每个任务都能找到最适合的执行器来完成。

4.jpg

(gpt理解的服务关系)

任务设计 紧接着,我们来深入探讨任务设计的细节。任务设计是确保系统高效运作的关键部分,包括任务的生成、匹配、状态变化以及执行过程中的各个环节。

5.jpg

当用户创建触发器时,系统首先根据调度规则计算最近一次需要执行的时间,生成一条触发事件,并插入到任务队列中。任务管理模块异步获取到期的触发事件,负责生成相应的task任务,并将任务推送到matching服务中。

matching服务接收到任务后,负责将其与合适的客户端进行匹配,并推送任务给客户端执行。客户端执行完任务后,将结果上报给任务管理模块。任务管理模块更新本次任务的结果后,重新计算下一次需要执行的时间,并再次将触发事件推送到任务队列,进入下一轮调度循环。

一些问题思考:

在任务调度整个系统中,任务匹配是确保任务能够被正确执行的关键部分。如上图所示,任务匹配模块设计分为三个主要部分:任务队列、客户端请求缓存、匹配模块。

1. 任务队列 (taskqueue) 任务队列是待调度任务的存储单元,所有待调度的任务都会维护在任务队列中。具体细分如下:

2. 客户端请求缓存 (clientcache) 客户端请求缓存是所有通过sdk接入的客户端节点的集合,这些节点的任务获取请求都会统一维护在该缓存中。

3. 匹配模块 (scheduler) 匹配模块是任务调度的核心部分,负责在有新任务生成或新节点接入时触发匹配动作。每次匹配尝试包括过滤和匹配两个主要步骤:

任务匹配流程:

4. 任务下发:完成匹配后的任务和客户端进行双向绑定,并将任务下发到客户端执行。 通过实现脚本纬度的队列管理确保了不同类型的任务互不干预,同时通过对过期/超时资源的异步处理,提高了任务处理的时效性和系统资源的利用率。调度过程中的filter&match模块作为核心中间流程,支持灵活的扩展和变更,为未来实现更复杂的调度策略提供扩展基础。

7.jpg

在某些业务场景中,单个任务的执行时间过长,为了解决这一问题,我们采用"分而治之"的策略。在创建触发器时,通过设置分片数量,可以将一次任务调度拆分成多个子任务,并发推送到不同的节点执行,同时会携带分片信息。这样不仅能提升任务执行的效率同时也能提升节点资源利用率。

分片机制示例

如上图所示,有五台集群机器,当前有两个任务task a和task b。假设它们的总处理时间都是 8 分钟:

通过分片机制,task b的执行时间缩短为task a的1/4,资源利用率提高了4倍。

代码示例:处理分片任务

在业务代码层面,我们可以通过gettaskinfo方法区分不同的子任务。以下是一个示例代码:

func (w *hellohandler) do(ctx job.context) error {
    info := job.gettaskinfo(ctx)
    fmt.printf("完整参数 :%s \n", info.getparam())
    fmt.printf("分片参数 :%s \n", info.getshardparam())
    fmt.printf("当前执行id :%s \n", info.getruninstanceid())
    fmt.printf("分片id :%d \n", info.getshardnum())
    fmt.printf("任务id :%v \n", info.gettaskid())
    ...
    return nil
}

通过上述代码,我们能够获取并处理任务的分片参数和分片id,从而避免业务层不知道应执行哪部分任务的问题。

8.jpg

在系统中,每个任务都有一个完整的生命周期。从任务创建到执行完成或失败,任务会经历多个状态转换。下面我们通过图示详细说明任务的生命周期及其各个状态的变化过程。

从图中可以看出,任务的生命周期可以分为以下几种状态:

任务与执行器

在实际业务场景中,不同脚本的任务量级差异较大。为了保证不同脚本之间任务处理的时效性互不影响,我们在任务匹配的设计上采用了命名空间+脚本id组合生成唯一索引的方法。这个索引用于将不同脚本的任务维护在独立的待匹配队列中。

通过以下图片进行设计说明(其中a、b、c表示不同的业务脚本,client表示业务侧的执行器,svc则是调度服务端)。

9.jpg

从图片可以看出,不同的触发器,只要绑定的是同一个业务脚本,那么其创建的任务都会进入到同一个队列中。当多个执行器来获取任务时,会依次从对应脚本的任务队列中获得匹配的任务。

优势

下面针对脚本的任务数量变化,详细介绍下执行器的处理逻辑。

假设有3个执行器,且脚本a的任务数超过3个,此时脚本a的任务并发处理速率就是3,即同时可以并发处理3个任务。执行器可以基于其配置的调度策略和处理速度,动态进行节点扩展,以保证任务的处理速率。

10.jpg

假设有3个执行器,但脚本a的任务数少于3个,此时第三个执行器将处于等待任务的状态,直到有新的任务生成并匹配成功后才能继续执行任务。

11.jpg

12.jpg

(gpt理解的任务设计)

sdk设计

13.jpg

在go-job系统中,业务服务通过sdk接入自定义的脚本,使任务处理更加灵活和高效。整个实现包含两个部分:脚本部分和sdk部分。

脚本部分 脚本部分是用户自定义的核心,通过实现do接口来声明一个自定义的脚本。每个脚本都可以根据具体的业务需求进行编写,从而实现个性化的任务处理逻辑。

sdk部分 sdk部分是系统与脚本交互的桥梁,它包含了连接管理、数据收发、任务执行、健康检查和配置管理等模块。

任务执行

sdk中的worker模块负责具体的任务处理逻辑,包括接收任务、初始化任务上下文、执行任务和上报执行结果。

具体步骤包括:

连接管理

14.jpg

sdk内部通过grpc的双向stream流来实现与调度平台的交互操作,上图展示了如何保证grpc双向流的稳定建立过程。整个模块分为两个主要部分:建立连接和断线重连。

建立连接过程确保客户端和服务端能够成功建立grpc连接,并进行数据交互。具体步骤如下:

当客户端和服务端之间的连接意外断开时,断线重连过程确保连接能够自动恢复,并继续任务处理。具体步骤如下:

通过上述建立连接和断线重连的详细步骤,sdk和任务调度平台的连接管理模块确保了客户端和服务端之间的稳定通信。即使在网络不稳定的情况下,也能通过自动重连机制恢复连接,保证任务调度和处理的连续性和可靠性。

15.jpg

(gpt理解的sdk)

三、实战指南 要实现一个自定义的脚本,并接入到任务调度平台只需要简单的5个步骤。

代码开发

第一步: 在项目中引入sdk包

第二步: 在项目的config结构体中引入相关config

import "go-job-sdk/config"

config struct {
     jobconfig config.config `yaml:"jobconfig"`
}

第三步: 创建一个执行器结构体,并实现do接口

import (
   "go-job-sdk/job"
)

type hellohandler struct{}

func (w *hellohandler) do(ctx job.context) error {
    // 可以通过gettaskinfo方法可以从上下文中获取任务的基本信息
   info := job.gettaskinfo(ctx)
   fmt.printf("参数 :%s \n", info.param)
   fmt.printf("当前执行id :%s \n", info.runinstanceid)
   fmt.printf("分片id :%d \n", info.shardnum)
   fmt.printf("执行超时时间 :%v \n", info.runtimeout)
   fmt.printf("任务id :%v \n", info.taskid)
   return nil
}

第四步: 接入go-job平台

import (
   ...
   "go-job-sdk/config"
   "go-job-sdk/job"
   "go-job-sdk/worker"
)

func main() {
   group := worker.newworkergroup(context.background(), cfg.jobconfig)
   group.add("hello-world1", &hellohandler{}) // key 服务内唯一
   group.add("hello-world2", &hellohandler{}) // key 服务内唯一

    // 启动: 连接并注册脚本到go-job
   if err := group.start(); err != nil {
      fmt.println(err)
      return
   }

    // 该方法会block在这里. 若不需要block, 可以不调用wait方法
   if err := group.wait(); err != nil {
      fmt.println(err)
      return
   }
}

第五步: 配置config

jobconfig:
  app: "demo" # 脚本所在服务名称
  disable: true # 是否停止job注册 默认:false
  jobcenterservice:
    rpcaddress: "xx" # 调度平台服务地址
    namespace: "test" # 命名空间
  handlers:
    hello-world1: # [脚本名称]关闭指定名称的脚本
      disable: true

截止到这里,自定义执行器就已经创建好了,接下来可以去调度平台创建触发器。

触发器创建

16.jpg

目前触发器的创建支持丰富的配置设置,包括:

17.jpg

任务查看

18.jpg

19.jpg

控制台支持查看触发器的调度历史,以及每个任务对应的执行详细,包括客户端ip、trace等信息。

四、成果与展望

自项目上线以来,项目已成功接入公司内部多个业务团队,通过持续迭代功能,基本满足了公司不同业务需求。未来,将进一步增强平台能力,主要包含以下几个方面:

以上是文章的完整内容,感谢大家抽出宝贵的时间阅读。

最后,让gpt为大家呈现一张全局架构插图,感谢观看!

20.jpg

*文/ fred

本文属得物技术原创,更多精彩文章请看:得物技术

未经得物技术许可严禁转载,否则依法追究法律责任!

(0)
打赏 微信扫一扫 微信扫一扫

您想发表意见!!点此发布评论

推荐阅读

缓存有大key?你得知道的一些手段

08-04

手把手案例!怎样拿开源的 GPT-2 训练小模型,挑战 GPT-3.5

08-04

吵了6年的数据库话题,会在冯若航这里终结吗?

08-04

纯php编写rtmp服务

08-04

2024 年了,云原生与微服务架构还有什么新鲜事儿?

08-04

一款新奇的操作系统:GodoOS,您的全能办公伙伴!

08-04

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论