作者介绍:黄超傑,蚂蚁数据智能部实时数据团队数据研发工程师,致力于数据湖技术在蚂蚁落地应用、蚂蚁广告实时数仓建设、蚂蚁数据成本治理等工作。
在开始之前
如果:
- 你想要提升数据时效,能够接受分钟级延迟
- 你好奇准实时相较离线、实时数据研发究竟能节约多少成本?开发方式有什么不同?
- 你希望数据流批一体、进一步降低成本、降低实时开发难度
- 你不知怎么去设计湖表、搭建湖仓链路
- 你准备探索学习一个新的数据存储引擎/技术栈
这篇文章很有可能会帮到你!
目录
第一章 引言
第二章 湖表设计探索
第三章 湖表应用探索
第四章 湖表运维方案
第五章 准实时、实时、离线方案成本对比
第六章 Paimon建表范式
在本文,你会收获到:
- Paimon建表范式导图(可跳转至⭐️第六章 ⭐️)
- 实时、离线、准实时成本比较方案(可跳转至第五章)
- 涵盖明细/维度/汇总表、指标计算、湖表应用、运维、成本的数据湖探索实践记录
🌟在文章开始之前,你可能会需要的一些前置知识:🌟
本文核心围绕Paimon和Flink这两个技术栈,若你了解对应的基本概念,将会更有助于理解。此处附上两个浅显易懂的文档,读者可按需阅读:
- Paimon基本概念
- Flink基本概念
此外,有部分内容会涉及多个实时、离线、准实时技术引擎,如表所示,读者可对标阿里或开源的相关技术进行理解。
类型 | 蚂蚁 | 阿里 | 开源 |
计算-Stream | AntFlink | VVR/Flink | ApacheFlink |
计算-Batch | ODPS | ODPS | ApacheHive |
存储-OLAP | Explorer | ADB,Hologres | Prosto,ClickHouse |
存储-NoSQL | Lindorm | Lindorm | ApacheHBase |
存储-DFS | Pangu | Pangu | ApacheHadoopHDFS |
消息 | SofaMQ | MetaQ | ApacheRocketMQ,ApacheKafka |
消息 | SLS | SLS,TT | ApacheRocketMQ,ApacheKafka |
数据可视化 | DeepInsight | FBI | Apache Superset |
第一章 引言
1.1 背景
互联网业务高速发展的当今,离线数仓虽然有着成熟的加工链路和相对低廉的成本,但面临数据时效低的问题,而实时计算任务虽然有着秒级延迟,但是任务开发运维复杂、涉及大数据量聚合的flink状态在内存的存储以及实时链路的消息中间件存储成本均比较昂贵。此外,在传统lambda架构下,实时离线各自独立,面对兼具离线、实时诉求的需求时,需要离线和实时技术人员按两条技术链路分别进行加工,中间还涉及大量核对口径、核对数据的“人力成本”,使得相关实时化诉求无法快速满足。
现今数据湖技术趋近成熟,有Apache Iceberg 、Apache Hudi、Apache Paimon这些开源数据湖技术。在各类技术选型和业界信息输入下,笔者了解到Paimon具备数据聚合处理、部分列更新能力,同时天然和flink集成,能结合在线表的CDC信息实现在线表->湖表的准实时同步,并且有相当数量的阿里、蚂蚁同学作为committer投入在Paimon的社区中。
综上,Paimon当前已明确作为蚂蚁数据湖方案选型。笔者希望结合上述实时、离线数据研发痛点和当下Paimon的特性,以某个场景为例,提供一个低门槛、低成本、分钟级延迟、流批一体化的数据研发方案或范式,供业内数据研发参考,也供湖仓技术的引擎侧和平台侧了解当下数据研发所关注的Paimon特性与研发痛点。同时,为了方案的透明化与易用性,探索与分析过程将在本文章作详细分享,供读者查阅与讨论。
1.2 业务场景
本技术探索中,笔者选取了蚂蚁广告业务作为探索场景,涵盖产品、报表、样本、特征场景的探索分析,并挑选了广告投放智能诊断场景的数据研发作为具体应用。
广告主在支付宝投放各类广告,业务运营同学需要结合广告的曝光、点击、转化情况与广告主的各类状态(如账户余额、投放计划等等)对广告投放策略进行调整(如调整素材、充值金额、投放时间段等),蚂蚁广告投放智能诊断产品将投放效果的数据分析思路 沉淀为一个个诊断问题,每个问题对应有若干个指标和规则。诊断产品能够让用户对广告投放情况进行全面诊断(当天或历史),查看广告在对应时间下命中哪些诊断问题,并进一步执行优化操作;诊断报表将各维度诊断指标通过OLAP引擎集成在宽表中,能够支持用户对诊断指标进行自定义多维度分析;诊断告警能及时对命中重要诊断问题的广告进行监控,并汇总推送给用户,用户及时调整投放策略。
蚂蚁广告投放智能诊断场景的数据服务示意图
上述业务形态需要兼具历史和实时数据的展示分析能力,实时时效性可接受分钟级延迟;大量指标的数据来源为广告业务主要流量数据与主要实体数据(如下图),对应了离线/实时重点基础层、中间层数据资产,在数量级和复杂度上均具备一定代表性。
广告主要流量行为
第二章 湖表设计探索
2.1 明细表设计探索
2.1.1 现状和痛点
广告曝光、点击、转化(后简称“展点转”)是广告业务的重要基础流量数据,服务于广告投放效果诊断、漏斗分析、广告模型样本等多个重要领域。
业务层面,面向分析报表/策略调控,有相当一部分场景需要串联展点转数据,以便于在一行数据中得到广告的曝光、点击、转化等信息,从不同维度/口径进行上卷聚合,进行分析/调控;面向算法样本,算法检索日志需要关联每个检索的广告是否有曝光/点击/转化,进而获取算法样本,给模型学习迭代。这两个广告业务场景
技术层面,从实时消费角度来看,当前实时中间层的广告曝光流量SLS由于量非常大,下游在消费时常出现热点(除非上高保集群),但在对内的许多报表或产品场景,指标的更新的延迟性能要求不高,分钟级可接受;从数据质量角度来看,广告日志数据会有重复下发的情况,而部分场景涉及资金计费,需要严格对数据去重,当前实时链路是采用 SLS+下游多个flink任务去重来解决,成本较高。
2.1.2 探索思路
在传统实时或离线的明细加工中,实时侧是把流量行为各自用SLS承接,并在下游针对特定业务去做对应口径下的行为关联操作,离线侧是直接把相关行为串联拼接沉淀至中间层明细资产,供各个业务使用,两条链路互相独立,互不干扰。
理想情况下,数据湖下的流量明细表能够同时服务于准实时消费(分钟级延迟)和离线消费(天级延迟),即一份资产,多种用途。
基于Paimon的partial update特性,可以尝试将广告重点流量行为数据写入一张Paimon宽表,一行数据记录【某次检索在某个展位的某个广告的投放的曝光、点击、转化信息】,把三种相关的行为在一张表中实现关联,下游不再需要在flink任务中去做join;若不对三种行为预先关联,也可基于Paimon主键表的first-row特性,使用Paimon表实现去重,下游也不再需要做去重。
因此,我们可抽象为三个方案探索:
- 方案一:所有明细流直接入湖,使用Paimon主键表能够构造宽表的特性,实现去重+关联,构成行为明细宽表
- 方案二:按行为区分明细流入湖,使用Paimon主键表的first-row特性,实现去重
- 方案三:按行为区分明细流入湖,使用Paimon apppend表承接数据,不去重,对标实时sls/离线ods表
明细数据处理涉及大量数据的etl,会消耗较多资源,这也是技术选型需要关注的重要方面,因此在下文中笔者还将围绕资源和技术复杂度进行比较。
2.1.3 方案探索与讨论
方案一 主键表(行为关联+去重 )
探究问题:
- 分区:小时分区 还是 天分区?如果前者,单个桶的存储大小较少,下游消费性能会较好,但是写入会因为有迟到数据导致writer数量多,影响性能;如果后者,单个桶的存储大小较大,下游消费性能可能会受影响,写入性能较好(因为最多只有两个分区在写)。
- 合并机制:partial-update 还是 aggregation?常识而言,是要采用partial-update,但是在广告场景中,三种流量数据存在大量公共字段存储,在设计之初,笔者希望这部分公共字段在写入时不进行重复更新,于是想到aggregation的first_value聚合函数,能够让字段值维持为第一个写入的值,“看似”可以避免重复更新。
表设计:
表 | 广告曝光、点击、转化明细宽表 | |
表类型 | 主键表 | |
变更机制 | lookup | lookup |
合并机制 | partial-update | aggregation |
写入设计 | 以曝光数据更新大部分公共字段(从业务含义上,大部分情况都是曝光先到,点击和转化后到),点击和转化只更新行为特有字段,避免大量重复字段的重复更新 | 每个行为任务写所有公共字段+行为特有字段,涉及到其他行为特有字段时候,默认写入空值。 聚合策略: first_not_null_value :公共字段、特有字段的写入聚合方式,取第一个非空值。 listagg:公共行为变动字段,采用 “行为打标+字段值”,进行聚合拼接,逗号分隔 |
参数设计 | 分区类型:天分区 生命周期:3天 分桶设计:固定分桶(因为有多个任务同时写入,不能使用动态分桶) 分桶数量:800 checkpoint间隔 = 1分钟 changelog-producer.row-deduplicate = true:避免重复数据下发,使得下游任务不用去重 | 分区类型:小时分区 /天分区 生命周期:3天 分桶设计:固定分桶(因为有多个任务同时写入,不能使用动态分桶) checkpoint间隔 = 1分钟 changelog-producer.row-deduplicate = true:避免重复数据下发,使得下游任务不用去重 |
评价 | 优:
缺:
| 优:
缺:
|
方案二 主键表(去重)
探究问题:
- 分桶:固定分桶 还是 动态分桶?
表名 | 广告曝光明细表 | 广告点击明细表 | 广告转化明细表 |
表类型 | 主键表 | ||
变更机制 | lookup | ||
合并机制 | first-row(去重) | ||
写入设计/字段设计 | 仅带入曝光行为 | 仅带入点击行为 | 仅带入转化行为 |
基本参数设计 | 分区类型:分区表(天分区) 生命周期:1天,按更新时间 分桶:动态分桶/固定分桶 checkpoint间隔 = 1分钟 |
方案三 append表
探索问题:
- Flink写入表的方案有两种,用哪种比较合适?
- 明细写入时在Flink任务去重
- 明细写入时不去重,在下游聚合时去重
表设计:
表 | 广告曝光、点击、转化表 |
表类型 | append表 |
基本参数设计 | 分区类型:分区表 生命周期:1天,按更新时间 分桶:动态分桶 checkpoint间隔 = 1分钟 |
表高级参数设计
明细表的高级参数,主要针对表的写入和消费进行优化,此处列举我们考虑的参数如下
表高级参数设计 | ||
参数名 | 参数值 | 备注 |
partition.expiration-strategy | udpate-time | 按数据更新时间对分区进行生命周期管理,和odps生命周期的逻辑保持一致。(默认是按分区字段的时间) |
partition.expiration-check-interval | 1h | 分区过期检查时间 |
dynamic-bucket.target-row-num | 1000000 | 考虑到流量明细一行内容较大,所以需要参数减少一个分桶容纳的数据条数(默认为200w),进而减少分桶小文件数,优化性能 |
snapshot.time-retained | 1d | snapshot保存一天,对于流读来说,代表回刷时间最多为1天 |
write-buffer-spillable | true | 避免小文件过多的参数 |
write-buffer-for-append | true | |
snapshot.expire.execution-mode | async | paimon 默认会同步删除过期快照。当需要删除的文件过多时,可能无法快速删除,出现反压。为了避免这种情况,用户可以通过设置使用异步过期(async) |
lookup-wait | false | 异步lookup,优化写入性能 |
2.1.4 资源比较
写入任务
方案 | 表设计 |
方案一 宽表 | aggregation宽表(小时分区) |
aggregation宽表(天分区) | |
partial udpate宽表(天分区) | |
方案二 first_row主键表 | 动态分桶-曝光 |
动态分桶-点击 | |
动态分桶-转化 | |
固定分桶-曝光、点击、转化明细写入一张表 | |
方案三 append表 | flink不去重,写入点击、曝光、转化append表 |
flink去重,写入点击、曝光、转化append表 |
结论:
- 整体资源消耗方面,方案三(不去重)<方案三(去重)<方案二(固定分桶)<方案二(动态分桶)<=方案一(天分区, partial update)<=方案一(天分区, aggregation),宽表>单一主键表>非主键表
- 如果数据延迟较大且数据量大,使用天分区比较合适
- 用Paimon去重数据时,数据量较大的情况下,使用固定分桶的资源消耗较少,这是因为动态分桶会不断加大分桶数,导致写入资源消耗无限制扩大。
- 明细数据是否去重写入append表,大致会带来两倍的资源消耗差距,所以在实际场景中,需要根据下游消费任务的数量是否足够多,数量越多,节约的下游去重资源就越多,这部分资源节约可能>在明细去重的资源消耗。
- 方案一、二消耗几乎一致,但是宽表方案在数据的存储上减少了冗余字段的存储,额外有节约存储资源。
消费任务
消费任务选择【广告创意曝光次数】汇总场景的资源消耗情况进行对比,用Paimon进行聚合计算。
方案 |
方案一 aggregation宽表(天分区),下游需要去重 |
方案一 partial update宽表(天分区),下游不需要去重 |
方案二,固定分桶表,下游不去重 |
方案三,写入时不去重,在下游去重 |
实验结论:
- 涉及到下游去重的资源消耗几乎是 下游不去重的两倍。
2.1.5 技术复杂度比较
写入任务
方案一 > 方案二 > 方案三
- 方案一:开发调试需要花费较多的时间,因为涉及多个任务的同时写入更新,需要预估分桶数,写入宽表的任务调试花了不少时间
- 方案二:一个任务直接写入三个主键表
- 方案三:一个任务写入一个表,append表参数也是最少的
消费任务
方案一>方案三>方案二
- 方案一:消费较为复杂,由于多种行为被关联了,需要判断不同行为对应的字段是否为空来判断是否为目标行为,涉及到aggregation表还需要对数据进行去重
- 方案二:直接消费明细
- 方案三:
- flink去重:直接消费明细
- 没有去重:下游消费时,需要在flink任务中去重
2.1.6 小结
结合上述资源比较和开发复杂度比较,笔者认为,在广告流量场景,选取 flink去重写入append表 方案资源消耗和开发复杂度是最优的。
2.2 维度表设计探索
2.2.1 现状与痛点
广告场景有众多实体关系(如下图),针对维度表的探索,我们聚焦在广告投放诊断产品场景,该产品需要针对“单元”实体的各类维度数据进行诊断,具体来说,需要将 创意、单元、计划、委托人、广告主维度表入湖,将多个维度的指标作为“状态指标”映射到单元上,以进行进一步的“诊断逻辑”判断。
当前该产品的数据加工链路中,实时使用Lindorm(类Hbase的存储引擎)作为维度宽表存储最新状态数据,离线用odps表存储历史状态数据,针对这两部分数据各自设计产品模块供用户使用。在数据湖中,怎么以实时离线一体化的形式将大量维度数据存储和映射?维度数据的历史存储问题怎么解决?这都是我们在数据湖维度表设计中遇到的问题。
2.2.2 探索思路
- 基础维度表入湖及其schema设计探索
- 维度宽表schema设计探索
- 尝试实现将离线和实时数据在同一张湖表中存储,统一写入与消费方式
2.2.3 基础维表设计
表类型 | 适用场景 | 构建方案 |
非分区表 | 不需要留存历史数据 | 批任务初始化存量+流任务增量同步 |
分区表 | 需要留存历史的维度数据,实现流批一体 | 分区有两类:
写入方式:(1个初始化批任务,1个流任务,1个日调度批任务)
|
分区维度表-【增全量合并】写入方案示意⬇️
2.2.4 维度宽表设计
打宽方式
要创建一张维度宽表,首先需要确定如何进行打宽操作。Paimon主键表支持非主键关联(1:N)的扩展,因此可以使用多个实体的基础维表进行互相关联,以实现宽表(通过父维度left join子维度实现扩展)。打宽任务可以分为两个步骤:
- 批任务初始化
- 流任务增量数据写入
需要特别注意的是,如果直接采用这种方式进行打宽,当“父子维度对象均为新建(insert)且子维度数据比父维度数据晚到”时,父维度在关联子维度时将无法找到对应的子维度对象,从而导致这部分数据丢失。为避免这种情况,写入任务可以采用双流join、延迟关联或批任务回补等方式。
非分区表
适用场景:不需要留存历史数据
方案:
- 批任务初始化存量,有多少个上游数据来源,对应多少个批任务
- 流任务增量同步,有多少个上游数据来源,对应多少个流任务
分区表
【讨论-使用标签留存历史快照】
维度表设置分区,主要是为了存储历史维度数据,而历史状态存储主要需要面对数据漂移的问题,而维度宽表的写入任务可能非常多,如果全都按照前述基础维表方案进行流批一体设计,复杂度会比较高,因此,如果用户能够容忍宽表的部分数据漂移,可以考虑结合Paimon的标签、分区机制,较简单地留存维度宽表历史数据,因此笔者在此处对标签的使用进行了一系列讨论
Paimon标签机制
围绕标签机制,有如下讨论:
1.报表端或者工程侧当前是否可以通过标签机制消费历史数据?
a.报表侧需要适配
b.工程侧可通过sql直接查询
2.按分区查询是否比按标签查询更快?如果前者比后者快,把标签数据导入到分区中,带来的额外存储资源消耗是否<<直接查询标签提效的收益?
- 标签本质化是把snapshot复制了一份,放在了tag文件夹里,查询的效果就是查snapshot。如果是把数据导入分区,也是在分区里生成一份snapshot,也是查snapshot,查询无差异,存储也无差异。
3.维表Join能否使用标签?
- 不能,一旦使用标签,就是直接查表,不是维表join的逻辑。
4.数据按照标签快照机制的存储是和业务含义是否相符?
- 如果按机器时间,要等待迟到数据的话,也会把没有迟到的数据错误打上标签导致数据漂移,消费方不得不在消费时再进行过滤; 若是状态变化的数据,状态就丢失了,数据错误。
- 如果按watermark的话可能丢数据,而且写入代码会更复杂。那么标签能否按某个字段来生成?--经查阅paimon源代码发现,不行
结合以上讨论,笔者整理Paimon标签创建方案可能存在的问题如下所示↓
【方案设计】
在上述讨论中,我们了解到标签机制只适用于能容忍一定数据误差的场景,因此,还需要考虑更周全的设计方案,更完整准确地留存数据。在下表的维度宽表方案设计中,笔者针对不同方案的任务写入、消费的方式进行详细描述,比较各个方案的优劣和适用场景。
方案描述中的“N”含义:假设维度宽表的诸多维度字段来自 N 个维度表,则对应有N个打宽的任务
方案一 TAG存储历史数据(有漂移) | |
方案描述 | 表设计:
写入设计:
消费设计:
|
优点 | 开发方案最简单 |
缺点 |
|
适用场景 |
|
方案二 TAG导入历史分区存储 (有漂移) | |
方案描述 | 表设计:
写入设计:
消费设计:
|
优点 |
|
缺点 |
|
适用场景 |
|
方案三 历史分区双写 (无漂移) | |
方案描述 | 表设计:
写入设计:
消费设计:
|
优点 |
|
缺点 | 方案复杂,需要运维(N+1 )个flink流任务+1个批任务 |
适用场景 |
|
方案四 同步上游全量写历史分区(无漂移) | |
方案描述 | 前提-要求 上游维表本身已按数据更新时间进行天分区存储,无漂移 如上游维表为业务工程表,则可按照前述【基础维表设计】的内容,将业务工程表做成Paimon分区表,并且每天严格按gmt_modified进行分区存储 写入:
|
优点 |
|
缺点 |
|
适用场景 |
|
附:历史分区双写方案 数据链路与代码示意↓
insert into table_name
select
col1,
col2,
col1_col2_gmt_modified,
col1_col2_gmt_modified as dt
from table_name
where dt ='latest'
;
insert into table_name
select
col3,
col3_gmt_modified,
col3_gmt_modified as dt
from table_name
where dt ='latest'
insert into table_name
select
col1,
col2,
col1_col2_gmt_modified,
'${bizdate}' as dt
from table_name
where dt ='latest' and col1_col2_gmt_modified < concat('${today}','00:00:00')
;
insert into table_name
select
col3,
col3_gmt_modified,
'${bizdate}' as dt
from table_name
where dt ='latest' and col1_col2_gmt_modified < concat('${today}','00:00:00')
维表join性能
维表join性能考量方面,笔者在广告投放诊断的指标计算join场景、广告算法样本join曝光场景,使用Lindorm维表与Paimon维表,在不同的静态存储数据量(10g,100g,1TB)和join tps(<100,1000,1万)下,对维表join性能进行探查,得出以下结论:
维表引擎 | Lindorm | Paimon |
数据时效性 | 秒级 | 分钟级 |
数据量 | 大规模(>1TB) | 中小规模(<1TB) |
流式Join 性能 (TPS) | 高 TPS 的实时join (>=1万tps) | 中等TPS (<1000 tps基本稳定运行;1000-1w tps有一定性能瓶颈,反压风险,需针对性优化) |
开发复杂度 | 较高 (Lindorm的理解成本较高,运维也较复杂) | 较低 (Paimon理解成本较低,并天然与flink集成) |
硬件成本 | 高 | 低 |
2.3 指标计算(汇总表)探索
2.3.1 现状与痛点
广告投放诊断产品涉及数百个汇总指标或状态指标计算,结合Paimon的技术特点,一方面,可充分利用Paimon自带的聚合能力计算,其相较实时flink聚合更节省资源,因为状态从Flink内存(Rocksdb)直接放到了Paimon的存储(DFS)中,后者比前者成本更低,还能以更简易的方式支持长周期指标计算;另一方面,Paimon表支持流/批消费,所有指标在湖表中存储,能提供统一的消费方式,规避原来实时/离线两条链路两套口径的方式,可进一步降低开发成本和消费理解成本。
2.3.2 探索思路
计算方面,笔者均使用Paimon的内置聚合函数进行指标计算,不再采用传统的flink聚合计算的方式。
存储方面,无论实时还是离线,一般都是用一张汇总表承接多个指标,也即宽表,因此指标计算探索设计主要围绕Paimon宽表展开:
- Paimon宽表结构可分为横表(partial update表)和竖表(aggregation表),可进行分析讨论
- 当前场景涉及多个父子维度指标,因此可考虑使用在子维度指标预聚合基础上,再聚合至父维度,与直接聚合至父维度的方案进行比较,预聚合也可降低明细层的读取压力/资源消耗。
2.3.3 方案探索与讨论
横表 vs 竖表
竖表schema示例
字段 | 聚合参数 |
对象id(主键) | 无 |
统计时间窗口(主键) | 无 |
指标名 | 无 |
最新指标值 | last_not_null_value |
求和指标值 | sum |
计数指标值 | count |
平均指标值 | avg |
实际数据示例⬇️
ad_id | metric_name | cnt(count) |
a | expo | 10 |
b | expo | 20 |
c | expo | 5 |
c | clk | 5 |
横表schema示例
字段 | 聚合参数 |
对象id(主键) | 无 |
统计时间窗口(主键) | 无 |
聚合指标1 | count |
聚合指标2 | last_not_null_value |
聚合指标3 | sum |
实际数据示例⬇️
ad_id | expo_cnt | clk_cnt |
a | 10 | 0 |
b | 20 | 0 |
c | 5 | 5 |
【方案讨论】
应用场景 | 评价维度 | Paimon汇总表设计方案 | |
横表(partial_udpate+aggregation) | 竖表(aggregation) | ||
作中间表 (需要触发变更,供下游计算) | 适用场景 |
|
|
缺陷讨论 | 如果采用partial-update+ agg+ lookup的形式写入计算多个指标,任意一个指标变动,整行都会下发(非相关指标的默认为空),所以可能会有以下几个问题:
|
| |
作维表/作结果表 (不需要设置表变更机制) | 适用场景 | 作维表:维度大宽表 作结果表:报表OLAP查询,产品查询 | 作结果表:指标列存,读取速度快,适合单指标消费场景; |
缺陷讨论 |
| 作结果表:报表场景下,往往涉及多个指标消费,可能还需后置打宽,泛用性较低。 作维表:竖表天然不适合作维表,一行信息量很少。 |
【关于横表的下游消费问题】
围绕上述讨论中,有关指标横表的【缺陷讨论】,笔者做了实际的实验验证如下
实验数据链路⬇️
当单元曝光汇总链路上游执行回刷操作,向partial update表回刷数据时,曝光汇总的任务运行情况如下↓
此时partial update表的下游消费【消耗指标】的任务↓运行状态出现变化,即-在曝光指标的上游回刷时,消耗指标的消费任务输入TPS也会有大幅增长,两个直观看似不想关的指标在此场景下互相影响了。
基于该现象,笔者将【消费消耗指标的任务】读取的source数据打印输出,分别统计【曝光回刷数据期间】和【常态运行期间】情况下的输出数据情况(消耗为空的数据行数,消耗不为空的数据行数),经比较发现:
- 无论是在回刷还是常态运行,曝光的数据更新会给下游消耗的消费任务带来额外的资源消耗(额外的数据为“消耗为空的数据”)
- 横表下发的数据中,消耗为空的占了1/3,也就是有1/3的数据都必须在下游任务中通过 消耗指标is not null 进行过滤,否则可能会引发一连串问题(如下游还有join逻辑,就会有很多无效的join发生)
【结论】
综上所述:
- 若计算指标需要沉淀在中间层,优先采用竖表形式,不宜在准实时中间层使用partial update横表(会有额外的资源浪费、耦合风险);
- 若涉及同个维度多个指标的同时消费,如大量OLAP报表场景,此时下游不再需要流读消费,可以采用partial update横表的形式,一次查询查出多个指标。
结合广告投放诊断产品的应用场景,笔者在该场景汇总层采用 先使用竖表进行指标计算,再打横至指标横表的方式进行研发。
预聚合 vs 直接聚合
在分钟级延迟即可满足需求的场景中,Paimon 的聚合性能相比 Flink 聚合具有更低的资源消耗和更简单的代码。因此,在处理聚合 PV、金额等累加场景时,使用 Paimon 完成数据聚合是一个高效的选择。
在实际业务场景中,可能需要在数据湖中围绕某个指标进行多维度的聚合,这些维度之间存在父子关联关系。在这种情况下,可以选择将明细数据写入不同维度的主键表中完成聚合,但这会导致多次重复读取明细数据,资源消耗较大(如图所示,方案一)。
为了进一步节省资源,考虑到 Paimon 的聚合特性,是否可以一次写入明细数据进行预聚合,然后再进行多次上卷聚合(如图所示,方案二),这是一个值得深入探讨的问题。
在 Paimon 的聚合特性中,SUM、COUNT 和 PRODUCT 等聚合函数支持回撤消息。因此,理论上,在完成第一次预聚合后,Paimon 的变更日志(Changelog)会产生 +U 和 -U 的数据。这些变更日志可以写入下游的 Paimon 表中,进行二次聚合,从而实现高效的多维度预聚合。
Paimon lookup机制图示
【实验结果】
方案 | 延迟 | 资源消耗 |
一次聚合 | 二次聚合比一次聚合延迟 1-2min | 二次聚合的资源消耗约为一次聚合的三分之一 |
二次聚合 |
【结论】
二次聚合优于一次聚合,更新延迟在分钟级可接受范围内,因此,在相关指标能进行上卷聚合时,优先进行二次聚合加工。
2.3.4 表设计
竖表-指标计算中间表设计
表基本信息 | ||
表类型 | 主键表 | |
写入模式(合并机制) | aggregation (写入时聚合) | |
分桶模式 | 固定分桶 | |
分桶键 | id+统计时间窗口+统计时间+指标名称 | |
主要字段组成 | 聚合逻辑 | 备注 |
对象id(主键) | 默认 | |
统计时间窗口(主键, 分区) | 默认 | 1D:天粒度 1H:小时粒度 latest: 最新状态值(涉及维度指标计算的延伸指标) ts: 长周期累积指标 |
统计时间(主键, 分区) | 默认 | 天粒度格式为yyyyMMdd 小时粒度格式为yyyyMMddHH 状态指标可为固定值,如20990101000000 累积指标(长周期指标)可选为累积起始时间+ts,如 '20240101ts' |
指标名称(主键, 分区) | 默认 | |
非主键维度值 | 默认 | 根据实际需要填写,常使用当前维度的父维度对象id,用于后续指标上卷计算 |
聚合求和值 | sum | |
聚合计数值 | sum | 注:当前Paimon 0.9的agg功能不再支持count,因此count_value字段采取的agg策略是sum,相关后续相关任务想要做计数聚合时,可采用 sum(1)的方式,直接往count_value字段写入1值,和count效果等同 |
聚合最新非空值 | last_non_null_value | |
聚合乘积值 | product | |
gmt_modified | last_non_null_value | 用于记录写入该表的数据更新时间,可由写入任务生成current timestamp |
max_upload_time | max | 最大行为上报时间(工程日志的上报时间),记录最新一条明细上报的时间,用以分析数据延迟 |
max_upload_time | max | 最大行为事件时间(用户行为发生时间),记录最新一条用户行为发生的时间,用以分析数据延迟 |
...(根据聚合函数,按需添加) | ... | ... |
参数设置 | ||
错误参数:⬇️ source.ignore-delete:true #下游需要二次聚合时,该参数不能等于true |
#下游需要流读该表时,需打开该参数。(此处下游需要将指标打横,所以配置该参数)
changelog-producer:lookup
#有多个任务写入时,必须打开该参数,同时加上压缩任务
write-only = true
#按分区数据更新时间进行分区生命周期管理
partition.expiration-strategy:update-time
#分区检查间隔时间
partition.expiration-check-interval:1d
#使得下游最多可回刷历史1天数据,根据实际业务需求填写
snapshot.time-retained: 1d
#若上游有回撤流(如有去重、聚合),除了sum、product 和 count 聚合函数字段外,其它字段均需要设置回撤
fields.<field-name>.ignore-retract = true
横表-指标结果表设计
表基本信息 | ||
表类型 | 主键表 | |
写入模式(合并机制) | partial-update (部分字段更新) | |
分桶模式 | 固定分桶 | |
分桶键 | id+统计时间窗口+统计时间 | |
字段组成 | 聚合逻辑 | 备注 |
对象id(主键) | 默认 | |
统计时间窗口(主键, 分区) | 默认 | 1D:天粒度 1H:小时粒度 latest: 最新状态值(涉及维度指标计算的延伸指标) ts: 长周期累积指标 |
统计时间(主键, 分区) | 默认 | 天粒度格式为yyyyMMdd 小时粒度格式为yyyyMMddHH 状态指标可为固定值,如20990101000000 累积指标(长周期指标)可选为累积起始时间+ts,如 '20240101ts' |
非主键维度值 | 默认 | 根据实际需要填写,常使用当前维度的父维度对象id,用于后续指标上卷计算 |
A来源指标 | 默认 | |
A来源指标更新时间 | 默认 | 可选,用来查验该来源的指标的更新时间 |
B来源指标 | 默认 | |
B来源指标更新时间 | 默认 | 可选,用来查验该来源的指标的更新时间 |
C来源指标 | 默认 | |
... | ||
参数设置 |
#可选,若该表不作流任务的source则不用设置(横表作为结果表,下游一般不再流消费了)
changelog-producer:lookup
#有多个任务写入时,必须打开该参数,同时加上压缩任务
write-only = true
#按分区数据更新时间进行分区生命周期管理
partition.expiration-strategy:update-time
#分区检查间隔时间
partition.expiration-check-interval:1d
#使得下游最多可回刷历史1天数据,根据实际业务需求填写
snapshot.time-retained: 1d
-
此处对多个来源的指标聚合逻辑采用默认(last_not_null_value),是考虑到需要回刷/稳定性要求较强的场景,所有指标在上游(指标竖表)做好计算,在结果表/维表处使用默认的last_not_null_value的聚合方式,这样就能实现【部分列回刷】,避免单个聚合指标的口径更新/回刷导致整个分区的数据都要重刷。
-
在指标横表中,笔者建议根据不同的【统计时间窗口】创建不同的表,而不是所有时间窗口的指标都往一个表写,否则会存在“某些指标字段在某些时间窗口总为空值”的情况,不易理解。
第三章 湖表应用探索
明细表、维度表、指标计算(汇总表)的开发,都还只是一个个“点”的问题,最终做这些点都需要串联成线,提供给业务应用(如产品、报表)。因此,笔者需要针对蚂蚁广告投放诊断的应用场景,结合当前湖表的消费方式和性能考量,设计对应的湖数据资产的整体架构设计和供给形式。
应用场景描述:
- 场景1:工程后端查数消费,供产品使用(涉及对客产品),需要支持快速点查,查询延迟敏感。
- 场景2:报表消费(涉及OLAP能力),内部运营人员使用,需要支持批量查询,并且后置进行复杂计算,查询延迟敏感度较低。
备注:笔者没有选择将应用层湖表同步写入其它OLAP引擎中(如蚂蚁的Explorer引擎)进行分析与比较,仅针对OLAP场景下,使用Paimon直接查询的性能进行讨论,其它OLAP引擎的性能比较不在本篇文章的范畴。
3.1 消费方式考量
经内部调研,Paimon表的消费可通过内部数据研发平台的API接口进行查询,也可通过该平台的手动查询功能进行查询,这些查询本质都是生成flink批任务sql对表进行查询,因此既支持点查,也支持批查。
面对这样的消费方式,数据应用层设计则需要综合考量以下几点问题来进行:
- 稳定性:查询的稳定性是否足够匹配应用场景的保障等级?
- 性能:查询的并发承载力是否足够?
- 安全:查询是否会存在数据安全问题?
如果Paimon的消费无法满足诉求,则需要考虑将Paimon数据同步至其它OLAP引擎中使用。
3.1 数据架构方案选型
基于【消费方式的分析与讨论】,结合前述明细表、维度表、指标计算探索,无论是指标横表、指标竖表、维度宽表实际都可以直接进行查询消费,在业务口径可以允许后置计算的情况下,甚至可以尝试直接从明细表/基础维度表来进行消费。因此,整体可分为四种方案范式(如下图):
各方案在各维度上的对比如下表所示:
比较模块 | 影响因素 | 方案一 | 方案二 | 方案三 (括号里为不做中间层的情况) | 方案四 (括号里为不做中间层的情况) |
时效 | 数据时效 (数字越大,时效越新) | 5 | 4 | 3(4) | 2(5) |
查询速度 (数字越大,查询越快) | 1-?(数据量过大可能直接查不出来) | 2 | 3 | 5 | |
开发 | 开发复杂度 (数字越大,越容易) | 5 | 3 | 4(3) | 1(4) |
运维 | 问题排查难易度 (数字越大,越容易) | 4 (大量逻辑在报表里,可能不好排查问题) | 3 | 2(3) | 4(1) |
运维节点数量 (影响主备链路搭建、链路稳定性,数字越大,节点数量越多) | 1 | 2 | 4(3) | 5(2) | |
数据回刷/更新复杂度 (数字越大,越简单) | 1(etl逻辑主要维护在报表里,基本在明细层比较少涉及回刷) | 4 (指标竖表回刷+维度横表回刷) | 2(3) (一层维度+指标横表回刷) | 1(2) (两层横表都要回刷) | |
延伸价值 | 泛用性 (数字越大,越泛用) | 0 | 3 | 4 | 4 |
成本 | 资源消耗 (数字越大,越节省资源) | 2(后置计算的flink批任务资源消耗也不低,甚至直接跑不动) | 3 | 2 | 1 最高,所有指标均需前置计算好 |
3.3 方案应用与实验
经实验,方案一在广告流量场景不适用,无法及时完成明细汇总的后置计算,故不在此作比较。工程和报表消费Paimon表数据是通过API生成Flink批任务查询,因此,在方案落地应用部分,笔者针对方案二、三、四的表生成Flink批任务查询,重点实验对比查询性能和资源成本消耗。
指标竖表+维度宽表
在报表场景中,指标竖表常需要打横,以进行多个指标的展示,打横代码示意如下:
SELECT 对象id(唯一识别id)
,SUM(COALESCE(last_non_null_value, 0)) FILTER (WHERE metric_name = 'ad_expo_pv') AS ad_expo_pv
,SUM(COALESCE(last_non_null_value, 0)) FILTER (WHERE metric_name = 'ad_clk_pv') AS ad_clk_pv
,SUM(COALESCE(last_non_null_value, 0)) FILTER (WHERE metric_name = 'ad_conv_cnt_chg') AS ad_conv_cnt_chg
,SUM(COALESCE(last_non_null_value, 0)) FILTER (WHERE metric_name = 'ad_cost_amt') AS ad_cost_amt
FROM 表名
WHERE
metric_name IN ('ad_expo_pv', 'ad_conv_cnt_chg', 'ad_cost_amt', 'ad_clk_pv')
and stat_time = '查询日期'
and group_id = 'xxxxx' --指定要查询的对象
GROUP BY 对象id(唯一识别id)
指标横表+维度宽表
指标横表通过Paimon的partial update机制建成,指标会被提前打横,一行包含多个指标,可同时满足报表/工程查询的多样诉求。
【指标+维度】宽表
构建大宽表同时将汇总指标和维度值维护在同一张表中,前置通过flink任务和Paimon的partial update机制,往一张大宽表同步数据。
需要注意的是,维度值由于涉及大量状态数据(有大量状态在一次更新后就不会再更新下发),所以还需要每天使用批任务更新初始化当天的维度值。
查询时效与成本对比
方案 | 点查速度 (主键) | 点查速度 (非主键) | 批查速度 | 消费复杂度 | 资源成本/开发成本 |
方案二 指标竖表+维度宽表 | 1秒+ | 2秒+ | 6+秒 |
| 最低 |
方案三 指标横表+维度宽表 | 1秒+ | 2秒+ | 6+秒 |
| 中等,需将汇总指标打横 |
方案四 指标+维度大宽表 | 1秒内 | 1秒内 | 2+秒 | 查询最简单,一次查询可以把所有汇总指标和状态指标都带出来 | 最高,需将汇总指标打横,然后再将维度宽表和指标宽表使用partial-update拼接 |
3.4 广告智能投放诊断报表的全链路设计
综上,笔者在广告智能投放诊断场景,最终决定采取 汇总指标横表+维度宽表 方案提供数据报表服务,即指标宽表 和维度宽表通过后置计算方式进行join消费,所有聚合计算前置完成,此外,也可选地使用用指标中间表提供部分不需要打横的特殊指标(如账户余额)。
报表中,会汇集所有广告投放的诊断情况汇总以及对应的诊断明细,报表分钟级延迟更新,全链路最高延迟<=5分钟。
第四章 湖表运维方案
以上几章集中阐述了湖表的开发与应用方案,准实时场景下的运维也非常重要,事关数据的质量与稳定,本章将针对这部分进行一定补充。
4.1 湖表数据回刷/更新
首先,建议非当日分区的数据回刷,都直接使用flink批任务处理
【涉及aggregation字段的回刷】:
aggregation是将聚合状态存储在表中累加,除了“last_value"/"last_non_null_value" 指标回刷/更新外,其余类型指标均不能直接重置任务来进行指标更新/回刷,否则会重复累加计算。若要进行数据回刷/更新,需要按照以下步骤进行⬇️
情况1:回刷时间在上游表的snapshot留存时间范围内
- 若涉及回刷最新时间分区,需先暂停当前写入任务
- 清空分区
- 重置任务为回刷起始位点
情况2: 上游snapshot已经过期,无法支持实时回刷
- 按情况1的方案,回刷最近分区
- 更久以前的数据回刷,使用批任务,指定上游分区数据,写入下游
【不涉及aggregation字段的回刷】
非aggregation字段其实默认是用Paimon的“last_value"/"last_non_null_value" 聚合参数,任务可直接进行重启回刷,指标将按最新值进行更新。
4.2 主备链路
- Flink任务:在两个高保集群构造主备任务
- Paimon表:在不同的dfs挂载点建主备表
4.3 数据准确性核对
- 与离线链路核对:若有离线数据链路核对,则可创建Paimon外部表,查询两表同一统计周期的指标进行批量核对ODPS任务读Paimon实验
- Paimon表DQC核对:可配置flink批任务,定期查询Paimon表进行DQC查验
4.4 表/任务监控
需要关注以下三点:
- Flink任务监控:数据加工任务+压缩任务
- Paimon表小文件数:小文件数过多,会导致底层存储引擎dfs出问题
- 分桶数(动态分桶):分桶数过多时,会影响表消费的查询速度,尤其是批读消费(涉及报表或产品消费)
第五章 准实时、实时、离线方案成本对比
5.1 对比思路
笔者所选择的广告投放诊断场景,同时具备离线和实时数据链路,现已针对该场景使用Paimon+Flink开发了对应准实时链路,前述重点探究了准实时链路的研发技术方案,如果能从成本角度对比 实时、离线、准实时的差异,将会更全面的评估准实时链路的价值。
当前实时的计算/存储引擎成本计费需要考量写入消耗+静态存储消耗+读取消耗,而本探索项目中,准实时链路并未完全对业务使用,读取消耗难以对齐评估,因此,笔者选择从预算提报的角度对成本资源进行评估(这也符合“打算使用数据湖开发,需要评估并申请预算”的常见场景逻辑)。
预算提报角度下,实时引擎只考虑 写入消耗 和静态存储,能与准实时链路对齐比较。如下图示,我们结合各引擎的资源消耗,按照各引擎的预算评估方式,将预算按照年初100%交付提报进行估算,并转换为每日成本金额,进而实现三种链路的成本统一度量。
5.2 实现路径
具体实现方面,笔者与Flink、Lindorm、SLS、ODPS、Paimon这几个引擎对应的开发团队、DRE、财务沟通对齐明确对应的成本估算逻辑,计算出引擎按预算提报的单价,并拉齐该业务场景下的三条链路的任务、表资源,进行多种维度比较和分析。
5.3 成本对比结果
具体成本数值不便在本文中公开透露,基于此,我们将下述比较中最少成本的设为基准值1,其他成本情况将以该基准值进行相对赋值(括号中标识)。
- 总成本:离线(1)<准实时(2)<实时(5)
- 明细层:准实时(1)<<离线(3)<实时(10)
- 指标计算:准实时比实时节约近64%成本,流量越大的任务节约越多
- 存储引擎:准实时(1)<离线(10)<<实时(200)
总体而言,实时数据服务(秒级延迟)与离线数据服务(T+1天延迟)相比,其提供的数据服务的成本是准实时数据服务(分钟级延迟)的六倍。准实时数据服务能够提供更灵活的数据形式,如湖表相较于实时Lindorm可直接进行SQL查询、支持批量查询,便于排查问题和进行OLAP消费。准实时数据服务还提供更统一的消费方式,即湖表可以同时进行流读和批读,同时存储最新和历史数据,从而进一步节约了人力成本。
在企业内部应用场景中(如内部运营、产品、数据分析的数据查询分析),用户通常能够接受分钟级的延迟。基于Paimon的准实时方案在资源成本和研发复杂度方面相较于纯实时方案具有显著优势,特别是在ODS层、DWD层数据存储以及超大规模数据聚合计算场景中。
第六章 Paimon建表范式
本章为前文【第二章】的范式总结,可供读者后续在实际的湖表开发、方案选型时进行查阅。
6.1 明细表
6.2 维度表
下图中涉及的【分区写入方式】的具体方案,可查阅【第二章-2.2 维度表设计探索】中的具体内容
6.3 汇总表
图中涉及的指标横表、指标竖表的具体设计,可查阅【第二章-2.3.4 表设计】
第七章 回顾与展望
7.1 回顾
在探索初期,我们围绕“Why Paimon”,与多方进行沟通交流,明确应用场景与预期价值。
在探索中期,我们聚焦“Paimon关键特性”,与平台、引擎、业务团队密切合作,充分应用与试验。
在探索后期,我们关注“成本与质量”,结合研发经验、平台能力、多方调研,对湖表的运维和成本方面进行了一定补充。
我们从0到1建设了广告业务的数据湖资产,以更简单的方式(Paimon+Flink)和更低成本(1/6)构建了更统一(流+批)的数据链路,并在过程中充分探索了各类建表规范、性能考量、方案选型。
当然,在探索过程中也有众多遗憾,比如准实时链路开发的研发效率评估、稳定性的评估、广告算法样本链路的应用等等,这些都希望能够在后续的工作中有所探索~
7.2 展望
基于Paimon的数据湖技术应用的展望,笔者目前认为有以下几方面:
- 对比当前的离线、实时技术栈,基于paimon的准实时链路的研发效率、稳定性该如何进行有效衡量(当前只是定性直观对比);
- Paimon的表性能的可观测性做得还不够完善,如小文件数、join性能、压缩性能的监控指标透出;
- 已有的实时和离线资产,有多少可以用湖来代替?怎么进行评估和识别?对应的成本收益会有多少呢?
- 既然已经整理出湖表建表范式,那能否有进一步的“傻瓜式选择”,识别用户的业务诉求,自动生成对应的湖表乃至flink任务,不再需要用户了解Paimon的众多参数(尤其是数据量少的场景),甚至不再需要对Paimon表、flink任务进行常规运维。
- Paimon表在很多情况需要多任务写入,对应需要“评估分桶数”,这对用户来说是一个很重的操作,而且往往存在一定误差(比如用odps的数据量来预估Paimon的数据量,大致有20~30%的误差),如果能够实现“全面的动态分桶”,将有效降低Paimon表的研发门槛。
- 当以Paimon为代表的数据湖技术方案成熟后,面对一个新的数据需求,我们后续如何评判使用 离线/实时/准实时技术方案呢?有没有一个评判标准?最直观的评判因素是“时效性诉求”,但这不能作为唯一依据,因为有时候用户不一定能做出这个判断,秒级、分钟级、小时级 这几种时效差异的业务感知在没有完成开发前是比较很弱的。有时候用户只是拍脑袋地认为“时效越高越好”,实际可能小时级的数据产出就已经能满足业务诉求。