资讯详情

实时数仓从零搭建:Kafka+Flink+Doris核心链路与避坑实践

📅 2026/10/20 5:45:49 | 华诺云谱 👁 阅读
实时数仓从零搭建:Kafka+Flink+Doris核心链路与避坑实践
前一阵子某电商项目的数据组找到我说大促期间的经营看板总是差一天业务想要看到实时的下单转化链路越细越好。当时我第一反应是这个需求已经不是离线调度能覆盖的事了得上一套实时数仓。实时数仓这个词这两年听得不少但“听过”和“搭过”完全是两码事真正动手之后才发现选型、分层、链路打通、稳定性控制每一步都有取舍也都有坑。这篇文章想把从零搭实时数仓第一阶段的完整思路讲清楚适合正在评估要不要上实时数仓、或者准备动手做第一版链路的同学参考。1. 为什么业务逼着你搭实时数仓从T1到秒级1.1 离线链路的困局大多数有一定规模的数据团队头两三年都会跑在一套经典离线数仓上业务库每天凌晨导出全量或增量数据日志文件按天分区落盘然后调度系统在凌晨触发 Hive 任务逐层执行清洗、关联、聚合最后产出当天报表。这套链路最大的优势是稳定、可控、成本低凌晨跑几个小时就能把前一天的指标算完。但稳定不等于满足业务。我接手过一个比较典型的场景负责运营的同事每天早上十点要看前一天的“下单-支付-退款”漏斗每到下午三点又要盯着当天实时成交额是不是达标。离线数仓只能回答前一个问题后一个问题基本靠手工查库、拼数等到离线任务跑完已经是第二天。更麻烦的是离线分层会做大量重复计算——“昨日成交额”每天全量重算数据量翻倍后调度时间从两小时拉长到五个小时资源不够任务互相抢偶尔还有某层跑挂、下游全部飘红的连锁故障。这里面的本质矛盾不是数据算不出来而是时间尺度不匹配。业务要的是“数据产生后立刻能看”离线数仓给的是“次日凌晨统一出数”中间的窗口差了一整天。后来我们梳理需求发现至少三分之一的指标查询其实都不需要精确到历史全量它们关心的是“当下这一刻发生了什么”。这部分需求如果继续压在离线链路上只会让下游越等越久指标口径越扯越乱。1.2 实时数仓到底解决什么实时数仓最简单的定义是数据从业务系统产生经过采集、传输、计算最终到达查询端口的整个过程延迟控制在秒级到分钟级。它仍然是一个“仓库”——有分层、有主题、有指标口径只不过消费和处理数据的方式从定时批处理换成了持续流式处理。实际业务中实时数仓解决的典型问题有三类实时状态监控例如大促大屏、线上服务健康度、库存水位。实时运营决策商品秒杀效果、优惠券核销、渠道投放调整。异常感知支付失败率突变、登录风控、订单卡单。这些场景的共同点是数据晚一分钟决策价值就少一分。实时数仓能把“事后复盘”变成“事中干预”这是它存在的最直接理由。当然也要说清楚实时数仓并不是凭空造一套新数据结构它复用的大部分还是经典数仓的思想只是换了处理引擎和存储底座。1.3 别被概念带偏实时数仓不是“干掉离线数仓”我见过不少团队一听说实时数仓就想着把离线链路全部替换掉这其实是个很大的误区。实时计算虽然延迟低但代价也很具体流式计算引擎常驻运行资源占用通常比每日批任务高处理逻辑稍复杂状态管理就会出现各种边界问题而且实时成本与数据规模基本成正比越往后越贵。更合理的做法是保留双链路离线数仓继续承担历史累计、全量重算、复杂分析实时数仓负责高时效、高频访问、反应式指标。两条链路共用同一套业务口径、统一的数据规范只是在时效和成本上做了取舍。我们当时上线的第一版实时数仓也只覆盖了订单、支付、退款三个核心主题后面慢慢补没有一上来就追求“全部实时”。这个阶段的核心产出不是一堆表而是一张清晰的“哪些指标需要实时、哪些指标保持离线”的清单。清单越明确后面搭链路越省心。2. 实时数仓的选型取舍我把核心链路定在 Kafka Flink Doris2.1 消息管道Kafka 仍是默认答案实时数仓的最前端必须有一个能扛住高吞吐、能缓存数据的消息管道。这一层我几乎没有犹豫直接选了 Kafka。不是说 Kafka 没有缺点而是在实时数仓这个场景里它和大多数计算引擎、查询引擎的配合度最好。Kafka 的优势主要体现在三件事上削峰填谷业务高峰期消息瞬间涌入Kafka 可以先把数据缓冲住让下游计算引擎按照自己的节奏消费。多消费组隔离同一份原始数据可以同时喂给实时数仓、实时风控、实时搜索索引等多个消费方互不干扰。数据可回溯Kafka 支持按偏移量重新消费管道出问题时可以从某个点位重新跑。对比过 Pulsar 和 RocketMQ。Pulsar 的存算分离架构很先进多租户能力也好但对小团队来说运维组件多、踩坑资料少RocketMQ 在消息事务、延迟消息上有优势但 Flink 原生连接器的成熟度不如 Kafka。实时数仓的核心诉求是“稳定地把数据送给计算引擎”Kafka 是当前生态里最稳的选择。2.2 计算引擎Flink 为什么比 Micro-batch 更合适计算引擎的选择是实时数仓选型里争议最大的部分。我当时对比了 Flink 和 Spark Structured Streaming最终选了 Flink理由主要落在三点。第一Flink 是真正的流式计算引擎。它处理一条消息就走一条消息的处理逻辑每条消息都会触发计算和状态更新。Spark Structured Streaming 本质上是微批模式把一小段时间内的数据攒起来再算调试逻辑上会更接近批处理但在秒级延迟场景下微批的延迟表现不如纯流式。第二Flink 的状态管理能力非常完整。实时计算里有一类需求很典型去重计数、累计金额、最近N天活跃用户。这些都需要跨多条消息维护状态。Flink 提供了强大的状态后端可以把状态存在 RocksDB、内存或外部状态存储里并且配合检查点机制做到故障恢复。Spark Structured Streaming 也能做状态但部分复杂窗口场景的支持会让人折腾很久。第三Flink SQL 已经足够成熟。实时数仓很多逻辑其实就是“过滤、关联、分组聚合”用 SQL 表达和维护成本很低。Flink 让开发人员写流处理任务像写表查询一样后台字段变动也能通过 SQL 快速调整。2.3 查询存储Doris 支撑高并发与主键更新算完的数据最终要给人看这就需要一个适合“高并发查询”的存储引擎。这里我选的是 Doris理由围绕“更新”和“查询”两个词。实时数仓的数据有一个特点它不是只写一次就固定不变了。比如订单状态从“待支付”变“已支付”比如用户标签随着行为不断累积都需要对已有数据做更新。如果存储引擎不支持高效的主键更新就得先删旧数据再插新数据数据量和实时性一上来就会崩。选型时我重点考虑过 ClickHouse。ClickHouse 的导入性能确实很强但在主键更新上走的是 MergeTree 家族的 mutation 机制改动成本高高频更新场景容易出现性能抖动。而 Doris 提供了 Unique Key 模型入实时料时指定唯一键相同键的后续记录自动覆盖旧记录这对“订单状态变化”这类场景简直太合适了。同时 Doris 支持分区分桶、物化视图、Rollup 表能直接扛住大量用户的并发查询。在实时数仓里我们经常把明细层结果通过 Flink 任务实时写入 Doris再在 Doris 上建视图做指标聚合报表工具直接查 Doris 就行。2.4 选型对照表层级可选方案我为什么这么选消息管道Kafka / Pulsar / RocketMQKafka 生态成熟、Flink 原生支持好计算引擎Flink / Spark Structured StreamingFlink 真流式、状态管理强、SQL 完善查询存储Doris / ClickHouse / ElasticsearchDoris 主键更新方便、高并发查询稳元数据与主题管理自研轻量元数据表小团队没必要一开始就上重型数据治理平台选型没有绝对正确只有“当前阶段最合适”。我们的核心原则是尽量选团队已有的技术栈、社区活跃度高的项目、以及能快速从官方文档定位问题的组件。那些概念很美但资料稀少的组件等团队有专门人力了再去研究更稳妥。3. 四层分层不只有离线版实时数仓的 ODS / DWD / DWS / ADS 怎么落地3.1 各层职责和落点经典离线数仓有 ODS、DWD、DWS、ADS 四层实时数仓同样沿用这套分层但每一层的“物理载体”发生了变化。层级主要职责实时数仓中的落点ODS 原始数据层原样接入业务日志、Binlog、埋点数据Kafka 原始 TopicDWD 明细数据层清洗、去重、规范化、维度补齐Kafka Topic 或 Doris 明细表DWS 汇总数据层按维度、时间窗口做轻量聚合Kafka Topic Doris 聚合表ADS 应用数据层面向报表、大屏的高效查询Doris 视图 / 物化视图ODS 层最关键的原则是“不改原始数据”。无论上游传来的是 JSON、Avro还是直接来自数据库的 Binlog尽量保持原样接入只做最小限度的格式标准化。这样做的好处是下游出错时可以随时回溯原始消息排查问题不需要找上游要数据。DWD 层则是实时数仓里工作量最大的部分。订单数据进来后要过滤掉测试单、解析商品信息、关联用户维度和商品维度还要把不同来源的数据拼成宽表。这一层的结果通常已经适合业务分析但仍然是明细级别没有做聚合。DWS 层开始做“预聚合”。比如按分钟统计每个商品的成交金额、按用户统计累计下单次数。聚合粒度一旦确定后续查询基本就不需要再扫全表明细性能会快很多。ADS 层是真正提供给业务的一层可以理解为 Doris 上的一组视图或物化视图报表系统和数据大屏直接查这层。3.2 消息流转拓扑实时数仓第一版的链路我梳理成下面这个样子业务库 / 埋点日志 │ ▼ Kafka ODS Topic ──► Flink 作业1清洗、维度关联 ──► Kafka DWD Topic │ ▼ Flink 作业2窗口聚合 │ ▼ Kafka DWS Topic │ ▼ Doris ADS / 明细表 │ ▼ 报表 / 大屏 / 接口查询整个链路分成了两个 Flink 作业而不是一个大作业。原因很简单第一个作业负责明细清洗输出结果可以重复消费多次供不同的下游使用第二个作业负责聚合计算。拆开后如果聚合逻辑调整不影响明细清洗如果上游字段变更也不需要动聚合作业。3.3 层与层之间的数据形态差异实时数仓分层后数据形态是有明显区别的这点和离线数仓一致。我习惯把这几个层级想成“原始车间、零件组装车间、成品打包车间”ODS 是堆着原材料的仓库DWD 是把原材料加工成标准零件DWS 是把标准零件拼成半成品ADS 则直接面对柜台销售。DWD 层的数据记录通常是以“事件”为单位一条订单数据越宽越好把能关联上的维度都拼进去方便后面任意切割分析。DWS 层则已经变成了“指标”比如订单主题下某个商品每分钟的支付金额一条记录就是一个聚合结果不能再拆回明细。这两者的区别决定了存储模型也不同明细层在 Doris 里用明细模型保留每一条流水汇总层在 Doris 里用聚合模型按照维度键相同自动合并。如果分不清哪层该存什么后面设计表结构的时候就会出现一个表里既想存明细又想存汇总的尴尬状态。4. 从零接第一条实时链路Kafka 到 Flink 的搭建实录4.1 准备开发环境版本组合实时数仓第一阶段我建议先在本地用容器把环境跑通再考虑生产部署。因为生产环境的网络、权限、资源限制一大堆如果本地链路都不通直接上生产只会被各种配置问题淹没。版本组合是第一个要确认的坑。我踩过一次很经典的坑Flink 和 Kafka 的版本不兼容连接器提交任务时报了一堆类库冲突排查半天发现是 Kafka 客户端版本低了。现在做实时数仓第一版推荐一个相对成熟的组合Flink 1.17 及以上Kafka 2.x 或 3.xDoris 2.xFlink 连接器版本与 Flink 主版本保持一致本地验证链路时可以直接用 Docker Compose 快速拉起一个 Kafka 和 Flink 环境。下面是我当时使用的容器编排片段version: 3.3 services: kafka: image: bitnami/kafka:3.4 ports: - 9092:9092 environment: KAFKA_CFG_NODE_ID: 1 KAFKA_CFG_PROCESS_ROLES: broker,controller KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 1kafka:9093 KAFKA_CFG_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093 KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: true flink-jobmanager: image: flink:1.17 command: jobmanager ports: - 8081:8081 environment: FLINK_PROPERTIES: jobmanager.rpc.address: flink-jobmanager flink-taskmanager: image: flink:1.17 command: taskmanager depends_on: - flink-jobmanager environment: FLINK_PROPERTIES: jobmanager.rpc.address: flink-jobmanager这个配置里注意两个细节Kafka 在较新版本中引入了 KRaft 模式不再强制依赖 Zookeeper配置相对简单如果想沿用 Zookeeper 模式也可以但要记得把 Zookeeper 的服务也加进 Compose。启动后用docker compose up -d等几十秒Kafka 和 Flink 的 Web 界面能访问就说明基础环境没问题了。4.2 本地梳理 Topic 和消息格式环境起来之后先别急着写 Flink 作业。我建议的第一步是手动往 Kafka 里造几条测试数据把消息格式、字段含义先固定下来。这一步看似简单却能省掉后面很多“字段名对不上”“格式解析失败”的麻烦。建议先创建三个 ODS Topic 分别对应订单、支付日志、退款信息Topic 分区数先设为 3这个数字后面会跟 Flink 并行度挂钩后面详细说。我用下面的命令创建订单 Topickafka-topics.sh --bootstrap-server localhost:9092 --create \ --topic ods_order \ --partitions 3 \ --replication-factor 1然后模拟一条订单消息{ order_id: 20241001A0001, user_id: u1001, product_id: p2001, amount: 99.9, order_status: PAID, event_time: 2024-10-01 12:00:00 }消息格式在实时数仓初期建议统一用 JSON。JSON 好处是直观、调试方便坏处是解析慢、字段冗余。等数据量上来之后可以再考虑切到 Avro 或者 Protobuf但第一版不要在这里纠结能跑通业务才是最重要的。4.3 Flink SQL 建源表和建目标表环境跑起来、消息也造好了接下来就是写 Flink 作业。这一步我用的是 Flink SQL因为它最直观也最容易让团队里只会写 SQL 的同事介入。首先在 Flink 里注册 Kafka ODS 源的连接器表CREATE TABLE ods_order_source ( order_id STRING, user_id STRING, product_id STRING, amount DECIMAL(10, 2), order_status STRING, event_time TIMESTAMP(3), WATERMARK FOR event_time AS event_time - INTERVAL 5 SECOND ) WITH ( connector kafka, topic ods_order, properties.bootstrap.servers localhost:9092, properties.group.id flink_real_warehouse, scan.startup.mode earliest-offset, format json, json.fail-on-missing-field false );这里我加了一个水印这是后续做时间窗口聚合的关键。WATERMARK FOR event_time AS event_time - INTERVAL 5 SECOND的意思是允许事件时间最多迟到 5 秒超过这个范围的迟到数据会被丢弃。第一版可以先把这个值设大一点比如 30 秒宁可晚几秒出结果也比丢数据强。然后建一个 DWD 层的结果表同样写到 KafkaCREATE TABLE dwd_order_info_sink ( order_id STRING, product_id STRING, amount DECIMAL(10, 2), order_status STRING, event_time TIMESTAMP(3) ) WITH ( connector kafka, topic dwd_order_info, properties.bootstrap.servers localhost:9092, format json );最后执行一条简单的清洗逻辑把订单状态不是CANCELLED的数据写入 DWDINSERT INTO dwd_order_info_sink SELECT order_id, product_id, amount, order_status, event_time FROM ods_order_source WHERE order_status CANCELLED;提交后登录 Flink Web UI能看到作业处于 RUNNING 状态。再用 Kafka 自带消费者确认一下 DWD Topic 里有没有数据kafka-console-consumer.sh --bootstrap-server localhost:9092 \ --topic dwd_order_info \ --from-beginning能看到刚才模拟的那条订单被过滤后输出就说明最基础的实时管道已经跑通了。4.4 提交任务后如何验证链路链路通了之后很多人会忽略“验证”这一步等到真正上线才发现各种边界问题。我总结了一套简单的验证清单迟到数据验证故意把一条订单的event_time改成十分钟前再发进 ODS看 Flink 是否把它算进当前窗口。格式异常验证发一条缺少order_id字段的消息确认 Flink 不会因为缺字段直接崩溃。重复消费验证关掉 Flink 作业再重启观察 Kafka 消费组 offset 是否从检查点恢复而不是从头重跑。积压验证用kafka-consumer-groups.sh --describe查看消费者 lag确保消费速度跟得上生产速度。第一次跑通链路可能只需要半天但这些验证做完才算是真正有信心把这条链路放到测试环境。5. 这一阶段最容易踩的坑offset、并行度与schema5.1 检查点设置不当导致重复消费或丢数据实时数仓最基础也最关键的保障是“不丢不重”。Flink 靠检查点机制实现这个目标但检查点的配置如果太激进或太保守都会出问题。我当时第一次上线就把自动检查点间隔设成了 10 分钟以为省资源。结果某天作业重启后Kafka 消费组的 offset 被回退到了 10 分钟前那 10 分钟内的所有数据全部被重复计算了一遍下游订单金额直接翻倍。复盘后发现Flink 默认是开启 checkpoint 的但只有配置了合理的间隔和超时时间恢复时才不会出现大范围回放。现在我的建议配置是检查点间隔 1 到 3 分钟超时时间设置成间隔的两倍同时开启 exactly-once 语义。对于从 Kafka 读取数据、写入 Kafka 的作业Flink 可以做到端到端的精确一次前提是连接器都开启了对应的 semantic 配置。5.2 Kafka 分区数和 Flink 并行度不对齐这个坑很隐蔽但一旦踩到直接表现为 Flink 作业明明有 9 个并行度却只有 3 个在跑数据剩下 6 个一直处于空闲状态。原因就是 Kafka Topic 只有 3 个分区“一个分区最多被一个并行子任务消费”是 Kafka 消费模型的基本规则。实时数仓里Kafka 分区数要和 Flink 作业的关键并行度匹配。假如你预期 Flink 作业会扩容到 6 个并行度Topic 分区数就应该至少是 6。分区数太小并行度再高也白搭分区数太大又会导致消息乱序范围扩大增加状态管理的复杂度。我在建 Topic 时会把分区数定成稍大一点比如第一版先建 12 个分区后面业务增长再扩容。Topic 分区数只能增加不能减少所以提前规划比事后补救重要得多。5.3 事件时间与水位的坑迟到数据无影无踪实时计算的时间有三种事件时间、摄入时间、处理时间。大多数实时数仓的指标都要求按“业务发生时间”来统计所以得用事件时间。但事件时间天然存在乱序问题网络抖动、客户端延迟都可能让早先产生的事件晚到。水印是 Flink 用来“给乱序数据划条线”的机制。水印设置得太保守结果是延迟增大设置得太宽松结果是迟到的数据被丢弃。我们第一版把水印延迟设为 5 秒结果线上偶发消息延迟超过 5 秒导致部分订单没进当天的分钟级看板业务方找上门时排查了半天。后来改成“水印延迟 30 秒 允许侧输出迟到数据”。侧输出是 Flink 一个很实用的能力把超过水印的事件单独收集起来存到旁路 Topic方便事后排查和重放。这个做法强烈建议在一开始就加上否则后面想查“到底哪批数据因为迟到被丢了”会非常痛苦。5.4 schema 变更的影响与应对离线数仓里字段变更改一下表结构第二天重新跑一遍当天的分区就行。实时数仓可没有“第二天重跑”这个概念上游接口加一个字段、改一个字段名如果 Flink 作业不感知轻则新字段丢了重则整条解析报错、作业挂掉。我们的处理思路是消息生产者尽量采用带 schema 的格式给每个字段做版本管理。DWD 层建表时用json.ignore-parse-errors true这类容错参数避免单个坏消息把整个作业搞挂。另外实时数仓的字段规范和离线数仓要共用一套谁改了离线表的字段必须同步评估对应的实时链路。这一阶段还有一个容易被忽略的小事Topic 命名规范。ods_order、dwd_order_info这样的命名虽然简单后面一旦主题多了就会乱。建议一开始就固定格式层级前缀 业务主题 数据域比如dwd_trade_order、dws_trade_product_minute这个习惯越早养成后期维护成本越低。第一阶段的实时数仓到这里就已经具备了最基础的“采集—清洗—明细输出”能力。下一步要做的事已经很清楚把 DWD 层结果落到 Doris再往上搭 DWS 聚合层把看板、报表真正喂起来。这一阶段最核心的收获不是把一个作业跑起来了而是把“实时链路稳定性”的意识建立起来——消息不丢、不重、迟到可追溯这三点做到了后面的路才好走。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑