需求方说"这个要实时"。半年后翻看板访问日志:每个工作日 9 点前后被打开一次,其余时间无人访问。而为这条"实时"链路付出的是三套新组件、一个长期维护人、一套每天都在报差异的对账脚本——时效价值接近零,成本是三倍。
"实时"不是需求,是一个成本极高的属性。它必须换来对应的时效价值。这篇给判据:什么时候必须实时、什么时候批量就够、两者混跑时那笔账从哪来。
1. 先把"实时"翻译成消费侧的等待能力
"实时"这个词在生产侧的嘴里和消费侧的手里是两件事。生产侧说的是"数据一产生就推出去",消费侧关心的是"我最早什么时候能用、晚了会怎样"。分界在消费侧,不在技术栈。
三个问题要连着问,缺一个就无法判断:
| 问题 | 要追问到什么程度 | 答不上来意味着什么 |
|---|---|---|
| 谁在什么时候用 | 具体角色 + 具体时刻(人、系统、还是另一个任务) | 消费方不存在或不明确,时效无从定价 |
| 晚了会怎样 | 损失类型:钱、决策、体验、合规 | 没有损失 → 时效不是属性,是偏好 |
| 晚多久还能接受 | 一个可测的数字:秒、分钟、小时、天 | 无法量化 → 既无法验证,也无法拒绝 |
第三问最关键:答案本身就是一个档位,档位决定成本。分档方式如下,注意每一档的成本是相对纯批量而言:
| 档位 | 消费侧容忍度 | 典型实现 | 成本量级(相对纯批量) |
|---|---|---|---|
| 毫秒级同步 | 请求内必须拿到 | 同步调用、同库同事务 | 最高,且耦合最强 |
| 秒级准实时 | 几秒到几十秒 | CDC + 消息队列 + 消费端 | 高:链路变长,运维面扩大 |
| 分钟级微批 | 几分钟 | 短周期调度(1–5 分钟) | 中:复用批量的实现与治理 |
| 小时/天级批量 | 小时到天 | 定时全量或增量 | 最低:可重算、可回补 |
最后两行的差常常被忽略:分钟级微批的消费体验与秒级流式相差不大,成本却低一个量级。大量"必须实时"的需求,真实容忍度其实落在分钟级——这是整篇里最省钱的一次判断。
还有一条常被跳过的档位:只读旁路。当消费方只是一个看板或一次人工排查,不参与任何自动决策时,最便宜的"实时"不是流处理,而是允许它直接对生产从库做一次限流的聚合查询。这样做的代价是查询侧的资源被占用,收益是整条链路不用建:
// 只读旁路的最小形态:限流 + 超时 + 明确的时间上界
select count(*) from orders
where created_at > now() - interval '15 minute'
─── 必须配:statement_timeout、独立只读账号、独立连接池(不与业务共用)
这条路径能被接受的前提是消费方容忍"偶发查不出来"。它换取的是零运维链路,代价是解析与治理责任留在数据库侧,且数据量一大就退化成事故——超过单机聚合的舒适区(典型量级:千万行以上、或需要多表关联)就该回到正式档位。
判断顺序上有个硬规则:把静默期一起问进去。先问"凌晨三点数据晚到二十分钟,有人打你电话吗",再谈技术。晚到没人打电话的时间段越长,档位就越该往下降。
2. 实时的三个隐性成本,与批量的三个优势
实时的显性成本(组件、机器、许可)通常在预算里看得见;隐性成本看不见,却在半年后集中爆发。
| 隐性成本 | 具体表现 | 批量的对照面 |
|---|---|---|
| 故障面变大 | 链路从 2 段变 5 段,任一段抖动都表现为"数据缺失" | 一次任务失败 = 一次重跑 |
| 无法重算 | 中间结果已被消费、状态已被覆盖,重放要重建全链路 | 重跑即可,输入还在原地 |
| 状态管理复杂 | 去重表、偏移量、窗口状态、水印都要持久化与清理 | 状态就是"上次跑到哪",一个字段 |
第二条最容易被低估。批量任务出错时的标准动作是"修完重跑",因为输入还在原地,输出可以被覆盖。实时链路里输出已经被下游消费、外部系统已经据此发过通知,重算就不再是内部动作,而是要向外解释的动作——解释成本往往比技术成本高。
由此得到一条反直觉的结论:"实时 + 可重算"这个组合,实际意味着你在做微批。想同时拿到真正的低时延和任意时点的可重算,只有两条路——每一条都留着完整历史输入(那你就是在按窗口重算),或者下游全部幂等可覆盖(这要求下游配合,属于组织成本而不是技术成本)。
3. 流处理的心智负担与混合架构的那笔账
下面四个问题在流式链路里是常态,在批量里几乎不存在:
| 问题 | 为什么出现 | 处理成本落在哪 |
|---|---|---|
| 乱序 | 多来源、不同网络路径到达 | 窗口 + 允许迟到时间;窗口越长状态越大 |
| 迟到数据 | 上游重试、设备离线补传 | 要么丢弃(口径出现缺口),要么回补(要求能重算) |
| 精确一次 | 消费失败重试带来重复 | 端到端幂等,或引入事务/两阶段提交 |
| 状态膨胀 | 窗口与去重表只增不减 | 需要 TTL 与清理策略,否则内存是定时炸弹 |
窗口与状态是一笔直接交易:窗口开得越大,抗乱序能力越强,状态占用与故障恢复时间越长。具体窗口与水印参数按所用引擎的当前版本核对,不同版本的默认语义与 API 名称有差异,别照抄旧文档。
混合架构(批量兜底 + 实时旁路)看起来两全,代价是两条链路两条口径:
业务库 ──CDC──► MQ ──► 流处理 ──► 实时视图(口径 A:最近 N 小时)
│
└──每日批量──► 数仓 ──► 历史视图(口径 B:全量、可重算)
─── 两者差异不是 bug,是口径不同:过滤规则、迟到处理、去重键都可能不一样
─── 因此必须有一个对账任务,否则"哪个数是对的"永远没有答案
对账任务是混合架构的固定成本,不是可选项。它的产出不是"一致",而是差异率与差异原因分类。差异率长期不收敛,通常说明两条链路的口径定义从一开始就没对齐,而不是某条链路有 bug。
演进顺序上有一条性价比极高的路径:
─── 第一步:先把批量做扎实(可重算、可回补、有对账)
─── 第二步:确认存在真实且可量化的时效缺口(例如"决策窗口只有 5 分钟")
─── 第三步:只给这个缺口加一条窄旁路,不迁移主链路
scheduler: daily // 主口径,全量,可重算
scheduler: every-5m // 旁路,只产出缺口所需的最小字段集
on_conflict: prefer_batch // 长期口径以批量为准,旁路只负责时效
先做批量、需要时加一条实时旁路是最稳的顺序:批量的实现与治理已经存在,旁路只承担时效,出问题可以随时关掉而不影响主口径。反过来走(先流式、再补批量)意味着要在已有实时口径上再叠一套对账,两边的成本都要付。
4. 度量与排错
判断做得对不对,看四个数。它们都能从链路本身取到,不需要额外的埋点体系:
// 端到端时延分布:均值没有决策价值,看分位
select
percentile_cont(0.50) within group (order by consume_ts - produce_ts) as p50,
percentile_cont(0.95) within group (order by consume_ts - produce_ts) as p95,
percentile_cont(0.99) within group (order by consume_ts - produce_ts) as p99
from event_log
where dt = current_date;
// 迟到数据比例:决定窗口该开多大,而不是凭感觉设值
select
sum(case when consume_ts - produce_ts > interval '60 second' then 1 else 0 end)::numeric
/ count(*) as late_ratio,
max(consume_ts - produce_ts) as worst_lag
from event_log
where dt = current_date;
// 混合架构的两个固定指标,按小时持续记录、只看趋势不看单点
diff_ratio // 差异行数 / 总行数
backfill_cost // 回补一小时数据所需墙钟时间,决定"多久能救回来"
| 现象 | 常见原因 | 处理方向 |
|---|---|---|
| 时延 p50 很小、p99 极大 | 少数分区或分片倾斜 | 查分区键分布,而不是先加机器 |
| 迟到比例随白天上升 | 上游重试集中在业务高峰 | 调大允许迟到时间,或把重试挪出高峰 |
| 对账差异率长期不收敛 | 两条链路的过滤规则/去重键不一致 | 先统一口径定义,再谈修链路 |
| 回补耗时超过业务容忍窗口 | 中间状态不可重建,回补要走全链路 | 降档为批量主口径,旁路推倒重做 |
| 关掉旁路后无人发现 | 时效价值从未被验证 | 那这个"实时"从第一天起就不需要 |
5. 边界:什么时候这个问题根本不成立
| 情况 | 结论 | 理由 |
|---|---|---|
| 消费侧本来就是人工或日结 | 批量即可 | 等待能力由组织节奏决定,不由技术决定 |
| 数据量极小(单机可全量装入内存) | 批量即可 | 全量重算耗时低于一次链路运维 |
| 时效价值无法量化 | 先量化,再决策 | 无法量化的收益无法证明,也无法在复盘时被辩护 |
| 数据用于审计与归档 | 批量 + 可验证 | 要的是可重算与留痕,不是时效 |
| 存在真实但极窄的缺口 | 只做窄旁路 | 不必迁移主链路,也不必维护两套全量口径 |
最后一条的判据很直接:时效缺口描述不清"谁在什么时候用",就先别改架构。把三问的答案写进需求文档,比把流处理集群搭起来便宜得多。
6. 自测
- 需求方说"要实时",第一个该问的问题是什么?
- 为什么说"实时 + 可重算"这个组合通常意味着你在做微批?
- 批量架构在出错恢复上的核心优势是什么?
- 混合架构里对账任务为什么不是可选项?
- 时延均值正常但 p99 极高,优先查什么?
- 什么情况下"实时化"这件事本来就没有收益?