小白从 0-1 搭建实时数仓实战

1,211 阅读7分钟

背景

随着公司业务复杂度越来越高,以往的数据收集都直接嵌入业务代码中,导致耦合度很高,在迭代过程中很容易因为业务代码改动导致数据收集不准确,而数据准确性对于数据分析、运营决策、算法推荐都是至关重要的。从公司长远发展战略角度出发,搭建一套无代码侵入、稳定性高、可靠性的数据收集平台对于数据挖掘、算法推荐都越来越迫切,使用传统的数据库来做数据分析、数据建模即费时费力,也不能很好到达预期效果,经过团队沟通后决定搭建数据仓库。

数据仓库,一个面向主题的、集成的、相对稳定的、反应历史变化的数据集合,主要用于数据分析、运营决策。所谓面向主题,它指的是数据仓库内的信息按照某个主题进行聚合,比如地区、成本、商品、收入、利润等等;所谓集成的,它指的是可以把不同数据库中的数据都汇聚在一起;所谓相对稳定的,它指的是数据仓库的数据不会像操作型数据库那样经常变化;所谓反映历史变化,它指的是数据仓库内的信息不只是反映企业当前情况,还可以记录分析从过去某一个时间点到现在的变化。

数据分层

1.png

搭建过程

流程图

2.png

实现方案

一期

  1. 经过调研想要实现对业务代码无侵入进行数据收集,当下主流的做法是使用 Flink-Cdc 直接监听 MySQL binlog 日志进行数据收集
  2. 暴露用户行为流上报 API 进行行为流数据收集
  3. 用户属性流、行为流数据统一以 Kafka 消息的方式进行上报
  4. 使用 Flink-Kafka 消费消息进行数据聚合计算,并将数据存入 MongoDB

遇到的问题:

  1. Flink-Cdc DebeziumSourceFunction 默认是 StartupOptions.initial() 第一次启动的时候,会把历史数据读过来全量做快照,而线上数据量非常大(亿级),Flink-Cdc Job 启动后直接导致数据库连接数飙升,业务服务无法正常连接数据库服务不断重启,整个系统被瘫痪
  2. 查阅相关资料后发现可以设置为 StartupOptions.latest() 只从binlog最新的位置开始读,而改成这种启动类型后,Flink-Cdc Job 上传任务之后总是报错,任务无法正常启动,经过源码调试后发现是因为 Flink 1.14.2 存在这个Bug,然后把版本改为 1.12.7 任务正常启动,DebeziumSourceFunction 配置设置代码如下:
public static DebeziumSourceFunction<String> buildSource(MySqlSourceConfig sqlSouresConfig, DebeziumDeserializationSchema<String> deserializationSchema) {

*// 2. 通过FlinkCDC构建SourceFunction并读取数据*

**DebeziumSourceFunction<String> build = MySQLSource.<String>*builder*()

.hostname(sqlSouresConfig.getHost())

.port(sqlSouresConfig.getPort())

.username(sqlSouresConfig.getUsername())

.password(sqlSouresConfig.getPassword())

*/***

** 启动参数 提供了如下几个静态方法*

** StartupOptions.initial() 第一次启动的时候,会把历史数据读过来(全量)做快照,后续读取binlog加载新的数据,如果不做 chackpoint 会存在重启又全量一遍。*

** StartupOptions.earliest() 只从binlog开始的位置读(源头),这里注意,如果binlog开启的时间比你建库时间晚,可能会读不到建库语句会报错,earliest要求能读到建表语句*

** StartupOptions.latest() 只从binlog最新的位置开始读*

** StartupOptions.specificOffset() 自指定从binlog的什么位置开始读*

** StartupOptions.timestamp() 自指定binlog的开始时间戳*

**/*

**.startupOptions(StartupOptions.*latest*())

*// 读取哪个库,可以读取多个库,默认监控库下所有表*

**.databaseList(sqlSouresConfig.getDatabases())

*// 监控库下的某些表 test_jdbc1.table,test_jdbc1.table1*

**.tableList(sqlSouresConfig.getTables())

*// 反序列化 用的是 Debezium 的 StringDebeziumDeserializationSchema() 格式不方便,所以要自定义*

*// .deserializer(new CustomerStringDebeziumDeserializationSchema())*

**.deserializer(deserializationSchema)

.build();

return build;

}

  1. Flink-Cdc 正常跑起来后发现 Kafka 消息总是堆积,而且消息越堆越多,经过分析发现 MongoDB Cpu 使用率始终是 90% 以上,Kafka 、Flink 相关指标都很健康,所以推断消息堆积是因为 MongoDB 达到性能瓶颈导致消费速度慢

二期

  1. 由于 MongoDB 达到性能瓶颈导致消息大量堆积,经过分析所有数据都是直接与 MongoDB 进行交互,存在大量查询、插入、修改操作,从而导致 CPU 占用始终很高。二期在 MongoDB 之前加了一层 Redis 缓存,所有数据计算直接在内存中完成,上线后果然消息不再堆积了
  2. 消息堆积问题解决后,并没有想象的那么顺利,营运同学反馈数据准确性不对,经过排查由于数据量非常大所以并发非常高,数据计算存在并发问题,因此在数据计算时使用 Redis 官方提供的分布式锁解决方案 Redisson 解决了并发问题

三期

  1. 长远来看使用 MongoDB 作为数据仓库的数据存储并不是很合理,MongoDB 不管是存储数据量级、高并发交互性能都不太适合大数据场景
  2. 经过调研 Hbase、Cassandra 是比较主流的大数据存储数据库,Hbase 需要比较完整 Hadoop 体系相对来说比较重,而 Cassandra 是非常轻量级的非关系型数据库,不管是数据存储还是读写性能都非常适合大数据存储、还能在不影响业务的情况下进行水平扩展、容灾备份,以下是阿里云对 Cassandra 的性能测试报告:

3.png

  1. 将 MongoDB 替换为 Cassandra 时去掉了中间的 Redis 缓存,上线观察发现在同样数据的情况下且去掉了 Redis 缓存 Cassandra CPU 占用率不到 12%

4.png

四期

  1. 之前因为并发问题导致数据不准确,非常影响营运同学数据分析以算法的有效推荐,想要数据准确必须在代码修复后跑一个完整周期才能重新使用数据,这样就导致数据恢复周期很长
  2. 数据仓库数据恢复能力变的格外重要,要求做到发现问题能立刻进行数据恢复,想要恢复数据就得保存所有上报的数据
  3. 当前每日有 2000w+ 数据,随着业务的发展后续消息量会以较快的速度增长,经过预估得保证能支撑每日 10000w+ 数据的恢复
  4. 保存这么大数据量比较容易,而查询这么大数据量确实一个难题,经过讨论分析仍然决定使用 Cassandra 作为数据存储,表结构如下:
CREATE TABLE "octopus_topic_event" (

"ymdhm" bigint,

"event_name" varchar,

"uuid" long,

"heard" varchar,

"message_model" text,

"create_time" long,

PRIMARY KEY ((ymdhm,event_name),uuid)

);

表设计依据:Cassandra 官方建议每个 Partition Key 下不超过 10w 行数据

Partition Key:ymdhm + event_name

uuid:保证每条数据的唯一性

吞吐量上限:ymdhm(24 * 60 = 1440) * event_name(10) * 10w= 1440 * 10 * 10w = 14400 w/日

应用场景

  1. 动态精准推荐
  2. 缘分、附近列表推荐
  3. 音视频匹配推荐
  4. 根据用户喜好进行功能推荐
  5. 数据报表、数据导出为运营决策提供有力数据支撑

积累经验

  1. 接触全新技术领域不能被眼前的困难吓倒,要想着怎样去克服困难只有这样才能慢慢解决各种问题,作为技术人员解决各种问题并且总结经验才能更快成长
  2. Cassandra 数据读写非常快,架构无中心化,维护成本低且支持动态水平扩展,非常适合大数据存储数据库
  3. Flink-CDC 非常适合做无侵入 ODS 数据采集,性能稳定