Apache IoTDB+Grafana工业数据可视化三步实践与排障

发布时间:2026/11/2 16:40:27
Apache IoTDB+Grafana工业数据可视化三步实践与排障

工业现场做数据可视化,最容易翻车的地方往往不是"图画得丑",而是数据从产生到显示这条链路上,任何一环没想清楚,后面全是返工。我见过太多项目,采集端辛辛苦苦把几千个测点的数据写进了时序库,结果 Grafana 面板一打开,转圈十几秒,曲线还缺了一段,运维同事跑过来问"是不是设备掉线了",其实设备好好的,是查询把库拖住了。这篇就围绕Apache IoTDB + Grafana这套组合,把我从建模、写入到看板搭建、上线排障的完整链路拆开讲一遍。标题里说的"3步",在我看来是建模、写入、可视化这三步,但真正决定成败的是每一步背后的取舍判断,所以我在这三步之上又补了排障和运维两块内容,凑成一篇可以直接抄作业的实践记录。适合正在做设备监控、能源监测、产线看板这类项目的朋友,无论你是刚接触时序数据库的新手,还是已经踩过一两个坑想找补的人,都能从里面拿到点东西。

1. 工业数据流的三个特征:为什么通用方案总差一口气

1.1 三条曲线背后是三种完全不同的查询模式

很多人一开始对时序数据的想象是"不就是按时间存个数值嘛",用关系库加个时间索引也能跑。小规模确实能跑,但工业现场的数据有个很讨厌的特点:写多读少、写密读散、写新读旧。一条产线上,几百个测点每秒采一次,一天下来就是几千万个数据点;而做看板的时候,运维只想看最近一小时,工艺工程师想看上周某个班次的对比,质量部门想拉半年的趋势做统计。这三种人打开同一个 Grafana 面板,背后触发的是完全不同的查询。

我把它归成三类查询模式,这个划分直接决定了后面所有的架构选择:

查询模式典型场景数据量级对系统的要求
最新值查询设备状态灯、实时看板每条序列取 1 个点极低延迟,可缓存
区间降采样趋势图、班次对比原始点百万级,聚合后几百点聚合下推,不能全量捞回
明细追溯故障回溯、工艺分析精确到毫秒的连续点高并发读,需限制范围

第一类查询如果按传统做法"每次查最新一条记录",在几千条序列上就是几千次查询,Grafana 一个面板刷新一次能把数据库打满。第二类如果不在数据库侧做聚合,把百万点拉到浏览器再算平均,网络传输就先把 Grafana 卡死了。第三类最危险,因为它看起来最简单,但一旦有人把时间范围选成"最近 30 天",就会变成一次全表扫描。

理解这三类模式之后你会发现,问题的核心不在于"用什么工具画图",而在于聚合和过滤能不能下推到存储层。这也是我后来坚定用时序库而不是关系库的根本原因——不是因为它名字里有"时序"两个字,而是它把降采样、最新值、时间窗口这些操作变成了原生能力。

1.2 IoTDB 与 Grafana 各自守住的边界

IO 层面的事情交给 IoTDB,展示层面的事情交给 Grafana,这条边界必须划清楚,划不清楚就会出现"在数据库里做计算"或者"在前端做聚合"这种两头不讨好的情况。

IoTDB 是 Apache 基金会下的开源时序数据库,它的核心设计思路是列式存储加时间分区,一个设备的一个物理量对应一条时间序列,物理量的数据连续写在一起,压缩比很高。它原生支持GROUP BY时间窗口聚合、LAST最新值查询、FILL空值填充这些操作,还有一套树形路径来组织测点。这套路径模型对工业场景特别友好,因为工业设备的层级关系天然就是树——厂区、车间、产线、设备、物理量,一层一层往下挂,路径本身就是设备台账。

Grafana 这边负责的事情很明确:时间范围的选取、图表的渲染、变量的联动、告警规则的触发。它不关心数据是怎么存的,只关心"我给一个查询,你返回一个时间序列数组"。所以 Grafana 的强项是展示层的灵活组合,弱项是它不知道你的数据量有多大——默认的查询策略在没有约束的情况下会拉全量数据,这就是很多看板卡死的直接原因。

一句实在话:Grafana 不会替你优化查询,它只会忠实地把你写的 SQL 发给后端。面板卡不卡,八成取决于你 SQL 写得糙不糙。

1.3 什么规模适合这套组合,什么规模要先缓一缓

实事求是地说,这套组合不是万能的。我按经验给几个档位:

测点在几千到几万级别、采集频率秒级到分钟级、看板并发十几个人以内,这套组合非常舒服,单机部署就能撑住,成本低、上手快。测点到了几十万、采集频率到毫秒级、还要做跨年度的历史分析,那就得考虑 IoTDB 的集群模式,同时 Grafana 侧必须做严格的查询约束,否则一个误操作就能拖垮整个分析链路。如果只是十几个测点做个简单的曲线观察,说实话用不用时序库都行,直接文件加 Grafana 的 CSV 插件也能对付,别过度设计。

我见过一个反面案例:某实验室的项目,一共就二十几个传感器,非要上一整套分布式时序库加高可用部署,运维成本比业务本身还高。工具选型要看的是"三年后数据量会长到多少",而不是"现在看起来够不够气派"。

2. 建模:路径、精度、编码定错,后面全是返工

2.1 从 root 开始逐层下降的命名约定

IoTDB 的数据模型是一棵树,所有序列都必须以root开头,后面跟着你自己定义的层级。这个层级怎么划,是整个项目里最容易被草率对待、又最难改的一件事——序列创建之后虽然能改路径,但涉及数据迁移,代价很大。

我的做法是固定五层:root.{业务域}.{车间或站点}.{产线或机组}.{设备}.{物理量}。举个具体的:

root.dt.ws01.line01.motor01.temperature root.dt.ws01.line01.motor01.vibration root.dt.ws01.line01.motor01.status

为什么不做得更扁平?因为 IoTDB 的很多能力都依赖路径前缀:SHOW TIMESERIES root.dt.ws01.line01.**可以一次列出整条产线的所有测点;SELECT ... FROM root.dt.ws01.line01.*可以对整条产线批量查询;TTL 也可以挂在某一层路径上,让旧产线的数据自动过期。层级如果拍平了,这些能力就全用不上,只能一条条序列去维护,测点一多就是灾难。

命名上还有几个细节值得提前约定。层级名里不要用点号,因为点号是路径分隔符,会直接把层级拆断;不要用中文和特殊字符,虽然部分版本能存,但在 URL 参数、变量传参、脚本拼接时到处出问题;同一层级的命名风格要统一,要么全小写下划线,要么全驼峰,别一半一半,后面写 Grafana 变量的正则时你会感谢自己。设备编号建议用固定的补零规则,motor01而不是motor1,这样按字典序排序的时候顺序才是对的。

2.2 时间精度在建库那一刻就锁死了

这是我认为最容易埋雷的一个点,必须单独说。IoTDB 支持毫秒、微秒、纳秒三种时间戳精度,这个精度是在系统配置里定的,全局统一,建库之后改不了。而 Grafana 的时间轴统一按毫秒处理。

如果数据库用了微秒精度,存进去的时间戳是 1700000000000000,Grafana 拿到这个数字会当成毫秒解释,结果就是曲线的时间整体往后飞跃了几十年,或者干脆画不出来。反过来,如果数据库是毫秒精度,你的采集端是微秒时间戳直接丢进来,数据就会挤在同一毫秒里,看起来像是"时间戳被截断了"。

我的建议非常明确:工业监控类项目,除非你的采集频率真的到了微秒级,一律用毫秒。秒级、百毫秒级的采集,毫秒精度绰绰有余。真要上微秒,那就得在写入端或者 Grafana 侧做单位换算,多一层转换就多一个出错的地方,务实的做法是能不换算就不换算。

配置在数据节点的配置文件里,参数是timestamp_precision,可选ms、us、ns。改这个参数意味着要清空数据目录重建,所以部署前先想清楚。

2.3 数据类型、编码与压缩的搭配逻辑

创建序列的时候要指定数据类型和编码方式,这一步很多人直接抄示例,其实里面是有讲究的。数据类型必须和实际数据匹配:温度、振动这类连续量用FLOAT或DOUBLE;设备开关状态用BOOLEAN;计数器、产量这类整数用INT32或INT64。用DOUBLE存所有东西看起来很省事,但存储和传输都会翻倍,几千万个点下这个差距很可观。

编码方式的选择逻辑是这样的:

数据特征推荐编码理由
浮点连续量(温度、压力)GORILLA相邻值差异小,异或压缩效果好
单调递增整数(累计产量)TS_2DIFF差分值基本恒定,压缩率极高
变化缓慢的整数(档位、状态码)RLE连续相同值只存一次
字符串枚举(报警文本)DICTIONARY重复字符串建字典
布尔开关量RLE长段 0 或 1 压缩明显

压缩算法(COMPRESSOR)默认是 SNAPPY,它在压缩率和 CPU 开销之间比较平衡,绝大多数场景直接用默认值就行。如果存储空间特别紧张,可以上 ZSTD,压缩率更高但写入的 CPU 消耗也更高;如果写入吞吐是瓶颈,可以考虑 LZ4。我的经验是:先别折腾压缩算法,先用默认配置把系统跑起来,等真正发现磁盘涨得太快再针对性调,否则很容易在调参上浪费大量时间却看不到明显收益。

建序列的语句大概长这样:

CREATE DATABASE root.dt; CREATE TIMESERIES root.dt.ws01.line01.motor01.temperature WITH DATATYPE=FLOAT, ENCODING=GORILLA, COMPRESSOR=SNAPPY; CREATE TIMESERIES root.dt.ws01.line01.motor01.status WITH DATATYPE=BOOLEAN, ENCODING=RLE;

另外一个实用技巧:如果采集端字段是动态的(不同型号设备上报的物理量不一样),可以用CREATE TIMESERIES ... WITH DATATYPE=FLOAT的批量写法配合自动创建 schema 的开关,让 IoTDB 在第一次写入时自动建序列。这个开关在开发阶段很好用,但上生产一定要关掉,否则采集端一个字段名拼错,库里就会多出一条永远不会有数据的空序列,日积月累路径树会越脏越乱。

2.4 TTL 与降采样:数据生命周期得提前规划

工业数据有个特点:老数据的访问频率断崖式下降。最近一周的数据天天有人看,三个月前的数据一个月可能就查一次,一年前的数据基本只是合规存档。如果不做生命周期管理,磁盘会被无限蚕食。

IoTDB 提供了 TTL(生存时间)机制,挂在路径上,单位是毫秒:

-- 原始数据保留 180 天 SET TTL TO root.dt.ws01.** 15552000000; -- 归档层的降采样数据保留 5 年 SET TTL TO root.dt_archive.ws01.** 157680000000;

这里的做法是分层存储:root.dt放原始高频数据,设置较短的 TTL;root.dt_archive放按分钟或小时聚合后的结果,TTL 设得很长。聚合任务可以用 IoTDB 的持续查询(Continuous Query)能力,或者用外部定时任务跑一次SELECT ... GROUP BY再写回归档路径。持续查询的好处是数据库内部完成,不用额外的调度系统;坏处是维护起来不如外部任务直观,出问题不好排查。中小项目我倾向外部定时任务,脚本简单,出错了日志清楚。

TTL 还有一个坑要注意:TTL 是异步清理的,不是到点就删。设置了 TTL 之后不要立刻去核对数据条数,清理有延迟是正常的,别以为没生效又去重复设置。

3. 写入链路:四种接法,各有各的适用面

3.1 Session API 与 JDBC 的边界在哪里

如果你是 Java 技术栈,并且对写入性能有要求,Session API 是首选。它是 IoTDB 原生的客户端接口,支持批量写入、多设备并行、连接池管理,写入吞吐比走 JDBC 高一个档次。典型写法是攒一批数据(比如 1000 个点或者 1 秒的数据)再一次性提交,而不是来一个点就写一次。这个批量大小是有讲究的:太小网络往返次数多,太大内存占用高且失败重试代价大。我的经验值是单批 500 到 2000 个点,具体要压测。

try (Session session = new Session("127.0.0.1", 6667, "root", "root")) { session.open(false); List<String> devices = Arrays.asList("root.dt.ws01.line01.motor01"); List<Long> times = Arrays.asList(System.currentTimeMillis()); List<List<String>> measurements = Arrays.asList( Arrays.asList("temperature", "vibration")); List<List<Object>> values = Arrays.asList( Arrays.asList(63.2f, 1.84f)); session.insertRecords(devices, times, measurements, values); }

JDBC 的适用场景不太一样。它适合已有系统改造——如果你的项目里已经有一套基于 JDBC 的数据访问层,不想为了接时序库重构,那用 JDBC 驱动改个连接串最快。它也适合做临时性的数据核对和脚本工具。但要注意,JDBC 走的是 SQL 解析路径,写入性能不如 Session,高频率写入场景下差距会很明显。

一个血的教训:别在采集端做"写一个点开一次连接"的操作。连接建立的开销远大于写入本身,几千个测点轮流这么干,数据库的连接数会瞬间打满,然后所有查询一起超时。

3.2 MQTT 直连与 REST 写入怎么选

现场设备如果是走 MQTT 上报的(这在物联网场景里非常普遍),IoTDB 内置了 MQTT 接入服务,可以在数据节点的配置文件里打开:

enable_mqtt_service=true mqtt_port=1883 mqtt_handler_pool_size=4

打开之后,设备直接把数据发到这个端口,IoTDB 按约定的 topic 解析成路径写进去。好处是少了一层网关程序,链路短、故障点少;坏处是 topic 到路径的映射规则是固定的,如果你的设备上报格式比较特殊(比如嵌套 JSON、需要做单位换算),就不太好处理,这时候还是老老实实写一个接入服务做转换。

REST 接口的适用面更广一些,任何语言都能调,特别适合脚本、边缘盒子、低代码平台这类不方便引入 SDK 的场景:

curl -X POST -H "Content-Type: application/json" \ -d '{"sql":"INSERT INTO root.dt.ws01.line01.motor01(time,temperature) VALUES(1700000000000,63.2)"}' \ http://127.0.0.1:18080/rest/v1/nonQuery

需要注意 REST 是按 SQL 走的,每次都要解析语句,吞吐比原生接口低,只适合低频或中等频率的写入,别拿它做主力写入通道。而且这条路径需要显式打开enable_rest_service=true并配置端口,默认是关闭的。

四种方式的取舍我整理成一张表:

接入方式吞吐能力改造成本适合场景
Session API高中(需引入 SDK)新建 Java 采集服务
JDBC中低(改连接串即可)已有系统改造
MQTT 接入中高低(设备侧改 topic)设备直连上报
REST 接口低极低(任何语言)脚本、边缘端、低频写入

3.3 历史数据回灌:CSV 导入的几个注意点

项目上线初期往往要把历史数据补进去,IoTDB 自带导入工具,在工具目录下执行:

./import-csv.sh -h 127.0.0.1 -p 6667 -u root -pw root -f history_2023.csv

CSV 的格式要求是第一列必须是时间,后面每列对应一条序列的完整路径:

Time,root.dt.ws01.line01.motor01.temperature,root.dt.ws01.line01.motor01.vibration 1700000000000,63.2,1.84 1700000060000,63.5,1.86

这里有几个我踩过的坑。第一,时间列的格式必须显式声明,工具默认按毫秒时间戳解析,如果你的 CSV 是2023-11-15 10:00:00这种可读格式,要加参数指定格式,否则会解析失败或者时间全变成 1970 年。第二,导入前先建好序列,虽然工具有自动创建选项,但自动创建的数据类型是猜的,浮点数被建成整型后精度就丢了。第三,大批量导入要分批,一个几百万行的 CSV 直接怼进去,中途失败重来一次会疯掉,按天或者按小时切分文件,失败可以断点续传。

导入完成后务必抽样校验,随便挑几个时间点用查询语句对一下原始值,别觉得导入脚本没报错就等于成功了。

4. Grafana 接入:四条路径的对比与我最终的选择

4.1 四种接法的横向对比

把 IoTDB 的数据搬到 Grafana 上,能走的路不止一条,我把常见的四种列出来,并且说明各自的适用面:

接入方式原理优点局限
官方数据源插件Grafana 直接调用 IoTDB 查询接口原生支持,配置简单,功能最全插件版本与 Grafana 版本需匹配
Graphite 协议兼容层IoTDB 开启 Graphite 接收端口,Grafana 用 Graphite 数据源无需额外插件查询能力受限,只支持简单聚合
REST + 通用 JSON 插件用通用 HTTP 数据源发请求,自己解析返回 JSON灵活,可自由拼接需要手写解析,不支持变量下推
中间层服务自建服务查 IoTDB 再暴露给 Grafana可做复杂加工多一个服务要维护

我的选择是官方数据源插件为主,REST 兜底。理由很直接:插件把时间范围传参、变量替换、降采样窗口这些东西都封装好了,面板配置起来最省事;而 REST 方式留着应对一些插件覆盖不到的特殊查询(比如需要做多步计算的场景)。

插件安装的路径有两条,如果你的 Grafana 能上外网,直接命令行安装最省事:

grafana-cli plugins install <插件名> systemctl restart grafana-server

不能联网的话,就去插件页面下载压缩包,解压到 Grafana 的插件目录(Linux 下通常是/var/lib/grafana/plugins),然后重启服务。重启之后在"数据源"里能搜到 IoTDB 就算装好了。装不上或者搜不到,八成是目录权限或者插件目录配置的问题,看一眼 Grafana 的启动日志,里面会明说"plugin not found"。

4.2 数据源配置里的几个关键字段

配置数据源的时候,主要填这几项:服务地址、端口(默认 6667)、用户名、密码。看起来很简单,但有几个细节会决定后面好不好用。

账号一定要用只读账号,不要图省事用 root。IoTDB 的用户权限是按路径和操作授权的,可以精确到只给某几条路径的读权限:

CREATE USER viewer 'ReadOnlyPass123'; GRANT READ_TIMESERIES ON root.dt.ws01.** TO USER viewer;

这样即使 Grafana 被误配置成执行写操作,也写不进去,等于加了一层保险。而且不同业务部门可以用不同账号,各看各的路径,天然做了数据隔离。

连接超时和最大连接数要调。默认值往往偏小,看板并发一多就排不上队,表现出来就是"面板偶尔转圈"。我把连接超时设到 10 秒,最大连接数按并发人数乘以 2 来估算,别设太大,超过数据库的最大连接限制会直接连接失败。

时区要显式确认。IoTDB 存的是 UTC 毫秒时间戳,Grafana 默认按浏览器时区渲染。如果两边都正常,显示没问题;但如果有环节做过时区转换(比如采集端把本地时间当 UTC 存了),曲线就会整体偏移。这个问题放在第 6 节细讲。

4.3 查询语句怎么写才不拖垮库

这是整套方案里最见功力的地方。同一张趋势图,SQL 写法不同,性能能差上百倍。

先说必须避免的写法:不带时间范围的全量查询。比如SELECT temperature FROM root.dt.ws01.line01.motor01后面什么都不加,这条语句在测试环境(数据少)跑得飞快,一上生产就成灾难。Grafana 的时间选择器虽然会把时间范围传进来,但数据源插件不一定自动帮你拼进 SQL,具体行为看插件版本,稳妥的做法是自己显式写时间条件。

带时间范围的查询:

SELECT temperature FROM root.dt.ws01.line01.motor01 WHERE time >= 1700000000000 AND time <= 1700003600000

但即使加了时间范围,如果时间窗口是最近 7 天、采集是秒级,这条语句仍会返回几十万个点,浏览器渲染直接卡死。真正的解法是在数据库侧降采样:

SELECT avg(temperature) FROM root.dt.ws01.line01.motor01 WHERE time >= 1700000000000 AND time <= 1700003600000 GROUP BY ([1700000000000, 1700003600000), 5m)

这条语句把 1 小时的数据按 5 分钟聚合,返回 12 个点,网络传输和渲染压力瞬间降到可以忽略。这里的5m就是聚合窗口,它应该和 Grafana 面板的宽度、时间范围联动,而不是写死。后面 5.1 节会说怎么用宏变量做到自动联动。

还有个容易被忽略的点:聚合函数的语义。求平均用avg,看极值用max_value或min_value(这两个函数在大数据量下会走更高效的路径),取区间内第一个和最后一个用first_value、last_value。别小看这个区别,用max和用max_value在千万级数据上性能差距明显,而且max会额外返回对应的时间点,如果你不需要那个时间点,就是白白的开销。

5. 面板搭建:从一条曲线到一屏总览

5.1 时间宏与降采样窗口的联动

写死聚合窗口的问题是:用户把时间范围从 1 小时改成 1 周,图还是按 5 分钟一个点,会返回 2000 多个点,又卡了。正确的做法是让窗口跟着时间范围自动变。

Grafana 提供了内置的间隔变量,配合 IoTDB 插件的时间宏,可以这么写:

SELECT avg(temperature) FROM root.dt.ws01.line01.motor01 WHERE time >= $__from AND time <= $__to GROUP BY ([$__from, $__to), $__interval)

$__from和$__to会被替换成当前选择范围的毫秒时间戳,$__interval会被替换成 Grafana 根据面板宽度和时间范围自动算出的间隔。这样一来,看 1 小时就是几十秒一个点,看 1 个月就是几小时一个点,返回的点数始终控制在几百的量级,图既清晰又不卡。

如果某个插件版本对这两个宏支持不完整,退一步的方案是用固定变量配合自定义的时间范围选项,把"最近 1 小时""最近 24 小时""最近 7 天"做成下拉选项,每个选项对应一套查询,虽然笨但一定跑得通。

还有一个实用技巧:在面板的查询选项里把Max data points显式设成一个值(比如 800),这样即使宏变量失效,Grafana 也会在拿到数据后做一次重采样,算是最后一道保险。不过这是客户端侧的兜底,不能替代数据库侧聚合,数据量太大照样会卡在传输环节。

5.2 多设备对比与 ALIGN BY DEVICE 的坑

车间主任最常提的需求是"把同一条产线上五台电机的温度画在一张图上对比"。直觉写法是用通配符:

SELECT avg(temperature) FROM root.dt.ws01.line01.* WHERE time >= $__from AND time <= $__to GROUP BY ([$__from, $__to), 1m)

但这里有个陷阱:多设备查询必须加ALIGN BY DEVICE,否则 IoTDB 会按时间戳做行对齐,不同设备在同一毫秒没有数据的位置会补空,结果就是一张稀稀拉拉、对不齐的图。正确写法:

SELECT avg(temperature) FROM root.dt.ws01.line01.* WHERE time >= $__from AND time <= $__to GROUP BY ([$__from, $__to), 1m) ALIGN BY DEVICE

加上之后返回的是按设备分组的结果,每台设备一条序列,Grafana 里就是五条曲线。设备多了图会花,这时候在 Grafana 的图例里做过滤,或者干脆拆成"小倍数图"(每个设备一个小图),比硬塞在一张图上好看得多。

设备采集频率不一致的时候还有个补充手段:加FILL(PREVIOUS)让空缺位置用上一个值填充,曲线会连续很多。但要清楚这是在掩盖数据缺失,如果缺失本身是故障信号(设备真的掉线了),填充之后的曲线会让你误判成"设备正常"。我的做法是趋势图用FILL(PREVIOUS)保证观感,同时单独做一个"数据到达率"的面板,用count统计每个时间窗口的点数,少了就说明有断点。两个面板配合看,才不会自欺欺人。

5.3 设备状态灯与变量联动

总览大屏上最有价值的往往不是曲线,而是那一排状态灯。做法是用LAST查询取最新值:

SELECT LAST status, temperature FROM root.dt.ws01.line01.*

返回每条序列的最新值和对应时间。Grafana 里用 Stat 面板配阈值着色,正常绿、警告黄、异常红,一眼就能看出哪台设备有问题。这里有个细节:要同时展示数据的时间戳,如果某台设备的数据已经十分钟没更新了,状态灯还显示绿色"正常"就是误导。可以在面板上加一列"最后更新时间",或者在 Grafana 里用"无数据"状态单独配色,让断线的设备显示灰色。

变量联动是让看板从"能用"变成"好用"的关键。做法是建一个 Query 类型的变量,查询语句列出所有可选设备:

SHOW TIMESERIES root.dt.ws01.line01.**

然后在变量的正则提取里配置root\.dt\.ws01\.line01\.([^.]+)\.,把设备名抽出来作为下拉选项。之后所有面板的 SQL 里用$device引用:

SELECT avg(temperature) FROM root.dt.ws01.line01.$device.* WHERE time >= $__from AND time <= $__to GROUP BY ([$__from, $__to), $__interval)

选哪台设备,所有面板一起跟着变,比每个面板单独配一遍省事得多。变量还可以做级联,先选车间再选产线再选设备,层级关系正好和 IoTDB 的路径结构对应上。

提醒一句:变量的正则一定要测。我见过正则里少写一个转义点号,结果把所有测点都匹配上了,下拉框里几百个选项,用户根本没法用。

5.4 告警规则:用 avg 还是 max 是个要命的选择

Grafana 的告警是基于查询结果做阈值判断的。很多人直接把趋势图那条avg查询复制过来配告警,逻辑是"平均温度超过 80 度就报警"。这个做法在降采样窗口比较宽的时候会漏报——一秒的尖峰被 5 分钟的窗口平均掉了,平均值可能只升高了 0.1 度,但设备那一刻已经过载了。

正确做法是告警查询单独写,用极值而不是均值:

SELECT max_value(temperature) FROM root.dt.ws01.line01.motor01 WHERE time >= $__from AND time <= $__to GROUP BY ([$__from, $__to), 1m)

窗口也要比趋势图细,1 分钟比 5 分钟更能抓住尖峰。代价是查询点数变多,但告警查询的频率通常不高(一分钟一次),这点开销可以接受。

另一个坑是告警的"无数据"处理。默认配置下,查询返回空结果会被判定成"无数据",而"无数据"是否触发告警是可以选的。如果设备断线导致没有数据,你又没把"无数据"设成告警状态,那就等于把最需要报警的情况漏掉了。我的配置是:无数据持续 3 个周期就报警,明确告诉运维"这台设备失联了",比静默好得多。

6. 上线之后才会暴露的问题与排查链路

6.1 曲线整体偏移了八小时

这是最高频的问题,没有之一。现象是:数据明明是三点的,图上显示成十一点,或者反过来。

排查链路我一般是这么走的:

第一步,确认数据库里存的值。用 CLI 直接查:

./start-cli.sh -h 127.0.0.1 -p 6667 -u root -pw root
SELECT temperature FROM root.dt.ws01.line01.motor01 WHERE time >= 1700000000000 AND time <= 1700003600000

如果返回的时间戳和预期一致,说明问题在展示层;如果不一致,问题在采集层。

第二步,确认 Grafana 的时区设置。Grafana 有三个地方管时区:浏览器时区、用户偏好时区、仪表盘时区。如果仪表盘时区被设成了 UTC,而你的运维同事在东八区,看起来就整体差 8 小时。这个在仪表盘设置里改成"浏览器时区"就好。

第三步,也是最隐蔽的一种:采集端把本地时间当 UTC 存了。比如采集程序用System.currentTimeMillis()拿的是正确的绝对时间,没问题;但如果用了某个格式化再解析的流程,把2023-11-15 10:00:00这个本地时间字符串按 UTC 解析成毫秒,存进去就偏了 8 小时。这种问题在数据库和 Grafana 两侧看都"正常",只有对着真实时钟才能发现。

判断方法很土但有效:在数据库里插一条当前时间的数据,然后看 Grafana 上显示的时间是不是和墙上时钟一致。不一致就一层层往上找。

6.2 面板转圈十秒以上

这个问题的排查我有个固定顺序,从外到内:

先看是不是单个面板慢还是整个仪表盘慢。如果只有一个面板慢,那基本就是它的 SQL 有问题;如果所有面板都慢,可能是数据源连接或者数据库整体负载的问题。

再看这条 SQL 在 CLI 里跑要多久。这一步很关键,它能直接把问题切成两半。CLI 里也慢,那就是数据库侧的问题,重点看时间范围是不是太宽、聚合窗口是不是太小、有没有走全量扫描。CLI 里飞快但 Grafana 里慢,那就是传输或渲染的问题,重点看返回的数据点数是不是太多(超过一两千个点浏览器就开始吃力了)。

真实案例里最常见的原因是聚合窗口没跟着时间范围联动。用户把范围从 1 小时拉到 30 天,SQL 里的GROUP BY还写死 1 分钟,结果要返回四万多个点,不慢才怪。改成$__interval之后立刻降到几百个点。

还有一种情况是仪表盘面板太多。一屏放 20 个面板,刷新间隔设 5 秒,等于每秒有 4 个查询打到数据库。这种场景下要么拉长刷新间隔,要么把不常看的面板挪到另一个仪表盘,要么用 Grafana 的"懒加载"设置让不可见的面板不查询。

6.3 数据突然断崖式归零

图上曲线好好的,突然掉到 0 或者直接断了。这个现象背后可能有四种原因,需要区分:

现象可能原因判断方法
曲线归零但继续有数据采集端上报了 0,或单位换算错误看原始值,看采集端日志
曲线断开一段后恢复网络抖动或采集中断用 count 查该时间段点数
曲线持续为直线采集端卡死,重复上报同一值查 distinct 值数量
曲线在 TTL 边界处消失数据已过保留期被清理查 TTL 设置和历史数据

第三种"持续直线"最阴险,因为它看起来完全正常。判断办法是查一段时间内的不同值数量:

SELECT count(temperature) FROM root.dt.ws01.line01.motor01 WHERE time >= $__from AND time <= $__to

再配合看数据的波动范围。如果一条温度曲线在半小时内数值一模一样,那大概率不是设备真的那么稳,而是采集端挂了在发缓存值。

排查这类问题一定要回到原始数据层看,不要在看板上猜。看板经过了聚合,很多细节已经被抹平了。

6.4 只读账号连上了却查不到数据

配置数据源的时候测试连接是通的,但一查就报无权限或者返回空。这基本是权限粒度的问题。

IoTDB 的权限是按路径授权的,GRANT READ_TIMESERIES ON root.dt.ws01.**只覆盖了ws01下面的路径。如果你的查询里用了root.dt.**这种更上层的通配,或者变量选项需要SHOW TIMESERIES的权限,就会失败。

排查方法是拿这个账号在 CLI 里手动登录,把面板里的 SQL 原样执行一遍:

./start-cli.sh -h 127.0.0.1 -p 6667 -u viewer -pw ReadOnlyPass123

报错信息会直接告诉你是哪条路径没权限。确认之后补授权:

GRANT READ_TIMESERIES ON root.dt.** TO USER viewer;

变量下拉框为空的情况还要额外检查一项:SHOW TIMESERIES属于元数据读取权限,有的版本需要单独授予,光给READ_TIMESERIES不够。这个细节文档里不一定显眼,但实际会遇到。

6.5 几条常见报错的快速对照

报错关键字大概率原因处理方向
Connection refused服务没起或端口不对检查进程和端口监听
Too many open connections连接池配置过小或泄漏调大池上限,检查是否未关闭连接
Permission denied路径权限未授予补 GRANT 语句
Meet error when insert数据类型不匹配或序列不存在核对建序列时的 DATATYPE
Query timeout时间范围过大或未降采样收紧范围,加 GROUP BY
No timeseries matched路径写错或通配不匹配用 SHOW TIMESERIES 核对实际路径

这张表我贴在项目文档首页,新人遇到问题先自查一遍,能解决八成常见情况,剩下的再找人。

7. 一些写在最后面的实操体会

整套东西跑通之后,我觉得最值得分享的不是某个具体配置,而是几个想明白就少走弯路的判断。

第一,建模的时间投入永远值得。我在项目前期花了差不多两天时间只做一件事:和设备厂商确认每个测点的物理含义、单位、取值范围、采集频率,然后据此设计路径和数据类型。当时觉得慢,但后来整个项目周期里几乎没有因为"路径不够用"或者"类型不对"返工过。反过来,我见过压缩建模时间、边做边加测点的项目,最后路径树乱成一团,做报表的时候要写几十条特判 SQL 才能把数据凑齐。

第二,看板的性能上限在 SQL 里,不在服务器配置里。加内存、加 CPU 能缓解问题,但解决不了"查询返回几十万个点"这个本质矛盾。每次面板变慢,第一反应应该是去看 SQL 返回了多少个数据点,而不是去扩容。

第三,告警和趋势图要用两套查询。趋势图追求好看和连贯,可以用均值加填充;告警追求准确,必须用极值加细窗口。把这两个需求混在一个查询里,两头都做不好。

第四,留一块"数据健康度"的看板给自己。我的做法是单独做一个仪表盘,放三个图:各产线的数据到达率、各设备的最后更新时间、写入延迟的分位数。这块看板平时没人看,但每次出问题它是最快定位的工具——一眼就能看出是采集端的问题还是数据库的问题。

最后再分享一个小技巧。如果你要给非技术同事做大屏,别把所有信息塞一屏。我的经验是一屏只放三层信息:最上面一排状态灯(有没有事)、中间一排关键指标的数字(有多大事)、下面一到两张趋势图(事情怎么发展的)。要深挖细节的入口放个链接跳转到详细仪表盘。这个结构用下来,车间同事的接受度明显比堆满曲线的版本高得多。