首頁
>
資源
>
技術(shù)解析

深入解析 HDF5 與 TsFile:時(shí)序數(shù)據(jù)存儲的較量

在大數(shù)據(jù)時(shí)代,高效的數(shù)據(jù)存儲和管理是科研和工業(yè)應(yīng)用成功的關(guān)鍵。HDF5 是一種嵌套的實(shí)驗(yàn)數(shù)據(jù)管理格式,TsFile 作為新型的時(shí)序數(shù)據(jù)存儲格式,各自具有獨(dú)特的特點(diǎn)和優(yōu)勢。本文將深入探討 HDF5 的起源、應(yīng)用場景、存在的問題,以及 TsFile 和 HDF5 的異同點(diǎn)。

01 HDF5 起源

HDF5,全稱 Hierarchical Data Format 分層數(shù)據(jù)格式,包含一整套用于存儲和管理數(shù)據(jù)的數(shù)據(jù)模型、庫和二進(jìn)制文件格式。它起源于 1987 年,由美國國家超級計(jì)算應(yīng)用中心(NCSA)的 GFTF 小組提出。

HDF5 的初衷是為了實(shí)現(xiàn)一種架構(gòu)無關(guān)的文件格式,滿足 NCSA 在使用多種不同計(jì)算平臺之間移送科學(xué)數(shù)據(jù)的需求。

02 應(yīng)用場景

HDF5 的應(yīng)用場景主要包括在科學(xué)計(jì)算、工程模擬、氣象預(yù)測等領(lǐng)域的實(shí)驗(yàn)數(shù)據(jù)管理需求。

場景一:科學(xué)數(shù)據(jù)存儲

在科學(xué)計(jì)算和研究領(lǐng)域,經(jīng)常需要存儲和處理具有復(fù)雜結(jié)構(gòu)的多維數(shù)據(jù),如多維矩陣和氣象網(wǎng)格數(shù)據(jù)。這些數(shù)據(jù)集通常包含大量的元數(shù)據(jù),需要一個(gè)能夠提供高效數(shù)據(jù)組織和訪問方式的存儲解決方案。

場景二:設(shè)備傳感器數(shù)據(jù)存儲

在設(shè)備監(jiān)控和傳感器網(wǎng)絡(luò)中,需要存儲來自各種傳感器的大量數(shù)據(jù)。例如,某航空機(jī)構(gòu)的結(jié)構(gòu)健康監(jiān)測系統(tǒng)的數(shù)據(jù),這些數(shù)據(jù)包括各種傳感器采集的振動(dòng)、溫度等信息,對設(shè)備的狀態(tài)監(jiān)測和故障預(yù)測具有重要意義。

HDF5與TsFile對比圖1-20251126.png

場景三:粒子仿真數(shù)據(jù)存儲

在粒子仿真領(lǐng)域,仿真程序可能會產(chǎn)生大量的實(shí)驗(yàn)數(shù)據(jù),包括粒子的軌跡、能量沉積等信息。這些數(shù)據(jù)對于理解物理過程和優(yōu)化仿真參數(shù)具有重要意義,需要一個(gè)能夠高效存儲和管理這些數(shù)據(jù)的系統(tǒng)。

HDF5與TsFile對比圖2-20251126.png

03 TsFile 簡介

在 HDF5 的應(yīng)用場景中,有不少實(shí)驗(yàn)數(shù)據(jù)的格式其實(shí)是時(shí)序數(shù)據(jù),TsFile 則是專為時(shí)序數(shù)據(jù)設(shè)計(jì)的列式存儲文件格式,由清華大學(xué)軟件學(xué)院團(tuán)隊(duì)主導(dǎo)研發(fā),并于 2023 年成為 Apache 頂級項(xiàng)目。TsFile 格式的優(yōu)勢為高性能、高壓縮比、自解析、支持靈活的時(shí)間范圍查詢。

HDF5與TsFile對比圖3-20251126.png

04 TsFile 與 HDF5 對比

下面從不同維度對 TsFile 與 HDF5 進(jìn)行詳細(xì)的對比:

HDF5與TsFile對比圖4-20251126.png

(1) 壓縮比

  • TsFile:結(jié)合時(shí)序?qū)S镁幋a(如 TS_2DIFF 時(shí)間戳差值編碼、GORILLA 浮點(diǎn)數(shù)壓縮等)與多種高效壓縮算法(如 SNAPPY、ZSTD、LZ4 等),通過協(xié)同優(yōu)化消除數(shù)據(jù)冗余。對于變長對象,動(dòng)態(tài)分配變長數(shù)據(jù)空間,避免字節(jié)對齊填充;采用緊湊存儲策略減少冗余,尤其適配稀疏數(shù)據(jù)和變長字符串場景。

  • HDF5:僅依賴通用壓縮算法(如 gzip、lzf、szip),缺乏針對時(shí)序數(shù)據(jù)的編碼,無法充分利用數(shù)據(jù)特征進(jìn)行優(yōu)化。對于變長對象,采用固定空間分配(如復(fù)合數(shù)據(jù)類型),導(dǎo)致字節(jié)對齊浪費(fèi)和稀疏數(shù)據(jù)存儲冗余高。

(2) 查詢過濾能力

  • TsFile:提供了強(qiáng)大的查詢過濾能力,支持根據(jù)序列 ID 和時(shí)間范圍精確讀取特定范圍的數(shù)據(jù),無需讀取全量數(shù)據(jù)。

  • HDF5:僅支持全量數(shù)據(jù)讀取,無法高效過濾特定范圍數(shù)據(jù)。

(3) 數(shù)據(jù)模型

  • TsFile:專為時(shí)序數(shù)據(jù)設(shè)計(jì),數(shù)據(jù)模型能夠更好地適應(yīng)時(shí)間序列數(shù)據(jù)的特征,采用輕量化的時(shí)間戳-數(shù)據(jù)點(diǎn)的模型,結(jié)構(gòu)簡潔高效。

  • HDF5:支持多維數(shù)組、復(fù)合類型等復(fù)雜結(jié)構(gòu),但模型復(fù)雜度高。

05 使用示例對比

以下兩個(gè)文件格式接口示例所使用數(shù)據(jù)的元數(shù)據(jù)信息為:在一個(gè)工廠 factory1 當(dāng)中的設(shè)備 device1 上產(chǎn)生的數(shù)據(jù),數(shù)據(jù)信息含有(時(shí)間 time long,值 s1 long)。

(1) TsFile 寫入示例

// 創(chuàng)建一個(gè)名為test的 tsfile文件
file.create("test.tsfile", O_WRONLY | O_CREAT | O_TRUNC, 0666);

// 創(chuàng)建表元數(shù)據(jù)來描述在tsfile當(dāng)中的表信息
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),
    });

// 使用文件句柄和表元數(shù)據(jù)信息,創(chuàng)建表數(shù)據(jù)的寫入器
auto* writer = new storage::TsFileTableWriter(&file, schema);

// 用寫入數(shù)據(jù)的元數(shù)據(jù)信息構(gòu)建tablet,用于批量寫入數(shù)據(jù)
storage::Tablet tablet("factory1", {"id1", "s1"}, {common::STRING, common::INT64},  {common::ColumnCategory::TAG, common::ColumnCategory::FIELD}, 10);

// 遍歷數(shù)據(jù) 將其組織為 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的數(shù)據(jù)寫入磁盤
writer->write_table(tablet);
// 將內(nèi)存當(dāng)中剩余的相關(guān)數(shù)據(jù)都寫入磁盤
writer->flush();
// 關(guān)閉寫入器
writer->close();

(2) HDF5 寫入示例

typedef struct {
    long time;
    long s1;
} Data;

// 創(chuàng)建一個(gè)名為test的hdf5文件,H5F_ACC_TRUNC說明如果文件已經(jīng)存在,會覆蓋原來的文件
file_id = H5Fcreate("test.h5", H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);

// 創(chuàng)建一個(gè)group掛載在rootgroup下面
group_id = H5Gcreate2(file_id, "factory1", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);

// 準(zhǔn)備數(shù)據(jù)維度數(shù)組,構(gòu)建dataspace
hsize_t dims[1] = { (hsize_t)rows };
hid_t dataspace_id = H5Screate_simple(1, dims, NULL);

// 準(zhǔn)備數(shù)據(jù)類型,構(gòu)建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);

// 設(shè)置數(shù)據(jù)集的chunk塊大小以及壓縮信息
hid_t dcpl = H5Pcreate(H5P_DATASET_CREATE);
// 設(shè)置 chunk 尺寸,chunk 尺寸不能大于數(shù)據(jù)集尺寸
hsize_t chunk_dims[1] = { (hsize_t)row };
H5Pset_chunk(dcpl, 1, chunk_dims);
// 設(shè)置 GZIP 壓縮,壓縮級別為1
H5Pset_deflate(dcpl, 1);

// 創(chuàng)建一個(gè)dataset掛載在前面的“factory1”group下面
dataset_id = H5Dcreate2(group_id, "machine1", datatype_id, dataspace_id, H5P_DEFAULT, dcpl, H5P_DEFAULT);

// 構(gòu)建數(shù)據(jù)容器和填充數(shù)據(jù)
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)
}

// 將數(shù)據(jù)寫入到dataset當(dāng)中
status = H5Dwrite(dataset_id, datatype_id, H5S_ALL, H5S_ALL, H5P_DEFAULT, dset);

// 關(guān)閉前面所創(chuàng)建的對象
free(dset);
status = H5Dclose(dataset_id);
status = H5Gclose(group_id);
status = H5Sclose(dataspace_id);
status = H5Fclose(file_id);

(3) TsFile 查詢示例

// 使用tsfilereader來打開名為test的tsfile文件
storage::TsFileReader reader;
reader.open("test.tsfile");

// 指定想要查詢的列名
storage::ResultSet* temp_ret = nullptr;
std::vector<std::string> columns;
columns.emplace_back("id1");
columns.emplace_back("s1");

// 指定查詢的表名,查詢的列,時(shí)間范圍,查詢結(jié)果會放置于最后的指針當(dāng)中
reader.query("factory1", columns, 0, 100, temp_ret);
auto ret = dynamic_cast<storage::TableResultSet*>(temp_ret);

// 檢查查詢是否異常,并不斷next獲取結(jié)果
bool has_next = false;
while ((code = ret->next(has_next)) == common::E_OK && has_next) {
    std::cout << ret->get_value<Timestamp>(1) << std::endl; // 時(shí)間戳列是第1列,之后的數(shù)值列的索引號從1開始
    std::cout << ret->get_value<int64_t>(1) << std::endl;
}

// 關(guān)閉查詢結(jié)果指針和讀取器
ret->close();
reader.close();

(4) HDF5 查詢示例

// 打開已有的hdf文件
file_id = H5Fopen("test.h5", H5F_ACC_RDONLY, H5P_DEFAULT);
// 打開rootgroup下面指定名字的group
group_id = H5Gopen2(file_id, "factory1", H5P_DEFAULT);
// 打開 factory1 這個(gè)group下面的指定名字的dataset
dataset_id = H5Dopen2(group_id, "machine1", H5P_DEFAULT);
// 獲取dataset的datatype和dataspace,為后續(xù)準(zhǔn)備結(jié)果容器做準(zhǔn)備
datatype_id = H5Dget_type(dataset_id);
dataspace_id = H5Dget_space(dataset_id);

// 拿到dataset的維度信息
int ndims = H5Sget_simple_extent_ndims(dataspace_id);
H5Sget_simple_extent_dims(dataspace_id, dims, NULL);

// 根據(jù)維度信息構(gòu)建結(jié)果數(shù)組容器
int rows = (int)dims[0];
Data *dset = (Data *)malloc(rows * sizeof(Data));

// 根據(jù)維度信息和數(shù)據(jù)類型,從dataset當(dāng)中讀取出結(jié)果
status = H5Dread(dataset_id, filetype, H5S_ALL, H5S_ALL, H5P_DEFAULT, dset);    

// 遍歷輸出所有數(shù)據(jù)
for (int i = 0; i < rows; i++) {
    printf("Row %d: time: %ld, s1: %ld", i, dset[i].time, dset[i].value);
}

// 關(guān)閉所有打開的資源
free(dset);
status = H5Dclose(dataset_id);
status = H5Gclose(group_id);
status = H5Sclose(dataspace_id);
status = H5Fclose(file_id);

(5) 接口對比

寫入方面

  • 元數(shù)據(jù)組織:

    由于 TsFile 的數(shù)據(jù)邏輯結(jié)構(gòu)是二維的 table,因此構(gòu)建 writer 僅需 table 的名字以及列的數(shù)據(jù)類型信息。

    而 HDF5 的最底層的數(shù)據(jù)邏輯結(jié)構(gòu)是 dataset,其支持復(fù)雜數(shù)據(jù)類型以及多維數(shù)據(jù),在構(gòu)建的時(shí)候,需要數(shù)據(jù)的維度信息以及復(fù)雜數(shù)據(jù)類型信息。

  • 數(shù)據(jù)組織:

    TsFile 需要將數(shù)據(jù)組織為其獨(dú)有的 tablet 結(jié)構(gòu),其中會將時(shí)間列數(shù)據(jù)進(jìn)行單獨(dú)的組織,從而實(shí)現(xiàn)數(shù)據(jù)的批量寫入。

    而 HDF5 當(dāng)中則是將數(shù)據(jù)組織為多維數(shù)組,所有的數(shù)據(jù)類型都是一視同仁的,其接口內(nèi)部會根據(jù)數(shù)組當(dāng)中的偏移量直接將數(shù)據(jù)序列化到磁盤。

查詢方面

  • 數(shù)據(jù)復(fù)雜查詢:

    HDF5 當(dāng)中更加確切的描述為數(shù)據(jù)的讀取,因?yàn)槠渥x取是一個(gè) chunk 的數(shù)據(jù)或者全部數(shù)據(jù),數(shù)據(jù)的處理工作則是與 HDF5 分離。

    相比之下,TsFile 所支持的是一種數(shù)據(jù)查詢的工作,在數(shù)據(jù)獲取上是并不全量的讀取,而是可以支持僅讀取一個(gè) table 當(dāng)中的部分列數(shù)據(jù),同時(shí)還支持對讀取的列數(shù)據(jù)進(jìn)行時(shí)間戳或者數(shù)值的過濾,這種過濾下推到文件層級,可以有效的減少傳輸?shù)臄?shù)據(jù)流量。

  • 結(jié)果組織:

    由于 TsFile 的結(jié)構(gòu)固定為二維的 table,所以僅需獲取列的數(shù)據(jù)類型就可以完成讀取的準(zhǔn)備工作,同時(shí),TsFile 的數(shù)據(jù)按批獲取的,對于較大數(shù)據(jù)量的讀取工作,可以大大減輕內(nèi)存的負(fù)載。

    相比之下,HDF5 的 dataset 由于維度和數(shù)據(jù)類型相對較為復(fù)雜,需要根據(jù)維度和類型準(zhǔn)備好數(shù)據(jù)結(jié)果的數(shù)組容器,才能開展數(shù)據(jù)的讀取。在數(shù)據(jù)的讀取上,HDF5 會將數(shù)據(jù)一次讀取到內(nèi)存當(dāng)中,在全量讀取上可能會有更好的表現(xiàn),但是也造成了短時(shí)內(nèi)存負(fù)載較高,需要更多的內(nèi)存資源才能完成相同的數(shù)據(jù)讀取工作。

06 應(yīng)用案例

在某航空項(xiàng)目中,數(shù)據(jù)主要來源于飛機(jī)上的傳感器。每年大約有上千次飛行,每次飛行約有三到四千個(gè)傳感器,這些傳感器采集了多種類型的參數(shù),每個(gè)參數(shù)的采樣頻率和數(shù)據(jù)長度各不相同。

HDF5與TsFile對比圖5-20251126.png

由于數(shù)據(jù)量龐大,對存儲空間的要求也相應(yīng)較高。在 HDF5 文件格式中,數(shù)據(jù)采用組(Group)和數(shù)據(jù)集(Dataset)的樹形結(jié)構(gòu)進(jìn)行組織,支持通過屬性(Attribute)存儲元數(shù)據(jù)。每個(gè)參數(shù)都單獨(dú)存儲為一個(gè)數(shù)據(jù)集,每個(gè)數(shù)據(jù)集是一個(gè)二維表格,包含時(shí)間列(time)和值列(value)。HDF5 采用多級 B+樹結(jié)構(gòu),每個(gè)組到其所有子組或數(shù)據(jù)集的映射通過一個(gè) B+ 樹記錄。

在專為時(shí)序數(shù)據(jù)設(shè)計(jì)的 TsFile 文件格式中,數(shù)據(jù)按“設(shè)備-測點(diǎn)”的樹形結(jié)構(gòu)組織,同一設(shè)備的所有測點(diǎn)數(shù)據(jù)在文件中連續(xù)存儲,并支持列式壓縮。其索引結(jié)構(gòu)為兩級 B 樹,從根節(jié)點(diǎn)到設(shè)備,再到時(shí)間序列。TsFile 將時(shí)間戳(time)和值(value)整合在單個(gè)文件中,無需分離存儲時(shí)間和值列,通過內(nèi)置索引支持快速數(shù)據(jù)檢索。

HDF5與TsFile對比圖6-20251126.png

在實(shí)際應(yīng)用中,真實(shí)飛參數(shù)據(jù)在 TsFile 格式中的寫入和查詢性能均優(yōu)于 HDF5 格式,且相同數(shù)據(jù)集存儲在 HDF5 中壓縮后約為 18TB,而在 TsFile 中壓縮后僅為 2.2TB。在默認(rèn)配置下,TsFile 的大小僅為 HDF5 的 14.31%,即 TsFile 的壓縮率是 HDF5 的 8 倍。

07 結(jié)語

TsFile 在時(shí)序模型、編碼壓縮、查詢過濾能力等方面具備優(yōu)勢,且 TsFile 的使用代碼也更為簡潔,大大降低了學(xué)習(xí)成本,提升了開發(fā)效率,這使得 TsFile 成為處理大規(guī)模時(shí)間序列數(shù)據(jù)的理想選擇之一。

目前,Apache TsFile 已成為繼時(shí)序數(shù)據(jù)庫 Apache IoTDB 之后,Apache 時(shí)序數(shù)據(jù)領(lǐng)域第二個(gè) Top-Level 項(xiàng)目,并已在 GitHub 開源:https://github.com/apache/tsfile。我們誠邀更多朋友參與試用,并提供寶貴意見!

更多內(nèi)容推薦:

下載時(shí)序數(shù)據(jù)庫 IoTDB 開源版

下載時(shí)序數(shù)據(jù)文件格式 TsFile