导读
本文将分享 58 同城利用 ClickHouse 搭建用户画像平台的架构实践。
1.
58 画像系统业务简介
2.
ClickHouse 实践
3.
成果与展望
分享嘉宾|
马建彪
5
8同城
资深数据研发工程师
编辑整理|辛颖
内容校对|李瑶
出品社区|
DataFun
01
首先介绍 58 同城用户画像系统的定位、核心功能以及面临的挑战。
1.
画像系统的定位
这是 58 同城画像应用平台-万象系统,上图展示的是系统首页。该系统是具有中台性质的数据管理应用平台,目标为打通数据壁垒,形成公司统一的数据资产、精准识别用户为业务提供全量闭环的用户解决方案。
2.
画像系统的核心功能
数据汇聚部分涵盖了标签整个生命周期的管理,包括以下三个核心功能:
-
标
签生产:提供快速上线标签的能力,将已加工好的标签发布到万象系统中,形成公司资产。
-
标
签市场:
是对
已接入到万象系统中的标签的知识管理,包括数据来源,加工口径,以及标签在业务中应用效果的跟踪。
-
标
签运维:对标签进行上线、下线的状态管理,以及对标签
支持
的
应用
场景管理。
数据应用的目的是提供便捷、快速的数据服务,让数据资产能够在业务场景中快速输出,响应动态变化的业务需求。主要包括以下三大功能:
-
人
群统计:获取
满足
人群组合的
集合基数
,可以辅助投放分析,进而确定投放的成本。
-
人
群分析:对
目标
人群
按
指定标签维度
进行分布
分析和
TGI
分析,辅助业务
人员了解目标
人群的情况。
-
人
群圈选:
是将满足条件组合的用户集合以文件的形式导出
形成人群包。
3. 用户画像系统的挑战
用户画像系统面临的挑战有很多,主要挑战可以总结为以下三个方面:
-
-
-
支
持覆盖
1
亿
+
用户的人群包以及每小时
1
万以上的吞吐量的人群圈选能力;
-
ClickHouse
实践
本节将介绍万象系统是如何利用 ClickHouse 解决上述问题的。
1.
为什么选择
ClickHouse
如图所示,对用户画像领域常用的四个组件进行了对比。
综合考虑之下,最终选择了 ClickHouse。在 ClickHouse 表现略逊色的领域如人群圈选和数据入库,均有对应的优化。
2.
ClickHouse
应用架构
设计
本节自下而上分别从数据结构、逻辑存储、查询语句来介绍整体的架构设计。
数据结构的设计,需要解决上述提到的功能挑战,如:多表关联,人群统计,人群分析,人群圈选,标签增减,数据入库等。如果采用横表的数据存储方式,在支持多标签时,必然涉及到多表关联整体性能会很差,无法满足场景需求。同时无法构建有效的索引键(其一,复杂类型无法有效排序;其二,全部维度都需要参与检索)
因此,采用竖表的方式存储数据,单值每个标签对应表里的一行或者多行数据;对于多值标签,数组的每个元素对应表中的一行数据。既可以实现便捷的标签增减,又可以基于标签名和标签值构建稀疏索引,提升查询效率。
另外,采用位图存储,一方面能够压缩数据量级,加速数据导入;另一方面通过位图的交并差运算可以代替多表关联,加速查询。
逻辑存储如图所示,包括分布式表和本地表。分布式表用于查询,本地表用于存储数据。本地表存储 Bitmap 数据时用到了 AggregatingMergeTree+MergeTree 两个表引擎,MergeTree 存储明细数据,AggregatingMergeTree 为明细表之上的物化视图表。本地表存储的是全局数据的子集,数据按分区键横向划分,利用位图构建的编码范围纵向划分,有助于加速查询。
以标签来源表粒度组织 ClickHouse 表。最外层是分布式表,每个 shard 上会有一个 AggregatingMergeTree 的本地表,以 dt 和 tag 作为表的两层分区,其中 dt 做数据版本控制,tag 做分区裁剪。按用户编码范围进行分桶,该用户的完整信息分到对应的 shard 中,实现分布式并行计算。示例中以每 2 的 30 次方范围划分。
场景一:人群统计。如图示例为常规查询语句,每个子查询解析后都会分发到各个子节点进行局部运算,然后再将局部运算的结果通过网络返回至协调节点进行子条件位图的聚合,最后在协调节点进行最终的逻辑运算。这种方式在基数比较大时会对网络 I/O 以及协调节点(node1)的计算造成很大的压力。
前面提到存储结构是可以满足分布式并行运算的,因此采用 cluster+view 函数组合的方式来实现。其中,view 将逻辑查询声明为临时表,cluster 将查询请求分发到指定集群的各个 Shard 中并行计算,最后 select 子句在协调节点对各 shard 返回的结果(只返回统计结果)进行汇总,这样通过两阶段聚合的操作,降低了 I/O 和协调节点的计算压力。
场景二:人群分布。如图中示例,因为采用 bitmap 的数据结构,所以需要目标群体与每一个标签值进行与操作。
-
-
通
过
with
子句将目标群体声明成一个标量,以加速与运算;
-
按
标签聚合,取每个标签下覆盖用户的
Top N
,
-
使
用
cluster
、
view
表函数实现分布式计算。
至此,clickhouse 的存储架构与查询架构已经介绍完毕,功能需求已经得到解决。
3.
ClickHouse
优化
基于上述存储和查询设计,用户画像查询功能已经实现,但在性能上还需要优化,主要是针对 I/O 和计算量的优化。与此相关的概念主要包括:datapart、稀疏索引、bitmap、I/O 优化
①
为什么需要查询时聚合呢?
场景二中显式
合并相同标签值。
因为 MergeTree 表引擎是按批次进行数据导入的,每个批次会形成一个不可修改的DataPart。clickhouse 会进行多个 DataPart 的合并,对于每个分区最终会合并为一个 DataPart,但是合并完成的时间不确定。所以在使用这种类型表引擎的时候,要么在查询时进行聚合,要么使用 Optimize 命令指定表数据合并。但是这两种方式对万象来说有都无法接受,前者需要浪费查询计算资源,后者会放大写入开销。
这里采用 MergeTree 表引擎,借助 spark 离线构建 bitmap 并 base64 加密后再导入到 ClickHouse 中,在 ClickHouse 中用物化列识别该字段。实现导入唯一位图,不在需要查询时聚合。
随着时间的推移,查询的性能波动很大。分析发现,ClickHouse 采用多磁盘的 JBOD 存储策略,数据写入阶段生成的 datapart 会轮询的落到每一个磁盘上;随着 datapart 的合并,会形成一个较大的 datapart,并且也只会落到一个磁盘上,在人群分析的这个高 I/O 的场景下,便产生了数据查询的瓶颈(单磁盘 I/O),造成相同查询前后响应差异大。
通过设置 max_bytes_to_merge_at_max_space_in_pool 参数避免形成较大 datapart 解决。此外控制每个导入 clickhouse 的批次大小与上述参数一致,能够降低写放大,加快数据导入的速度。
上图是主键索引的示意结构,datapart 内部 clickhouse 会对主键列,生成primary.idx 的索引文件,对每一个列生成 .mrk 后缀的标记文件和 .bin 后缀的数据文件。主键默认以 8192 行数据作为索引粒度,生成一条索引记录,索引的值即 8192 行中的第一个值。虚线框中为数据文件,包含多个压缩数据块,每个数据块会有一个或者多个 granule 组成,granule 是 ClickHouse 中的最小存储单元,跟主键的索引粒度一致。.mrk 文件是索引与数据文件的桥梁,此文件中包含压缩块的地址和解压后的地址。
因此,对于少量数据行的查询,越小的 granule 扫描的数据越少,性能越好。
对于人群统计与人群圈选,都是少量条件的命中,因此综合选择后采用 128 行作为一个索引颗粒。(settings index_granularity=128)
ClickHouse 的位图的是 roaringbitmap 类型,该类型将数据分为高 16 位和低 16位,低 16 位用来存储具体的用户编号。低 16 位存储结构叫做 Container,面向不同的数据特性分为 BitmapContainer、ArrayContainer 和 RunContainer。
如图我们对编码方式进行了比对,顺序编码可以获得更高的压缩比,更少的高位索引,既可以减少数据量也可以加速计算。因此将位图的优化转化为用户编码的实现问题。我们采用分桶顺序编码的方式实现,如图中右侧所示。
人群圈选慢的原因在于 clickhouse 查询由单个协调节点组装结果集后返回。
面对这个问题,我们采用了两阶段检索的思路。将人群圈选分为 query 和 fetch 两个阶段,query 阶段进行结果集的过滤,使用 ClickHouse 实现,结果集以 bitmap 的结构导出,整个阶段耗时 1s 左右。fetch 阶段进行结果集的获取与导出,借助 Spark 的分布式处理实现。
此外用户编码字典表如图所示,也采用 bitmap 的结构进行存储,由桶号、bitmapStr 和用户标识数组 3 列,用户标识数组中元素按对应编码顺序存储。通过一次 map 转换,内部利用位图与运算即可完成编码的反解逻辑。
以上是对万象系统的存储结构、查询结构和优化的介绍。
用户画像系统的成果与展望
1.
服务效果
2.
用户画像系统后续规划
-
优
化查询架构,
减少
本地节点频繁建立连接的问题。
-
-