每年的年度大促,都是对数据系统的一次公开考试。我今年负责的这块工作,是支撑某头部旅行平台的年度大促实时数据大屏——一块调度室里所有人抬头就能看到的大屏,上面滚动着实时成交金额、各目的地热门程度、订单趋势、支付成功率等核心指标。
最开始的版本,刷新延迟经常跑到十几秒,大屏一卡,满屋子人都在看数据。后来把链路从“批量计算+关系型数据库”重构为“实时写入+Hologres实时数仓”,最终做到了秒级响应,高峰期也能稳定扛住。这篇文章就把整个过程中的关键思路、表设计、SQL优化和踩坑记录整理出来,希望能帮到正在做类似实时大屏或实时数仓的朋友。
1. 实时大屏为什么那么难做
很多人以为大屏难在“好看”,其实真正难的是背后的数据链路。大屏天然有“三高”特征:高频率刷新、高并发查询、高实时性要求。而大促场景还会再加上一个“数据洪峰”——流量是平日的几十倍,而且集中在几个小时内爆发。
1.1 大促场景下大屏的核心难点
先看一组我在项目里实际面对的需求:
| 指标类型 | 示例指标 | 更新频率 | 大屏展示方式 |
|---|---|---|---|
| 交易类 | 实时成交额、订单量、客单价 | 秒级 | 数字滚动、趋势图 |
| 流量类 | UV、PV、热门目的地排行 | 秒级 | 柱状图、排行榜 |
| 质量类 | 支付成功率、平均响应耗时 | 10秒级 | 折线图、仪表盘 |
| 地域类 | 各区域订单分布 | 分钟级 | 地图热力层 |
关键点在于,大屏上的每个数字都是“有生命”的——指标要跟着业务实时跳动,不能像离线报表那样算完再刷。订单每产生一笔,大屏上的成交额就要跟着涨。支付失败,成功率曲线就要往下掉。这种需求决定了背后必须有一条“实时计算 + 实时存储 + 秒级查询”的链路。
1.2 大屏卡顿的真正原因
我最初接到的问题是个很典型的架构矛盾:数据落在离线数仓里,每天凌晨算好前一天的指标,再同步到大屏展示库。大促当天数据完全不对,又额外加了一套基于流式计算的实时任务,把结果写进一个关系型数据库,大屏前端每5秒轮询一次查询。
这套方案在低峰期看着没问题,一旦流量上来,问题就全暴露了:
- 流式计算任务每来一条数据就查一次维表做关联,导致结果表写入并发极高,数据库连接被打满。
- 大屏的SQL里有很多
GROUP BY和ORDER BY,高峰期要扫描大量数据才能算出排行,单条查询就要好几秒。 - 前端每5秒发起几十个请求,数据库连接池被查询任务占满,写入和查询互相争抢资源。
最后的结果是:大屏页面卡死,数据延迟从几秒变成几分钟,调度室里一片哀嚎。
1.3 重新思考选型,而不是简单加机器
踩过坑之后,我重新梳理了整个问题。大屏场景的本质是“高并发小查询 + 海量实时写入”,这和传统的数据仓库场景完全不同——传统数仓偏向“跑大任务”,而大屏需要的是“在线服务能力”。单纯加机器解决不了根本矛盾,必须换一个底层思路。
当时摆在我面前的有几个候选方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 关系型数据库分库分表 | 生态成熟,运维简单 | 实时写入高并发难处理,聚合查询性能差,扩展要改业务代码 |
| 时序数据库 | 写入快,适合监控数据 | 明细数据存储能力弱,复杂 SQL 支持差,和业务指标模型不匹配 |
| Elasticsearch | 查询灵活,聚合能力强 | 高峰期写入毛刺明显,数据一致性一般,大促时经常出现延迟飙高 |
| Hologres | 实时写入并发高,查询秒级,支持标准SQL | 需要一定的调优经验,物化视图使用不当也会踩坑 |
选Hologres的核心原因有几点:第一,它是真正的实时数仓,写入和查询可以同时跑,不像传统架构需要离线/实时两套系统;第二,它兼容PostgreSQL语义,团队迁移成本低,我之前那套SQL大部分能复用;第三,它对高并发小查询做了专门优化,特别适合大屏这种“短平快”的查询模式。
2. 整体架构与数据链路设计
思路理清之后,我开始搭建新的链路。核心原则是:把“大屏查询”和“业务明细”分离,让大屏永远只查预聚合好的结果,绝不直接面对原始流水。
2.1 链路全景
最终的结构是这个样子:
业务端产生订单、支付、浏览等事件 → 消息队列收集 → 实时计算任务做清洗和维表关联 → 结果写入Hologres → 大屏服务通过API查询Hologres → 前端渲染。
听起来简单,但每一步都有讲究。消息队列负责削峰填谷,让实时计算不至于被瞬时流量冲垮;实时计算任务负责把原始事件“翻译”成指标口径统一的数据;Hologres作为核心存储层,既要承接每秒几万条的写入,又要支撑大屏几十个指标的同时查询。
2.2 大屏查询服务的设计
大屏前端不是直接连Hologres,而是经过一层查询服务。这个服务有几个作用:
- 统一指标口径。前端要的“成交额”和“GMV”到底怎么算,在服务层固化成逻辑,避免每个图表各算各的。
- 做查询合并。30个小图表其实有很多共用的维度,比如“按小时聚合的成交额”和“按小时聚合的订单量”,可以合并成一次查询,减少后端压力。
- 加一层Redis缓存。虽然Hologres查询已经很快了,但“前5秒的指标”和“这个5秒的指标”在大屏上差别不大,缓存几秒完全没影响,还能给系统留出缓冲。
2.3 表结构设计的三个关键点
表结构设计是大屏性能的基石,我在这块踩了不少坑,总结出三个关键点:
第一,明细表、聚合表、维度表分开。明细表存实时写入的原始订单数据,保留完整的维度字段;聚合表存按时间+维度预聚合的指标;维度表存目的地、城市、类目等静态信息。大屏只查聚合表和维度表,明细表只给排查问题用。
第二,用时间字段做分区键。Hologres按时间分区可以大大缩小查询范围。我的做法是小时级分区,大屏查最近24小时的数据时,最多扫24个分区,相比扫全表节省巨大。
第三,合理选择存储模式。明细表用列存,压缩比高,适合分析;维度表和查询频繁的小表用行存,单条查询更快。这块没有绝对标准,要根据查询模式实验调整。
3. 秒级响应的核心技术实现
架构搭好之后,最核心的问题就是:如何让大屏上的指标做到真正的秒级响应。这里有几个关键实现,每一项都直接影响最终效果。
3.1 用物化视图做实时预聚合
大屏查询最怕的就是对着明细表做GROUP BY。哪怕Hologres性能再好,几十个图表同时在大促高峰期做明细聚合,查询时间也会被拉长。
我的方案是:在Hologres里建一对物化视图,把“按分钟+目的地聚合的订单量、GMV”和“按分钟聚合的成功率、UV”这些高频指标提前算好。
-- 示例:构建分钟级聚合的物化视图 CREATE MATERIALIZED VIEW agg_order_minute AS SELECT DATE_TRUNC('minute', ts) AS minute_ts, destination_id, COUNT(*) AS order_cnt, SUM(amount) AS gmv, SUM(CASE WHEN status = 'SUCCESS' THEN 1 ELSE 0 END) AS success_cnt FROM order_detail GROUP BY minute_ts, destination_id;物化视图的好处是双重的:写入时自动增量更新,查询时不走原始明细,直接读结果。这样大屏的每条SQL都很轻,哪怕同时几十个查询过来,也能稳稳扛住。
这里有个经验:物化视图的维度不要设计得过细。我最初按“分钟+目的地+渠道+用户等级”做了个超细粒度的物化视图,结果查询虽然快,但物化视图本身的构建和存储成本高了不少。后来精简成真正会在大屏上用到的维度,性能和资源达到了平衡。
3.2 大屏查询SQL的写法优化
即使有了物化视图,查询SQL的写法依然重要。大屏查询有几个特别容易犯的错误,我列出来供大家参考:
不要SELECT *。大屏图表只需要两三个字段,没必要把整行数据拉出来。减少IO是秒级响应最基本的一步。
优先过滤时间范围。大屏默认看最近24小时,这个过滤条件必须写在SQL里。我看到有人写了大屏SQL不带时间过滤,结果全表扫描,几十秒都出不来。加上WHERE ts >= NOW() - INTERVAL '24 hours'之后,性能天壤之别。
避免在查询时做复杂的表达式计算。比如SUM(CASE WHEN ...)这种写法,如果能在流式计算阶段就处理好,不要在查询时算。把所有可预计算的逻辑尽量下沉到写入链路。
限制返回行数。排行榜只需要Top N,SQL里限制LIMIT 50,不要让框架把几千行数据都拉回来再排序。
3.3 写入链路的实时保障
大屏的秒级响应不只是“查得快”,还得保证“数据新”——指标要比上一秒更新,不然大屏就变成静态图了。这背后是实时写入链路在支撑。
在Hologres端,我重点做了几件事:
- 攒批写入。实时计算任务每攒够500条或500ms就批量写入一次,避免高频小请求带来的网络开销,写入吞吐明显提升。
- 幂等去重。消息队列在某些异常场景下会重复投递数据,我在实时任务里加了一个去重逻辑,保证同一笔订单不会被重复计入指标。
- 高峰期写入并行度调整。大促前我会把Hologres的写入shard数增加,让写入吞吐跟上流量;大促结束后再收回来,节省成本。
实测下来,高峰期每秒几万条的写入,Hologres的写入延迟基本稳定在毫秒级,给查询留出了充足的资源缝隙。
4. 常见问题与线上排查实录
方案上线后,不可能一帆风顺。我在这里记录几个典型问题,包括排查思路和最终解法,这些都是踩过坑之后得到的真经验。
4.1 问题一:高峰期写入延迟突然升高
大促当天,Hologres监控面板显示写入延迟从2ms飙到200ms,大屏数据开始有时间差。
排查过程:
- 先看资源水位。CPU和内存使用率确实偏高,但不至于打满,说明不是单纯的资源不足。
- 再看实时计算任务日志。发现任务在高峰期频繁触发背压,导致攒批周期变长。
- 进一步定位,问题出在维表关联上——实时任务每来一条订单,都要去查目的地维表做关联,维表查询延迟被拉高,回压到写入链路。
解决方案:
- 给实时任务加了本地缓存,维表查询命中率提升到90%以上。
- 同时把维表数据在任务启动时预加载到内存,减少运行时查询。
- 写入端增加攒批的条数阈值,让每次写入更大的批次。
调整后,写入延迟恢复到了毫秒级,大屏数据全程追平。
4.2 问题二:大屏出现数据跳变和毛刺
上线第三天,业务方反馈成交额曲线有时候会突然掉下去再弹回来,看着像“数据回退”。
排查过程:
我去对比了实时链路和离线对账链路的数据。实时链路因为存在“事件乱序”——同一个订单的支付事件比下单事件更晚到达,按时间聚合时把支付额算到了下一个分钟,导致前一个分钟的值偏低,后面一个分钟又偏高。
解决方案:
在流式计算中对事件时间做了窗口对齐,同时为Hologres的物化视图增加了“延迟数据容忍窗口”。这样晚到的数据会在窗口内修正,而不是立刻造成曲线跳动。
这个问题的教训是:实时数仓里的时间概念非常讲究,一定要区分“事件时间”和“处理时间”。不处理乱序,大屏曲线就永远会有毛刺。
4.3 问题三:大屏查询超时,尤其集中在某个图表
大促进行到一半,某个“城市排行”图表的查询偶尔超时。
排查过程:
- 单独执行该SQL,发现它带了
ORDER BY gmv DESC,但没有指定分区裁剪条件,导致查询扫描了多个分区。 - 再看物化视图,城市维度没有单独建视图,查询需要实时聚合明细,高峰期资源一紧张就超时。
解决方案:
- 为上榜类指标单独建了一张“城市维度聚合表”,按小时更新。
- 查询SQL里加上了分区条件,只查最近6小时。
- 给该查询在服务层加了一层Redis缓存,降低重复查询频率。
最后大屏所有指标都稳定在了秒级响应,直到大促结束,再没出过超时问题。
5. 另外几个值得说的细节
除了上面的几条主线,还有几个容易被忽略的细节,我觉得对做同类系统的人有参考价值。
5.1 压测一定要按真实比例做
我见过太多团队压测时只用“预估流量的50%”跑,结果上线直接被流量冲垮。正确的做法是,按峰值流量的1.5到2倍做压测,而且压测流量比例要和真实业务一致——比如订单、支付、浏览事件的比例不能差太多,否则会掩盖链路上的瓶颈。
大促前我组织了一次全链路压测,从消息队列到实时计算再到Hologres到大屏服务,全链路一起压。压测发现了两个隐性瓶颈:一个是消息队列消费组的并发不够,另一个是大屏服务连接池偏小。两层都调整后,系统才真正达到“可上线”状态。
5.2 监控体系要能看到“每一环”
大屏系统链路很长,任何一个环节出问题,最终都表现为“大屏卡了”。没有分环节的监控,排查问题就只能靠猜。
我建立了一套分层监控:
| 环节 | 监控指标 | 告警阈值 |
|---|---|---|
| 消息队列 | 消费延迟 | 超过5分钟告警 |
| 实时计算 | 处理延迟、背压率 | 背压率超过60%告警 |
| Hologres | 写入延迟、查询耗时、资源使用 | 查询P99超过3秒告警 |
| 大屏服务 | API响应时间、错误率 | 错误率超过1%告警 |
这套监控上线后,我们最快一次定位问题只用了5分钟,在线下排查效率提升非常明显。
5.3 值班预案比优化更重要
大促当天,再稳的系统也可能出意外。提前准备一份“如果大屏卡了先干什么”的值班手册,比现场研究日志更有效。我的预案里写了:先查哪几个面板、按什么顺序切换备用方案、谁负责通知业务方。这些看似琐碎,但关键时刻能救命。
6. 一些个人经验体会
这套系统从设计到上线大概花了两周时间,核心不在“用了一个厉害的产品”,而在“把实时数仓的思路真正落到业务上”。我个人最大的体会是:实时大屏和离线报表看起来都是“展示数据”,但背后的设计逻辑完全不同。离线报表可以容忍一小时甚至一天的延迟,大屏一秒都忍不了;离线报表查询再慢也有人等着,大屏查询慢了所有人都会注意到。
如果你也在做类似的事情,我建议先别急着写代码,把指标口径和时间语义想清楚。数据链路里百分之八十的问题,都出在“口径不一致”和“时间对不齐”上。把这些定清楚了,后面每一步都会顺畅很多。
另外,物化视图是实时数仓的利器,但也要克制地使用。不要为了追求“快”而建一堆复杂的物化视图,因为每个物化视图都会消耗写入和存储资源。我的原则是:大屏上真实需要的指标才建,宁可查询时多算一点,也不让存储无限膨胀。
最后分享一个小技巧:大屏上线前,可以把大促当天的数据录下来回放一遍,模拟从00:00开始的数据增长过程。这样很多问题会在演练阶段暴露,而不是在真正的战斗中才发现。这个习惯我一直保留着,每次大促前都这么做,基本上线后都能平稳度过。