百度垂类离线计算系统发展历程
作者 | 弘远君
01
02
a.原始离线处理系统: 本阶段主要是实现业务加工入口从0到1的构建。整体上还没有形成完备的框架体系并且开发成本较高,并且所有业务逻辑混在一个公共服务中,不同数据通过不同配置调用不同的策略逻辑。
b.业务离线处理架构: 本阶段初步形成一整套的业务服务处理框架,有统一的服务框架和开发阶段,实现了云原生和服务隔离的模式。
c.Serverless架构: 本阶段业务的接入效率上进一步提高,业务从管理服务面向转向管理业务加工函数,业务只需注册函数就可以快速测试上线,同时支持容器实例的自动伸缩,使得在业务的使用效率得到的极大提升的同时相应资源成本急剧压缩。
d.数据智能架构: 在原有服务部署的基础上同时实现了数据的管理,从函数管理进一步升级成为需求管理,实现多语言服务框架支持的基础上,成本进一步压缩&效率提升。
03
3.1 原始离线处理系统
3.1.1 业务特点
多数业务,核心诉求最主要的是通用能力的建设。
个别业务,有少量简单的自定义加工处理的需求。
3.1.2 核心设计
用户数据代理:主要是为了适配业务数据接入方式,数据的协议打平,主要是不同用户协议的RPC做建库层的数据转化。
用户通用需求:主要是为用户共性需求做统一处理,会根据不同业务的不同配置做功能处理:比如业务图片视频的转化处理,业务用户版本管理处理以及用户的核心机制等。
统一数据加工模块:主要是为了统一用户数据的加工处理,主要是给所有接入的业务的数据提供了一个可以数据加工的入口。
3.2 业务处理架构
3.2.1 业务特点
效率:业务在相同的模块里面开发,导致业务开发上线的过程中经常遇到冲突,以及上线排队的问题,导致整体生效周期比较长。
稳定性:由于这种业务流量混合的模式隔离性比较差,导致单个业务出问题,经常会影响所有的业务。
效率:服务开发上线的过程是完全隔离的,业务在开发上线的过程中完全没有业务阻塞,整体服务上线周期从天级缩短到小时级别。
稳定性:由于每个业务服务执行逻辑都有完全独立的app,导致业务的稳定性大大提升,不会存在业务之间的互相影响。
3.2.3 核心设计
业务处理框架:建设一个数据处理框架,业务可以基于这个框架来实现自己的业务处理功能。
任务平台:每个任务注册、升级、管理都可以通过任务平台来进行管理。
统一网关:所有数据的统一入口,统一接收上游数据,并且做数据的转发和分发。
业务APP:每个业务都有自己独立服务的app,每个业务开发是基于框架的独立分支开发,上线是每个业务单独上线,业务处理的服务流量也都是完全隔离的。
3.3 Serverless架构
3.3.1 业务特点&解决问题
业务学习成本:大量的新业务接入需要进行开发,导致框架本身学习成本需要几天的时间,新业务的接入和开发的时间基本上都在周级别。
资源如何节省:大量的业务也带来app的高速膨胀,原来接入的数百台机器已经用满,到了资源瓶颈,当前的资源量无法支持业务的快速膨胀。
3.3.3 核心设计
应用层:针对于基础服务架构,上面给业务开放的各种云服务,从业务的 接入、开发到后面的调试、测试,到服务上线后的监控。
服务层:这部分是整体架构的基础,应用层都是基于这里进行开发的,这部分是整个系统的基石,属于整体系统的核心框架。
调度层:通过根据流量形式的扩缩容,保证后续服务可以自动化的进行服务操作。
极致抽象的业务框架:是核心框架基础中的基础,提供新的开发范式同时,为后续智能调度奠定良好基础。
高度复用的基础服务:强大丰富的后端服务能力封装,支持业务低成本复用,降低开发成本同时提升稳定性。同时系统还提供强大的编排能力,低成本支持业务从简单到复杂的发展。
极致抽象的业务框架
如上图左边就是用户实际的执行代码,对于函数的接口定义:入参和返回值都是Dict类型。业务的代码逻辑直接在函数中实现,需要变更的数据,如上图所示业务在根目录下增加了一个时间戳字段,然后把更新后的结果传递给下游。
高度复用的基础服务
架构通用能力:包括索引处理(倒排、正排、向量索引),数据审核(政治敏感数据/色情数据识别&过滤),多路分发、数据建库等能力。
与业务联合研发的能力:数据的低质过滤能力(实现数据清洗/归一化/数据去重/类目拼接),数据多元融合,数据质量打分计算(质量打分/作弊识别/物料打分)。
基于公司强大基础能力: 多媒体处理服务(外链转内链/OCR/水印计算/重复图计算/主体识别/视频转储等),自然语言处理服务,数据沉淀服务。
3.4 数据智能架构
3.4.1 业务特点
效率业务的迭代开发很多都是针对少数几个字段,但是当前业务同学仍然需要了解服务全拓扑才能开发。
随着业务复杂深耕业务的开发迭代业务开发者出现多语言开发情况来提高服务的开发和执行效率。
业务如何实现新类目的快速接入、在不了解全面的情况下快速迭代。
计算引擎执行效率更高,用更少的资源计算跑更多的服务资源。
业务问题能不能快速定位&发现以及问题的自动处理。
3.4.2 核心思路
数据管理:出了原始服务管理外,引入数据全面管理,为列计算奠定基础。
编排能力:将原来业务手动编排的函数能力扩展成为需求声明式自动编排的能力。
服务处理:极致高效的服务处理能力,支持多种不同业务开发者的高效开发效率,将整体的计算从行计算转向列计算,提高整体的计算复用。
控制能力:将原来自动伸缩的能力扩展成为智能控制能力,做到问题的自动识别、自动分发、自动分析 和 自动处理。
3.4.3 核心设计
应用层:业务直接可见的相关服务,包括业务从开始数据接入到服务运行的全周期各阶段各种应用。
逻辑层:也可以称之为编排层,负责将原始的业务需求表达转化成为线上真实运行的服务, 业务通过Codeless 平台化选择勾选自己的功能集合,以及对应数据映射关系进行提交,将用户提供的功能 & 数据的绑定关系转化为业务的自定义的功能 以及映射关系。
服务层:计算系统的核心,业务实际计算运行在这层,主要包含计算引擎 和服务架构两部分。向上承接逻辑层的编排结果运行服务,向下提供基础信号作为控制层的输入。
控制层:包含智能调度和智能控制两部分:智能控制主要是通过自动接受业务指标数据进行智能控制保证服务稳定,而智能调度是除了根据数据流量进行进行自动伸缩以外还可以根据业务服务关系进行流量复用,减少业务的重复计算。
需求表达逻辑
业务数据:业务数据需要功能的最小单位进行切割,后续算子进行配置转化以及服务绑定的列式计算。这里说的有点绕口,其实本质上就是把业务数据的原始Proto或者数据Schema注册一下就可以。
功能集合:功能集合既可以用户直接使用的系统提供的默认模版的功能集合 也可以是用户通过平台自定义的集合,值的注意的是每个功能都都有其指定的传入参数,比如图片计算,需要传入图片URL,向量计算需要传入原始数据 & 向量参数等等。功能完成后也会有对应的输出结果业务可以指定。
需求表达服务:用户的原始需求转化为功能模版的组合。这个阶段会根据用户配置,将原始算子和数据进行绑定,原始用户抽象的需求实例化成带数据的算子集合。
需求编排服务:通过业务算子的本身的依赖关系和数据依赖的血缘关系进行表达组合,将带数据的算子构建出N个有向无环图。
计算引擎实现
图计算引擎:主要控制的业务拓扑的表达关系,通过有向无环图来表达。
算子执行引擎:主要控制业务真实业务函数的执行,不同语言有不同的函数实现,例如Python、GoLang、C/C++都有自己的执行引擎。
图计算引擎实现:实现的核心功能就是实现支持表达有向无环图的框架,使用C++实现的,图下图部分中,蓝色部分就是有向无环图的框架实现,当然我们这个框架的底层实现支持了多种数据的表达:
本地队列表达:如图左下角部分表达的就是一个简单的拓扑表达,拓扑关系是通过本地的无锁队列进行串联的,每个队列下面挂载一个业务算子,业务算子处理完成数据后,算子拓扑配置管理分发到下游的本地队列中,如果是最后一个算子,数据结果全部处理完成后输出到远程队列Producer中。左侧这边是框架内部的通用功能: 远程队列表达:每个蓝色大框表示一个具体实例,实例之间使用远程队列交互,如果每个实例里面只有一个业务算子,这种的交互方式类似于远程队列的表达方式。 混合方式表达:更多使用方式是混合使用,一个巨型拓扑会拆分成多个子拓扑,每个子拓扑使用本地队列的方式进行表达,而子拓扑之间使用远程队列的方式进行表达。 多语言引擎执行层:语言执行层整体框架整体根据不同的语言有不同实现模式,大体上分成三部分:编译型、解释型、原生C++。
线程管理:每个业务算子默认都是多线程的分发模式(如果业务算子只支持单线程,可以把线程数设置成1)。
分发模式:数据消费分发模式默认是轮询分发,固定KEY分发(实现保序)以及按消费能力分发。
数据压缩:这个默认支持常见数据压缩方法,是可选的吞吐优化手段,支持LZ4、Gzip、Snappy等常见压缩算法。
解释型:最典型的就是Python实现方式,这种方式也是过去同学最喜欢用的开发语言之一。图上图最上面,左边灰色部分是C++开发的部分,最右边黄色部分是业务代码,中间这部分就是C++转Python的交互引擎调用。这种方式的本质就C++的开发引擎使用PyBind实现指定接口服务解释器,根据指定的数据序列化和反序列化方式进行操作,在函数调用时再实时转化为python的Dict。
编译型:典型是Golang的使用方式。与Python的实现类似,左边灰色部分是C++开发的部分,最右边黄色部分是Golang业务代码,中间是C++转Golang的交互引擎。Golang的实现相比Python的方式有点复杂,本质是通过原生CGO作为用户接口,为了统一用户接口层,其实分成两部分:左半部分是直接跟C++交互,直接用C++实现负责把原生C++转化为基础的C类型的函数指针进行调用。右半部分使用Golang实现负责把原生序列化好的函数反序列化成业务结构体,然后再进行真正调用。
原生C++:这里很多人可能觉得奇怪,C++不是也是编译型的么,已经有编译型的实现模式何必画蛇添足,增加这么一种实现模式,其实本质不然,golang虽然是编译型语言,底层框架的实现由于尽量考虑通用性,数据传递的过程中势必需要进行序列化和反序列操作,而在原生的C++的实现过程中我们在实现的过程完全摒弃所有的序列化和反序列操作,数据在本地队列中的传递完全是业务的数据指针(而非序列化数据),而每个业务算子在处理数据过程中,直接根据原始的数据指针通过反射机制(实现方式有很多最简单的 map)可以直接获取对应列的数据项,整个过程无锁的超高效率。这里的核心优化思路数据链式处理过程。拉链上的每个算子,都共享同一个输入和输出。每个算子,其实就是一个接口相同的函数,这样就可以随意地调整函数指针的组合,形成不同的处理链。比如:一个请求走 A 模型排序,一个请求要走 B 模型排序,他们可以共享前序的算子,只在最后一个算子有所不同。在链式处理基础上,算子也可以是继承同一基类接口的派生类,或者 lambda 表达式。结合工厂模式等一些编程技巧,处理链的调整可以配置化、动态化、脚本化。实测执行业务单算子普通的纯数据项带分支的多节点的拓扑计算单机(单线程/算子)可以达到数十万的处理能力。
智能控制实现
阶段1: 传统配置化的规则引擎的配置(上图中右上角黄色部分),配置多个采集指标项的逻辑关系(与或交非), 这里主要是针对问题的基础分析功能,判定规则是否触发。
阶段2: 基于这个基础分析的结果,进行后置Function的执行分析,这个主要是针对复杂问题的分析补充, 最终执行引擎根据这个返回结果进行函数执行。
前提: 开发者需要配置好处理逻辑规则(以及规则依赖的数据项,必填) & 回调函数(选填)。
数据解析器: 数据解析器主要承担的数据的原始抽取的工作,一共分成如下3步:
a.配置解析: 逻辑执行根据开发者配置的数据信息解析;
b.数据抽取: 根据解析出来的配置通过数据接口进行获取,可以从统一接口根据配置的信息从不同的介质充抽取所需求的信息;
c.数据归一化: 将不同介质的原始数据归一化成为统一的数据格式供规则管理器使用。
规则管理器: 规则管理器主要承担核心的逻辑分析工作,一共分成如下几步:
a.规则解析: 根据开发者配置的规则逻辑,将原始配置信息,解释成原始的规则树。
b.执行计算: 根据数据解析器提供的数据结果和配置的函数规则分别执行计算。执行计算过程中最重要的就是基础分析器,整体提供了5大基础能力,数十种常见的逻辑计算来辅助规则配置。
c.规则逻辑运算: 根据上层解析出来的规则树 和 每个数据项执行完成的计算结果进行逻辑运算,并根据执行的结果确定是否进行高级数据分析器,如果判断结果为真则根据所配置的后置函数进行处理。
高级数据分析器: 如图所示有两种模式,对于简单基础分析可以判断结果的,直接给默认的处理函数进行数据拓传;对于简单逻辑规则无法准确表达的,开发者可以自定义后置分析函数, 函数会将原始数据和基础计算的计算结果作为参数传出来,开发者只需要通过处理后的数据描述清楚分析逻辑即可。
动作执行器: 就是这个分析器的真正的执行引擎,根据规则运算的结果中包含的参数进行动态调整。
04
往期推荐
点这里 ↓↓↓ 记得 关注✔ 标星⭐ 哦
微信扫码关注该文公众号作者