Skip to content

Commit 65ab99f

Browse files
committed
Docs update
1 parent 1cf5346 commit 65ab99f

File tree

3 files changed

+148
-27
lines changed

3 files changed

+148
-27
lines changed

README.md

+148-27
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ prometheus 的核心开发者 Fabian Reinartz 写了一篇文章 [《Writing a T
4343
* **🖇 mmap 内存映射**
4444
* **📍 索引设计**
4545
* **🗂 存储布局**
46-
* **📋 代码测试**
46+
* **🚥 代码测试**
47+
* **❓ FAQ**
4748

4849
## 💡 数据模型 & API 文档
4950

@@ -204,7 +205,7 @@ func main() {
204205
}
205206
```
206207

207-
这次我把这段时间学习的内容整理一下,尝试完整介绍如何从零开始实现一个小型的 TSDB。
208+
下面我把这段时间学习的内容进行了整理,尝试完整介绍如何从零开始实现一个小型的 TSDB。
208209

209210
<p align="center"><image src="./images/教我做事.png" width="320px"></p>
210211

@@ -218,9 +219,9 @@ Gorilla 论文 4.1 小节介绍了压缩算法,先整体看一下压缩方案
218219

219220
<p align="center"><image src="./images/gorilla.png" width="600px"></p>
220221

221-
**Timestamp 压缩:**
222+
**Timestamp DOD 压缩:**
222223

223-
在时序的场景中,每个时序点都有一个对应的 Timestamp,一条时序序列中相邻数据点的间隔是有规律可循的。一般来讲,监控数据的采集都是会以固定的时间间隔进行的,所以我们就可以用差值来记录时间间隔,更进一步,我们可以用差值的差值来记录以此来减少存储空间。
224+
在时序的场景中,每个时序点都有一个对应的 Timestamp,一条时序序列中相邻数据点的间隔是有规律可循的。一般来讲,监控数据的采集都是会以固定的时间间隔进行的,所以就可以用差值来记录时间间隔,更进一步,我们可以用差值的差值来记录以此来减少存储空间。
224225

225226
```golang
226227
t1: 1627401800; t2: 1627401810; t3: 1627401820; t4: 1627401830
@@ -234,13 +235,13 @@ t1: 1627401800; dod1: 0; dod2: 0; dod3: 0;
234235

235236
实际环境中当然不可能每个间隔都这么均匀,由于网络延迟等其他原因,差值会有波动。
236237

237-
**Value 压缩:**
238+
**Value XOR 压缩:**
238239

239240
***Figure: IEEE 浮点数以及 XOR 计算结果***
240241

241242
<p align="center"><image src="./images/float64.png" width="600px"></p>
242243

243-
当两个数据点数值值比较接近的话,通过异或操作计算出来的结果是比较相似的,利用这点我们就可以通过记录前置零和后置零个数以及数值部分来达到压缩空间的目的
244+
当两个数据点数值值比较接近的话,通过异或操作计算出来的结果是比较相似的,利用这点就可以通过记录前置零和后置零个数以及数值部分来达到压缩空间的目的
244245

245246
下面通过算法具体实现来介绍一下,代码来自项目 [dgryski/go-tsz](https://github.com/dgryski/go-tsz)。代码完全按照论文中给出的步骤来实现。
246247

@@ -418,7 +419,7 @@ series
418419

419420
时序数据有很强的时间特性(这不是废话吗 🧐),即大多数查询其实只会查询**最近时刻**的数据,这里的「最近」是个相对概念。所以没必要维护一条时间线的完整生命周期,特别是在 Kubernetes 这种云原生场景,Pod 随时有可能会被扩缩容,也就意味着一条时间线的生命周期可能会很短。如果我们一直记录着所有的时间线,那么随着时间的推移,数据库里的时间线的数量会呈现一个线性增长的趋势 😱,会极大地影响查询效率。
420421

421-
在 Gorilla 论文中也提出了一个概念「序列分流」,这个概念描述的是一组时间序列变得不活跃,即不再接收数据点,取而代之的是有一组新的活跃的序列出现的场景。
422+
这里引入一个概念「序列分流」,这个概念描述的是一组时间序列变得不活跃,即不再接收数据点,取而代之的是有一组新的活跃的序列出现的场景。
422423

423424
```golang
424425
series
@@ -438,7 +439,7 @@ series
438439
<-------------------- time --------------------->
439440
```
440441

441-
我们将多条时间线的数据按一定的时间跨度切割成多个小块,每个小块本质就是一个独立小型的数据库,这种做法另外一个优势是清除过期操作的时候非常方便,只要将整个块给删了就行 👻。内存中保留最近两个小时的热数据(Memory Segment),其余数据持久化到磁盘(Disk Segment)。
442+
我们将多条时间线的数据按一定的时间跨度切割成多个小块,每个小块本质就是一个独立小型的数据库,这种做法另外一个优势是清除过期操作的时候非常方便,只要将整个块给删了就行 👻(梭哈是一种智慧)。内存中保留最近两个小时的热数据(Memory Segment),其余数据持久化到磁盘(Disk Segment)。
442443

443444
***Figure: 序列分块***
444445

@@ -491,6 +492,10 @@ func (tsdb *TSDB) getHeadPartition() (Segment, error) {
491492
}
492493
```
493494

495+
***Figure: Memory Segment 两部分数据***
496+
497+
<p align="center"><image src="./images/memory-segment.png" width="500px"></p>
498+
494499
写入的时候支持数据时间回拨,也就是支持**有限的**乱序数据写入,实现方案是在内存中对还没归档的每条时间线维护一个链表(同样使用 AVL Tree 实现),当数据点的时间戳不是递增的时候存储到链表中,查询的时候会将两部分数据合并查询,持久化的时候也会将两者合并写入。
495500

496501
## 🖇 mmap 内存映射
@@ -533,7 +538,7 @@ mmap 内存映射的实现过程,总的来说可以分为三个阶段:
533538

534539
## 📍 索引设计
535540

536-
**TSDB 的索引查询,是通过 Label 组合来锁定到具体的时间线**
541+
**TSDB 的查询,是通过 Label 组合来锁定到具体的时间线进而确定分块偏移检索出数据**
537542

538543
* Sid(MetricHash/-/LabelHash) 是一个 Series 的唯一标识。
539544
* Label(Name/-/Value) => vm="node1"; vm="node2"; iface="eth0"。
@@ -586,7 +591,77 @@ sid2; sid3; sid5
586591

587592
假设我们的查询只支持**相等匹配**的话,格局明显就小了 🤌。查询条件是 `{vm=~"node*", iface="eth0"}` 肿么办?对 label1、label2、label3 和 label4 一起求一个并集吗?显然不是,因为这样算的话那结果就是 `sid3`
588593

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+
```
590665

591666
然而,确定相同的 LabelName 也是一个问题,因为 Label 本身就代表着 `Name:Value`,难不成我还要遍历所有 label 才能确定嘛,这不就又成了全表扫描???
592667

@@ -817,31 +892,47 @@ seg-1627738753-1627746013
817892
{__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
818893
```
819894

820-
这样一条数据按照 JSON 格式进行网络通信的话,大概是 190Byte,初略计算一下。
895+
这样一条数据按照 JSON 格式进行网络通信的话,大概是 200Byte,初略计算一下。
821896

822-
190 * 9912336 = 1883343840Byte = 1796M
897+
200 * 9912336 = 1982467200Byte = 1890M
823898

824899
可以选择 ZSTD 或者 Snappy 算法进行二次压缩(默认不开启)。还是上面的示例代码,不过在 TSDB 启动的时候指定了压缩算法。
825900

901+
**ZstdBytesCompressor**
826902
```golang
827903
func main() {
828904
store := mandodb.OpenTSDB(mandodb.WithMetaBytesCompressorType(mandodb.ZstdBytesCompressor))
829905
defer store.Close()
830906

831907
// ...
832908
}
833-
```
834909

835-
再执行一遍程序来看看压缩效果。
910+
// 压缩效果 28M -> 25M
836911

837-
```shell
838912
❯ 🐶 ll seg-1627711905-1627719165
839913
Permissions Size User Date Modified Name
840914
.rwxr-xr-x 25M chenjiandongx 1 Aug 00:13 data
841915
.rwxr-xr-x 110 chenjiandongx 1 Aug 00:13 meta.json
842916
```
843917

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+
多多少少还是有点效果的 🐶...
845936

846937
<p align="center"><image src="./images/就这.png" width="320px"></p>
847938

@@ -853,7 +944,7 @@ Permissions Size User Date Modified Name
853944

854945
<p align="center"><image src="./images/segment.png" width="380px"></p>
855946

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。
857948

858949
***Figure: Data Block***
859950

@@ -878,11 +969,27 @@ Series Block 记录了每条时间线的元数据,字段解释如下。
878969

879970
<p align="center"><image src="./images/series-block.png" width="620px"></p>
880971

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+
```
882980

883981
**编码 Metadata**
884982

885983
```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+
886993
func (s *binaryMetaSerializer) Marshal(meta Metadata) ([]byte, error) {
887994
encf := newEncbuf()
888995

@@ -920,7 +1027,7 @@ func (s *binaryMetaSerializer) Marshal(meta Metadata) ([]byte, error) {
9201027

9211028
encf.MarshalUint64(uint64(meta.MinTs))
9221029
encf.MarshalUint64(uint64(meta.MaxTs))
923-
encf.MarshalString(magic)
1030+
encf.MarshalString(magic) // <-- magic here
9241031

9251032
return ByteCompress(encf.Bytes()), nil
9261033
}
@@ -945,11 +1052,11 @@ func (s *binaryMetaSerializer) Unmarshal(data []byte, meta *Metadata) error {
9451052
return ErrInvalidSize
9461053
}
9471054

1055+
// labels block
9481056
offset := 0
9491057
labels := make([]seriesWithLabel, 0)
9501058
for {
9511059
var labelName string
952-
9531060
labelLen := decf.UnmarshalUint16(data[offset : offset+uint16Size])
9541061
offset += uint16Size
9551062

@@ -959,7 +1066,6 @@ func (s *binaryMetaSerializer) Unmarshal(data []byte, meta *Metadata) error {
9591066

9601067
labelName = decf.UnmarshalString(data[offset : offset+int(labelLen)])
9611068
offset += int(labelLen)
962-
9631069
sidCnt := decf.UnmarshalUint32(data[offset : offset+uint32Size])
9641070
offset += uint32Size
9651071

@@ -972,10 +1078,10 @@ func (s *binaryMetaSerializer) Unmarshal(data []byte, meta *Metadata) error {
9721078
}
9731079
meta.Labels = labels
9741080

1081+
// series block
9751082
rows := make([]metaSeries, 0)
9761083
for {
9771084
series := metaSeries{}
978-
9791085
sidLen := decf.UnmarshalUint16(data[offset : offset+uint16Size])
9801086
offset += uint16Size
9811087

@@ -985,13 +1091,10 @@ func (s *binaryMetaSerializer) Unmarshal(data []byte, meta *Metadata) error {
9851091

9861092
series.Sid = decf.UnmarshalString(data[offset : offset+int(sidLen)])
9871093
offset += int(sidLen)
988-
9891094
series.StartOffset = decf.UnmarshalUint64(data[offset : offset+uint64Size])
9901095
offset += uint64Size
991-
9921096
series.EndOffset = decf.UnmarshalUint64(data[offset : offset+uint64Size])
9931097
offset += uint64Size
994-
9951098
labelCnt := decf.UnmarshalUint32(data[offset : offset+uint32Size])
9961099
offset += uint32Size
9971100

@@ -1007,7 +1110,6 @@ func (s *binaryMetaSerializer) Unmarshal(data []byte, meta *Metadata) error {
10071110

10081111
meta.MinTs = int64(decf.UnmarshalUint64(data[offset : offset+uint64Size]))
10091112
offset += uint64Size
1010-
10111113
meta.MaxTs = int64(decf.UnmarshalUint64(data[offset : offset+uint64Size]))
10121114
offset += uint64Size
10131115

@@ -1019,8 +1121,27 @@ func (s *binaryMetaSerializer) Unmarshal(data []byte, meta *Metadata) error {
10191121

10201122
<p align="center"><image src="./images/深度理解.png" width="320px"></p>
10211123

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!***
10231143

1144+
![bilibili](./images/bilibili.png)
10241145

10251146
## 📑 License
10261147

images/bilibili.png

2.34 MB
Loading

images/memory-segment.png

65.9 KB
Loading

0 commit comments

Comments
 (0)