3、数据存储与索引:分布式存储(HDFS、MinIO)、时序数据库(InfluxDB)、空间索引(R-Tree)与检索策略

数据存好了,怎么存得稳、查得快,是路采系统能不能用的关键。

说实话,我见过不少团队,采集车跑了一整天,回来发现数据写坏了,或者回放时卡成幻灯片。嗯,问题往往就出在存储和索引上。

3.1 分布式存储:HDFS 与 MinIO

路采数据有多大?一台车,64线激光雷达+6路摄像头,一小时轻松上百GB。单机硬盘根本扛不住。所以,分布式存储是必须的。

3.1.1 HDFS:适合大文件批处理

HDFS 是 Hadoop 生态的老将。它的设计理念很简单:文件切块(默认128MB),多副本(默认3副本),分散存储。

我个人的习惯:HDFS 适合存原始数据包(.pcap、.bag)。这些文件大、写一次读多次,正好对路。

关键配置项:
  • dfs.replication:副本数。路采数据建议设2,省钱又安全。
  • dfs.blocksize:块大小。激光雷达数据建议256MB,减少元数据压力。
  • dfs.namenode.name.dir:NameNode 元数据路径。一定要配多个磁盘,否则 NameNode 挂了整个集群就废了。
我曾经踩过的坑: 有一次 HDFS 集群的 DataNode 磁盘写满,导致数据写入失败。排查半天才发现是磁盘均衡策略没配好。后来我加了 balancer 定时任务,再没出过问题。

3.1.2 MinIO:兼容 S3 的轻量方案

如果你觉得 HDFS 太重,MinIO 是个好选择。它兼容 AWS S3 接口,部署简单,性能也不错。

为什么我推荐 MinIO?

  • 单机就能跑,适合小团队或边缘节点。
  • 支持纠删码(Erasure Coding),比副本更省空间。
  • 有 Web 管理界面,方便查看数据。

我在项目中常用 MinIO 存中间处理结果,比如去噪后的点云、压缩后的图像。这些数据访问频繁,MinIO 的 S3 接口让集成变得很简单。

小技巧: MinIO 的 bucket 命名建议用 项目名_传感器类型_日期 的格式,比如 projectA_lidar_20241001。这样后续检索时一目了然。

3.2 时序数据库:InfluxDB

路采数据里,除了点云和图像,还有大量时序信号:车速、GPS 坐标、IMU 姿态、CAN 总线数据。这些数据的特点是:高频写入、按时间查询、很少更新

传统关系数据库(MySQL、PostgreSQL)处理这种场景效率很低。你想想看,每秒几千条写入,还要做时间范围查询,MySQL 的 B+ 树索引很快就扛不住了。

这时候,时序数据库就派上用场了。我个人最常用的是 InfluxDB。

3.2.1 InfluxDB 的核心概念

概念 说明 路采场景示例
Measurement 类似关系数据库的表 vehicle_speedgps_position
Tag 可索引的元数据 vehicle_idsensor_type
Field 实际数值 speed_kmhlatitude
Timestamp 时间戳(纳秒精度) 2024-10-01T10:30:00.123456789Z

写入示例:

# 写入车速数据
INSERT vehicle_speed,vehicle_id=car01,sensor_type=can speed_kmh=65.3 1727764200000000000

# 写入 GPS 数据
INSERT gps_position,vehicle_id=car01 latitude=39.9042,longitude=116.4074 1727764200000000000

3.2.2 查询优化策略

InfluxDB 的查询性能,很大程度上取决于 Tag 的设计。我见过有人把所有字段都设成 Tag,结果索引爆炸,写入变慢。

我的建议:

  • Tag 只放基数低的字段,比如车辆ID、传感器类型。
  • Field 放数值型数据,比如速度、加速度。
  • 时间戳用纳秒精度,避免时间重叠导致数据覆盖。
避坑指南: 我曾经在 InfluxDB 里存了 IMU 的原始数据,每秒 200 条写入。结果发现查询时经常超时。后来我把数据做了降采样(Downsampling),每 10 秒聚合一次,查询速度提升了 10 倍。

3.3 空间索引:R-Tree 与检索策略

路采数据里,空间信息无处不在。比如:这帧点云是在哪个路口采集的? 这段视频覆盖了哪些路段?

传统的一维索引(B+ 树)处理不了这种二维空间查询。你需要的是空间索引。

3.3.1 R-Tree 原理

R-Tree 的核心思想是:用最小外接矩形(MBR)来近似表示空间对象。每个节点对应一个矩形区域,子节点的矩形完全包含在父节点内。

查询时,从根节点开始,判断查询区域与节点矩形是否相交。如果相交,就继续往下查;如果不相交,直接跳过。

说白了,这就是一种空间上的二分法。只不过二分的是二维空间,而不是一维数值。

3.3.2 在路采数据中的应用

我常用的做法是:

  1. 把每段路采数据的地理范围(经纬度边界)存成 MBR。
  2. 用 R-Tree 索引这些 MBR。
  3. 查询时,输入一个地理区域(比如某个路口方圆 500 米),R-Tree 快速返回所有相关的数据段。

代码示例(Python + RTree 库):

import rtree

# 创建 R-Tree 索引
idx = rtree.index.Index()

# 插入数据段:id=0,MBR=(经度最小, 纬度最小, 经度最大, 纬度最大)
idx.insert(0, (116.40, 39.90, 116.41, 39.91))

# 查询:某个路口方圆 500 米
query_area = (116.405, 39.905, 116.415, 39.915)
results = list(idx.intersection(query_area))
print(f"命中数据段 ID: {results}")
小提示: 如果数据量很大(百万级),建议用 PostGIS 或 Elasticsearch 的 Geo 功能。它们底层都用了 R-Tree 的变种,而且支持分布式。

3.3.3 检索策略:时间+空间联合查询

实际场景中,你往往需要同时按时间和空间检索。比如:找出 2024年10月1日 上午10点到11点,经过北京国贸路段的激光雷达数据。

我的策略是:

  • 先时间后空间:先用 InfluxDB 的时间索引缩小范围,再用 R-Tree 做空间过滤。
  • 或者反过来:如果空间范围很小,先空间后时间。
  • 混合索引:有些系统支持时空联合索引(如 Geomesa),但配置复杂,我一般不用。
注意: 千万别在查询时做全表扫描。我见过有人写 SQL 直接 WHERE time BETWEEN ... AND ... 不加索引,结果查一次要 10 分钟。嗯,后来加了时序索引和空间索引,查询时间降到了 1 秒以内。

3.4 总结

数据存储和索引,说白了就是用空间换时间,用结构换效率

  • 大文件用 HDFS 或 MinIO 存。
  • 时序信号用 InfluxDB 管。
  • 空间位置用 R-Tree 索引。
  • 查询时,时间+空间联合检索,效率最高。

我个人觉得,这部分是路采系统里最容易被忽视的。很多人只关注算法,结果数据存得乱七八糟,回放时卡成狗。嗯,希望你能避开这些坑。