上一章聊到行式存儲、列式存儲的基本概念,並介紹了 TsFile 是如何存儲數據以及基本概念。詳情請見: 時序資料庫 Apache-IoTDB 源碼解析之文件格式簡介(三) 打一波廣告,歡迎大家訪問IoTDB 倉庫,求一波 Star 。歡迎關註頭條號:列炮緩開局,歡迎關註OSCHINA博客 這一章主要 ...
上一章聊到行式存儲、列式存儲的基本概念,並介紹了 TsFile 是如何存儲數據以及基本概念。詳情請見:
時序資料庫 Apache-IoTDB 源碼解析之文件格式簡介(三)
打一波廣告,歡迎大家訪問IoTDB 倉庫,求一波 Star 。歡迎關註頭條號:列炮緩開局,歡迎關註OSCHINA博客
這一章主要想聊一聊:
- TsFile的文件概覽
- TsFile的數據塊
TsFile文件概覽
一個完整的 TsFile 是由圖中的幾大塊組成,圖中的數據塊與索引塊之間使用 1 個位元組的分隔符 2
來進行分隔,這個分隔符的意義是當 TsFile 損壞的時候,順序掃描 TsFile 時,依然可以判斷下一個是 MetaData 是什麼東西。
1. 識別符(Magic)
現在各種軟體五花八門,很多軟體都擁有自己的文件格式用來存儲數據內容,但當硬碟上文件非常多的時候如何有效的識別是否為自己的文件,確認可以打開呢?經常用 windows 系統的朋友可能會想到用擴展名,但假如文件名丟失了,那我們如何知道這個文件是不是能被程式正確訪問呢?
這時候通常會使用一個獨有的字元填充在文件開頭和結尾,這樣程式只要訪問 1 個固定長度的字元就知道這個文件是不是自己能正常訪問的文件了,當然,TsFile 作為一個資料庫文件,肯定需要在這個識別符上精心打造一番,它看起來是這樣:
(decimal) 84 115 70 105 108 101
(hex) 54 73 46 69 6c 65
(ASCII) T s F i l e
非常 cool 。
2.文件版本(Version)
再精妙的設計也難免產生一些問題,那麼就需要升級,那麼文件內容也一樣,有時候當你的改動特別大了,就會出現完全不相容的兩個版本,這個很好理解不過多解釋。TsFile 中採用了 6 個位元組來保存文件版本信息,當前 0.9.x 版本看起來就是這樣:
(decimal) 48 48 48 48 48 50
(hex) 30 30 30 30 30 32
(ASCII) 0 0 0 0 0 2
3.數據塊
3.1 ChunkGroup
文件的數據塊中包含了多個 ChunkGroup ,其中 ChunkGroup 的概念已經在上一章聊過,它代表了設備(邏輯概念上的一個集合)一段時間內的數據,在 IoTDB 中稱為 Device。
在實際的文件中,ChunkGroup是由多個 Chunk 和一個 ChunkGroupFooter 組成。其中最後一個 Chunk 的結尾和 ChunkGroupFooter 之間使用 1 個位元組的分隔符 0
來做區分,ChunkGroupFooter 沒有什麼具體作用,不做詳細解釋。
3.2 Chunk
一個 ChunkGroup 中包含了多個 Chunk,它代表了測點數據(邏輯概念上的某一類數據的集合,如體溫數據),在 IoTDB 中稱為 Measurement。
在實際文件中 Chunk 是由 ChunkHeader 和多個 Page 組成,並被 1 個位元組的分隔符 1
包裹。ChunkHeader中主要保存了當前 Chunk 的數據類型、壓縮方式、編碼方式、包含的 Pages 占用的位元組數等信息。
3.3 Page
一個 Chunk 中包含多個 Page,它是一個數據組織方式,數據大小被限制在 64K 左右。
在實際文件中由 PageHeader 和 PageData 組成。其中 PageHeader 里主要保存了,當前 page 里的一些預聚合信息,包含了最大值、最小值、開始時間、結束時間等。他的存在是非常有意義的,因為當某些特定場景的讀時候,不必要解開 page 的數據就能夠得到結果,比如說 selece 體溫 from 王五 where time > 1580950800
, 當讀到 PageHeader 的時候,找到 startTime 和 endTime 就能判斷是否可以使用當前 page。 這個聚合信息的結構同樣出現在索引塊中,下一章再具體聊這個聚合結構。
3.4 PageData
一個 Page 中包含了一個 PageData,裡面有兩個數組:時間數組和值數組,且這兩個數組的下標是對齊的,也就是時間數組中的第一個對應值數組中的第一個。舉個例子:
timeArray: [1,2,3,4]
valueArray: ['a', 'b', 'c', 'd']
在page中就是這樣保存的數據,其中 1 代表了時間 1970-01-01 08:00:00 後的 1 毫秒,對應的值就是 'a'。
數據塊展示
我們繼續使用上一章聊到的示例數據來展示真正的TsFile中是如何保存的。
時間戳 | 人名 | 體溫 | 心率 |
---|---|---|---|
1580950800 | 王五 | 36.7 | 100 |
1580950911 | 王五 | 36.6 | 90 |
當數據被寫入 TsFile 中,大概就是下麵一個展示的情況,這裡省略了索引部分。
POSITION| CONTENT
-------- -------
0| [magic head] TsFile
6| [version number] 000002
// 因為 6個位元組的magic + 6個位元組的 version 所以 chunkGroup 從 12 開始
||||||||||||||||||||| [Chunk Group] of wangwu begins at pos 12, ends at pos 253, version:0, num of Chunks:2
// 這裡展示的是 ChunkHeader 中保存的信息
12| [Chunk] of xinlv, numOfPoints:1, time range:[1580950800,1580950800], tsDataType:INT32,
[minValue:100,maxValue:100,firstValue:100,lastValue:100,sumValue:100.0]
| [marker] 1 // chunk 的真正開始是從這個分隔符 1 開始的
| [ChunkHeader] // header 的數據在上面展示了
| 1 pages //這裡保存的具體數據
| time:1580950800; value:100
// 下一個 chunk
121| [Chunk] of tiwen, numOfPoints:1, time range:[1580950800,1580950800], tsDataType:FLOAT,
[minValue:36.7,maxValue:36.7,firstValue:36.7,lastValue:36.7,sumValue:36.70000076293945]
| [marker] 1
| [ChunkHeader]
| 1 pages
| time:1580950800; value:36.7
230| [Chunk Group Footer]
| [marker] 0 // chunkFooter 和 chunk 使用 0 作為分隔
| [deviceID] wangwu
| [dataSize] 218
| [num of chunks] 2
||||||||||||||||||||| [Chunk Group] of wangwu ends
回想我們的查詢語句 select 體溫 from 王五
, 當經歷過索引之後會得到 offset 的值等於 121 ,這時候我們只需要調用reader.seek(121)
,從這裡開始就是所有體溫數據的開始點,從這裡一直讀到 230 的 ChunkGroupFooter 結構的時候,就可以返回給用戶數據了。
有興趣自己實驗的朋友可以,引入 TsFile 的包,自行實驗,下麵給出測試代碼:
<dependency>
<groupId>org.apache.iotdb</groupId>
<artifactId>tsfile</artifactId>
<version>0.9.1</version>
</dependency>
public static void main(String[] args) throws IOException, WriteProcessException {
MeasurementSchema chunk1 = new MeasurementSchema("tiwen", TSDataType.FLOAT, TSEncoding.PLAIN);
MeasurementSchema chunk2 = new MeasurementSchema("xinlv", TSDataType.INT32, TSEncoding.PLAIN);
Schema chunks = new Schema();
chunks.registerMeasurement(chunk1);
chunks.registerMeasurement(chunk2);
TsFileWriter writer = new TsFileWriter(new File("test"), chunks);
RowBatch chunkGroup = chunks.createRowBatch("wangwu");
long[] timestamps = chunkGroup.timestamps;
Object[] values = chunkGroup.values;
timestamps[0] = 1580950800;
float[] tiwen = (float[]) values[0];
int[] xinlv = (int[]) values[1];
// 寫入王五的體溫
tiwen[0] = 36.7f;
//寫入王五的心率
xinlv[0] = 100;
chunkGroup.batchSize++;
timestamps[1] = 1580950800;
// 寫入第二條王五的體溫
tiwen[1] = 36.6f;
//寫入第二條王五的心率
xinlv[1] = 90;
chunkGroup.batchSize++;
writer.write(chunkGroup);
writer.close();
}
執行完成之後你可以使用 IoTDB 中的 TsFileSketchTool 來查看文件結構,得到文中示例的展示結果;或者使用 od 等工具查看,祝玩兒的開心。IoTDB 0.9.1 版本下載
這一章聊到了 TsFile 分為了 數據塊
和 索引塊
,並且介紹了數據塊的具體組成部分和查詢邏輯。那麼索引塊是什麼結構,怎樣完成了在大量混雜的數據中搜索到的想要的數據,請持續關註。