Prometheus 數據存儲那些事兒

本篇文章使用的是 Prometheus v2.40 源碼

本篇文章主要是整理一下 Prometheus 的時序數據庫是怎麼存儲以及管理數據的,希望這篇文章能給大家帶來一定的啓發。

概述

我們先來看看 Prometheus 的整個架構圖:

Prometheus architecture

對於 Prometheus Server 來說,整個數據處理流程分爲三部分:Exporter 指標的收集、Scraper 數據的抓取、TSDB 數據的存儲及查詢;Exporter 其實不同的服務有不同的收集實現,類似 Mysqld-exporter、node exporter 等我們這裏不做過多的解讀,大家基本只需要知道它會暴露一個端口,由 Scraper 定時從它上面採集就好了。Scraper 數據抓取部門我們下面再詳細說明。

我們先來認識一下,什麼是時序數據庫 (Time Series Database, TSDB)。 TSDB 是專門用來存儲隨時間變化的數據,如股票價格、傳感器數據等。時間序列(time-series)的是某個變量隨時間變化的所有歷史,而樣本 (sample)指的是歷史中該變量的瞬時值:

回到我們的主角 Prometheus, 它會將所有采集到的樣本(sample)數據以時間序列(time-series)的方式保存在內存數據庫中,並且定時保存到硬盤上。時間序列是按照時間戳和值的序列順序存放的,每條 time-series 通過指標名稱(metrics name)和一組標籤集(labelset)命名。如下所示,可以將時間序列理解爲一個以時間爲 Y 軸的數字矩陣:

  ^
  │   . . . . . . . . . . . . . . . . .   . .   node_cpu{cpu="cpu0",mode="idle"}
  │     . . . . . . . . . . . . . . . . . . .   node_cpu{cpu="cpu0",mode="system"}
  │     . . . . . . . . . .   . . . . . . . .   node_load1{}
  │     . . . . . . . . . . . . . . . .   . .  
  v
    <------------------ 時間 ---------------->

所以根據上面的圖,我們可以理解到,每一個點稱爲一個樣本(sample),樣本由以下三部分組成:

TSDB 數據寫入

對於 Prometheus 的 TSDB 先是寫入到 head 數據塊和 WAL(Write-Ahead-Log)預寫日誌中,head 數據塊是位於內存中,WAL 日誌是用來做數據的臨時持久化用的,防止掉電重啓之後仍然能恢復內存中的數據。head 內存中的數據寫入一段時間之後會通過 mmap 以數據塊 chunk 的形式刷到磁盤中,內存中只保留對 chunk 的引用。當持久化到磁盤的 chunks 數據量達到一定閾值的時候,就會將這批老數據從 chunks 中剝離出來變成 block 數據塊。更老的多個小的 block 會定期合成一個大的 block,最後直到 block 保存時間達到閾值被刪除。

默認所有數據都會放在 ./data 目錄下面,裏面存放了 chunks_head、wal、block 三種類型的數據。

./data
├── 01GJ9EKDWSS1TA1V0RBP707V21
│   ├── chunks
│   │   └── 000001
│   ├── index
│   ├── meta.json
│   └── tombstones
├── chunks_head
│   └── 000012 
└── wal
    ├── 00000013
    ├── 00000014
    ├── 00000015
    └── checkpoint.00000012
        └── 00000000

每個 block 會存儲 2 小時時間窗口內所有 series 指標數據,每個 block 文件名都會使用 github.com/oklog/ulid 這個庫生成不重複的文件名,裏面包含了 metadata 文件、index 文件、chunks 文件夾,所有指標數據都存放在 chunks 文件夾中,chunks 中包含了多個數據段信息,每個數據段會按照 512MB 分成一個文件存儲,被刪除的數據會存放在 tombstone 文件夾中。

chunks_head 文件夾裏面也包含了多個 chunks ,當內存的 head block 寫不下了會將數據存放在這個文件夾下面,並保留對文件的引用。

wal 文件夾裏面存放的數據是當前正在寫入的數據,裏面包含多個數據段文件,一個文件默認最大 128M,Prometheus 會至少保留 3 個文件,對於高負載的機器會至少保留 2 小時的數據。wal 文件夾裏面的數據是沒有壓縮過的,所以會比 block 裏面的數據略大一些。

head block

v2.19 之前,最近 2 小時的指標數據存儲在內存中,v2.19 引入 head block,最近的指標數據存儲在內存中,當內存存滿時將數據刷入到磁盤中,並通過一個引用關聯刷到磁盤的數據。

head block 是唯一活躍的 block,除了它以外,其它 blocks 都是不可變的。我們在上面也說了,數據每次從 scraper 抓取過來之後都會存放到 appender 裏面,這個 appender 實際上就是 headAppender, 通過調用 Append 方法將數據暫時緩存起來,通過 appender 來做批量的添加,然後 commit 纔會真正寫數據。

// tsdb/head_append.go
func (a *headAppender) Append(ref storage.SeriesRef, lset labels.Labels, t int64, v float64) (storage.SeriesRef, error) {
    // 獲取該stripeseries對應的memSeries,ref實際上是lset的hash值
    s := a.head.series.getByID(chunks.HeadSeriesRef(ref))
    if s == nil {
         
        var created bool
        var err error
        // 不存在則創建爲該指標創建一個 memSeries
        s, created, err = a.head.getOrCreate(lset.Hash(), lset)
        if err != nil {
            return 0, err
        }
        if created {
            a.series = append(a.series, record.RefSeries{
                Ref:    s.ref,
                Labels: lset,
            })
        }
    }
    ...
    // 將數據緩存起來
    a.samples = append(a.samples, record.RefSample{
        Ref: s.ref,
        T:   t,
        V:   v,
    })
    a.sampleSeries = append(a.sampleSeries, s)
    return storage.SeriesRef(s.ref), nil
}

所有的指標數據都是存放在 stripeseries,每次都需要 lset(相當於是指標的 key)獲取對應的 memSeries,getByID 裏面會根據傳入的 lset 維度來加鎖,通過分段鎖的方式減少鎖衝突。memSeries 是真正的數據存放的地方,存放了最近添加的指標對應的(t,v)鍵值對。所以這裏只是設置了 sampleSeries 和 samples 的關聯關係,等到下面 Commit 的時候會將相應的數據取出來添加到 memSeries 。

// tsdb/head_append.go
func (a *headAppender) Commit() (err error) {
     
    // 寫入wal日誌
    if err := a.log(); err != nil {
        _ = a.Rollback() // Most likely the same error will happen again.
        return errors.Wrap(err, "write to WAL")
    } 
    ...
    for i, s := range a.samples {
        series = a.sampleSeries[i]
        series.Lock()  
        ...
        // 寫入數據
        ok, chunkCreated = series.append(s.T, s.V, a.appendID, a.head.chunkDiskMapper, chunkRange)
        series.Unlock()
    }
    ... 
    return nil
}

Commit 方法會將保存在 samples 中的數據拿出來,然後通過調用 memSeries 的 append 將數據循環寫入。

// tsdb/head_append.go
func (s *memSeries) append(t int64, v float64, appendID uint64, chunkDiskMapper *chunks.ChunkDiskMapper, chunkRange int64) (sampleInOrder, chunkCreated bool) {
    // 判斷head 裏面的 chunk 是否已滿需要寫入到磁盤,創建新的chunk
    c, sampleInOrder, chunkCreated := s.appendPreprocessor(t, chunkenc.EncXOR, chunkDiskMapper, chunkRange) 
    // 調用 xorAppender 添加指標數據
    s.app.Append(t, v) 
    c.maxTime = t
    ...
    return true, chunkCreated
}

寫入的時候會校驗當前寫入的 chunk 是否已經寫滿了 120 個 sample ,如果寫滿了,那麼需要將老的數據從 head chunk 中通過 mmap 寫入到磁盤中。如果設置的是 15s 的抓取間隔,那麼 120 個 sample 的跨度是 30 分鐘。

// tsdb/head_append.go
func (s *memSeries) appendPreprocessor(
    t int64, e chunkenc.Encoding, chunkDiskMapper *chunks.ChunkDiskMapper, chunkRange int64,
) (c *memChunk, sampleInOrder, chunkCreated bool) { 
    const samplesPerChunk = 120

    c = s.head()

    if c == nil {  
        // head chunk 裏面還沒有chunk ,那麼先創建一個
        c = s.cutNewHeadChunk(t, e, chunkDiskMapper, chunkRange)
        chunkCreated = true
    } 

    numSamples := c.chunk.NumSamples()  
    if numSamples == 0 {
        c.minTime = t
        // chunkRange 默認是2hour,這裏算的下次開始的時間是個以2爲倍數的整數時間
        s.nextAt = rangeForTimestamp(c.minTime, chunkRange)
    }
    // 到1/4時,重新計算預估nextAt,下一個chunk的時間
    if numSamples == samplesPerChunk/4 {
        s.nextAt = computeChunkEndTime(c.minTime, c.maxTime, s.nextAt)
    } 
    // 到達時間,或數據刷的太快,以至於chunk裏面數據量已經超過 240個samples,創建新的headChunk
    if t >= s.nextAt || numSamples >= samplesPerChunk*2 {
        c = s.cutNewHeadChunk(t, e, chunkDiskMapper, chunkRange)
        chunkCreated = true
    }

    return c, true, chunkCreated
}

上面這個時間計算很有意思,寫入 head chunk 的時候會會校驗是否寫入數量已達 1/4,如果是的話調用 computeChunkEndTime 函數根據已寫入的 1/4 的數據計算平均寫入速率,計算出 120 個 sample 會在大約什麼時候寫完,然後返回時間作爲 chunk 的切割時間

這裏很沒有通過直接判斷寫入數量是否達到 samplesPerChunk 來切割,而是通過時間,這就有很大的彈性空間。如果數據突然寫慢了,chunk 數量不足 120 個 sample 也會進行切割,如果寫快了 chunk 裏面的 sample 數量會超過 120 個,但不能超過 2 倍的 samplesPerChunk 。

當然,在寫 head chunk 的時候還有大小限制, 大小也是 128M 爲一個文件寫入:

const MaxHeadChunkFileSize = 128 * 1024 * 1024 // 128 MiB.
func (f *chunkPos) shouldCutNewFile(bytesToWrite uint64) bool { 
    return f.offset == 0 || // First head chunk file.
        f.offset+bytesToWrite > MaxHeadChunkFileSize // Exceeds the max head chunk file size.
}

如果確定要切割 chunk 的話會調用 cutNewHeadChunk 方法將老的數據通過 mmap 的方式寫入到磁盤,然後給 memSeries 創建新的 head chunk,只保留對舊數據的引用。

現在假設數據持續寫入,過了一段時間之後會將 mmap 映射的 chunk 進行壓縮並作爲一個 block 進行持久化。

tsdb 在初始化的時候會後臺運行一個 goroutine,每分鐘檢查一下 chuan 的 chunkRange 跨度是否大於 chunkRange*3/2

//tsdb/db.go
func (db *DB) run() { 
    for { 
        select {
        case <-time.After(1 * time.Minute):  
            select {
            case db.compactc <- struct{}{}:
            default:
            }
        case <-db.compactc:
            // 校驗是否進行壓縮
            err := db.Compact()
            ...
        case <-db.stopc:
            return
        }
    }
}

Compact 方法裏面會根據調用 compactable 方法進行校驗:

func (h *Head) compactable() bool {
    return h.MaxTime()-h.MinTime() > h.chunkRange.Load()/2*3
}

chunkRange 默認值是 DefaultBlockDuration 爲 2 小時:

DefaultBlockDuration = int64(2 * time.Hour / time.Millisecond)

也就是校驗當前寫入的數據的時間跨度是否超過 3 小時,超過的話就會進行數據壓縮。我們假設設置的是每 15s 抓取一次,一個 chunk 寫滿是 120 個 sample,也就是 30 分鐘,所以每寫滿 3 小時 6 個 chunk 就會進行一次壓縮並生成 block。

然後壓縮的時候挑選最近 2 小時的指標數據進行壓縮,具體代碼也比較討巧,它是通過獲取 head 裏面最小的數據時間然後向上 2 小時取整獲取一個時間返回:

//tsdb/db.go
// 最小數據時間
mint := db.head.MinTime()
// 最大數據時間
maxt := rangeForTimestamp(mint, db.head.chunkRange.Load())

func rangeForTimestamp(t, width int64) (maxt int64) {
    // width 爲2小時
    return (t/width)*width + width
}

所以對於我們上面這個例子寫滿一個 chunk 需要 30 分鐘,所以壓縮兩小時數據,恰好是 4 個 chunk 了。

根據這篇 New in Prometheus v2.19.0: Memory-mapping of full chunks of the head block reduces memory usage by as much as 40% 文章的 benchmark 結果來看,將 head block 中一部分數據剔除出去只保留 120 samples 大約節省了 40% 左右的內存佔用,並且這部分的內存在重啓之後不再需要從 WAL log 中進行重置從而也帶來了更快的啓動速度。

wal

wal 是一個日誌序列用來記錄數據庫發生的一些操作,每次在寫入、修改、刪除之前就會先記錄一條在 wal 日誌裏。主要作用就是在程序掛了之後還能用這份日誌數據來做恢復用,因爲我們前面也說了,head 裏面的 chunk 並沒有持久化。

wal 按序列號依次遞增存儲在 wal 文件夾裏面,每個文件被稱爲 segment 默認 128MB 大小。Prometheus 稱這樣的文件爲 Segment,其中存放的就是對內存中 series 以及 sample 數據的備份。

另外還包含一個以 checkpoint 爲前綴的子目錄,由於內存中的時序數據經常會做持久化處理,wal 中的數據也將因此出現冗餘。所以每次在對內存數據進行持久化之後也需要對 Segment 做清理。但是被刪除的 Segment 中部分的數據依然可能是有用的,所以在清理時我們會將肯定無效的數據刪除,剩下的數據就存放在 checkpoint 中。而在 Prometheus 重啓時,應該首先加載 checkpoint 中的內容,再按序加載各個 Segment 的內容。

在磁盤上文件的目錄結構看起來如下所示:

data
└── wal
    ├── checkpoint.000003
    |   ├── 000000
    |   └── 000001
    ├── 000004
    └── 000005

在清理的時候會選取 2/3 的 segment 來刪除:

//tsdb/head.go
// 獲取磁盤的 wal Segments 時間跨度範圍
first, last, err := wlog.Segments(h.wal.Dir())
// 重新調整被刪除的結束時間,只刪除2/3 的數據
last = first + (last-first)*2/3

所以比方說現在有 5 個 segment:

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

那麼 000000 000001 000002 000003 這幾個文件將要被刪除。但是這些記錄不能直接刪了,比如 series 記錄只會寫一次,那麼需要把它找出來,還有一些 samples 目前不需要被刪除,也需要找出來,然後創建 checkpoint 文件並寫入。

所以在刪除的時候需要將 000000 000001 000002 000003 這幾個文件遍歷一遍,將 head 裏面不再使用的 series 刪除;因爲在刪除 segment 的時候會傳入一個時間 T,表示在這個時間點之前的數據都已經持久化成 block 了,所以 wal 不需要保存,所以需要將 samples 記錄在時間 T 之前的數據刪除;

// tsdb/wlog/checkpoint.go
func Checkpoint(logger log.Logger, w *WL, from, to int, keep func(id chunks.HeadSeriesRef) bool, mint int64) (*CheckpointStats, error) {
    ...
    var (
        buf              []byte
        recs             [][]byte
    )
    // segment reader
    r := NewReader(sgmReader)
    ...
    for r.Next() {
        // buf 起始位置
        start := len(buf)
        //讀出數據
        rec := r.Record()

        switch dec.Type(rec) {
        case record.Series:
            series, err = dec.Series(rec, series) 
            // Drop irrelevant series in place.
            repl := series[:0]
            for _, s := range series {
                // 校驗該 series 是否還存在在 head 中
                if keep(s.Ref) {
                    //保留
                    repl = append(repl, s)
                }
            } 
            if len(repl) > 0 { // 將要保留的數據寫入到 buffer 中
                buf = enc.Series(repl, buf)
            }
            stats.TotalSeries += len(series)
            stats.DroppedSeries += len(series) - len(repl)
            ...
        case record.Samples:
            samples, err = dec.Samples(rec, samples) 
            repl := samples[:0]
            for _, s := range samples {
                // 校驗該sample的時間是否在mint之後
                if s.T >= mint {
                    //之後的數據需要保留
                    repl = append(repl, s)
                }
            }
            if len(repl) > 0 {// 將要保留的數據寫入到 buffer 中
                buf = enc.Samples(repl, buf)
            }
            stats.TotalSamples += len(samples)
            stats.DroppedSamples += len(samples) - len(repl)
            ...
         // 將buf數據寫入到recs中
        recs = append(recs, buf[start:])
         // 如果buf 中的數據已經超過 1M,需要將數據寫入到 checkpoint中
        if len(buf) > 1*1024*1024 {
            if err := cp.Log(recs...); err != nil {
                return nil, errors.Wrap(err, "flush records")
            }
            buf, recs = buf[:0], recs[:0]
        }
    }
    // 遍歷完之後將殘餘數據checkpoint中
    if err := cp.Log(recs...); err != nil {
        return nil, errors.Wrap(err, "flush records")
    }
}

block

每個 block 實際上就是一個小型數據庫,內部存儲着該時間窗口內的所有時序數據,因此它需要擁有自己的 index 和 chunks。除了最新的、正在接收新鮮數據的 block 之外,其它 blocks 都是不可變的。

一個時間段內(默認 2 小時)的所有數據,只讀,用 ULID 命名。每一個 block 內主要包括:

./data 
├── 01BKGTZQ1SYQJTR4PB43C8PD98
│   ├── chunks
│   │   └── 000001
│   ├── tombstones
│   ├── index
│   └── meta.json

index 數據查找

我們現在看看 index 整體的數據結構:

┌────────────────────────────┬─────────────────────┐
│ magic(0xBAAAD700) <4b>     │ version(1) <1 byte> │
├────────────────────────────┴─────────────────────┤
│ ┌──────────────────────────────────────────────┐ │
│ │                 Symbol Table                 │ │
│ ├──────────────────────────────────────────────┤ │
│ │                    Series                    │ │
│ ├──────────────────────────────────────────────┤ │
│ │                 Label Index 1                │ │
│ ├──────────────────────────────────────────────┤ │
│ │                      ...                     │ │
│ ├──────────────────────────────────────────────┤ │
│ │                 Label Index N                │ │
│ ├──────────────────────────────────────────────┤ │
│ │                   Postings 1                 │ │
│ ├──────────────────────────────────────────────┤ │
│ │                      ...                     │ │
│ ├──────────────────────────────────────────────┤ │
│ │                   Postings N                 │ │
│ ├──────────────────────────────────────────────┤ │
│ │               Label Index Table              │ │
│ ├──────────────────────────────────────────────┤ │
│ │                 Postings Table               │ │
│ ├──────────────────────────────────────────────┤ │
│ │                      TOC                     │ │
│ └──────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────┘

我們這裏介紹一些和數據查詢有關的結構,其餘的詳細信息可以去這裏看: index。

首先是 TOC(Table Of Content),它存儲的內容是其餘六部分的位置信息,即它們的起始位置在 index 文件中的偏移量。結構如下:

┌─────────────────────────────────────────┐
│ ref(symbols) <8b>                       │
├─────────────────────────────────────────┤
│ ref(series) <8b>                        │
├─────────────────────────────────────────┤
│ ref(label indices start) <8b>           │
├─────────────────────────────────────────┤
│ ref(label offset table) <8b>            │
├─────────────────────────────────────────┤
│ ref(postings start) <8b>                │
├─────────────────────────────────────────┤
│ ref(postings offset table) <8b>         │
├─────────────────────────────────────────┤
│ CRC32 <4b>                              │
└─────────────────────────────────────────┘

如果我們要找某個指標在 chunk 中的位置,那麼首先可以通過上面這些偏移去查找倒排索引。因爲 prometheus 存放的 key/value 數據很多,所以爲了實現快速查找在 index 裏面構建了倒排索引,並將數據存放在 Posting offset table 以及 Posting 中。

倒排索引方式組織如下:

通過這種方式可以快速的找到對應的 seriesId 。

假如我們要找上面 lable 對應的 series ,那麼就需要先到 Posting offset table 找到對應的 offset,這個 offset 是 Posting 的偏移,去到裏面找到對應的條目,之後取與,得出對應的 seriesId 。那麼在我們上面的例子中最後得到的 seriesId 列表就是 {Series2,Series3}

找到對應的 seriesId 之後就去 index 的 series 段中去找對應的 chunk 信息。series 段中首先存儲 series 的各個 label 的 key/value 信息。緊接着存儲 series 相關的 chunks 信息,包含每個 chunk 的時間窗口,以及該 chunk 在 chunks 子目錄下具體的位置信息,然後再按照 chunk 文件的訪問,即可找到最終的原始數據。

┌──────────────────────────────────────────────────────────────────────────┐
│ len <uvarint>                                                            │
├──────────────────────────────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────────────────────────────┐ │
│ │                     labels count <uvarint64>  //lable數量             │ │
│ ├──────────────────────────────────────────────────────────────────────┤ │
│ │ lable列表     ┌────────────────────────────────────────────┐          │ │
│ │              │ ref(l_i.name) <uvarint32>   //label名      │          │ │
│ │              ├────────────────────────────────────────────┤          │ │
│ │              │ ref(l_i.value) <uvarint32>  //label值      │          │ │
│ │              └────────────────────────────────────────────┘          │ │
│ │                             ...                                      │ │
│ ├──────────────────────────────────────────────────────────────────────┤ │
│ │                     chunks count <uvarint64>  //chunk數量             │ │
│ ├──────────────────────────────────────────────────────────────────────┤ │
│ │ chunk列表     ┌────────────────────────────────────────────┐          │ │
│ │              │ c_0.mint <varint64>                        │          │ │
│ │              ├────────────────────────────────────────────┤          │ │
│ │              │ c_0.maxt - c_0.mint <uvarint64>            │          │ │
│ │              ├────────────────────────────────────────────┤          │ │
│ │              │ ref(c_0.data) <uvarint64>                  │          │ │
│ │              └────────────────────────────────────────────┘          │ │
│ │              ┌────────────────────────────────────────────┐          │ │
│ │              │ c_i.mint - c_i-1.maxt <uvarint64>          │          │ │
│ │              ├────────────────────────────────────────────┤          │ │
│ │              │ c_i.maxt - c_i.mint <uvarint64>            │          │ │
│ │              ├────────────────────────────────────────────┤          │ │
│ │              │ ref(c_i.data) - ref(c_i-1.data) <varint64> │          │ │
│ │              └────────────────────────────────────────────┘          │ │
│ │                             ...                                      │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────────────────┤
│ CRC32 <4b>                                                               │
└──────────────────────────────────────────────────────────────────────────┘

總結

其實通篇看下來我們可以很清晰的瞭解到 Prometheus 數據寫入思路其實是很清晰的。因爲 tsdb 數據有其特點,它是基於相對穩定頻率持續產生的一系列指標監測數據,那麼存儲就是一些標籤鍵加上一個時間序列作爲一個大 key,值就是一個數字,這些數據就構成了一個時間序列數據,因此鍵值數據庫天然地可以作爲時序數據的載體。

然後對於數據的存儲 Prometheus 按冷熱數據進行分離,最近的數據肯定是看的最多的,所以緩存在內存裏面,爲了防止宕機而導致數據丟失因而引入 wal 來做故障恢復。數據超過一定量之後會從內存裏面剝離出來以 chunk 的形式存放在磁盤上這就是 head chunk。對於更早的數據會進行壓縮持久化變成 block 存放到磁盤中。

對於 block 中的數據由於是不會變的,數據較爲固定,所以每個 block 通過 index 來索引其中的數據,並且爲了加快數據的查詢引用倒排索引,便於快速定位到對應的 chunk。

Reference

https://tech.ipalfish.com/blog/2020/03/31/the-evolution-of-prometheus-storage-layer/

https://github.com/prometheus/prometheus/blob/main/tsdb/docs/format/index.md

https://liujiacai.net/blog/2021/04/11/prometheus-storage-engine/

Be smarter in how we look at matchers https://github.com/prometheus-junkyard/tsdb/pull/572

Prometheus TSDB (Part 1): The Head Block https://ganeshvernekar.com/blog/prometheus-tsdb-the-head-block/

https://prometheus.kpingfan.com/

Index Disk Format https://github.com/prometheus/prometheus/blob/main/tsdb/docs/format/index.md

https://yunlzheng.gitbook.io/prometheus-book/

TSDB format https://github.com/prometheus/prometheus/blob/release-2.40/tsdb/docs/format/README.md

https://prometheus.io/docs/prometheus/latest/storage/

https://grafana.com/blog/2022/09/07/new-in-grafana-mimir-introducing-out-of-order-sample-ingestion/

https://github.com/prometheus/prometheus/blob/release-2.40/tsdb/docs/refs.md

Persistent Block and its Index https://ganeshvernekar.com/blog/prometheus-tsdb-persistent-block-and-its-index

head block https://segmentfault.com/a/1190000041199554

New in Grafana Mimir: Introducing out-of-order sample ingestion https://grafana.com/blog/2022/09/07/new-in-grafana-mimir-introducing-out-of-order-sample-ingestion/?mdm=social

https://segmentfault.com/a/1190000041117609

https://github.com/YaoZengzeng/KubernetesResearch/blob/master/Prometheus%E5%AD%98%E5%82%A8%E6%A8%A1%E5%9E%8B%E5%88%86%E6%9E%90.md

https://heapdump.cn/article/2282672

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/wk9OZu4zAbLm1F6YSO87LQ