战略引擎的设计与运行 快手 Dragonfly

从 2018 年开局,快手的整个业务出现极速开展的形态,团队也在极速扩张中。在过去的五年中,DAU 从 1 亿增长至 3.76 亿。在 2021 年,快手的 DAU 曾经超越了 3 亿。关键介绍场景也从早期的发现页、关注页和同城页等几个关键页面,扩展到了如今的上百个介绍场景,包括电商、直播、增长、海外及本地生存等等。

随同着业务的极速开展,研发团队从几十人扩展到了上千人。在这种背景下,业务方面发生了两个关键诉求:第一个是宿愿极速搭建一个新的介绍场景;另一个是极速复制有效的战略。

早期,为了满足这两个诉求,开发团队选用复制已有性能的架构代码,以减速开发环节。但是,随着场景数量的始终参与,这种形式已难以接受。因此,从久远来看,快手须要从新扫视并优化现有的代码架构,以确保其能够顺应未来的开展需求。

同时,这种做法还会参与整个架构的保养难度。在快手,架构和算法人员配比拟低,造成架构工程师压力较大。在线系对抗切代码都是用 C++ 言语编写的,随着算法团队人数的参与,系统越来越复杂,对线下代码品质的把控变得愈加艰巨。经常由于代码 bug 造成线上稳固性疑问。

另外,工程团队和算法团队之间存在自然的目的差异。例如,算法团队谋求极速迭代和试验上线,而工程团队更看重系统保养老本。因此,快手须要平衡两个团队的需求和目的,以确保整个系统的稳固性和可保养性。

另一种重大的状况是,名目的整个算法和工程代码写在同一个上班空间中,造成代码存在重大的耦合现象,就像 DNA 双链结构,彼此相互撑持,又相互纠缠在一同。这就象征着双方都或许对对方发生一些异常的影响。

在这样一个形式下,工程团队往往会堕入循环重构的怪圈。随着系统迭代期间的始终增长,整个系统的复杂度也逐渐提高。当到达必定阈值时,工程团队须要投入少量人力启动系统的重构。重构成功后,系统的复杂度会降落到必定水平,但随着期间的推移,经过一两年的迭代,系统又会变得复杂起来,须要再次重构。这种循环往返的现象每 1~2 年就会出现一次性。

每次启动这样的大型重构,对工程团队的消耗十分大,造成团队成员很难集中精神去关注其它更有价值的架构更新。因此,如何冲破这个循环重构的宿命就成为了一个急需处置的关键疑问。

经过深化剖析,团队发现了疑问的外围要素是业务代码和架构代码之间存在适度的耦合。为了处置这个疑问,快手自主研发了一套战略引擎框架。上方将详细引见该框架是如何处置这些疑问的。

Dragonfly 在定位上是一个面向搜广推畛域的通用图引擎框架及其周边工具所构建的开出现态。该系统为快手外部搜广推服务提供了一致的基座引擎,同时为下层业务提供了灵敏的流程编排才干。

在底层,内置了一些高效的数据模型,并提供了丰盛的周边工具。上图展现了整个架构,最下层是各个搜广推畛域的战略引擎,上方是允许战略服务、召回服务、粗排精排服务的外围层,最底下是图调度引擎。经过DSL算子编排,成功地将算法的开发形式从 C++ 为主转变为以 Python 为主。

上图中展现了一段代码示例,以 Dragonfly 编写战略的形式出现,十分直观。代码定义了一个 flow 流程对象,相似于上班流的概念。在这个 flow 中,提供了很多方法,每个方法面前都有一个相应的算子。经过脚本,可以很容易了解这个流程。前两个方法是两步召回,从索引和服务中做召回,之后启动去重、曝光、过滤等。再依据 dislike 特色启动过滤截断。接着进入下一个阶段,启动多样性的打散,而后启动截断并前往。

经过 Python 的 DSL 形容,算法同窗可以不编写 C++ 代码,而是经过 Python 脚本繁难、直观地形容一段战略流程,这个 DSL 脚本将被编译成一个 Json 文件,交给线上的 C++ 服务运转。这样既享用了 Python 编写逻辑的便利性,也没有就义线上的性能。

为了成功这个效果,咱们做了两个外围的形象:流程形象和数据形象。

应用算子化的方法加上 DAG 来拆分和形象整个业务性能为各个算子。这些算子包括一些通用的算子,如过滤、召回、模型预估等。这些算子基本上由架构编写和保养,各个业务可以间接复用,无需重复编写。

经过自定义算子来满足一些通用算子无法满足的定制化的业务逻辑。这些自定义算子可以由业务人员自行编写,以成功高度灵敏的需求。

经过经常使用先前展现的 Python DSL,开发人员可以轻松编排这些算子。并且,由于这个脚本实质上是一段 Python 代码,因此可以在此基础上应用 Python 自身语法才干成功更复杂的代码拆分及模块化控制等性能。

整个 DAG 构图形式是基于数据驱动的隐式构图,因此,一切的算子都可以做到全流程漂移。

如图所示,假定咱们有一个蕴含六个算子的 flow,其中 B、C、D 三个是异步的算子,它们区分有下游依赖 E 和 F。依据数据依赖相关和拓扑序,可以惟一地反推出一幅 DAG 图,其中 B、C 是并行的相关,B、C、E 全体与 D 也是并行的相关。因此,整个流程的处置结构就像上图中部所示。

经过这样的构图机制,通常上可以构建出恣意复杂的业务逻辑和业务流程。这里提到的可全程漂移象征着,假设将各个方法随便换位置,那么构图的结构也会智能出现变动。

最后,整个流程的调度是一个全程无锁的设计,以允许在线的高并发需求。

除了流程形象,Dragonfly 具有的另一项关键形象才干是对数据的形象。它提供了一种高性能的数据结构,叫做> 从逻辑上看,DataFrame 表结构就像图示的二维数据表,以 item 侧的数据为例,每行代表一个 Item,每列代表其属性或特色。Common 侧的数据实践上也是相似的,但由于 common 的数据关于一切item都是共享的,所以作为底层经常使用一个特化单行的> 此外,Dragonfly 框架对结构启动了诸多性能优化,例如零拷贝技术,这种技术贯通于索引数据以及DataFrame间的数据传递等各个方面。

Dragonfly 框架更初级的性能是对逻辑表的片面允许。在复杂的业务场景中,或许须要处置一张大型物理表,每个团队只有要专一于其中的一局部流程。因此,可以基于底层物理表创立逻辑表,这一律念相似于数据库中的视图。与视图的只读不同,Dragonfly 架构创立的逻辑表具有可读写性,可以作为其余团队划分数据操作空间的参考。经过这种形式,整个团队可以更明晰、更灵敏地控制其数据操作空间。

经过一致数据接口,咱们得以轻松地启动数据读写管控,可以随便地梳理并监控在线数据的读写经常使用状况,包括能否存在不合规的数据经常使用。整个框架内置了安保保证机制,确保了数据的并发安保。

前面引见的是框架底层的外围才干,但用户关键会感知到的是 DSL 这一层。Dragonfly 提供了一些更高阶的形象才干,如规范算子的封装,用户可以间接经常使用。关于用户来说,同步或异步是无感的,只有要繁难地调用算子接口,无需关心底层是同步还是异步成功。

此外,Dragonfly 在底层提供了分主流程控制、数据并行计算等高阶性能。如上图展现了数据并行计算的示例,假定要计算某个分数,类型为 double,假设每个计算的分数须要一毫秒,那么 8 个串行计算就须要 8 毫秒。但是,实践上每个分数的计算都是独立的,可以将其视为数据上的并行操作。经过框架提供的 @ parallel 装璜器,可以指定参数将数据分片,每片蕴含 4 个 item,每个线程处置一片数据,成功并行操作,将 8 毫秒降落到 4 毫秒。原理上跟向量化减速是一样的。

关于流程,Dragonfly 提供了@async装璜器协助将子流程异步化,还提供了模块化组件协助下层 DSL 构建更复杂的业务逻辑。与传统的 C++ 代码成功相比,经常使用 Dragonfly 的 DSL 只有要繁难地参与装璜器即可成功诸多复杂的性能。

经过 Dragonfly Python DSL 咱们将整个算法和架构的研发上班空间启动划分和隔离,成功档次清楚。在 DSL 之上是算法的上班空间,算法人员只有要编写DSL编排算子并提交性能,而无需关心底层算子的成功。在 DSL 之下是架构的上班空间,架构人员只有要编写算子,并提供二进制文件以运转性能。架构关于下层业务逻辑无需关心。这样就成功了明晰的档次划分,使得两者之间不会发生剧烈的耦合效应,防止了相互搅扰的状况。

,Dragonfly 曾经撑持了整个搜推行畛域的上千个在离线服务的运转,成功了笼罩整个介绍在线外围链路的技术形式。如图,运行范围不只笼罩战略服务,还包括整个链路上的召回服务、粗排精排重排等服务。

经过驳回这一套技术形式,成功了几个关键的目的。首先,一致的技术模型成功了整个在线服务协定。这个技术模型也为咱们提供了便利的监控条件,可以轻松监控整个链路每个服务的外部算子状况、CPU 消耗等系统资源目的。此外,一些底层优化和编译器优化也可以经过一次性开发,在一切服务中复用。

当一切服务都驳回这一套形式时,全链路将出现一个灵敏的形态。这象征着链路节点上的每个节点都可以灵敏地切分和融合。假设某个节点的服务随着业务迭代期间的延伸而变得臃肿,造成单机资源无法接受,可以将其从大单体服务切分为两个小单体服务。雷同,假设发现某些服务对单机的资源消耗过低,可以将高低游的两个服务启动灵敏融合,将其变为一个服务。这种灵敏的架构可以像微架构一样,使咱们能够灵敏地调整整个链路的架构,包括新旧服务的迁徙。依据迁徙阅历,驳回这一套技术形式可以使原有服务的 C++ 代码量缩小 50~80%,清楚降落了代码的复杂性和线上稳固性安保危险。

为了让业务团队高效地经常使用这个框架,仅仅做好一个框架是远远不够的。要让用户充沛体验到这个框架的长处,须要构建一个宏大的生态系统工具。

上图展现了目前提供的相关工具。这些工具笼罩了整个战略研发的全流程,包括上线前的编码辅佐、性能调试,以及上线后的目的监控和报警剖析。这套框架的最大长处在于,一切这些生态工具都可以做到一次性树立全业务复用。接上去引见几个重点工具。

这是网页版调试工具,用户可以在页面上间接编写 DSL,并经过网页运转检查结果。经过这个网页版工具,用户可以成功零部署的在线编写调试。秒级照应,用户可以在 Python 代码中繁难结构输入输入数据,并口头检查效果。用户除了可以间接检查结果,也可以经过 Python print 检查流程中的特色数据,或检查底层 C++ 算子的 glog 日志。这样可以协助用户繁难地调试程序逻辑。

针对曾经上线的在线服务,假设出现不良状况须要考查,整个框架具有智能打点的性能。经过用户 ID(uid),可以追踪历史记载中某条恳求的完整口头状况;可以了解到该恳求经过了哪些算子,每个算子的耗时和输入数据等详细消息。经过这种形式,开发人员可以启动历史追踪,排查或许出现的疑问。

经过这种形式,可以从上至下、由粗到细地展现业务流程,能够更明晰、直观地了解整个业务流程的详情。

许多开发团队都面临着算法代码有限收缩的疑问,须要一个有效的预防机制。Dragonfly 框架可以智能监测在线运转的无用算子并启动召回,甚至可以识别无用的分支。这样,系统可以活期生成报告,比如右侧的按分支生成的报告。这个报告可以准确地定位到哪位作者在哪个文件、哪个函数中写了哪个无用的分支,以及它曾经有多少天没有被经常使用过了。

有了这样的报告,就可以间接定位到编写无用代码的人,将报举报送给相关作者,促使其启动深化剖析并删除这些无用的代码。这样,可以有效防止代码有限收缩,防止给未来的系统精简和重构带来不用要的压力和消耗。

展望整个框架,未来将在以下三个方面继续发力:

正在启动中的上班是提供 numa-aware 的才干。新一代 CPU 架构正在转向多 numa 的架构,这或许让已有服务的性能未处于最佳形态。为了充散施展配件性能,须要下层代码能够更深化地感知 CPU 架构。目前框架可以协助下层算子更繁难的感知到 numa 架构的状况,并灵敏控制内存调配战略,以成功更优的访存性能。

此外还可以依据整个链路相关口头图优化,以降落线上服务耗时,并将这种优化扩展到全链路。

得益于全链路一致基座引擎的允许,可以轻松成功全链路特色控制和数据血源追踪。未来,系统将能够智能检测出哪些数据已无用并间接将其删除。

同时咱们将树立系统的自污染才干,实现代码控制的智能化。系统将能够识别出无用的逻辑或低效代码,并从代码库中活期智能删除,从而继续保证系统的强健性。

宿愿在未来增强整个生态工具链的树立,提供更智能的工具,用 AI 技术驱动更高效的研发流程。

系统也将努力于提供更完整的综合处置打算,甚至提供 to B 的才干。

假设您对此畛域感兴味并宿愿参与咱们的行列,共同发明出色的成绩,请将简历发送至邮箱 fangjianbing@kuaishou.com。咱们将一同努力于构建更弱小、更智能的工具和服务。谢谢大家的介入和允许!

Q1:Dragonfly的自定义算子的表白才干能否会比拟有限,或许说它能否总能翻译成 C++,还是能够笼罩一些比拟复杂的 C++ 的介绍逻辑。

取决于对算子形象的水平。由于这个算子并不是间接翻译成 C++ 代码,而是由 C++ 代码组装起来。比如要做一个过滤,须要首先把过滤相关的逻辑先形象进去,有哪些是专用的,哪些是可以性能化的,形象出一个过滤的外围逻辑,给大家复用,而并不是间接在 Python 外面写一个很粗疏很详细的过滤代码,而后把它翻译过去。算子并不是细到代码行级别的粒度。

Q2:自定义算子的拆分粒度是什么?在实践开发中能否属于自定义算子,是工程侧还是算法侧的同窗去选择这件事件。

这件事普通会是算法侧自己去开发,工程侧普通会去开发一些通用性的算子。自定义算子这局部会存在一些事实的疑问,比如不同算法同窗的形象才干以及代码才干是不一样的,他们对自定义算子的粒度把控以及设计或许是存在短少的,会造成自定义算子的复用性很有限。

Q3:第三个疑问是关于控制流的,和 TensorFlow 的图的实质区别是什么?控制流详细是如何成功的?

概念上相似于 TensorFlow,也是数据驱动的构图形式。TensorFlow 间接泄露成一个变量,经过变量的传递就能推断出数据依赖相关。但数据在咱们 DSL 中是一个暗藏的概念,比如文中的例子,相似 ctr、pctr 这样的数据,它算是作为一特性能项,体如今某一个算子的性能外面。从代码级别过去看,并没有存在这么一个 pctr 的 python 变量去承接这个数据,而是暗藏在 DSL 之下的一个概念,由于数据相关十分复杂,很难用变量传递去表白明晰。这是与 TensorFlow 的一个区别。TensorFlow 是经过参数传递的形式启动数据传递,而咱们的控制流是经过函数的性能,它的入参有一个叫 pctr 的字面量值,后续某一个算子有一个 pctr 的值作为它性能的输入,这样去判别出它的前后依赖相关,所以整个逻辑也都是靠拓扑加数据依赖的形式去构图。总之,原理上相似,但详细成功细节上不太一样。

Q4:关于微服务划分,Dragonfly 成功的文件是一个微服务,想了解一下 DSL 中的算子的组织和微服务的划分有什么样的相关?

一个 DSL 实践上最终对应的是一个服务的性能,比如战略服务,最终会生成一个 Json 的性能,而后交给系统去部署运转起来,对应的下游的召回粗排精排重排,也区分有一个独立的性能。同等于一个 DSL 对应一个线上的服务节点。假设要做节点拆分,实践上就是把 DSL 前半局部的方法调用摘进去,把它挪到另外一个 DSL 的文件外面。

您可能还会对下面的文章感兴趣: