
“rea”不是我随手乱敲的三个字母。它是我们团队内部一个实时事件分析系统的项目代号全称是 Real-time Event Analytics。最早接到这个需求时运营手里只有 T1 的离线报表活动上线后异常率高不高、支付链路通不通都要等到第二天才知道等发现问题用户已经流失了一大截。所以我决定搭一条实时链路埋点数据从客户端产生后经过消息队列和流式处理分钟级计算出异常事件、转化漏斗和核心业务指标并且能随时查明细、配告警。这个代号为 rea 的项目后来稳定运行了很长时间期间帮我们抓到过好几次真实的功能故障。如果你正在做日志分析、业务指标监控、用户行为实时统计或者只是想把实时链路的骨架跑通这篇文章应该对你有用。文中涉及的配置参数、表设计、排查手段全部来自真实落地不是理想化方案。1. rea 项目概述与整体设计思路1.1 需求拆解实时分析到底要解决哪几件事接手这个项目前我把需求拆成了三类每类的使用者、数据形态、时延要求完全不一样。第一类是实时大盘。业务方要看到当前这一刻的访问量、下单数、支付成功率、报错次数颗粒度至少到分钟级。这类需求的特点是数据量大、可容忍小范围误差但不希望长时间看不到数据。第二类是实时告警。当某个接口的错误率连续两分钟超过阈值或者支付失败数量突然翻倍系统要能够在三分钟内通知到相关同学。这类需求对时延和准确性要求最高。第三类是事后追溯。某个用户反馈“下单失败”我需要立刻把他在最近十分钟内的完整事件序列捞出来看看卡在哪一步。这种查询通常带着用户ID、设备ID、事件时间这几个条件需要底层存储支持快速的点查和范围扫描。这三类需求混在一起注定了不能只靠一套简单的“日志收集搜索引擎”搞定。实时大盘和实时告警需要增量计算事后追溯需要明细存储和高效索引。所以我从一开始就定了方向数据进来之后分两条路走一条做实时聚合计算一条做明细节流互不干扰各自服务不同的查询场景。1.2 为什么选择自研而不是直接上商业方案在动手之前我也认真评估过现成的商业产品或开源全家桶。商业方案的好处是开箱即用控制台里点点鼠标就能配置数据源甚至自带可视化看板。但问题也很现实费用高、数据离库成本高、二次定制能力弱。我们团队的数据体量虽然不算超大但字段经常变业务规则平均两周就会调整一次商业产品的字段模型一旦定死后续改起来非常痛苦。开源全家桶的路线我也调研过比如直接用现成的日志系统加一套开源时序数据库。这个方案的优点是生态成熟缺点是链路太“重”很多组件并不是为业务埋点设计的字段语义、聚合粒度、权限模型都跟我们的需求有出入。调研到最后我得出一个结论——与其花大量时间迁就外部框架不如只借最核心的几件基础设施然后自己写一套轻量的事件接入层和计算逻辑。这样成本可控、扩展灵活后面加的每个字段、每条新规则都在自己手里。所以 rea 的最终架构是这样的客户端/服务端埋点 SDK 负责生成事件消息队列负责削峰和缓冲流式计算引擎负责清洗和窗口聚合分析型数据库负责明细存储和查询再加上一个轻量的告警服务和看板服务。这一整套东西能跑在普通配置的服务器上也能在数据量翻倍时通过加分区、加并行度的方式横向扩展。2. 事件模型与数据接入的关键细节2.1 事件协议怎么设计一份可落地的埋点规范实时分析系统的地基是事件协议协议定得不好后续每改一次字段都要在消费端做兼容特别痛苦。我第一版用的是 JSON因为测试方便、排查问题直接看消息内容就行。但上线后发现在高峰期每秒几万条事件的情况下JSON 解析很有压力于是第二版换成了二进制序列化格式。这里给个建议如果你们的数据量一天在千万级以下JSON 完全够用如果已经到了亿级尽早换二进制性能差距不是一个量级。一个比较合理的事件模型至少要分成两部分公共字段和业务字段。公共字段包括 app_id、user_id、session_id、device_id、platform、event_name、event_time、ingest_time。业务字段是一个 map 结构用来放这个事件独有的信息例如订单事件的 order_id、金额、商品ID页面事件的 page_id、停留时长。这种“公共字段打平 业务字段字典”的模式弹性非常大新增业务字段不需要改主表结构只要保证消费端能把 map 解析出来就行。埋点规范里最容易忽略的一件事是用户ID的缺省处理。很多匿名用户在被识别前user_id 是空的这时候必须由 SDK 生成一个临时设备ID否则后续做漏斗分析时第一步的“访问量”和第二步的“登录量”永远对不上人。我们的做法是如果业务方没有传 user_idSDK 先读本地缓存的设备标识没有再生成 UUID然后同时上报 device_id 和 user_id_anon在流式清洗阶段再根据映射表把匿名ID改成真实用户ID。2.2 时间戳与乱序延迟实时系统里最容易被忽略的坑事件时间业务发生时间和数据处理时间服务器收到时间是两个完全不同的概念。刚开始做实时统计的时候我犯过一个经典错误直接用处理时间来做窗口聚合。结果是客户端因为网络慢发不出数据或者用户手机离线了一段时间补报的历史数据会被计算进当前窗口整个大盘的指标就失真了。正确做法是采用事件时间。客户端在生成事件时带上自己的本地时间服务端收到后以这个时间为准来做窗口划分。但这会引出一个新问题事件到达不是严格按时间有序的总有一些事件会“迟到”可能迟到几秒也可能迟到几分钟。Flink 这类流处理引擎处理这个问题靠的是水位线机制。简单说水位线是流处理引擎根据已经收到的数据时间戳推导出的一个“当前进度标记”它告诉系统在水位线之前的事件已经基本到齐了可以触发窗口计算了。我实际配置的时候把乱序容忍度设成了 10 秒。这个值不能拍脑袋需要看客户端上报的真实时延分布。在我们的场景里90% 的事件能在 5 秒内送达95% 的事件能在 10 秒内送达所以 10 秒是一个比较平衡的选择。调大了指标延迟高调小了乱序事件会大量落到窗口外导致聚合结果偏低。后来通过监控发现晚到事件占比控制在 1% 以内整个指标系统的可靠性才算达标。3. 流式处理与存储查询的实操配置3.1 消息队列与流式引擎的选型及关键参数这里我用的是经典的 Kafka 加 Flink 组合。很多团队纠结要不要上更新的框架我的观点是在中小规模下这套组合最稳、资料最多、出了问题最容易被搜到解决方案。Kafka 的 topic 划分按事件域来做比如 order_topic、user_topic、error_topic。不要所有事件都塞进一个 topic否则业务量增长之后单一主题的吞吐和消费端逻辑都会成为瓶颈。分区数量的计算要考虑目标吞吐量。我们按单条消息平均 1KB、未来峰值每秒 3 万条估算单分区写入大约每秒 10MB3 个副本下集群的网络开销不小所以核心 topic 都给了 12 个分区普通 topic 给 6 个分区。Flink 消费端的并行度跟分区数保持一致避免出现部分子任务空转、部分子任务过载的情况。Flink 侧有几个参数值得说。第一是 Checkpoint 间隔我设成了 60 秒存储后端用 RocksDB。RocksDB 虽然性能不如纯内存状态后端但状态一大就不会 OOM进程稳定性高很多。第二是语义设置消费端 Kafka 源和输出端做了精确一次语义但这个语义的前提是下游存储支持幂等写入否则“精确一次”只是计算引擎层面的精确一次数据库落盘那一步仍然可能重复。第三是空闲分区问题这一点我踩了坑后面会专门讲。3.2 实时聚合的窗口计算窗口到底设多长才合理窗口聚合是 rea 最核心的逻辑。窗口太短抖动大业务方看着曲线心惊胆战窗口太长告警不灵敏实时系统价值直接减半。我第一版只做 1 分钟滚动窗口测试时发现错误率指标在低峰期波动特别大经常误报。后来改成了一套分层策略1 分钟滚动窗口用于实时告警重点关注错误事件、支付失败事件。5 分钟滚动窗口用于趋势大盘统计 PV、UV、转化率、成功率。15 分钟滑动窗口用于运营活动复盘窗口步长 5 分钟既有平滑效果又不会太迟钝。窗口计算的代码模式化程度很高。每一项指标都单独定义聚合函数和输出字段遇到跨窗口的环比需求就把当前窗口结果和时间戳作为 key 写到状态里等下一个窗口触发时读出来做差。这个状态维护逻辑一定要设置 TTL我用的是 15 分钟过期否则状态会无限膨胀。还有个细节聚合结果的下游存储要提前建好预聚合表否则每个窗口都往明细表里写一批聚合数据查询时还要一遍遍重算白白浪费计算资源。一个可复用的配置是在分析型数据库里把“事件时间精确到分钟”建成分区字段聚合结果直接按分区写入查询时秒级返回。3.3 指标结果与明细数据双通道存储一张表还是两张表rea 在存储层做得比较“重”但其实结构很清晰。实时聚合结果和原始明细分开落库用途完全不同。聚合结果我放到了一套时序型存储里。时序存储对时间范围查询优化得比较好写路径也很适合 append-only 的指标数据。每条聚合记录的 key 是“指标名称 事件类型 应用ID 时间窗口”value 是计数、求和、均值等统计值。这块存储承担的是看板曲线和告警阈值检查查询频率高但单条数据很小。原始明细则放到分析型列式数据库里。明细表的结构是扁平化的把公共字段全部作为独立列业务字段统一塞进一个 json 列。排序键按照“事件时间、应用ID、事件类型”排列这样最常见的时间范围查询会直接命中排序前缀扫描的数据量可以缩减到原先的十分之一。我在明细表上建了一个“用户最近事件”的查询接口输入 user_id 和时间范围平均响应在 300 毫秒以内这个体验对排查问题来说足够好了。双通道存储带来的一个额外要求是数据写入要支持批量写。聚合结果一个窗口才产生一条单条插入开销很高所以做了攒批逻辑攒够 1000 条或 5 秒强制刷一次。明细写入压力更大采用分区批量导出的方式避免高频小事务把数据库的写入路径打满。3.4 告警与可视化看板的落地细节看板和告警看似是最后一步其实决定了整个系统能不能真正被用起来。最先要做的是定义指标的“绝对值阈值”和“同比/环比阈值”。绝对值阈值好理解错误率超过 5% 就报警。环比阈值需要一些技巧直接用昨天的同时刻做基准会遇到“昨天凌晨本来就没流量”这类极端值问题所以基准线取“过去 7 天同一时刻的中位数”这样能把正常波动过滤掉。告警通知要分级不要一上来就轰炸。我们分了三层P0 是核心支付链路整体不可用电话加群消息P1 是错误率超过阈值但服务还可用发群消息并置顶P2 是指标异常波动但影响面有限只在告警台留存。这套分级最大的好处是让告警真正有敬畏感否则到处响铃等于没响。看板这边的经验是第一屏只放 6 个核心卡片包括实时 PV、实时 UV、支付成功率、错误率、平均响应时间、漏斗转化率。所有图表的刷新周期统一为 1 分钟时间范围支持回看最近 24 小时。业务方后来提的需求都是“能不能加一个维度下钻”这类需求依赖前面做好公共字段字典不然每个下钻都要改底层表成本会非常高。4. 实战中的典型问题与排查技巧4.1 消费堆积与反压数据堵住的定位手段rea 上线后的第一个线上事故就是消费堆积。某天下午运营反馈大盘数据延迟了将近二十分钟我第一反应是去查消费端 lag果然某个关键 topic 的 lag 指数级上涨。但直接原因藏在下面不是 Flink 计算变慢而是下游明细库的批量写入出现了锁等待导致写入吞吐从每秒 5000 条跌到 1000 条反压一路传导到 Kafka 消费端。这类问题的排查顺序非常重要。先看下游数据库的慢查询和写入延迟再看流计算引擎的反压指标最后才是 Kafka 本身。如果上来就盯着 Kafka 的 lag很容易把“下游堵塞”误判成“消费能力不足”然后盲目加并行度结果下游更堵。正确的处理是先把下游恢复等 lag 自然消掉如果持续高水位再做分区扩容。这里还有一个容易忽略的点分区数的上限问题。Kafka 的分区数在创建之后虽然能扩但扩容后旧的 key 分布不会自动重平衡新分区可能长期没数据。我现在的习惯是核心 topic 提前按两年后的峰值估算分区数宁可初期多占一点资源也不等到故障时再扩容。4.2 水位线异常为什么窗口迟迟不触发有一次我们更新线上程序某个事件类型的数据量骤减随后整个看板都停止更新。查了两小时发现根因是水位线机制中的一个经典陷阱Flink 的水位线是所有 Kafka 分区取最小值。只要有一个分区持续没有新数据引擎不知道该不该推进时间整个作业的水位线就卡在那里所有窗口都不触发。解决方法是给数据源设置空闲分区超时。一旦某个分区超过设定的空闲时间引擎就把它从水位线计算中剔除。这个参数我在测试环境完全没踩到因为测试环境数据量小且持续有埋点线上业务有高峰低谷一遇到低谷就现出原形。另外还有一个习惯值得养成观察 Flink 作业的水位线曲线上限而不是只看吞吐。水位线长期不动比 CPU 高更值得警惕。4.3 晚到数据与幂等写入数字为什么对不上实时统计和离线统计对不上是这类系统的世纪难题。有一次运营发现 rea 上报的支付成功数是离线报表的 97%看起来不多但活动期间金额差距有几十万业务方很紧张。定位之后发现是两个问题叠加一是晚到数据被侧输出流丢弃聚合结果自然偏少二是窗口结果重复写入时某些场景做了覆盖更新但明细表里同一个事件出现了两条记录。第一个问题靠延迟容忍度和侧输出回流。在 Flink 里设置 allowedLateness让窗口关闭后仍能等一段时间接收该窗口的晚到数据合并进结果状态超过允许时间段的数据进入侧输出流由专门的修正作业定期把修正值合并回存储。第二个问题则是靠下游幂等主键来解决。聚合结果表的去重键是“窗口时间 指标 维度”明细表针对重复上报做事件ID去重。这两套机制跑通之后实时和离线的差距从 3% 降到了 0.5% 以内。4.4 常见问题速查表一条一条对照排查现象可能原因优先排查位置常用解决措施看板数据长时间不刷新水位线卡住Flink 水位线曲线、空闲分区数设置分区空闲超时消费 lag 持续上涨下游写入变慢或反压数据库慢查询、Flink 反压指标优化写入、扩容并行度实时数据低于离线数据晚到事件被丢弃或重复去重失效侧输出流量、事件ID去重调大延迟容忍、补修正作业指标波动异常剧烈窗口过短或阈值设置过窄聚合曲线、告警规则分层窗口、调整灵敏度明细查询越来越慢分区策略失效或排序键不合适数据库分区大小、表排序键重建排序键、清理冷数据这张表是 rea 团队后来每次处理线上问题的“第一手工具”。大多数时候问题不会藏在很深的位置按着表格从上往下看五分钟内都能定位到根因。5. rea 的后续迭代与扩展建议5.1 从单环境到多场景复用的演进rea 最初只服务一条业务线后来被其他团队看中开始承载更多的场景。这个阶段最需要做的是把“事件协议管理”和“指标口径管理”抽出来做成独立模块。事件协议管理解决的是每个接入方都可以定义自己的事件字段但公共字段必须过校验指标口径管理解决的是不同团队对“支付成功率”的定义可能不同有的把取消订单算分母有的不算所以每个指标都要有明确的版本标识。多场景复用之后资源隔离也要跟上。小流量业务的 topic 和核心业务的 topic 混在同一个集群上某个业务的突发流量会影响另一个业务的消费延迟。现在的做法是至少分成核心集群和普通集群每个集群有独立的配额管理核心集群的故障不要波及普通业务反过来也一样。5.2 数据质量监控先有后优的必经之路实时系统迭代到后期稳定性已经不是难点数据准确性才是。rea 做了一件事对每类核心事件增加了一个“数据质量基线”。比如支付事件基线设定为实时计数和离线统计的偏差不能超过 1%如果超过就自动触发质量告警而不是等业务方来投诉。质量监控的逻辑不难就是每天定时跑一个对账任务把多维聚合结果和离线数仓的结果做对比。但它带来的好处很直接所有偏离都发生在还没有影响业务决策之前。如果你也打算做实时系统建议不要把所有精力都放在“怎么算得快”上在“怎么算得准”这块至少留一半时间。多说一句真正把 rea 打磨到能放心用靠的不是某一个灵光乍现的方案而是持续几个月的压测、对账、告警规则调优。每个参数都是从线上数据里一点点测出来的没有捷径。