实时大屏要秒级刷新,但Flink窗口是1分钟——怎么做到'看起来实时'?
先说结论
实时大屏的延迟 = Flink处理延迟 + OLAP写入延迟 + 前端刷新延迟。三个环节都要优化,不能只优化一个。
'看起来实时'的核心技巧:① Flink用小窗口(10秒滚动窗口)做预聚合;② OLAP引擎用Aggregate模型做增量合并;③ 前端用WebSocket推送代替轮询。
不是所有指标都需要秒级:PV/UV等趋势类指标1分钟更新就够了,只有关键操作指标(实时花费、实时收入)才需要秒级。分级处理可以节省大量资源。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
业务方看实时大屏时最常说的一句话是'这个数是实时的吗?怎么感觉有延迟?'。实际上大多数'实时大屏'并不是真正的毫秒级实时——Flink的窗口聚合是分钟级的,OLAP引擎的查询也有几百毫秒延迟,前端的轮询间隔也要5-10秒。那怎么做到让业务方觉得'这就是实时的'?我在某短剧出海公司做过广告ROI实时大屏,从最初的'分钟级更新'优化到'5秒级感知更新',今天把方案讲清楚。
端到端延迟拆解
| 环节 | 优化前 | 优化后 | 优化手段 |
|---|---|---|---|
| Flink处理 | 60秒(1分钟窗口) | 10秒(10秒窗口) | 缩小窗口 + 提前触发 |
| OLAP写入 | 5-10秒(批量写入) | 1-2秒(实时Upsert) | Hologres实时写入模式 |
| 前端刷新 | 10秒(HTTP轮询) | 1-2秒(WebSocket推送) | WebSocket + 增量更新 |
| 端到端总延迟 | 75-80秒 | 12-15秒 | 三个环节同时优化 |
Flink端优化:小窗口 + 提前触发
最直接的优化是缩小Flink的聚合窗口。但窗口太小会导致输出的数据量暴增(1分钟窗口每分钟输出1条,10秒窗口每分钟输出6条),下游OLAP压力变大。所以要配合OLAP的Aggregate模型使用:
-- Flink SQL:10秒滚动窗口预聚合
INSERT INTO dws_channel_roi_10s -- 写入Hologres/Doris
SELECT
TUMBLE_START(event_time, INTERVAL '10' SECOND) AS window_start,
channel,
country_code,
COUNT(*) AS click_pv,
COUNT(DISTINCT user_id) AS click_uv, -- 10秒内的UV
SUM(CASE WHEN is_paid = 1 THEN pay_amount ELSE 0 END) AS revenue
FROM dwd_ad_click_stream
GROUP BY
TUMBLE(event_time, INTERVAL '10' SECOND),
channel,
country_code;
-- 注意:10秒窗口的UV不能直接SUM得到1分钟UV
-- 因为同一个用户可能出现在多个10秒窗口里
-- UV的精确计算需要在查询层用HyperLogLog或Bitmap去重
UV精确性问题:小窗口的UV不能简单相加——同一用户可能跨窗口。解决方案有两个:① OLAP引擎用Bitmap去重(Doris的BITMAP_UNION);② 牺牲精度用HyperLogLog近似去重(误差约1-2%,但性能极好)。我们的实时大屏用HyperLogLog,离线报表用精确去重——实时要速度,离线要精度。
OLAP端优化:Aggregate模型增量合并
Flink每10秒输出一批聚合结果,OLAP引擎需要把多个10秒窗口的结果合并成分钟级/小时级的指标。这里用Doris的Aggregate模型或Hologres的实时写入模式:
-- Doris Aggregate模型:自动合并多个小窗口的结果
CREATE TABLE dws_channel_roi_realtime (
stat_minute DATETIME, -- 分钟级时间戳
channel VARCHAR(64),
country_code VARCHAR(16),
click_pv BIGINT SUM, -- SUM聚合:自动累加
click_uv_hll HLL HLL_UNION, -- HLL聚合:自动合并去重
revenue DECIMAL(12,2) SUM -- SUM聚合:自动累加
) AGGREGATE KEY(stat_minute, channel, country_code)
DISTRIBUTED BY HASH(channel) BUCKETS 16;
-- Flink每10秒写入一批数据
-- Doris自动按stat_minute+channel+country_code聚合
-- 查询时:同一分钟的多个10秒窗口自动合并为1行
-- click_pv自动SUM,click_uv_hll自动HLL_UNION
这种方案的好处是:Flink只管往OLAP写小窗口的预聚合结果,OLAP引擎自动做二次聚合。前端查询时直接查分钟级或小时级的结果,不需要关心底层是10秒窗口还是1分钟窗口。
前端优化:WebSocket推送 + 增量更新
传统的大屏前端用HTTP轮询——每10秒请求一次API拉最新数据。问题是:① 有无效请求(数据没变也请求);② 最坏情况下延迟=轮询间隔(10秒)。用WebSocket可以做到数据更新后立即推送给前端:
// 前端更新方案对比
// 方案一:HTTP轮询(简单但延迟高)
// 前端每10秒请求一次:GET /api/realtime/roi
// 延迟:0-10秒(取决于轮询时机)
// 优点:实现简单
// 缺点:无效请求多、延迟不可控
// 方案二:WebSocket推送(推荐)
// 后端监听OLAP表的变更,有新数据时主动推送
// 延迟:1-2秒(数据写入OLAP后立即推送)
// 优点:延迟低、无无效请求
// 缺点:需要WebSocket服务器
// 方案三:SSE(Server-Sent Events)
// 单向推送,比WebSocket轻量
// 适合只需要服务端推送、不需要双向通信的场景
// 我们的大屏用SSE——因为大屏只展示不交互
面试高频追问
追问一:实时大屏的数据和离线报表对不上怎么办?
一定会对不上,原因在前面第6篇已经详细讲过——窗口边界、延迟数据、HLL近似去重都会导致差异。我们的做法是在大屏上明确标注'实时数据(预估)',让业务方知道这是预估值不是精确值。同时每天凌晨离线链路跑完后自动覆盖实时数据,保证T+1的数据是精确的。业务方对这个方案很接受——实时大屏看趋势和方向,精确数据看离线报表。
追问二:实时大屏的QPS怎么控制?很多人同时看会不会把OLAP打挂?
这是一个容易被忽视的问题。如果30个人同时盯着大屏,每人每5秒刷新一次,OLAP引擎每秒就要处理6个查询。应对方案是加一层缓存:后端服务每隔5秒查一次OLAP把结果缓存到Redis,所有前端请求都从Redis读缓存。这样不管多少人看大屏,OLAP的压力始终是'每5秒1次查询',不会随观看人数增长。
追问三:大屏上的数据突然掉零了怎么处理?
这通常是Flink作业挂了或者OLAP写入失败。我们做了三个保障:① 前端做'零值保护'——如果最新数据比上一次小于50%以上,前端展示上一次的值并标记'数据加载中',不直接展示0;② 后端做健康检查——每分钟检查实时表最近5分钟有没有新数据写入,没有就触发告警;③ 降级方案——实时数据异常时自动切换展示离线T-1的数据,保证大屏不会空白。
实战踩坑
我在某短剧出海公司做实时大屏时遇到过一个有意思的问题:大屏上广告花费金额每隔10秒跳一下,不是平滑增长的。业务方觉得看着不专业,'正经的实时大屏应该是数字平滑滚动的'。
原因很简单——我们的Flink窗口是10秒,每10秒输出一次聚合结果,所以大屏上的数字也是每10秒跳一次。解决方案是前端做'数字补间动画'——收到新数据后,前端把旧数值平滑过渡到新数值(用requestAnimationFrame做数字滚动动画),视觉上看起来就是平滑增长的。这是一个纯前端的优化,但效果非常好——业务方觉得'这才像实时的'。
JavaScript
// 前端数字补间动画(伪代码)
function animateNumber(element, fromValue, toValue, duration) {
const startTime = performance.now();
const diff = toValue - fromValue;
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// 使用easeOutCubic缓动函数,让数字变化更自然
const eased = 1 - Math.pow(1 - progress, 3);
const currentValue = fromValue + diff * eased;
element.textContent = formatNumber(currentValue);
if (progress < 1) requestAnimationFrame(update);
}
requestAnimationFrame(update);
}
// 每次收到新数据时调用:animateNumber(el, oldValue, newValue, 2000)
// 2秒内从旧值平滑过渡到新值
总结
实时大屏的'秒级感知'需要Flink、OLAP、前端三端协同优化。Flink用小窗口预聚合,OLAP用Aggregate模型自动合并,前端用WebSocket推送+数字补间动画。面试时讲实时大屏,重点说清楚:端到端延迟怎么拆解的、每个环节怎么优化的、UV精确性怎么权衡的。如果能再说出'零值保护'和'降级方案'这些细节,说明你考虑问题很全面。
实时大屏搞定了,下一篇聊出海业务的经典难题——跨时区数据处理,10+个国家的数据怎么统一?
你们的实时大屏延迟是多少秒?用了什么前端刷新方案?评论区聊聊~
关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长