实时大屏项目复盘:从需求沟通到性能优化的全流程记录

📅 2026/7/22 0:20:58
实时大屏项目复盘:从需求沟通到性能优化的全流程记录
实时大屏项目复盘从需求沟通到性能优化的全流程记录老板说下周高管会要展示一块实时数据大屏于是数据分析师就开启了为期两周的全栈之旅——从需求沟通、数据对接、接口开发到性能调优每个环节都有意想不到的坑。今天做一次完整复盘。一、需求沟通你以为的大屏可能不是同一个东西项目启动会上业务方给我发了一张参考图就类似这种数据要实时刷新。我一看好家伙——地图热力、跑马灯、动态排名、环形图、趋势折线……粗略一数至少 12 个图表模块。这时候不能直接说行我开干了。需求沟通的第一要务是对齐实时的定义。最终对齐的结果是核心指标GMV、订单量、UV需要5 秒级刷新图表数据30 秒级刷新地图热力数据1 分钟刷新即可。这直接决定了技术方案——不同刷新频率的数据走不同通道。还有一个被忽略但致命的细节大屏要展示的是当日累计还是实时瞬时业务方原话是展示实时数据但实际上他们想要的是今日截至当前的累计值而不是这一刻正在发生的值。一字之差数据逻辑天差地别。二、数据层ClickHouse 物化视图加速大屏的查询场景非常固定——都是按分钟/小时粒度的聚合查询。如果用 MySQL 直接查明细表大屏刷新一次可能要跑十几秒。ClickHouse 在这种场景下是更合适的选择。-- ClickHouse 物化视图每分钟自动聚合订单数据 CREATE MATERIALIZED VIEW mv_order_min_agg ENGINE AggregatingMergeTree() PARTITION BY toYYYYMMDD(minute_time) ORDER BY (minute_time, category_id) POPULATE -- 自动回填历史数据 AS SELECT toStartOfMinute(order_time) AS minute_time, category_id, -- 订单量 countState() AS order_count_state, -- GMV 求和 sumState(order_amount) AS gmv_state, -- 用户数去重 uniqState(user_id) AS uv_state FROM orders GROUP BY minute_time, category_id; -- 查询时使用 Merge 后缀函数合并聚合状态 SELECT minute_time, category_id, countMerge(order_count_state) AS order_count, sumMerge(gmv_state) AS gmv, uniqMerge(uv_state) AS uv FROM mv_order_min_agg WHERE minute_time today() GROUP BY minute_time, category_id ORDER BY minute_time DESC;物化视图的好处是数据写入时自动完成聚合查询时直接读结果不需要每次刷新大屏都扫明细表。实测下来12 个图表模块的所有查询从原来的 8-15 秒压缩到了 300ms 以内。三、接口层并发请求 分级缓存大屏前端的 12 个图表模块如果串行请求总耗时是sum(每个接口耗时)用户体验极差。前后端分离后前端可以并发请求但后端要顶住 12 个查询同时打过来的压力。import asyncio import aiomysql from functools import lru_cache import time class DashboardService: 大屏数据服务 def __init__(self): self.cache {} # 简单内存缓存 async def fetch_all_dashboard_data(self): 并发获取所有大屏模块数据 tasks [ self.get_gmv_trend(), # GMV 趋势 self.get_order_ranking(), # 品类排行 self.get_uv_realtime(), # 实时 UV self.get_map_heatmap(), # 地图热力 self.get_conversion_funnel(),# 转化漏斗 self.get_top_products(), # 热销商品 ] # asyncio.gather 并发执行所有查询 results await asyncio.gather(*tasks, return_exceptionsTrue) # 组装返回对异常的模块返回占位数据 module_names [gmv_trend, order_ranking, uv, heatmap, funnel, top_products] response {} for name, result in zip(module_names, results): if isinstance(result, Exception): response[name] {error: str(result), data: None} else: response[name] result return response def get_gmv_trend(self): GMV 趋势 —— 30 秒缓存 cache_key gmv_trend if cache_key in self.cache: cached_time, cached_data self.cache[cache_key] if time.time() - cached_time 30: # 缓存 30 秒 return cached_data # 执行查询并更新缓存 data self._query_gmv_from_clickhouse() self.cache[cache_key] (time.time(), data) return data这里踩的最大的坑是Python 的asyncio和 ClickHouse 驱动的兼容性。ClickHouse 的 Python 客户端clickhouse-driver默认是同步的需要在线程池中执行。我们后来换成了asynchClickHouse 官方异步客户端性能提升了约 40%。四、性能优化与上线压测阶段发现两个瓶颈瓶颈一数据库连接池耗尽。12 个并发查询 5 秒刷新频率加上同时可能有多个大屏页面打开投屏、PC、平板连接数瞬间打满。解决方案是将连接池从默认的 5 提升到 50并增加连接超时回收机制。建议同时给连接池加上慢查询日志便于上线后快速定位是哪个查询吃掉了连接。瓶颈二前端渲染性能。地图热力图在数据量超过 5000 个点后出现明显卡顿。解决思路是在服务端做数据抽稀——只返回前 2000 个热点def downsample_heatmap(points, max_points2000): 地图热力点抽稀保证前端渲染流畅 if len(points) max_points: return points # 按热度值降序排列保留前 max_points 个 points_sorted sorted(points, keylambda x: x[value], reverseTrue) return points_sorted[:max_points]上线当天的监控有必要设好——特别是数据库慢查询、接口响应时间和错误率import logging import time from functools import wraps def monitor_api(api_name): API 性能监控装饰器 def decorator(func): wraps(func) async def wrapper(*args, **kwargs): start time.perf_counter() try: result await func(*args, **kwargs) elapsed time.perf_counter() - start # 记录接口耗时超过 1 秒告警 if elapsed 1.0: logging.warning( f[{api_name}] 响应超时: {elapsed:.2f}s ) return result except Exception as e: elapsed time.perf_counter() - start logging.error( f[{api_name}] 异常: {e}, 耗时: {elapsed:.2f}s ) raise return wrapper return decorator五、总结大屏项目看起来是做一个展示页面但实际上考验的是数据流全链路的工程能力。从需求沟通阶段对实时定义的澄清到数据层物化视图的预聚合再到接口层的并发优化和分级缓存以及上线后的性能监控——缺少任何一个环节都会翻车。最重要的教训只有一条**大屏的性能瓶颈永远在数据层和查询层而不是前端渲染层。**用对 ClickHouse 的物化视图比在前端做任何优化都管用十倍。最后提醒一点这个方案在上生产之前建议先用灰度流量验证一周确认资源消耗在预期范围内再全量推送。我们在实际项目中因为跳过了这步有一次把缓存集群打挂了教训深刻。