十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

08-时序数据可视化与异常分析:让数据开口说话

08-时序数据可视化与异常分析:让数据开口说话 时序数据可视化与异常分析让数据开口说话大家好我是黒漂技术佬。前面几篇我们把数据采了、存了、查了但你有没有发现一个问题——这些数据全是数字。运营人员对着满屏的26.3, 26.5, 27.1, 29.8...看半天也看不出啥名堂。但如果你把同样的数据画成一条曲线温度从26度突然飙到30度的那个拐点一眼就能看到。这就是可视化的力量。一、时序数据可视化的价值人脑对数字的感知能力很弱对图形的感知能力极强。同样一组温度数据26.3, 26.5, 26.7, 27.1, 28.3, 30.1, 32.5, 33.8, 33.2, 31.5, 29.8, 28.2你能一眼看出哪个时刻温度开始异常吗很难。但画成折线图那个陡峭的上升曲线直接跳到你眼前。可视化在工控物联网场景中至少解决三个问题实时监控运维人员一眼掌握所有设备运行状态不用挨个查数字趋势分析长期数据画成曲线设备老化趋势、季节性规律一目了然异常发现数据可视化后异常值往往表现为曲线上的尖峰、断崖或平台比数值阈值告警更直观二、可视化方案选型方案一Grafana 对接 InfluxDB主流方案Grafana 是开源的数据可视化平台和 InfluxDB 是天作之合。优点是配置即图表几乎不用写代码缺点是图表样式定制能力有限且独立于你的业务系统。适合场景运维监控大盘、设备实时状态看板。方案二SpringBoot ECharts 自研报表ECharts 是百度开源的图表库现在归Apache了通过 JavaScript 渲染各种图表。后端用 SpringBoot 查 InfluxDB 返回 JSON前端用 ECharts 绘图。优点是完全可控能和业务系统深度集成缺点是要自己写前后端代码开发量大。适合场景面向业务方的运营报表、需要和订单/库存数据联动的综合看板。两种方案的对比维度GrafanaSpringBoot ECharts开发成本低配置为主高需开发前后端图表丰富度高内置几十种面板中ECharts也够用定制能力中模板变量、告警规则高完全自主控制业务集成弱独立系统强嵌入业务页面告警能力内置Alert规则需自研典型场景运维监控业务报表实际项目中两种方案经常并用Grafana 给运维团队用ECharts 看板给业务团队用。三、Grafana 对接 InfluxDB安装 Grafana# Docker方式最简单dockerrun-d--namegrafana\-p3000:3000\grafana/grafana:latest# 默认账号密码: admin / admin启动后访问http://localhost:3000用默认账号登录。配置 InfluxDB 数据源进入 Grafana → Configuration → Data Sources → Add data source选择InfluxDB填写关键配置Query Language: Flux必须选Flux不是InfluxQL URL: http://localhost:8086 Authentication: 勾选 With Credentials或在下面填Token Organization: your-org Token: 你的API Token Default Bucket: cabinet_raw点击Save Test看到 “Data source is working” 就说明通了创建 Dashboard 和 PanelDashboard 是看板一个 Dashboard 可以包含多个 Panel图表。创建一个 Panel 时核心是写 Flux 查询。Panel 1实时温度折线图在 Panel 的 Query 区域直接写 Fluxfrom(bucket: cabinet_raw) | range(start: v.timeRangeStart, stop: v.timeRangeStop) | filter(fn: (r) r[_measurement] cabinet_metrics) | filter(fn: (r) r[device_id] cabinet-001) | filter(fn: (r) r[_field] temp) | aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)关键点v.timeRangeStart和v.timeRangeStop是 Grafana 内置变量自动绑定面板右上角的时间范围选择器。用户选过去1小时查询就自动查1小时v.windowPeriod也是内置变量Grafana 根据时间范围自动选择合适的聚合粒度。选过去1小时可能用1分钟聚合选过去7天可能用1小时聚合Panel 配置Visualization: Time Series折线图 Title: cabinet-001 温度变化 X轴: time Y轴: temp (°C)Panel 2多设备温度对比from(bucket: cabinet_raw) | range(start: v.timeRangeStart, stop: v.timeRangeStop) | filter(fn: (r) r[_measurement] cabinet_metrics) | filter(fn: (r) r[_field] temp) | filter(fn: (r) r[device_id] cabinet-001 or r[device_id] cabinet-002 or r[device_id] cabinet-003) | aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false)Grafana 会自动按device_id分组画出三条不同颜色的曲线。Panel 3设备状态表格from(bucket: cabinet_raw) | range(start: -5m) | filter(fn: (r) r[_measurement] cabinet_metrics) | filter(fn: (r) r[_field] temp or r[_field] voltage or r[_field] current) | group(columns: [device_id, _field]) | last()Visualization 选Table展示每个设备的最新温度、电压、电流值。使用模板变量Grafana 的模板变量Variables让你在看板上加一个下拉框动态切换查询参数。比如选择不同设备Dashboard Settings → Variables → Add variable配置Name: device Type: Query Query: import influxdata/influxdb/schema schema.measurements(bucket: cabinet_raw) // 或者用tag values查询在 Panel 的 Flux 查询中使用变量from(bucket: cabinet_raw) | range(start: v.timeRangeStart, stop: v.timeRangeStop) | filter(fn: (r) r[device_id] ${device}) | filter(fn: (r) r[_field] temp)看板顶部会出现一个下拉框选择不同设备所有 Panel 自动刷新。Grafana 告警规则Grafana 内置告警引擎可以基于查询结果自动触发告警。配置示例温度超过10°C时告警进入 Alerting → Alert Rules → New Alert Rule配置查询和条件Query A: from(bucket: cabinet_raw) | range(start: -5m) | filter(fn: (r) r[_measurement] cabinet_metrics) | filter(fn: (r) r[_field] temp) | group(columns: [device_id]) | last() Condition: WHEN last() OF query(A, 5m, now) IS ABOVE 10配置告警通知渠道Contact Point类型: 钉钉 Webhook / 飞书 Webhook / 邮件 消息模板: ${device_id} 温度异常: 当前 ${temp}°C阈值 10°C保存后Grafana 每5秒检查一次查询结果超过阈值就发通知四、SpringBoot ECharts 自研报表当 Grafana 满足不了业务定制需求时就得自己来。后端接口RestControllerRequestMapping(/api/chart)publicclassChartController{privatefinalInfluxDBUtilinfluxDBUtil;publicChartController(InfluxDBUtilinfluxDBUtil){this.influxDBUtilinfluxDBUtil;}/** * 查询设备温度变化数据给ECharts折线图用 */GetMapping(/temp-trend)publicMapString,ObjecttempTrend(RequestParamStringdeviceId,RequestParam(defaultValue1)inthours){StringfluxString.format(from(bucket: \cabinet_raw\)\n | range(start: -%dh)\n | filter(fn: (r) r[\_measurement\] \cabinet_metrics\)\n | filter(fn: (r) r[\device_id\] \%s\)\n | filter(fn: (r) r[\_field\] \temp\)\n | aggregateWindow(every: 5m, fn: mean, createEmpty: false)\n | sort(columns: [\_time\]),hours,deviceId);ListFluxTabletablesinfluxDBUtil.queryRaw(flux);ListStringtimeAxisnewArrayList();ListDoubletempValuesnewArrayList();for(FluxTabletable:tables){for(FluxRecordrecord:table.getRecords()){timeAxis.add(formatTime(record.getTime()));tempValues.add((Double)record.getValue());}}MapString,ObjectresultnewHashMap();result.put(timeAxis,timeAxis);result.put(tempValues,tempValues);result.put(deviceId,deviceId);returnresult;}privateStringformatTime(Instantinstant){returnDateTimeFormatter.ofPattern(HH:mm).withZone(ZoneId.systemDefault()).format(instant);}}前端 ECharts 渲染dividtempChartstylewidth:100%;height:400px;/divscriptsrchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/scriptscriptconstchartecharts.init(document.getElementById(tempChart));fetch(/api/chart/temp-trend?deviceIdcabinet-001hours1).then(resres.json()).then(data{chart.setOption({title:{text:data.deviceId 温度变化},tooltip:{trigger:axis},xAxis:{type:category,data:data.timeAxis},yAxis:{type:value,name:温度(°C)},series:[{name:温度,type:line,data:data.tempValues,smooth:true,markLine:{data:[{yAxis:10,name:高温阈值,lineStyle:{color:#ff0000}}]}}]});});/scriptmarkLine画了一条红色的高温阈值线数据曲线超过这条线就能直观看到异常。五、报表统计场景日报/周报/月报自动生成用 InfluxDB Task 定时聚合数据生成报表写入 MySQL前端直接查 MySQL 展示// Task: 每天凌晨1点生成昨天的日报 option task { name: daily_report, every: 1d, offset: 1h } // 日均温度 avgTemp from(bucket: cabinet_raw) | range(start: -1d) | filter(fn: (r) r[_measurement] cabinet_metrics) | filter(fn: (r) r[_field] temp) | group(columns: [device_id]) | mean() | set(key: _field, value: daily_avg_temp) // 日最高温度 maxTemp from(bucket: cabinet_raw) | range(start: -1d) | filter(fn: (r) r[_measurement] cabinet_metrics) | filter(fn: (r) r[_field] temp) | group(columns: [device_id]) | max() | set(key: _field, value: daily_max_temp) // 合并写入日报Bucket union(tables: [avgTemp, maxTemp]) | to(bucket: cabinet_daily, org: your-org)SpringBoot 再起一个定时任务每天凌晨2点从cabinet_daily读取数据写入 MySQL 报表表供前端查询展示。设备运行率统计设备运行率 运行时长 / 总时长 × 100%。通过统计设备状态 measurement 中compressor_on true的时长占比来计算// 统计某设备过去24小时的压缩机运行率 running from(bucket: cabinet_raw) | range(start: -24h) | filter(fn: (r) r[_measurement] cabinet_status) | filter(fn: (r) r[device_id] cabinet-001) | filter(fn: (r) r[_field] compressor_on) | filter(fn: (r) r[_value] 1) | count() total from(bucket: cabinet_raw) | range(start: -24h) | filter(fn: (r) r[_measurement] cabinet_status) | filter(fn: (r) r[device_id] cabinet-001) | filter(fn: (r) r[_field] compressor_on) | count() // 运行率 running / total × 100% // 在Java端计算百分比异常告警次数排行统计各设备过去7天的告警次数排出Top 10from(bucket: cabinet_raw) | range(start: -7d) | filter(fn: (r) r[_measurement] cabinet_events) | filter(fn: (r) r[event_type] ALARM) | group(columns: [device_id]) | count() | group() | sort(columns: [_value], desc: true) | limit(n: 10)结果直接喂给 ECharts 画柱状图哪个设备老出问题一目了然。六、异常数据分析阈值告警最简单直接的异常检测设一个上下限超出就告警。// 阈值规则配置publicclassAlarmRule{privateStringfield;// 监控字段: temp, voltage, currentprivatedoubleminValue;// 下限privatedoublemaxValue;// 上限privateintdurationSec;// 持续时间秒避免瞬时波动误报}// 检查逻辑publicbooleancheckThreshold(Stringfield,doublevalue,AlarmRulerule){returnvaluerule.getMinValue()||valuerule.getMaxValue();}关键细节是持续时间判断温度瞬间跳到11°C然后马上回落不算异常可能是传感器噪声。只有持续超过阈值5分钟才告警。用 Flux 实现// 温度持续5分钟超过10°C才告警 from(bucket: cabinet_raw) | range(start: -10m) | filter(fn: (r) r[_measurement] cabinet_metrics) | filter(fn: (r) r[_field] temp) | filter(fn: (r) r[_value] 10.0) | group(columns: [device_id]) | count() // 如果5分钟内有超过60条5分钟÷5秒60条说明持续超温突变检测有些异常不是超阈值而是突然跳变。比如温度一直稳定在5°C突然跳到8°C——虽然没超10°C阈值但这种跳变本身可能意味着制冷系统出了问题。/** * 突变检测当前值与过去N分钟平均值的偏差超过阈值 */publicbooleancheckSuddenChange(StringdeviceId,doublecurrentValue,Stringfield){// 查过去10分钟的平均值StringfluxString.format(from(bucket: \cabinet_raw\)\n | range(start: -10m)\n | filter(fn: (r) r[\_measurement\] \cabinet_metrics\)\n | filter(fn: (r) r[\device_id\] \%s\)\n | filter(fn: (r) r[\_field\] \%s\)\n | mean(),deviceId,field);doubleavgValuequeryAvgValue(flux);doubledeviationMath.abs(currentValue-avgValue);// 温度偏差超过3°C视为突变returndeviation3.0;}同比环比把当前数据和历史同期对比发现不正常但没超阈值的异常。比如某台售货柜本周平均温度比上周高了2°C虽然都还在正常范围内但这个上升趋势可能意味着制冷系统在缓慢老化。// 本周平均温度 thisWeek from(bucket: cabinet_10m) | range(start: -7d) | filter(fn: (r) r[_measurement] cabinet_metrics_10m) | filter(fn: (r) r[device_id] cabinet-001) | filter(fn: (r) r[_field] temp) | mean() | findRecord(fn: (key) true, idx: 0) // 上周平均温度 lastWeek from(bucket: cabinet_10m) | range(start: -14d, stop: -7d) | filter(fn: (r) r[_measurement] cabinet_metrics_10m) | filter(fn: (r) r[device_id] cabinet-001) | filter(fn: (r) r[_field] temp) | mean() | findRecord(fn: (key) true, idx: 0) // 输出环比变化 difference thisWeek._value - lastWeek._value七、无人售货柜运营数据可视化看板实战把以上内容整合成一个完整的运营看板看板布局┌─────────────────────────────────────────────────────────┐ │ 无人售货柜运营看板 │ ├──────────────┬──────────────┬──────────────┬───────────┤ │ 在线设备数 │ 今日告警次数 │ 平均温度 │ 异常设备数 │ │ 985/1000 │ 23 │ 5.2°C │ 15 │ ├──────────────┴──────────────┴──────────────┴───────────┤ │ │ │ 全设备实时温度分布图热力图 │ │ │ ├─────────────────────────┬───────────────────────────────┤ │ │ │ │ Top10 告警设备排行 │ 设备运行率统计柱状图 │ │ 柱状图 │ │ │ │ │ ├─────────────────────────┴───────────────────────────────┤ │ │ │ 选中设备温度/电压/电流趋势图多Y轴折线图 │ │ │ ├─────────────────────────────────────────────────────────┤ │ 设备明细表格设备ID | 温度 | 电压 | 状态 | 最后上报时间 │ └─────────────────────────────────────────────────────────┘数据来源拆分看板模块数据源查询方式在线设备数InfluxDB cabinet_raw统计5分钟内有数据的设备数今日告警次数InfluxDB cabinet_eventscount filter(event_typeALARM)平均温度InfluxDB cabinet_raw所有设备temp的mean()异常设备数应用层计算温度超阈值的设备去重计数温度热力图InfluxDB cabinet_rawgroup(device_id) last()告警排行InfluxDB cabinet_eventsgroup(device_id) count sort limit运行率InfluxDB cabinet_status压缩机ON时长占比趋势图InfluxDB cabinet_rawaggregateWindow(mean)设备明细表格InfluxDB cabinet_rawgroup(device_id) last()这套看板用 Grafana 搭一个下午就能搞定。如果用 ECharts 自研大概需要2~3天但可以和你的业务后台深度整合。数据可视化不是锦上添花而是数据落地的最后一公里。采得到、存得好、查得快最后还得看得见——运营人员看到的不是数字是设备健康度、是业务趋势、是需要立即处理的异常。这条链路通了你的时序数据平台才真正有价值。
返回列表