导读:知乎基于业务需求搭建了 DMP 平台,本文详细的介绍了 DMP 的工作原理及架构演进过程,同时介绍了 Apache Doris 在 DMP 平台的应用实践,本文对大家了解 DMP 工作方式很有帮助,欢迎阅读。
作者|用户理解&数据赋能研发 Leader 侯容
DMP 业务背景
DMP 平台是大家老生常谈的话题。在早期广告系统出现之后就拥有了类似的 DMP 平台,比如:腾讯的广点通、阿里巴巴的达摩盘等都是业界做的比较好的 DMP 平台典型。而知乎搭建属于自己的 DMP 平台,一方面是因为知乎有相关的站内运营业务;另一方面也是因为我们可以通过搭建 DMP 平台支持内部系统对接、同时还可以协助完成相关业务发展以及定制化需求建设的目的。
DMP 业务包含:业务模式、业务场景以及业务需求。
图1.1 DMP业务
DMP 平台设计的方向:为了找到我们的核心客户,并在后续对我们的核心客户完成如广告投放等营销操作,让核心客户跟我们的内容之间能够有更好的匹配。
业务模式
DMP 平台业务模式
- 从站外转站内。典型场景是广告主在进行广告投放的过程中,如何通过 Mapping 将可能出现的站外人群转到站内,并在站内的系统上承接这些用户包。
- 从站内转站外。 在知乎内先找到定向用户后再去用这些用户在三方投广告。
- 站内运营。 包括内容运营,用户运营以及活动运营。一方面可以增加知乎相关内容的宣传,另一方面进行客户定位并精准解决某些客户的问题与需求。与此同时,我们也可以通过活动设计来提升业务效果。
业务场景
基于这三种业务模式,主要应用的业务场景:
- 信息流方面。 拿推荐场景举例:推荐场景中会有定向推荐以及定向提权两种诉求。定向推荐是我们把推送内容定向推送给某些用户,而定向提权是我们把推送内容在被推送的用户身上完成提权并重新打分。
- 广告侧实时竞价。 得知该用户身上挂了哪些广告之后可以进行实时竞价,通过排序选出最适合该用户的广告。
- 详情页。 详情页中会有弹窗提示:比如说某个用户点击某个详情之后,若该用户没有达到目标条件,会弹窗引导来该用户达到条件。
- 活动平台。 设置活动的目标用户。针对不一样的目标群体,展示不同的活动信息。
- 触达系统。 比如在推送消息、弹窗和短信时,可以拿到一类具体的用户,之后向该类用户进行发布相应的 Push 和站内信等。
- 站外投放。 找到合适的用户群并在站外为其投放相应的广告。
业务需求
基于业务模式场景,在人群方面能做的事情可以分为三类:
对接系统
一般分为以下 3 种情况:
- 该用户命中了哪些人群包。拿广告系统为例,该人群包 ID 可以 Mapping 成一个广告,也就是该用户命中了哪些广告。
- 内部人群包。人群包对内部而言就是把内容推荐给谁,或者给谁发布内容的 Push。
- 对外部的广告。当我们筛选出一类用户需要投放在站外时,这时候就是在使用对外部的人群包。对于这两个人群包之间的区别而言,人群 ID 会有不同:一种是站内的通用 ID,另外一种是基于不同投放平台上对应的对外 ID。
人群定向
人群定向包括导入/导出、基于某些特征进行标签圈选、人群泛化、用户量预估等。
- 人群泛化,拿到比较小的种子人群包后,基于规则寻找相似特征,再通过对相似特征的置信度进行调整,扩展更多的人群。
- 用户量预估,选中一批用户后需要立即了解这批用户的数量有多少。
人群洞察
包括画像洞察,用户的内部画像以及两个不同人群包之间的对比分析。
业务流程
由于当前 DMP 业务的三种场景面向人群不同,会提供向站内与站外不同系统来完成这批人群相关的运营动作。
据此情况,我们组织人群定向功能、获取到目标用户之后进行 Mapping ,拿到用户在站内或站外投放的效果回收之后,获取目标用户进行构成分析与对比分析,进行用户洞察。若目标达成,那么本次投放顺利达成;若目标未达成,运营侧会做相关假设:是否可以再加一个特征或特殊操作进一步提升业务?提出假设之后,设计 AB 实验,经过 AB 试验后,我们又会对目标人群进行一些调整。以上就是我们的运营流程。
图1.2 DMP业务流程
站内运营自闭环
人群定向。 通过标签圈选,选择历史上有活动效果或导入喜欢此活动的人群,进行泛化完成基础人群包选择,以此来确定目标人群。
进行投放。 由于很多业务在推荐侧的信息流、触达系统、详情系统以及广告引擎等系统中进行对接,可以利用以上系统和业务来完成对目标用户在站内不同流量场景投放。
投放之后。 获取本次投放的效果并进行分析。比如我们做的操作是发 Push,谁点击了Push、阅读时间等行为,可以分析有哪些用户更喜欢我们此次发布的 Push,从而获得目标用户的典型特征。
若此次 Push 的点击量达成推送目标,那么目标完成;若点击量没有达成目标,我们会进行一个假设,比如最初预测点击 Push 的男性>女性,但最后得出的结果相反时,我们会通过 TGI 算法进行排序,找出这两次差别的典型特征,完成设计并产出 AB 实验。
通过 AB 实验我们可以对前后的人群包再做一次对比并发布相关的 Push。如果点击量有所提升,我们在后续过程中就会不断的完成循环,最终找到基于我们运营场景的领域的精准用户。
站内向站外投放
基于已经积累的用户特征数据,找出在知乎内部有几率产生站外效果的人群,并划出该类人群的范围。再通过 Mapping,可以把站内的 ID 转换成在三方投放平台上产出的 ID 并进行投放。
由于这个过程我们的站内系统不同,并不能直接拿到相应的埋点数据供我们进行数据链路建设,所以就必须要通过三方投放平台上下载相应的埋点数据,通过类似的场景完成数据导入后再进行后续流程的建设。这也就导致了整个过程的效果回收会比较长。
站外转站内
假如我是一名知乎站外的广告主,我要投放一个牙膏类的产品,但是我对知乎的用户并不是特别了解。通过前期所做的运营调研,可以发现历史购买牙膏的人群包是什么样子。那么就可以把前期调研所得到的人群包通过 ID Mapping 转换为知乎 ID 并导入生成目标人群。但是广告主拿到购买牙膏的人群可能存在与知乎用户重合度较低的情况。这时候启用第二个功能,也就是人群泛化功能。
人群泛化会把导入人数较少的种子人群连接到知乎,这个过程可以对用户达成的所有特征在 AI模型中完成训练。可以训练出种子人群在知乎所有用户特征下的模型是什么,之后再把所有用户的全部特征灌入得到的模型之中进行推理。这样得到带有置信度的目标用户。
若广告主认为基于之前的调研结果来看,相关目标人群在知乎中为 1,000 万左右,此时我们就可以选择对于目标用户的置信度。比如说当置信度为 0.7 时,得到结果为 2,000 万;之后我们把置信度调整为 0.8 时,得到结果为 1,000 万,此时我们就可以选择 0.8 的置信度完成广告引擎的对接并进行投放后分析效果。
基于上述运营流程,我们可以抽象出 DMP 平台最核心的功能包括洞察、定向以及 ID mapping。
画像特征
图1.3 DMP画像特征
我们根据上述的用户画像,构建出了画像特征。其中标签是最重要的部分,也是离散部分。连续部分包括了用户的停留时长以及相关的用户行为,比如:某人在某地做了什么事等,这些都属于连续特征。特征方面,在该特征还没有打上标签之前,我们会统称为普通特征。
功能梳理
图1.4 DMP功能梳理
基于DMP平台的功能,向右侧拓展为业务功能。业务功能会服务于运营、销售或站内的应用系统,包括人群定向、人群洞察以及相关的 ID Mapping。向左侧拓展是信息量巨大且十分重要的特征接入部分。
当前 DMP 平台由于单从标签方面就有 250 万的标签量级,在用户X标签也有 1100 亿相关用户数据,同时业务方面对部分标签具有实时性要求。这也就导致在特征接入过程中需要做很多事情。
接下来将为大家介绍具体功能。
- 人群定向。 人群定向方面整体上分为导入与导出、特征圈选以及人群泛化这三个功能。
- 人群洞察。 包括构成分析和对比分析两种功能。构成分析部分我们可以简单理解为一个饼图或柱状图。对比分析是多个人群对比分析。
- ID Mapping。 整体上将无论是 oai、idfa、手机号,全部硬生成知乎的连续统一 ID,而且这个连续 ID 基本是严格自增的。
- 特征接入
-
- 建设方式分为实时特征及离线特征
- 标签组方面有离线和实时两种接入方式。其中树状标签主要用来应对复杂场景,如用户对某话题在阅读和互动方面的是多选的树形结构。
DMP 架构与实现
图2.1 DMP业务架构
我认为架构对于实现最终目标是很重要的阶段,但并不是必要阶段。只要我们把所有功能都进行完善就可以完成我们所有的业务实践,但是这样会导致在系统经过不断膨胀后,所对应的维护成本也会不断变高,稳定性变差,最后导致没有人可以维护的窘迫情况发生。架构主要可以为我们解决在多个复杂业务功能场景下,如何以低成本的方式进行维护迭代并有目标的去针对某个模块进行优化,但并不能解决实际的业务功能问题。
基于以上我对架构的认知,对业务以及整体 DMP 架构进行拆解:
DMP 使用用户
DMP 系统对接的是 3 类用户:
- 平台方面,包括广告平台、信息流、广告引擎以及触达系统。
- 操作人员,包括运营、投放以及销售等业务相关操作人员。
- 诸如特征开发的产品及相关内部产品。
而这三部分人所对接的最前台的系统也是不一样的。
首先我们认为,平台或系统方面会与 DMP 的接口层对接。接口主要分为三种:
第一种接口是诸如广告引擎和信息流经常请求用户命中了哪些人群包列表。 在广告引擎内,完成请求之后就可以直接把人群包列表变成某个广告 ID 并完成竞价。信息流与广告引擎类似:当前用户若命中了我们要提权某内容或领域标签时,我们就会进行提权。该接口的设计就是典型的高稳定性、高并发、高吞吐。我们可以通过线上数据来进行该接口与其他接口的承载差别对比:该接口当前承载了 10 万 qps,由于接口对接了公司的核心系统,因此不能有任何抖动与故障,对它的稳定性要求达到 S 级,所以该接口也有多机缓存和高并发方面的相关设计,需要能够达到高稳定性、高并发、高吞吐的目标。
第二部分是站内与站外的人群包,该部分和上述内容也比较类似,都会对接到我们最核心的系统。一旦人群包无法圈选人群,后面整体的营销与定向投放也都会受到影响。对于 DMP 前台部分,该部分和接口层存在着明显区别:DMP 前台主要对接的是我们的内部运营同学与销售同学。DMP 前台若产生异常情况,只是会不能进行新的洞察以及人群定向的,不会影响正常使用历史人群。由于该部分会对接众多的销售和人群而不是对接重请求的接口,使用的复杂性也就必须要降到最低,减少在运营方面的培训成本,所以 DMP 前台就需要具备操作简单且使用成本低的特点。
第三方面是对接我们的内部系统,这部分主要会降低我们日常开发的成本。
DMP 核心功能
DMP 能够支持人群圈选、泛化、人群洞察的核心业务模块;支持标签生产, ID Mapping 还有计算任务运维和存储方面的功能。
DMP 业务模块
DMP 业务模块分为上下两层,向上的业务层实现新增功能的低成本化,重点在于可扩展性;向下的业务层随着人群与业务功能的增长,整体的开发或技术投入成本不会有太大的产出,也就是资源上的可扩展性。
DMP 基础设施
最下方是基础设施,需要保证基础设施相关的稳定性。
我们判断接口的依据是请求的接口主要承载是 Redis;Doris 主要承载了 DMP 前台和整体业务功能;后台部分主要承载是 MySQL 与 TiDB。以上是我们当前具体底层数据库的相关承载方面。
有人会问 Redis 成本是否会太高?不会的。因为核心的圈选人群逻辑都是在 Doris 上实现的,存放的大量相关标签都是通过 Doris 进行存放,只有在某个广告要指定某目标人群的某几个特征进行排列组合并且完成泛化时,我们会圈选出某个人群包 ID 对应的结果,最后才导出存放到 Redis 中。因此 Redis 的主要目的是用来扛高并发,实际的存放量很少。
DMP 平台功能盘点
功能盘点主要分为业务向与基础向两部分。
图2.2 DMP平台功能盘点-业务向
业务向
业务向我们能够支持人群定向以及人群洞察两部分能力。
人群定向:
- 人群预估:比如说对性别、年龄、感兴趣的话题、该用户手机品牌是等多个条件进行排列组合,要求能够在 1 秒内完成精确结构的人群特征量级预估。
- 人群圈选:经过精确结构的人群数量预估后,可以在分钟级别内将预估结果转化为要进行投放和使用的相关人群包。
- 人群包泛化:泛化的能力要求尽可能简单,比如说我选择有历史的人群包后,就可以进行人群泛化并有具体的执行度选择。
人群洞察:
- 可以探索当前活动入口画像,并完成流量回收。比如说我向 100 万人发布了推送,其中有 3 万人点击,那么可以对这 3 万人进行流量回收,与已推送的 100 万人进行对比,就可以这 3 万人明显的用户特征,方便我们后续提取出更精准的用户群体。
基础向
另外 DMP 架构还有一些基础功能,包括了主要特征建设、ID mapping 以及计算任务运维。
图2.3 DMP平台功能盘点-基础向
这三个基础功能不仅可以让我们快速完成实时和批量计算,还能够帮助我们解决新老版本滚动上线的问题。因为我们当前无论是通过 AI、数据采买、特征筛选,找到一个用户,即使是性别这种最基础的特征,也是在不断优化的过程,但每次优化是没有办法快速进行运营影响的评估,因此就需要做到多版本灰度上线,并进行滚动上线。
特征建设
特征整体有两部分,一种是原子特征,一种是派生特征。
- 在建设原子特征时,我们就需要从离线或实时数据中生产大量相同基准的特征。
- 对于派生特征,会基于已生产的特征再生产一个特征。举例:假如我们认为某群体是高消费能力群体,放在一个简单的场景中,我们可能会圈选出一位在 18-25 岁之间并在一二线城市的女性,并认为这样的特征可能是对化妆品消费能力比较高的群体特征。之后我们就会把该特征作为派生特征进行存储并去加快后续计算速度并降低运营筛选的成本。
特征建设可以做到能力隔离,以此来提升我们特征建设和上线效率。
Mapping能力
包括设备 ID Mapping、用户特征 ID Mapping、泛化特征 ID mapping。该部分整体场景主要是统一 ID,并将 ID 从差别较大、类型不同的不连续 ID 变成连续统一的 int 型自增 ID。
计算任务运维
任务运维主要是完成 DAG 的调度与计算资源管理。如果大家用过 Doris 的话,就会知道 Doris 会使用最快的速度完成每一个 SQL 的执行。因此在进行人群预估时就需要做好排队的速度,否则突然有一波运营动作或热点事件时,可能会出现预估出多个人群包的状况并把所有资源都占满,这样都会互相受到影响,所以就需要通过任务运维进行资源的优先级排队,逐一执行相关人群包的圈选工作。
总结
- 特征建设可以做到能力隔离,以此来提升我们特征建设和上线效率。
- ID Mapping 屏蔽了我们 ID Mapping 的困难成本。我们会分为完成原子特征建设、完成派生特征建设以及进行基础设施的建设这三部分。当基础设施建设同学完成屏蔽或在架构上隔离之后,特征建设的同学就不需要管 ID Mapping 方面的问题,只需要管专注于建设特征即可。
- 计算任务运维部分,对于业务开发同学并不需要知道底层到底发生了什么,为此我们要有一个同学完成对底层的封装后向上层提供一个接口,业务侧可以直接使用底层的功能的同时屏蔽了底层的复杂性。通过抽象与屏蔽,可以明显的提升最终上线与建设的效率,并能让其他某些工作从研发侧转移到运营侧。
举例: 我们当前有两种特征,第一种是原子特征。在形成原子特征的过程中,写一个 SQL 就可以形成一个特征。分析师与业务产品均可以参与特征的建设过程。第二种是派生特征。我们在运营后台上具备派生特征的交并差的能力,一些业务上的运营动作可以直接在管理后台进行操作并完成派生特征的建设。这样主要的工作量从研发侧逐渐转移到了产品侧与业务侧,明显的提升了各种能力和特征上线的效率。
DMP 核心介绍
DMP 核心部分有两方面:数据的写入/导入以及快查/快读。写入和导入是链路及存储的一部分,快查和快读我会在后续进行介绍。
特征数据链路及存储
图3.1 特征数据链路及存储
写入部分流程首先是离线链路:离线链路会从各个业务的 Hive 存储上跑相关的 SQL 并生成一个 Tag 表。我们会在 Hive 上落一份 Tag 表后完成离线 Mapping。这个离线 Mapping 过程会请求通过用户设备核心自动生成统一连续的用户 ID,同时在离线 Mapping 的过程会把 imei、idfa、oaid 等数据进行转换和唯一绑定,若过程结束后发现新用户,则生成新ID,若是老用户则获取用户 ID。通过这个过程,生成 ID mapping 的表,再进行大小写等复杂流程就可以得到用户唯一 ID 与映射 ID 的 Mapping 表,这就是我们得到的第一个表。
接着我们会在 ID Mapping 后进行枚举采集:当前标签组是 125 个,由 120 个离线特征和 5 个实施特征组成。当我们完成这 125 个相关数据的开发之后,数据相应的原子特征就可以通过 Mapping 直接拿出来。之所以要进行枚举采集是因为用户在使用过程中需要标签的搜索功能,当用户搜索标签时,250 万人工录入的成本过高,因此我们在离线和实时处理的过程中会将枚举采集出来,并且通过 Bulk Load 的写到 ElasticSearch 中。在这个过程也会生成连续的自增 ID 去映射用户标签的倒排表,也就是 tag_map 表,这是我们得到的第二个表。另外还存在第三个表用户行为表,这张表是我们在实时数仓方面构建的,因此没有单独强调那一部分。
基于上述三张表的部分,我们形成了三套存储:
- 第一套是在 ElasticSearch 上的搜索标签存储。
- 第二套是在 Doris 上,也是最核心的存储。
- 第三套是整体 ID Mapping 的存储。
获取到这 3 个存储后,可以进行多种 Join 和查询,为后续的洞察及人群定向提供了基础。
接下来为大家公布几个量级:用户X标签量级,为 1,100 亿;ID Mapping 是一个宽表,量级是8.5亿;ElastichSearch,量级是250万。这三个量级也是我们为什么选择 ElasticSearch 和 Doris 的原因。
人群定向流程
上述的数据导入后形成 3 张表,这里是利用这 3 张表产生人群相关定向和人群包。
图3.2 人群定向流程
人群定向流程分为两种:
- 第一个是通过购物车筛选人群标签后进行人群预估,最后完成人群圈选回写到 Redis 的流程。
- 第二个是人群泛化,通过 AI 平台完成 AI 模型的整体训练及人群的推理,再回写到 Doris 中,通过置信度进行选择并打上标签。
简单介绍一下这两个流程的过程:
整体的标签搜索。 用户的前台在产出标签搜索的事件之后就会去完成标签的搜索:通过思考各种名字组合寻找想要的标签后,我们会把这个标签放在标签购物车中并立。这个过程就是不停的向人群购物车中加各种标签和组合条件后,查看人群数量的过程。
这个过程存在的原因是在日常运营使用中,我们会对每次推广或目标群体进行量级预估。如果这个事件原本只涉及 200 万到 300 万左右的人群,经过人群圈选预估出来是 5,000 万,那么肯定是我们圈选条件不够精准,这个情况下我们就需要逐渐添加各种精准的条件,并把圈选控制在合适范围的量级后再形成人群包,所以这个过程会不断进行循环并获取到合适的标签/特征的组合。在获取到合适的组合之后,我们需要确定这个标签的目标和人群是,这个过程就会生成人群包。生成人群包的过程会进行连表操作并关联原数据,同时也会关联 ID Mapping 的表。若出现导出到站外的情况,则会做ID Mapping 的表并完成站外的 ID 转换。之后再把导出的人群包 ID 与人群 ID 写入 Reids中,写入之后进行通知。
如果只需要提供人群包来发布推送和短信等的业务就不需要写到 Redis 之中,这样可以大量释放存储并写到离线存储上。比如说一方面是 HDFS,另外一方面是我们对接的对象存储就会写到这些存储之中。由这些存储直接传给推动系统后,信息系统就可以直接拿到人群包并批量的给相关人群发布相关 Push 或推送。
人群泛化。 人群泛化流程最开始可能会有上传人群包的过程,也有可能没有。这个过程主要解决有些业务中,我们拥有某些历史活动的人群并需要进行人群泛化的问题。如果说它的人群包之前点击过我们的 Push,可以直接筛选,筛选完成之后关联所有的用户特征进行用户训练,模型训练完成后再对全站用户进行推理,推理出一批带有置信度的人群 ID 的结果并返回写到 Doris 之中。在这个过程中会同时发起另外一个流程,此流程会对用户侧的泛化的结果进行筛选,可以根据合适的置信度选择合适的数量。
接下来为大家介绍几个常用流程: 在开发完成之后,最核心的流程就是加标签和购物车并完成圈选后,传统的人群进行泛化的流程。但是经过和运营侧沟通后,我们发现日常工作中,运营侧实际上会将我们这几个流程反复进行叠加使用,实际的使用有这么几种:拿到带有历史效果的人群并进行泛化,但是完成泛化之后效果他的用户特征也会被相应被扩大,之后再叠加本次运营特点的标签后完成圈选并进行使用。
第二种是获取到历史效果后进行洞察和分析。 包括查看用户的画像后再重新根据标签关系圈选,之后又叠加了一次历史正向人群包后再去进行泛化。泛化之后再实现分发条件,最后再进行圈选,将该人群包给广告与相关的投放业务。运营侧会做很多基于原子能力以外更复杂的一些组合后再进行使用。
人群定向性能优化
背景
图3.3 人群定向性能优化 背景与难点
当前 DMP 系统中有两大功能,第一大功能是人群定向,另外一大功能是人群洞察。基于这两大功能会有一个底层的功能是建设各种用户方面的画像特征。当我们完成拆解之后,我们就会发现人群定向的这部分功能是运营侧或业务侧的痛点。
场景要求
- 人群预估,针对投放和营销场景,运营侧会有人数预期,那么会构建相应规模的购物车,持续在购物车中加入新的特征,需要立即看到新的特征加入之后会圈选出多少人,而不是每次加入新的特征后都需要很长时间的等待。
- 人群圈选,针对热点运营。运营侧在日常工作中会持续跟进发生的各种热点事件,当发生了某些热点事件后,要快速的圈选出人群包发布 Psuh 和 推荐。如果圈选过程需要好几分钟,就会错过热点事件。
难点
- 第一个数据量极大,如上图标注。
- 第二个期望时间很短,人群预估与人群筛选分别能够在一秒钟内和一分钟内完成。
性能优化(1)
第一阶段优化我们通过了以下几点来解决这两个问题:
图3.3 人群定向性能优化 第一阶段
倒排索引和按条件查询
图3.4 人群定向性能优化 倒排索引及ID Mapping
- 首先,倒排索引方面,我们将查询条件由原先的 and or not 改成了 bitmap 函数的交并差;同时我们把连续数值打散成为了离散标签。举例:用户的年龄是大于 0,小于 100 的 int 型,如果按照数字顺序进行筛选,运营侧是不好把控的,圈选的过程中也会导致使用效果不理想。因此我们把按照顺序排列的年龄打上另外的标签,称为年龄段,比如 18-25,0-18 等。
- 接着,把原先的 and or not 的查询转换为了倒排索引的相关查询,原先建立的表就会变成按照 tag\_group 、tag\_value_id 、置信区间的标识、bitmap 的顺序排序。同时基于这部分我们也需要进行 ID Mapping,ID Mapping 在导入的过程中的核心就是要把用户 ID 变成连续自增的。
查询逻辑变更
图3.5 人群定向性能优化 查询逻辑变更
原先的查询条件是 where 条件中的 and、or、not,现在经过复杂的手段,把原先的查询条件修改成 bitmap\_and,bitmap\_or,bitmap_not,我们通过业务代码,将外部运营通过可视化后台配置的 and、or、not 的逻辑全部改为函数式的逻辑,相当于把 where 条件放到了函数和聚合逻辑之中。
但经过优化之后还会存在 2 个问题:
第一个问题是单一的 bitmap 过大,第二个问题是 bitmap 的空间分散。这两个问题集中导致每次进行交并差聚合时网络 IO 特别高。
底层 Doris 中用的是 brpc。在数据交换的过程中,因为每一个单一的 bitmap 都很大,就会导致 brpc 传输拥堵,有时甚至会出现上百兆的 bitmap 进行交换的情况。上百兆的 bitmap 进行交并差计算时性能很低,基本上我们想要达它达到 1 分钟圈选人群,1 秒钟进行人群预估是不可能的。
性能优化(2)
基于仍存在的问题,我们进行了第二阶段的优化。
图3.6 人群定向性能优化 第二阶段
分而治之
第二阶段的核心的思路是分治。当我们进行了第一波上线后,发现人群预估能力是分钟级别,圈选基本上要到 10 分钟开外了。分治的思路是将全站的用户全部打成连续自增 ID 后,按照某个程度进行分组。比如说 0-100 万是一组,100 万-200 万是一组...逐渐分为几个组别。全站用户的交并差,可以等价于分组之后的交并差结果之和。
图3.7 人群定向性能优化 分治
数据预置
当我们发现这个规律之后,通过分而治之可以做相关的数据预置。利用 Doris 中 Colocate group 特性,把每个分组内 100 万人全部放到某一台物理机上,避免网络的开销。
算子优化
全部放到某一个物理机上之后,就可以把聚合的算子由原先 bitmap\_and\_not 的 bitmap not 和bitmap count 替换成一个函数来实现。此时基于 Doris 团队的新版本,增加了类似 bitmap\_and\_not_count 的组合函数后,性能相对于原先的嵌套函数有了比较明显的优化。
解决方案
基于上述解决思路,我们设计了新的解决方案。
新的解决方案以上 3 个思路进行拆分,包括查询逻辑的变更,预估变成子逻辑的求和、人群圈选变成子逻辑的合并。
- 由于把原先几个 bitmap 的计算变成了多个小组 bitmap 计算,能进一步的提升多线程的并行度,使计算速度提升;同时也对代码进行了优化,将可复合的 bitmap\_and\_or_not 函数在提交时合并成同一个函数;在写入过程中把分组 ID 和相应的百万分组进行写入调整。
- 离线和实时之中都会写相应的 tag 表。在完成 tag 表的写入之后可以把每一个 tag 之中不同的 user tag 写到不同的物理机上:比如可以将 300 万拆开分别写在三台不同的物理机上,完成物理机方面的区隔。这里借助了 Colocate group 以及 Group key 进行设置。完成写入之后,计算过程从原先的整体计算变成独立按照每一个 Group 进行计算。由于整体的 bitmap 很大,每一个独立的 Group 又都在一台物理机上面进行计算,速度有非常明显的提升。
- 在每一个 Group 计算之后进行合并,合并之后,人群预估变成了不同物理机上面的数字简单加和,结果基本达到秒出。人群圈选也就变成了不同物理机上面的 bitmap,再 Shuffle 出去做最后的合并,这个过程量级很小,可以做到 1 分钟之内输出结果。
优化结果
下面两张截图分别是还没有进行合并之前以及合并之后的查询计划。
优化前: 在查询的过程中,首先我们需要针对某一个 tag 做一个 bitmap\_and 和 bitmap\_not 或者 bitmap_or,在这之后另外几个 tag 也会做相同的聚合,在聚合完之后再做一次 Shuffle,最后进行 Join。同时另外的部分也会进行聚合,经过聚合之后再进行 Shuffle 和 Join。
这几次聚合过程中,每一个 tag 都有非常高的成本,都需要经过聚合—网络传输—再聚合—再网络传输的过程后再做 Join。
优化后: 查询计划有了非常明显的改变。只需要通过一个函数在合并的过程中进行查询,合并完成之后就可以完成最终的结果合并。无论是 int 类型的相加还是 bitmap 的合并都只有最后一层,速度有显著提升。原先人均预估可需要分钟级别完成,优化后,只需要几百毫秒便可完成,即使是复杂到上千个条件也只需要一秒就可以完成。
人群圈选也和上述过程类似:在条件复杂的情况下,可以做到一分多钟到两分多种之间完成。如果只有几十到 一百个的条件的话,人群圈选都可以在一分钟左右完成。
整个过程主要对数据进行了拆分,由 Doris 的 Colocate 原理把拆分后的数据提前预置在某一台物理机上面,通过优化,可以满足大部分场景的运营要求。
未来及展望
业务向
图4.1 未来与展望 业务向
如红色框选所见,当前的系统流程是人群定向之后进行 Mapping,在用户洞察上是围绕人群进行建设的,同时与各个业务侧在 Mapping、洞察以及人群等环节进行对接。但是在这个流程中,如何通过运营达成目标、如何设计 AB 方案,两个部分是松耦合的。
未来我们希望 DMP 运营平台不光是松耦合的模式,而且能够在在业务上执行强耦合、强绑定的模式。这样的运营模式在使用过程中会更舒服,可以完全在 DMP 平台上完成了整体运营流程,并可以根据运营效果设计相关的 AB 实验,不断优化。
技术向
图4.2 未来与展望 技术向
技术建设过程中,最主要的就是圈选人群。运营侧甚至会选几百个条件进行人群圈选。而这些运营人员可能分属在不同业务,这会导致他们的基础条件写得很相似。对于这种相似的基础条件我们会人工建立相应的 bitmap 进行预合并,再去基于此特征圈选,由于预合并的缘故会明显提升我们后续的执行速度。
第一个是查询效率。 对所有运营的人群圈选进行定期扫描及 SQL Parser。经过解析自动设计 SQL 的聚合条件进行预聚合,合成相应的 bitmap 的同时注册到相关的特征。在人群圈选时我们也会通过相同的 SQL Parser 自动将原先圈选的 SQL 改写,在改写之前可能会有好几十个特征,而他们又正好等于某一个派生特征的结果,此时就可以直接替换成派生特征。这个举动能进一步的提升我们查询的圈选速率。
第二个是导入速度。 我们经过五天的时间,每天需要导入大概 2TB 的数据量,存储了11TB 的数据,数据量比较大,我们希望在导入的过程中可以进一步的提速。当前我们了解到业界有做 Spark 直接撰写具体 OLAP 引擎文件,我们也在思考是否可以通过 Spark 直接撰写 Doris Tablet 文件并挂载到 FE 上面,让我们能够快速完成导入或写入。
Q&A 环节
Q:知乎的标签体系有多少标签?记录量是多少?后台是一张还是多张的大宽表?在人群圈选的时候进行表链接,业务人员能否实时显示圈选出的人群特征和人群数量?
A:知乎的标签体系很大,包含了用户、内容、商业以及业务方面治理与安全等很多方面的标签,DMP 系统方面主要会与用户方面的标签进行对接。就单论通过认证且正在使用的标签组而言就有将近 700 多个,如果在加上业务方面在提未认证标签可以达到上千个。对于我们正在使用的用户方面的标签有 120 个标签组以及 5 个实时标签,总共 125 个标签。
记录量方面有 1100 亿的记录量。
后台不是一张宽表。在子标签完成生成后,会生成出独立的 tag1、tag2、tag3 的数据源表。经过我们将这些表写入 DMP 之后最终才会变成一个大宽表,在 DMP 中是问题中的一个大宽表,在业务中则是每个独立的标签表。多张大宽表在进行人群标签圈选时会进行连接,我们在经过数据处理后,会将数据写入到一张表中而不再是一张大宽表。
由于我们的优化,在这一张表中的存储的文件已经不会再按照 Tag ID 这种查询进度缓慢的方式进行分散。我们会按照存储的 Key,比如说 0~100 万的 ID 都会分在相同的地方进行存储。我们在计算的过程会在同一台物理机扫描出来,在经过聚合逻辑后就可以拿到结果。所以也就能够做到实时圈选相关数量的结果。
Q:人群圈选是基于经验进行标签组合圈选吗?投入后的效果如何分析?是独立的分析平台工具吗?如何知道投放人群包的转化率?转化是否回到打标签中利用另外的分析平台进行分析?
A:人群圈选可以分为两部分。第一部分是我们基于运营的经验进行圈选,这个部分中又分为已知人群圈选与未知人群圈选两个分支。
已知人群圈选,意味着运营已经对这个场景非常明确。能够熟知我们在运营的用户群体就是某个性别以及用户年龄段等,这时候我们就会基于历史经验进行圈选。对于完全未知的用户特征,我们会直接圈大盘。
这两种运营流程的区别就在于已知用户群体圈选的准确率会更高。基于已知的结果,我们几乎不再需要不用进行 AB 实验就可以完成本次投放。对于完全未知的用户特征而言,如果直接圈大盘的话,我们就一定需要进行小流量的 AB 实验发现点击 Push 的用户都满足某一个兴趣后,就可以基于这部分兴趣积累经验,之后再设计一个 AB 实验并调整人群特征至合适场景,直到效果逐渐的达到期望目标后,就会从未知的人群变为已知人群。
还存在另外一种经验。比如说广告主的经验,广告主可能在知乎中并没有历史投放经验,但是广告主知道购买过我的产品人群有哪些,比如说他们手机号的加密 MD5 或手机 idfa 的加密 MD5 等,这样就可以将其他站投放过的效果完成导入,形成基本的人群。通过人群泛化,和站内所有的特征进行 Join 后去训练模型,通过 AI 的能力自动寻找到我的历史购买人群有怎么样的显著特征,之后就可以完成这部分泛化的全选。基于泛化的全选后,还是会经过相同的链路并完成这部分的数次循环循,之后就可以知道我这个场景下应该投放给哪些用户。
转化率我们在单独的地方进行查看,这也是我后期想要集成在 DMP 平台内做到的功能。我们在单独的页面上可以看不同 Push 的转化率。DMP 平台上面只能通过效果回收进行查看。
Q:后台都是基于 Doris 吗?多少节点是一个集群呢?
A:后台主要的计算方面都是基于 Doris。在高吞吐方面我们也依赖于 Redis。TPP 方面我们用了 TiDB。当前 Doris 集群是 6 节点,64 核心 256g 的 BE;3个 FE 是 6 节点,16 核心 32g 的集群配置。
Q:人群放大靠谱吗?所有的人群圈选占比有多大,用的是什么算法?
A:人群放大是比较靠谱的。从运营侧的反馈可以得知:如果只通过广告主或只通过基于列入历史运营效果拿到的数据基本上无法支持完成本次运营,但是如果把我们所有的特征都加入并进行训练的话,基本每次都会有比较明显的提升,在CTR方面,能够达到 80%-90%。置信度调整为了 80%。
人群圈选业务使用占比会比一般圈选要少一些。对于一般圈选而言,我们当前历史上已有的特征也带有置信度。我们基于这些已有特征基本就可以完成绝大部分的运营工作。而人群泛化主要是用来解决的是当我对这部分客户完全没有认知,同时又想将站内全部随机大盘用户导入,进行用户群体特征探测的情况。这个过程其实对运营侧而言工作量比较大,只有在这种特定情况下才会选用泛化,所以泛化的占比按照比例来讲是不多的。比如说每天有 300 个基于特征和标签的定向,而每天基于算法方面的泛化是 1~2 次。
用的是什么算法我还没有细看过。当前我们会通过数据来调用 AI 同学的相关的算法。我们当前提供的就是将用户的所有特征都准备好后灌入到 AI 的自动训练的模型之中。在完成训练之后,我们再调用这个模型并把所有特征都灌入进行推理。
Q:AB 如果要用 Reids 查标签该如何设计?要如何保持实时性呢?
A:对于问题中 A 表和 B 表要查标签,数据量会爆炸,这个情况是的确存在的。所以我建议做标签,最好所有的标签都在这一个表里。通过我们当前经历的探索得出的结论,我们对于该问题的解决方案就是每一台物理机可能会存多个 100 万,但是要确保每一个 100 万的分段都在同一台物理机上,它就可以变成这台物理机的 Scan 以及聚合之后进行直接运算,所以它就不存在双表的 Join 问题,可以直接在表内进行聚合。我们这边有好几个类似于 bitmap and or not 的标签的计算,但是在算子方面,算子已经是被合并在聚合算子里面并完成聚合,聚合后再做一个最终的数据合并,这样的话性能会好很多,而且也能避免 A 表和 B 表做 Join 的结果。
对于第二个问题,我们完成人群的 ID 聚合都会通过这个函数。当这个函数走完之后,它会生成当前投放特征下的人群列表,我才完成 Join。在这个时候,普通的 Join 就不会有非常爆炸的数量,也不会涉及到上千亿的快速的查询计算。
Q:可以解读一下关于 250 万个标签的相关内容吗?
A:大家可以在图 1.3 中看到,出现像 250 万个标签主要是因为一个性别在标签组内算作 1,而在标签方面则会拥有男、女、其他 3 个标签。在手机品牌中,一个标签组下我们当前也是收录了将近 20 多个手机品牌的标签。之后还有话题兴趣的标签组中相当多的话题兴趣的标签数量。比如说知乎站内其实有很多话题,某些用户可能对影视内容感兴趣,也可能对母婴内容感兴趣,同时也可能对教育或学生内容感兴趣,以上的话题兴趣有具有连续的共性点。连续标签方面我们会在后续的文章中继续为大家介绍。当前用户画像的内容方面,如果从标签进行分组,都是属于离散标签。连续标签更多的是用户行为或者是操作数值等。
Q:标签和特征的关系是什么?标签又是怎样建立的?
A:我们定义特征是要比较比标签大的,可以理解为我们当前的特征中 90% 是标签,剩下 10% 是用户行为的比例。
加入社区
欢迎更多热爱开源的小伙伴加入 Apache Doris 社区,参与社区建设,除了可以在 GitHub 上提 PR 或 Issue 之外,也欢迎大家积极参与到社区日常建设中来,比如:
参加社区征文活动,进行技术解析、应用实践等文章产出;作为讲师参与 Doris 社区的线上线下活动;积极参与 Doris 社区用户群的提问与解答等。
最后,欢迎更多的开源技术爱好者加入 Apache Doris 社区,携手成长,共建社区生态。
SelectDB 是一家开源技术公司,致力于为 Apache Doris 社区提供一个由全职工程师、产品经理和支持工程师组成的团队,繁荣开源社区生态,打造实时分析型数据库领域的国际工业界标准。基于 Apache Doris 研发的新一代云原生实时数仓 SelectDB,运行于多家云上,为用户和客户提供开箱即用的能力。
相关链接:
SelectDB 官方网站:
Apache Doris 官方网站:
Apache Doris Github:
https://github.com/apache/doris
Apache Doris 开发者邮件组: