☰
数据仓库流处理技术全解析:从实时数仓到Flink实践
2026/10/17 4:50:17 网站建设 项目流程

大数据领域数据仓库的流处理技术

做数仓的朋友这几年应该都有一个很直观的感受:实时数仓这个词从概念到落地,速度比我们想象中快得多。早些年聊流处理,大家的第一反应是“这东西能不能稳定跑在线上”,现在聊流处理,聊的已经是“怎么把离线数仓那套模型体系平滑地迁移到实时链路里”。数据仓库的流处理技术,说白了就是让数据在产生的那一刻就被处理、被清洗、被关联、被聚合,而不是等它落进离线表里再慢慢算。这篇文章我会从架构演进、核心概念、实操链路到踩坑记录,把流处理在数仓场景里的完整脉络梳理一遍,希望能给正在做实时化改造或者准备入坑流计算的读者一些参考。

我接触流处理的时间不算短,从最开始的Storm到后来的Flink,中间也经历过Spark Streaming的过渡期。坦白说,真正让流处理在数仓领域站稳脚跟的,不是某个引擎本身的性能,而是整个技术生态的成熟——消息队列的可靠性、状态存储的持久化、精确一次语义的完善、以及数据湖和OLAP引擎对实时写入的友好度,这些东西叠加在一起,才让“实时数仓”从演示项目变成了生产环境里的常客。这篇文章的价值在于,我会把那些文档里写得不透、实践中坑最多的地方摊开来讲,比如水位线的设置逻辑、状态后端的选择、Join的超时控制、反压的排查思路,这些细节才是决定实时任务能不能长期稳定跑下去的关键。

1. 内容整体设计与思路拆解

1.1 为什么数据仓库一定要引入流处理

传统的数据仓库,无论是星型模型还是雪花模型,核心思路都是“先落地、再计算”。业务数据库里的变更数据通过ETL工具定期同步到数仓的ODS层,然后在DWD层做清洗和维度退化,再在DWS层做汇总,最后供ADS层消费。这个链路在数据量可控、时效性要求不高的时候完全没问题,但一旦业务方开始提“我要看今天实时的GMV”“我要看最近五分钟的转化漏斗”,离线链路的短板就非常明显了——一次调度链路走下来,最快也要几十分钟,而且频繁跑批对计算资源的压力很大。

流处理解决的不是“性能”问题,而是“时效”问题。它的核心思路是让数据在流动的过程中就被处理,相当于把原来离线数仓里“分阶段计算”的模式,变成了一条实时流淌的数据管道。数据从业务端产生,经过采集、清洗、关联、聚合,到最终可查询的状态,整个过程从小时级压缩到了秒级。更重要的是,流处理不是要把离线数仓推翻重来,而是和离线链路共存,形成一套“离线保证全量、实时保证时效”的双轨体系。

1.2 流批一体:从两套系统到一套架构的演进

早期做实时数仓,很常见的一个做法是搭两套完全独立的链路:离线走Hive数仓,实时走Flink算完写Redis或者Kafka,然后应用层直接读。两个链路各算各的,结果经常对不上——实时算出来的GMV和离线算出来的GMV差了百分之几,业务来问为什么,只能解释说口径不一致。这个问题本质上不是计算能力的问题,而是两套系统在数据模型、计算口径、维度数据上各说各话。

流批一体的思路,是把两条链路的底层统一起来。Flink本身支持流处理和批处理两套API,但真正让流批一体落地的关键,是计算引擎之上的那些东西:统一的元数据管理、统一的SQL语法、统一的数据模型定义。简单说,同一段SQL,既能以流的方式跑,也能以批的方式跑,结果口径天然一致。这样一来,实时和离线的数仓分层可以共用同一套模型定义,只是数据到达和计算触发的机制不同。不过流批一体在实际落地时还有很多细节要处理,比如维表的实时关联、迟到数据的处理策略、流式结果和批式结果的最终一致性,这些都需要在架构设计阶段就考虑清楚。

1.3 我为什么推荐以Flink为核心构建实时数仓

流处理引擎的选择,说实话绕不开Flink。Storm太底层,很多东西要自己造轮子;Spark Streaming严格说是微批,延迟在秒级到分钟级,处理精确一次语义也比较吃力;Kafka Streams虽然轻量,但在复杂的状态管理和多流Join上不如Flink成熟。Flink的优势,总结下来有四个层面。

第一个层面是状态管理。Flink的Stateful Stream Processing能力,让“流上跑Join”“流上跑聚合”成为可能。数仓场景里大量的业务逻辑,比如订单和支付的关联、用户维度的拉宽、漏斗的逐级统计,本质上都需要保存中间状态,Flink把状态的存储、恢复、快照做到了生产可用。第二个层面是精确一次语义,这个在实时数仓里几乎是刚需,因为数仓的结果是要给业务决策用的,多算或少算一笔订单都是事故。第三个层面是流批一体SQL,Flink的Table API和SQL支持,让离线数仓的开发人员可以快速上手实时任务,不需要每个任务都手写Java代码。第四个层面是生态衔接,Flink和Kafka、Pulsar、Hive、Iceberg、Hudi、Doris等组件的整合都非常顺畅,可以很容易地嵌入已有的数仓体系。

2. 核心细节解析与实操要点

2.1 流处理的数据管道基础:消息队列选型与Topic设计

实时数仓的底座,一定是消息队列,目前最主流的选择是Kafka。Kafka的高吞吐、持久化、分区机制和消费组模型,天然适合做流数据的中转站。但消息队列选型只是第一步,真正的难点在于Topic的规划。

我见过很多实时项目跑着跑着就乱了,往往不是因为Flink任务写得有问题,而是Topic设计太随意。一个比较稳妥的分类方式是按照数据层级来划分Topic:ODS层存原始数据,命名类似ods_业务_表名;DWD层存清洗和关联后的明细数据,命名类似dwd_主题_业务过程;DWS层存汇总指标,命名类似dws_主题_粒度。这样做的好处是,每个链路的数据责任边界非常清晰,排查问题的时候可以直接根据Topic定位到具体环节。

另一个实操层面很重要的点,是Topic的分区数设置。分区的数量和Flink的并行度是联动的,理论上一个分区对应一个Flink的读并行度。分区数太小会限制吞吐,分区数太大又会导致文件碎片多、元数据开销大。一个经验值是,单分区的吞吐能达到几MB/s到几十MB/s,设置分区数时可以按照预估峰值流量除以单分区能力再乘以冗余系数来估算。比如预期峰值流量50MB/s,单分区按10MB/s算,那至少要有5个分区,再考虑重平衡和故障恢复的余量,设置为8到10个分区比较合适。

2.2 关键概念:水位线、窗口与状态后端的正确理解

流处理有几个核心概念,很多人一开始容易混淆,理解不到位会在生产环境踩大坑。

先说水位线。流处理面对的数据不是整齐排队到达的,网络抖动、上游任务卡顿、数据重放都可能导致数据乱序。比如订单系统生成了一个10:00:00的事件,但因为网络原因,这个事件10:00:30才到达Flink。如果没有水位线机制,按事件时间做的窗口计算就会漏掉这条数据。水位线的本质,是告诉Flink“我目前已经处理到了什么时间点”,早于这个时间点的数据必须被处理完,否则之后触发的窗口计算就不会包含它们。

水位线的设置策略,直接决定了“迟到数据”和“低延迟”之间的平衡。设得太宽松,比如允许迟到10秒,那大部分乱序都能处理,但窗口结果会延迟输出,实时性变差。设得太紧,比如只允许迟到2秒,实时性好了但漏数风险增加。实际操作中,我一般建议先用基准测试看一下数据乱序的实际分布,再据此设置水位线延迟,同时配合Flink的allowedLateness机制做兜底。

再说窗口。流处理的窗口类型有Tumbling Window(滚动窗口)、Sliding Window(滑动窗口)、Session Window(会话窗口)。实时数仓里最常用的是滚动窗口做分钟级、小时级的统计,比如每5分钟统计一次各渠道的订单量。滑动窗口一般用在需要平滑曲线的场景,比如最近15分钟的平均在线人数。窗口计算的一个关键细节是,窗口的状态数据会保留在内存或状态后端里,窗口越大、数据量越大,对状态的存储压力就越大。这也是为什么实时数仓的汇总层一般不追求特别大的窗口,而是用“累计值+增量值”的方式来存储数据,避免状态无限膨胀。

状态后端的选择同样决定了一个实时任务的稳定上限。状态后端有三种:MemoryStateBackend(现在叫JobManager在内存里),适合小状态量的调试场景,生产环境不推荐。FsStateBackend(现在叫HashMapStateBackend)把状态存在TaskManager内存里,周期性快照到外部存储,适合中等状态量。RocksDBStateBackend是生产环境的默认选择,状态存储在本地磁盘的RocksDB里,可以支撑非常大的状态量,但性能比纯内存方案要低一些。数仓任务里但凡涉及Join和累计聚合,状态量通常都不会太小,所以只要不是纯过滤清洗类任务,我建议直接上RocksDB。

2.3 实时数仓与离线数仓的分层映射

实时数仓不是把离线数仓的那套结构原样照搬过来,因为流处理的计算模型和批处理不同,存储介质也不同。离线数仓的ODS层是Hive表,DWD层是宽表,DWS层是汇总表,每个表的存储和计算是解耦的。实时数仓里,ODS层往往是Kafka的原始Topic,DWD层可以是Kafka的清洗后Topic,也可以落到下游的OLAP引擎里,DWS层通常是实时的预聚合结果,最终以指标服务的形式暴露给应用。

这个映射关系,直接决定了实时任务的链路编排。我自己的实践是:ODS到DWD的环节用Flink做清洗、补全、格式转换和简单的数据过滤,输出回Kafka;DWD到DWS的环节用Flink做维表关联、聚合计算,结果写入OLAP引擎或者指标服务。至于DWS以上的部分,则基本由OLAP引擎通过查询来承接。

有一个容易忽略的点:实时数仓的DWD层是不是一定要通过Kafka中转?其实不一定。如果DWD的结果需要被多个下游任务消费,那Kafka中转是合算的;如果DWD的结果只有一个消费方,直接Flink写入下游存储可以减少一次序列化和网络传输。这里的取舍要根据实际业务链路来判断,不要照搬别人的架构。

3. 实操过程与核心环节实现

3.1 一条实时订单统计链路的完整搭建

为了把前面这些概念串起来,我拿一个非常典型的场景来演示:实时的订单金额和订单量的分钟级统计,同时关联用户维度信息。

先看全链路的技术选型和数据流向。业务数据库里有一张订单表,表里记录订单ID、用户ID、商品ID、订单金额、订单状态、下单时间等字段。用户信息有一张独立的维度表。整个实时链路,第一步是通过CDC工具把订单表的变更事件实时采集到Kafka的ODS层Topic。第二步是Flink从ODS Topic消费数据,做必要的清洗,然后关联用户维度表,把用户ID换成用户姓名和用户等级,得到明细数据写入Kafka的DWD层Topic。第三步是Flink从DWD层Topic消费数据,按订单状态过滤(只看已支付订单),按分钟聚合,把统计结果写入一个支持实时写入的OLAP引擎,同时以JSON格式推送到一个Redis,供前端实时展示。

这条链路覆盖了流处理在数仓场景里的几乎所有关键环节:数据采集(CDC)、消息中转(Kafka)、清洗关联(Flink)、维度补充(维表Join)、聚合计算(Window)、结果输出(OLAP + Redis)。

3.2 关键代码示例:Flink SQL实时消费与清洗

先说纯Flink SQL的写法,这也是现在大多数实时数仓团队的首选方式,因为上手成本低、可读性强,而且不容易出逻辑错误。下面这段代码,做的是从Kafka的ODS层Topic消费订单数据、简单过滤、解析时间字段、然后写入Kafka的DWD层Topic。

-- 第一步:定义Kafka ODS源表 CREATE TABLE kafka_ods_orders ( order_id BIGINT, user_id BIGINT, product_id BIGINT, amount DECIMAL(10, 2), status INT, order_time TIMESTAMP(3), WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND ) WITH ( 'connector' = 'kafka', 'topic' = 'ods_orders', 'properties.bootstrap.servers' = 'localhost:9092', 'properties.group.id' = 'dwd_orders_group', 'format' = 'debezium-json', 'scan.startup.mode' = 'latest-offset' ); -- 第二步:定义Kafka DWD目标表 CREATE TABLE kafka_dwd_orders ( order_id BIGINT, user_id BIGINT, product_id BIGINT, amount DECIMAL(10, 2), status INT, order_time TIMESTAMP(3), dwd_insert_time TIMESTAMP(3) ) WITH ( 'connector' = 'kafka', 'topic' = 'dwd_orders', 'properties.bootstrap.servers' = 'localhost:9092', 'format' = 'json', 'sink.partitioner' = 'default' ); -- 第三步:清洗与转换逻辑 INSERT INTO kafka_dwd_orders SELECT order_id, user_id, product_id, amount, status, order_time, CURRENT_TIMESTAMP AS dwd_insert_time FROM kafka_ods_orders WHERE status IS NOT NULL; -- 过滤掉状态字段为空脏数据

这段SQL里有几个细节值得说明。WATERMARK那行是关键,它定义了水位线,允许最多5秒的乱序延迟,这个值在真实环境下要根据数据乱序分布来调。如果你们的数据经过多个中间链路才到达Kafka,乱序可能不止5秒,那这个值就要相应调大。scan.startup.mode设置的是从最新的offset消费,如果是离线追数场景,就要改成earliest-offset从头消费。这里用的是debezium-json格式,因为CDC采集工具通常以这个格式输出。

3.3 维表关联的两个方案与选型分析

上面这段SQL只做了清洗,还没有关联用户维度信息。Flink里做维表关联有两种主流做法,一种是同步查外部存储,一种是加载到Flink的内存里做广播Join。

第一种,同步查外部存储。Flink的JDBC连接器可以让你在流处理的过程中对MySQL或者Redis发起实时查询。这种方式的好处是维表数据永远是新鲜的,用户改了昵称下一次关联就能拿到。坏处是实时任务会对维表存储产生比较大的查询压力,一旦维表服务出现性能瓶颈,整个实时链路都会受影响。

第二种,维表数据广播到TaskManager内存。用Flink的Broadcast State功能,把维表数据周期性地全量加载到每个TaskManager的内存里,流数据和维表在本地做HashJoin。这种方式的查询性能非常高,因为不走网络。坏处是内存开销大,维表数据量不能太大,而且维表更新的频率受广播间隔限制,通常做不到实时反映维表变化。

我自己的选型经验是:维表数据量在百万级别以下、更新不频繁的场景,用Broadcast方案。维表数据量大、更新频繁、且被多个实时任务共享的场景,用同步查询外部存储,同时给维表服务加缓存层,降低实时任务的查询压力。还有一个折中方案:用Flink的CDC功能直接监听维表的变化事件,把维表的变化数据流和主流数据流做双流Join。这个方案实时性和状态量上平衡得比较好,但开发复杂度高一些。

3.4 分钟级汇总的窗口计算实现

接着实现DWS层的分钟级统计。下面这段SQL,消费DWD Topic的数据,按事件时间做1分钟滚动窗口的聚合。

CREATE TABLE kafka_dwd_orders ( order_id BIGINT, user_id BIGINT, product_id BIGINT, amount DECIMAL(10, 2), status INT, order_time TIMESTAMP(3), WATERMARK FOR order_time AS order_time - INTERVAL '5' SECOND ) WITH ( 'connector' = 'kafka', 'topic' = 'dwd_orders', 'properties.bootstrap.servers' = 'localhost:9092', 'properties.group.id' = 'dws_min_orders_group', 'format' = 'json', 'scan.startup.mode' = 'latest-offset' ); CREATE TABLE kafka_dws_order_stats ( window_start TIMESTAMP(3), window_end TIMESTAMP(3), order_count BIGINT, total_amount DECIMAL(16, 2) ) WITH ( 'connector' = 'kafka', 'topic' = 'dws_order_min_stats', 'properties.bootstrap.servers' = 'localhost:9092', 'format' = 'json' ); INSERT INTO kafka_dws_order_stats SELECT TUMBLE_START(order_time, INTERVAL '1' MINUTE) AS window_start, TUMBLE_END(order_time, INTERVAL '1' MINUTE) AS window_end, COUNT(*) AS order_count, SUM(amount) AS total_amount FROM kafka_dwd_orders WHERE status = 2 -- 假设2为已支付 GROUP BY TUMBLE(order_time, INTERVAL '1' MINUTE);

窗口计算的最终结果,是以窗口的启止时间作为分组键输出的。这么做的好处是下游在查询“某分钟有多少订单”的时候,可以用等值条件去查询,而不需要做额外的时间范围过滤。

3.5 维表关联的异步I/O优化

如果你的维表关联走的是同步查询外部存储,那一定要考虑用Flink的Async I/O来优化。同步查询维表,每来一条数据就要请求一次外部存储,这个过程的网络开销和等待时间占整个处理时延的比例非常高。Flink的Async I/O可以把“等待查询结果”的时间重叠起来,同时发出多个查询请求,整体吞吐能提升数倍甚至十多倍。

实现Async I/O,需要继承Flink的AsyncFunction接口,重写asyncInvoke方法,在方法内部发起异步的维表查询请求,查询完成后把结果传给ResultFuture。有一点容易忽略:Async I/O的异步查询,必须配合“乱序完成”的允许,也就是允许先发出的请求后返回结果,因为外部存储的响应时间本身是不确定的。如果你对结果顺序有严格要求,需要在回调里做排序缓冲,但这会进一步增加状态存储开销。

3.6 数据落盘与下游OLAP引擎的选择

DWS层的统计结果,最终要落到一个能支持高并发查询的引擎里,供数据产品和业务方使用。目前实时数仓的查询引擎选择,主要落在ClickHouse和Doris两类上。ClickHouse的优势在于极致的单表查询性能,列式存储、向量化执行,对聚合类查询的响应速度非常快。Doris的优势在于更好的分布式能力、标准的SQL支持、以及良好的数据更新能力,适合偏数仓的建模方式。

这两者怎么选,没有标准答案,更多看团队的技术栈和具体场景。如果团队之前已经熟悉ClickHouse,而且业务方主要就是做指标看板类的查询,ClickHouse完全够用。如果希望数仓的分层建模能力更完整,比如DWD明细层和DWS汇总层都要统一管理,Doris的设计理念更贴合数仓的分层模型。还有一个实操层面的细节:ClickHouse对数据更新的支持偏弱,如果你的实时结果经常要修正或删除之前的数据,ClickHouse的MergeTree系列会有点吃力。Doris在Unique Key模型上对更新支持得更好。所以从思路上说,数据变更频繁的场景优先考虑Doris,纯追加型数据的海量分析优先考虑ClickHouse。

4. 常见问题与排查技巧实录

4.1 水位线不推进,窗口结果永远不出来

我之前负责的一个实时任务,窗口统计的结果迟迟不出现。排查的第一步,我先看Flink UI上的Watermark曲线,发现水位线卡在任务启动初期的时间点,完全没有向前推进。这种情况,十有八九是源头数据里没有Watermark的触发条件。

具体原因通常是两类。一类是上游的Kafka Topic长时间没有新数据,Flink默认的水位线机制在没有新事件到达时不会主动推进,窗口就一直等下去。解决方式是在源表上设置空闲分区超时,通过table.exec.source.idle-timeout参数,让空闲的Kafka分区主动生成推进水位线的Watermark,避免窗口卡死。另一类是事件时间字段的解析出了问题,比如时间戳格式不对,或者时区偏移导致数据的时间看起来永远比当前时间晚,自然推不动水位线。这类问题,可以通过在SQL里显式转换时间字段的类型、设置正确的时区参数来解决。

4.2 数据倾斜引发的处理延迟

实时数仓里一个非常典型的问题:某个大卖家贡献了全平台30%以上的订单,如果按店铺ID分组做统计,那这个店铺对应的Flink分片负载远远高于其他分片,整个窗口的计算结果要等最慢的分片算完才能输出。这就是数据倾斜。

Flink状态下处理数据倾斜的思路,可以分两步走。第一步,从上游RODUCE源头减少热点,但如果业务数据本身就是头部的头部,源头不均衡避免不了。第二步,在聚合计算时加一个“预聚合+汇总”的两阶段设计,第一层把数据打散到随机Key上做部分聚合,第二层再按真实业务Key汇总。Flink SQL里可以通过加一个随机数前缀来改变分组Key,比如GROUP BY CONCAT(shop_id, '-', MOD(ABS(HASH_CODE(shop_id)), 10))。这个方案在窗口聚合场景下效果很好,但要注意,层间流转会增加网络IO和序列化开销,需要根据倾斜程度权衡是否值得做。

4.3 状态过期膨胀导致的OOM风险

前面提到RocksDB状态后端适合大规模状态,但如果状态数据无限增长而不设过期时间,再大的磁盘也会被撑满,随之而来的就是内存紧张和任务不稳定。实时数仓里,最常见的状态膨胀原因是两种:不设置空闲状态的过期时间,以及窗口过大导致窗口状态未及时清理。

Flink提供了State TTL机制,可以在状态上设置存活时间。超过TTL的状态数据会被异步清理。对于Join场景和去重场景,这个过期时间要根据业务逻辑合理设置。比如订单和支付的关联Join,订单从生成到支付,绝大多数在30分钟内完成,那Join左边订单流的State TTL就可以设30分钟。设太大会浪费存储,设太小会漏掉延迟支付的关联。

4.4 反压问题的排查与缓解

Flink UI上出现BackPressure告警,说明某个算子处理不过来,数据积压在缓冲区里。反压的影响是链路性的——下游卡住,上游的积压越来越多,最终可能引起整个任务的Checkpoint超时、延迟飙升。

排查反压的思路,第一步是看Flink UI的Back Pressure状态栏,定位到出现反压的顶点算子。第二步看这个算子的核心指标:如果是Source端反压,通常是下游处理慢导致的消息拉取变慢,需要优化下游算子;如果是Join或聚合算子反压,一般是状态操作慢或者数据量大;如果是Sink端反压,大概率是下游存储写入性能跟不上。

我遇到最多的其实是Sink端反压,比如实时结果写入ClickHouse时,如果某个分区的数据量大,ClickHouse的写入吞吐跟不上,Sink的阻塞就会传导到整个链路。缓解方案包括优化写入方式的批量大小、增加并发度、或者在下游存储上做分区分桶的优化,让写入压力更均衡。反压本身不是“错误”,它是流处理系统的自我保护机制,真正要解决的永远是“到底哪里慢了”这个根因问题。

4.5 实时结果与离线结果对不上怎么办

这是实时数仓上线后业务方最常问的问题:昨天实时看的GMV和今天跑完离线数仓出的GMV,差了几万块钱,为什么。这个问题我每次都要从两个方向去排查。

一是数据口径一致性的问题。两条链路在口径上有差异的用户,才会导致对不上。最常见的是实时链路为了延迟,提前用了状态不够完整的数据,比如维表数据没有及时更新,或者水位线设置太紧导致漏了迟到的数据。另一种情况是数据转换逻辑不一致,离线链路做了一步方言转换或过滤,实时链路漏掉了。

二是最终一致性的问题。实时和离线本来就是两次独立的计算,结果存在一个时间窗口内的差异是正常的。减少差异的思路是推进流批一体,让两条链路共用同一套SQL逻辑和元数据定义;同时在结果输出层面,该对齐的时间单位要对齐,该修正的迟到数据要定期修正,比如用离线批任务每天重算一遍T-1的实时指标,以离线结果为准做订正。

4.6 常见问题速查表

现象可能原因排查与解决方向
窗口结果迟迟不输出水位线未推进、事件时间字段解析错误、Kafka分区空闲无数据检查Watermark曲线,设置idle-timeout,检查时间字段格式与时区
任务延迟持续上升数据倾斜、Sink写入慢、算子并行度不足定位反压顶层算子,检查分组Key的热点分布,评估并行度设置
Join结果少数据State TTL过短、维表数据未及时更新延长TTL、检查广播间隔或维表查询的缓存策略
Checkpoint频繁失败状态体积过大、反压导致快照超时优化状态结构、增加超时时间、排查上游反压
结果重复或丢失关闭了精确一次语义、重启后offset重置错误检查语义配置、确认Checkpoint和Kafka offset对齐策略
数据倾斜导致的OOM单个Key数据量极大,聚合算子内存压力大两阶段聚合、加随机前缀打散热点Key

5. 流处理技术在数据仓库场景的扩展方向

5.1 数据湖与流处理的融合

实时数仓做到后期,会发现一个尴尬:实时链路算完的结果,如果要参与更复杂的跨主题分析,还是得回到离线数仓或数据湖里查。数据湖和流处理的融合,解决的就是这个“实时结果不能在湖上被灵活分析”的问题。以Flink + Iceberg/Hudi为代表的方案,让流处理的结果可以直接以准实时的方式写入数据湖,形成湖上的实时数据文件。这样一来,数据湖里的数据既有批的完整性,又有流的时效性。

这个方向在实操里也有一些需要适应的地方。流式写入小文件的问题非常明显,需要开启湖格式自带的compaction机制,定期合并小文件。写入的事务性也要注意,Flink和Iceberg/Hudi的集成提供了行级或小文件级的事务保障,但要理解的是它仍然和传统关系型数据库的事务隔离级别有差别,设计下游查询时要考虑到可能读到中间状态的可能性。

5.2 指标平台与实时计算的服务化封装

实时数仓的值,最终不是体现在Flink任务本身,而是体现在业务方能不能高效地拿到指标。很多团队会把实时计算的结果封装成一个指标服务平台:实时指标的计算口径、调度状态、数据质量状况都在平台层统一管理,业务方通过API直接查询,不需要关心底层是Flink还是ClickHouse。

这个方向我自己认为是实时的数仓发展的高阶形态。实时任务是高度分散的,每个任务独立管理会非常痛苦。封装成平台之后,任务的启停、监控、告警可以统一做;口径的变更可以通过平台管理;数据质量规则也可以统一挂载在指标层面。如果你所在的团队已经做了好几个实时数仓项目,我建议提前考虑这个方向,早期规划不一定一次做到,但数据模型和管理初始化方式要留出扩展空间。

开头我不是说过一句话吗——流批一体也好、实时数仓也好,本质上都是要解决“业务要看实时数据,但数仓算得太慢”的矛盾。我在实操中最深的体会是,实时数仓和离线数仓的关系,不是替代,而是配合。那些基础厚的团队,往往也是把实时和离线两套链路都吃透,然后才做融合的团队。流处理技术本身并不难学,难的是在真实业务场景里,把每一层链路、每一个参数都调校到稳定运行的状态。踩过几次坑之后,你大概就会明白,判断一个实时数仓好不好,不在于它用了多新潮的技术组件,而在于它能稳定支撑多少个“今天就要看到”的数据需求。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询