天天看點

Prometheus 作為大廠監控标配,一定要做到知根知底!(存儲篇)

作者:每日程式設計

引言

目前各大廠監控方案首選(或參考)均是 Prometheus+Grafana ,作為時序資料庫的基準參考,一定要對Prometheus 知根知底。

雖然大廠把時序做到分布式,也是基于Prometheus的存儲引擎做的外層路由封裝。其中京東的baudtime就是基于Prometheus的存儲引擎作為分布式shard,上層添加的meta route(兩層時間路由)和proxy層做的封裝,一切都是圍繞Prometheus生态的附加而已。掌握了Prometheus也就熟悉了各司的監控系統,架構上大同小異。本篇将詳細介紹Prometheus的存儲原理。

Prometheus 作為大廠監控标配,一定要做到知根知底!(存儲篇)

存儲原理

接元件篇~

最近的資料儲存在記憶體中

Prometheus将最近的資料儲存在記憶體中,這樣查詢最近的資料會變得非常快,然後通過一個compactor定時将資料打包到磁盤。資料在記憶體中最少保留2個小時(storage.tsdb.min-block-duration)。

由于在Prometheus中會頻繁的對map[hash/refId]memSeries進行操作,例如檢查這個labelSet對應的memSeries是否存在,不存在則建立等。由于golang的map非線程安全,是以其采用了分段鎖去拆分鎖。

memChunk在記憶體中儲存的正是采用XOR算法壓縮過的資料。

引入以Label為key的反向索引,反向索引存儲的refId必須是有序的。

底層存儲

Prometheus 作為大廠監控标配,一定要做到知根知底!(存儲篇)

a.prometheus按照block塊的方式來存儲資料,每2小時為一個時間機關,首先會存儲到記憶體中,當到達2小時後,Prometheus會進行一次資料壓縮,将記憶體中的資料落盤。

最近的Block一般是存儲了2小時的資料,而較為久遠的Block則會通過compactor進行合并,一個Block可能存儲了若幹小時的資訊。值得注意的是,合并操作隻是減少了索引的大小(尤其是符号表的合并),而本身資料(chunks)的大小并沒有任何改變。

所有的Chunk檔案在磁盤上都不會大于512M,當寫入磁盤單個檔案超過512M的時候,就會自動切分一個新的檔案。

block:随着資料量的增長,tsdb會将小的block合并成大的block,這樣不僅可以減少資料存儲,還可以減少記憶體中的block個數,便于對資料進行檢索。

每個block都有一個全局唯一的名稱。block的命名規則為:名稱總長度16位元組。前6個位元組為時間戳,後10個位元組為随機數;通過block的檔案名确定這個block的建立時間,友善按照時間對block進行排序。

block組成:

chunk:用于儲存壓縮後的時序資料

index:用于對監控資料進行快讀的檢索,記錄chunk的偏移位置,由5張表構成

tombstone:tsdb在删除block資料塊時會将整個目錄删除,但如果隻删除一部分資料塊的内容,就可以通過tombstone進行軟删除

meta.json:記錄block的中繼資料資訊,主要包括一個資料塊記錄樣本的起始時間,截止時間,樣本數等資訊。

入口是插入倒排時序資料:

如果lset已經在series中了,則直接傳回;

否則擷取一個seriesId:

将label key/value插入到h.values;

将label key/value和seriesId插入到h.postings中(大map);

b.為防止程式異常而導緻資料丢失,采用了WAL機制,即2小時内記錄的資料存儲在記憶體中的同時,還會記錄一份日志,存儲在block下的wal目錄中。當程式再次啟動時,會将wal目錄中的資料寫入對應的block中,進而達到恢複資料的效果.

checkpoint就是用來清理wal日志的。

當2hour的記憶體資料被壓縮成block存儲至硬碟時:

該時間之前的wal日志就可以删除了;因為已經持久化到硬碟了,即使prometheus執行個體宕掉,也不會丢資料;

此時,prometheus生成一個checkpoint,進行wal日志的清理;

而關于持久化存儲的問題,prometheus實際上并沒有試圖解決。它的做法是定義出标準的讀寫接口,進而可以将資料存儲到任意一個第三方存儲上。

prometheus-data

./data

├── 01BKGV7JBM69T2G1BGBGM6KB12 # block ID

│ └── meta.json #包含了整個Block的所有中繼資料

├── 01BKGTZQ1SYQJTR4PB43C8PD98 # block ID

│ ├── chunks # Block中的chunk檔案

│ │ └── 000001

│ ├── tombstones # 資料删除記錄檔案

│ ├── index # 索引

│ └── meta.json # bolck元資訊

├── chunks_head # head記憶體映射

│ └── 000001

└── wal # 預寫日志

├── 000000002

└── checkpoint.00000001

└── 00000000

查找

queryEngine元件用于rules查詢和計算,通過方法promql.NewEngine完成初始化

一個表達式或子表達式可以總結為以下四種類型:

瞬時向量(Instant vector ):一組時間序列,包含每個時間序列的單一樣本,所有共享相同的時間戳;

範圍向量(Range vector):一組時間序列,包含每個時間序列在一段時間内的資料點範圍;

标量(Scalar):一個簡單的數字浮點值;

字元串(String):一個簡單的字元串值(目前未使用);

先從選擇Block開始,周遊所有Block的meta.json,找到具體的Block,通過Labels找資料是通過反向索引。我們的反向索引是儲存在index檔案裡面的。

反向索引:

prometheus tsdb中的index以反向索引的方式組織:

給每個series配置設定1個id:

用seriesId查詢series,這是前向索引,查詢時間複雜度=O(1);

構造label的索引:

若seriesId={2,5,10,29}都含有label: app='nginx';

那麼,對于app='nginx", {2,5,10,29}就是它的反向索引;

給每個序列配置設定一個唯一ID,查詢ID的複雜度是O(1),然後給每個标簽建一個倒排ID表。比如包含app ="nginx"标簽的ID為1,11,111那麼标簽"nginx"的倒排序索引為[1,11,111];

首先我們通路的是Posting offset table。由于反向索引按照不同的LabelPair(key/value)會有非常多的條目。是以Posing offset table就是決定到底通路哪一條Posting索引。offset就是指的這一Posting條目在檔案中的偏移。

Posting中的Ref(Series2)和Ref(Series3)即為這兩Series在index檔案中的偏移。

Series以Delta的形式記錄了chunkId以及該chunk包含的時間範圍。這樣就可以很容易過濾出我們需要的chunk,然後再按照chunk檔案的通路,即可找到最終的原始資料。

blockQuerier根據不同的block,構造不同的indexReader來讀取Label索引;blockQuerier使用Postings()得到[]seriesId後,再使用chunkReader最終讀取到時序資料(t/v)。

通過LableNames()查詢所有的lableName;

通過LabelValues(name)查詢labelName對應的labelValues;

通過postings查詢到key、value對應的[]seriesId,最終使用seriesId+chunkReader查詢最終的時序資料(t/v)

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

查詢流程簡述:

查詢入口:加載記憶體block和磁盤block,構造出blockQuerier,根據不同的block構造出不同的indexReader,BlockQuerier使用indexReader查詢postings資訊;

首先根據時間範圍加載block,定位到所有的block後查找index的TOC表,在根據标簽查找标簽表,最後根據seriesId定位到chunk中的時序。

遠端存儲介紹

目前Prometheus支援OpenTsdb、InfluxDB、Elasticsearch等後端存儲,通過擴充卡實作Prometheus存儲的remote write和remote read接口,便可以接入Prometheus作為遠端存儲使用。

面試題: 為什麼需要對Block進行合并?

a.對tombstones介紹我們知道Prometheus在對資料的删除操作會記錄在單獨檔案stombstone中,而資料仍保留在磁盤上。是以,當stombstone序列超過某些百分比時,需要從磁盤中删除該資料。

b.如果樣本資料值波動非常小,相鄰兩個Block中的大部分資料是相同的。對這些Block做合并的話可以減少重複資料,進而節省磁盤空間。

c.當查詢命中大于1個Block時,必須合并每個塊的結果,這可能會産生一些額外的開銷。

d.如果有重疊的Block(在時間上重疊),查詢它們還要對Block之間的樣本進行重複資料删除,合并這些重疊塊避免了重複資料删除的需要。

面試題2:查找詳細檔案記錄(過程說明)

攝入的采樣每兩個小時被分成一個block。每個block由一個包含一個或多個chunk檔案的目錄組成,其中,這些chunk檔案包含該時間視窗的所有時間序列采樣,以及一個中繼資料檔案和索引檔案(對塊檔案中的時間序列名額名稱和标簽進行索引)

目前輸入采樣的block儲存在記憶體中,尚未完全持久化。

Prometheus将保留至少3個write-ahead-log檔案,然而高流量的伺服器可能會看到超過3個WAL檔案,因為它需要保留至少兩個小時的原始資料。

prometheus按照block塊的方式來存儲資料,每2小時為一個時間機關,首先會存儲到記憶體中,當到達2小時後,會自動寫入磁盤中。

為防止程式異常而導緻資料丢失,采用了WAL機制,即2小時内記錄的資料存儲在記憶體中的同時,還會記錄一份日志,存儲在block下的wal目錄中。當程式再次啟動時,會将wal目錄中的資料寫入對應的block中,進而達到恢複資料的效果。

當删除資料時,删除條目會記錄在tombstones 中,而不是立刻删除。

prometheus采用的存儲方式稱為“時間分片”,每個block都是一個獨立的資料庫。優勢是可以提高查詢效率,查哪個時間段的資料,隻需要打開對應的block即可,無需打開多餘資料。

TSDB 的設計有兩個核心:block 和 WAL,而 block 又包含 chunk、index、meta.json、tombstones。

預設最小的 block 儲存 2h 監控資料。如果步數為 3、步長為 3,則 block 的大小依次為:2h、6h、18h。随着資料量的不斷增長,TSDB 會将小的 block 合并成大的 block,例如将 3 個 2h 的 block 合并成一個 6h 的 block,這樣不僅可以減少資料存儲,還可以減少記憶體中的 block 個數,便于對資料進行檢索。

每個 block 都有全局唯一的名稱,通過 ULID原理生成,可以通過 block 的檔案名确定這個 block 的建立時間,進而很友善地按照時間對 block 排序。

chunks 用于儲存壓縮後的時序資料。每個 chunk 的大小為 512MB,如果超過,則會别截斷成多個 chunk 儲存,且以數字編号命名。

 index 是為了對監控資料進行快速檢索和查詢而設計的,主要用來記錄 chunk 中時序的偏移位置。

TOC 表。TOC 表是 index 的入口,記錄 index 檔案中其他表的位置。在寫入其他表的資料之前都會先将目前的偏移量(8 位元組)作為該表的位址記錄,在讀取 index 時首先讀取的是 TOC 表。

符号表(Symbol Table)。TSDB 對磁盤的利用發揮到了極緻,為了避免标簽重複存儲,對每個标簽隻存儲一次,在使用該标簽時直接使用符号表中的索引。

時序清單(Series)。記錄該 block 中每個時序的标簽及這些時序在該 block 中關聯的 chunk 塊。

标簽索引表(Label Index Table)。将具有相同标簽名稱(key)的标簽組合到一起,進而形成标簽索引(Label Index),然後通過标簽索引表去查找這些索引。

Postings 表。每個 Posting 都代表一個标簽和時序的關聯關系,Postings 表則是 Posting 的索引表。

舉個例子:

假如要查找某個時間段内某種名額的監控資料,TSDB 就會首先根據該時間段找到所有的 block,并加載每個 block 的 index 檔案,之後要先讀取 index 的 TOC 表才能找到其他表。對 TOC 表的讀取很簡單,直接讀取 index 檔案的最後 52 位元組(6 張表 * 每張表 8 位元組偏移量 + 4 位元組的 CRC 校驗和)即可。之後找到符号表,就可以确定這個名額标簽的名稱和值在符号表中的索引 ID,後續的查找都是基于這個 ID 的查找。

繼續閱讀