

MySQL 撑了 8 个月就顶不住了
万村乐数字乡村里跟物联网相关的场景不算少:种养管理要看大棚温湿度,河长制要看水位和水质,人居环境要看垃圾房满溢,还有变压器、水泵这类设备的运行状态。这些点位一开始全塞在 MySQL 8.0.28 的一张iot_metric表里,跟业务库共用实例。
数量级摊开看就明白问题在哪。一个村按 60 个采集点算,每点每分钟上报 1 次,一天是 86400 条;平台上跑几百个村,日增就是几千万行。8 个月之后那张表接近 40 亿行,SELECT一个大棚近 7 天的曲线要跑十几秒,磁盘也涨到不敢看。加索引没用,问题不在索引,在于关系库的行存对这种写多读少、按时间范围扫描的负载本来就不划算。
选型时我们的约束条件很现实
万村乐所有版本都支持私有化独立部署,客户机器配置参差不齐,不少县里给的是 4 核 8G 的虚拟机。这条约束直接砍掉了一批方案:需要 ZooKeeper、需要独立元数据节点、需要三副本起步的,一律不考虑。
我们对比过三个方向。InfluxDB 单机版好用,但开源版没有集群,扩容路径不清晰;ClickHouse 查询很猛,可运维复杂度和内存占用对 8G 机器不友好;TDengine 的超级表模型跟设备台账的结构贴得比较近,单机部署一个二进制就能跑,综合下来选了它。生态上要考虑一点:万村乐的可视化大数据平台原生支持 MySQL 数据源,时序库这边需要额外做一层查询适配。
建模:超级表 + 一设备一子表
关系库时代我们习惯把所有指标堆在一张宽表里,时序库不能这么干。TDengine 的做法是一类设备建一张超级表,每个设备实例自动派生一张子表,标签列存不变的属性。
village_id放进标签列是刻意的。万村乐后台的权限按省、市、县、镇、村五级划分,时序库这边没有 MybatisPlus 的租户拦截器帮忙,只能靠标签过滤在查询层收口,服务端拼 SQL 时强制带上当前会话的区划范围,绝不让前端传什么就查什么。
按标签查询的写法跟宽表完全不一样,一次能横扫一整个村:
降采样和保留策略,是省钱的关键
原始数据按分钟存,查一年的曲线就是 52 万个点,浏览器画不出来也没人看得清。我们分了三档:原始数据保留 30 天,5 分钟粒度保留 1 年,1 小时粒度长期保留。
保留期用数据库的 keep 参数控制,到期自动清理,不用写定时任务删数据。这一点比 MySQL 省心不少,以前删历史数据要分批DELETE,锁表锁到运维半夜起来盯。
三档存下来,磁盘占用大约是原来全量存 MySQL 的十分之一量级。落到县级私有化机器上,一年的历史数据从原本几百 GB 压到了几十 GB,备份也变得可行。

写入侧:批量、乱序和补传
设备上报走 MQTT,网关订阅之后转写时序库。这里有三个细节值得记一笔。
批量写入必须做。逐条INSERT时单机每秒只能扛几千行,攒批到 500 条或者 200 毫秒触发一次,吞吐量就上去了。攒批逻辑写在网关侧,用一个定长队列加定时刷。
乱序数据要允许。村里网络不稳,设备断网后会把缓存的历史点一次性补传,时间戳落在几小时前。时序库对乱序写入有时间窗限制,超出窗口的会被丢弃,我们把这个窗口按业务放宽到 24 小时,再长的补传数据走离线导入通道,单独写进降采样表。
设备时钟不可信。有一批太阳能供电的水位计掉电重启后时间回到出厂值,上报的时间戳是 2020 年。网关加了一道校验,设备时间和服务端时间偏差超过 2 小时就用服务端时间覆盖,同时打日志报点位异常,让运维去现场校时。
告警判定放在流里做,不放在轮询里
早期告警是定时任务每分钟扫一遍全表比阈值,几百个村扫下来一轮要好几十秒,还越跑越慢。改成流式计算之后,判定跟着写入走。
规则本身存在 MySQL 业务库里,网关启动时加载,支持按村覆盖默认阈值。判定结果不直接推给村干部,先过一道持续时长过滤:温度超阈值持续 10 分钟才报警,避免开棚门那一瞬间的尖峰刷屏。这套逻辑跟安防那边的连续命中是同一个思路,做基层系统,控制打扰比追求灵敏更重要。
告警产生之后走万村乐原有的消息推送网关,短信、微信模板消息、APP Push 三条通道按用户偏好投递,事件同时落进设备台账的历史记录,供后面复盘。
迁移过程
存量数据没有全量搬。我们只把近 90 天的数据用脚本按村分批导进时序库,更早的历史留在 MySQL 归档表里,前端查一年以上的数据时走另一个接口。双写跑了两周,比对两边的日均值和极值确认一致后,才把读流量切过去,MySQL 那张大表确认无误后直接DROP,磁盘一次性回收了 300 多 GB。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。