跳转至

深入专题:时序、告警与容量

这篇从一个具体规模开始,推导数据模型、写入、查询、聚合、告警和 OTA 为什么需要不同的处理路径。

场景与假设

设备:100,000 台
在线率:80%
心跳:30 秒一次
遥测:在线设备每 10 秒一条
每条遥测:20 个指标
原始 JSON:约 600 字节

平均心跳

100,000 ÷ 30 ≈ 3,333 条/秒

平均遥测

80,000 ÷ 10 = 8,000 条/秒

每天遥测消息

8,000 × 86,400 ≈ 6.91 亿条/天

如果把 20 个指标拆成 20 行:

约 138 亿指标行/天

这说明数据模型选择会让行数产生数量级差异。

不要只用原始 JSON 大小估算

存储占用还包括:

  • 行头和对齐;
  • 时间、tenant_id、device_id;
  • 索引;
  • WAL;
  • 副本;
  • 备份;
  • 临时排序;
  • 聚合表。

600 字节消息不等于数据库只增加 600 字节。

粗估时可以使用放大系数,之后必须用真实样本导入验证。

宽表、窄表与 JSON

宽表

time, device_id, temperature, pressure, vibration, ...

优点:

  • 一条上报一行;
  • 常见指标查询简单;
  • 行数较少。

缺点:

  • 不同型号字段差异大;
  • Schema 变化;
  • 稀疏列。

窄表

time, device_id, metric, value

优点:

  • 指标灵活;
  • 通用查询。

缺点:

  • 行数极大;
  • 单次上报拆多行;
  • 多指标组合查询更复杂。

JSON/JSONB

优点:

  • 灵活;
  • 保留原始结构。

缺点:

  • 高频聚合和索引代价;
  • 类型和单位治理困难;
  • 随意查询可能很慢。

常见折中:

  • 原始消息归档到对象存储;
  • 关键稳定指标进入宽表;
  • 少量扩展字段进入 JSON;
  • 最新状态单独保存;
  • 根据业务聚合。

写入路径为什么要批量

每条消息单独事务:

8,000 次/秒事务

事务提交、WAL 刷盘和网络往返成本很高。

可以:

MQ
→ Consumer 收集 100~1000 条或等待 50ms
→ 批量 COPY/INSERT

取舍:

  • 批次越大,吞吐更高;
  • 等待越久,写入可见延迟更高;
  • Consumer 崩溃时未提交批次会重投;
  • 批量仍需 message_id/约束处理重复。

分区和 Chunk

按时间分区使查询:

WHERE time >= now() - interval '1 day'

只扫描相关时间范围。

Chunk 太小:

  • 表和元数据太多;
  • 规划成本高。

Chunk 太大:

  • 热索引难以留在内存;
  • 压缩、删除和维护粒度太粗。

选择需要结合:

  • 每天写入量;
  • 活跃查询时间范围;
  • 内存;
  • 保留策略;
  • 压缩周期。

索引从查询反推

常见查询:

SELECT time, temperature
FROM telemetry
WHERE tenant_id = ?
  AND device_id = ?
  AND time >= ?
  AND time < ?
ORDER BY time;

候选索引围绕:

tenant_id, device_id, time

如果系统保证 device_id 全局唯一,tenant_id 可能不必重复进入所有索引;但权限查询仍必须验证租户。

不要为 20 个指标分别无脑加索引。写入会被索引维护拖慢。

最新值与历史值分离

页面看当前温度:

错误:

每次从 6.9 亿条/天的历史表 ORDER BY time DESC LIMIT 1

即使有索引,也会给高频列表查询增加压力。

维护最新状态:

device_latest_metric
或 Redis Hash/String

更新需要版本/时间判断,避免晚到旧数据覆盖新值。

只有 incoming_time >= current_time 才更新

如果设备时钟不可信,使用 server_received_time 或同时保存两者。

聚合怎样保持可合并

5 分钟聚合:

count
sum
min
max
first
last

小时平均不能简单平均十二个“5 分钟平均”,因为每个窗口样本数可能不同:

正确小时平均 = sum(各窗口 sum) / sum(各窗口 count)

这就是为什么聚合表应保留可继续合并的中间量。

迟到数据

设备断网后补发一小时前数据:

  • 原始表按事件时间写入旧 Chunk;
  • 已生成的 5 分钟聚合需要刷新;
  • 实时看板是否显示补发值要明确;
  • 告警是否补触发要有业务规则。

“历史数据正确”和“实时告警不扰民”可能需要不同策略。

告警如何避免风暴

单设备抖动

80.1 → 79.9 → 80.2

使用:

  • 持续时间;
  • 连续次数;
  • 迟滞;
  • 冷却时间。

大规模共同故障

一个网关离线,500 台子设备离线。

如果每台都:

创建告警
→ 发短信
→ 创建工单

会形成通知风暴。

需要拓扑和根因抑制:

网关离线主告警
→ 子设备标记受影响
→ 聚合展示
→ 只通知主告警

告警去重键

tenant + resource + rule + dimension + active_epoch

持续异常更新同一活动告警的次数和 last_seen,恢复后关闭。下次重新异常创建新的 epoch。

OTA 对容量的冲击

10,000 台设备下载 50MB 固件:

总数据约 500GB

如果 10 分钟内同时下载:

约 6.7Gbps,不含协议开销

因此 OTA 需要:

  • CDN/对象存储;
  • 分批和速率限制;
  • 设备随机抖动;
  • 地区和网络分组;
  • 下载断点续传;
  • 不通过业务 API 服务器转发整个固件;
  • 监控下载、校验、安装、重启各阶段。

容量不是只有“能扛住”

还要定义降级:

  • 遥测积压时先保证命令和心跳;
  • 报表变慢时不影响实时状态;
  • AI 分析拥堵时回到规则告警;
  • OTA 高峰限制新批次;
  • 非关键 WebSocket 推送可以抽样或合并。

这要求不同流量使用独立 Topic、队列、消费者和资源配额,避免互相拖垮。

一份可信容量报告

至少包含:

  1. 假设;
  2. 平均与峰值;
  3. 每层放大;
  4. 单机实测;
  5. 瓶颈;
  6. 扩展方式;
  7. 降级;
  8. 监控;
  9. 未验证风险。

不要只写“Redis 单机十万 QPS,所以可以支持十万设备”。设备数、消息率、查询率和状态变化率不是同一个量。

深度自测

  1. 一条 600 字节 JSON 为什么不等于数据库增加 600 字节?
  2. 宽表、窄表、JSON 各自最主要的取舍是什么?
  3. 批量写入为什么提高吞吐,又引入什么风险?
  4. 为什么小时平均不能直接平均 5 分钟平均?
  5. 晚到数据对历史聚合和实时告警有什么不同影响?
  6. 网关离线怎样避免 500 条通知?
  7. 10,000 台设备 OTA 为什么不能同时开始?
  8. “十万设备”为什么不是一个足够的容量指标?
参考答案要点
  1. 还有行元数据、索引、WAL、副本、备份和临时空间。
  2. 宽表查询简单但 Schema 固定;窄表灵活但行数大;JSON 灵活但治理和聚合成本高。
  3. 减少事务和网络往返,但增加可见延迟、内存批次和崩溃重投。
  4. 每个窗口样本数可能不同,要用总 sum/总 count。
  5. 历史应修正聚合;实时告警是否补触发要防止迟到风暴。
  6. 识别拓扑根因,主告警通知,子设备事件聚合抑制。
  7. 会形成带宽和服务洪峰,应灰度、限速和随机抖动。
  8. 还需连接率、消息率、消息大小、查询率、变化率、峰值和下游放大。

完成标准

  • 能完成消息、行数、存储和带宽估算
  • 能根据查询选择宽表、窄表或混合模型
  • 能解释批量写、分区、索引和聚合
  • 能处理迟到数据与最新值
  • 能设计告警抑制和 OTA 灰度
  • 能写一份包含降级的容量报告