Prometheus TSDB存儲原理


Prometheus 包含一個存儲在本地磁盤的時間序列數據庫,同時也支持與遠程存儲系統集成,比如grafana cloud 提供的免費雲存儲API,只需將remote_write接口信息填寫在Prometheus配置文件即可。

image-20220412141006992

本文不涉及遠程存儲接口內容,主要介紹Prometheus 時序數據的本地存儲實現原理。

什么是時序數據?


在學習Prometheus TSDB存儲原理之前,我們先來認識一下Prometheus TSDB、InfluxDB這類時序數據庫的時序數據指的是什么?

時序數據通常以(key,value)的形式出現,在時間序列采集點上所對應值的集,即每個數據點都是一個由時間戳和值組成的元組。

identifier->(t0,v0),(t1,v1),(t2,v2)...

Prometheus TSDB的數據模型

<metric name>{<label name>=<label value>, ...} 

具體到某個實例中

requests_total{method="POST", handler="/messages"}  

在存儲時可以通過name label來標記metric name,再通過標識符@來標識時間,這樣構成了一個完整的時序數據樣本。

 ----------------------------------------key-----------------------------------------------value---------
{__name__="requests_total",method="POST", handler="/messages"}   @1649483597.197             52

一個時間序列是一組時間上嚴格單調遞增的數據點序列,它可以通過metric來尋址。抽象成二維平面來看,二維平面的橫軸代表單調遞增的時間,metrics 遍及整個縱軸。在提取樣本數據時只要給定時間窗口和metric就可以得到value

series

時序數據如何在Prometheus TSDB存儲?


上面我們簡單了解了時序數據,接下來我們展開Prometheus TSDB存儲(V3引擎)

Prometheus TSDB 概覽

image-20220413104124771

在上圖中,Head 塊是TSDB的內存塊,灰色塊Block是磁盤上的持久塊。

首先傳入的樣本(t,v)進入 Head 塊,為了防止內存數據丟失先做一次預寫日志 (WAL),並在內存中停留一段時間,然后刷新到磁盤並進行內存映射(M-map)。當這些內存映射的塊或內存中的塊老化到某個時間點時,會作為持久塊Block存儲到磁盤。接下來多個Block在它們變舊時被合並,並在超過保留期限后被清理。

Head中樣本的生命周期

image-20220413120050962

當一個樣本傳入時,它會被加載到Head中的active chunk(紅色塊),這是唯一一個可以主動寫入數據的單元,為了防止內存數據丟失還會做一次預寫日志 (WAL)

image-20220413120803681

一旦active chunk被填滿時(超過2小時或120樣本),將舊的數據截斷為head_chunk1。

image-20220413121223066

head_chunk1被刷新到磁盤然后進行內存映射。active chunk繼續寫入數據、截斷數據、寫入到內存映射,如此反復。

image-20220413121732282

內存映射應該只加載最新的、最被頻繁使用的數據,所以Prometheus TSDB將就是舊數據刷新到磁盤持久化存儲Block,如上1-4為舊數據被寫入到下圖的Block中。

image-20220413113035412

此時我們再來看一下Prometheus TSDB 數據目錄基本結構,好像更清晰了一些。

./data
├── 01BKGV7JBM69T2G1BGBGM6KB12    
│   └── meta.json
├── 01BKGTZQ1SYQJTR4PB43C8PD98   # block ID
│   ├── chunks  	   # Block中的chunk文件
│   │   └── 000001     
│   ├── tombstones     # 數據刪除記錄文件
│   ├── index          # 索引
│   └── meta.json	   # bolck元信息
├── chunks_head		   # head內存映射
│   └── 000001		  
└── wal			       # 預寫日志
    ├── 000000002	  
    └── checkpoint.00000001
        └── 00000000
WAL 中checkpoint的作用

我們需要定期刪除舊的 wal 數據,否則磁盤最終會被填滿,並且在TSDB重啟時 replay wal 事件時會占用大量時間,所以wal中任何不再需要的數據,都需要被清理。而checkpoint會將wal 清理過后的數據做過濾寫成新的段。

如下有6個wal數據段

data
└── wal
    ├── 000000
    ├── 000001
    ├── 000002
    ├── 000003
    ├── 000004
    └── 000005

現在我們要清理時間點T之前的樣本數據,假設為前4個數據段:

檢查點操作將按000000 000001 000002 000003順序遍歷所有記錄,並且:

  1. 刪除不再在 Head 中的所有序列記錄。
  2. 丟棄所有 time 在T之前的樣本。
  3. 刪除T之前的所有 tombstone 記錄。
  4. 重寫剩余的序列、樣本和tombstone記錄(與它們在 WAL 中出現的順序相同)。

checkpoint被命名為創建checkpoint的最后一個段號checkpoint.X

這樣我們得到了新的wal數據,當wal在replay時先找checkpoint,先從checkpoint中的數據段回放,然后是checkpoint.000003的下一個數據段000004

data
└── wal
    ├── checkpoint.000003
    |   ├── 000000
    |   └── 000001
    ├── 000004
    └── 000005
Block的持久化存儲

上面我們認識了wal和chunks_head的存儲構造,接下來是Block,什么是持久化Block?在什么時候創建?為啥要合並Block?

Block的目錄結構

├── 01BKGTZQ1SYQJTR4PB43C8PD98   # block ID
│   ├── chunks  	   # Block中的chunk文件
│   │   └── 000001     
│   ├── tombstones     # 數據刪除記錄文件
│   ├── index          # 索引
│   └── meta.json	   # bolck元信息

磁盤上的Block是固定時間范圍內的chunk的集合,由它自己的索引組成。其中包含多個文件的目錄。每個Block都有一個唯一的 ID(ULID),他這個ID是可排序的。當我們需要更新、修改Block中的一些樣本時,Prometheus TSDB只能重寫整個Block,並且新塊具有新的 ID(為了實現后面提到的索引)。如果需要刪除的話Prometheus TSDB通過tombstones 實現了在不觸及原始樣本的情況下進行清理。

tombstones 可以認為是一個刪除標記,它記載了我們在讀取序列期間要忽略哪些時間范圍。tombstones 是Block中唯一在寫入數據后用於存儲刪除請求所創建和修改的文件。

tombstones中的記錄數據結構如下,分別對應需要忽略的序列、開始和結束時間。

┌────────────────────────┬─────────────────┬─────────────────┐
│ series ref <uvarint64> │ mint <varint64> │ maxt <varint64> │
└────────────────────────┴─────────────────┴─────────────────┘

meta.json

meta.json包含了整個Block的所有元數據

{
    "ulid": "01EM6Q6A1YPX4G9TEB20J22B2R",
    "minTime": 1602237600000,
    "maxTime": 1602244800000,
    "stats": {
        "numSamples": 553673232,
        "numSeries": 1346066,
        "numChunks": 4440437
    },
    "compaction": {
        "level": 1,
        "sources": [
            "01EM65SHSX4VARXBBHBF0M0FDS",
            "01EM6GAJSYWSQQRDY782EA5ZPN"
        ]
    },
    "version": 1
}

記錄了人類可讀的chunks的開始和結束時間,樣本、序列、chunks數量以及合並信息。version告訴Prometheus如何解析metadata

Block合並

image-20220413113035412

我們可以從之前的圖中看到當內存映射中chunk跨越2小時(默認)后第一個Block就被創建了,當 Prometheus 創建了一堆Block時,我們需要定期對這些塊進行維護,以有效利用磁盤並保持查詢的性能。

Block合並的主要工作是將一個或多個現有塊(source blocks or parent blocks)寫入一個新塊,最后,刪除源塊並使用新的合並后的Block代替這些源塊。

為什么需要對Block進行合並?

  1. 上面對tombstones介紹我們知道Prometheus在對數據的刪除操作會記錄在單獨文件stombstone中,而數據仍保留在磁盤上。因此,當stombstone序列超過某些百分比時,需要從磁盤中刪除該數據。
  2. 如果樣本數據值波動非常小,相鄰兩個Block中的大部分數據是相同的。對這些Block做合並的話可以減少重復數據,從而節省磁盤空間。
  3. 當查詢命中大於1個Block時,必須合並每個塊的結果,這可能會產生一些額外的開銷。
  4. 如果有重疊的Block(在時間上重疊),查詢它們還要對Block之間的樣本進行重復數據刪除,合並這些重疊塊避免了重復數據刪除的需要。
  5. image-20220414120529698

如上圖示例所示,我們有一組順序的Block[1, 2, 3, 4]。數據塊1,2,和3可以被合並形成的新的塊是[1, 4]。或者成對壓縮為[1,3]。 所有的時間序列數據仍然存在,但是現在總體的數據塊更少。 這顯著降低了查詢成本。

Block是如何刪除的?

對於源數據的刪除Prometheus TSDB采用了一種簡單的方式:即刪除該目錄下不在我們保留時間窗口的塊。

如下圖所示,塊1可以安全地被刪除,而2必須保留到完全落在邊界之后

image-20220413202322093

因為Block合並的存在,意味着獲取越舊的數據,數據塊可能就變得越大。 因此必須得有一個合並的上限,,這樣塊就不會增長到跨越整個數據庫。通常我們可以根據保留窗口設置百分比。

如何從大量的series中檢索出數據?


在Prometheus TSDB V3引擎中使用了倒排索引,倒排索引基於它們內容的子集提供對數據項的快速查找,例如我們要找出所有帶有標簽app ="nginx"的序列,而無需遍歷每一個序列然后再檢查它是否包含該標簽。

首先我們給每個序列分配一個唯一ID,查詢ID的復雜度是O(1),然后給每個標簽建一個倒排ID表。比如包含app ="nginx"標簽的ID為1,11,111那么標簽"nginx"的倒排序索引為[1,11,111],這樣一來如果n是我們的序列總數,m是查詢的結果大小,那么使用倒排索引的查詢復雜度是O(m),也就是說查詢的復雜度由m的數量決定。但是在最壞的情況下,比如我們每個序列都有一個“nginx”的標簽,顯然此時的復雜度變為O(n)了,如果是個別標簽的話無可厚非,只能稍加等待了,但是現實並非如此。

標簽被關聯到數百萬序列是很常見的,並且往往每次查詢會檢索多個標簽,比如我們要查詢這樣一個序列app =“dev”AND app =“ops” 在最壞情況下復雜度是O(n2),接着更多標簽復雜度指數增長到O(n3)、O(n4)、O(n5)... 這是不可接受的。那咋辦呢?

如果我們將倒排表進行排序會怎么樣?

"app=dev" -> [100,1500,20000,51166]
"app=ops" -> [2,4,8,10,50,100,20000]

他們的交集為[100,20000],要快速實現這一點,我們可以通過2個游標從列表值較小的一端率先推進,當值相等時就是可以加入到結果集合當中。這樣的搜索成本顯然更低,在k個倒排表搜索的復雜度為O(k*n)而非最壞情況下O(n^k)

剩下就是維護這個索引,通過維護時間線與ID、標簽與倒排表的映射關系,可以保證查詢的高效率。


以上我們從較淺的層面了解一下Prometheus TSDB存儲相關的內容,本文仍然有很多細節沒有提及,比如wal如何做壓縮與回放,mmap的原理,TSDB存儲文件的數據結構等等,如果你需要進一步學習可移步參考文章。通過博客閱讀:iqsing.github.io


本文參考於:

Prometheus維護者Ganesh Vernekar的系列博客Prometheus TSDB

Prometheus維護者Fabian的博客文章Writing a Time Series Database from Scratch(原文已失效)

PromCon 2017: Storing 16 Bytes at Scale - Fabian Reinartz


免責聲明!

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



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