告别直播卡顿:我们如何用 Flink SQL 打造秒级监控大盘

87 阅读9分钟

背景

不久前公司刚上线一个直播APP,需要关注性能相关的指标。 需求就是构建一个准实时的直播视频平台性能监控数据大盘。

这个大盘的目标用户主要是 运维工程师 (SRE)、产品经理和开发工程师。它通过实时计算客户端上报的性能日志,提供一个全局的、秒级更新的 用户体验 (QoE) 和服务质量 (QoS) 视图。

1. 具体工作流程

  1. 数据采集:客户端(App或网页播放器)在用户观看直播时,将关键性能事件(如开始播放、首帧渲染、播放中卡顿等)上报到 Kafka。
  2. 实时处理:Flink 作业消费这些日志,进行实时清洗和解析。
  3. 指标聚合:通过一个 5分钟的滑动窗口,每5秒计算一次,聚合出核心性能指标。
  4. 数据存储:将聚合结果写入 Doris
  5. 数据可视化:最后,他们那边数据可视化工具会连接到 Doris,将这些指标以图表的形式展现在监控大盘上。

2. 核心指标分析及关注原因

下表详细解释了代码中计算的核心指标及其业务价值。

指标 (Metric)计算逻辑业务含义为什么重要?
play_total_uv触发 performance_live_play_event 事件的去重用户数总播放用户数这是衡量平台活跃度的基础。所有后续的“率”指标都以此为分母,它定义了分析问题的范围。如果这个指标本身暴跌,说明是更严重的问题(如入口失效、大面积服务不可用)。
first_render_fail_uv触发 first_render_frame_event 事件且 code 不为 '0' 的去重用户数首帧失败用户数有多少用户点击播放后,连视频的第一个画面都没能成功看到。这是用户体验的“第一道门槛”。
success_rate(1 - 首帧失败用户数 / 总播放用户数) * 100播放成功率这是衡量直播可用性的 核心黄金指标。如果用户连播放都打不开,后续的一切体验都无从谈起。成功率的任何微小下降都值得警惕,因为它直接影响到用户流失。
vfreeze_uv触发 quality_event 事件且 vfreeze_count > 0 的去重用户数发生视频卡顿的用户数在一次完整的观看过程中,有多少用户经历了至少一次的视频画面静止(即“卡了”)。
vfreeze_rate(卡顿用户数 / 总播放用户数) * 100卡顿率这是衡量直播流畅度的 核心黄金指标。卡顿是用户观看直播时最不能忍受的体验问题之一。高卡顿率会严重影响用户满意度和留存率,是性能优化的重点关注对象。
total_gbpsvbitrate (视频码率) + abitrate (音频码率) 的总和总消耗带宽这个指标反映了实时的带宽成本和用户的清晰度体验。运维可以根据它进行容量规划和成本控制;产品和开发则可以分析码率策略是否合理,是否在清晰度和流畅度之间取得了最佳平衡。

3. 技术实现

该监控大盘的核心是一个 Flink SQL 流处理作业。它直接消费 Kafka 中的原始日志,通过一系列的转换和聚合,最终将高价值的性能指标写入 Doris 数据库。以下是关键技术点的解析:

3.1 Flink SQL 性能调优

为了确保监控的准实时性(5秒更新)和计算效率,我们在 Flink 作业中实施了多项关键的性能优化,这些优化主要针对高基数(大量独立用户)场景下的聚合计算:

  • 开启 Mini-Batch 和 Local-Global 聚合

    • 配置:

      • table.exec.mini-batch.enabled: true
      • table.exec.mini-batch.allow-latency: 5s
      • table.optimizer.agg-phase-strategy: TWO_PHASE
    • 原理与价值: 我们开启了微批处理(Mini-Batch),让 Flink 缓存5秒的数据再进行处理。这极大地减少了对状态后端的访问次数。同时,我们启用了 TWO_PHASE 聚合策略,即“Local-Global 聚合”。Flink 会先在各个 TaskManager 上对本地数据进行一次预聚合,然后才将预聚合的结果发送到下游进行全局聚合。这两个参数的组合,可以极大地减少网络 shuffle 的数据量和状态的读写次数,显著提升聚合性能。

  • COUNT DISTINCT 性能优化

    • 配置: table.optimizer.distinct-agg.split.enabled: true
    • 原理与价值: 在我们的场景中,几乎所有核心指标(如 play_total_uv, vfreeze_uv)都涉及到对用户ID的去重计数(COUNT(DISTINCT uid))。这是一个非常消耗资源的算子,极易在数据倾斜时产生性能瓶颈。通过启用此参数,Flink 会将 COUNT DISTINCT 操作拆分为两个阶段:首先,将数据根据 uid 进行 shuffle,并在第一层聚合中去除重复值;然后,在第二层聚合中进行简单的 COUNT 计数。这种方式将压力分摊到多个节点,有效避免了单点瓶颈,是处理大规模去重计数的关键优化。需要注意的事,参数需要依赖 mini-batch 参数的开启。
给大家看下优化开启前后 flink DAG 的变化
  • 开启前
  • 开启后

其实这里的优化思路和离线处理 count(distinct)的思路是一样的,只不过我们不需要去更改 flink sql 解决 count(distinct),所以 flink 的这个参数还是很方便的,而且开启后,我把任务的资源分配给到最小依然无压力

3.2 SQL 核心逻辑

  1. 数据源定义 (Source) :

    • 创建一个 Kafka 表 client_log,直接消费原始日志流。
    • 使用 WATERMARK 定义事件时间,为后续的窗口计算提供时间基础。
    CREATE TABLE client_log (
      raw_log STRING,
      ts TIMESTAMP(3) METADATA FROM 'timestamp',
      WATERMARK FOR ts AS ts
    ) WITH (
      'connector' = 'kafka',
      'topic' = 'your-business-log-topic'-- 已脱敏
      'properties.bootstrap.servers' = 'kafka-server-1:9092,kafka-server-2:9092'-- 已脱敏
      'properties.group.id' = 'your-flink-group-id'-- 已脱敏
      'scan.startup.mode' = 'latest-offset',
      'format' = 'raw'
    )
    
  2. 数据清洗 (ETL) :

    • 通过一个临时的 etl 视图对数据进行预处理。
    • 首先过滤出与性能统计相关的三类事件 (performance_live_play_event, first_render_frame_event, quality_event)。
    • 使用 get_json_object 函数从复杂的 JSON 日志体中提取出 uid, event, code, vfreeze_count 等关键字段,并将它们转换为正确的类型。这一步将非结构化的数据转化为了干净、扁平的结构化数据,为后续聚合做好了准备。
    SELECT
      get_json_object(data,'$.uid'AS uid,
      get_json_object(data,'$.event'AS event,
      CAST(get_json_object(get_json_object(data,'$.extend_long'), '$.code'AS STRING) AS code,
      CAST(get_json_object(get_json_object(data,'$.extend_long'), '$.vfreeze_count'AS BIGINTAS vfreeze_count,
      CAST(get_json_object(get_json_object(data,'$.extend_long'), '$.vbitrate'AS BIGINTAS vbitrate,
      CAST(get_json_object(get_json_object(data,'$.extend_long'), '$.abitrate'AS BIGINTAS abitrate,
      ts
    FROM (
      SELECT
         SPLIT(raw_log, '|')[5AS data,
         ts
      FROM client_log
      -- 过滤出平台日志,并指定事件名
      WHERE SPLIT(raw_log, '|')[1= 'platformLog_your-app-client_performance-log' -- 已脱敏
        AND JSON_VALUE(SPLIT(raw_log, '|')[5], '$.event'IN (
          'performance_live_player_first_render_frame_event',
          'performance_live_play_event',
          'performance_live_player_quality_event'
        )
    ) t
    
  3. 指标聚合 (Aggregation) :

    • 这是整个流程的核心。我们使用 HOP 滚动窗口函数,定义了一个窗口长度为5分钟,滑动步长为5秒的滑动窗口,完美匹配了业务需求。
    • GROUP BY 子句中,通过 COUNT(DISTINCT CASE WHEN ...) 的标准 SQL 技巧,在一个查询内高效地计算出所有需要的 _uv 指标。
    • success_ratevfreeze_rate 等比率指标也在这里同步计算完成。
    SELECT
      -- 计算播放总UV
      count(distinct CASE WHEN event = 'performance_live_play_event' THEN uid ELSE NULL ENDAS play_total_uv,
      -- 计算首帧失败UV
      count(distinct CASE WHEN event = 'performance_live_player_first_render_frame_event' AND coalesce(code, '0'<> '0' THEN uid ELSE NULL ENDAS first_render_fail_uv,
      -- 计算卡顿UV
      count(distinct CASE WHEN event = 'performance_live_player_quality_event' AND vfreeze_count > 0 THEN uid ELSE NULL ENDAS vfreeze_uv,
      -- 计算播放成功率和卡顿率
      ROUND(...) AS success_rate,
      ROUND(...) AS vfreeze_rate,
      -- 计算总带宽
      SUM(coalesce(vbitrate, 0+ coalesce(abitrate, 0)) AS total_gbps,
      -- 定义5分钟滑动窗口,每5秒计算一次
      HOP_START(ts, INTERVAL '5' SECONDINTERVAL '5' MINUTES) AS window_start,
      HOP_END(ts, INTERVAL '5' SECONDINTERVAL '5' MINUTES) AS window_end
    FROM etl
    GROUP BY HOP(ts, INTERVAL '5' SECONDINTERVAL '5' MINUTES)
    
  4. 数据宿 (Sink) :

    • 将最终计算出的指标结果写入到 Doris 表 jaco_performance_live_metrics 中。
    • 通过配置 sink.buffer-flush.interval='5s',确保每5秒钟就有新的数据被刷新到 Doris,从而保证了监控大盘的准实时性。
    CREATE TEMPORARY TABLE doris_output (...) WITH (
        'connector' = 'doris',
        'fenodes' = 'doris-fe-node1:8031,doris-fe-node2:8031'-- 已脱敏
        'username' = 'your_username'-- 已脱敏
        'password' = 'your_password'-- 已脱敏
        'table.identifier' =  'your_db.your_table'-- 已脱敏
        'sink.enable.batch-mode'='true',
        'sink.buffer-flush.interval'='5s'
    );
    
    -- 将聚合结果插入Doris
    INSERT INTO doris_output
    SELECT * FROM dws_total
    

总结

总而言之,这段 Flink 代码的商业价值在于它构建了一个数据驱动的决策系统

  • 对运维/SRE: 它提供了一个灵敏的“战场雷达”。success_rate 的下降或 vfreeze_rate 的飙升都会在5秒内反映到大盘上,触发告警,使团队能在故障影响扩大前迅速响应,将问题扼杀在摇篮里。
  • 对产品经理: 它量化了抽象的用户体验。新功能或播放器优化策略上线后,是好是坏不再凭感觉,而是通过 success_ratevfreeze_rate 的具体升降来客观衡量,为产品迭代提供了坚实的数据依据。
  • 对开发工程师: 它提供了精准的问题诊断线索。first_render_fail_uv 增多,客户端同学可以立即聚焦排查首帧逻辑;vfreeze_rate 升高,则清晰地指向了流媒体传输、CDN 或后端服务等潜在瓶颈。

这个系统将海量、原始的客户端日志,通过实时的流式计算,提炼成了能够直接指导运维、驱动产品迭代、并为研发提供线索的高价值业务洞察

更多 Flink / Spark / Doris / Paimon 实战文章,请关注公众号 「数据慢想」

往期文章推荐