在互联网大潮中,新东方在IT技术上也不断重构,持续投入大数据建设,研发大数据的相关技术和应用,从而快速而精准地响应业务需求,并用数据为集团各级领导提供决策依据。新东方的大数据应用主要包括两部分:
为了满足日益增长的业务需求,集团开始投入数仓建设。在数据仓库建设的初期,以业务驱动为主。通过阿里云的MaxCompute为核心构建数据仓库,直接集成业务库数据以及WEB应用的OSS日志等,然后在数据仓库中分析业务数据并产生统计分析结果。初期的架构如下:
根据业务需要,将中小型规模的结果导入MySQL并支持数据更新,数据规模较大的只读结果则导入 MongoDB。
然后Web服务层查询MySQL和MongoDB并向用户提供服务接口, Web服务层也可以通过Lightning加速接口直接查询MaxCompute的数据。
Lightning协议是MaxCompute查询加速服务,支持以PostgreSQL协议及语法连接访问MaxCompute数据,相比MaxCompute提供的odps jdbc接口速度要快得多。原因是后者把每次访问作为一个Map-Reduce处理,即使很小的数据量查询响应时间也要超过10秒,而 Lightning能将延时降到百毫秒内,满足业务结果报表的展示需求。目前Lightning服务进入服务下线阶段,新的加速服务由Hologres加速集群替代。
使用这套架构可以在较短的时间内满足报表开发、用户画像和推荐服务等需求,为新东方的日常运营和招生引流提供较好的数据支持。但是随着业务的开展,这套架构越来越难以满足用户的需求,主要体现在:
要解决以上的业务痛点,就需要找到能满足实时数仓建设需求的产品。大数据团队调研了多种实时数仓方案,基于新东方的数据和应用特点进行选型,方案比对如下:
产品 | Ad-hoc查询 | 高并发支持(QPS) | SQL支持 | TP(交易)支持 | 与MaxCompute/Flink集成 | 文档和技术支持 |
ClickHouse 20.1 | 支持PB级以上 | 默认支持100的并发查询,qps取决于单个查询的响应时间 | 单表查询支持较好,复杂报表查询支持较弱 | 通过mutation支持update,较弱 | 支持 | 文档丰富,社区支持较好 |
Doris 0.9 | 支持PB级以上 | 数百 | 兼容MySQL | 不支持 | 通过兼容MySQL与MaxCompute集成,与Flink的集成 不明确 | 文档和社区都较差 |
Hologres 1.1 | 支持PB级以上 | 数万以上 | 兼容PostgreSQL | DDL支持 | 与MaxCompute直接在存储层集成,并且都兼容PostgreSQL,提供Flink Connector集成 | 阿里在线文档和技术支持 |
Tidb 4.x (含Tiflash) | 支持PB级以上 | 数万以上 | 兼容MySQL | 支持 | 支持 | 文档丰富,社区支持较好 |
Elastic Search 7.x | 支持PB级以上 | 数万以上 | 不支持标准SQL | 不支持 | 支持与MaxCompute集成,Flink Connector只支持Source | 文档丰富,社区支持较好 |
从以上的表格能看出,Tidb和Hologres可以较好地解决新东方在大数据方面面临的问题,但是Tidb需要私有云部署并运维,而MaxCompute部署在公有云,两者在不同的云环境。Hologres是阿里云提供的云原生服务,并与MaxCompute都部署在公有云,且在Pangu文件层紧密集成,数据交换效率远高于其他外部系统,两者都兼容PostgreSQL,从离线数据仓库开发迁移到实时数据仓库开发难度降低。
基于以上的分析,选择Hologres作为实时数仓。
实时数仓是在离线数仓的基础上,基于Lambda架构构建,离线和实时同时进行建设。
架构的各组件说明:
1)数据源:
2)CDC数据总线(简称CDC)
3)离线数据处理
4)实时数据处理
实时数据处理基于阿里云托管的 Flink流式计算引擎。与离线数仓处理固定日期的数据(如T+1)不同,实时数仓处理的是流式数据,从任务启动开始,就一直运行,除非异常终止,否则不会结束。数仓的层次与离线数仓类似,根据实时处理的特点做了简化。如下表所示:
数仓层次 | 描述 | 数据载体 |
ODS层 | 与数据源表结构相似,数据未经过处理 | Kafka Topic/cdc Connector |
DWD/DWS层 | 数据仓库层,根据业务线/主题处理数据,可复用 | Kafka Topic |
DIM层 | 维度层 | holo 维表,Kafka Topic |
ADS层 | 应用层,面向应用创建,存储处理结果 | holo实时结果表,Kafka Topic |
5)Hologres 数据查询
Hologres同时作为实时数据和MaxCompute离线数据加速查询的分析引擎,存储所有的实时数仓所需的数据表,包括维度数据表(维表)、实时结果表、存量数据表以及查询View和外表等。数据表的定义和用途如下表所示:
数据表名称 | 描述 | 数仓层次 | 数据源 |
维度数据表 | 维度建模后的数据表,在实时计算时事实表通过JDBC查询 | DIM层 | 初始化数据来自离线数仓dim 层CDCFlink维表计算任务 |
实时结果表 | 实时数仓的计算结果表 | 实时数仓DWS/ADS层 | 实时数仓的DWS/ADS层计算任务 |
存量结果表 | 离线数仓的计算结果表 | 实时数仓DWS/ADS层 | 离线数仓的DWS/ADS层计算任务 |
查询view | 合并实时和存量结果,对外提供统一的展示View | 实时数仓ADS层 | 存量结果表实时结果表 |
外表 | 来自MaxCompute的数据表引用 | 各层次 | 离线数仓 |
备份表 | 备份实时计算一段时间内的数据,用于做数据校验和问题诊断 | DWD/DWS层 | 实时数仓 |
通过新的架构,支持了新东方集团内如下应用场景:
一个典型的实时任务处理流程如下图所示:
由于实时处理源数据和结果都是动态的,数据验证无法在任务中进行。可以在Hologres中,对实时数仓的各层落仓结果进行验证。由于实时处理和时间相关,每一层次的数据都需要带上一个处理时间戳(Process Time)。在Lambda架构中,将实时结果和离线结果进行比对,假设离线处理周期为T+1, 则实时处理取时间戳与昨天的数据进行比对,计算出准确率。如果是Kappa架构,需要进行逻辑验证,并与业务人员处理的结果数据进行比对。
Kafka Topic一般存储几天内的数据,不能提供全量数据,所以需要从离线数仓进行全量数据初始化,将维表、ADS层结果等导入Hologres。
1)Lookup
在Flink计算任务中,流表和Hologres的维度数据表Join,就是Lookup。Lookup需要解决两个问题:
对于问题1, 在创建Hologres的维度表时,需要根据Flink SQL的需要去设置表的各类索引,尤其是Distribution key和Clustering key,使之与Join的关联条件列一致,有关Hologres维表的索引会在后面小节提到。
对于问题2,维表和流表Join中,处理两者数据不同步的问题,通过设置窗口可以解决大部分问题,但是因为watermark触发窗口执行,需要兼顾维表数据延迟较多的情况,因而watermark duration设置较高,从而导致了数据处理任务的Latency很高,有时不符合快速响应的业务要求,这时可以采用联合Join,,将双流Join和Lookup结合起来。
维表数据包括两部分:
1. Hologres维表,查询全量数据.
2. 从维表对应的Kafka Topic创建的流表,查询最新的数据。Join时,先取维表对应的流表数据,如果不存在取Hologres维表的数据。
以下是一个例子,t_student(学员表)的流表和t_account(用户表) Join获取学员的user id
combined join
//stream table:stream_uc_account
val streamUcAccount: String =
s"""
CREATE TABLE `stream_t_account` (
`user_id` VARCHAR
,`mobile` VARCHAR
.......(omitted)
,WATERMARK FOR event_time AS event_time - INTERVAL '20' SECOND
) WITH (
'connector' = 'kafka'
.......(omitted)
)
""".stripMargin
//dim table:t_account
val odsUcAccount: String =
s"""
CREATE TABLE `t_account` WITH (
'connector' = 'jdbc',
.......(omitted)
) LIKE stream_t_account (excluding ALL)
""".stripMargin
//query sql: combined join
val querySql:String =
s"""
select
coalesce(stm_acc.user_id,acc.user_id) as user_id
from t_student stu
LEFT JOIN stm_acc
ON stu.stu_id = stm_acc.student_id
AND stu.modified_time
BETWEEN stm_acc.modified_time - INTERVAL '5' MINUTE
AND stm_acc.modified_time + INTERVAL '5' SECOND
LEFT JOIN uc_account FOR SYSTEM_TIME AS OF stu.process_time AS acc
ON stu.stu_id = acc.student_id
2)维表性能的优化
Flink SQL在Lookup时,流表每一条数据到来,会对Join的维表执行一次点查,Join的条件就是查询条件,例如对于流表stm_A和维表dim_B,Join条件为stm_A.id = dim.B.id
当 id=id1的stm_A数据到来时,会产生一条查询: select
Hologres索引包括: distribution key,clustering key,bitmap key,segment key(event timestamp) , 有关索引,可以参考 holo表的创建和索引
注意:维表推荐用Hologres行存表,但是在实际情况中,因为维表还用于adhoc一类的分析查询业务,所以本实践中大部分维表是列存表,以下实践结论是基于列存表和查询情况设定的,仅供参考,请根据业务情况合理设置。
实践结论1:维表的Join列设置成distribution key
由于当前使用列存作为维度表,维表的列数会影响查询性能,对于同一个维表,8个列和16个列的性能相差50%以上,建议join用到的列都设置成distribution key,不能多也不能少。如果使用行存表,没有这个限制。
实践结论2:尽可能减少维表的属性列
在应用中,维表可能有多个维度列会被用于Join,例如表T1,有两个维度列F1、F2分别用做和流表A,B的Join条件。根据F1和F2之间的关系,如果F1..F2→1..n,就在F1上创建distribution key, 反过来则在F2上创建,即在粒度较大的维度列上创建distribution key。
实践结论3: 一个维度表有多个维度列并且是Hierarchy时,在粒度较大的列上创建distribution key,并用在Join条件中
如果 F1..F2是多对多的关系,说明一个维表有两个交错的维度,而不是层次维度(hierarchy)上,需要进行拆分。
查询时,不管Lookup是否必须用到distribution key索引列,都要把distribution key索引放在Join条件里
示例: 维表t1有两个维度列:stu_code和roster_code,distribution key加在stu_code上
流表stm_t2需要 Lookup 维表t1,关联条件是两个表的roster_code相同
select
from FROM stm_t2 stm
JOIN t1 FOR SYSTEM_TIME AS OF stm.process_time AS dim
ON stm.stu_code = dim.stu_code and stm.roster_code = dim.roster_code
经过半年的实时数仓建设,并在集团内广泛使用。为业务的带来的价值如下:
参考资料:
1.知乎(阿里云Hologres)-《新东方基于Hologres实时离线一体化数仓建设实践》