订阅更新

实时大屏要秒级刷新,但Flink窗口是1分钟——怎么做到'看起来实时'?

数仓项目手册 第 16 题 约 9 分钟读完

本文目录
  1. 先说结论
  2. 端到端延迟拆解
  3. Flink端优化:小窗口 + 提前触发
  4. OLAP端优化:Aggregate模型增量合并
  5. 前端优化:WebSocket推送 + 增量更新
  6. 面试高频追问
  7. 实战踩坑
  8. 总结

先说结论

实时大屏的延迟 = 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年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器