本文是結合耗子叔的視頻及Prometheus作者部分原文整理,加上部分個人理解而來,膜拜大神~
1、概述
Prometheus
是一套開源的監控&報警&時間序列數據庫的組合
Prometheus
內部主要分為三大塊,Retrieval
是負責定時去暴露的目標頁面上去抓取采樣指標數據,Storage
是負責將采樣數據寫磁盤,PromQL
是Prometheus
提供的查詢語言模塊
其有着非常高效的時間序列數據存儲方法,每個采樣數據僅僅占用3.5byte
左右空間
在早期有一個單獨的項目叫做 TSDB
,但是,在2.1.x
的某個版本,已經不單獨維護這個項目了,直接將這個項目合並到了prometheus
的主干上了
prometheus
每次抓取的數據,對於操作者來說可見的格式(即在prometheus
界面查詢到的值)
requests_total{path="/status", method="GET", instance="10.0.0.1:80"} @1534317560938 94355
意思就是在1534317560938
這個時間點,10.0.0.1:80
這個實例上,GET /status
這個請求的次數累計是 94355
次
最終存儲在TSDB
中的格式為
{__name__="requests_total", path="/status", method="GET", instance="10.0.0.1:80"}
2、時間序列
Data scheme
數據標識
identifier -> (t0, v0), (t1, v1), (t2, v2), (t3, v3), ...
Prometheus Data Model
數據模型
<metric name>{<label name>=<label value>, ...}
Typical set of series identifiers
- Query 查詢
__name__="requests_total"
:查詢所有屬於requests_total
的序列
method="PUT|POST"
:查詢所有序列中方法是PUT
或POST
的序列
3、二維模型
-
Write寫:每個目標暴露成百上千個不同的時間序列,寫入模式是完全垂直和高度並發的,因為來自每個目標的樣本是獨立的
-
Query查:查詢數據時可以並行和批處理
series
^
│ . . . . . . . . . . . . . . . . . . . . . . {__name__="request_total", method="GET"}
│ . . . . . . . . . . . . . . . . . . . . . . {__name__="request_total", method="POST"}
│ . . . . . . .
│ . . . . . . . . . . . . . . . . . . . ...
│ . . . . . . . . . . . . . . . . . . . . .
│ . . . . . . . . . . . . . . . . . . . . . {__name__="errors_total", method="POST"}
│ . . . . . . . . . . . . . . . . . {__name__="errors_total", method="GET"}
│ . . . . . . . . . . . . . .
│ . . . . . . . . . . . . . . . . . . . ...
│ . . . . . . . . . . . . . . . . . . . .
v
<-------------------- time --------------------->
二維模型中橫軸表示時間,縱軸表示各數據點
這類設計會帶來的問題如下
存儲問題
如上圖所示,在二維模型中的讀寫差別是很大的
(時間序列查詢)讀時帶來的隨機讀問題和查詢帶來的隨機寫問題,(查詢)讀往往會比寫更復雜,這是很慢的。盡管用了SSD
,但會帶來寫放大的問題,SSD
是4k
寫,256k
刪除,SSD
之所以快,實際上靠的是算法,因此在文件碎片如此大的情況下,都是不能滿足的
理想狀態下的寫應該是順序寫、批量寫,對於相同的時間序列讀應該也是順序讀
4、存儲策略的演進
4.1 1.x版本
1.x版本下,存儲情況是這樣的
- 每個時間序列都對應一個文件
- 在內存中批量處理1kb的的chunk
┌──────────┬─────────┬─────────┬─────────┬─────────┐ series A
└──────────┴─────────┴─────────┴─────────┴─────────┘
┌──────────┬─────────┬─────────┬─────────┬─────────┐ series B
└──────────┴─────────┴─────────┴─────────┴─────────┘
. . .
┌──────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ series XYZ
└──────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
chunk 1 chunk 2 chunk 3 ...
存在的問題:
-
chunk
保存在內存中,如果應用程序或節點崩潰,它可能會丟失 -
由於時間序列的維度很多,對於的文件個數也會很多,這可能耗盡操作系統的
inode
-
上千的
chunk
保存在硬盤需要持久化,可能會導致磁盤I/O
非常繁忙 -
磁盤
I/O
打開很多的文件,會導致非常高的延遲 -
舊數據需要清理,這可能會導致
SSD
的寫放大 -
非常大的
CPU
、內存、磁盤資源消耗 -
序列的丟失和變動
例如一些時間序列變得不活躍,而另一些時間序列變得活躍,原因在於例如k8s
中應用程序的連續自動擴展和頻繁滾動更新帶來的實例的ip
等變化,每天可能會創建數萬個新應用程序實例,以及全新的時間序列集
因此,即使整個基礎設施的規模大致保持不變,隨着時間的推移,數據庫中的時間序列也會線性增長。即使Prometheus
服務器能夠收集1000
萬個時間序列的數據,但如果必須在10
億個序列中找到數據,查詢性能會受到很大影響
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 數據存儲分塊
-
01xxxxx 數據塊
ULID
,和UUID
一樣,但是是按照字典和編碼的創建時間排序的 -
chunk 目錄
包含各種系列的原始數據點塊,但不再是每個序列對應一個單一的文件
-
index 數據索引
可以通過標簽找到數據,這里保存了
Label
和Series
的數據 -
meta.json 可讀元數據
對應存儲和它包含的數據的狀態
-
tombstone
刪除的數據將被記錄到這個文件中,而不是從塊文件中刪除
-
wal 預寫日志Write-Ahead Log
WAL
段將被截斷到checkpoint.X
目錄中 -
chunks_head
在內存中的數據
-
數據將每2小時保存到磁盤中
-
WAL用於數據恢復
-
2小時塊可以高效查詢范圍數據
分塊存儲后,每個目錄都是獨立的存儲目錄,結構如下:
$ 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
分塊存儲對應着Blocks
,可以看做是小型數據庫
-
將數據分成互不重疊的塊
每個塊都充當一個完全獨立的數據庫
包含其時間窗口的所有時間序列數據
有自己的索引和塊文件集
-
每個數據塊都是不可變的
-
當前塊可以追加數據
-
所有新數據都寫入內存數據庫
-
為了防止數據丟失,還寫了一個臨時WAL
t0 t1 t2 t3 now
┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
│ │ │ │ │ │ │ │ ┌────────────┐
│ │ │ │ │ │ │ mutable │ <─── write ──── ┤ Prometheus │
│ │ │ │ │ │ │ │ └────────────┘
└───────────┘ └───────────┘ └───────────┘ └───────────┘ ^
└──────────────┴───────┬──────┴──────────────┘ │
│ query
│ │
merge ─────────────────────────────────────────────────┘
4.2.2 block合並
上面分離了block
后,會帶來的問題
- 當查詢多個塊時,必須將它們的結果合並到一個整體結果中
- 如果我們需要一個星期的查詢,它必須合並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 數據保留
|
┌────────────┐ ┌────┼─────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
│ 1 │ │ 2 | │ │ 3 │ │ 4 │ │ 5 │ . . .
└────────────┘ └────┼─────┘ └───────────┘ └───────────┘ └───────────┘
|
|
retention boundary
第1
塊可以被安全刪除,第2
塊必須保持直到它完全超出邊界
塊合並帶來的影響
- 塊壓縮可能使塊太大而無法刪除
- 需要限制塊的大小
最大塊大小 = 保留窗口 * 10%
4.2.4 查詢和索引
主要特點
-
使用倒排索引,倒排索引提供基於其內容子集的數據項的快速查找。例如,可以查找所有具有標簽的系列,
app=”nginx"
而無需遍歷每個系列並檢查它是否包含該標簽 -
正向索引,為每個序列分配一個唯一的
ID
,通過它可以在恆定的時間內檢索
一個目錄中保存了很多Series
,如果想要根據一個Label
來查詢對應的所有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”
需要先分別找出對應的倒排索引,再求交集,由此會帶來一定的時間復雜度O(N2
,為了減少時間復雜度,實際上倒排索引中的SeriesID
是有序的,那么采取ZigZag
的查找方式,可以保證在O(N)
的時間復雜來找到最終的結果
4.2.6 WAL
通過mmap
(不經過文件系統的寫數據方式),同時在內存和WAL
預寫日志Write-Ahead Log
中保存數據,即可以保證數據的持久不丟失,又可以保證崩潰之后從故障中恢復的時間很短,因為是從內存中恢復
4.2.7 小結
新的存儲結構帶來的好處
- 在查詢某個時間范圍時,可以輕松忽略該范圍之外的所有數據塊。它通過減少檢查數據集來輕松解決數據流失問題
- 當完成一個塊時,可以通過順序寫入一些較大的文件來保存內存數據庫中的數據。避免任何寫放大,並同樣為
SSD
和HDD
提供服務 - 保留了
V2
的良好特性,即最近查詢最多的塊總是在內存中的 - 不再受限於固定的
1KiB
塊大小來更好地對齊磁盤上的數據。可以選擇對單個數據點和所選壓縮格式最有意義的任何大小 - 刪除舊數據變得非常便宜和即時,只需要刪除一個目錄。在舊版本的存儲中,必須分析和重寫多達數億個文件,這可能需要數小時才能收斂
參考
https://www.bilibili.com/video/BV1a64y1X7ys
https://fabxc.org/tsdb/
http://ganeshvernekar.com/blog/prometheus-tsdb-the-head-block/