百度搜索 exgraph 图执行引擎设计与实践
作者 | 搜索 Go 研发组
导读
百度搜索 exgraph 图执行引擎设计重点分成三个部分:图描述语言、图执行引擎、对接扩展。
图描述语言是一种基于文本可读的图描述语言,用于描述任务中的算子以及算子之间的依赖关系,即让人可以理解,也可以被计算机理解并执行。
图执行引擎是 exgraph 的核心,负责根据图描述语言生成的图语法树进行高效执行。它支持如串行、并行、中断、选择等范式,以满足不同场景下的需求。
对接扩展则提供了与其他协议框架的接口,方便用户将 exgraph 集成到现有的系统中。
总之,exgraph 图执行引擎设计的目标是实现高效、灵活的任务编排,以满足复杂逻辑处理需求。
全 4430 字,预计阅读时间 12 分钟。
01 背景
搜索展现架构承载模版选择、实时摘要补充、展现数据适配、结果渲染等职责,当前由 PHP 开发、HHVM 执行,对接数十个产品线,数百个精细化的展现策略由 100+RD 共同开发。随着搜索业务产品日益复杂和生成式大模型产品开发需要,展现架构面临以下难题:
1、HHVM 基础设施停止维护,且不支持异步并行支持,架构升级难度大;
2、历史累计的多个展现策略框架分布在各个阶段,且各自参数不同,研发难度大。
通过调研,了解到 DAG 有向无环图,将 DAG 图中顶点描述为业务拆分后的一个个算子,边及其方向作为执行顺序,一对一作为串行执行,一对多作为并发执行,即使是很复杂的业务也可以用这套逻辑进行表达。且代码实现较简单,还能用 graphviz 将 DAG 图生成图片,将整个逻辑可视化。
△算子化后的逻辑执行视图
好像很完美~~
但似乎还有些问题:
1、对于简单逻辑,DAG 图不复杂,用 graphviz 构建图也很简单,但一旦顶点数量爆发,可阅读性急速下降。而不幸的是,搜索的 PHP 模块几百个策略,如果迁移进来,预计会有几百个顶点,构建这个图以及这个图的可读性,依然很差;
2、简单意味着功能弱。
比如搜索有多种版式:手百内、手百外、纯 NA 渲染等,下游顶点根据上游顶点的执行结果来选择不同的版式渲染。这种场景下只能呆呆的在每个版式顶点内自行判断是否执行,而不能由上游顶点直接选择一个版式分支执行。
比如执行到某个顶点,发现后续不用执行了,逻辑执行没有好的退场机制。
各个算子间传递数据怎么处理。
...
02 图执行引擎
DAG 能满足大多数场景的需要,但依然不够。所以搜索设计了一套超集于 DAG 的图描述,并在这个描述上,添加逻辑执行的高级功能,与 web 框架进行融合,逐步诞生了 exgraph 图执行引擎。
exgraph 图执行引擎设计重点分成两个三个部分:图描述语言、图执行引擎、对接扩展(用来对接协议框架)。
2.1 图描述语言
2.1.1 核心语法
算子:业务执行的最小单位,通常一个单词就是一个算子(语法单独定义的关键词除外)。
串行组:即两个算子按照顺序执行,在图上表示为用箭头连接:
△串行组
并发组:即多个算子并发的执行,在图上用中括号[]包围:
△并发组
属性:图上所有用大括号{}包围的,都是属性。属性用于通过图描述传递参数给代码。
△属性
算子、串行组、并发组都是一个执行单元,意味着,他们可以互相包含(算子是最小的执行单元,不能包含别的执行单元)。比如:
△互相包含
上面的这个描述,用人话说就是:
1、执行 a 算子
2、并发地:
执行 b 算子,
执行 c 算子,然后执行 d 算子,然后执行 e 算子
执行 f 算子,然后再并发地执行 g 算子和 h 算子
3、最后再执行 i 算子
子图:主图支持通过文件引入的方式,引入另一个图嵌入到主图
△主图引入 sub_graph 子图
通过上面简单的介绍,你已经掌握几乎全部图描述语言语法了,可以开始思考,将自己所负责的业务如何用图进行描述了。
另外,为了更好的适配业务场景,exgraph 还设计了几种指令来处理特殊场景。
扩展指令
START 指令:图开始的标记,用做给图设置属性。
△START 指令
目前 START 指令用来指导创建 HTTP 的 handler,直接让图引擎承接 http 处理、streaming rpc 处理请求。
MIDWARE 指令:包装含义。
△MIDWARE 指令
可以在执行 c 算子前,先执行 b 算子,并控制是否执行 c 算子;也可以在执行 c 算子前后,执行一些通用的逻辑。
SWITCH 指令:选择执行分支。
△SWITCH 指令
可以在switch_pc_or_wise算子内,选择执行哪个分支。
基于图描述语言,用纯文本的方式就可以将业务整体描述,很好的解决了 DAG 图构图复杂性问题,并允许自定义一些高级用法。
2.2 图执行引擎
上面介绍的图描述语言,让“人”可以更加简单的方式了解到程序的执行流程,但也仅仅只是个描述而已。
如何让其按照我们设定的描述将逻辑跑起来呢?
首先介绍一个重要的、执行单元必须实现的接口:
其中*Context 负责传递所有信息到各个算子,提供:算子选项(算子{}附带的内容)内容获取、数据传递等功能。
在上面的章节中讲到算子、串行组、并发组都是一个执行单元,其实就是说,它们都实现了 Job 接口。
exgraph 图执行引擎是:将图解析后的语法树作为入参,搭配全局算子注册,让算子按照预定的规则执行起来。
它的执行过程近似于:
em~~ 简单的有点像把大象放冰箱的过程,但实际远不止如此。
想一下,如果你执行到 a 算子,发现没有必要执行 b 算子了,怎么办?又或者 a 有数据要传递到 b 算子,怎么办?
2.2.1 对象容器
exgraph 中实现了一个并发安全的对象容器,用户可以通过*engine.Context 提供的接口,方便的设置和获取对象,就像这样:
对象容器再存入时,将其类型作为标识符,取值时也通过相同类型的变量,通过反射赋值。
2.2.2 依赖注入和对象导出
有了对象容器,exgraph 设计了支持基于 struct tag 的对象依赖注入和导出功能,且采用脚本生成代码的方式实现:
利用 struct tag 和生成的代码,用户在使用算子时,实现了以下功能:
1、inject tag 可以直接通过算子属性获取对象,省去了繁琐的取值过程,并支持:canLost=true 表示允许对象不存在,canNil=true 表示循序对象值为 nil。
2、extract tag 则允许用户直接赋值为算子属性,由生成的代码赋值将对象导出到对象容器中,且支持:canNil=true 表示允许导出对象值为 nil,repace=true 表示允许替换对象。
2.2.3 中断和跳过
为方便程序逻辑执行,exgraph 内置了几种中断跳过逻辑:
1、全局错误中断
2、全局正常中断
3、跳过串行组
2.3 执行优化
exgraph 执行的一个声明周期内,大部分对象都允许池化。
2.3.1 对象池
对于算子:exgraph 内部对每个注册的算子,都是注册到一个 sync.Pool 中,算子对象在执行完成后,执行 reset 后返回到对象池内。
对于放入对象容器的对象:在 exgraph 执行引擎结束时,会循环对每个对象检测是否实现了 Release 接口,如果实现接口就会调用,用户就可以在 Release 时将对象 reset 后返回对象池内。
2.3.2 其他优化
exgraph 在执行每个算子时默认在当前 goroutine 执行,除非用户显示的给算子设置了超时时间 a{timeout="1s"}。
依赖注入和对象导出,是基于脚本生成代码的,而非反射。
03 场景案例
3.1 同路径不同逻辑
背景:搜索 PC 和 wise(移动端)同模块执行,检索路径都为/s
方案:可以用 SWITCH 选择模式,通过一个算子来判断使用哪个分支:
3.2 PHP 策略迁移 Go
背景:搜索展现架构当前逐步由 PHP 迁移到 Go。在过渡期,PHP 代码迁移到 Go 之后,需要通过抽样验证 Go 代码逻辑无误,即:命中抽样,执行 Go 代码,否则执行 PHP 代码。而且需要迁移的 PHP 策略很多,如果没有统一的机制来支持,成本很高。
方案:用 MIDWARE 指令,用 CommonDealPhpOrGoStrategy 算子作为判断包装,判断命中抽样时,允许执行 DemoStrategy1 算子,并带标识到 PHP,不执行 PHP 相应逻辑。
否则不执行 DemoStrategy1 而执行 PHP 相应逻辑。
关键的是,迁移后的 Go 算子都不需要做特殊处理,正常迁移代码加上 MIDWARE 就能支持以上功能。
搜索技术平台研发部正在招募 AI 研发工程师,欢迎感兴趣的同学投递简历:
linzecheng@baidu.com
——END——
推荐阅读
版权声明: 本文为 InfoQ 作者【百度Geek说】的原创文章。
原文链接:【http://xie.infoq.cn/article/858dfa3f6fe767b696de7fb8f】。文章转载请联系作者。
评论