博客 / 詳情

返回

深入解析 HDF5 與 TsFile:時序數據存儲的較量

在大數據時代,高效的數據存儲和管理是科研和工業應用成功的關鍵。HDF5 是一種嵌套的實驗數據管理格式,TsFile 作為新型的時序數據存儲格式,各自具有獨特的特點和優勢。本文將深入探討 HDF5 的起源、應用場景、存在的問題,以及 TsFile 和 HDF5 的異同點。

01 HDF5 起源

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

HDF5 的初衷是為了實現一種架構無關的文件格式,滿足 NCSA 在使用多種不同計算平台之間移送科學數據的需求。

02 應用場景

HDF5 的應用場景主要包括在科學計算、工程模擬、氣象預測等領域的實驗數據管理需求。

場景一:科學數據存儲

在科學計算和研究領域,經常需要存儲和處理具有複雜結構的多維數據,如多維矩陣和氣象網格數據。這些數據集通常包含大量的元數據,需要一個能夠提供高效數據組織和訪問方式的存儲解決方案。

場景二:設備傳感器數據存儲

在設備監控和傳感器網絡中,需要存儲來自各種傳感器的大量數據。例如,某航空機構的結構健康監測系統的數據,這些數據包括各種傳感器採集的振動、温度等信息,對設備的狀態監測和故障預測具有重要意義。

00cd0375fb4f1601a4dae4b1ed90d7db

場景三:粒子仿真數據存儲

在粒子仿真領域,仿真程序可能會產生大量的實驗數據,包括粒子的軌跡、能量沉積等信息。這些數據對於理解物理過程和優化仿真參數具有重要意義,需要一個能夠高效存儲和管理這些數據的系統。

242a478301aadcac1fe9d20d21a871c7

03 TsFile 簡介

在 HDF5 的應用場景中,有不少實驗數據的格式其實是時序數據,TsFile 則是專為時序數據設計的列式存儲文件格式,由清華大學軟件學院團隊主導研發,並於 2023 年成為 Apache 頂級項目。TsFile 格式的優勢為高性能、高壓縮比、自解析、支持靈活的時間範圍查詢。

f1b8ac7d2ea320426945e0bc1b98b478

04 TsFile 與 HDF5 對比

下面從不同維度對 TsFile 與 HDF5 進行詳細的對比:

c53e7c5abe079f976e4590515923f635

(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 查詢示例

// 使用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");

// 指定查詢的表名,查詢的列,時間範圍,查詢結果會放置於最後的指針當中
reader.query("factory1", columns, 0, 100, temp_ret);
auto ret = dynamic_cast<storage::TableResultSet*>(temp_ret);

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

// 關閉查詢結果指針和讀取器
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 這個group下面的指定名字的dataset
dataset_id = H5Dopen2(group_id, "machine1", H5P_DEFAULT);
// 獲取dataset的datatype和dataspace,為後續準備結果容器做準備
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);

// 根據維度信息構建結果數組容器
int rows = (int)dims[0];
Data *dset = (Data *)malloc(rows * sizeof(Data));

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

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

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

(5) 接口對比

寫入方面

  • 元數據組織:

由於 TsFile 的數據邏輯結構是二維的 table,因此構建 writer 僅需 table 的名字以及列的數據類型信息。

而 HDF5 的最底層的數據邏輯結構是 dataset,其支持複雜數據類型以及多維數據,在構建的時候,需要數據的維度信息以及複雜數據類型信息。

  • 數據組織:

TsFile 需要將數據組織為其獨有的 tablet 結構,其中會將時間列數據進行單獨的組織,從而實現數據的批量寫入。

而 HDF5 當中則是將數據組織為多維數組,所有的數據類型都是一視同仁的,其接口內部會根據數組當中的偏移量直接將數據序列化到磁盤。

查詢方面

  • 數據複雜查詢:

HDF5 當中更加確切的描述為數據的讀取,因為其讀取是一個 chunk 的數據或者全部數據,數據的處理工作則是與 HDF5 分離。

相比之下,TsFile 所支持的是一種數據查詢的工作,在數據獲取上是並不全量的讀取,而是可以支持僅讀取一個 table 當中的部分列數據,同時還支持對讀取的列數據進行時間戳或者數值的過濾,這種過濾下推到文件層級,可以有效的減少傳輸的數據流量。

  • 結果組織:

由於 TsFile 的結構固定為二維的 table,所以僅需獲取列的數據類型就可以完成讀取的準備工作,同時,TsFile 的數據按批獲取的,對於較大數據量的讀取工作,可以大大減輕內存的負載。

相比之下,HDF5 的 dataset 由於維度和數據類型相對較為複雜,需要根據維度和類型準備好數據結果的數組容器,才能開展數據的讀取。在數據的讀取上,HDF5 會將數據一次讀取到內存當中,在全量讀取上可能會有更好的表現,但是也造成了短時內存負載較高,需要更多的內存資源才能完成相同的數據讀取工作。

06 應用案例

在某航空項目中,數據主要來源於飛機上的傳感器。每年大約有上千次飛行,每次飛行約有三到四千個傳感器,這些傳感器採集了多種類型的參數,每個參數的採樣頻率和數據長度各不相同。

b76c3fa868374244a49e4a1636ed4c65

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

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

0dbf348cbe4fb00d19dda3453f9f26a1

在實際應用中,真實飛參數據在 TsFile 格式中的寫入和查詢性能均優於 HDF5 格式,且相同數據集存儲在 HDF5 中壓縮後約為 18TB,而在 TsFile 中壓縮後僅為 2.2TB。在默認配置下,TsFile 的大小僅為 HDF5 的 14.31%,即 TsFile 的壓縮率是 HDF5 的 8 倍。

07 結語

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

目前,Apache TsFile 已成為繼時序數據庫 Apache IoTDB 之後,Apache 時序數據領域第二個 Top-Level 項目,並已在 GitHub 開源:https://github.com/apache/tsfile。我們誠邀更多朋友參與試用,並提供寶貴意見!

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.