三年前一次需求评审,业务负责人指着竞品的大屏截图问:为什么人家能做到秒级刷新,我们不能?当时我的第一反应是“又来了一个要做实时大屏的”,但会议室安静下来之后,我开始意识到真正的问题在于:那些指标刷得再快,也不过是把T+1的报表搬到了秒级,业务看完后还是靠人去手动下一个判断。而竞品背后的逻辑,是让系统根据当前状态自动采取下一步动作。从“看到数据”到“替人做决策”,中间差的正是大数据领域数据架构设计中现在最被关注、也最容易做偏的一块——实时决策架构设计。
这篇文章不是给你画一张标准架构图就完事,而是会把我在实际项目中定义需求、做技术选型、搭实时链路、处理数据一致性、最后复盘故障的完整过程拆开来讲。适合正在做实时数仓、实时风控、实时库存或推荐系统的大数据开发同学,也适合准备从离线数仓转向实时方向、想理解整体架构设计的读者。
1. 实时决策要解决的根本问题,和离线分析完全不一样
1.1 从“实时大屏”到“实时决策”:需求定义必须对齐
业务方说“实时”的时候,我建议先别急着聊技术。把需求拆开看,大多数情况下其实是两种完全不同的诉求:
第一种叫实时可视化,本质是把指标更快地呈现给人看。秒级刷新大盘、大屏、监控面板都属于这一类。技术核心是降低查询延迟、加快数据流动速度,但人的分析和判断环节并没有变。
第二种叫实时决策,本质是系统基于最新状态自动做出业务动作。比如库存不足自动触发补货、支付风控自动拦截、异常流量自动限流。这类需求关心的是决策环路的时长,而不仅仅是指标可见的延迟。
很多团队做了半天,最后发现还是在做“更快的报表”,就是因为没把这两种诉求分开。实时决策架构关注的是:事件发生之后,能不能在目标时间窗口内完成特征计算、规则判断或模型推理,然后把动作执行下去,最后还能评估效果。这个闭环和大屏展示完全是两回事。
我自己喜欢用一个生活化类比跟业务方对齐:实时大屏相当于在车里装一个仪表盘,转速快慢看得清清楚楚,但真正让你不用频繁踩刹车的是自动巡航系统。自动巡航需要在每个瞬间感知速度、前车距离、道路坡度,然后输出油门或者刹车动作。这个系统如果只把数据刷得快而不做判断、不执行动作,是没有意义的。
1.2 实时决策的三种时延档位,决定了这套架构长什么样
对齐需求后,第一个要确认的就是时延目标。不同决策场景对待数据的耐心差距非常大,基本可以分成三档:
| 决策类型 | 典型场景 | 可容忍时延 | 核心技术形态 |
|---|---|---|---|
| 亚秒级决策 | 支付风控、账号安全、反作弊、限流 | 100ms级别 | 事件驱动 + 本地特征缓存 + 规则/模型内嵌服务,基本不会走全链路分布式计算 |
| 秒级决策 | 实时库存、营销触达、行情预警、异常检测 | 1到5秒 | Kafka + Flink + 特征存储 + 决策服务,是当前最主流的实时决策链路 |
| 分钟级决策 | 动态定价、调度优化、运营策略调整 | 1到10分钟 | 准实时数仓 + 轻量计算 + 定时任务,部分场景甚至能用微批处理实现 |
这三档决定了后面的存储选型、计算引擎选择、部署形态。做亚秒级决策时,如果你把事件先丢到Kafka再走Flink关联一堆维表,链路响应可能就到不了亚秒级;但如果目标只是分钟级调度,那确实没必要为每个请求引入复杂流计算,一个小批量任务加一张实时聚合表就够。
所以我的习惯是:在和业务方确认需求时一定会追问一句——“这个决策晚五秒执行会有什么后果?晚五分钟呢?”往往追问完,就筛掉了一半“伪实时需求”。
1.3 离线数仓为什么撑不起自动决策
很多人会问:我每天离线跑一次数据,把结果表更新得很频繁,比如把调度频率从一天一次改成五分钟一次,不也能做决策吗?
从工程上讲,这样做确实可以部分逼近实时,但本质上仍然解决不了三类问题。
第一是时间窗口问题。批任务即使调度设成分钟级,任务启动时间、资源排队、数据延迟波动,仍然会导致结果实际产出的时间不可控。而很多决策它的有效窗口非常短:一个用户已经下单但库存不足的场景,你晚30秒处理,这个用户可能已经流失了。批处理链路在时间上的长尾效应,是离线架构很难根治的。
第二是查询模型不适配。离线的BI分析是你问它答——比如昨天各区域销量如何、哪个品类库存周转慢。但实时决策是事件驱动——当一个行为发生时,系统需要快速检索某个实体的最新状态,然后立刻做出判断返回。这种“按实体取状态、按事件算特征”的访问模型,跟离线大宽表join完全不同。它要求的是点查能力、状态存储和时间序列窗口计算,而不是大规模全量扫描。
第三是治理逻辑不同。离线数据讲究全量可回溯、口径稳定,今天跑错了明天还能重跑;实时决策要求低延迟、高可用,并且要在线处理重复、乱序、迟到这些情况。你把离线链路硬改成近实时,只会让调度陷阱一个个暴露出来,而且运维成本远高于设计一套专门的实时决策架构。
所以我在一开始就把文章的基调定下来:实时决策不是高级报表,它是把数据分析能力变成业务系统的一个前置环节。这个概念一旦想通,后面的技术选型就顺理成章了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 选型之前先算三笔账:延迟账、吞吐账、成本账
2.1 延迟预算账:端到端每一跳能损耗多少毫秒
我见过太多团队在选型时把各个组件单独做benchmark,得出的结论是每个组件都很快,但连起来业务方却反馈卡顿。问题出在只看了每个组件的最好成绩,没有做端到端的延迟预算。
假设你的目标链路是秒级决策——事件产生到决策动作执行完毕不超过2秒。那从事件上报到出结果,中间走的每一跳大致是这样的预算:
- 客户端或服务端事件上报:100ms以内(这里面包括网络传输、心跳批量发送等)
- 接入层接收、反序列化、数据校验:50ms以内
- 消息队列排队与消费:50到200ms(要特别关注P99而不是平均延迟)
- 实时计算层清洗、关联、窗口状态计算:300~500ms(如果涉及多流join和维表查询,这里会更高)
- 特征/状态查询:50ms以内(一般走Redis或本地状态)
- 规则引擎或模型推理:50~100ms
- 决策结果回传与业务执行:100ms以内
把这些加起来,比较理想的情况下已经超过1秒了。如果想做500ms以内的决策,上面这套以Kafka+Flink为中心的通用链路基本就不够,需要把决策能力下沉到离数据更近的位置,或者把大量计算前置预聚合。
这里我有个很深的体会:设计实时决策链路时,你心里必须有一张“延迟预算表”,就像做性能优化时要先拆解CPU时间一样。这样当某个环节的延迟超标时,你才知道是哪一个环节打破了预算,而不是靠感觉去猜。
另外一定要关注P99和P999。平均延迟是500ms的数据链路,P99很可能已经到3秒了。对于决策系统来说,P99超标意味着约1%的业务会体验不到真正的实时,如果这个业务是自动补货,那1%的晚到就可能直接造成断货。所以从第一天起,端到端全链路的延迟监控就必须做起来。
2.2 吞吐与分区账:流量估算不是越精确越好,但不能不估
延迟解决的是“来一个事件能不能及时处理”的问题,吞吐解决的是“同时来一万个事件会不会被压垮”的问题。两者缺一不可。
算吞吐账有一个相对实用的粗略方法。假设业务高峰期每秒产生10万条业务事件,平均每条大约500字节,那峰值流量就是每秒50MB的数据。Kafka单分区在写多读少的场景下大致能支撑几MB每秒的写入吞吐,为了留余量,通常我会让单分区吞吐不超过其理论值的一半,也就是约2到5MB/s。这样算下来,Kafka topic需要大概16到24个分区才比较稳妥。
但这只是算完了“存储写入”这一层。接着要考虑实时计算层的并行度扩展是否跟得上。Flink消费Kafka时,并行度如果高于分区数,多余并行度是空转的;低于分区数,又会有部分分区消费不及时。所以分区数设计要跟下游计算并行度对齐,一般按并行度等于分区数或者分区数是并行度的整数倍来规划。
还有一个容易被忽视的坑是热点数据。流量估算再准,如果某个SKU或某个用户ID是超级热点,比如大促时的爆款商品,它的库存状态更新天然要keyBy到同一个处理节点上,这时候加多少并行度都没用,单点就是瓶颈。这类热点场景的处理方案要么做读写分离,要么通过业务规则把热点拆细到子状态再合并,要么在架构上接受“热点状态单点更新”的物理约束,把决策链路设计成读多写少的模式。
2.3 成本与运维账:不是每个指标都值得实时化
聊完了延迟和吞吐,再算成本账。
实时决策链路和一键跑批的离线任务完全是两种成本模型。离线任务跑完释放资源,实时链路需要长期独占至少一套Kafka集群、一套Flink集群、若干个状态后端和在线存储节点。以我经历过的项目估算,同样处理量级的实时计算集群资源成本通常是离线处理的3到5倍,而且这是持续性的费用,不是一次性开销。
更隐性的成本在运维。离线任务跑错了可以回滚重跑,它的数据是静止的,什么时候纠正都来得及。实时链路一旦有代码逻辑错误或者依赖服务抖动,错误会在第一时间传递到下游,甚至已经被决策引擎执行成动作了,那时候就不是“把任务重跑一遍”那么简单,而是要发补偿任务、通知业务方回滚。这个运维成本一天到晚都在产生,所以我经常跟团队说:“能离线算的不要实时算,能分钟级算的不要秒级算”。
具体落地时,可以给数据资产做分级:一部分核心决策指标走全实时高可用链路,一部分辅助运营数据走分钟级准实时批量任务,剩下的分析复盘数据继续走离线数仓。这样做出来的整体架构既保证高价值决策的实时性,又不会让成本失去控制。
2.4 组件选型对照:一套可以复用的决策链路清单
延迟、吞吐、成本三笔账算完之后,选型就不是拍脑袋了。下面是我在实践中沉淀下来的一套组件选型对照,不一定适用于所有公司,但可以作为初始方案:
| 架构层 | 组件建议 | 核心选型理由 | 常见误用方式 |
|---|---|---|---|
| 事件采集接入 | 埋点SDK、Logstash、Flume、CDC工具 | 统一事件格式、保证可追踪、支持断点续传 | 让每个业务方自由定义消息格式,后期清洗成本爆炸 |
| 消息队列 | Kafka为主;Pulsar可作为备选 | 吞吐高、生态好、分区有序、支持持久化重放 | 把Kafka当数据库存全量数据,topic数量失控 |
| 实时计算 | Flink为主 | 真正流式计算、状态管理、精确一次语义、SQL成熟 | 用Spark Streaming的微批硬撑秒级场景,实时性不够 |
| 在线特征/状态存储 | Redis、HBase | Redis适合高并发点查,HBase适合海量宽表 | 用HBase做高并发热点查询,延迟达不到要求 |
| 聚合分析存储 | Doris、ClickHouse | 适配多维实时分析,支撑分钟级和离线场景 | 把全部明细和在线决策状态都塞进去,查询变慢 |
| 规则/模型推理 | Drools、Aviator、自研推理服务 | 支持规则热更新、模型在线推理 | 规则写死在代码里,业务方改一条规则要发一次版 |
这里面有一个很根本的雷区希望大家避开:不要让一个组件承担所有职责。我见过一个团队把Kafka里存的实时明细再做一次全量查询,就为了算历史累计值,结果消息积压和查询超时同时爆发。“消息队列负责传输、流计算负责加工、在线存储负责点查、OLAP负责分析”,每层各司其职,实时决策链路才可控。
3. 实时决策链路的分层架构是怎么搭出来的
3.1 标准六层结构:从事件接入到决策执行
选型完成后,就要把这些组件组装成一个能够稳定运转的整体。我在设计实时决策数据架构时,倾向于把链路明确分成六层,每一层都有清晰的边界,团队之间按接口协作,互不干扰。
第一层是事件接入层。所有业务事件(订单、支付、库存变更、用户行为等)通过统一SDK或标准化接口上报到平台。这一层最关键的是统一事件规范:必须带事件ID、事件时间、实体ID、业务类型和扩展字段。后期所有实时计算的坑,有一半以上都源于这层没有做好规范。
第二层是消息总线层。标准做法是基于Kafka按主题划分不同的数据流,例如订单事件流、库存事件流、用户行为流。分区键的选择非常重要,同一实体的消息必须进同一个分区才能保证顺序。操作型消息以实体ID作为key,比如同一个SKU的库存变更事件必须进同一分区。
第三层是实时数仓计算层。这层负责对原始事件做清洗、维度补充、乱序修正、窗口聚合。它和离线数仓的分层逻辑对齐但实现方式不同,后面专门展开说。
第四层是特征与状态存储层。计算层产出的实体最新状态、实时特征、聚合指标要落到在线存储,供决策服务查询。这里又分两种情况:极低延迟的只用本地状态或Redis,海量实体特别是长尾用户或商品的画像就放HBase。
第五层是规则引擎与模型推理层。决策服务拿到特征后,会跑业务规则或模型推理。规则引擎要做到配置化、热更新,模型推理服务可以独立部署,输出决策建议或动作指令。这一层是“数据架构”和“业务系统”真正交汇的地方,也常常是两个团队最容易争执的地方。
第六层是决策执行与反馈层。决策动作要落到具体业务系统,比如创建补货单、拦截支付、发送优惠券。执行成功还是失败、业务效果如何,都要通过消息回传形成闭环。
之所以坚持把六层在架构图上画清楚,是因为我见过太多做实时决策的团队只在第一到第四层使劲,第五层以后的事情交给业务系统自己处理。结果数据链路做了很多,但决策时延、决策命中率、决策效果这些核心指标完全不可控。原因很简单:你没把架构延伸到决策的发生现场和反馈回路。
3.2 实时数仓的分层和离线不一样:DWD和DWS要在流上做
这套链路的核心中间层,是大家常说的“实时数仓层”。离线数仓有ODS、DWD、DWS、ADS,实时链路其实也沿用了一样的概念,但它的内涵很大不同。
实时数仓的ODS层就是Kafka里的原始事件流。事件进来自带粗粒度时间,可能有很多脏数据,字段名不统一,格式混乱,不能直接给下游用。这里可以直接把业务Topic映射成Flink表,做基础校验后重新写入一个新的Kafka主题,作为统一ODS。
DWD层负责清洗、过滤、维度补齐、数据标准化。例如库存变更事件要和仓库维度关联,得到仓库所属区域、仓库类型,再补齐SKU所属类目;订单事件要把支付、取消、超时这些不同流程产生的消息统一成标准动作,方便后续决策。下面是一个典型Flink SQL流处理示例,把原始库存事件转成带维度信息的DWD事件:
sql复制CREATE TABLE kafka_ods_inventory_event (
event_id STRING,
warehouse_id BIGINT,
sku_id BIGINT,
event_type STRING,
qty INT,
occur_time BIGINT,
event_time AS TO_TIMESTAMP_LTZ(occur_time, 3),
WATERMARK FOR event_time AS event_time - INTERVAL '3' SECOND
) WITH (
'connector' = 'kafka',
'topic' = 'ods_inventory_event',
'properties.group.id' = 'dwd_inventory_group',
'scan.startup.mode' = 'earliest-offset',
'format' = 'json'
);
CREATE TABLE dwd_inventory_event (
event_id STRING,
warehouse_id BIGINT,
region_id BIGINT,
sku_id BIGINT,
category_id BIGINT,
event_type STRING,
qty INT,
event_time TIMESTAMP(3)
) WITH (
'connector' = 'kafka',
'topic' = 'dwd_inventory_event',
'format' = 'json'
);
INSERT INTO dwd_inventory_event
SELECT
e.event_id,
e.warehouse_id,
w.region_id,
e.sku_id,
s.category_id,
e.event_type,
e.qty,
e.event_time
FROM kafka_ods_inventory_event e
LEFT JOIN dim_warehouse FOR SYSTEM_TIME AS OF e.event_time AS w
ON e.warehouse_id = w.warehouse
