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

本文是結(jié)合耗子叔的視頻及 Prometheus 作者部分原文整理,加上部分個人理解而來,膜拜大神~
1、概述
Prometheus是一套開源的監(jiān)控&報警&時間序列數(shù)據(jù)庫的組合
Prometheus內(nèi)部主要分為三大塊,Retrieval是負(fù)責(zé)定時去暴露的目標(biāo)頁面上去抓取采樣指標(biāo)數(shù)據(jù),Storage是負(fù)責(zé)將采樣數(shù)據(jù)寫磁盤,PromQL是Prometheus提供的查詢語言模塊
其有著非常高效的時間序列數(shù)據(jù)存儲方法,每個采樣數(shù)據(jù)僅僅占用3.5byte左右空間
在早期有一個單獨的項目叫做 TSDB,但是,在2.1.x的某個版本,已經(jīng)不單獨維護(hù)這個項目了,直接將這個項目合并到了prometheus的主干上了
prometheus每次抓取的數(shù)據(jù),對于操作者來說可見的格式(即在prometheus界面查詢到的值)
requests_total{path="/status", method="GET", instance="10.0.0.1:80"} @1534317560938 94355
意思就是在1534317560938這個時間點,10.0.0.1:80這個實例上,GET /status 這個請求的次數(shù)累計是 94355次
最終存儲在TSDB中的格式為
{__name__="requests_total", path="/status", method="GET", instance="10.0.0.1:80"}
2、時間序列
Data scheme數(shù)據(jù)標(biāo)識
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 查詢
__name__="requests_total":查詢所有屬于requests_total的序列
method="PUT|POST":查詢所有序列中方法是PUT或POST的序列
3、二維模型
Write 寫:每個目標(biāo)暴露成百上千個不同的時間序列,寫入模式是完全垂直和高度并發(fā)的,因為來自每個目標(biāo)的樣本是獨立的
Query 查:查詢數(shù)據(jù)時可以并行和批處理
series
^
│ . . . . . . . . . . . . . . . . . . . . . . {__name__="request_total", method="GET"}
│ . . . . . . . . . . . . . . . . . . . . . . {__name__="request_total", method="POST"}
│ . . . . . . .
│ . . . . . . . . . . . . . . . . . . . ...
│ . . . . . . . . . . . . . . . . . . . . .
│ . . . . . . . . . . . . . . . . . . . . . {__name__="errors_total", method="POST"}
│ . . . . . . . . . . . . . . . . . {__name__="errors_total", method="GET"}
│ . . . . . . . . . . . . . .
│ . . . . . . . . . . . . . . . . . . . ...
│ . . . . . . . . . . . . . . . . . . . .
v
<-------------------- time --------------------->
二維模型中橫軸表示時間,縱軸表示各數(shù)據(jù)點
這類設(shè)計會帶來的問題如下
存儲問題

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

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