ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

大屏上十几块图表几分钟换一次数,推送方案该怎么定?

大屏上十几块图表几分钟换一次数,推送方案该怎么定? 一、十七块图表挂一天会闪多少次镇上的数字乡村大屏是前年装的一块拼接屏上摆了十七块图表左边是人口和户籍的统计中间是产业和补贴的走势右下角挂着一块实时事件流数据源来自六个业务库更新频率大多是三分钟一次事件流那块是十秒一次。展厅对外开放讲解员早上开机晚上闭馆才关中间大概要连续挂九个小时中途还有巡展和视频会议画面只要一抖站在前面的人都能看出来。最早那版前端用的是全屏定时刷新每块图表各自定一个定时器五秒拉一次接口拿到数据就重新渲染整块组件。按十七块图表算每分钟要发出两百零四次请求九个小时下来大约十一万次而业务库三分钟才变一次数也就是说这些请求里有九成九拿到的是和上一次完全一样的数据。展厅的访客感觉不到可后台的接口调用量一直居高不下夜里跑批的时候还会撞到一起。比请求量更让人头疼的是闪烁。图表重新渲染时会先清空画布再画新的一帧折线从零点重新拉出来柱状图的入场动画也重放一次十七块轮流闪整面墙看起来像在呼吸。有一次县里的领导临时来参观讲解员刚把鼠标移到其中一块饼图上那块图正好被刷新清空悬浮提示直接消失了。这件事之后我们才下决心把刷新方式改掉。二、要更新的其实只有很小的比例把数据更新的规律摸清楚之后问题变得很具体。六个数据源里人口和户籍这两块是日更的一天只变一次产业和补贴是分钟级的三分钟一批事件流更新得勤十秒一条。也就是说十七块图表里真正需要高频更新的只有三块剩下十四块一天动一次就够了用一个统一的五秒轮询去覆盖全部等于让十四块安静的图表陪着三块频繁的图表空转。整屏刷新的成本也不只是网络请求。每次重建图表实例ECharts 都要重新计算坐标轴刻度、重新布局标签、重新绑事件一块折线图的完整初始化大约要十几毫秒十七块加起来两百多毫秒这期间主线程被占着页面上的滚动、点击都会卡顿一下。展厅那台一体机是四年前的配置CPU 在刷新瞬间能冲到七成以上风扇声明显变响。所以我们要的其实不是更快而是更准。让发生变化的数据自己去通知关心它的那块图表没变化的数据一次都不要碰同时让变化本身不引起整块画布的重建。这个目标拆开来一共有三个约束更新必须落在数据集这一层而不是页面这一层渲染必须是就地改数据而不是销毁重建连接必须能在展厅网络抖动之后自己恢复并且不丢状态。三、轮询、推送与混合三条路第一条路是把轮询做精细一点按图表的更新频率分别设定时器日更的十分钟拉一次分钟级的三十秒拉一次再加一个 ETag 或者版本号做条件请求没变化就返回三零四。这条路改动量小两天就能上线代价是空请求依然存在只是频次降了下来而且每块图表的轮询节奏互不相干同一时刻可能有三四个请求并发接口压力分布得并不均匀。第二条路是把长连接建起来服务端发现有数据变化就主动推给前端。这条路实时性占优事件流那块可以做到秒级到达缺点是前端要自己处理断线重连、消息乱序、重复推送这些事服务端也要为每个连接维护一份订阅关系。展厅的网络走的是无线网桥中间隔着两层交换机每天都会有那么几次瞬断重连逻辑要是写得潦草图表就停在旧数据上不动了。第三条路是混合指标类数据走推送明细类数据走轮询。我们最后选的就是这条。理由有两个一是明细数据量普遍偏大比如某一次补贴发放的明细一次可能有几百行用长连接推过来反而占带宽让用户在点开的时候按需拉一次更划算二是明细的实时性要求本来就低晚几十秒没有人会介意。指标类数据则反过来量小、更新频繁、对及时性敏感正好适合推送。四、按数据集粒度推送服务端的模型很简单一份数据对应一个数据集数据集有一个稳定的标识比如人口概况叫 ds_population产业走势叫 ds_industry事件流叫 ds_events。业务方写数据之后不需要知道有哪些大屏在看着它只要调一次变更通知接口把数据集标识和自己的版本号传进来推送服务负责把这条消息分发给所有订阅了这个数据集的连接。这套推送在万村乐数字乡村的大屏上跑了很久屏幕挂一天也不用手动刷新中途偶尔断一次网也是自己接回来的。前端拿到的消息里带着数据集标识它先在自己的图表注册表里查一下有没有订阅这个标识的组件有就把新数据交给那个组件让它就地更新没有就直接丢弃不做任何兜底刷新。注册表里每一块图表登记的订阅关系在组件挂载时建立卸载时解除。就地更新这一步用了 ECharts 的 setOption第二个参数里把 notMerge 设成 false让新旧配置按字段合并而不是整体替换同时把 lazyUpdate 设成 true把重绘推迟到下一帧。折线图只更新 series 里的 data 数组坐标轴、图例、提示框的配置一个字都不动所以图表的入场动画不会重放鼠标悬停的提示框也不会被清掉。改完之后展厅那面墙彻底安静下来只有真正变数的三块会轻轻动一下。五、消息格式与重连参数推送消息用一份紧凑的 JSON字段一共五个ds 是数据集标识seq 是服务端自增的推送序号ts 是生成时间戳ver 是数据版本号rows 是本次变更的行数组。seq 的用途是给前端排查顺序客户端记住每一条连接上收到过的 seq收到比当前小的就直接丢弃避免网络乱序把一个旧值写回图表。ver 用来跟轮询接口对齐同一次变更在推送和轮询里拿到的是同一个版本。重连参数定了一张固定的退避表第一次断开等一秒之后按二倍递增依次是两秒、四秒、八秒、十六秒封顶三十秒每次等待时间再叠加正负两成的随机抖动避免十几台大屏在同一个瞬间一起重连把服务端冲垮。重连成功之后客户端主动发一条 resync 请求服务端回一份全量快照前端用它把订阅过的数据集全部覆盖一遍然后再恢复增量接收。六、踩过的三个坑第一个坑是重连之后会漏消息。现象是展厅的网络瞬断了大概四十秒恢复之后事件流那块图表还停在三分钟前的数据上一直不动手动刷新一下页面才跟上。根因是我们只推增量断开的那段时间里服务端产生的变更没有人接收重连的时候也没有补发的动作前端自然就不知道自己缺了什么。改法是在重连成功后由客户端发一次 resync服务端按数据集回全量快照前端整块替换把缺掉的中间过程直接跳过。第二个坑是服务端的推送线程被一台慢客户端拖住。现象是某台大屏所在网段的交换机重启那台机器上的浏览器没有及时读走数据结果另外两台大屏的数据也跟着停了十几秒。根因是推送线程直接把消息写进网络套接字写操作在缓冲区满的时候会阻塞一个连接卡住就把整条推送线程占死了。改法是每个连接配一个长度两百五十六的发送队列和一个独立的写线程队列满了就丢最旧的那条因为增量消息本身可以丢客户端重连时会补齐。第三个坑是大屏长时间运行内存缓涨。现象是新版推送上线之后一体机上的浏览器内存从三百八十兆一路涨到一点二吉挂到第六个小时开始出现明显的卡顿切页面的响应肉眼可见地变慢。根因是早期那版更新逻辑每次都先 dispose 再 new 一个图表实例ECharts 实例、绑在画布上的事件监听、还有动画的帧回调都没有被完整回收。改法是实例复用一块图表从头到尾只有一个实例数据更新只走 setOption同时把不再使用的旧数据行显式置空内存曲线回到四百兆上下并且不再持续上涨。七、这层解决不了什么推送能管的是消息及时送到管不了数据本身对不对。上游业务库如果写进去的是错的数我们只会把错的数更快地画到大屏上。我们做过一个折中的办法推送消息里带一个来源标识前端在角落显示一块很小的更新时间戳讲解员一眼能看出来这块图的数据是几点几分进来的出现异常时可以当场判断是数据源没跑还是画面没更新。长连接也不是所有场景都合适。局域网内部的展厅用起来很舒服可一旦大屏被放到公网环境中间的网关、防火墙、反向代理都可能把连接掐掉有的代理默认六十秒无流量就断开需要靠心跳维持。我们目前的心跳间隔定在二十五秒留了一点冗余同时把心跳包做到只有几个字节不给带宽添负担。这些参数在每一处部署环境里都得重新验证一遍。订阅关系的容量也有边界。我们现在的推送服务单机能稳定承载八百个连接超过之后需要按数据集做分片让不同的连接落到不同的实例上。这套方案在大屏这个场景里够用因为一个县里的大屏数量本来就不多可要是拿去做面向公众的实时推送连接数会涨两个数量级那时候要换的就是另一套架构了跟现在这套不是一个量级的问题。八、小结这套增量推送是万村乐数字乡村里仅有的一块长连接代码改过三次重连策略就稳定了现在最长的记录是一台一体机连续挂了十一天没有人工干预。真正的收益不在实时性上而在安静上十七块图表里只有三块会动画面上不再有那种整屏一起闪的动静讲解员可以放心地把鼠标停在任意一块图上。回头看我们改掉的最关键的一件事是把更新的粒度从页面降到了数据集其次是让前端只在收到订阅的数据集变更时才动手。这两个决定加起来把大屏的请求量从每天十一万次压到了三万次以内服务端推送的峰值也稳定在每分钟六十条上下。真正的难点从来不是让数据动起来而是让不该动的地方一点都不动。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进