一、为什么气象数据必须用时序数据库,而不是 MySQL
这是新手最爱踩的认知坑。把气象数据塞进 CREATE TABLE weather (id, ts, t, h, ...) 的 MySQL,前 100 万行很爽,后面就崩了:
- 写入模式完全不同:气象数据是"持续高频追加、几乎不更新、按时间范围查",MySQL 的 B+ 树为"随机读写+事务"设计,海量 append 会写放大严重、索引膨胀。
- 时间序列查询:"查过去 24 小时每分钟平均温度"在 MySQL 要扫大量行+GROUP BY;时序库用时间分区 + 降采样(downsampling)秒回。
- 存储压缩:时序库针对"时间戳+数值"做列式 + _delta 压缩,10 亿点可能只占几 GB;MySQL 同样数据几十 GB。
- 数据生命周期:气象数据久了要自动降精度归档(热数据秒级、冷数据分钟级),时序库原生 TTL + 连续查询;MySQL 要手写定时任务。
一句话:数据是"时间为主"就用时序库,是"记录为主"才用宽表/关系库(宽表选型我们昨天那篇 HBase vs Cassandra 讲过)。气象站是典型的时序场景。
二、时序库选型:InfluxDB vs TDengine
| 维度 | InfluxDB(2.x) | TDengine(国产,3.x) |
|---|---|---|
| 架构 | 单机/TSM 引擎,集群版收费 | 开源集群,分布式原生 |
| 写入性能 | 高 | 极高(常快数倍) |
| 查询语言 | InfluxQL / Flux | SQL 兼容(学习成本低) |
| 资源占用 | 中 | 低(单节点扛很高吞吐) |
| 生态 | Grafana 原生支持 | Grafana 有插件,国内资料多 |
| 适用 | 国际项目、Flux 灵活分析 | 国内、要 SQL、要省钱省资源 |
本篇用 TDengine 演示(SQL 友好、单机就能跑、 Grafana 有官方插件),你换成 InfluxDB 逻辑完全一致。
三、一键部署:EMQX + TDengine + Grafana
用 Docker Compose 把三件套起起来:
yaml
# docker-compose.yml
version: '3'
services:
tdengine:
image: tdengine/tdengine:3.2.0.0
ports: ["6030:6030", "6041:6041"]
volumes: ["tdengine-data:/var/lib/taos"]
emqx:
image: emqx:5.6.0
ports: ["1883:1883", "18083:18083"] # MQTT + 控制台
grafana:
image: grafana/grafana:10.2.0
ports: ["3000:3000"]
environment:
GF_SECURITY_ADMIN_PASSWORD: admin
volumes: ["grafana-data:/var/lib/grafana"]
volumes:
tdengine-data:
grafana-data:
bash
bash
docker compose up -d
TDengine 里建库建表(超级表 + 子表,每站一张子表是 TDengine 的最佳实践):
sql
sql
-- 建库: 保留 365 天, 按天分片, 10 年归档
CREATE DATABASE weather KEEP 365 DURATION 1d;
USE weather;
-- 超级表 (模板)
CREATE STABLE s_weather (
ts TIMESTAMP,
t FLOAT, h FLOAT, p FLOAT,
lux FLOAT, rain INT, pm25 FLOAT,
batt FLOAT, rssi INT
) TAGS (station_id BINARY(16));
-- 每站一张子表 (首次收到数据时自动建, 见消费端代码)
四、Python 消费端:订阅 MQTT → 落库
这是数据落地的核心。用 paho-mqtt 订阅 + 通配所有站点,executemany 批量写 TDengine(REST 接口,免驱动):
python
python
import paho.mqtt.client as mqtt
import requests, json, time
TDENGINE_URL = "http://localhost:6041/rest/sql/weather"
USER, PASS = "root", "taosdata"
def write_to_tdengine(rows):
# rows: [(station, ts, t, h, p, lux, rain, pm25, batt, rssi), ...]
sql = "INSERT INTO "
# 按站点分表写入 (TDengine 子表)
tables = {}
for r in rows:
st = r[0]
tables.setdefault(st, []).append(r[1:])
stmt = ""
for st, vals in tables.items():
stmt += f"station_{st} USING s_weather TAGS('{st}') VALUES "
for v in vals:
stmt += f"('{v[0]}',{v[1]},{v[2]},{v[3]},{v[4]},{v[5]},{v[6]},{v[7]},{v[8]}) "
requests.post(TDENGINE_URL, auth=(USER, PASS), data=stmt.encode())
buffer = []
def on_message(client, userdata, msg):
payload = json.loads(msg.payload)
station = payload["device"]
row = (station, payload["ts"], payload["t"], payload["h"],
payload["p"], payload["lux"], payload["rain"],
payload["pm25"], payload["batt"], payload["rssi"])
buffer.append(row)
if len(buffer) >= 50: # 攒批写, 减少 IO
write_to_tdengine(buffer); buffer.clear()
client = mqtt.Client()
client.on_message = on_message
client.connect("localhost", 1883, 60)
client.subscribe("weather/+") # 订阅所有站点
client.loop_forever()
关键优化:**攒批写(50 条一批)**而非每条直写,TDengine 的写入吞吐能差一个数量级。这也是时序库 vs MySQL 的一大优势------批量追加写极快。
五、Grafana 看板:把曲线画出来
Grafana 加 TDengine 数据源(装 tdengine-datasource 插件或用 SimpleJSON),然后建 Dashboard。一个站的温度/湿度/气压面板 SQL:
sql
sql
-- 温度曲线 (过去 24h, 1 分钟降采样)
SELECT _wstart, AVG(t) FROM s_weather
WHERE ts > NOW - 24h
INTERVAL(1m) FILL(PREV);
-- 多站对比 (气温)
SELECT station_id, AVG(t) FROM s_weather
WHERE ts > NOW - 1h
GROUP BY station_id;
Grafana 里拖几个 Panel:温度时序图、湿度时序图、气压变化(天气预报看气压趋势!)、PM2.5 柱状、以及一张"设备健康度"表格(batt/rssi 低于阈值标红)。一张看板,N 台站一目了然。
六、告警:别等数据异常了你才知道
用 Grafana Alerting 或 TDengine 的流式计算,配置三条关键告警:
- 高温告警 :
SELECT AVG(t) FROM s_weather WHERE ts>NOW-5m INTERVAL(1m) > 38→ 邮件/钉钉通知(大棚/机房场景重要)。 - 降雨告警 :
rain = 1持续 → 通知(联动灌溉/排水)。 - 低电量/离线告警 :
batt < 3.4或NOW - last_ts > 15m(断网/没电静默死亡,呼应①篇的 batt/rssi 设计)。
sql
sql
-- 离线检测: 某站最近 15 分钟没数据 (断网/没电)
SELECT station_id FROM s_weather
WHERE ts < NOW - 15m
GROUP BY station_id; -- 配合定时任务或流式规则触发告警
七、现场踩坑 5 连
- 消费端挂了数据全丢:MQTT 用 QoS 0 则 broker 不留存。对策:broker 开"保留消息 + 会话",或消费端做持久队列(如 Kafka 中转,呼应我们第六周 Kafka/Pulsar)。
- 时钟漂移导致时间错乱:ESP32 用 NTP 同步(①篇坑 2),否则断网补传的时间戳乱序,时序库曲线断裂。
- TDengine 子表爆炸:每站一张子表是对的,但别用"每 10 分钟一张表",会炸。坚持"每设备一张子表"。
- Grafana 时区坑 :默认 UTC,看板时间比本地差 8 小时。对策:Grafana 配
timezone: Asia/Shanghai。 - 批量写阻塞主循环:消费端写库慢会拖慢 MQTT 接收。对策:写库放独立线程/协程,接收和落库解耦。
八、总结与下篇预告
② 篇我们把① 篇上送的数据真正"接住并变现":用 TDengine(或 InfluxDB)时序库 替代 MySQL,享受高频追加+降采样+自动 TTL 的红利;用 Docker Compose 一键拉起 EMQX + TDengine + Grafana ;写了攒批落库的 Python 消费端 ;在 Grafana 里画出温度/湿度/气压曲线并配置高温/降雨/低电量/离线四类告警。现在你的气象站不再是"会发数据的盒子",而是一个可观测、可告警、能回看的微型气象系统。
下一篇( 云端 AI 预测),我们兑现"AI 天气站"的 AI 部分:用历史时序训练一个轻量预测模型(Prophet / LSTM / 甚至下沉 TinyML 到 ESP32),预测未来几小时的温度与降雨概率,并把预测结果画进同一张 Grafana 看板。到那时,它就真的从"气象记录仪"升级成"天气预言机"了。
下一篇预告
《AI 天气站 ③:云端 AI 预测》------兑现"AI 天气站"的 AI 部分:用历史时序训练轻量预测模型(Prophet / LSTM / 或下沉 TinyML 到 ESP32),预测未来几小时温湿度与降雨概率,画进同一张 Grafana 看板。下一篇敬请期待。
系列导航: