20.06.11 神策.张倩琼-数据建模与存储选型 - 图1
编者按
2020 年 6 月 11 日,神策数据举办了我们的第一场技术直播。现场神策基础研发部的负责人张倩琼老师主讲了《数据建模与存储选型》。
大数据技术是神策的根本,数据建模是神策产品得以成功的第一步。倩琼老师的这个讲座,诚意满满,讨论了几种不同的技术架构具体的差异和特色。以下内容,根据倩琼老师的现场录音整理而成。
关注神策数据公众号,回复「第一讲」即可获取 PPT 资料。
▼▼▼
大家好,我是神策数据的架构师张倩琼。很高兴今天晚上在这里和大家分享《数据建模与存储选型》。首先我会从我自身的职业经历来介绍一下我所经历的数据模型的变迁。其次会介绍数据建模相关的内容以及 etl 相关的事情。再次对于各类数据,我们如何做存储选型。最后我会介绍神策在存储选型上的考虑。顺便说一下,今天提到的数据特指用户行为数据。

一、数据模型的典型方案

1、第一代数据模型
首先,这里所说的模型分代并不是行业共识,而是从我自身的职业经历出发,所做的一个模型划分。
08 年,我加入了腾讯的搜索部门,我们小组正好负责腾讯的 10 型日志。10 型日志是腾讯搜搜网页搜索产生的用户行为日志。数据模型非常简单,主要描述哪里的用户在什么时间搜索了哪些关键词。其元数据也非常简单,直接将字段固化在程序当中,字段之间采用 \t 分隔,日志量也比较少,每天只有几十 GB,单机存储就够了。
我们保留最近三天的日志,过期数据压缩存储。通过自制的类 SQL 的单机引擎 LogScan,依据 Lex 和 Yacc 来做词法语法分析。数据应用也非常简单,主要供产品经理 来统计 PV、UV,甚至一些简单的数据挖掘,如我当时正好依据 10 型日志来做相关推荐,用户在一段时间内连续搜索了几个关键词,那么可以依据他搜索的这几个关键词来推断他当前兴趣。并依据兴趣来做相关搜索推荐,以吸引用户点击,进而缩短搜索路径,提升搜索效率。
第一代数据模型的痛点或者说缺点很明显:

  • 字段固化,当日志格式有变,需要数据处理程序也做相应改动;
  • 存储结构自定义,没有很好的查询引擎去支撑;
  • 数据按天存储,只能按天做这种 PV、UV 统计,不能做跨天的复杂分析;
  • 扩展性也不好,因为它只是针对搜索产品特殊设计的日志格式,其它各类产品,要想扩展上去并不容易。

2、第二代数据模型
再说第二代数据模型。我 12 年 10 月加入百度,13 年有幸参与到百度的 UDW 建设。UDW 的舞台比当年腾讯的搜索产品大得多,它面向百度全公司的数据平台,百度所有业务线的数据,都会汇集到 UDW。数据模型更复杂,有了 event 的概念,在描述上更加的泛化。符合 4W1H 模式:某个人在什么时间什么地点以什么样的方式干了什么样的事,都可以用 event 来描述。它的 schema,是公司内部开发的 dt-meta,可以看作是类似 Hive Metastore 的元数据管理系统。因为 UDW 面向全公司,数据量非常大,每天的数据量在数百 TB 上下。存储系统基于开源的 HDFS,查询引擎基于 Hive,做了二次开发并进行了服务化封装。我们主要将 UDW 作为基层数据,供上层的各类分析使用。
第二代数据模型已经比较完备,但是也有缺点。大部分来自于历史包袱,如百度的多数业务线,日志没有统一规划,将它们导入到 UDW 并不是一件容易的事情。需要建立一个格式化的中间层,再从固定的中间层导入数据到 UDW,这两个链条都非常复杂,实时性并不好。百度天然无账号,所以用户数据是有缺失的。虽然这里我们可以通过 ID 来描述一个人,但是却很难去精准地定位这个人。虽然百度也一直在尝试建立用户体系,还通过各种手段自建了用户属性服务——UPS,但是它和 UDW 的结合其实并不好。
3、第三代数据模型
再看第三代数据模型,这是 15 年神策创立以来,一直采用的 user-event 模型,它的 scheme 存放于基于 MySQL 的 meta 服务。存储系统除了上面介绍到的 HDFS,还引入了 Kudu。查询引擎选择了 Impala。神策内部的各类产品,都是基于这一套用户行为数据仓库。

二、数据建模

接下来是数据建模。这里是针对用户行为数据来说的。数据模型就像一个分层的金字塔,可以分为原始数据层、结构化数据层、实体层、数据集市层和数据智能层。数据量从底向上逐层减少,信息聚合与汇总的程度逐层提高。一般公司,业务日志多数都会采集,但对于上层的支持普遍是缺失的。

1、原始数据层

原始数据层,主要是刚才所说的采集的用户最原始的行为日志,还包括一些数据库的原始表等等。这一层通常是非结构化的。一般不会对外提供访问服务,仅用于追溯问题或恢复数据等一些特殊场景。

2、结构化数据层

结构化数据层,可以依据原始数据层来建立,它和原始日志同一数量级,但因为是格式化的,存储数据量会少一些。因为这一层很贴近原始数据层,我们通常不会在其上做复杂的 ETL 处理。这一层一般也不会对外提供直接的数据访问,而是作为长期的结构化备份存在。
原始数据层和结构化数据层很接近,它们并存主要是由于,在企业的既有成熟业务中,日志多数没有什么格式。需要我们抽象出一层结构化数据层。这样,再往上的数据建设,可以从结构化数据层得到数据。因为数据本身格式统一,可以使用大规模的并行 ETL 框架来处理,会极大增加转换效率。
如果直接从原始数据层来转换数据到实体层,那么首先面临一个困难:各个业务的数据格式不一致,每类数据都需要特殊处理。如将转换任务下发给各个业务层,不只是在在效率上,在安全上也存在极大的问题。对某一特定业务,他们的日志格式可能变动不频繁。但当我们放眼全公司时,因为有非常多的业务,那么对于日志格式的变动,就是一个常态了。直接从原始数据层抽取数据,也不利于应对这种变化。新业务可以直接打结构化日志,就少了原始数据层到结构化数据层的转化处理,加快数据处理效率。
结构化数据层的存储格式有一个推荐方案—谷歌开源的 Proto Buffer,它的优点是支持多语言,且非常轻便高效。另外,向前、向后的兼容性也比较好,可以比较方便地去应对用户日志格式的变更问题。主要缺点是它不像 json 一样肉眼可读,需要自己编写维护工具。

3、实体层

实体层非常重要,它的数据量与原始日志在同一数量级,但是存储更高效。这一层要经过比较完整的 ETL 处理,使其直接为上层应用服务,如用户画像、个性化推荐等等,可以直接由这一层提供访问服务。这一层的数据建模也最重要。我们常见的典型模型—星型模型、雪花模型和星系模型,都是针对这一层的建设。如何选择事实表、维度表,就是这一层要重点解决的问题。
以互联网产品的用户分析为例。套用神策的 User-Event 模型,我们可以以 Event 为核心,适当地扩展维度表。
Event 本质上是已发生的事情,理论上不可变。但是用户的很多属性是可变的,比如说用户的年龄会随着自然年的增长而增长,用户的地域,确切地说是用户的住址,可能会随着工作的变迁或者搬家等也有所变动,甚至用户的性别,也可能因变性改变。所以这里将 User 表独立出来,作为一张特殊的维度表。
除了用户属性,还有一些特殊属性也要求可变,这里的可变不是说已经发生的历史事件产生了变更,而是在我们的采集过程中,有些属性我们当时采集不到,或者有些属性就是随时间变化而变化的。这时,就可以通过维度表扩展。例如商品信息,像商品库存、价格等,是经常变动的,不适合固化在事件表里,而应该存放于扩展的维度表。对于其他不变的维度(属性),最好的方式就是将它放到事件表。这样,行为分析时,可以避免过多的数据 join,以提升分析效率。通过事件表、用户表以及扩展维度表这三者的 join,可以满足绝大多数用户行为分析需求。
对于用户行为表,一条记录描述的是一个用户做了什么,比如说在什么时间,哪个用户(用户 ID),发生了什么事情(Event Type),然后再是一些详细信息。例如,按时间序列来说,用户 123 通过百度渠道注册了我们的产品;之后在另一个时间,用户 123 进行 app 登录;之后,用户 123 搜索了 iPad;最后用户 123 以 4888 的价格支付了订单。大家可以看到,这张表非常容易地还原了当时用户 123 的操作场景。刚才也说了,用户行为表被设计成了不可变的,再加上这部分数据量非常大,所以这部分数据以追加为主,辅之以有限的修改和删除。之后在存储上,我们也会依据这个特点来做适合它的存储选型。
对于用户 123,除了用户 ID 外,还有性别、注册渠道、会员等级等等属性。我们将这些数据放到用户表里面,它的一个特点是属性会随时变化,另外,相对于用户行为表来说,数据量会极大的下降。比如说现在全世界的人口也只有 60 多亿。即使一个用户有多个马甲,用户表能达到千亿量级,也已经相当也不起了。基于这种特点,数据量相对有限,但经常修改。存储选型上,这就是它的重点考量。
回到刚才的数据金字塔,针对实体层,我们简化后的数据模型就变成了用户行为表和用户表。这个数据模型非常简单,那么原始日志层和结构化数据层其实都可以省掉,或者结构化日志我们不省略,通过实时导入工具,将它加载到用户行为表和用户属性表中去。我们也可以通过代码埋点的方式,实时上报用户行为数据。它足够简单,可以做到秒级导入,也可以满足各种各样的分析需求。
ETL 是不可不提的一个事情。顾名思义,它是对数据的抽取、转化和加载。这里有几个事情要做,比如数据校验和清洗,主要是要过滤掉无效的数据,用干净的数据来替换一些脏数据等等;如加列补列,拿 UA 字段来说,可以从中解析出用户使用的设备,采用的浏览器等等。也可以从 IP 字段中解析出一些地理信息,如用户所在的省市等。这里也需要补充一些通过正常采集没能采集到的信息,如一些字典数据,需要通过 ETL 将其补充到事件信息中。
当然假如果这些字典数据经常变动的,它就适合作为一个维度数据,放在 item 表中;再就是脱敏处理,对于一些数据,比如用户的隐私数据,像生日、身份证号等等,还有一些用户敏感的信息,比如一些消费记录、通讯记录等等,这些数据是需要做脱敏处理的;ID-Mapping 是这个阶段一个非常重要的功能,它可以跨屏贯通一个用户。在现今移动互联网时代,同一个用户是拥有多个设备的,那么他可能在多个设备上去操作同一个产品,ID-Mapping 就是将用户在多设备上的行为序列,关联在一起,作为一个完整的序列展示出来。
这儿有一些经验教训要同步给大家:
一、数据模型要尽量简洁
模型简洁 ETL 才会简单高效,才会做到更快速的导入。
二、不到万不得已,要尽量避免 join 操作
大家都知道 join 操作是一个非常昂贵的操作,在上层分析的时候,join 会极大地降低 ETL 效率。因此我们神策的事件表中,包含了各种不变的维度。如必须要进行 join ,那么就尽早,越早 join,后面的处理也就越容易。

3、数据集市层

数据集市层的数据量就非常小了,比原始日志至少小一个数量级,甚至多个数量级。它的数据来自数据仓库实体层,偶尔也会有部分数据基于格式化数据层。这里一般都是针对一些特定的主题或者一些部门的聚合数据。因为这一层的数据量大大减少,信息聚合程度又大大丰富,所以数据的主要访问需求,都应由这一层满足。以商品信息库为例,应该包含商品的进货量、库存量、销量、利润等等信息,作为一个聚合表存在。这张聚合表有可能作为上层实体层的一个商品维度表,以实体层来提供服务。

4、数据智能层

最后再说数据智能层,这一层比原始日志的数据量少多个数量级。它的数据来自数据仓库层和数据集市层。这一层通常是为满足特定的应用,采用专有算法得到,一般直接供在线服务使用。
这里举两个例子:
一、多维数据,是按照维度和指标进行了一些聚合计算的结果数据,用来满足多维分析需求。多维数据有两种存储方式,具体选择哪一种还是要根据具体的业务来,这一块后面会专门讲到。
二、用户画像数据,它描述了每个用户的一些自然属性、以及长期兴趣和短期兴趣等等。用户画像,主要是满足个性化推荐等一些个性化的数据应用需求。用户画像数据,它也描述了用户的自然属性,它和事件模型中的 User 表区别在于,user 表主要是包含用户自然属性,如用户的年龄、用户的住址。而画像数据,还包括我们挖掘出的标签、兴趣。比如我们根据用户的一些行为历史,可以分析出用户喜好。像这个人喜欢看什么样的电影,喜欢哪类体育运动等等。然后在业务上,我们就可以根据他的兴趣爱好去做个性化推荐。

三、数据存储的选型

1、行存储和列存储

再说数据存储的选型。数据存储主要分行存储和列存储。它们的特性非常互补。例如行存储写入效率非常高,列存储写入效率就非常低;行存储的压缩率比较低,列存储的压缩率却很高;再说扫描,行存储的读取扫描的效率非常低,但列存储在这上面的效率非常高;对于一些有修改需求的记录,选择行存储非常方便,但相对应的,基于列存储的修改非常困难。
将数据看作一个矩阵的话,列存恰恰是行存的转置,一个完整的记录行,在列存中会分散到各个列文件。因为每一列都是同属性的数据,数据高度同质化,列存的压缩率会非常高。写入效率上,数据应用都是按行产生的,但在使用列存时,需要打散到各个列文件中。写一条记录会涉及到非常多的列文件,效率自然就不高。再说扫描效率,主要是针对分析场景,数据分析一般是不会用到全部列,如只关注最感兴趣的几个列,那么在列存上使用列裁剪,数据量少了,扫描效率就上去了,当然要做全量数据扫描,列存不一定比行存效率高。在列存中,要想修改一条记录,可能会涉及非常多的列文件,这不仅加大了修改难度,也很难保证事务性。
根据刚才的介绍,行存特别适合基于事务的应用,而列存就比较适合分析类的应用。文本格式,还有 Hadoop上 的 Sequence File、Avro 等,都是比较典型的行存格式。列存中比较有名的有 Parquet 和 ORCFile。当时百度的 UDW 就是基于 ORCFile。神策基于 Parquet,这跟我们选取的查询引擎和存储引擎有关。

2、各层数据的存储选型

下面说一下各层数据的存储选型。
原始数据层,主要是文本的原始日志,数据量非常大,所以比较适合使用廉价的大型行存储,比如 hdfs 来存放。
结构化数据层,它虽然是二进制的数据格式,但是为了比较快速地追加写,仍然优选行存,它的数据量和原始数据层相当,HDFS 依然是这层最好的选择。
实体层主要服务上层业务,面对各类分析需求,列存非常适合。在这一层,像 Adhoc 的即时查询需求,我们一般会通过 SparkSQL 或 Impala 这类高效工具;一些比较复杂的数据应用需求,可能需要写一些比较复杂的算法,又或者数据量非常巨大,那么我们可以使用 Hive 或者 MapReduce。
对于数据集市层和数据智能层,如主题数据,也主要是为了分析类需求,可以采用和实体层一样的存储方案;对于用户画像数据,它可能会用到各类业务,甚至在线业务中(比如实时推荐),对访问时延及吞吐有更高的要求。
我们可以在计算完成后,导入到实时表现更好存储系统中,如 Hbase 或其它的 KV 存储系统;而对于多维数据,我们需要依据实际的需求选择存储方案。这里面有 MOLAP 和 ROLAP 两种。MOLAP 需要预先定义好一些维度和指标,并且完成相应的聚合运算。它的优点是查询速度非常快,并发也比较高,存储占用的空间也很小,适合在线业务。但它也有一个非常大的缺点:因为它是预计算的,所以调整维度或指标时,非常不灵活。假设维度或指标变了,需要一个很长时间的重新装载过程;另外,它的实时性比较差,这也是因为经过预计算,无法从明细数据中直接得到相应的结果。
现在开源界的 Kylin 和 Druid 都是比较优秀的 MOLAP 方案。而对于关系型 ROLAP,它在查询时可以从最细粒度的明细数据做聚合运算,MOLAP 的优点恰恰就是它的缺点,如它在查询速度上一般会比 MOLAP 慢至少一个量级,并发也比较差。但是它的优点也非常明显,维度和指标非常灵活,可以满足非常灵活的分析需求,应用拓展性也非常好。在业界,Vertica 和 Greenplum 都是表现比较优秀的产品。当然也可以利用开源的 Hadoop 生态,自制 ROLAP 数据仓库。
神策选用了 ROLAP。这是因为面向用户分析市场,模型要简单,可以支撑比较灵活的分析需求。在查询性能上,它要比 MOLAP 慢,但分析需求本身不需要特别高的实时性,我们还能通过数据组织优化、查询优化和一些缓存,也可以做到相对比较好的性能。比如我们大多数的事件分析,也仅在 10 秒左右。

四、神策的方案

神策的 Event 数据使用 Parquet+HDFS 做为主存储,按时间做了分区,分区内的数据按用户 ID 局部有序。这样在多数事件分析上都有较好的性能,但也有问题:Parquet 文件只能批量写入,无法实时写入。我们通过 KUDU 解决这个问题。它是 Cloudera 开源的新一代面向实时分析的存储引擎,kudu 底层存储格式类似 Parquet,存放于它自己实现的本地存储上。
Kudu 本身就支持实时写入、实时更新和实时查询。它的扫描性能比较 Parquet 略差,比 Hbase 要高一些,而写入性能比 Hbase 要差一些。在神策内部,我们将它作为热数据的缓存层。通过 Parquet 和 Kudu 的融合,我们做到了数据的实时导入。在查询上,我们通过视图对外屏蔽了用户对存储系统的感知。也即是说,在上层,用户看到的仅是用户行为表,针对用户行为表的查询,在我们系统内部,会通过 Union 将 kudu 和 Parquet 的数据汇总后展现给用户。这里 Kudu 的数据会定时的转储到 hdfs上,以提升它的扫描性能。
user 和 item 数据的要求是能够随时修改,因为数据量比较小,所以我们直接采用了 Kudu 存储。
虽然 Event 数据被设计为不可变,但在神策服务了上千家客户,尤其是深入到行业化之后,对于 Event 数据的修改也变成了一个实实在在的需求。所以在接下来的未来,神策也将推出部分事件可变的模型。