在大数据时代,高效的数据存储和管理是科研和工业应用成功的关键。HDF5 是一种嵌套的实验数据管理格式,TsFile 作为新型的时序数据存储格式,各自具有独特的特点和优势。本文将深入探讨 HDF5 的起源、应用场景、存在的问题,以及 TsFile 和 HDF5 的异同点。
01 HDF5 起源
HDF5,全称 Hierarchical Data Format 分层数据格式,包含一整套用于存储和管理数据的数据模型、库和二进制文件格式。它起源于 1987 年,由美国国家超级计算应用中心(NCSA)的 GFTF 小组提出。
HDF5 的初衷是为了实现一种架构无关的文件格式,满足 NCSA 在使用多种不同计算平台之间移送科学数据的需求。
02 应用场景
HDF5 的应用场景主要包括在科学计算、工程模拟、气象预测等领域的实验数据管理需求。
场景一:科学数据存储
在科学计算和研究领域,经常需要存储和处理具有复杂结构的多维数据,如多维矩阵和气象网格数据。这些数据集通常包含大量的元数据,需要一个能够提供高效数据组织和访问方式的存储解决方案。
场景二:设备传感器数据存储
在设备监控和传感器网络中,需要存储来自各种传感器的大量数据。例如,某航空机构的结构健康监测系统的数据,这些数据包括各种传感器采集的振动、温度等信息,对设备的状态监测和故障预测具有重要意义。
场景三:粒子仿真数据存储
在粒子仿真领域,仿真程序可能会产生大量的实验数据,包括粒子的轨迹、能量沉积等信息。这些数据对于理解物理过程和优化仿真参数具有重要意义,需要一个能够高效存储和管理这些数据的系统。
03 TsFile 简介
在 HDF5 的应用场景中,有不少实验数据的格式其实是时序数据,TsFile 则是专为时序数据设计的列式存储文件格式,由清华大学软件学院团队主导研发,并于 2023 年成为 Apache 顶级项目。TsFile 格式的优势为高性能、高压缩比、自解析、支持灵活的时间范围查询。
04 TsFile 与 HDF5 对比
下面从不同维度对 TsFile 与 HDF5 进行详细的对比:
(1) 压缩比
- TsFile:结合时序专用编码(如 TS_2DIFF 时间戳差值编码、GORILLA 浮点数压缩等)与多种高效压缩算法(如 SNAPPY、ZSTD、LZ4 等),通过协同优化消除数据冗余。对于变长对象,动态分配变长数据空间,避免字节对齐填充;采用紧凑存储策略减少冗余,尤其适配稀疏数据和变长字符串场景。
- HDF5:仅依赖通用压缩算法(如 gzip、lzf、szip),缺乏针对时序数据的编码,无法充分利用数据特征进行优化。对于变长对象,采用固定空间分配(如复合数据类型),导致字节对齐浪费和稀疏数据存储冗余高。
(2) 查询过滤能力
- TsFile:提供了强大的查询过滤能力,支持根据序列 ID 和时间范围精确读取特定范围的数据,无需读取全量数据。
- HDF5:仅支持全量数据读取,无法高效过滤特定范围数据。
(3) 数据模型
- TsFile:专为时序数据设计,数据模型能够更好地适应时间序列数据的特征,采用轻量化的时间戳-数据点的模型,结构简洁高效。
- HDF5:支持多维数组、复合类型等复杂结构,但模型复杂度高。
05 使用示例对比
以下两个文件格式接口示例所使用数据的元数据信息为:在一个工厂 factory1 当中的设备 device1 上产生的数据,数据信息含有(时间 time long,值 s1 long)。
(1) TsFile 写入示例
- // 创建一个名为test的 tsfile文件
- file.create("test.tsfile", O_WRONLY | O_CREAT | O_TRUNC, 0666);
- // 创建表元数据来描述在tsfile当中的表信息
- auto* schema = new storage::TableSchema(
- "factory1",
- {
- common::ColumnSchema("id", common::STRING, common::LZ4, common::PLAIN, common::ColumnCategory::TAG),
- common::ColumnSchema("s1", common::INT64, common::LZ4, common::TS_2DIFF, common::ColumnCategory::FIELD),
- });
- // 使用文件句柄和表元数据信息,创建表数据的写入器
- auto* writer = new storage::TsFileTableWriter(&file, schema);
- // 用写入数据的元数据信息构建tablet,用于批量写入数据
- storage::Tablet tablet("factory1", {"id1", "s1"}, {common::STRING, common::INT64}, {common::ColumnCategory::TAG, common::ColumnCategory::FIELD}, 10);
- // 遍历数据 将其组织为 tablet
- for (int row = 0; row < 5; row++) {
- long timestamp = row;
- tablet.add_timestamp(row, timestamp);
- tablet.add_value(row, "id1", "machine1");
- tablet.add_value(row, "s1", static_cast<int64_t>(row));
- }
- // 将tablet的数据写入磁盘
- writer->write_table(tablet);
- // 将内存当中剩余的相关数据都写入磁盘
- writer->flush();
- // 关闭写入器
- writer->close();
复制代码 (2) HDF5 写入示例
- typedefstruct {
- long time;
- long s1;
- } Data;
- // 创建一个名为test的hdf5文件,H5F_ACC_TRUNC说明如果文件已经存在,会覆盖原来的文件
- file_id = H5Fcreate("test.h5", H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
- // 创建一个group挂载在rootgroup下面
- group_id = H5Gcreate2(file_id, "factory1", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
- // 准备数据维度数组,构建dataspace
- hsize_t dims[1] = { (hsize_t)rows };
- hid_t dataspace_id = H5Screate_simple(1, dims, NULL);
- // 准备数据类型,构建datatype
- hid_t datatype_id = H5Tcreate(H5T_COMPOUND, sizeof(Data));
- H5Tinsert(filetype, "time", 0, H5T_NATIVE_LONG);
- H5Tinsert(filetype, "s1", sizeof(long), H5T_NATIVE_LONG);
- // 设置数据集的chunk块大小以及压缩信息
- hid_t dcpl = H5Pcreate(H5P_DATASET_CREATE);
- // 设置 chunk 尺寸,chunk 尺寸不能大于数据集尺寸
- hsize_t chunk_dims[1] = { (hsize_t)row };
- H5Pset_chunk(dcpl, 1, chunk_dims);
- // 设置 GZIP 压缩,压缩级别为1
- H5Pset_deflate(dcpl, 1);
- // 创建一个dataset挂载在前面的“factory1”group下面
- dataset_id = H5Dcreate2(group_id, "machine1", datatype_id, dataspace_id, H5P_DEFAULT, dcpl, H5P_DEFAULT);
- // 构建数据容器和填充数据
- Data *dset = (Data *)malloc(rows * sizeof(Data));
- for (int i = 0; i < rows; i++) {
- dset[i].time = static_cast<int64_t>(i);
- dset[i].s1 = static_cast<int64_t>(i)
- }
- // 将数据写入到dataset当中
- status = H5Dwrite(dataset_id, datatype_id, H5S_ALL, H5S_ALL, H5P_DEFAULT, dset);
- // 关闭前面所创建的对象
- free(dset);
- status = H5Dclose(dataset_id);
- status = H5Gclose(group_id);
- status = H5Sclose(dataspace_id);
- status = H5Fclose(file_id);
复制代码 (3) TsFile 查询示例
[code]// 使用tsfilereader来打开名为test的tsfile文件storage::TsFileReader reader;reader.open("test.tsfile");// 指定想要查询的列名storage::ResultSet* temp_ret = nullptr;std::vector columns;columns.emplace_back("id1");columns.emplace_back("s1");// 指定查询的表名,查询的列,时间范围,查询结果会放置于最后的指针当中reader.query("factory1", columns, 0, 100, temp_ret);auto ret = dynamic_cast(temp_ret);// 检查查询是否异常,并不断next获取结果bool has_next = false;while ((code = ret->next(has_next)) == common::E_OK && has_next) { std::cout get_value(1) |