@@ -43,7 +43,8 @@ prometheus 的核心开发者 Fabian Reinartz 写了一篇文章 [《Writing a T
43
43
* ** 🖇 mmap 内存映射**
44
44
* ** 📍 索引设计**
45
45
* ** 🗂 存储布局**
46
- * ** 📋 代码测试**
46
+ * ** 🚥 代码测试**
47
+ * ** ❓ FAQ**
47
48
48
49
## 💡 数据模型 & API 文档
49
50
@@ -204,7 +205,7 @@ func main() {
204
205
}
205
206
```
206
207
207
- 这次我把这段时间学习的内容整理一下 ,尝试完整介绍如何从零开始实现一个小型的 TSDB。
208
+ 下面我把这段时间学习的内容进行了整理 ,尝试完整介绍如何从零开始实现一个小型的 TSDB。
208
209
209
210
<p align =" center " ><image src =" ./images/教我做事.png " width =" 320px " ></p >
210
211
@@ -218,9 +219,9 @@ Gorilla 论文 4.1 小节介绍了压缩算法,先整体看一下压缩方案
218
219
219
220
<p align =" center " ><image src =" ./images/gorilla.png " width =" 600px " ></p >
220
221
221
- ** Timestamp 压缩:**
222
+ ** Timestamp DOD 压缩:**
222
223
223
- 在时序的场景中,每个时序点都有一个对应的 Timestamp,一条时序序列中相邻数据点的间隔是有规律可循的。一般来讲,监控数据的采集都是会以固定的时间间隔进行的,所以我们就可以用差值来记录时间间隔 ,更进一步,我们可以用差值的差值来记录以此来减少存储空间。
224
+ 在时序的场景中,每个时序点都有一个对应的 Timestamp,一条时序序列中相邻数据点的间隔是有规律可循的。一般来讲,监控数据的采集都是会以固定的时间间隔进行的,所以就可以用差值来记录时间间隔 ,更进一步,我们可以用差值的差值来记录以此来减少存储空间。
224
225
225
226
``` golang
226
227
t1: 1627401800 ; t2: 1627401810 ; t3: 1627401820 ; t4: 1627401830
@@ -234,13 +235,13 @@ t1: 1627401800; dod1: 0; dod2: 0; dod3: 0;
234
235
235
236
实际环境中当然不可能每个间隔都这么均匀,由于网络延迟等其他原因,差值会有波动。
236
237
237
- ** Value 压缩:**
238
+ ** Value XOR 压缩:**
238
239
239
240
*** Figure: IEEE 浮点数以及 XOR 计算结果***
240
241
241
242
<p align =" center " ><image src =" ./images/float64.png " width =" 600px " ></p >
242
243
243
- 当两个数据点数值值比较接近的话,通过异或操作计算出来的结果是比较相似的,利用这点我们就可以通过记录前置零和后置零个数以及数值部分来达到压缩空间的目的 。
244
+ 当两个数据点数值值比较接近的话,通过异或操作计算出来的结果是比较相似的,利用这点就可以通过记录前置零和后置零个数以及数值部分来达到压缩空间的目的 。
244
245
245
246
下面通过算法具体实现来介绍一下,代码来自项目 [ dgryski/go-tsz] ( https://github.com/dgryski/go-tsz ) 。代码完全按照论文中给出的步骤来实现。
246
247
@@ -418,7 +419,7 @@ series
418
419
419
420
时序数据有很强的时间特性(这不是废话吗 🧐),即大多数查询其实只会查询** 最近时刻** 的数据,这里的「最近」是个相对概念。所以没必要维护一条时间线的完整生命周期,特别是在 Kubernetes 这种云原生场景,Pod 随时有可能会被扩缩容,也就意味着一条时间线的生命周期可能会很短。如果我们一直记录着所有的时间线,那么随着时间的推移,数据库里的时间线的数量会呈现一个线性增长的趋势 😱,会极大地影响查询效率。
420
421
421
- 在 Gorilla 论文中也提出了一个概念 「序列分流」,这个概念描述的是一组时间序列变得不活跃,即不再接收数据点,取而代之的是有一组新的活跃的序列出现的场景。
422
+ 这里引入一个概念 「序列分流」,这个概念描述的是一组时间序列变得不活跃,即不再接收数据点,取而代之的是有一组新的活跃的序列出现的场景。
422
423
423
424
``` golang
424
425
series
@@ -438,7 +439,7 @@ series
438
439
<- ------------------- time --------------------->
439
440
```
440
441
441
- 我们将多条时间线的数据按一定的时间跨度切割成多个小块,每个小块本质就是一个独立小型的数据库,这种做法另外一个优势是清除过期操作的时候非常方便,只要将整个块给删了就行 👻。内存中保留最近两个小时的热数据(Memory Segment),其余数据持久化到磁盘(Disk Segment)。
442
+ 我们将多条时间线的数据按一定的时间跨度切割成多个小块,每个小块本质就是一个独立小型的数据库,这种做法另外一个优势是清除过期操作的时候非常方便,只要将整个块给删了就行 👻(梭哈是一种智慧) 。内存中保留最近两个小时的热数据(Memory Segment),其余数据持久化到磁盘(Disk Segment)。
442
443
443
444
*** Figure: 序列分块***
444
445
@@ -491,6 +492,10 @@ func (tsdb *TSDB) getHeadPartition() (Segment, error) {
491
492
}
492
493
```
493
494
495
+ *** Figure: Memory Segment 两部分数据***
496
+
497
+ <p align =" center " ><image src =" ./images/memory-segment.png " width =" 500px " ></p >
498
+
494
499
写入的时候支持数据时间回拨,也就是支持** 有限的** 乱序数据写入,实现方案是在内存中对还没归档的每条时间线维护一个链表(同样使用 AVL Tree 实现),当数据点的时间戳不是递增的时候存储到链表中,查询的时候会将两部分数据合并查询,持久化的时候也会将两者合并写入。
495
500
496
501
## 🖇 mmap 内存映射
@@ -533,7 +538,7 @@ mmap 内存映射的实现过程,总的来说可以分为三个阶段:
533
538
534
539
## 📍 索引设计
535
540
536
- ** TSDB 的索引查询 ,是通过 Label 组合来锁定到具体的时间线 。**
541
+ ** TSDB 的查询 ,是通过 Label 组合来锁定到具体的时间线进而确定分块偏移检索出数据 。**
537
542
538
543
* Sid(MetricHash/-/LabelHash) 是一个 Series 的唯一标识。
539
544
* Label(Name/-/Value) => vm="node1"; vm="node2"; iface="eth0"。
@@ -586,7 +591,77 @@ sid2; sid3; sid5
586
591
587
592
假设我们的查询只支持** 相等匹配** 的话,格局明显就小了 🤌。查询条件是 ` {vm=~"node*", iface="eth0"} ` 肿么办?对 label1、label2、label3 和 label4 一起求一个并集吗?显然不是,因为这样算的话那结果就是 ` sid3 ` 。
588
593
589
- 厘清关系就不难看出,** 只要对相同的 LabelName 做并集然后再对不同的 LabelName 做交集就可以了** 。这样算的正确结果就是 ` sid3 ` 和 ` sid5 ` 。实现的时候用到了 Roaring Bitmap,一种优化的位图算法。
594
+ 厘清关系就不难看出,** 只要对相同的 Label Name 做并集然后再对不同的 Label Name 求交集就可以了** 。这样算的正确结果就是 ` sid3 ` 和 ` sid5 ` 。实现的时候用到了 Roaring Bitmap,一种优化的位图算法。
595
+
596
+ ** Memory Segment 索引匹配**
597
+ ``` golang
598
+ func (mim *memoryIndexMap ) MatchSids (lvs *labelValueSet , lms LabelMatcherSet ) []string {
599
+ // ...
600
+ sids := newMemorySidSet ()
601
+ var got bool
602
+ for i := len (lms) - 1 ; i >= 0 ; i-- {
603
+ tmp := newMemorySidSet ()
604
+ vs := lvs.Match (lms[i])
605
+ // 对相同的 Label Name 求并集
606
+ for _ , v := range vs {
607
+ midx := mim.idx [joinSeparator (lms[i].Name , v)]
608
+ if midx == nil || midx.Size () <= 0 {
609
+ continue
610
+ }
611
+
612
+ tmp.Union (midx.Copy ())
613
+ }
614
+
615
+ if tmp == nil || tmp.Size () <= 0 {
616
+ return nil
617
+ }
618
+
619
+ if !got {
620
+ sids = tmp
621
+ got = true
622
+ continue
623
+ }
624
+
625
+ // 对不同的 Label Name 求交集
626
+ sids.Intersection (tmp.Copy ())
627
+ }
628
+
629
+ return sids.List ()
630
+ }
631
+ ```
632
+
633
+ ** Disk Segment 索引匹配**
634
+ ``` golang
635
+ func (dim *diskIndexMap ) MatchSids (lvs *labelValueSet , lms LabelMatcherSet ) []uint32 {
636
+ // ...
637
+
638
+ lst := make ([]*roaring.Bitmap , 0 )
639
+ for i := len (lms) - 1 ; i >= 0 ; i-- {
640
+ tmp := make ([]*roaring.Bitmap , 0 )
641
+ vs := lvs.Match (lms[i])
642
+
643
+ // 对相同的 Label Name 求并集
644
+ for _ , v := range vs {
645
+ didx := dim.label2sids [joinSeparator (lms[i].Name , v)]
646
+ if didx == nil || didx.set .IsEmpty () {
647
+ continue
648
+ }
649
+
650
+ tmp = append (tmp, didx.set )
651
+ }
652
+
653
+ union := roaring.ParOr (4 , tmp...)
654
+ if union.IsEmpty () {
655
+ return nil
656
+ }
657
+
658
+ lst = append (lst, union)
659
+ }
660
+
661
+ // 对不同的 Label Name 求交集
662
+ return roaring.ParAnd (4 , lst...).ToArray ()
663
+ }
664
+ ```
590
665
591
666
然而,确定相同的 LabelName 也是一个问题,因为 Label 本身就代表着 ` Name:Value ` ,难不成我还要遍历所有 label 才能确定嘛,这不就又成了全表扫描???
592
667
@@ -817,31 +892,47 @@ seg-1627738753-1627746013
817
892
{__name__=" cpu.busy" , node=" vm0" , dc=" 0" , foo=" bdac463d-8805-4cbe-bc9a-9bf495f87bab" , bar=" 3689df1d-cbf3-4962-abea-6491861e62d2" , zoo=" 9551010d-9726-4b3b-baf3-77e50655b950" } 1627710454 41
818
893
```
819
894
820
- 这样一条数据按照 JSON 格式进行网络通信的话,大概是 190Byte ,初略计算一下。
895
+ 这样一条数据按照 JSON 格式进行网络通信的话,大概是 200Byte ,初略计算一下。
821
896
822
- 190 * 9912336 = 1883343840Byte = 1796M
897
+ 200 * 9912336 = 1982467200Byte = 1890M
823
898
824
899
可以选择 ZSTD 或者 Snappy 算法进行二次压缩(默认不开启)。还是上面的示例代码,不过在 TSDB 启动的时候指定了压缩算法。
825
900
901
+ ** ZstdBytesCompressor**
826
902
``` golang
827
903
func main () {
828
904
store := mandodb.OpenTSDB (mandodb.WithMetaBytesCompressorType (mandodb.ZstdBytesCompressor ))
829
905
defer store.Close ()
830
906
831
907
// ...
832
908
}
833
- ```
834
909
835
- 再执行一遍程序来看看压缩效果。
910
+ // 压缩效果 28M -> 25M
836
911
837
- ``` shell
838
912
❯ 🐶 ll seg-1627711905 -1627719165
839
913
Permissions Size User Date Modified Name
840
914
.rwxr -xr-x 25M chenjiandongx 1 Aug 00 :13 data
841
915
.rwxr -xr-x 110 chenjiandongx 1 Aug 00 :13 meta.json
842
916
```
843
917
844
- 体积变化 28M -> 25M。就这???
918
+ ** SnappyBytesCompressor**
919
+ ``` golang
920
+ func main () {
921
+ store := mandodb.OpenTSDB (mandodb.WithMetaBytesCompressorType (mandodb.SnappyBytesCompressor ))
922
+ defer store.Close ()
923
+
924
+ // ...
925
+ }
926
+
927
+ // 压缩效果 28M -> 26M
928
+
929
+ ❯ 🐶 ll seg-1627763918 -1627771178
930
+ Permissions Size User Date Modified Name
931
+ .rwxr -xr-x 26M chenjiandongx 1 Aug 14 :39 data
932
+ .rwxr -xr-x 110 chenjiandongx 1 Aug 14 :39 meta.json
933
+ ```
934
+
935
+ 多多少少还是有点效果的 🐶...
845
936
846
937
<p align =" center " ><image src =" ./images/就这.png " width =" 320px " ></p >
847
938
@@ -853,7 +944,7 @@ Permissions Size User Date Modified Name
853
944
854
945
<p align =" center " ><image src =" ./images/segment.png " width =" 380px " ></p >
855
946
856
- TOC 描述了 Data Block 和 Meta Block(Series Block + Labels Block)的大小 ,用于后面对 data 进行解析读取。Data Block 存储了每条时间线具体的数据点,时间线之间数据紧挨存储。DataContent 就是使用 Gorilla 差值算法压缩的 block。
947
+ TOC 描述了 Data Block 和 Meta Block(Series Block + Labels Block)的体积 ,用于后面对 data 进行解析读取。Data Block 存储了每条时间线具体的数据点,时间线之间数据紧挨存储。DataContent 就是使用 Gorilla 差值算法压缩的 block。
857
948
858
949
*** Figure: Data Block***
859
950
@@ -878,11 +969,27 @@ Series Block 记录了每条时间线的元数据,字段解释如下。
878
969
879
970
<p align =" center " ><image src =" ./images/series-block.png " width =" 620px " ></p >
880
971
881
- 了解完设计,再看看代码实现。
972
+ 了解完设计,再看看 Meta Block 编码和解编码的代码实现,binaryMetaSerializer 实现了 ` MetaSerializer ` 接口。
973
+
974
+ ``` golang
975
+ type MetaSerializer interface {
976
+ Marshal (Metadata) ([]byte , error )
977
+ Unmarshal ([]byte , *Metadata) error
978
+ }
979
+ ```
882
980
883
981
** 编码 Metadata**
884
982
885
983
``` golang
984
+ const (
985
+ endOfBlock uint16 = 0xffff
986
+ uint16Size = 2
987
+ uint32Size = 4
988
+ uint64Size = 8
989
+
990
+ magic = " https://github.com/chenjiandongx/mandodb"
991
+ )
992
+
886
993
func (s *binaryMetaSerializer ) Marshal (meta Metadata ) ([]byte , error ) {
887
994
encf := newEncbuf ()
888
995
@@ -920,7 +1027,7 @@ func (s *binaryMetaSerializer) Marshal(meta Metadata) ([]byte, error) {
920
1027
921
1028
encf.MarshalUint64 (uint64 (meta.MinTs ))
922
1029
encf.MarshalUint64 (uint64 (meta.MaxTs ))
923
- encf.MarshalString (magic)
1030
+ encf.MarshalString (magic) // <-- magic here
924
1031
925
1032
return ByteCompress (encf.Bytes ()), nil
926
1033
}
@@ -945,11 +1052,11 @@ func (s *binaryMetaSerializer) Unmarshal(data []byte, meta *Metadata) error {
945
1052
return ErrInvalidSize
946
1053
}
947
1054
1055
+ // labels block
948
1056
offset := 0
949
1057
labels := make ([]seriesWithLabel, 0 )
950
1058
for {
951
1059
var labelName string
952
-
953
1060
labelLen := decf.UnmarshalUint16 (data[offset : offset+uint16Size])
954
1061
offset += uint16Size
955
1062
@@ -959,7 +1066,6 @@ func (s *binaryMetaSerializer) Unmarshal(data []byte, meta *Metadata) error {
959
1066
960
1067
labelName = decf.UnmarshalString (data[offset : offset+int (labelLen)])
961
1068
offset += int (labelLen)
962
-
963
1069
sidCnt := decf.UnmarshalUint32 (data[offset : offset+uint32Size])
964
1070
offset += uint32Size
965
1071
@@ -972,10 +1078,10 @@ func (s *binaryMetaSerializer) Unmarshal(data []byte, meta *Metadata) error {
972
1078
}
973
1079
meta.Labels = labels
974
1080
1081
+ // series block
975
1082
rows := make ([]metaSeries, 0 )
976
1083
for {
977
1084
series := metaSeries{}
978
-
979
1085
sidLen := decf.UnmarshalUint16 (data[offset : offset+uint16Size])
980
1086
offset += uint16Size
981
1087
@@ -985,13 +1091,10 @@ func (s *binaryMetaSerializer) Unmarshal(data []byte, meta *Metadata) error {
985
1091
986
1092
series.Sid = decf.UnmarshalString (data[offset : offset+int (sidLen)])
987
1093
offset += int (sidLen)
988
-
989
1094
series.StartOffset = decf.UnmarshalUint64 (data[offset : offset+uint64Size])
990
1095
offset += uint64Size
991
-
992
1096
series.EndOffset = decf.UnmarshalUint64 (data[offset : offset+uint64Size])
993
1097
offset += uint64Size
994
-
995
1098
labelCnt := decf.UnmarshalUint32 (data[offset : offset+uint32Size])
996
1099
offset += uint32Size
997
1100
@@ -1007,7 +1110,6 @@ func (s *binaryMetaSerializer) Unmarshal(data []byte, meta *Metadata) error {
1007
1110
1008
1111
meta.MinTs = int64 (decf.UnmarshalUint64 (data[offset : offset+uint64Size]))
1009
1112
offset += uint64Size
1010
-
1011
1113
meta.MaxTs = int64 (decf.UnmarshalUint64 (data[offset : offset+uint64Size]))
1012
1114
offset += uint64Size
1013
1115
@@ -1019,8 +1121,27 @@ func (s *binaryMetaSerializer) Unmarshal(data []byte, meta *Metadata) error {
1019
1121
1020
1122
<p align =" center " ><image src =" ./images/深度理解.png " width =" 320px " ></p >
1021
1123
1022
- ## 📋 代码测试
1124
+ ## 🚥 代码测试
1125
+
1126
+ ## ❓ FAQ
1127
+
1128
+ ** Q: Is mandodb cool?**
1129
+
1130
+ A: Not sure
1131
+
1132
+ ** Q: Is mando awesome?**
1133
+
1134
+ A: Definitely YES!
1135
+
1136
+ ** Q: What's the hardest part of this project?**
1137
+
1138
+ A: Writing this document 😂...
1139
+
1140
+ ** Q:Anything else?**
1141
+
1142
+ *** Life is magic. Coding is art. 🍻 Bilibili!***
1023
1143
1144
+ ![ bilibili] ( ./images/bilibili.png )
1024
1145
1025
1146
## 📑 License
1026
1147
0 commit comments