Grafana Loki查詢加速:如何在不新增資源的前提下提升查詢速度
來自Grafana Loki query acceleration: How we sped up queries without adding resources,介紹了Loki如何透過n-grams + 布隆過濾器來加速查詢。
在過去的5年中,我們在平衡特性開發和支援大規模使用者之時,改善了日誌聚合系統。早在Loki 3.0之前,我們就已經將峰值吞吐量從Loki 1.0的 10GB/s 提升到了1TB/s 以上。但為了更進一步,並符合低成本、易使用的準則,我們需要尋求一種更加明智的方式。
例如,最近我們在過濾查詢時遇到一個有趣的事情:查詢時會訪問大量根本不需要的資料。例如在一個對7天資料的查詢中,我們的Grafana Labs生產叢集處理了280PB的日誌,但從結果上看,大約有140PB的搜尋日誌並不匹配任何過濾表示式,換句話說,對50%的資料的查詢並沒有返回任何結果。更糟糕的是,在65%的資料(182PB)處理中,每1百萬日誌僅返回了1條日誌行。
當然,我們可以透過增加更多的計算資源來解決吞吐量問題,但這與我們的願景(Loki應該是高效可擴充套件的)不符。我們將此看做是一個挑戰,也是一個機會。那麼如何在保持Loki易用性和成本的同時提升過濾查詢的效率?
如何加速Loki的查詢
在深入這個話題之前,我們需要介紹本文中的兩個概念: n-grams and 布隆過濾器:
- 我們使用n-grams提供子字串排列。如假設給定字串"abcdef",那麼三元組(n-grams的n=3)為"abc"、"bcd"、"cde"和"def"
- 布隆過濾器是一種用於機率匹配的資料結構。它可以判斷假陽性,但無法判斷假陰性。它在資料庫層面有相當長的應用歷史。
正如上面所述,我們觀察到,請求時間內的大部分資料都是不相關的資料,對特定的查詢來說,這些資料毫無價值。我們需要識別並消除這些不相關資料,確保只搜尋相關的資料。為此我們需要關注"不相關的資料在哪裡",並找到一種"足夠好的"空間有效的資料結構來評估出一個過濾查詢的相關資料的所在位置,然後忽略掉其餘資料。
另一個關鍵要素是n-grams。它可以幫助Loki維護"結構無意識性"(即沒有schemas)。該演算法會建立很多資料,但鑑於很多日誌行都有共同的資訊,因此可以大大減少建立的資料量。例如,下面粗體的部分在所有日誌中都是重複的,這部分內容就稱之為共同資訊:
msg=”adding item to cart” userID="a4hbfer74g" itemID="jr8fdnasd65u4"
msg=”adding item to cart” userID="a4hbfer74g" itemID="78kjasdj4hs21"
msg=”adding item to cart” userID="h74jndvys6" itemID="yclk37uzs95j8”
幸運的是,布隆過濾器可以解決重複資料在空間上的問題。下面這張圖展示了一條訪問日誌,其中綠色部分表示日誌中經常出現的資訊,黃色部分表示有時候出現的資訊,而紅色部分表示很少出現的資訊。
將上述技術組合起來,就得到了如下方式,後續章節將詳細介紹它是如何運作的:
這意味著,可以透過布隆過濾器來提升過濾查詢的效率, 而在觀察中發現,布隆過濾器的大小僅為日誌的2%。
除了已經討論過的所有內容之外,查詢加速還附帶了一種全新的查詢分片策略,在該策略中,我們利用布隆過濾器來生成更少、更公平的查詢分片。
傳統上,透過分析TSDB的索引統計資料,Loki會將資料劃分為大小大致相等的分最接近二次冪的分片。但事實上並非如此,有些序列的資料量要大於其他序列的資料量,導致分片資料處理不均衡。
我們使用布隆過濾器來降低查詢前端在準備階段需要處理的chunks數,並將chunks均衡地分發到每個需要處理的分片查詢上。
布隆過濾器元件是如何工作的
下面讓我們看下如何建立布隆過濾器,以及如何使用它們來匹配過濾表示式。我們引入了兩個元件:Bloom compactor和Bloom gateway。
compactor會從物件儲存的chunks之上構建布隆過濾器。我們為每個序列構建一個Bloom,並將其聚合為block檔案。Bloom blocks中的序列遵循與Loki的TSDB和分片計算相同的排序方案。由於相同分片中的序列有可能存在於相同的block,因此有利於本地資料查詢。除blocks之外,compactor還維護了一個包含對Bloom blocks的引用以及構建所需要的TSDB索引檔案的後設資料檔案列表。
Bloom compactor是可以水平擴充套件的,它們使用一個環來分割租戶,並宣告對序列的key空間的子集的所有權。對於一個給定的序列,負責該序列的compactor會遍歷其所有的chunks中日誌行來構建一個Bloom。對於每個日誌行,我們會計算其n-gram,並將每個n-gram的雜湊值以及每個n-gram加上chunk識別符號的雜湊值追加到Bloom中,前者用於讓gateways跳過整個序列,而後者用於跳過整個chunks。
例如,在chunk "aaf67d"中有一條日誌行"abcdef",首先計算其n-grams: "abc", "bcd", "cde", "def",然後追加序列的布隆:hash("abc")、hash("abc" + "aaf67d") … hash("def")、hash("def" + "aaf67d")。
另一方面Bloom Gateway會處理來自index gateway的chunks過濾請求,它使用一系列chunks和過濾表示式來匹配布隆過濾器,並移除掉哪些不匹配過濾表示式的chunks。
類似Bloom compactors,Bloom gateways也是可以水平擴充套件的,並使用環來實現和compactor相同的目的:租戶分片和序列key空間。index gateways使用環,並基於chunk的序列指紋來確定應該傳送chunk過濾請求的bloom gateway。
將n-grams而非整條日誌行新增到compactor的布隆過濾器中,可以實現Bloom gateway的部分匹配。例如上面例子中,過濾表示式 |= "abcd"
可以生成兩個n-grams: "abc" 和 "bcd",兩種都匹配布隆。
對於序列中的每個chunk,我們可以透過將chunk ID追加到n-gram的方式來進行布隆測試。
下圖展示瞭如何構建布隆,並利用它來跳過不匹配過濾表示式的n-grams的序列和blocks。
可以看到bloom compactor用於構建bloom,而bloom gateway則會查詢compactor構建的bloom。
n-grams的大小是可以配置的,n-grams越長,追加到布隆過濾器的tokens越少。但同時也需要更長的過濾表示式來匹配布隆過濾器。例如,當n-grams長度為3時,則過濾表示式的長度也最少需要3個字元。
建立一個好的使用者體驗
當然,我們的首先目標是加快查詢速度,這種方式更傾向於查詢一些比較少見的資料,如UUID。
布隆過濾器非常適用於精確查詢,例如,如果你要在所有日誌中查詢一個特定的消費者ID或一個特定的錯誤碼,對於開發者或支援工程師來說這是一種常見的使用模式。由於布隆過濾器可以讓Loki只處理可能包含這些術語的日誌,而這些術語可能出現在很少的日誌行中,所以對搜尋時間的影響是巨大的。
所有跡象都表明布隆過濾器確實有效。早期的內部測試發現,透過引入布隆過濾器,Loki可以在查詢時跳過相當大比例的日誌資料。在我們的測試環境中發現,相比之前的查詢,現在可以過濾掉70%到90%的chunks。
下面是一個使用布隆過濾器執行"大海撈針"式的查詢的結果,該查詢包括幾個過濾條件,代表了我們看到客戶在由Loki支援的Grafana Cloud Logs上執行的典型使用場景。
使用注意事項
當前實現中,並不是所有的查詢都能受益於布隆過濾器。下面看下哪些地方會用到布隆過濾器,哪些地方不會用到布隆過濾器。
下面場景的查詢可以使用布隆過濾器:
- 包含至少一個過濾表示式,如
{env="prod"} |= "order=17863472" | logfmt
就可以使用布隆過濾器,但{env="prod"} | logmt
則不會
下面查詢則會阻止使用布隆過濾器:
- 布隆過濾器並不適用於非等式過濾:常規和正規表示式過濾器。如
!= "debug"
和!~ "(staging|dev) "
都不會使用布隆過濾器 line_format
之後的過濾表示式也不會使用布隆過濾器。如 |= `level="error"` | logfmt | line_format "ERROR {{.err}}" |= `traceID="3ksn8d4jj3"` 其中 |= `level="error"`將使用布隆過濾器,但 |= `traceID="3ksn8d4jj3"` 則不會使用。- 查詢尚未從Bloom gateways下載的Bloom blocks。
總結
Loki新引入的這個特性包含兩個元件compactor和gateway。compactor用於生成序列的布隆過濾器, 布隆過濾器有兩種,一種是根據序列表示式生成的n-grams布隆過濾器,另一種是將序列表示式和物件儲存的buckets關聯起來的布隆過濾器。當gateway接收到一個查詢請求時,會根據該請求序列表示式生成n-grams,然後在第一個布隆過濾器中查詢是否存在該序列,如果不存在則直接返回,如果存在則繼續透過第二個過濾器排除掉不匹配請求表示式的buckets,進而減少需要處理的buckets。