淺談Prometheus的數據存儲


本文是結合耗子叔的視頻及Prometheus作者部分原文整理,加上部分個人理解而來,膜拜大神~

1、概述

Prometheus是一套開源的監控&報警&時間序列數據庫的組合

Prometheus內部主要分為三大塊,Retrieval是負責定時去暴露的目標頁面上去抓取采樣指標數據,Storage是負責將采樣數據寫磁盤,PromQLPrometheus提供的查詢語言模塊

其有着非常高效的時間序列數據存儲方法,每個采樣數據僅僅占用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":查詢所有序列中方法是PUTPOST的序列

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,但會帶來寫放大的問題,SSD4k寫,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 數據索引

    可以通過標簽找到數據,這里保存了LabelSeries的數據

  • 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 小結

新的存儲結構帶來的好處

  • 在查詢某個時間范圍時,可以輕松忽略該范圍之外的所有數據塊。它通過減少檢查數據集來輕松解決數據流失問題
  • 當完成一個塊時,可以通過順序寫入一些較大的文件來保存內存數據庫中的數據。避免任何寫放大,並同樣為 SSDHDD提供服務
  • 保留了V2的良好特性,即最近查詢最多的塊總是在內存中的
  • 不再受限於固定的1KiB塊大小來更好地對齊磁盤上的數據。可以選擇對單個數據點和所選壓縮格式最有意義的任何大小
  • 刪除舊數據變得非常便宜和即時,只需要刪除一個目錄。在舊版本的存儲中,必須分析和重寫多達數億個文件,這可能需要數小時才能收斂

參考
https://www.bilibili.com/video/BV1a64y1X7ys
https://fabxc.org/tsdb/
http://ganeshvernekar.com/blog/prometheus-tsdb-the-head-block/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM