CCO是Chief Customer Officer的缩写,也是阿里巴巴集团客户体验事业部的简称。随着业务的多元化发展以及行业竞争的深入,用户体验问题越来越受到关注。CCO体验业务运营小二日常会大量投入在体验洞察分析中,旨在通过用户的声音数据结合交易、物流、退款等业务数据,洞察发现消费者/商家体验链路上的卡点问题并推进优化,带给消费者和商家更好服务体验。
以今年3月为例,通过统计日志数据发现,共有80+业务同学提交了22000+个Query,都是围绕着用户心声和业务数据的多维度交叉分析,如果按照每个Query小二平均投入10分钟进行编写、执行、检查等操作来计算的话,共计投入458工作日,也就意味着这80+业务同学人均每个月至少有1周的时间全部投入在数据处理、运行上。业务侧大量的洞察分析诉求也使得体验洞察的数字化和智能化能力建设势在必行,我们需要有能支持到业务复杂场景Ad-hoc查询的数据能力和产品能力。
通过对数据产品的不断迭代,我们采用Hologres+FBI支撑CCO体验小二所有数据探索需求,月均50亿+明细数据聚合查询秒级返回,支持100+业务小二大促、日常的体验运营洞察分析,助力业务小二单次洞察分析提效10倍以上,解放业务同学的生产力。在文本中,我们也将会介绍CCO数据洞察产品基于Hologres在BI查询场景的最佳实践。
结合业务,我们梳理了当前CCO体验洞察数据应用的几个特点:
因此在整体数据方案落地的过程中,如何快速响应业务不断变化的需求,同时考虑业务上的数据特点,选择相对稳定且高可用的方案是我们需要面对的问题。这里主要经历了三个阶段。
这个阶段还未支持实时的洞察能力,采用的方式是比较常规的预计算聚合Cube结果集,即在MaxCompute侧将所需要的交叉维度指标预计算好,形成一个ADS层的聚合指标结果宽表,通过外表或者DataX工具将聚合结果写入到OLAP引擎加速查询。此阶段CCO较为主流的OLAP引擎选型主要是ADB、MySQL等。这种方案在应对较少且相对稳定的维度和指标组合时较为适用,因为结果已经预计算好,只需要针对结果表进行简单聚合计算,ADB也提供了稳定的查询加速能力。以下为整体数据链路结构的简单示意图:
但是随着业务场景的更加复杂化,存在的问题也极为明显:
这个阶段实时化洞察已经在很多场景有较强的诉求,故需要同时结合实时链路来考虑方案。方案一不适合实时链路的建设,主要在于预计算的多维汇总宽表难以确定PK,一旦维度组合发生变化,PK需要重新定义,无法稳定的支持upsert/update操作。
所以在这个阶段主要针对扩展性灵活性等问题重新设计了方案。主要的思路是不做维度的预计算,而是抽取洞察场景内事实表的实体对象ID,构建基于这些实体对象ID的轻度汇总DWS指标层,然后将DWS指标事实表和实体对象的DIM表直接写入到OLAP引擎,在数据服务或者FBI数据集这一层直接join查询。
以共享零售为例,业务的本质是买家下单,货从卖家流转到买方。这里的参与的对象有商品、商家、买家、骑手等,我们构建以商品ID+商家ID+买家ID+骑手ID的联合主键,计算在这个主键下的各业务模块的指标汇总事实表,然后利用OLAP引擎的强大的交互分析能力直接关联事实表和维表做多维分析查询。数据链路结构的简单示意图如下:
这种方案对比方案一解决了扩展性问题,提升了灵活度,维度的扩展只需要简单调整维表即可,遇上行业的调整甚至无需做任何处理;同时PK稳定,也能支持到实时upsert。但也因为数据展现端关联查询逻辑复杂,性能上对OLAP引擎要求较高。存在的问题可以总结为以下几点:
为了能支持到更加丰富的场景以及支持到实时离线联邦查询下灵活的窗口应用,我们方案的考虑方向转向为不再做指标的预计算,直接将明细数据写入到OLAP引擎,在数据集/数据服务等服务层直接关联DIM表即席查询。同样这对OLAP引擎的性能要求极高,CCO在去年实时架构升级之后,参见CCO x Hologres:实时数仓高可用架构再次升级,双11大规模落地,借助Hologres列存强大的OLAP能力及实时离线联邦查询能力使该方案落地变为可能。
没有最好的方案,只有在对应场景下做出取舍后相对适用的方案。在这个阶段,我们牺牲了一定的查询性能,选择了对场景支持更丰富、实时离线联邦查询以及扩展灵活度更支持更佳的方案。当然在淘系这类较大数据量的业务场景中,我们也做了一定的优化和取舍。如在实际处理中,对于相对稳定的维度我们在MaxCompute/Flink处理写入了明细,只对于行业类目等这类易调整且相对敏感的维度直接在数据集/数据服务关联查询。
三种方案对比:
场景 | 方案一:预聚合 | 方案二:轻度汇总 | 方案三:明细即席查询 |
查询性能(较大数据量) | 较好 | 一般 | 一般 |
维度支持 | 支持丰富但数据量易爆炸 | 支持范围固定 | 维度支持丰富 |
扩展性 | 较差 | 好 | 较好 |
去重计算 | 存在膨胀 | 存在膨胀 | 可精确计算 |
实时离线联邦查询窗口对比 | 不支持 | 不支持 | 灵活支持 |
行业回刷 | 需要回刷 | 无需回刷 | 无需回刷 |
结合CCO体验业务在数据洞察应用场景中数据量大、周期长、链路范围广、维度特征多、实时离线对比窗口及快照特征诉求多等需求特点,我们利用Hologres+FBI的各种特性不断在实践中设计优化整体的解决方案。从数据应用诉求来说,用户可以接受一定时间的返回延迟,涉及较大数据量读写但同时查询QPS较低,因而我们选择牺牲一定的查询RT,选择使用基于Hologres明细的即席查询的方案,整体流批两条链路结构如下:
如上所示,整体的方案是相对典型的Lambda结构:
以下为我们针对上面提到的前阶段数据使用存在的各种问题,在实践应用中的一些详细的技术方案。
主要查询场景是基于明细按时间范围的OLAP查询,数据规模单日分区超数十亿,同时也需要按天更新回刷数据,所以Hologres表的属性选择上,是列存+业务主键PK+日期分区表。
Table Group的设置一般根据使用场景、数据量大小、Join频次综合考虑。需要关联的表放入同一个Table Group,通过Local Join减少数据的Shuffle,可极大提升查询效率。
Shard Count根据数据量选择合适的大小。Shard数过小数据的读写会存在瓶颈,而Shard数过大会导致日常固定的开销以及查询启动的开销增大造成浪费,大量的Shard数过大的表同时启动查询也容易给集群的负载造成压力,影响使用性能。目前体验洞察实践中,日增量亿级的交易类明细结果Shard Count设置为128,退款、咨询求助等日增量千万左右的明细表Shard Count设置为32。
Hologres提供了Distribution Key、Clustering Key、Segment Key、Bitmap Columns等一系列的索引方式对表进行优化,合理的使用各类索引,可以大幅提升使用性能。分布建Distribution Key只能是PK或PK的部分字段,选择基于PK来设定;对于商家、类目、行业等经常用在Filter和Range场景的字段,我们对应的设置了聚簇索引Clustering Key。而对于大量的二分类的维度特征以及枚举较少的字段,如是否直播订单、商家分层等,我们对应设置了位图索引Bitmap Columns等。
BEGIN;
CREATE TABLE "public"."ads_case_di"
(
"date_id" TEXT NOT NULL,
"case_id" INT8 NOT NULL,
"industry_name" TEXT NOT NULL,
"seller_id" INT8 NOT NULL,
"seller_nick" INT8 NOT NULL,
"is_presale_order" TEXT,
"is_live_order" TEXT,
XXX ,
PRIMARY KEY ("date_id","case_id")
)
PARTITION BY LIST (date_id);
CALL SET_TABLE_PROPERTY('"public"."ads_case_di"', 'orientation', 'column');
CALL SET_TABLE_PROPERTY('"public"."ads_case_di"', 'segment_key', '"date_id"');
CALL SET_TABLE_PROPERTY('"public"."ads_case_di"', 'clustering_key', '"industry_name","seller_nick"');
CALL SET_TABLE_PROPERTY('"public"."ads_case_di"', 'bitmap_columns','"is_presale_order","is_live_order"');
CALL SET_TABLE_PROPERTY('"public"."ads_case_di"', 'dictionary_encoding_columns', '"industry_name","seller_nick","is_presale_order","is_live_order"');
CALL SET_TABLE_PROPERTY('"public"."ads_case_di"', 'time_to_live_in_seconds', '17280000');
COMMIT;
在Flink作业定义Hologres Sink表时,需要配置`partitionRouter`和`createPartTable`参数来保证流作业数据Sink到实时的分区以及在路由不到分区时自动创建分区。
partitionRouter = 'true'
createPartTable = 'true'
Holo的分区表是子母表结构,子表的当日分区作为流作业的Sink表,T+1及之前的分区为离线任务Batch写入,在每天上午离线任务调度结束数据生成后覆盖实时作业写入的数据。而在T+1的离线数据写入的时候,如何避免写入时出现空分区或者查询抖动,目前的方案是批写入临时子表然后rename并挂载到母表,可以瞬间完成T+1分区的数据切换,避免影响应用端使用体验。以下以某个表示例。
BEGIN;
--线上表分区子表,如果不存在分区,就创建该分区
create table if not exists ads_tb_di_${bizdate} partition of ads_tb_di
for values in ('${bizdate}');
--批数据写入的中间表子表
create table if not exists ads_tb_di_batch_${bizdate} partition of ads_tb_di_batch
for values in ('${bizdate}');
--解除线上表依赖关系
ALTER TABLE ads_tb_di DETACH PARTITION ads_tb_di_${bizdate};
--解除中间表依赖关系
ALTER TABLE ads_tb_di_batch DETACH PARTITION ads_tb_di_batch_${bizdate};
--名称互换
ALTER TABLE ads_tb_di_${bizdate} RENAME to ads_tb_di_temp_${bizdate};
ALTER TABLE ads_tb_di_batch_${bizdate} RENAME to ads_tb_di_${bizdate};
--挂依赖
ALTER TABLE ads_tb_di ATTACH PARTITION ads_tb_di_${bizdate} FOR VALUES in ('${bizdate}');
--删除临时批表
drop TABLE ads_tb_di_temp_${bizdate};
commit;
在BI的使用上,我们选择FBI(阿里集团内部的一款BI分析产品)。目前FBI一个组件只支持一个数据集,为了支持多维交叉分析应用,我们比较常见的方案是在数据集SQL中将所有可能用到的表拼接起来以备查询。但实际的即席查询场景中,用户选择的指标和维度可能只使用到了数据集中的部分表,如果全量查询数据集,会造成浪费同时也会影响查询性能。
结合FBI的 Velocity语法和Fax函数等特性配置动态查询可以实现根据用户的选择动态路由裁剪,在数据集中如下使用Velocity语法添加判断语句,在扩展指标中配置动态查询的参数。这里的${tableindexorder} == 'order' 代表交易明细表,数据量较大。
在实际的即席查询场景中,如用户只选择了“纠纷介入率”这类指标和维度,和交易数据没有关系,那么最终执行的query将不会命中${tableindexorder} == 'order' 这个分支下的SQL,借此实现对数据集SQL的裁剪,从而避免了每次查询都全量执行整体数据集,可以根据实际使用场景按照“不使用则不查询”的原则提升查询效率。
大促场景下实时离线联邦查询的诉求十分常见,尤其当前时间点位同环比历史同期时段点位这类对比需求,目前基于明细宽表的即席查询架构更加灵活高效。首先离线部分无需再进行预计算,尤其如果对比点位比较细的话,如5分钟、10分钟这类窗口点位的对比,那离线需要预计算准备的数据较为复杂,数据量也十分大。另外对于活动当天退款量、退款金额的累计趋势这类很常规的诉求的实现,也不再需要通过Flink计算每个点位的数值,再通过窗口函数进行聚合。直接对关键时间字段增加打点字段,一个简单的窗口函数即可完成累计趋势图的绘制。比如以下为一个10分钟窗口累计趋势的示例:
select date_id
,create_time_10min ---10分钟向后打点
,rfd_cnt --当前时间窗口退款量
,rfd_amt --当前时间窗口退款金额
,sum(rfd_cnt) over(partition by date_id order by create_time_10min asc) as total_rfd_cnt --累计退款量
,sum(rfd_amt) over(partition by date_id order by create_time_10min asc) as total_rfd_amt---累计退款金额
from (
select date_id
,create_time_10min
,count(*) rfd_cnt
,sum(refund_real_amt) as rfd_amt
from ads_tb_di
where date_id in ('20201111','20211111') --大促当天和历史同比
group by date_id
,create_time_10min
) t
;
--create_time_10min 这里是对退款发起时间的打点字段,等同于replace(substr(FROM_UNIXTIME(UNIX_TIMESTAMP(case_create_time) - (UNIX_TIMESTAMP(case_create_time)% (10 * 60)) + (10 * 60)),12,5),':','')
由于采用了Hologres分区表的设计方式,当遇到需要同时回刷多个历史分区的情况时,由于Hologres分区是子母表结构且不支持向母表Insert数据,这里实现动态回刷多分区这类场景相对麻烦一些,Hologres当前不支持程序块脚本,一般需要通过python/perl等脚本来进行对分区子表的循环操作。在这里我们采用DataWorks的控制节点配置用以相对简单的实现对Hologres分区表的动态回刷。
在体验洞察的场景里,有着大量的去重计算的诉求,比如咨询万笔订单求助量等这类指标,咨询场景中会话量的计算大多是基于非主键列的计算,在目前这种基于明细的查询下,虽然避免了预计算结果集上聚合数据值膨胀的情况,但大量的distinct操作极其影响性能。因而应对去重计算,在不同场景下我们做了些不同的优化方案选择。
在首屏核心指标块这类重要的呈现场景,比如万单求助量、小蜜发起量等重要观测指标的大数概览统计,因为指标的精确性要求,我们会使用distinct去重计算,这类指标数量不多,也因为不涉及下钻分析只是概览统计,对于离线场景可以在FBI等展示端设置较长的缓存周期,查询命中缓存的概率较高,可以一定程度的减少distinct带来的性能影响。
对于行业、类目等这一类重要并且高频被使用到的的维度场景,并且这些维度对计算的精度也有着较高的诉求,为了保证去重计数查询的性能,我们利用Hologres的RoaringBitmap的数据压缩和去重特性在较大数据量下进行计算。因为RoaringBitmap本质上还是做了一层预聚合计算,如果维度太多粒度太细数据量也会膨胀的比较厉害,为了保证优化的效果,这里我们选取部分重要维度,结合前文提到的FBI Velocity语法判断,当查询的维度命中在RoaringBitmap基础聚合的维度范围时,通过RoaringBitmap快速返回结果。RoaringBitmap去重示例如下:
CREATE EXTENSION IF NOT EXISTS roaringbitmap; --创建roaringbitmap extention
-----创建映射表,用以映射去重字段serv_id到32位int类型
BEGIN;
CREATE TABLE public.serv_id_mapping (
serv_id text NOT NULL,
serv_id_int32 serial,
PRIMARY KEY (serv_id)
);
CALL set_table_property('public.serv_id_mapping', 'clustering_key', 'serv_id');
CALL set_table_property('public.serv_id_mapping', 'distribution_key', 'serv_id');
CALL set_table_property('public.serv_id_mapping', 'orientation', 'column');
COMMIT;
-----创建基础聚合结果表
BEGIN;
CREATE TABLE ads_tb_roaringbitmap_agg (
date_id text NOT NULL, --日期字段
bu_type text,
industry_name text,
cate_level1_name text,
cate_level2_name text,
cate_level3_name text,
uid32_bitmap roaringbitmap, -- 去重计算结果计算
primary key(bu_type, industry_name,cate_level1_name,cate_level2_name, cate_level3_name, date_id)--查询维度和时间作为主键,防止重复插入数据
);
CALL set_table_property('public.ads_tb_roaringbitmap_agg', 'orientation', 'column');
CALL set_table_property('public.ads_tb_roaringbitmap_agg', 'clustering_key', 'date_id');
CALL set_table_property('public.ads_tb_roaringbitmap_agg', 'event_time_column', 'date_id');
CALL set_table_property('public.ads_tb_roaringbitmap_agg', 'distribution_key', 'bu_type,industry_name,cate_level1_name,cate_level2_name,cate_level3_name');
end;
--------将映射表里没有的serv_id写入进去
WITH
serv_ids AS ( SELECT serv_id FROM ads_xxx_crm_serv_total_chl_di WHERE date_id = '${bizdate}' GROUP BY serv_id )
,new_serv_ids AS ( SELECT a.serv_id FROM serv_ids a LEFT JOIN serv_id_mapping b ON (a.serv_id = b.serv_id) WHERE b.serv_id IS NULL )
INSERT INTO serv_id_mapping SELECT serv_id
FROM new_serv_ids
;
------按照聚合条件聚合后插入roaringbitmap聚合结果表
WITH
aggregation_src AS( SELECT date_id,bu_type, industry_name,cate_level1_name,cate_level2_name, cate_level3_name, serv_id_int32 FROM ads_xxx_crm_serv_total_chl_di a INNER JOIN serv_id_mapping b ON a.serv_id = b.serv_id WHERE a.date_id = '${bizdate}' )
INSERT INTO ads_tb_roaringbitmap_agg
SELECT date_id
,bu_type
, industry_name
,cate_level1_name
,cate_level2_name
,cate_level3_name
,RB_BUILD_AGG(serv_id_int32)
FROM aggregation_src
where cate_level3_name is not null
and bu_type is not null
GROUP BY date_id
,bu_type
, industry_name
,cate_level1_name
,cate_level2_name
,cate_level3_name
;
-------执行查询,RB_CARDINALITY 和 RB_OR_AGG 聚合计算
SELECT bu_type
, industry_name
,cate_level1_name
,cate_level2_name
,cate_level3_name
,RB_CARDINALITY(RB_OR_AGG(serv_id32_bitmap)) AS serv_cnt ---去重计算结果字段
FROM ads_tb_roaringbitmap_agg
WHERE date_id = '${bizdate}'
GROUP BY bu_type
, industry_name
,cate_level1_name
,cate_level2_name
,cate_level3_name;
而对于大多数维度场景,对去重并不是要求100%精确,使用Hologres自身的APPROX_COUNT_DISTINCT近似计算,去重精度误差可达1%以内,在可接受范围内且不会大幅影响查询性能。同时可如下通过调整精度参数来控制计算的精确度,但也会相应的增加计算开销,实测默认参数值17就可以达到较好的去重精度。
set hg_experimental_approx_count_distinct_precision = 20;
同时Hologres 1.3版本也支持了UNIQ函数,跟count distinct是一样的语义,但是计算效率更高,更节省内存,后续我们将会使用。
前文提到了CCO侧体验洞察分析存在大量的快照类特征诉求,比如用户咨询时刻的货物状态、物流节点等,这类快照对分析用户求助、退款时候的真实的境况和诉求及其重要。而这类快照在各类系统中不太可能都有业务埋点,因此需要数据侧去加工得到对应的数据。这类快照数据如果通过批任务处理存在的主要问题是无法精准的获取快照状态,比如咨询时的物流节点,通过离线ETL处理需要比对咨询时间和物流各节点的时间卡先后顺序得出当时的节点状态,对节点的枚举是否全面要求极高,并且处理复杂程度也较高。
因此,通过实时的消息结合实时更新的持久化存储的维表或线上接口来生成快照类数据是较为合适的方案,以咨询时订单状态的实现为例,我们接入咨询创建的TT/MQ,发生咨询之后去查询对应订单维表或者TC接口,返回的数据写入当天的实时分区,在T+1日我们通过Hologres的外表导出的功能,将T日实时写入的这类快照状态字段从Hologres导出到MaxCompute做持久化离线存储,在批任务的链路里离线分区的快照类字段可JOIN这份数据产出,同时也可以用以后续的数据回刷、业务洞察分析。
--回写至MaxCompute
INSERT INTO ads_holo_imp_di_foreign --外表,映射ODPS表ads_xxx_holo_imp_di
(
date_id
,serv_id
,xxx
)
SELECT date_id
,serv_id
,xxx
FROM ads_total_chl_di
WHERE date_id= '${bizdate}';
一体化体验洞察于本年初上线,目前主要支持在淘系退款、咨询万求等场景的实时多维交叉分析、智能异常检测,月均50亿+数据量级下的聚合查询基本均能在秒级返回,支持到100+业务小二大促、日常的体验运营洞察分析,助力业务小二单次洞察分析提效10倍以上。
双11大促期间(11.1-11.20),一体化洞察提交执行的Query数为66w+,假设50%的Query为有效查询,同样按照每个Query小二过去平均需要投入10分钟进行编写、执行、检查等操作来计算,共计节省了6875人日,当然如果没有对应的数据/产品能力,小二受限于SQL技能以及开发成本也不会产生这么多查询,但也侧面反映了一体化洞察对小二们工作效率的有力提升。
由于目前上游依赖的中间层离线和实时模型还不能完全统一,整体的数据架构还是比较传统的Lambda架构,需要维护离线、实时两套任务,开发、任务运维的成本较高,并且实时、离线数据存在一定的差异。当然从一套代码实现原先流批两条链路的的角度来说,目前基于Hologres的架构下存储统一、计算统一的前提都是具备的,后续我们主要推进DWD中间层的模型统一,完成一体化体验洞察整体数据架构流批一体。
为了整体快速上线,目前仍有大量的FBI数据集直连Hologres库而非托管在数据服务平台。因而数据集的监控、压测、慢查询的预警优化等没法依托数据服务平台的能力纳入统一管理,为了保障数据的稳定性、高可用性,后续需要将体验洞察的所有数据集依托服务平台集中管控。
作者:张乃刚(花名:隽驰),CCO数据开发
原文链接
本文为阿里云原创内容,未经允许不得转载。