服务器 频道

携程前端自动化任务平台TaskHub开发实践

  本文介绍了前端自动化任务中的难题,讲解了Taskhub如何通过拆分核心和辅助模块来应对这些挑战,并结合两个实际场景展示了Taskhub在提升自动化任务排障效率等方面的表现。最后,讨论了Taskhub使用的RPC BFF框架如何提升整体开发效率,并分享了使用RPC BFF的经验。

  一、前言

  本文讨论的自动化,是指通过代码的方式,将原本人工完成的相关操作由机器代为处理,如此达到释放人力和提升效率等目标。

  然而实现自动化的过程不总是那么顺利,自动化后的效果未必那么理想。可能有的团队会发现自动化脚本尽管释放了业务人员,但成本却转移到开发人员身上。他们需要投入大量时间去更新失效的自动化逻辑,出现问题后的排障时间和难度也随之增加。如果自动化任务的成功率低、问题修复速度慢,那么大量的工作将不得不降级为人工处理,自动化的业务价值无从体现。

  我们发现,通过完善自动化任务的相关基建(包括任务调度引擎和任务管理平台等),上述问题可以得到显著的控制和缓解。本文将分享携程旅游研发在这方面的尝试和经验,希望能为其他开发者和团队提供参考。

  二、平台背景

  旅游内部不少业务都有自动化需求,部分团队已经上线了自己的自动化项目,这些项目由不同的团队维护,但均有相似的痛点:

  无法及时关闭自动化任务:由于缺乏有效的控制手段,自动化任务一旦重启就难以中断。

  排障效率低下:目前大部分自动化任务是前端自动化任务,前端由于站点更新、网络波动、代理异常等常见原因导致任务无法完成,出错的原因很杂,不能复现场景,难以查找根因。

  日志查找困难:现有的日志系统以时间为锚点记录日志,没有划分任务的边界。一次自动化任务产生的日志通常是一个普通请求的2到5倍,需要从大量连续的任务执行日志中找到需要的信息,并且随着任务增加,存储的日志的文件数量也随之增长,查找变得更加困难。

  复现困难:任务的入参通常和日志柔和在一起,导致复现问题时难以分析。

  日志没有权限限制:任何人都可以查看日志,可能导致敏感信息泄露。

  三、平台目标

  为了解决自动化的共性问题,减少重复开发,我们希望通过创建一个统一的平台来提升自动化效率和可靠性,具体目标如下:

  提高排错效率:通过详细的日志记录和辅助工具,低成本快速还原任务场景,便于快速定位和复现。

  被动感知任务异常:建立实时监控和通知机制,实时检测并推送任务异常信息,减少人为监控负担,完全释放人力。

  聚焦业务本身:让开发者专注于核心业务逻辑,不必担心自动化过程中可能出现的性能问题,通过平台提供的工具确保自动化任务高效、稳定地运行。

  基于以上目标,我们设计开发出前端自动化任务平台TaskHub,一个能够帮助自动化提效的解决方案。

  四、TaskHub介绍

  在TaskHub中,我们把自动化脚本的执行,称为任务。

  任务是无状态的,任务与任务之间没有直接关系。任务的设计目标是实现某一特定的自动化功能。

  在自动化过程中,核心是确保任务的正确执行。任务执行过程中产生的副作用不应阻碍任务的执行。因此,我们将TaskHub主要分为两个部分:平台和引擎。平台用于记录和管理任务执行过程中的产物,而引擎专门负责任务的执行。

  平台:可以方便查看、配置、中断任务。

  引擎:负责调度任务执行,是任务执行的核心。

  整体设计:  

  引擎通过NPM的方式安装,以 SDK 的方式运行在项目中。这样做有以下几个好处:

  1)引擎和平台完全解耦。即使平台不可用也不影响任务运行,确保任务高可用。

  2)灵活部署。现有自动化业务不会因为接入 TaskHub 改变原来的部署方式,业务可根据自身需求按需部署。

  接下来分别介绍下平台和引擎。

  4.1 TaskHub平台  

  平台包含三个主要模块:项目、任务和日志。它们之间是一对多的关系:一个项目包含多个任务,一个任务包含多条日志。每个模块都有严格的权限校验,确保数据安全和访问控制。

  项目模块:项目是任务的集合。它包含项目的基本信息,并负责管理项目成员和权限,为同一类任务做统一配置。

  任务模块:包含任务的输入、输出、运行时间等信息。任务流程被结构化展示,使任务的输入和输出更加清晰,降低复现难度,提高排错效率。

  日志模块:记录任务运行的所有日志,分为业务日志和系统日志两个子模块,方便快速切换和筛查。除了传统的文字日志,TaskHub还支持图片日志。如果在自动化过程中遇到意外错误,可以截图记录错误页面,留下错误快照,方便后期排查时快速了解错误发生的场景。

  4.2 TaskHub引擎

  TaskHub 引擎的核心是调度执行用户的脚本。用户只需编写任务的 .ts 文件,引擎会为每个任务分配一个独立的 Node 子进程,每个任务都在自己的子进程中运行。

  这样设计有以下几个好处:

  1)数据隔离。借助进程数据隔离的特性,无成本实现业务数据的隔离,带来了数据的安全性。

  2)任务易清理。任务执行完成时,使用 process.exit()退出子进程即可释放资源和依赖。此前度假部分自动化任务的方案在释放资源时有较重的心智负担,需仔细编排以避免影响其他任务。

  3)任务可远程关闭。度假部分自动化场景有中断执行的需求,独立的子进程使得中断更简单,直接对子进程进行操作即可。

  4.2.1 如何初始化项目

  在 TaskHub平台 注册一个新项目后,使用 TaskHub SDK 提供的 engine 对象来初始化 TaskHub引擎。只需将项目ID传入 engine.initProject 方法,引擎就会与 TaskHub平台 上的项目进行绑定。这样,后续的任务日志、状态等信息就会与相应的项目关联起来。

  初始化引擎代码示意如下:  

  接下来,通过调用 project 的 addTask 方法,即可启动任务,引擎内部会使用子进程运行用户的脚本。  

  以上就是运行TaskHub自动化任务的核心代码。

  4.2.2 引擎内部设计

  在 TaskHub 引擎中,每当一个新任务启动时,引擎会创建一个新的子进程,并在子进程中运行任务,如下图所示:  

  在简单的自动化场景中,通过主进程启动任务可以满足大多数需求。然而,在复杂的业务场景中,仅仅依靠主进程启动一个子进程来运行任务是不够的。

  在任务执行过程中,子进程对于主进程来说是一个黑盒。主进程无法直接了解任务当前运行到哪一步,以及任务的当前状态。为了便于主进程和子进程之间的数据流转,TaskHub 引擎建立了主进程和子进程之间的双向通信机制。

  以获取任务运行结果为例,当子进程运行任务后,我们希望在主进程中获取任务的返回结果。

  我们可以在主进程中使用之前创建的 task 实例来注册监听事件,等待子进程发送消息。具体代码如下所示:  

  

  在任务脚本中,根据需要发送任务状态的更新,如下所示:  

  需要注意的是,子进程可以向主进程发送消息,而主进程也可以收发子进程的消息。这种双向通信机制在以下场景中非常有用:

  1)状态监控:主进程可以实时接收子进程的状态更新,了解任务执行的每一步骤。

  2)任务控制:主进程可以向子进程发送指令,例如暂停、继续或终止任务。

  3)异常处理:子进程在遇到问题时,可以立即通知主进程,使得异常情况能够迅速得到响应和处理。

  4)数据传输:主进程和子进程之间可以交换数据,确保任务执行所需的信息流畅传递。

  以上是引擎内部任务调度的实现,接下来介绍引擎如何与TaskHub平台进行通信。

  4.2.3 引擎外部通信

  如上面所说,TaskHub 平台的主要用途是记录自动化脚本执行过程中产生的日志、任务状态,以及其他需要被持久化的状态。所以主要通信的内容包括:任务状态的变更、日志推送、引擎轮询获取需要主动终止的任务。  

  引擎通信接口 IMessageSender

  整体设计中提到 TaskHub 引擎与 TaskHub 平台是解耦的。引擎内部定义了一份接口 IMessageSender,只要实现了接口就能与引擎共同运行,TaskHub 平台只是引擎接口的一份实现。

  接口定义如下:  

  我们期望 TaskHub 平台的可用性等级是稳定的,不期望在更高可用性等级要求的应用接入时被迫提升自己的可用性等级。所以,通信接口中关于日志的部分, 引擎对 原有的日志平台 也做了一份实现,作为TaskHub日志系统的兜底方案。

  当 TaskHub 平台不可用时,引擎与平台的通信会降级到兜底方案,此时部分能力是受限的,例如终止任务的能力。但是这并不会影响自动化任务的执行,引擎的调度能力、脚本的日志留痕等能力仍然可用,并且,所有运行日志可以在原有的日志平台获取。

  以上是引擎设计相关的内容,除此之外,TaskHub还提供了两个辅助 SDK 以补充 TaskHub 平台的使用:

  logger:帮助用户记录日志到TaskHub平台,支持文字和图片日志。

  media:集成了OSS平台,提供简单易用的API将本地图片或base64格式的图片上传至服务器,方便任务运行过程中对图像数据的管理。

  五、使用案例

  5.1 度假业务自动化数据录入

  度假业务内部有一个需求,业务人员需要定期在某个站点录入数据。后来,将数据结构化处理后,通过自动化程序定时进行数据录入,代替之前的人工操作。

  虽然这种自动化模式解决了一部分问题,但在实践中也发现了一些新的问题:

  1)排障效率低。在自动化的过程中,由于站点加载的资源很多,涉及出错的原因很杂,通过现有日志系统排障需要投入大量时间,效率低下,占用了宝贵的开发时间。

  2)无法及时关闭单个任务。目前只能通过关闭整个自动化应用来关闭某个正在运行的任务,这个过程不仅会关闭其他正在运行的任务,而且整个关闭的流程很长,无法及时关闭。

  在接入TaskHub后,自动化系统的容错率和排障效率得到了显著提升。

  收益:

  及时关闭任务:每个任务运行在不同的子进程上,可以杀掉子进程立即关闭单个任务。

  提升日志排障效率:除了可以及时关闭外,TaskHub 显著提升了查找日志排障的效率,节省了大量时间。

  以下图表对比了传统日志查找与TaskHub任务日志的差异:

  传统日志查找:开发人员需要从大量日志文件中找到错误发生时的日志文件,然后再从文件中定位到异常任务的错误日志,费时费力,容易遗漏关键细节。  

  TaskHub任务日志:TaskHub为不同任务划分边界,不再需要从大量杂乱的日志中寻找关键信息。每个任务的日志被结构化记录,便于快速查找和定位问题。此外,任务的输入输出也清晰可见,可以快速复现场景。  

  任务详情  

  任务日志

  5.2 复杂业务场景的自动化改造

  在更为复杂的业务场景中,自动化需求往往更加多样化。例如,当前有一个需求:人工需要定期在某个站点上查看页面特定元素的状态,如果满足某些条件,则去另一个站点录入数据。

  为了将该项目进行自动化改造,可以将场景拆分为以下两个需求:

  需求1:在某个站点上通过查找页面来获得业务数据。

  需求2:定期在某个站点上录入数据,录入数据之前需要判断【需求1】中的业务结果。

  具体实现方案:

  需求1:设计成一个接口,通过调用接口返回的结果来获得业务数据。

  需求2:设计成定时任务,定期执行。每次执行之前先请求【需求1】的接口,判断条件是否满足。

  这是很自然的自动化改造,能够释放人力资源,并且任务也被合理地拆分。然而,在实际运行中,可能无法完全达到预期效果:

  1)人工并未完全释放:加入自动化后,人工仍需定时关注自动化任务是否正常运行,无法彻底摆脱手动监控。

  2)排障复杂度增加:随着前置任务的增加,自动化排障的复杂度直线上升。当【需求2】的任务运行出现异常时,如果发现是由于前置的【需求1】出现问题,就需要根据错误的发生时间去【需求1】的机器上查找日志。若有多个前置任务,排查成本将大幅增加。

  我们来看下加入TaskHub之后,会有哪些改变。

  1)任务异常主动通知:从原来的主动查看任务状态,转变为被动接收任务异常通知。任务出现异常时,系统会主动提醒,减少了人工监控的负担。

  2)快速排障:通过捕捉错误快照,将其记录到 TaskHub 的图片日志中,开发人员能够快速定位并解决问题。

  3)清晰的排障链路:TaskHub 不仅提升了单一任务日志的查找效率,对于多个串联任务也同样适用。通过在下游日志中打印上游任务ID,可以快速查找上游日志,无需在多个机器日志中来回跳转。

  下图展示了一个三个串联任务的排障例子。  

  值得注意的是,TaskHub 并没有改变原有项目的设计,只是将任务的运行方式从 Node 转变为 TaskHub 引擎。

  TaskHub 将自动化中的任务概念独立出来,本质上,任务就是脚本的执行。无论任务是通过接口调用、定时函数还是定时器唤起,任务的唤起方式虽然不同,但任务执行的逻辑保持一致。这样一来,开发者可以更专注于任务的逻辑实现,再根据需求唤起任务即可。

  到目前为止,已有 12 个自动化项目使用 TaskHub 运行,累计完成了 48w 次自动化任务,记录了 1300w 条日志。

  六、RPC BFF优秀实践

  在进行 TaskHub BFF 的技术选型时,我们对比了当前多种主流的技术栈。由于 TaskHub BFF 有多个调用方,并且在客户端和服务端都有消费场景,综合考虑上手成本和多个消费者之间的同步成本等因素,最终选择了 RPC BFF。

  关于RPC BFF 更多介绍可以查看文章《携程度假基于 RPC 和 TypeScript 的 BFF 设计与实践》。

  传统服务开发中,服务提供方需要撰写服务代码,然后在第三方平台同步接口文档,消费者再根据文档约定在不同调用环境中使用这些接口。  

  而在 RPC BFF 模式的开发中,服务提供方撰写服务代码之后,消费者只需通过一个命令就可以获得接口的调用文档和调用函数。这种方式不仅简化了开发流程,还提高了接口调用的一致性和可靠性。  

  在使用RPC BFF的过程中,我们也总结了一些优秀实践,希望能够给大家带来一些启发。

  6.1 收敛类型

  在设计 RPC 接口的返回值时,有些开发者可能会沿用朴素函数的设计思路,即当接口返回成功时,返回一个成功标识和数据;当失败时,则返回一个失败状态和错误码,让消费端根据不同的错误码进行相应操作,如下所示:  

  这种设计存在以下几个问题:

  1)逻辑不够清晰:简单地将返回结果分为成功和失败,实际开发中可能还有“可接受的错误”或其他复杂的状态,扩展性不佳。

  2)错误码维护复杂:前后端都需要维护同一套错误码,增加了开发和维护成本。

  3)错误处理遗漏:编码时容易遗漏未处理的错误状态,增加了系统不稳定性。

  其实,可以使用 RPC BFF 的 Union 类型,将所有可能返回的状态联合起来,合并为最终返回的类型。  

  通过聚合所有返回结果,不再需要维护同一套错误码,并且通过类型系统就能确保所有可能的状态都被处理。如下图所示,在消费返回结果时,代码编辑器中有代码提示,不会遗漏任何状态。

  如果接口或类型有更新,只需要一个命令就可以同步更新接口和接口类型。  

 

  6.2 原子化过程

  RPC BFF 的核心理念是面向函数的接口编程,得益于它对底层通信细节的封装,开发者只需考虑函数的功能即可。

  与 RESTful 优先资源的理念不同,RPC BFF 的原子是一个过程。REST 将资源暴露出来,而 RPC 是将过程暴露出来。

  所以,可以通过分析服务流程(过程),将其拆解为最小不可分割的流程,再将这些流程通过函数实现。

  只需保证所有原子化的过程都被很好地处理。在原子过程之上的过程调用,只需按需组合这些原子过程,从而形成一个过程流,最终形成一个有向无环图(Directed Acyclic Graph)。  

  例如,有一个接口接收一个 ProjectId ,并返回该 ID 对应的项目信息。如果未获取到项目,则返回未找到。  

  getProjectById既可以对外暴露,也可以在其他函数中调用。

  假设有一个业务流程,需要首先检查项目是否存在,然后根据项目状态执行不同的操作。如下图所示,在 getProjectSetting 中,我们调用接口的方式就像调用本地函数一样,并且利用前面说的返回值类型组合,清晰地完成代码逻辑。 

  通过这种方式,有以下几个收益:

  1)高复用性:原子化的函数可以在不同的业务场景中复用,减少代码重复。

  2)清晰明确:每个函数只负责一个具体的操作,逻辑清晰,易于维护和测试。

  3)组合灵活:可以根据业务需求,灵活组合原子化过程,构建复杂的业务逻辑。

  七、结语

  通过重新梳理整个自动化流程,我们拆分出了运行自动化任务的核心部分和辅助模块。在保证自动化任务高可用的基础上,提高了自动化的整体效率和容错率。考虑到自动化场景的复杂性,我们特别设计了主进程与子进程的双向通信机制,以应对各种自动化场景的挑战。

  此外,我们还提供了两个自动化场景案例,详细分析TaskHub如何帮助提升自动化效率。

  最后,我们结合团队在RPC BFF的实践,分享了一些使用经验。

  未来,我们将继续探索更多的自动化场景,并不断完善TaskHub。

0
相关文章