VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

我建議您閱讀 Alexander Valyalkin 2019 年末報告的文字記錄“VictoriaMetrics 中的 Go 優化”

維多利亞指標 — 一個快速且可擴展的 DBMS,用於以時間序列的形式儲存和處理資料(記錄形成時間和與該時間對應的一組值,例如透過定期輪詢感測器的狀態或收集指標)。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

這是該報告的視頻鏈接 - https://youtu.be/MZ5P21j_HLE

幻燈片

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

向我們介紹你自己。 我是亞歷山大·瓦利亞金。 這裡 我的 GitHub 帳戶。 我對 Go 和效能優化充滿熱情。 我寫了很多有用的和不太有用的函式庫。 他們從以下任一開始 fast,或與 quick 字首。

我目前正在研究 VictoriaMetrics。 它是什麼以及我在那裡做什麼? 我將在本次演講中討論這一點。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

報告概要如下:

  • 首先,我會告訴你什麼是VictoriaMetrics。
  • 那我就告訴你什麼是時間序列。
  • 然後我會告訴你時間序列資料庫是如何運作的。
  • 接下來,我將向您介紹資料庫架構:它由什麼組成。
  • 然後讓我們繼續討論 VictoriaMetrics 的最佳化。 這是對倒排索引的最佳化,也是Go中bitset實現的最佳化。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

觀眾中有人知道 VictoriaMetrics 是什麼嗎? 哇,很多人已經知道了。 這是一個好消息。 對於那些不知道的人來說,這是一個時間序列資料庫。 它是基於ClickHouse架構,基於ClickHouse實現的一些細節。 例如,MergeTree、所有可用處理器核心上的平行運算以及透過處理放置在處理器快取中的資料區塊來最佳化效能。

VictoriaMetrics 提供比其他時間序列資料庫更好的資料壓縮。

它可以垂直擴展 - 也就是說,您可以在一台電腦上添加更多處理器、更多 RAM。 VictoriaMetrics 將成功利用這些可用資源並提高線性生產力。

VictoriaMetrics還可以水平擴展——也就是說,你可以為VictoriaMetrics叢集添加額外的節點,它的效能將幾乎呈線性增長。

正如您所猜測的,VictoriaMetrics 是一個快速資料庫,因為我無法編寫其他資料庫。 它是用 Go 編寫的,所以我在這次聚會上談論它。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

誰知道什麼是時間序列? 他也認識很多人。 時間序列是一系列對 (timestamp, значение),其中這些對按時間排序。 該值是一個浮點數 – float64。

每個時間序列都由一個鍵唯一標識。 這個密鑰由什麼組成? 它由一組非空的鍵值對組成。

這是時間序列的範例。 這個系列的關鍵是一個正確的清單: __name__="cpu_usage" 是指標的名稱, instance="my-server" - 這是收集該指標的計算機, datacenter="us-east" - 這是該計算機所在的資料中心。

我們最終得到了一個由三個鍵值對組成的時間序列名稱。 該鍵對應於一個對的列表 (timestamp, value). t1, t3, t3, ..., tN - 這些是時間戳, 10, 20, 12, ..., 15 — 對應的值。 這是給定係列在給定時間的 CPU 使用率。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

時間序列可以用在什麼地方? 有人有什麼主意嗎?

  • 在 DevOps 中,您可以測量 CPU、RAM、網路、rps、錯誤數等。
  • 物聯網-我們可以測量溫度、壓力、地理座標和其他東西。
  • 還有金融——我們可以監控各種股票和貨幣的價格。
  • 此外,時間序列也可用於監控工廠的生產流程。 我們有用戶使用 VictoriaMetrics 來監控機器人的風力渦輪機。
  • 時間序列對於從各種設備的傳感器收集資訊也很有用。 例如,對於發動機; 用於測量輪胎壓力; 用於測量速度、距離; 用於測量汽油消耗量等
  • 時間序列也可用於監控飛機。 每架飛機都有一個黑盒子,用於收集飛機健康狀況的各種參數的時間序列。 時間序列也用於航空航天工業。
  • 醫療保健是血壓、脈搏等。

可能還有更多的應用程式我忘記了,但我希望您理解時間序列在現代世界中得到了積極的使用。 而且它們的使用量每年都在增加。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

為什麼需要時間序列資料庫? 為什麼不能使用常規的關係資料庫來儲存時間序列?

因為時間序列通常包含大量信息,在傳統資料庫中很難儲存和處理。 因此,出現了專門的時間序列資料庫。 這些基地有效儲存積分 (timestamp, value) 使用給定的密鑰。 它們提供了一個 API,用於透過鍵、單一鍵值對、多個鍵值對或正規表示式讀取儲存的資料。 例如,你想找出美國某個資料中心所有服務的CPU負載,那麼你需要使用這個偽查詢。

通常時間序列資料庫提供專門的查詢語言,因為時間序列 SQL 較不適合。 雖然有支援SQL的資料庫,但不太適合。 查詢語言如 普羅姆QL, InfluxQL, , Q。 我希望有人至少聽過其中一種語言。 很多人可能都聽過 PromQL。 這是普羅米修斯查詢語言。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

以 VictoriaMetrics 為例,這就是現代時間序列資料庫架構的樣子。

它由兩個部分組成。 這是倒排索引的儲存和時間序列值的儲存。 這些存儲庫是分開的。

當新記錄到達資料庫時,我們首先訪問倒排索引以查找給定集合的時間序列標識符 label=value 對於給定的指標。 我們找到這個標識符並將該值保存在資料存儲中。

當有請求從 TSDB 檢索資料時,我們會先去倒排索引。 讓我們得到一切 timeseries_ids 與該集合相符的記錄 label=value。 然後我們從資料倉儲中取得所有必要的數據,索引為 timeseries_ids.

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

讓我們來看看一個時間序列資料庫如何處理傳入的選擇查詢的範例。

  • 首先她得到了一切 timeseries_ids 來自包含給定對的倒排索引 label=value,或滿足給定的正規表示式。
  • 然後,它以給定的時間間隔從資料儲存中檢索找到的資料點 timeseries_ids.
  • 之後,資料庫根據使用者的請求對這些資料點執行一些計算。 之後它返回答案。

在本次演講中,我將向您介紹第一部分。 這是一個搜尋 timeseries_ids 透過倒排索引。 你可以稍後看第二部分和第三部分 VictoriaMetrics 來源,或等我準備其他報告:)

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

讓我們繼續討論倒排索引。 許多人可能認為這很簡單。 誰知道什麼是倒排索引以及它是如何運作的? 唉,人已經不多了。 讓我們試著了解它是什麼。

其實很簡單。 它只是一個將鍵映射到值的字典。 什麼是鑰匙? 這對夫婦 label=value哪裡 label и value - 這些是線條。 並且值是一個集合 timeseries_ids,其中包括給定的對 label=value.

倒排索引可以讓你快速找到所有內容 timeseries_ids,其中給出了 label=value.

還可以讓你快速找到 timeseries_ids 幾對的時間序列 label=value,或情侶 label=regexp。 這是怎麼發生的? 透過找到集合的交集 timeseries_ids 對於每對 label=value.

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

讓我們看看倒排索引的各種實作。 讓我們從最簡單的簡單實作開始。 她看起來像這樣。

功能 getMetricIDs 取得字串列表。 每行包含 label=value。 該函數傳回一個列表 metricIDs.

怎麼運作的? 這裡我們有一個全域變量,叫做 invertedIndex。 這是一本普通詞典(map),它將把字串映射到整數切片。 該行包含 label=value.

函數實作:獲取 metricIDs 為了第一 label=value,然後我們完成其他所有事情 label=value, 我們懂了 metricIDs 對於他們來說。 並呼叫該函數 intersectInts,這將在下面討論。 該函數傳回這些列表的交集。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

正如您所看到的,實現倒排索引並不是很複雜。 但這是一個幼稚的實現。 它有什麼缺點? 這種簡單實作的主要缺點是這樣的倒排索引儲存在 RAM 中。 重新啟動應用程式後,我們會遺失該索引。 沒有將該索引儲存到磁碟。 這樣的倒排索引不太可能適合資料庫。

第二個缺點也與內存有關。 倒排索引必須適合 RAM。 如果它超過了 RAM 的大小,那麼顯然我們會得到 - 內存不足錯誤。 並且該程式將無法運行。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

這個問題可以使用現成的解決方案來解決,例如 級別數據庫Rocks資料庫.

簡而言之,我們需要一個能讓我們快速完成三個操作的資料庫。

  • 第一個操作是錄音 ключ-значение 到這個資料庫。 她做得很快,其中 ключ-значение 是任意字串。
  • 第二個操作是使用給定鍵快速搜尋值。
  • 第三個操作是透過給定前綴快速搜尋所有值。

LevelDB 和 RocksDB - 這些資料庫是由 Google 和 Facebook 開發的。 首先是 LevelDB。 然後 Facebook 的人採用了 LevelDB 並開始改進它,他們創造了 RocksDB。 現在Facebook內部幾乎所有的內部資料庫都運行在RocksDB上,包括已經轉移到RocksDB和MySQL上的資料庫。 他們給他取了個名字 我的搖滾.

倒排索引可以使用LevelDB來實現。 怎麼做? 我們另存為密鑰 label=value。 該值是該對存在的時間序列的標識符 label=value.

如果我們有許多具有給定對的時間序列 label=value,那麼這個資料庫中將會有很多行具有相同的鍵和不同的 timeseries_ids。 取得全部列表 timeseries_ids,以此開頭 label=prefix,我們進行範圍掃描,並針對該資料庫進行了最佳化。 也就是說,我們選擇所有以 label=prefix 並獲得必要的 timeseries_ids.

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

下面是 Go 中的範例實作。 我們有一個倒排索引。 這就是LevelDB。

此功能與簡單實現的功能相同。 它幾乎逐行重複簡單的實作。 唯一的一點是,而不是轉向 map 我們訪問倒排索引。 我們得到第一個的所有值 label=value。 然後我們遍歷所有剩餘的對 label=value 並取得它們對應的metricID集合。 然後我們找到交點。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

一切似乎都很好,但這個解決方案也有缺點。 VictoriaMetrics最初實作了基於LevelDB的倒排索引。 但最終我不得不放棄。

為什麼? 因為 LevelDB 比簡單的實作慢。 在一個簡單的實作中,給定給定的鍵,我們立即檢索整個切片 metricIDs。 這是一個非常快速的操作 - 整個切片都可供使用。

在LevelDB中,每次呼叫函數時 GetValues 你需要遍歷所有以 label=value。 並取得每行的值 timeseries_ids。 這樣的 timeseries_ids 收集其中的一片 timeseries_ids。 顯然,這比簡單地透過鍵存取常規地圖要慢得多。

第二個缺點是 LevelDB 是用 C 寫的。從 Go 呼叫 C 函數不是很快。 這需要數百納秒。 這並不是很快,因為與用 go 編寫的常規函數呼叫需要 1-5 奈秒相比,效能相差數十倍。 對於 VictoriaMetrics 來說,這是一個致命的缺陷:)

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

所以我編寫了自己的倒排索引實作。 他打電話給她 合併集.

Mergeset 是基於 MergeTree 資料結構。 這個資料結構借鑒自ClickHouse。 顯然,mergeset應該針對快速搜尋進行最佳化 timeseries_ids 根據給定的密鑰。 合併集完全用 Go 寫。 你可以看到 GitHub 上的 VictoriaMetrics 原始碼。 mergeset的實作在資料夾中 /lib/合併集。 您可以嘗試弄清楚那裡發生了什麼。

合併集 API 與 LevelDB 和 RocksDB 非常相似。 也就是說,它允許您快速保存新記錄並透過給定前綴快速選擇記錄。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

稍後我們會討論mergeset的缺點。 現在我們來談談VictoriaMetrics在生產中實現倒排索引時出現了哪些問題。

他們為何出現?

第一個原因是高流失率。 翻譯成俄語,這是時間序列的頻繁變化。 這是一個時間序列結束、一個新序列開始或許多新時間序列開始的時間。 這種情況經常發生。

第二個原因是時間序列數量龐大。 最初,當監控越來越流行時,時間序列的數量很少。 例如,對於每台計算機,您需要監控 CPU、記憶體、網路和磁碟負載。 每台計算機 4 個時間序列。 假設您有 100 台計算機和 400 個時間序列。 這是很少的。

隨著時間的推移,人們發現他們可以測量更精細的資訊。 例如,不是測量整個處理器的負載,而是單獨測量每個處理器核心的負載。 如果您有 40 個處理器核心,那麼您就有 40 倍的時間序列來測量處理器負載。

但這還不是全部。 每個處理器核心可以有多種狀態,例如空閒時的空閒狀態。 並且也工作在使用者空間、工作在核心空間等狀態。 每個這樣的狀態也可以作為單獨的時間序列來測量。 這也會使行數增加 7-8 倍。

從一個指標中,我們只獲得一台計算機的 40 x 8 = 320 個指標。 乘以 100,我們得到 32,而不是 000。

然後 Kubernetes 出現了。 而且情況變得更糟,因為 Kubernetes 可以託管許多不同的服務。 Kubernetes 中的每個服務都由許多 Pod 組成。 而這一切都需要監控。 此外,我們會不斷部署您的服務的新版本。 對於每個新版本,必須建立新的時間序列。 結果,時間序列的數量呈指數級增長,我們面臨大量時間序列的問題,這就是所謂的高基數。 與其他時間序列資料庫相比,VictoriaMetrics 成功地應對了這個問題。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

讓我們仔細看看高流失率。 是什麼導致生產中的高流失率? 因為標籤和標記的某些含義是不斷變化的。

以 Kubernetes 為例,它有這樣的概念 deployment,即當您的應用程式推出新版本時。 由於某種原因,Kubernetes 開發人員決定將部署 ID 新增到標籤中。

這導致了什麼? 此外,隨著每次新的部署,所有舊的時間序列都會中斷,取而代之的是新的時間序列以新的標籤值開始 deployment_id。 這樣的行可能有數十萬甚至數百萬。

所有這一切的重要一點是,時間序列的總數正在增長,但當前活動和接收資料的時間序列的數量保持不變。 這種狀態稱為高流失率。

高流失率的主要問題是確保在一定時間間隔內給定標籤集的所有時間序列的搜尋速度恆定。 通常這是最後一小時或最後一天的時間間隔。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

如何解決這個問題呢? 這是第一個選項。 這是隨著時間的推移將倒排索引分成獨立的部分。 也就是說,經過一些時間間隔,我們完成目前倒排索引的處理。 並建立一個新的倒排索引。 又一個時間間隔過去了,我們創造了一個又一個。

當從這些倒排索引中取樣時,我們找到一組落在給定區間內的倒排索引。 因此,我們從那裡選擇時間序列的 id。

這節省了資源,因為我們不必查看不在給定間隔內的部分。 也就是說,通常,如果我們選擇最後一小時的數據,那麼對於先前的時間間隔,我們會跳過查詢。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

還有另一種選擇可以解決這個問題。 這是為了每天儲存當天發生的時間序列 ID 的單獨清單。

與先前的解決方案相比,該解決方案的優點是我們不會重複不會隨時間消失的時間序列資訊。 它們始終存在並且不會改變。

缺點是這樣的方案實現起來比較困難,而且調試起來也比較困難。 VictoriaMetrics 選擇了這個解決方案。 歷史上就是這樣發生的。 與前一個解決方案相比,該解決方案也表現良好。 因為這個解決方案沒有被實現,因為它必須在每個分區中複製不改變的時間序列的數據,即不隨著時間的推移而消失。 VictoriaMetrics主要針對磁碟空間消耗進行了最佳化,先前的實作使磁碟空間消耗變得更糟。 但這種實作更適合最大限度地減少磁碟空間消耗,因此選擇了它。

我不得不和她戰鬥。 困難在於,在這個實現中你仍然需要選擇一個更大的數字 timeseries_ids 對於資料而言,倒排索引是時間分區的。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

我們是如何解決這個問題的? 我們用一種原始的方式解決了這個問題——在每個倒排索引條目中儲存多個時間序列標識符而不是一個識別碼。 也就是說,我們有一把鑰匙 label=value,它出現在每個時間序列中。 現在我們保存了幾個 timeseries_ids 在一個條目中。

這是一個例子。 以前我們有 N 個條目,但現在我們有一個條目,其前綴與所有其他條目相同。 對於上一個條目,該值包含所有時間序列 ID。

這使得倒排索引的掃描速度提高了 10 倍。 它允許我們減少快取的記憶體消耗,因為現在我們儲存字串 label=value 只在快取中一起出現過N次。 如果您在標籤和標籤中儲存長行,那麼這條線可能會很大,Kubernetes 喜歡將其推到那裡。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

加快倒排索引搜尋速度的另一個選擇是分片。 建立多個倒排索引而不是一個,並按鍵在它們之間分片資料。 這是一套 key=value 蒸汽。 也就是說,我們得到了幾個獨立的倒排索引,我們可以在多個處理器上並行查詢它們。 先前的實作僅允許在單處理器模式下運行,即僅在一個核心上掃描資料。 此解決方案可讓您同時掃描多個核心上的數據,就像 ClickHouse 喜歡做的那樣。 這就是我們計劃實施的。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

現在讓我們回到我們的羊—交集函數 timeseries_ids。 讓我們考慮一下可能有哪些實作。 這個功能可以讓你找到 timeseries_ids 對於給定的集合 label=value.

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

第一個選項是一個幼稚的實作。 兩個嵌套循環。 這裡我們得到函數輸入 intersectInts 兩片—— a и b。 在輸出處,它應該向我們傳回這些切片的交集。

一個幼稚的實現看起來像這樣。 我們迭代切片中的所有值 a,在這個迴圈中我們遍歷 slice 的所有值 b。 我們將它們進行比較。 如果它們匹配,那麼我們就找到了交集。 並將其保存在 result.

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

有什麼缺點? 二次複雜度是其主要缺點。 例如,如果您的尺寸是切片 a и b 一次一百萬,那麼這個函數永遠不會給你回答案。 因為它需要一兆次迭代,即使對現代電腦來說這也是很多次了。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

第二種實作是基於map的。 我們創建地圖。 我們將切片中的所有值放入此映射中 a。 然後我們在一個單獨的循環中遍歷切片 b。 我們檢查這個值是否來自切片 b 在地圖中。 如果存在,則將其新增至結果。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

有什麼好處? 優點是只有線性複雜度。 也就是說,對於較大的切片,函數執行速度會更快。 對於百萬大小的切片,此函數將執行 2 萬次迭代,而不是前一個函數的兆次迭代。

缺點是該函數需要更多記憶體來建立該地圖。

第二個缺點是散列的巨大開銷。 這個缺點不是很明顯。 對我們來說,這也不是很明顯,所以最初在 VictoriaMetrics 中,交叉點的實作是透過地圖來實現的。 但隨後分析表明,主處理器時間花費在寫入映射並檢查該映射中是否存在值。

為什麼CPU時間會浪費在這些地方呢? 因為 Go 對這些行執行了哈希操作。 也就是說,它計算鍵的哈希值,然後在 HashMap 中的給定索引處存取它。 哈希計算操作在幾十納秒內完成。 這對於 VictoriaMetrics 來說很慢。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

我決定實現一個專門針對這種情況優化的位元集。 這就是兩個切片的交集現在的樣子。 這裡我們創建一個位集。 我們將第一個切片中的元素加入其中。 然後我們檢查第二個切片中是否存在這些元素。 並將它們添加到結果中。 也就是說,它與前面的例子幾乎沒有什麼不同。 這裡唯一的事情是我們用自訂函數取代了對地圖的訪問 add и has.

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

乍一看,如果之前使用標準地圖,然後調用一些其他函數,那麼這似乎應該運行得更慢,但分析表明,在 VictoriaMetrics 的情況下,這個東西的運行速度比標準地圖快 10 倍。

此外,與映射實作相比,它使用的記憶體要少得多。 因為我們在這裡儲存位元而不是八位元組值。

這種實現的缺點是它不是那麼明顯,不是微不足道的。

許多人可能沒有註意到的另一個缺點是,這種實現在某些情況下可能無法正常運作。 也就是說,它針對特定情況(即 VictoriaMetrics 時間序列 id 的交集情況)進行了最佳化。 這並不意味著它適合所有情況。 如果使用不當,我們不會得到效能提升,而是出現記憶體不足錯誤和效能下降。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

讓我們考慮一下這個結構的實作。 如果您想查看,它位於 VictoriaMetrics 來源的資料夾中 庫/uint64set。 它專門針對 VictoriaMetrics 案例進行了最佳化,其中 timeseries_id 是一個 64 位元值,其中前 32 位元基本上不變,只有最後 32 位元發生變化。

該資料結構不會儲存在磁碟上,它只在記憶體中運行。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

這是它的 API。 這不是很複雜。 該 API 是專門針對使用 VictoriaMetrics 的特定範例而客製化的。 也就是說,這裡沒有多餘的功能。 以下是 VictoriaMetrics 明確使用的函數。

有功能 add,這增加了新的值。 有一個功能 has,它檢查新值。 並且有一個功能 del,這會刪除值。 有一個輔助函數 len,它傳回集合的大小。 功能 clone 克隆很多。 及功能 appendto 將此集合轉換為切片 timeseries_ids.

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

這就是這個資料結構的實現的樣子。 集合有兩個元素:

  • ItemsCount 是一個輔助字段,用於快速傳回集合中的元素數量。 沒有這個輔助字段也是可以的,但必須在此處添加它,因為 VictoriaMetrics 經常在其演算法中查詢位元集長度。

  • 第二個字段是 buckets。 這是結構的切片 bucket32。 每個結構存儲 hi 場地。 這些是高 32 位。 還有兩片—— b16his и bucketsbucket16 結構。

16 位元結構第二部分的前 64 位元儲存在這裡。 這裡儲存每個位元組的低 16 位元的位元集。

Bucket64 由一個陣列組成 uint64。 使用這些常數計算長度。 合而為一 bucket16 最大可存儲 2^16=65536 少量。 如果將其除以 8,則為 8 KB。 如果再除以 8,就是 1000 uint64 意義。 那是 Bucket16 – 這是我們的 8 KB 結構。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

讓我們看看這個結構中新增值的方法之一是如何實現的。

一切都始於 uint64 含義。 我們計算高 32 位,我們計算低 32 位。 讓我們回顧一下一切 buckets。 我們將每個桶中的前 32 位與新增的值進行比較。 如果它們匹配,那麼我們呼叫該函數 add 在結構b32中 buckets。 並在那裡添加低 32 位元。 如果它回來了 true,那麼這意味著我們在那裡添加了這樣的值而我們沒有這樣的值。 如果回傳的話 false,那麼這樣的意義就已經存在了。 然後我們增加結構中的元素數量。

如果我們沒有找到您需要的人 bucket 具有所需的高值,然後我們呼叫該函數 addAlloc,這將產生一個新的 bucket,將其添加到桶結構中。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

這是函數的實現 b32.add。 它與之前的實現類似。 我們計算最高有效 16 位,最低有效 16 位。

然後我們遍歷所有高 16 位元。 我們找到匹配項。 如果存在匹配,我們將呼叫 add 方法,我們將在下一頁中考慮該方法 bucket16.

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

而且這裡是最低級別,應該盡可能優化。 我們計算為 uint64 切片位元中的 id 值以及 bitmask。 這是給定 64 位元值的掩碼,可用於檢查該位元是否存在或設定它。 我們檢查該位是否已設定並設定它,然後返回存在。 這是我們的實現,與傳統地圖相比,它使我們能夠將時間序列相交 id 的操作速度加快 10 倍。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

除了這個優化之外,VictoriaMetrics還有很多其他的優化。 大多數這些優化都是出於某種原因而添加的,但是是在對生產中的程式碼進行分析之後添加的。

這是優化的主要規則 - 不要假設這裡存在瓶頸而添加優化,因為結果可能不會存在瓶頸。 優化通常會降低程式碼的品質。 因此,只有在分析之後並且最好在生產中才值得優化,以便這是真實的數據。 如果有人有興趣,您可以查看 VictoriaMetrics 原始碼並探索其中的其他優化。

VictoriaMetrics 中的 Go 最佳化。 亞歷山大·瓦亞爾金

我有一個關於位集的問題。 與 C++ 向量 bool 實作非常相似,優化了位元集。 您是從那裡實施的嗎?

不,不是從那裡開始的。 在實現這個位元集時,我以這些 ids 時間序列的結構知識為指導,這些時間序列在 VictoriaMetrics 中使用。 而它們的結構是這樣的:高32位元基本上不變。 低 32 位元可能會發生變化。 位越低,改變的頻率就越高。 因此,該實作專門針對該資料結構進行了最佳化。 據我所知,C++ 實作針對一般情況進行了最佳化。 如果針對一般情況進行最佳化,這意味著它對於特定情況來說並不是最佳的。

我還建議您觀看阿列克謝·米洛維德的報道。 大約一個月前,他談到了 ClickHouse 中針對特定專業的最佳化。 他只是說,在一般情況下,C++ 實現或其他一些實現經過專門設計,可以在醫院中正常運作。 它的性能可能比像我們這樣的特定於知識的實現更差,我們知道前 32 位大部分是不變的。

我還有第二個問題。 與 InfluxDB 的根本差異是什麼?

有許多根本性的差異。 在效能和記憶體消耗方面,InfluxDB 在測試中顯示,當您​​擁有大量(例如數百萬)高基數時間序列時,記憶體消耗會增加 10 倍。 例如,VictoriaMetrics 每百萬活動行消耗 1 GB,而 InfluxDB 消耗 10 GB。 這是一個很大的區別。

第二個根本差異是 InfluxDB 有奇怪的查詢語言-Flux 和 InfluxQL。 與相比,它們處理時間序列不太方便 普羅姆QL,由 VictoriaMetrics 支持。 PromQL 是 Prometheus 的一種查詢語言。

還有一個差異是 InfluxDB 有一個稍微奇怪的資料模型,其中每一行可以儲存具有不同標籤集的多個欄位。 這些行進一步分為各種表。 這些額外的複雜性使該資料庫的後續工作變得複雜。 很難支持和理解。

在 VictoriaMetrics 中,一切都變得更加簡單。 在那裡,每個時間序列都是一個鍵值。 該值是一組點 - (timestamp, value),關鍵是集合 label=value。 字段和測量之間沒有分離。 它允許你選擇任何數據,然後組合、加、減、乘、除,不像 InfluxDB,據我所知,不同行之間的計算仍然沒有實現。 即使實現了,也很困難,你必須寫很多程式碼。

我有一個澄清的問題。 我是否正確理解您提到的某種問題,即該倒排索引不適合內存,因此存在分區?

首先,我展示了標準 Go 映射上倒排索引的簡單實作。 此實作不適合資料庫,因為此倒排索引不會儲存到磁碟,且資料庫必須儲存到磁碟,以便該資料在重新啟動時仍然可用。 在此實作中,當您重新啟動應用程式時,您的倒排索引將消失。 您將無法存取所有數據,因為您將無法找到它。

你好! 感謝您的報告! 我叫帕維爾。 我來自野莓。 我有幾個問題想問你。 問題一。 您是否認為,如果您在構建應用程式架構時選擇了不同的原則並隨著時間的推移對數據進行分區,那麼您也許能夠在搜索時交叉數據,僅基於一個分區包含一個分區的數據這一事實一段時間,也就是在一個時間間隔內,你就不用擔心你的棋子分散的情況不同了? 問題 2 - 既然您正在使用位元集和其他所有內容實現類似的演算法,那麼也許您嘗試過使用處理器指令? 也許你嘗試過這樣的優化?

我馬上回答第二個問題。 我們還沒到那一步。 但如果有必要,我們會到達那裡。 第一個問題是什麼?

您討論了兩種情況。 他們說他們選擇了第二個,實現更複雜。 他們不喜歡第一個,即資料按時間分區。

是的。 在第一種情況下,索引的總容量會更大,因為在每個分區中,我們必須儲存持續通過所有這些分區的時間序列的重複資料。 如果您的時間序列流失率很小,即不斷使用相同的序列,那麼在第一種情況下,與第二種情況相比,我們會損失更多的磁碟空間佔用量。

所以 - 是的,時間分區是一個不錯的選擇。 普羅米修斯使用它。 但普羅米修斯還有另一個缺點。 合併這些資料時,需要在記憶體中保留所有標籤和時間序列的元資訊。 因此,如果它合併的資料塊很大,那麼合併過程中記憶體消耗會增加很多,這與VictoriaMetrics不同。 合併時,VictoriaMetrics 根本不消耗記憶體;無論合併的資料區塊有多大,僅消耗數千位元組。

您使用的演算法使用記憶體。 它標記包含值的時間序列標籤。 透過這種方式,您可以檢查一個資料數組和另一個資料數組中是否存在配對。 並且您了解是否發生了相交。 通常,資料庫實作遊標和迭代器來儲存其當前內容並運行排序的數據,因為這些操作非常複雜。

為什麼我們不使用遊標來遍歷資料呢?

是。

我們將排序的行儲存在 LevelDB 或合併集中。 我們可以移動遊標並找到交點。 我們為什麼不使用它? 因為它很慢。 因為遊標意味著你需要為每一行呼叫一個函數。 一次函數呼叫需要 5 奈秒。 如果你有 100 行,那麼事實證明我們只花了半秒的時間來呼叫該函數。

有這樣的事,是的。 我的最後一個問題。 這個問題聽起來可能有點奇怪。 為什麼無法在資料到達時讀取所有必要的聚合並將它們保存為所需的形式? 為什麼要在一些系統(如 VictoriaMetrics、ClickHouse 等)中保存大量數據,然後在它們上花費大量時間?

我將舉一個例子使其更清楚。 我們來談談小型玩具車速表是如何運作的? 它記錄您行駛的距離,始終將其加到一個值,然後添加第二個值。 並分裂。 並獲得平均速度。 你也可以做同樣的事情。 即時添加所有必要的事實。

好吧,我明白這個問題了。 你的例子有它的一席之地。 如果您知道需要什麼聚合,那麼這是最好的實現。 但問題是,人們在 ClickHouse 中保存了這些指標、一些數據,但他們還不知道將來如何聚合和過濾它們,所以他們必須保存所有原始數據。 但是,如果您知道需要計算平均值,那麼為什麼不計算它而不是在那裡存儲一堆原始值呢? 但這只有在您確切知道自己需要什麼的情況下才能實現。

順便說一句,用於存儲時間序列的資料庫支援聚合計數。 例如,普羅米修斯支持 記錄規則。 也就是說,如果您知道需要什麼單位,就可以做到這一點。 VictoriaMetrics 還沒有這個,但通常在 Prometheus 之前,這可以在重新編碼規則中完成。

例如,在我之前的工作中,我需要計算過去一小時內滑動視窗中的事件數。 問題是我必須在 Go 中進行自訂實現,即用於計數這個東西的服務。 這項服務最終並非微不足道,因為它很難計算。 如果您需要以固定時間間隔對某些聚合進行計數,那麼實作可能會很簡單。 如果你想統計滑動視窗中的事件,那麼它並不像看起來那麼簡單。 我認為這在ClickHouse或時間序列資料庫中還沒有實現,因為它很難實現。

還有一個問題。 我們只是在談論平均,我記得曾經有過帶有碳後端的石墨這樣的東西。 而且他知道如何對舊數據進行稀疏化,即每分鐘留下一個點,每小時留下一個點等等。原則上,如果我們需要原始數據,相對來說,一個月,其他一切都可以,這是相當方便的。被稀疏化。 但 Prometheus 和 VictoriaMetrics 不支援此功能。 有計劃支援嗎? 如果沒有,為什麼不呢?

謝謝你的提問。 我們的用戶定期詢問這個問題。 他們詢問我們何時會添加對下採樣的支援。 這裡有幾個問題。 首先,每個使用者都明白 downsampling 有些不同:有人想要得到給定區間內的任意點,有人想要最大值、最小值、平均值。 如果許多系統將資料寫入您的資料庫,那麼您就無法將所有資料集中在一起。 每個系統可能需要不同的細化。 而這很難實施。

第二件事是,VictoriaMetrics 與 ClickHouse 一樣,針對處理大量原始資料進行了最佳化,因此如果系統中有許多核心,它可以在不到一秒鐘的時間內處理 50 億行資料。 在 VictoriaMetrics 中掃描時間序列點 – 每核心每秒 000 個點。 而且這種性能可以擴展到現有的核心。 也就是說,例如,如果您有 000 個核心,則每秒將掃描 20 億個點。 而 VictoriaMetrics 和 ClickHouse 的這項特性減少了對下採樣的需求。

另一個特點是 VictoriaMetrics 有效地壓縮了這些資料。 生產中平均壓縮為每點 0,4 到 0,8 位元組。 每個點都是時間戳+值。 並且平均被壓縮到不到XNUMX個位元組。

謝爾蓋. 我有個問題。 最小記錄時間量是多少?

一毫秒。 我們最近與其他時間序列資料庫開發人員進行了對話。 他們的最小時間片是一秒。 例如,在 Graphite 中,它也是一秒鐘。 在 OpenTSDB 中也是一秒。 InfluxDB 具有奈秒精度。 在 VictoriaMetrics 中它是一毫秒,因為在 Prometheus 中它是一毫秒。 而VictoriaMetrics最初是作為Prometheus的遠端存儲而開發的。 但現在它可以保存其他系統的資料。

與我交談的人說他們具有秒到秒的精度 - 這對他們來說已經足夠了,因為這取決於時間序列資料庫中儲存的資料類型。 如果這是 DevOps 數據或來自基礎設施的數據,您以每分鐘 30 秒的間隔收集數據,那麼秒精度就足夠了,您不需要任何其他東西。 如果您從高頻交易系統收集這些數據,那麼您需要奈秒的精確度。

VictoriaMetrics中的毫秒精確度也適用於DevOps案例,並且可以適用於我在報告開頭提到的大多數案例。 它唯一可能不適合的是高頻交易系統。

謝謝你! 還有一個問題。 PromQL 中的相容性是什麼?

完全向後相容。 VictoriaMetrics 完全支援 PromQL。 此外,它還在 PromQL 中添加了額外的高級功能,稱為 MetricsQL。 YouTube 上有一個關於此擴充功能的討論。 我在春天在聖彼得堡舉行的監測聚會上發表了演講。

電報頻道 維多利亞指標.

只有註冊用戶才能參與調查。 登入, 請。

是什麼阻止您改用 VictoriaMetrics 作為 Prometheus 的長期存儲? (寫在評論裡,我會把它加到投票中))

  • 企業排放佔全球 71,4%我不使用普羅米修斯5

  • 企業排放佔全球 28,6%不知道 VictoriaMetrics2

7 位用戶投票。 12 名用戶棄權。

來源: www.habr.com

添加評論