溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

怎樣寫(xiě)一個(gè)時(shí)間序列數(shù)據(jù)庫(kù)

發(fā)布時(shí)間:2021-12-02 11:10:47 來(lái)源:億速云 閱讀:121 作者:柒染 欄目:互聯(lián)網(wǎng)科技

怎樣寫(xiě)一個(gè)時(shí)間序列數(shù)據(jù)庫(kù),很多新手對(duì)此不是很清楚,為了幫助大家解決這個(gè)難題,下面小編將為大家詳細(xì)講解,有這方面需求的人可以來(lái)學(xué)習(xí)下,希望你能有所收獲。

在 Prometheus 上,監(jiān)控系統(tǒng)包含一個(gè)自定義的時(shí)間序列數(shù)據(jù)庫(kù),并且集成在 Kubernetes 上。

在許多方面上 Kubernetes 展現(xiàn)出了 Prometheus 所有的設(shè)計(jì)用途。它使得持續(xù)部署continuous deployments,彈性伸縮auto scaling和其他高動(dòng)態(tài)環(huán)境highly dynamic environments下的功能可以輕易地訪問(wèn)。查詢語(yǔ)句和操作模型以及其它概念決策使得 Prometheus 特別適合這種環(huán)境。但是,如果監(jiān)控的工作負(fù)載動(dòng)態(tài)程度顯著地增加,這就會(huì)給監(jiān)控系統(tǒng)本身帶來(lái)新的壓力??紤]到這一點(diǎn),我們就可以特別致力于在高動(dòng)態(tài)或瞬態(tài)服務(wù)transient services環(huán)境下提升它的表現(xiàn),而不是回過(guò)頭來(lái)解決 Prometheus 已經(jīng)解決的很好的問(wèn)題。

Prometheus  的存儲(chǔ)層在歷史以來(lái)都展現(xiàn)出卓越的性能,單一服務(wù)器就能夠以每秒數(shù)百萬(wàn)個(gè)時(shí)間序列的速度攝入多達(dá)一百萬(wàn)個(gè)樣本,同時(shí)只占用了很少的磁盤(pán)空間。盡管當(dāng)前的存儲(chǔ)做的很好,但我依舊提出一個(gè)新設(shè)計(jì)的存儲(chǔ)子系統(tǒng),它可以修正現(xiàn)存解決方案的缺點(diǎn),并具備處理更大規(guī)模數(shù)據(jù)的能力。

備注:我沒(méi)有數(shù)據(jù)庫(kù)方面的背景。我說(shuō)的東西可能是錯(cuò)的并讓你誤入歧途。你可以在 Freenode 的 #prometheus 頻道上對(duì)我(fabxc)提出你的批評(píng)。

問(wèn)題,難題,問(wèn)題域

首先,快速地概覽一下我們要完成的東西和它的關(guān)鍵難題。我們可以先看一下 Prometheus 當(dāng)前的做法 ,它為什么做的這么好,以及我們打算用新設(shè)計(jì)解決哪些問(wèn)題。

時(shí)間序列數(shù)據(jù)

我們有一個(gè)收集一段時(shí)間數(shù)據(jù)的系統(tǒng)。

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

每個(gè)數(shù)據(jù)點(diǎn)是一個(gè)時(shí)間戳和值的元組。在監(jiān)控中,時(shí)間戳是一個(gè)整數(shù),值可以是任意數(shù)字。64 位浮點(diǎn)數(shù)對(duì)于計(jì)數(shù)器和測(cè)量值來(lái)說(shuō)是一個(gè)好的表示方法,因此我們將會(huì)使用它。一系列嚴(yán)格單調(diào)遞增的時(shí)間戳數(shù)據(jù)點(diǎn)是一個(gè)序列,它由標(biāo)識(shí)符所引用。我們的標(biāo)識(shí)符是一個(gè)帶有標(biāo)簽維度label dimensions字典的度量名稱。標(biāo)簽維度劃分了單一指標(biāo)的測(cè)量空間。每一個(gè)指標(biāo)名稱加上一個(gè)唯一標(biāo)簽集就成了它自己的時(shí)間序列,它有一個(gè)與之關(guān)聯(lián)的數(shù)據(jù)流value stream。

這是一個(gè)典型的序列標(biāo)識(shí)符series identifier集,它是統(tǒng)計(jì)請(qǐng)求指標(biāo)的一部分:

requests_total{path="/status", method="GET", instance=”10.0.0.1:80”}requests_total{path="/status", method="POST", instance=”10.0.0.3:80”}requests_total{path="/", method="GET", instance=”10.0.0.2:80”}

讓我們簡(jiǎn)化一下表示方法:度量名稱可以當(dāng)作另一個(gè)維度標(biāo)簽,在我們的例子中是 __name__。對(duì)于查詢語(yǔ)句,可以對(duì)它進(jìn)行特殊處理,但與我們存儲(chǔ)的方式無(wú)關(guān),我們后面也會(huì)見(jiàn)到。

{__name__="requests_total", path="/status", method="GET", instance=”10.0.0.1:80”}{__name__="requests_total", path="/status", method="POST", instance=”10.0.0.3:80”}{__name__="requests_total", path="/", method="GET", instance=”10.0.0.2:80”}

我們想通過(guò)標(biāo)簽來(lái)查詢時(shí)間序列數(shù)據(jù)。在最簡(jiǎn)單的情況下,使用 {__name__="requests_total"} 選擇所有屬于 requests_total 指標(biāo)的數(shù)據(jù)。對(duì)于所有選擇的序列,我們?cè)诮o定的時(shí)間窗口內(nèi)獲取數(shù)據(jù)點(diǎn)。

在更復(fù)雜的語(yǔ)句中,我們或許想一次性選擇滿足多個(gè)標(biāo)簽的序列,并且表示比相等條件更復(fù)雜的情況。例如,非語(yǔ)句(method!="GET")或正則表達(dá)式匹配(method=~"PUT|POST")。

這些在很大程度上定義了存儲(chǔ)的數(shù)據(jù)和它的獲取方式。

縱與橫

在簡(jiǎn)化的視圖中,所有的數(shù)據(jù)點(diǎn)可以分布在二維平面上。水平維度代表著時(shí)間,序列標(biāo)識(shí)符域經(jīng)縱軸展開(kāi)。

series  ^     |   . . . . . . . . . . . . . . . . .   . . . . .   {__name__="request_total", method="GET"}  |     . . . . . . . . . . . . . . . . . . . . . .   {__name__="request_total", method="POST"}  |         . . . . . . .  |       . . .     . . . . . . . . . . . . . . . .                  ...   |     . . . . . . . . . . . . . . . . .   . . . .     |     . . . . . . . . . .   . . . . . . . . . . .   {__name__="errors_total", method="POST"}  |           . . .   . . . . . . . . .   . . . . .   {__name__="errors_total", method="GET"}  |         . . . . . . . . .       . . . . .  |       . . .     . . . . . . . . . . . . . . . .                  ...   |     . . . . . . . . . . . . . . . .   . . . .   v    <-------------------- time --------------------->

Prometheus 通過(guò)定期地抓取一組時(shí)間序列的當(dāng)前值來(lái)獲取數(shù)據(jù)點(diǎn)。我們從中獲取到的實(shí)體稱為目標(biāo)。因此,寫(xiě)入模式完全地垂直且高度并發(fā),因?yàn)閬?lái)自每個(gè)目標(biāo)的樣本是獨(dú)立攝入的。

這里提供一些測(cè)量的規(guī)模:?jiǎn)我?Prometheus 實(shí)例從數(shù)萬(wàn)個(gè)目標(biāo)中收集數(shù)據(jù)點(diǎn),每個(gè)數(shù)據(jù)點(diǎn)都暴露在數(shù)百到數(shù)千個(gè)不同的時(shí)間序列中。

在每秒采集數(shù)百萬(wàn)數(shù)據(jù)點(diǎn)這種規(guī)模下,批量寫(xiě)入是一個(gè)不能妥協(xié)的性能要求。在磁盤(pán)上分散地寫(xiě)入單個(gè)數(shù)據(jù)點(diǎn)會(huì)相當(dāng)?shù)鼐徛?。因此,我們想要按順序?qū)懭敫蟮臄?shù)據(jù)塊。

對(duì)于旋轉(zhuǎn)式磁盤(pán),它的磁頭始終得在物理上向不同的扇區(qū)上移動(dòng),這是一個(gè)不足為奇的事實(shí)。而雖然我們都知道 SSD  具有快速隨機(jī)寫(xiě)入的特點(diǎn),但事實(shí)上它不能修改單個(gè)字節(jié),只能寫(xiě)入一頁(yè)或更多頁(yè)的 4KiB 數(shù)據(jù)量。這就意味著寫(xiě)入 16 字節(jié)的樣本相當(dāng)于寫(xiě)入滿滿一個(gè)  4Kib 的頁(yè)。這一行為就是所謂的寫(xiě)入放大,這種特性會(huì)損耗你的 SSD。因此它不僅影響速度,而且還毫不夸張地在幾天或幾個(gè)周內(nèi)破壞掉你的硬件。

關(guān)于此問(wèn)題更深層次的資料,“Coding for SSDs”系列博客是極好的資源。讓我們想想主要的用處:順序?qū)懭牒团繉?xiě)入分別對(duì)于旋轉(zhuǎn)式磁盤(pán)和 SSD 來(lái)說(shuō)都是理想的寫(xiě)入模式。大道至簡(jiǎn)。

查詢模式比起寫(xiě)入模式明顯更不同。我們可以查詢單一序列的一個(gè)數(shù)據(jù)點(diǎn),也可以對(duì) 10000  個(gè)序列查詢一個(gè)數(shù)據(jù)點(diǎn),還可以查詢一個(gè)序列幾個(gè)周的數(shù)據(jù)點(diǎn),甚至是 10000  個(gè)序列幾個(gè)周的數(shù)據(jù)點(diǎn)。因此在我們的二維平面上,查詢范圍不是完全水平或垂直的,而是二者形成矩形似的組合。

記錄規(guī)則可以減輕已知查詢的問(wèn)題,但對(duì)于點(diǎn)對(duì)點(diǎn)ad-hoc查詢來(lái)說(shuō)并不是一個(gè)通用的解決方法。

我們知道我們想要批量地寫(xiě)入,但我們得到的僅僅是一系列垂直數(shù)據(jù)點(diǎn)的集合。當(dāng)查詢一段時(shí)間窗口內(nèi)的數(shù)據(jù)點(diǎn)時(shí),我們不僅很難弄清楚在哪才能找到這些單獨(dú)的點(diǎn),而且不得不從磁盤(pán)上大量隨機(jī)的地方讀取。也許一條查詢語(yǔ)句會(huì)有數(shù)百萬(wàn)的樣本,即使在最快的  SSD 上也會(huì)很慢。讀入也會(huì)從磁盤(pán)上獲取更多的數(shù)據(jù)而不僅僅是 16 字節(jié)的樣本。SSD 會(huì)加載一整頁(yè),HDD  至少會(huì)讀取整個(gè)扇區(qū)。不論哪一種,我們都在浪費(fèi)寶貴的讀取吞吐量。

因此在理想情況下,同一序列的樣本將按順序存儲(chǔ),這樣我們就能通過(guò)盡可能少的讀取來(lái)掃描它們。最重要的是,我們僅需要知道序列的起始位置就能訪問(wèn)所有的數(shù)據(jù)點(diǎn)。

顯然,將收集到的數(shù)據(jù)寫(xiě)入磁盤(pán)的理想模式與能夠顯著提高查詢效率的布局之間存在著明顯的抵觸。這是我們 TSDB 需要解決的一個(gè)基本問(wèn)題。

當(dāng)前的解決方法

是時(shí)候看一下當(dāng)前 Prometheus 是如何存儲(chǔ)數(shù)據(jù)來(lái)解決這一問(wèn)題的,讓我們稱它為“V2”。

我們創(chuàng)建一個(gè)時(shí)間序列的文件,它包含所有樣本并按順序存儲(chǔ)。因?yàn)槊繋酌敫郊右粋€(gè)樣本數(shù)據(jù)到所有文件中非常昂貴,我們?cè)趦?nèi)存中打包 1Kib  樣本序列的數(shù)據(jù)塊,一旦打包完成就附加這些數(shù)據(jù)塊到單獨(dú)的文件中。這一方法解決了大部分問(wèn)題。寫(xiě)入目前是批量的,樣本也是按順序存儲(chǔ)的。基于給定的同一序列的樣本相對(duì)之前的數(shù)據(jù)僅發(fā)生非常小的改變這一特性,它還支持非常高效的壓縮格式。Facebook  在他們 Gorilla TSDB 上的論文中描述了一個(gè)相似的基于數(shù)據(jù)塊的方法,并且引入了一種壓縮格式,它能夠減少 16 字節(jié)的樣本到平均 1.37 字節(jié)。V2 存儲(chǔ)使用了包含 Gorilla 變體等在內(nèi)的各種壓縮格式。

   +----------+---------+---------+---------+---------+           series A   +----------+---------+---------+---------+---------+          +----------+---------+---------+---------+---------+    series B          +----------+---------+---------+---------+---------+                               . . . +----------+---------+---------+---------+---------+---------+   series XYZ +----------+---------+---------+---------+---------+---------+    chunk 1    chunk 2   chunk 3     ...

盡管基于塊存儲(chǔ)的方法非常棒,但為每個(gè)序列保存一個(gè)獨(dú)立的文件會(huì)給 V2 存儲(chǔ)帶來(lái)麻煩,因?yàn)椋?/p>

  • 實(shí)際上,我們需要的文件比當(dāng)前收集數(shù)據(jù)的時(shí)間序列數(shù)量要多得多。多出的部分在

    序列分流Series Churn

    上。有幾百萬(wàn)個(gè)文件,遲早會(huì)使用光文件系統(tǒng)中的 inode。這種情況我們只能通過(guò)重新格式化來(lái)恢復(fù)磁盤(pán),這種方式是***有破壞性的。我們通常不想為了適應(yīng)一個(gè)應(yīng)用程序而格式化磁盤(pán)。

  • 即使是分塊寫(xiě)入,每秒也會(huì)產(chǎn)生數(shù)千塊的數(shù)據(jù)塊并且準(zhǔn)備持久化。這依然需要每秒數(shù)千次的磁盤(pán)寫(xiě)入。盡管通過(guò)為每個(gè)序列打包好多個(gè)塊來(lái)緩解,但這反過(guò)來(lái)還是增加了等待持久化數(shù)據(jù)的總內(nèi)存占用。

  • 要保持所有文件打開(kāi)來(lái)進(jìn)行讀寫(xiě)是不可行的。特別是因?yàn)?99% 的數(shù)據(jù)在 24  小時(shí)之后不再會(huì)被查詢到。如果查詢它,我們就得打開(kāi)數(shù)千個(gè)文件,找到并讀取相關(guān)的數(shù)據(jù)點(diǎn)到內(nèi)存中,然后再關(guān)掉。這樣做就會(huì)引起很高的查詢延遲,數(shù)據(jù)塊緩存加劇會(huì)導(dǎo)致新的問(wèn)題,這一點(diǎn)在“資源消耗”一節(jié)另作講述。

  • 最終,舊的數(shù)據(jù)需要被刪除,并且數(shù)據(jù)需要從數(shù)百萬(wàn)文件的頭部刪除。這就意味著刪除實(shí)際上是寫(xiě)密集型操作。此外,循環(huán)遍歷數(shù)百萬(wàn)文件并且進(jìn)行分析通常會(huì)導(dǎo)致這一過(guò)程花費(fèi)數(shù)小時(shí)。當(dāng)它完成時(shí),可能又得重新來(lái)過(guò)。喔天,繼續(xù)刪除舊文件又會(huì)進(jìn)一步導(dǎo)致 SSD 產(chǎn)生寫(xiě)入放大。

  • 目前所積累的數(shù)據(jù)塊僅維持在內(nèi)存中。如果應(yīng)用崩潰,數(shù)據(jù)就會(huì)丟失。為了避免這種情況,內(nèi)存狀態(tài)會(huì)定期的保存在磁盤(pán)上,這比我們能接受數(shù)據(jù)丟失窗口要長(zhǎng)的多?;謴?fù)檢查點(diǎn)也會(huì)花費(fèi)數(shù)分鐘,導(dǎo)致很長(zhǎng)的重啟周期。

我們能夠從現(xiàn)有的設(shè)計(jì)中學(xué)到的關(guān)鍵部分是數(shù)據(jù)塊的概念,我們當(dāng)然希望保留這個(gè)概念。***的數(shù)據(jù)塊會(huì)保持在內(nèi)存中一般也是好的主意。畢竟,***的數(shù)據(jù)會(huì)大量的查詢到。

一個(gè)時(shí)間序列對(duì)應(yīng)一個(gè)文件,這個(gè)概念是我們想要替換掉的。

序列分流

在 Prometheus 的上下文context中,我們使用術(shù)語(yǔ)序列分流series churn來(lái)描述一個(gè)時(shí)間序列集合變得不活躍,即不再接收數(shù)據(jù)點(diǎn),取而代之的是出現(xiàn)一組新的活躍序列。

例如,由給定微服務(wù)實(shí)例產(chǎn)生的所有序列都有一個(gè)相應(yīng)的“instance”標(biāo)簽來(lái)標(biāo)識(shí)其來(lái)源。如果我們?yōu)槲⒎?wù)執(zhí)行了滾動(dòng)更新rolling update,并且為每個(gè)實(shí)例替換一個(gè)新的版本,序列分流便會(huì)發(fā)生。在更加動(dòng)態(tài)的環(huán)境中,這些事情基本上每小時(shí)都會(huì)發(fā)生。像 Kubernetes 這樣的集群編排Cluster orchestration系統(tǒng)允許應(yīng)用連續(xù)性的自動(dòng)伸縮和頻繁的滾動(dòng)更新,這樣也許會(huì)創(chuàng)建成千上萬(wàn)個(gè)新的應(yīng)用程序?qū)嵗⑶野殡S著全新的時(shí)間序列集合,每天都是如此。

series  ^  |   . . . . . .  |   . . . . . .  |   . . . . . .  |               . . . . . . .  |               . . . . . . .  |               . . . . . . .  |                             . . . . . .  |                             . . . . . .  |                                         . . . . .  |                                         . . . . .  |                                         . . . . .  v    <-------------------- time --------------------->

所以即便整個(gè)基礎(chǔ)設(shè)施的規(guī)模基本保持不變,過(guò)一段時(shí)間后數(shù)據(jù)庫(kù)內(nèi)的時(shí)間序列還是會(huì)成線性增長(zhǎng)。盡管 Prometheus 很愿意采集 1000 萬(wàn)個(gè)時(shí)間序列數(shù)據(jù),但要想在 10 億個(gè)序列中找到數(shù)據(jù),查詢效果還是會(huì)受到嚴(yán)重的影響。

當(dāng)前解決方案

當(dāng)前 Prometheus 的 V2 存儲(chǔ)系統(tǒng)對(duì)所有當(dāng)前保存的序列擁有基于 LevelDB 的索引。它允許查詢語(yǔ)句含有給定的標(biāo)簽對(duì)label pair,但是缺乏可伸縮的方法來(lái)從不同的標(biāo)簽選集中組合查詢結(jié)果。

例如,從所有的序列中選擇標(biāo)簽 __name__="requests_total" 非常高效,但是選擇  instance="A" AND __name__="requests_total" 就有了可伸縮性的問(wèn)題。我們稍后會(huì)重新考慮導(dǎo)致這一點(diǎn)的原因和能夠提升查找延遲的調(diào)整方法。

事實(shí)上正是這個(gè)問(wèn)題才催生出了對(duì)更好的存儲(chǔ)系統(tǒng)的最初探索。Prometheus 需要為查找億萬(wàn)個(gè)時(shí)間序列改進(jìn)索引方法。

資源消耗

當(dāng)試圖擴(kuò)展  Prometheus(或其他任何事情,真的)時(shí),資源消耗是永恒不變的話題之一。但真正困擾用戶的并不是對(duì)資源的絕對(duì)渴求。事實(shí)上,由于給定的需求,Prometheus  管理著令人難以置信的吞吐量。問(wèn)題更在于面對(duì)變化時(shí)的相對(duì)未知性與不穩(wěn)定性。通過(guò)其架構(gòu)設(shè)計(jì),V2  存儲(chǔ)系統(tǒng)緩慢地構(gòu)建了樣本數(shù)據(jù)塊,這一點(diǎn)導(dǎo)致內(nèi)存占用隨時(shí)間遞增。當(dāng)數(shù)據(jù)塊完成之后,它們可以寫(xiě)到磁盤(pán)上并從內(nèi)存中清除。最終,Prometheus  的內(nèi)存使用到達(dá)穩(wěn)定狀態(tài)。直到監(jiān)測(cè)環(huán)境發(fā)生了改變&mdash;&mdash;每次我們擴(kuò)展應(yīng)用或者進(jìn)行滾動(dòng)更新,序列分流都會(huì)增加內(nèi)存、CPU、磁盤(pán) I/O 的使用。

如果變更正在進(jìn)行,那么它最終還是會(huì)到達(dá)一個(gè)穩(wěn)定的狀態(tài),但比起更加靜態(tài)的環(huán)境,它的資源消耗會(huì)顯著地提高。過(guò)渡時(shí)間通常為數(shù)個(gè)小時(shí),而且難以確定***資源使用量。

為每個(gè)時(shí)間序列保存一個(gè)文件這種方法也使得一個(gè)單個(gè)查詢就很容易崩潰 Prometheus  進(jìn)程。當(dāng)查詢的數(shù)據(jù)沒(méi)有緩存在內(nèi)存中,查詢的序列文件就會(huì)被打開(kāi),然后將含有相關(guān)數(shù)據(jù)點(diǎn)的數(shù)據(jù)塊讀入內(nèi)存。如果數(shù)據(jù)量超出內(nèi)存可用量,Prometheus  就會(huì)因 OOM 被殺死而退出。

在查詢語(yǔ)句完成之后,加載的數(shù)據(jù)便可以被再次釋放掉,但通常會(huì)緩存更長(zhǎng)的時(shí)間,以便更快地查詢相同的數(shù)據(jù)。后者看起來(lái)是件不錯(cuò)的事情。

***,我們看看之前提到的 SSD 的寫(xiě)入放大,以及 Prometheus  是如何通過(guò)批量寫(xiě)入來(lái)解決這個(gè)問(wèn)題的。盡管如此,在許多地方還是存在因?yàn)榕刻∫约皵?shù)據(jù)未精確對(duì)齊頁(yè)邊界而導(dǎo)致的寫(xiě)入放大。對(duì)于更大規(guī)模的  Prometheus  服務(wù)器,現(xiàn)實(shí)當(dāng)中會(huì)發(fā)現(xiàn)縮減硬件壽命的問(wèn)題。這一點(diǎn)對(duì)于高寫(xiě)入吞吐量的數(shù)據(jù)庫(kù)應(yīng)用來(lái)說(shuō)仍然相當(dāng)普遍,但我們應(yīng)該放眼看看是否可以解決它。

重新開(kāi)始

到目前為止我們對(duì)于問(wèn)題域、V2  存儲(chǔ)系統(tǒng)是如何解決它的,以及設(shè)計(jì)上存在的問(wèn)題有了一個(gè)清晰的認(rèn)識(shí)。我們也看到了許多很棒的想法,這些或多或少都可以拿來(lái)直接使用。V2  存儲(chǔ)系統(tǒng)相當(dāng)數(shù)量的問(wèn)題都可以通過(guò)改進(jìn)和部分的重新設(shè)計(jì)來(lái)解決,但為了好玩(當(dāng)然,在我仔細(xì)的驗(yàn)證想法之后),我決定試著寫(xiě)一個(gè)完整的時(shí)間序列數(shù)據(jù)庫(kù)&mdash;&mdash;從頭開(kāi)始,即向文件系統(tǒng)寫(xiě)入字節(jié)。

性能與資源使用這種最關(guān)鍵的部分直接影響了存儲(chǔ)格式的選取。我們需要為數(shù)據(jù)找到正確的算法和磁盤(pán)布局來(lái)實(shí)現(xiàn)一個(gè)高性能的存儲(chǔ)層。

這就是我解決問(wèn)題的捷徑&mdash;&mdash;跳過(guò)令人頭疼、失敗的想法,數(shù)不盡的草圖,淚水與絕望。

V3&mdash;宏觀設(shè)計(jì)

我們存儲(chǔ)系統(tǒng)的宏觀布局是什么?簡(jiǎn)而言之,是當(dāng)我們?cè)跀?shù)據(jù)文件夾里運(yùn)行 tree 命令時(shí)顯示的一切。看看它能給我們帶來(lái)怎樣一副驚喜的畫(huà)面。

$ 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

在最頂層,我們有一系列以 b- 為前綴編號(hào)的block。每個(gè)塊中顯然保存了索引文件和含有更多編號(hào)文件的 chunk 文件夾。chunks 目錄只包含不同序列數(shù)據(jù)點(diǎn)的原始?jí)Kraw chunks of data points。與 V2 存儲(chǔ)系統(tǒng)一樣,這使得通過(guò)時(shí)間窗口讀取序列數(shù)據(jù)非常高效并且允許我們使用相同的有效壓縮算法。這一點(diǎn)被證實(shí)行之有效,我們也打算沿用。顯然,這里并不存在含有單個(gè)序列的文件,而是一堆保存著許多序列的數(shù)據(jù)塊。

index 文件的存在應(yīng)該不足為奇。讓我們假設(shè)它擁有黑魔法,可以讓我們找到標(biāo)簽、可能的值、整個(gè)時(shí)間序列和存放數(shù)據(jù)點(diǎn)的數(shù)據(jù)塊。

但為什么這里有好幾個(gè)文件夾都是索引和塊文件的布局?并且為什么存在***一個(gè)包含 wal 文件夾?理解這兩個(gè)疑問(wèn)便能解決九成的問(wèn)題。

許多小型數(shù)據(jù)庫(kù)

我們分割橫軸,即將時(shí)間域分割為不重疊的塊。每一塊扮演著完全獨(dú)立的數(shù)據(jù)庫(kù),它包含該時(shí)間窗口所有的時(shí)間序列數(shù)據(jù)。因此,它擁有自己的索引和一系列塊文件。

t0            t1             t2             t3             now +-----------+  +-----------+  +-----------+  +-----------+ |           |  |           |  |           |  |           |                 +------------+ |           |  |           |  |           |  |  mutable  | <--- write ---- ┤ Prometheus | |           |  |           |  |           |  |           |                 +------------+ +-----------+  +-----------+  +-----------+  +-----------+                        ^       +--------------+-------+------+--------------+                              |                              |                                                  query                              |                                                    |                            merge -------------------------------------------------+

每一塊的數(shù)據(jù)都是不可變的immutable。當(dāng)然,當(dāng)我們采集新數(shù)據(jù)時(shí),我們必須能向最近的塊中添加新的序列和樣本。對(duì)于該數(shù)據(jù)塊,所有新的數(shù)據(jù)都將寫(xiě)入內(nèi)存中的數(shù)據(jù)庫(kù)中,它與我們的持久化的數(shù)據(jù)塊一樣提供了查找屬性。內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)可以高效地更新。為了防止數(shù)據(jù)丟失,所有傳入的數(shù)據(jù)同樣被寫(xiě)入臨時(shí)的預(yù)寫(xiě)日志write ahead log中,這就是 wal 文件夾中的一些列文件,我們可以在重新啟動(dòng)時(shí)通過(guò)它們重新填充內(nèi)存數(shù)據(jù)庫(kù)。

所有這些文件都帶有序列化格式,有我們所期望的所有東西:許多標(biāo)志、偏移量、變體和 CRC32 校驗(yàn)和。紙上得來(lái)終覺(jué)淺,絕知此事要躬行。

這種布局允許我們擴(kuò)展查詢范圍到所有相關(guān)的塊上。每個(gè)塊上的部分結(jié)果最終合并成完整的結(jié)果。

這種橫向分割增加了一些很棒的功能:

  • 當(dāng)查詢一個(gè)時(shí)間范圍,我們可以簡(jiǎn)單地忽略所有范圍之外的數(shù)據(jù)塊。通過(guò)減少需要檢查的數(shù)據(jù)集,它可以初步解決序列分流的問(wèn)題。

  • 當(dāng)完成一個(gè)塊,我們可以通過(guò)順序的寫(xiě)入大文件從內(nèi)存數(shù)據(jù)庫(kù)中保存數(shù)據(jù)。這樣可以避免任何的寫(xiě)入放大,并且 SSD 與 HDD 均適用。

  • 我們延續(xù)了 V2 存儲(chǔ)系統(tǒng)的一個(gè)好的特性,最近使用而被多次查詢的數(shù)據(jù)塊,總是保留在內(nèi)存中。

  • 很好,我們也不再受限于 1KiB 的數(shù)據(jù)塊尺寸,以使數(shù)據(jù)在磁盤(pán)上更好地對(duì)齊。我們可以挑選對(duì)單個(gè)數(shù)據(jù)點(diǎn)和壓縮格式最合理的尺寸。

  • 刪除舊數(shù)據(jù)變得極為簡(jiǎn)單快捷。我們僅僅只需刪除一個(gè)文件夾。記住,在舊的存儲(chǔ)系統(tǒng)中我們不得不花數(shù)個(gè)小時(shí)分析并重寫(xiě)數(shù)億個(gè)文件。

每個(gè)塊還包含了 meta.json 文件。它簡(jiǎn)單地保存了關(guān)于塊的存儲(chǔ)狀態(tài)和包含的數(shù)據(jù),以便輕松了解存儲(chǔ)狀態(tài)及其包含的數(shù)據(jù)。

mmap

將數(shù)百萬(wàn)個(gè)小文件合并為少數(shù)幾個(gè)大文件使得我們用很小的開(kāi)銷就能保持所有的文件都打開(kāi)。這就解除了對(duì) mmap(2) 的使用的阻礙,這是一個(gè)允許我們通過(guò)文件透明地回傳虛擬內(nèi)存的系統(tǒng)調(diào)用。簡(jiǎn)單起見(jiàn),你可以將其視為交換空間swap space,只是我們所有的數(shù)據(jù)已經(jīng)保存在了磁盤(pán)上,并且當(dāng)數(shù)據(jù)換出內(nèi)存后不再會(huì)發(fā)生寫(xiě)入。

這意味著我們可以當(dāng)作所有數(shù)據(jù)庫(kù)的內(nèi)容都視為在內(nèi)存中卻不占用任何物理內(nèi)存。僅當(dāng)我們?cè)L問(wèn)數(shù)據(jù)庫(kù)文件某些字節(jié)范圍時(shí),操作系統(tǒng)才會(huì)從磁盤(pán)上惰性加載lazy load頁(yè)數(shù)據(jù)。這使得我們將所有數(shù)據(jù)持久化相關(guān)的內(nèi)存管理都交給了操作系統(tǒng)。通常,操作系統(tǒng)更有資格作出這樣的決定,因?yàn)樗梢匀媪私庹麄€(gè)機(jī)器和進(jìn)程。查詢的數(shù)據(jù)可以相當(dāng)積極的緩存進(jìn)內(nèi)存,但內(nèi)存壓力會(huì)使得頁(yè)被換出。如果機(jī)器擁有未使用的內(nèi)存,Prometheus  目前將會(huì)高興地緩存整個(gè)數(shù)據(jù)庫(kù),但是一旦其他進(jìn)程需要,它就會(huì)立刻返回那些內(nèi)存。

因此,查詢不再輕易地使我們的進(jìn)程 OOM,因?yàn)椴樵兊氖歉嗟某志没臄?shù)據(jù)而不是裝入內(nèi)存中的數(shù)據(jù)。內(nèi)存緩存大小變得完全自適應(yīng),并且僅當(dāng)查詢真正需要時(shí)數(shù)據(jù)才會(huì)被加載。

就個(gè)人理解,這就是當(dāng)今大多數(shù)數(shù)據(jù)庫(kù)的工作方式,如果磁盤(pán)格式允許,這是一種理想的方式,&mdash;&mdash;除非有人自信能在這個(gè)過(guò)程中超越操作系統(tǒng)。我們做了很少的工作但確實(shí)從外面獲得了很多功能。

壓縮

存儲(chǔ)系統(tǒng)需要定期“切”出新塊并將之前完成的塊寫(xiě)入到磁盤(pán)中。僅在塊成功的持久化之后,才會(huì)被刪除之前用來(lái)恢復(fù)內(nèi)存塊的日志文件(wal)。

我們希望將每個(gè)塊的保存時(shí)間設(shè)置的相對(duì)短一些(通常配置為 2 小時(shí)),以避免內(nèi)存中積累太多的數(shù)據(jù)。當(dāng)查詢多個(gè)塊,我們必須將它們的結(jié)果合并為一個(gè)整體的結(jié)果。合并過(guò)程顯然會(huì)消耗資源,一個(gè)星期的查詢不應(yīng)該由超過(guò) 80 個(gè)的部分結(jié)果所組成。

為了實(shí)現(xiàn)兩者,我們引入壓縮compaction。壓縮描述了一個(gè)過(guò)程:取一個(gè)或更多個(gè)數(shù)據(jù)塊并將其寫(xiě)入一個(gè)可能更大的塊中。它也可以在此過(guò)程中修改現(xiàn)有的數(shù)據(jù)。例如,清除已經(jīng)刪除的數(shù)據(jù),或重建樣本塊以提升查詢性能。

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) +--------------------------+  +--------------------------+  +-----------+

在這個(gè)例子中我們有順序塊 [1,2,3,4]。塊 1、2、3 可以壓縮在一起,新的布局將會(huì)是 [1,4]?;蛘?,將它們成對(duì)壓縮為 [1,3]。所有的時(shí)間序列數(shù)據(jù)仍然存在,但現(xiàn)在整體上保存在更少的塊中。這極大程度地縮減了查詢時(shí)間的消耗,因?yàn)樾枰喜⒌牟糠植樵兘Y(jié)果變得更少了。

保留

我們看到了刪除舊的數(shù)據(jù)在 V2 存儲(chǔ)系統(tǒng)中是一個(gè)緩慢的過(guò)程,并且消耗  CPU、內(nèi)存和磁盤(pán)。如何才能在我們基于塊的設(shè)計(jì)上清除舊的數(shù)據(jù)?相當(dāng)簡(jiǎn)單,只要?jiǎng)h除我們配置的保留時(shí)間窗口里沒(méi)有數(shù)據(jù)的塊文件夾即可。在下面的例子中,塊  1 可以被安全地刪除,而塊 2 則必須一直保留,直到它落在保留窗口邊界之外。

                      | +------------+  +----+-----+  +-----------+  +-----------+  +-----------+ | 1          |  | 2  |     |  | 3         |  | 4         |  | 5         |   . . . +------------+  +----+-----+  +-----------+  +-----------+  +-----------+                      |                      |             retention boundary

隨著我們不斷壓縮先前壓縮的塊,舊數(shù)據(jù)越大,塊可能變得越大。因此必須為其設(shè)置一個(gè)上限,以防數(shù)據(jù)塊擴(kuò)展到整個(gè)數(shù)據(jù)庫(kù)而損失我們?cè)O(shè)計(jì)的最初優(yōu)勢(shì)。

方便的是,這一點(diǎn)也限制了部分存在于保留窗口內(nèi)部分存在于保留窗口外的塊的磁盤(pán)消耗總量。例如上面例子中的塊 2。當(dāng)設(shè)置了***塊尺寸為總保留窗口的 10% 后,我們保留塊 2 的總開(kāi)銷也有了 10% 的上限。

總結(jié)一下,保留與刪除從非常昂貴到了幾乎沒(méi)有成本。

如果你讀到這里并有一些數(shù)據(jù)庫(kù)的背景知識(shí),現(xiàn)在你也許會(huì)問(wèn):這些都是***的技術(shù)嗎?&mdash;&mdash;并不是;而且可能還會(huì)做的更好。

在內(nèi)存中批量處理數(shù)據(jù),在預(yù)寫(xiě)日志中跟蹤,并定期寫(xiě)入到磁盤(pán)的模式在現(xiàn)在相當(dāng)普遍。

我們看到的好處無(wú)論在什么領(lǐng)域的數(shù)據(jù)里都是適用的。遵循這一方法***的開(kāi)源案例是 LevelDB、Cassandra、InfluxDB 和 HBase。關(guān)鍵是避免重復(fù)發(fā)明劣質(zhì)的輪子,采用經(jīng)過(guò)驗(yàn)證的方法,并正確地運(yùn)用它們。

脫離場(chǎng)景添加你自己的黑魔法是一種不太可能的情況。

索引

研究存儲(chǔ)改進(jìn)的最初想法是解決序列分流的問(wèn)題。基于塊的布局減少了查詢所要考慮的序列總數(shù)。因此假設(shè)我們索引查找的復(fù)雜度是 O(n^2),我們就要設(shè)法減少 n 個(gè)相當(dāng)數(shù)量的復(fù)雜度,之后就相當(dāng)于改進(jìn) O(n^2) 復(fù)雜度。&mdash;&mdash;恩,等等&hellip;&hellip;糟糕。

快速回顧一下“算法 101”課上提醒我們的,在理論上它并未帶來(lái)任何好處。如果之前就很糟糕,那么現(xiàn)在也一樣。理論是如此的殘酷。

實(shí)際上,我們大多數(shù)的查詢已經(jīng)可以相當(dāng)快響應(yīng)。但是,跨越整個(gè)時(shí)間范圍的查詢?nèi)匀缓苈?,盡管只需要找到少部分?jǐn)?shù)據(jù)。追溯到所有這些工作之前,最初我用來(lái)解決這個(gè)問(wèn)題的想法是:我們需要一個(gè)更大容量的倒排索引。

倒排索引基于數(shù)據(jù)項(xiàng)內(nèi)容的子集提供了一種快速的查找方式。簡(jiǎn)單地說(shuō),我可以通過(guò)標(biāo)簽 app="nginx" 查找所有的序列而無(wú)需遍歷每個(gè)文件來(lái)看它是否包含該標(biāo)簽。

為此,每個(gè)序列被賦上一個(gè)唯一的 ID ,通過(guò)該 ID 可以恒定時(shí)間內(nèi)檢索它(O(1))。在這個(gè)例子中 ID 就是我們的正向索引。

示例:如果 ID 為 10、29、9 的序列包含標(biāo)簽 app="nginx",那么 “nginx”的倒排索引就是簡(jiǎn)單的列表 [10, 29, 9],它就能用來(lái)快速地獲取所有包含標(biāo)簽的序列。即使有 200 多億個(gè)數(shù)據(jù)序列也不會(huì)影響查找速度。

簡(jiǎn)而言之,如果 n 是我們序列總數(shù),m 是給定查詢結(jié)果的大小,使用索引的查詢復(fù)雜度現(xiàn)在就是 O(m)。查詢語(yǔ)句依據(jù)它獲取數(shù)據(jù)的數(shù)量 m 而不是被搜索的數(shù)據(jù)體 n 進(jìn)行縮放是一個(gè)很好的特性,因?yàn)?m 一般相當(dāng)小。

為了簡(jiǎn)單起見(jiàn),我們假設(shè)可以在恒定時(shí)間內(nèi)查找到倒排索引對(duì)應(yīng)的列表。

實(shí)際上,這幾乎就是 V2 存儲(chǔ)系統(tǒng)具有的倒排索引,也是提供在數(shù)百萬(wàn)序列中查詢性能的***需求。敏銳的人會(huì)注意到,在最壞情況下,所有的序列都含有標(biāo)簽,因此 m 又成了 O(n)。這一點(diǎn)在預(yù)料之中,也相當(dāng)合理。如果你查詢所有的數(shù)據(jù),它自然就會(huì)花費(fèi)更多時(shí)間。一旦我們牽扯上了更復(fù)雜的查詢語(yǔ)句就會(huì)有問(wèn)題出現(xiàn)。

標(biāo)簽組合

與數(shù)百萬(wàn)個(gè)序列相關(guān)的標(biāo)簽很常見(jiàn)。假設(shè)橫向擴(kuò)展著數(shù)百個(gè)實(shí)例的“foo”微服務(wù),并且每個(gè)實(shí)例擁有數(shù)千個(gè)序列。每個(gè)序列都會(huì)帶有標(biāo)簽 app="foo"。當(dāng)然,用戶通常不會(huì)查詢所有的序列而是會(huì)通過(guò)進(jìn)一步的標(biāo)簽來(lái)限制查詢。例如,我想知道服務(wù)實(shí)例接收到了多少請(qǐng)求,那么查詢語(yǔ)句便是 __name__="requests_total" AND app="foo"

為了找到滿足兩個(gè)標(biāo)簽選擇子的所有序列,我們得到每一個(gè)標(biāo)簽的倒排索引列表并取其交集。結(jié)果集通常會(huì)比任何一個(gè)輸入列表小一個(gè)數(shù)量級(jí)。因?yàn)槊總€(gè)輸入列表最壞情況下的大小為 O(n),所以在嵌套地為每個(gè)列表進(jìn)行暴力求解brute force solution下,運(yùn)行時(shí)間為 O(n^2)。相同的成本也適用于其他的集合操作,例如取并集(app="foo" OR app="bar")。當(dāng)在查詢語(yǔ)句上添加更多標(biāo)簽選擇子,耗費(fèi)就會(huì)指數(shù)增長(zhǎng)到 O(n^3)O(n^4)、O(n^5)&hellip;&hellip;O(n^k)。通過(guò)改變執(zhí)行順序,可以使用很多技巧以優(yōu)化運(yùn)行效率。越復(fù)雜,越是需要關(guān)于數(shù)據(jù)特征和標(biāo)簽之間相關(guān)性的知識(shí)。這引入了大量的復(fù)雜度,但是并沒(méi)有減少算法的最壞運(yùn)行時(shí)間。

這便是 V2 存儲(chǔ)系統(tǒng)使用的基本方法,幸運(yùn)的是,看似微小的改動(dòng)就能獲得顯著的提升。如果我們假設(shè)倒排索引中的 ID 都是排序好的會(huì)怎么樣?

假設(shè)這個(gè)例子的列表用于我們最初的查詢:

__name__="requests_total"   ->   [ 9999, 1000, 1001, 2000000, 2000001, 2000002, 2000003 ]     app="foo"              ->   [ 1, 3, 10, 11, 12, 100, 311, 320, 1000, 1001, 10002 ]              intersection   =>   [ 1000, 1001 ]

它的交集相當(dāng)小。我們可以為每個(gè)列表的起始位置設(shè)置游標(biāo),每次從最小的游標(biāo)處移動(dòng)來(lái)找到交集。當(dāng)二者的數(shù)字相等,我們就添加它到結(jié)果中并移動(dòng)二者的游標(biāo)??傮w上,我們以鋸齒形掃描兩個(gè)列表,因此總耗費(fèi)是 O(2n)=O(n),因?yàn)槲覀兛偸窃谝粋€(gè)列表上移動(dòng)。

兩個(gè)以上列表的不同集合操作也類似。因此 k 個(gè)集合操作僅僅改變了因子 O(k*n) 而不是最壞情況下查找運(yùn)行時(shí)間的指數(shù) O(n^k)。

我在這里所描述的是幾乎所有全文搜索引擎使用的標(biāo)準(zhǔn)搜索索引的簡(jiǎn)化版本。每個(gè)序列描述符都視作一個(gè)簡(jiǎn)短的“文檔”,每個(gè)標(biāo)簽(名稱 + 固定值)作為其中的“單詞”。我們可以忽略搜索引擎索引中通常遇到的很多附加數(shù)據(jù),例如單詞位置和和頻率。

關(guān)于改進(jìn)實(shí)際運(yùn)行時(shí)間的方法似乎存在無(wú)窮無(wú)盡的研究,它們通常都是對(duì)輸入數(shù)據(jù)做一些假設(shè)。不出意料的是,還有大量技術(shù)來(lái)壓縮倒排索引,其中各有利弊。因?yàn)槲覀兊摹拔臋n”比較小,而且“單詞”在所有的序列里大量重復(fù),壓縮變得幾乎無(wú)關(guān)緊要。例如,一個(gè)真實(shí)的數(shù)據(jù)集約有  440 萬(wàn)個(gè)序列與大約 12 個(gè)標(biāo)簽,每個(gè)標(biāo)簽擁有少于 5000  個(gè)單獨(dú)的標(biāo)簽。對(duì)于最初的存儲(chǔ)版本,我們堅(jiān)持使用基本的方法而不壓縮,僅做微小的調(diào)整來(lái)跳過(guò)大范圍非交叉的 ID。

盡管維持排序好的 ID 聽(tīng)起來(lái)很簡(jiǎn)單,但實(shí)踐過(guò)程中不是總能完成的。例如,V2 存儲(chǔ)系統(tǒng)為新的序列賦上一個(gè)哈希值來(lái)當(dāng)作 ID,我們就不能輕易地排序倒排索引。

另一個(gè)艱巨的任務(wù)是當(dāng)磁盤(pán)上的數(shù)據(jù)被更新或刪除掉后修改其索引。通常,最簡(jiǎn)單的方法是重新計(jì)算并寫(xiě)入,但是要保證數(shù)據(jù)庫(kù)在此期間可查詢且具有一致性。V3  存儲(chǔ)系統(tǒng)通過(guò)每塊上具有的獨(dú)立不可變索引來(lái)解決這一問(wèn)題,該索引僅通過(guò)壓縮時(shí)的重寫(xiě)來(lái)進(jìn)行修改。只有可變塊上的索引需要被更新,它完全保存在內(nèi)存中。

基準(zhǔn)測(cè)試

我從存儲(chǔ)的基準(zhǔn)測(cè)試開(kāi)始了初步的開(kāi)發(fā),它基于現(xiàn)實(shí)世界數(shù)據(jù)集中提取的大約 440 萬(wàn)個(gè)序列描述符,并生成合成數(shù)據(jù)點(diǎn)以輸入到這些序列中。這個(gè)階段的開(kāi)發(fā)僅僅測(cè)試了單獨(dú)的存儲(chǔ)系統(tǒng),對(duì)于快速找到性能瓶頸和高并發(fā)負(fù)載場(chǎng)景下的觸發(fā)死鎖至關(guān)重要。

在完成概念性的開(kāi)發(fā)實(shí)施之后,該基準(zhǔn)測(cè)試能夠在我的 Macbook Pro 上維持每秒 2000 萬(wàn)的吞吐量 &mdash;&mdash; 并且這都是在打開(kāi)著十幾個(gè)  Chrome 的頁(yè)面和 Slack  的時(shí)候。因此,盡管這聽(tīng)起來(lái)都很棒,它這也表明推動(dòng)這項(xiàng)測(cè)試沒(méi)有的進(jìn)一步價(jià)值(或者是沒(méi)有在高隨機(jī)環(huán)境下運(yùn)行)。畢竟,它是合成的數(shù)據(jù),因此在除了良好的***印象外沒(méi)有多大價(jià)值。比起最初的設(shè)計(jì)目標(biāo)高出  20 倍,是時(shí)候?qū)⑺渴鸬秸嬲?Prometheus 服務(wù)器上了,為它添加更多現(xiàn)實(shí)環(huán)境中的開(kāi)銷和場(chǎng)景。

我們實(shí)際上沒(méi)有可重現(xiàn)的 Prometheus 基準(zhǔn)測(cè)試配置,特別是沒(méi)有對(duì)于不同版本的 A/B 測(cè)試。亡羊補(bǔ)牢為時(shí)不晚,不過(guò)現(xiàn)在就有一個(gè)了!

我們的工具可以讓我們聲明性地定義基準(zhǔn)測(cè)試場(chǎng)景,然后部署到 AWS 的 Kubernetes 集群上。盡管對(duì)于全面的基準(zhǔn)測(cè)試來(lái)說(shuō)不是***環(huán)境,但它肯定比 64 核 128GB 內(nèi)存的專用裸機(jī)服務(wù)器bare metal servers更能反映出我們的用戶群體。

我們部署了兩個(gè) Prometheus 1.5.2 服務(wù)器(V2 存儲(chǔ)系統(tǒng))和兩個(gè)來(lái)自 2.0 開(kāi)發(fā)分支的 Prometheus (V3  存儲(chǔ)系統(tǒng))。每個(gè) Prometheus 運(yùn)行在配備 SSD  的專用服務(wù)器上。我們將橫向擴(kuò)展的應(yīng)用部署在了工作節(jié)點(diǎn)上,并且讓其暴露典型的微服務(wù)度量。此外,Kubernetes  集群本身和節(jié)點(diǎn)也被監(jiān)控著。整套系統(tǒng)由另一個(gè) Meta-Prometheus 所監(jiān)督,它監(jiān)控每個(gè) Prometheus 的健康狀況和性能。

為了模擬序列分流,微服務(wù)定期的擴(kuò)展和收縮來(lái)移除舊的 pod 并衍生新的 pod,生成新的序列。通過(guò)選擇“典型”的查詢來(lái)模擬查詢負(fù)載,對(duì)每個(gè) Prometheus 版本都執(zhí)行一次。

總體上,伸縮與查詢的負(fù)載以及采樣頻率極大的超出了 Prometheus 的生產(chǎn)部署。例如,我們每隔 15 分鐘換出 60%  的微服務(wù)實(shí)例去產(chǎn)生序列分流。在現(xiàn)代的基礎(chǔ)設(shè)施上,一天僅大約會(huì)發(fā)生 1-5 次。這就保證了我們的 V3  設(shè)計(jì)足以處理未來(lái)幾年的工作負(fù)載。就結(jié)果而言,Prometheus 1.5.2 和 2.0 之間的性能差異在極端的環(huán)境下會(huì)變得更大。

總而言之,我們每秒從 850 個(gè)目標(biāo)里收集大約 11 萬(wàn)份樣本,每次暴露 50 萬(wàn)個(gè)序列。

在此系統(tǒng)運(yùn)行一段時(shí)間之后,我們可以看一下數(shù)字。我們?cè)u(píng)估了兩個(gè)版本在 12 個(gè)小時(shí)之后到達(dá)穩(wěn)定時(shí)的幾個(gè)指標(biāo)。

請(qǐng)注意從 Prometheus 圖形界面的截圖中輕微截?cái)嗟?Y 軸

怎樣寫(xiě)一個(gè)時(shí)間序列數(shù)據(jù)庫(kù)

堆內(nèi)存使用(GB)

內(nèi)存資源的使用對(duì)用戶來(lái)說(shuō)是最為困擾的問(wèn)題,因?yàn)樗鄬?duì)的不可預(yù)測(cè)且可能導(dǎo)致進(jìn)程崩潰。

顯然,查詢的服務(wù)器正在消耗內(nèi)存,這很大程度上歸咎于查詢引擎的開(kāi)銷,這一點(diǎn)可以當(dāng)作以后優(yōu)化的主題??偟膩?lái)說(shuō),Prometheus 2.0  的內(nèi)存消耗減少了 3-4 倍。大約 6 小時(shí)之后,在 Prometheus 1.5 上有一個(gè)明顯的峰值,與我們?cè)O(shè)置的 6  小時(shí)的保留邊界相對(duì)應(yīng)。因?yàn)閯h除操作成本非常高,所以資源消耗急劇提升。這一點(diǎn)在下面幾張圖中均有體現(xiàn)。

怎樣寫(xiě)一個(gè)時(shí)間序列數(shù)據(jù)庫(kù)

CPU 使用(核心/秒)

類似的模式也體現(xiàn)在 CPU 使用上,但是查詢的服務(wù)器與非查詢的服務(wù)器之間的差異尤為明顯。每秒獲取大約 11 萬(wàn)個(gè)數(shù)據(jù)需要 0.5  核心/秒的 CPU 資源,比起評(píng)估查詢所花費(fèi)的 CPU 時(shí)間,我們的新存儲(chǔ)系統(tǒng) CPU 消耗可忽略不計(jì)??偟膩?lái)說(shuō),新存儲(chǔ)需要的 CPU  資源減少了 3 到 10 倍。

怎樣寫(xiě)一個(gè)時(shí)間序列數(shù)據(jù)庫(kù)

磁盤(pán)寫(xiě)入(MB/秒)

迄今為止最引人注目和意想不到的改進(jìn)表現(xiàn)在我們的磁盤(pán)寫(xiě)入利用率上。這就清楚的說(shuō)明了為什么 Prometheus 1.5 很容易造成 SSD  損耗。我們看到最初的上升發(fā)生在***個(gè)塊被持久化到序列文件中的時(shí)期,然后一旦刪除操作引發(fā)了重寫(xiě)就會(huì)帶來(lái)第二個(gè)上升。令人驚訝的是,查詢的服務(wù)器與非查詢的服務(wù)器顯示出了非常不同的利用率。

在另一方面,Prometheus 2.0 每秒僅向其預(yù)寫(xiě)日志寫(xiě)入大約一兆字節(jié)。當(dāng)塊被壓縮到磁盤(pán)時(shí),寫(xiě)入定期地出現(xiàn)峰值。這在總體上節(jié)省了:驚人的 97-99%。

怎樣寫(xiě)一個(gè)時(shí)間序列數(shù)據(jù)庫(kù)

磁盤(pán)大小(GB)

與磁盤(pán)寫(xiě)入密切相關(guān)的是總磁盤(pán)空間占用量。由于我們對(duì)樣本(這是我們的大部分?jǐn)?shù)據(jù))幾乎使用了相同的壓縮算法,因此磁盤(pán)占用量應(yīng)當(dāng)相同。在更為穩(wěn)定的系統(tǒng)中,這樣做很大程度上是正確地,但是因?yàn)槲覀冃枰幚砀叩男蛄蟹至?,所以還要考慮每個(gè)序列的開(kāi)銷。

如我們所見(jiàn),Prometheus 1.5 在這兩個(gè)版本達(dá)到穩(wěn)定狀態(tài)之前,使用的存儲(chǔ)空間因其保留操作而急速上升。Prometheus 2.0  似乎在每個(gè)序列上的開(kāi)銷顯著降低。我們可以清楚的看到預(yù)寫(xiě)日志線性地充滿整個(gè)存儲(chǔ)空間,然后當(dāng)壓縮完成后瞬間下降。事實(shí)上對(duì)于兩個(gè)  Prometheus 2.0 服務(wù)器,它們的曲線并不是完全匹配的,這一點(diǎn)需要進(jìn)一步的調(diào)查。

前景大好。剩下最重要的部分是查詢延遲。新的索引應(yīng)當(dāng)優(yōu)化了查找的復(fù)雜度。沒(méi)有實(shí)質(zhì)上發(fā)生改變的是處理數(shù)據(jù)的過(guò)程,例如 rate() 函數(shù)或聚合。這些就是查詢引擎要做的東西了。

怎樣寫(xiě)一個(gè)時(shí)間序列數(shù)據(jù)庫(kù)

第 99 個(gè)百分位查詢延遲(秒)

數(shù)據(jù)完全符合預(yù)期。在 Prometheus 1.5 上,查詢延遲隨著存儲(chǔ)的序列而增加。只有在保留操作開(kāi)始且舊的序列被刪除后才會(huì)趨于穩(wěn)定。作為對(duì)比,Prometheus 2.0 從一開(kāi)始就保持在合適的位置。

我們需要花一些心思在數(shù)據(jù)是如何被采集上,對(duì)服務(wù)器發(fā)出的查詢請(qǐng)求通過(guò)對(duì)以下方面的估計(jì)來(lái)選擇:范圍查詢和即時(shí)查詢的組合,進(jìn)行更輕或更重的計(jì)算,訪問(wèn)更多或更少的文件。它并不需要代表真實(shí)世界里查詢的分布。也不能代表冷數(shù)據(jù)的查詢性能,我們可以假設(shè)所有的樣本數(shù)據(jù)都是保存在內(nèi)存中的熱數(shù)據(jù)。

盡管如此,我們可以相當(dāng)自信地說(shuō),整體查詢效果對(duì)序列分流變得非常有彈性,并且在高壓基準(zhǔn)測(cè)試場(chǎng)景下提升了 4 倍的性能。在更為靜態(tài)的環(huán)境下,我們可以假設(shè)查詢時(shí)間大多數(shù)花費(fèi)在了查詢引擎上,改善程度明顯較低。

怎樣寫(xiě)一個(gè)時(shí)間序列數(shù)據(jù)庫(kù)

攝入的樣本/秒

快速地看一下不同 Prometheus 服務(wù)器的攝入率。我們可以看到搭載 V3  存儲(chǔ)系統(tǒng)的兩個(gè)服務(wù)器具有相同的攝入速率。在幾個(gè)小時(shí)之后變得不穩(wěn)定,這是因?yàn)椴煌幕鶞?zhǔn)測(cè)試集群節(jié)點(diǎn)由于高負(fù)載變得無(wú)響應(yīng),與 Prometheus  實(shí)例無(wú)關(guān)。(兩個(gè) 2.0 的曲線完全匹配這一事實(shí)希望足夠具有說(shuō)服力)

盡管還有更多 CPU 和內(nèi)存資源,兩個(gè) Prometheus 1.5.2 服務(wù)器的攝入率大大降低。序列分流的高壓導(dǎo)致了無(wú)法采集更多的數(shù)據(jù)。

那么現(xiàn)在每秒可以攝入的絕對(duì)absolute maximum樣本數(shù)是多少?

但是現(xiàn)在你可以攝取的每秒絕對(duì)樣本數(shù)是多少?

我不知道 &mdash;&mdash; 雖然這是一個(gè)相當(dāng)容易的優(yōu)化指標(biāo),但除了穩(wěn)固的基線性能之外,它并不是特別有意義。

有很多因素都會(huì)影響 Prometheus  數(shù)據(jù)流量,而且沒(méi)有一個(gè)單獨(dú)的數(shù)字能夠描述捕獲質(zhì)量。***攝入率在歷史上是一個(gè)導(dǎo)致基準(zhǔn)出現(xiàn)偏差的度量,并且忽視了更多重要的層面,例如查詢性能和對(duì)序列分流的彈性。關(guān)于資源使用線性增長(zhǎng)的大致猜想通過(guò)一些基本的測(cè)試被證實(shí)。很容易推斷出其中的原因。

我們的基準(zhǔn)測(cè)試模擬了高動(dòng)態(tài)環(huán)境下 Prometheus 的壓力,它比起真實(shí)世界中的更大。結(jié)果表明,雖然運(yùn)行在沒(méi)有優(yōu)化的云服務(wù)器上,但是已經(jīng)超出了預(yù)期的效果。最終,成功將取決于用戶反饋而不是基準(zhǔn)數(shù)字。

注意:在撰寫(xiě)本文的同時(shí),Prometheus 1.6 正在開(kāi)發(fā)當(dāng)中,它允許更可靠地配置***內(nèi)存使用量,并且可能會(huì)顯著地減少整體的消耗,有利于稍微提高 CPU 使用率。我沒(méi)有重復(fù)對(duì)此進(jìn)行測(cè)試,因?yàn)檎w結(jié)果變化不大,尤其是面對(duì)高序列分流的情況。

Prometheus 開(kāi)始應(yīng)對(duì)高基數(shù)序列與單獨(dú)樣本的吞吐量。這仍然是一項(xiàng)富有挑戰(zhàn)性的任務(wù),但是新的存儲(chǔ)系統(tǒng)似乎向我們展示了未來(lái)的一些好東西。

第一個(gè)配備 V3 存儲(chǔ)系統(tǒng)的 alpha 版本 Prometheus 2.0 已經(jīng)可以用來(lái)測(cè)試了。在早期階段預(yù)計(jì)還會(huì)出現(xiàn)崩潰,死鎖和其他 bug。

存儲(chǔ)系統(tǒng)的代碼可以在這個(gè)單獨(dú)的項(xiàng)目中找到。Prometheus 對(duì)于尋找高效本地存儲(chǔ)時(shí)間序列數(shù)據(jù)庫(kù)的應(yīng)用來(lái)說(shuō)可能非常有用,這一點(diǎn)令人非常驚訝。

這里需要感謝很多人作出的貢獻(xiàn),以下排名不分先后:

Bjoern Rabenstein 和 Julius Volz 在 V2 存儲(chǔ)引擎上的打磨工作以及 V3 存儲(chǔ)系統(tǒng)的反饋,這為新一代的設(shè)計(jì)奠定了基礎(chǔ)。

Wilhelm Bierbaum 對(duì)新設(shè)計(jì)不斷的建議與見(jiàn)解作出了很大的貢獻(xiàn)。Brian Brazil 不斷的反饋確保了我們最終得到的是語(yǔ)義上合理的方法。

看完上述內(nèi)容是否對(duì)您有幫助呢?如果還想對(duì)相關(guān)知識(shí)有進(jìn)一步的了解或閱讀更多相關(guān)文章,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝您對(duì)億速云的支持。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI