淺談Prometheus的數(shù)據(jù)存儲(chǔ)
目錄
1、概述
2、時(shí)間序列
3、二維模型
4、存儲(chǔ)策略的演進(jìn)
4.1 1.x 版本
4.2 2.x 版本

本文是結(jié)合耗子叔的視頻及 Prometheus 作者部分原文整理,加上部分個(gè)人理解而來(lái),膜拜大神~
1、概述
Prometheus是一套開(kāi)源的監(jiān)控&報(bào)警&時(shí)間序列數(shù)據(jù)庫(kù)的組合
Prometheus內(nèi)部主要分為三大塊,Retrieval是負(fù)責(zé)定時(shí)去暴露的目標(biāo)頁(yè)面上去抓取采樣指標(biāo)數(shù)據(jù),Storage是負(fù)責(zé)將采樣數(shù)據(jù)寫(xiě)磁盤(pán),PromQL是Prometheus提供的查詢(xún)語(yǔ)言模塊
其有著非常高效的時(shí)間序列數(shù)據(jù)存儲(chǔ)方法,每個(gè)采樣數(shù)據(jù)僅僅占用3.5byte左右空間
在早期有一個(gè)單獨(dú)的項(xiàng)目叫做 TSDB,但是,在2.1.x的某個(gè)版本,已經(jīng)不單獨(dú)維護(hù)這個(gè)項(xiàng)目了,直接將這個(gè)項(xiàng)目合并到了prometheus的主干上了
prometheus每次抓取的數(shù)據(jù),對(duì)于操作者來(lái)說(shuō)可見(jiàn)的格式(即在prometheus界面查詢(xún)到的值)
requests_total{path="/status", method="GET", instance="10.0.0.1:80"} @1534317560938 94355
意思就是在1534317560938這個(gè)時(shí)間點(diǎn),10.0.0.1:80這個(gè)實(shí)例上,GET /status 這個(gè)請(qǐng)求的次數(shù)累計(jì)是 94355次
最終存儲(chǔ)在TSDB中的格式為
{__name__="requests_total", path="/status", method="GET", instance="10.0.0.1:80"}
2、時(shí)間序列
Data scheme數(shù)據(jù)標(biāo)識(shí)
identifier -> (t0, v0), (t1, v1), (t2, v2), (t3, v3), ...
Prometheus Data Model數(shù)據(jù)模型
<metric name>{<label name>=<label value>, ...}
Typical set of series identifiers

Query 查詢(xún)
__name__="requests_total":查詢(xún)所有屬于requests_total的序列
method="PUT|POST":查詢(xún)所有序列中方法是PUT或POST的序列
3、二維模型
Write 寫(xiě):每個(gè)目標(biāo)暴露成百上千個(gè)不同的時(shí)間序列,寫(xiě)入模式是完全垂直和高度并發(fā)的,因?yàn)閬?lái)自每個(gè)目標(biāo)的樣本是獨(dú)立的
Query 查:查詢(xún)數(shù)據(jù)時(shí)可以并行和批處理
series
^
│ . . . . . . . . . . . . . . . . . . . . . . {__name__="request_total", method="GET"}
│ . . . . . . . . . . . . . . . . . . . . . . {__name__="request_total", method="POST"}
│ . . . . . . .
│ . . . . . . . . . . . . . . . . . . . ...
│ . . . . . . . . . . . . . . . . . . . . .
│ . . . . . . . . . . . . . . . . . . . . . {__name__="errors_total", method="POST"}
│ . . . . . . . . . . . . . . . . . {__name__="errors_total", method="GET"}
│ . . . . . . . . . . . . . .
│ . . . . . . . . . . . . . . . . . . . ...
│ . . . . . . . . . . . . . . . . . . . .
v
<-------------------- time --------------------->
二維模型中橫軸表示時(shí)間,縱軸表示各數(shù)據(jù)點(diǎn)
這類(lèi)設(shè)計(jì)會(huì)帶來(lái)的問(wèn)題如下
存儲(chǔ)問(wèn)題

如上圖所示,在二維模型中的讀寫(xiě)差別是很大的
(時(shí)間序列查詢(xún))讀時(shí)帶來(lái)的隨機(jī)讀問(wèn)題和查詢(xún)帶來(lái)的隨機(jī)寫(xiě)問(wèn)題,(查詢(xún))讀往往會(huì)比寫(xiě)更復(fù)雜,這是很慢的。盡管用了SSD,但會(huì)帶來(lái)寫(xiě)放大的問(wèn)題,SSD是4k寫(xiě),256k刪除,SSD之所以快,實(shí)際上靠的是算法,因此在文件碎片如此大的情況下,都是不能滿足的

理想狀態(tài)下的寫(xiě)應(yīng)該是順序?qū)?、批量?xiě),對(duì)于相同的時(shí)間序列讀應(yīng)該也是順序讀
4、存儲(chǔ)策略的演進(jìn)
4.1 1.x 版本
1.x 版本下,存儲(chǔ)情況是這樣的
每個(gè)時(shí)間序列都對(duì)應(yīng)一個(gè)文件 在內(nèi)存中批量處理 1kb 的的 chunk
┌──────────┬─────────┬─────────┬─────────┬─────────┐ series A
└──────────┴─────────┴─────────┴─────────┴─────────┘
┌──────────┬─────────┬─────────┬─────────┬─────────┐ series B
└──────────┴─────────┴─────────┴─────────┴─────────┘
. . .
┌──────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ series XYZ
└──────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
chunk 1 chunk 2 chunk 3 ...
存在的問(wèn)題:
chunk保存在內(nèi)存中,如果應(yīng)用程序或節(jié)點(diǎn)崩潰,它可能會(huì)丟失由于時(shí)間序列的維度很多,對(duì)于的文件個(gè)數(shù)也會(huì)很多,這可能耗盡操作系統(tǒng)的
inode上千的
chunk保存在硬盤(pán)需要持久化,可能會(huì)導(dǎo)致磁盤(pán)I/O非常繁忙磁盤(pán)
I/O打開(kāi)很多的文件,會(huì)導(dǎo)致非常高的延遲舊數(shù)據(jù)需要清理,這可能會(huì)導(dǎo)致
SSD的寫(xiě)放大非常大的
CPU、內(nèi)存、磁盤(pán)資源消耗序列的丟失和變動(dòng)
例如一些時(shí)間序列變得不活躍,而另一些時(shí)間序列變得活躍,原因在于例如k8s中應(yīng)用程序的連續(xù)自動(dòng)擴(kuò)展和頻繁滾動(dòng)更新帶來(lái)的實(shí)例的ip等變化,每天可能會(huì)創(chuàng)建數(shù)萬(wàn)個(gè)新應(yīng)用程序?qū)嵗约叭碌臅r(shí)間序列集
因此,即使整個(gè)基礎(chǔ)設(shè)施的規(guī)模大致保持不變,隨著時(shí)間的推移,數(shù)據(jù)庫(kù)中的時(shí)間序列也會(huì)線性增長(zhǎng)。即使Prometheus服務(wù)器能夠收集1000萬(wàn)個(gè)時(shí)間序列的數(shù)據(jù),但如果必須在10億個(gè)序列中找到數(shù)據(jù),查詢(xún)性能會(huì)受到很大影響
series
^
│ . . . . . .
│ . . . . . .
│ . . . . . .
│ . . . . . . .
│ . . . . . . .
│ . . . . . . .
│ . . . . . .
│ . . . . . .
│ . . . . .
│ . . . . .
│ . . . . .
v
<-------------------- time --------------------->
4.2 2.x 版本
2.x 時(shí)代的存儲(chǔ)布局
https://github.com/prometheus/prometheus/blob/release-2.25/tsdb/docs/format/README.md
4.2.1 數(shù)據(jù)存儲(chǔ)分塊
01xxxxx 數(shù)據(jù)塊
ULID,和UUID一樣,但是是按照字典和編碼的創(chuàng)建時(shí)間排序的chunk 目錄
包含各種系列的原始數(shù)據(jù)點(diǎn)塊,但不再是每個(gè)序列對(duì)應(yīng)一個(gè)單一的文件
index 數(shù)據(jù)索引
可以通過(guò)標(biāo)簽找到數(shù)據(jù),這里保存了
Label和Series的數(shù)據(jù)meta.json 可讀元數(shù)據(jù)
對(duì)應(yīng)存儲(chǔ)和它包含的數(shù)據(jù)的狀態(tài)
tombstone
刪除的數(shù)據(jù)將被記錄到這個(gè)文件中,而不是從塊文件中刪除
wal 預(yù)寫(xiě)日志 Write-Ahead Log
WAL段將被截?cái)嗟?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(150, 84, 181);">checkpoint.X目錄中chunks_head
在內(nèi)存中的數(shù)據(jù)
數(shù)據(jù)將每 2 小時(shí)保存到磁盤(pán)中
WAL 用于數(shù)據(jù)恢復(fù)
2 小時(shí)塊可以高效查詢(xún)范圍數(shù)據(jù)
分塊存儲(chǔ)后,每個(gè)目錄都是獨(dú)立的存儲(chǔ)目錄,結(jié)構(gòu)如下:
$ tree ./data
./data
├── b-000001
│ ├── chunks
│ │ ├── 000001
│ │ ├── 000002
│ │ └── 000003
│ ├── index
│ └── meta.json
├── b-000004
│ ├── chunks
│ │ └── 000001
│ ├── index
│ └── meta.json
├── b-000005
│ ├── chunks
│ │ └── 000001
│ ├── index
│ └── meta.json
└── b-000006
├── meta.json
└── wal
├── 000001
├── 000002
└── 000003
分塊存儲(chǔ)對(duì)應(yīng)著Blocks,可以看做是小型數(shù)據(jù)庫(kù)
將數(shù)據(jù)分成互不重疊的塊
每個(gè)塊都充當(dāng)一個(gè)完全獨(dú)立的數(shù)據(jù)庫(kù)
包含其時(shí)間窗口的所有時(shí)間序列數(shù)據(jù)
有自己的索引和塊文件集
每個(gè)數(shù)據(jù)塊都是不可變的
當(dāng)前塊可以追加數(shù)據(jù)
所有新數(shù)據(jù)都寫(xiě)入內(nèi)存數(shù)據(jù)庫(kù)
為了防止數(shù)據(jù)丟失,還寫(xiě)了一個(gè)臨時(shí) WAL
t0 t1 t2 t3 now
┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
│ │ │ │ │ │ │ │ ┌────────────┐
│ │ │ │ │ │ │ mutable │ <─── write ──── ┤ Prometheus │
│ │ │ │ │ │ │ │ └────────────┘
└───────────┘ └───────────┘ └───────────┘ └───────────┘ ^
└──────────────┴───────┬──────┴──────────────┘ │
│ query
│ │
merge ─────────────────────────────────────────────────┘
4.2.2 block 合并
上面分離了block后,會(huì)帶來(lái)的問(wèn)題
當(dāng)查詢(xún)多個(gè)塊時(shí),必須將它們的結(jié)果合并到一個(gè)整體結(jié)果中 如果我們需要一個(gè)星期的查詢(xún),它必須合并 80 多個(gè) block 塊
t0 t1 t2 t3 t4 now
┌────────────┐ ┌──────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
│ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 mutable │ before
└────────────┘ └──────────┘ └───────────┘ └───────────┘ └───────────┘
┌─────────────────────────────────────────┐ ┌───────────┐ ┌───────────┐
│ 1 compacted │ │ 4 │ │ 5 mutable │ after (option A)
└─────────────────────────────────────────┘ └───────────┘ └───────────┘
┌──────────────────────────┐ ┌──────────────────────────┐ ┌───────────┐
│ 1 compacted │ │ 3 compacted │ │ 5 mutable │ after (option B)
└──────────────────────────┘ └──────────────────────────┘ └───────────┘
4.2.3 數(shù)據(jù)保留
|
┌────────────┐ ┌────┼─────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
│ 1 │ │ 2 | │ │ 3 │ │ 4 │ │ 5 │ . . .
└────────────┘ └────┼─────┘ └───────────┘ └───────────┘ └───────────┘
|
|
retention boundary
第1塊可以被安全刪除,第2塊必須保持直到它完全超出邊界
塊合并帶來(lái)的影響
塊壓縮可能使塊太大而無(wú)法刪除 需要限制塊的大小
最大塊大小 = 保留窗口 * 10%
4.2.4 查詢(xún)和索引
主要特點(diǎn)
使用倒排索引,倒排索引提供基于其內(nèi)容子集的數(shù)據(jù)項(xiàng)的快速查找。例如,可以查找所有具有標(biāo)簽的系列,
app=”nginx"而無(wú)需遍歷每個(gè)系列并檢查它是否包含該標(biāo)簽正向索引,為每個(gè)序列分配一個(gè)唯一的
ID,通過(guò)它可以在恒定的時(shí)間內(nèi)檢索
一個(gè)目錄中保存了很多Series,如果想要根據(jù)一個(gè)Label來(lái)查詢(xún)對(duì)應(yīng)的所有Series,具體流程是什么呢
為每個(gè)Series中的所有Label都建立了一個(gè)倒排索引
| Label | Series |
|---|---|
__name__="requests_total" | {__name__="requests_total", path="/status", method="GET", instance=”10.0.0.1:80”} |
path="/status" | {__name__="requests_total", path="/status", method="GET", instance=”10.0.0.1:80”} |
method="GET" | {__name__="requests_total", path="/status", method="GET", instance=”10.0.0.1:80”} |
instance=”10.0.0.1:80” | {__name__="requests_total", path="/status", method="GET", instance=”10.0.0.1:80”} |
正向索引的引入,給每個(gè)Series分配了一個(gè)ID,便于組合查詢(xún)
| Label | SeriesID |
|---|---|
__name__="requests_total" | 1001 |
path="/status" | 1001 |
method="GET" | 1001 |
instance=”10.0.0.1:80” | 1001 |
例如,如果查詢(xún)的語(yǔ)句是:__name __ =“requests_total” AND app =“nginx”
需要先分別找出對(duì)應(yīng)的倒排索引,再求交集,由此會(huì)帶來(lái)一定的時(shí)間復(fù)雜度O(N2,為了減少時(shí)間復(fù)雜度,實(shí)際上倒排索引中的SeriesID是有序的,那么采取ZigZag的查找方式,可以保證在O(N)的時(shí)間復(fù)雜來(lái)找到最終的結(jié)果
4.2.6 WAL
通過(guò)mmap(不經(jīng)過(guò)文件系統(tǒng)的寫(xiě)數(shù)據(jù)方式),同時(shí)在內(nèi)存和WAL預(yù)寫(xiě)日志Write-Ahead Log中保存數(shù)據(jù),即可以保證數(shù)據(jù)的持久不丟失,又可以保證崩潰之后從故障中恢復(fù)的時(shí)間很短,因?yàn)槭菑膬?nèi)存中恢復(fù)
4.2.7 小結(jié)
新的存儲(chǔ)結(jié)構(gòu)帶來(lái)的好處
在查詢(xún)某個(gè)時(shí)間范圍時(shí),可以輕松忽略該范圍之外的所有數(shù)據(jù)塊。它通過(guò)減少檢查數(shù)據(jù)集來(lái)輕松解決數(shù)據(jù)流失問(wèn)題 當(dāng)完成一個(gè)塊時(shí),可以通過(guò)順序?qū)懭胍恍┹^大的文件來(lái)保存內(nèi)存數(shù)據(jù)庫(kù)中的數(shù)據(jù)。避免任何寫(xiě)放大,并同樣為 SSD和HDD提供服務(wù)保留了 V2的良好特性,即最近查詢(xún)最多的塊總是在內(nèi)存中的不再受限于固定的 1KiB塊大小來(lái)更好地對(duì)齊磁盤(pán)上的數(shù)據(jù)。可以選擇對(duì)單個(gè)數(shù)據(jù)點(diǎn)和所選壓縮格式最有意義的任何大小刪除舊數(shù)據(jù)變得非常便宜和即時(shí),只需要?jiǎng)h除一個(gè)目錄。在舊版本的存儲(chǔ)中,必須分析和重寫(xiě)多達(dá)數(shù)億個(gè)文件,這可能需要數(shù)小時(shí)才能收斂
參考資料
https://www.bilibili.com/video/BV1a64y1X7ys
[2]https://fabxc.org/tsdb/
[3]http://ganeshvernekar.com/blog/prometheus-tsdb-the-head-block/
