实时数仓系统构建

背景介绍

当前的数据报表服务采用定时离计算的方式构建数仓,但随着业务对实时性的要求变高,需要实现一套实时入库方案。

 

问题分析

对外提供服务的大宽表分基础表,事实表和统计表三类,事实表和纬度表都包含纬度信息。在实时流处理过程中,每来一条事实数据,用纬度id查询纬度数据,将纬度数据和事实数据写入具体的业务topic中。这里最核心的步骤是用纬度id查询纬度数据。因为实时数据的吞吐量比较大,如果每来条一条数据去mysql中查询一次,mysql扛不抗的住还好说,每次查询mysql的耗时是不能忍受的。一般的做法都是利用cache缓存mysql数据,实时数据查询纬度数据的时候先从cache中查,如果查不到再从db中查,将查到的数据放入cache,方便下次使用。 上述方案虽然解决了事实和纬度数据join性能的问题,但同样也带来了数据不一致的问题。也就是说存在mysql数据已更改,但cache中还是旧数据的情况,那么新生成的大宽表纬度数据是过期数据。其实不止新生成的大宽表纬度数据是过期数据,之前大宽表中涉及到的纬度数据都是过期数据。

业务库数据存在mysql中,olap引擎用的是clickhouse。中间数据存在kafka或者mysql。虽然维表数据可以用内存数据库(redis,memcache等)存储,由于需要引入其他组件,相对比较麻烦,而mysql的性能也不差,所以维表这里全都是mysql存储的。中间数据涉及到部分字段更新的存储在mysql,全量数据更新的用kafka。

解决方案探索

既然大宽表中存在过期的纬度信息,不产生错误数据或者把错误数据纠正一下就可以了。

方案一

采用lamda架构,开发两套程序,一套实时,一套离线。实时数据join维表时利用快照的方式读取,保证及时性。离线每隔一段时间执行一次,保证正确性。

方案二

方案一的缺点是需要开发两套程序,维护起来不方便。有没有可能用一套程序来实现?kappa架构就是来干这个的。Kappa架构的核心思想:

1、用Kafka或者类似MQ队列系统收集各种各样的数据,需要几天的数据量就保存几天。

2、当需要全量重新计算时,重新起一个流计算实例,从头开始进行处理,并输出到一个新的结果存储中。

3、当新的实例做完后,停止老的流计算实例,并把老的一些结果删除。

在当前的应用场景,遇到维表数据变更,从头开始消费数据效率有点低,而且大部分数据是没有变更的,无需重新消费。我们可以修改下kappa架构,只消费维表的变更数据,将维表的数据更新到大宽表即可。我们对外提供服务的olap引擎,一般只支持全字段数据覆盖,不支持部分字段update。即使支持部分字段update,当一个维表字段变更涉及到大宽表很多数据时,update的性能会很差,而且还会影响olap引擎对外提供服务的性能。所以一般情况下是根据维表变更数据查出大宽表变更数据,将其写到消息队列中,再消费队列写入到大宽表,覆盖旧数据。假设大宽表join来3张维表的数据,具体流程如下:

1、在一个任务中消费3张维表的topic,将变更数据汇总到一起

2、根据变更的维表数据,从db中查出需要变更的事实数据

3、将需要变更的事实数据,写到事实数据对应的topic中,复用之前的消费事实数据topic join维表数据的流。

方案三

我们当前遇到的是维表数据变更导致的大宽表数据变更问题。如果大宽表中只存放纬度id,不存放纬度属性数据,查询的时候将多张表的数据join一下,返回大宽表数据。

这种方案可以说完美解决了大宽表中纬度更新的问题,而且大宽表不再包含属性数据,减少了存储空间。但这种方案对olap引擎的性能要求较高。因为之前是关联好数据,直接查询,现在需要查多个数据,再关联,查询性能肯定没之前好。 两表join的时候,olap引擎一般是将小表放到内存中,做mapjoin。如果纬度数据很小,这种方案优势很大。如果纬度数据较大,需要很好的划分hash,或者采用其他方案。

目前市面上支持join,且性能不错的olap引擎,clickhouse和doris都可以。

解决方案

我们的数据需求可以分为三类:

1、mysql的数据原封不动的copy到clickhouse

我们公司内部有一个mysql平台,可以增量、全量将数据写到指定的topic,所以这里我们只有实时方案,消费topic数据写到clickhouse。如果需要回溯数据,从新全量导出数据到topic即可。

2、mysql数据join一些纬度,写到clickhouse

我们这边的数据量不大,本来是准备用方案三的,但clickhouse join的性能很差,所以放弃方案三。我们这边的业务场景大都是维表数据很少,事实数据很多,极端情况下会出现一条纬度数据更改,所有大宽表数据都需更改的问题。所以这里我们选了方案一,实时部分join维表快照写数据到clickhouse,保证实时性;离线部分每半小时执行一次,保证正确性。

3、mysql数据做汇总后join纬度,写到clickhouse

针对于轻度汇总数据,比如说每小时或者每天汇总一次,可以利用flink watermark相关的语法实现,join纬度数据,flink sql支持的也很完善。 针对于重度汇总数据,比如说一个月汇总一次,这类需求通过实时来算不合适(状态数据过多影响checkpoint;任务重启恢复比较麻烦),一般都是写sql离线计算。

一些细节

1、元数据变更问题

任务启动时从数据地图读取表字段信息,入库mysql时,按照数据地图中的字段信息入库到clickhouse。任务执行过程中mysql加了一个字段,此时由于内存中的数据地图信息是过期数据,所以还是按照之前的字段入库,也就是新字段没有入到clickhouse中。 比较笨的方案就是,在数据地图上面修改字段信息后,手动重启任务,重新加载元数据,可以解决问题。 还可以每隔一段时间读下数据地图,更新内存中的元数据。 如果嫌每次读数据地图代价太大,还可以将数据地图的变更信息写到一个地方,比如说db,定时轮询db检测是否有更新,有更新的话就从数据地图重新加载数据。

2、数据版本迭代问题

假设中间表存储在kafka,某个时间中间表tableA将字段A修改为a,那么消息队列中A和a都会存在。但入库的数据流只会采用一种元数据(要么是A,要么是a),所以入库的数据流不论采用哪种元数据都会报错。 这种可以通过版本号来解决。 比如说tableA有个版本字段,当前的版本是1.0,将字段A修改为a后,版本改为1.1,数据入库时指定入库的版本,如果配置为1.1,那么包含字段A的数据就会被过滤掉,如果配置为1.0,就会忽略包含字段a的数据。

3、实时和离线同时更新mysql导致的死锁问题

如果二者是按照id批量更新mysql数据,问题不大,死锁发生的概率比较小,可以忍受。 但如果二者是通过唯一索引更新数据,由于唯一索引重复后会加next-key锁(包含gap锁),此时发生死锁的概率非常大。 如果可以从业务上来解决,把按唯一索引更新改为按主键更新是最完美的方案。

如果业务上面解决不了,可以采用下面的办法减少死锁概率:

1、实时和离线都采用单线程写入db

2、减少单个事务提交的数据量

posted @   民宿  阅读(317)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示