Grafana 系列文章(十一):Loki 中的標籤如何使日誌查詢更快更方便

東風微鳴發表於2023-02-08

?️URL: https://grafana.com/blog/2020/04/21/how-labels-in-loki-can-make-log-queries-faster-and-easier/

?Description:

關於標籤在 Loki 中如何真正發揮作用,你需要知道的一切。它可能與你想象的不同

在我們從事 Loki 專案的第一年的大部分時間裡,問題和反饋似乎都來自熟悉 Prometheus 的人。畢竟,Loki 就像 Prometheus--不過是針對日誌的!"。

但是最近,我們看到越來越多的人嘗試使用 Loki,他們沒有 Prometheus 的經驗,而且許多人來自於具有不同策略的系統,以處理日誌。這就帶來了很多關於 Loki 一個非常重要的概念的問題,即使是 Prometheus 專家也想了解更多:標籤 (Labels)!

這篇文章將涵蓋很多內容,以幫助每一個剛接觸 Loki 的人和想要複習的人。我們將探討以下主題。

什麼是標籤 (Label)?

標籤是鍵值對,可以被定義為任何東西!我們喜歡把它們稱為後設資料 (metadata),用來描述日誌流。如果你熟悉 Prometheus,你會習慣性地看到一些標籤,比如jobinstance,我將在接下來的例子中使用這些。

我們用 Loki 提供的刮削 (scrape) 配置也定義了這些標籤。如果你正在使用 Prometheus,在 Loki 和 Prometheus 之間擁有一致的標籤是 Loki 的超級優勢之一,使你 非常容易將你的應用程式指標 (Metrics) 與你的日誌 (Logs) 資料聯絡起來

Loki 如何使用標籤

Loki 中的標籤執行一個非常重要的任務。它們定義了一個流。更確切地說,每個標籤的鍵和值的組合都定義了流。如果只有一個標籤值發生變化,就會產生一個新的流。

如果你熟悉 Prometheus,那裡使用的術語是系列 (series);但是,Prometheus 有一個額外的維度:度量名稱 (metric name)。Loki 簡化了這一點,沒有度量名稱,只有標籤,我們決定使用流而不是系列。

讓我們舉個例子:

scrape_configs:
 - job_name: system
   pipeline_stages:
   static_configs:
   - targets:
      - localhost
     labels:
      job: syslog
      __path__: /var/log/syslog

這個配置將跟蹤一個檔案並分配一個標籤:job=syslog。你可以這樣查詢:

{job=”syslog”}

這將在 Loki 建立一個流。

現在讓我們把這個例子擴大一點:

scrape_configs:
 - job_name: system
   pipeline_stages:
   static_configs:
   - targets:
      - localhost
     labels:
      job: syslog
      __path__: /var/log/syslog
 - job_name: system
   pipeline_stages:
   static_configs:
   - targets:
      - localhost
     labels:
      job: apache
      __path__: /var/log/apache.log

現在我們正在跟蹤兩個檔案。每個檔案只得到一個標籤和一個值,所以 Loki 現在將儲存兩個資料流。

我們可以用幾種方式查詢這些流:

{job=”apache”} <- 顯示標籤 job 是 apache 的日誌
{job=”syslog”} <- 顯示標籤 job 是 syslog 的日誌
{job=~”apache|syslog”} <- 顯示標籤 job 是 apache **或** syslog 的日誌

在最後一個例子中,我們使用了一個 regex 標籤匹配器來記錄使用標籤 job 的兩個值的流。現在考慮一下如何也使用一個額外的標籤:

scrape_configs:
 - job_name: system
   pipeline_stages:
   static_configs:
   - targets:
      - localhost
     labels:
      job: syslog
      env: dev
      __path__: /var/log/syslog
 - job_name: system
   pipeline_stages:
   static_configs:
   - targets:
      - localhost
     labels:
      job: apache
      env: dev
      __path__: /var/log/apache.log

現在我們可以這樣做,而不是使用正規表示式:

{env=”dev”} <- 返回 env=dev 的所有日誌,本例中包括兩個日誌流

希望你現在開始看到標籤的力量。透過使用一個標籤,你可以查詢許多資料流。透過結合幾個不同的標籤,你可以建立非常靈活的日誌查詢。

標籤是 Loki 的日誌資料的索引。它們被用來尋找壓縮的日誌內容,這些內容以塊形式單獨儲存。每個獨特的標籤和值的組合都定義了一個流,一個流的日誌被分批壓縮,並作為塊儲存。

為了使 Loki 的效率和成本效益,我們必須負責任地使用標籤。下一節將更詳細地探討這個問題。

基數 (Cardinality)

前面的兩個例子使用的是靜態定義的標籤,只有一個值;但是,有一些方法可以動態地定義標籤。讓我們用 Apache 的日誌和你可以用來解析這樣的日誌行的大量的重合詞來看看。

11.11.11.11 - frank [25/Jan/2000:14:00:01 -0500] "GET /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
- job_name: system
   pipeline_stages:
      - regex:
        expression: "^(?P<ip>\\S+) (?P<identd>\\S+) (?P<user>\\S+) \\[(?P<timestamp>[\\w:/]+\\s[+\\-]\\d{4})\\] \"(?P<action>\\S+)\\s?(?P<path>\\S+)?\\s?(?P<protocol>\\S+)?\" (?P<status_code>\\d{3}|-) (?P<size>\\d+|-)\\s?\"?(?P<referer>[^\"]*)\"?\\s?\"?(?P<useragent>[^\"]*)?\"?$"
    - labels:
        action:
        status_code:
   static_configs:
   - targets:
      - localhost
     labels:
      job: apache
      env: dev
      __path__: /var/log/apache.log

這個片語匹配日誌行的每一個元件,並將每個元件的值提取到一個捕獲組中。在管道程式碼中,這些資料被放置在一個臨時資料結構中,允許在處理該日誌行時將其用於多種用途(此時,這些臨時資料被丟棄)。關於這一點的更多細節可以在 這裡 找到。

從該重合碼中,我們將使用兩個捕獲組,根據日誌行本身的內容動態地設定兩個標籤。

action(例如,action="GET",action="POST") status_code(例如, status_code="200", status_code="400")。

現在讓我們看幾個例子行:

11.11.11.11 - frank [25/Jan/2000:14:00:01 -0500] "GET /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
11.11.11.12 - frank [25/Jan/2000:14:00:02 -0500] "POST /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
11.11.11.13 - frank [25/Jan/2000:14:00:03 -0500] "GET /1986.js HTTP/1.1" 400 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
11.11.11.14 - frank [25/Jan/2000:14:00:04 -0500] "POST /1986.js HTTP/1.1" 400 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"

在 Loki 中,將建立以下資料流:

{job=”apache”,env=”dev”,action=”GET”,status_code=”200”} 11.11.11.11 - frank [25/Jan/2000:14:00:01 -0500] "GET /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
{job=”apache”,env=”dev”,action=”POST”,status_code=”200”} 11.11.11.12 - frank [25/Jan/2000:14:00:02 -0500] "POST /1986.js HTTP/1.1" 200 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
{job=”apache”,env=”dev”,action=”GET”,status_code=”400”} 11.11.11.13 - frank [25/Jan/2000:14:00:03 -0500] "GET /1986.js HTTP/1.1" 400 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"
{job=”apache”,env=”dev”,action=”POST”,status_code=”400”} 11.11.11.14 - frank [25/Jan/2000:14:00:04 -0500] "POST /1986.js HTTP/1.1" 400 932 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 GTB6"

這四條日誌行將成為四個獨立的流,並開始填充四個獨立的塊。

任何符合這些標籤/值組合的額外日誌行將被新增到現有的流中。如果有另一個獨特的標籤組合進來(例如 status_code="500"),就會建立另一個新的流。

現在想象一下,如果你為 ip 設定一個標籤。不僅每個來自使用者的請求都成為一個獨特的流。每個來自同一使用者的具有不同動作或狀態程式碼的請求都將得到它自己的流。

做一些簡單的計算,如果有四個常見的動作(GET, PUT, POST, DELETE)和四個常見的狀態程式碼(雖然可能不止四個!),這將是 16 個流和 16 個獨立的塊。現在,如果我們用一個標籤來表示 ip,就把這個數字乘以每個使用者。你可以很快有幾千或幾萬個流。

這會導致很高的 cardinality。會殺死 Loki。

當我們談論 cardinality 時,我們指的是標籤和值的組合以及它們創造的流的數量。高 cardinality 是指使用具有大範圍可能值的標籤,如 ip,或結合許多標籤,即使它們有一個小而有限的值集,如使用 status_code 和 action。

高 cardinality 導致 Loki 建立一個巨大的索引(讀作:????),並將成千上萬的小塊衝到物件儲存中(讀作:慢)。目前,Loki 在這種配置下表現很差,執行和使用起來將是最不划算和最沒有樂趣的。

使用並行化 (parallelization) 的最佳 Loki 效能

現在你可能會問:如果使用大量的標籤或有大量數值的標籤是不好的,那麼我應該如何查詢我的日誌呢?如果沒有一個資料是有索引的,那查詢豈不是很慢?

當我們看到使用 Loki 的人習慣於使用其他索引重複的解決方案時,他們似乎覺得有義務定義大量的標籤,以便有效地查詢他們的日誌。畢竟,許多其他的日誌解決方案都是關於索引的,這也是常見的思維方式。

在使用 Loki 時,你可能需要忘記你所知道的東西,看看如何用並行化的方式來解決這個問題。Loki 的超能力是將查詢分解成小塊,並將其並行排程,這樣你就可以在小時間內查詢大量的日誌資料。

這種粗暴的方法聽起來可能並不理想,但讓我解釋一下為什麼會這樣。

大型索引是複雜而昂貴的。通常情況下,你的日誌資料的全文索引與日誌資料本身的大小相同或更大。為了查詢你的日誌資料,你需要載入這個索引,而且為了效能,它可能應該在記憶體中。這是很難擴充套件的,當你攝入更多的日誌時,你的索引會很快變大。

現在讓我們來談談 Loki,它的索引通常比你攝入的日誌量小一個數量級。因此,如果你能很好地保持你的資料流和資料流的流失,那麼與攝取的日誌相比,索引的增長非常緩慢。

Loki 將有效地保持你的靜態成本儘可能低(索引大小和記憶體要求以及靜態日誌儲存),並使查詢效能成為你可以在執行時控制的水平擴充套件。

為了瞭解這一點,讓我們回過頭來看看我們查詢特定 IP 地址的訪問日誌資料的例子。我們不想用一個標籤來儲存 IP。相反,我們使用一個過濾器表示式來查詢它。

{job=”apache”} |= “11.11.11.11”

在幕後,Loki 會將該查詢分解成更小的片段(分片),併為標籤所匹配的流開啟每個分片,開始尋找這個 IP 地址。

這些分片的大小和並行化的數量是可配置的,並基於你提供的資源。如果你願意,你可以把分片的間隔配置到 5m,部署 20 個查詢器,在幾秒鐘內處理幾十億位元組的日誌。或者你可以瘋狂地配置 200 個查詢器,處理 TB 級的日誌。

這種較小的索引和平行的暴力查詢與較大/較快的全文索引之間的權衡,使得 Loki 能夠比其他系統節省成本。操作大型索引的成本和複雜性很高,而且通常是固定的--無論你是否查詢它,你都要一天 24 小時為它付費。

這種設計的好處是,你可以決定你想擁有多少查詢能力,而且你可以按需改變。查詢效能成為你想在上面花多少錢的一個函式。同時,資料被大量壓縮並儲存在低成本的物件儲存中,如 S3 和 GCS。這使固定的運營成本降到最低,同時還能實現令人難以置信的快速查詢能力

最佳實踐

這裡有一些 Loki 目前最有效的標籤做法,可以給你帶來 Loki 的最佳體驗。

1. 推薦靜態標籤

像主機、應用程式和環境這些東西是很好的標籤。它們對於一個給定的系統/應用程式來說是固定的,並且有限定的值。使用靜態標籤可以使你更容易在邏輯上查詢你的日誌(例如,給我看一個給定的應用程式和特定環境的所有日誌,或者給我看一個特定主機上的所有應用程式的所有日誌)。

2. 謹慎使用動態標籤

太多的標籤值組合會導致太多的資料流。在 Loki 中,這樣做的懲罰是一個大索引和儲存中的小塊,這反過來又會降低效能。

為了避免這些問題,在你知道你需要它之前,不要為某樣東西新增標籤。使用過濾表示式 ( |= "text", |~ "regex", ...) 並對這些日誌進行暴力處理。這很有效--而且速度很快。

從早期開始,我們就使用 promtail 管道為level動態地設定了一個標籤。這對我們來說似乎很直觀,因為我們經常想只顯示level="error"的日誌;然而,我們現在正在重新評估這一點,因為寫一個查詢。{app="loki"} |= "level=error"對我們的許多應用來說,證明與{app="loki",level="error"}一樣快。

這似乎令人驚訝,但如果應用程式有中等至低容量,該標籤導致一個應用程式的日誌被分成多達五個流,這意味著 5 倍的塊被儲存。而載入塊有一個與之相關的開銷。想象一下,如果這個查詢是{app="loki",level!="debug"}。這將不得不比{app="loki"} != "level=debug"}載入多的多資料塊。

上面,我們提到在你需要它們之前不要新增標籤,那麼你什麼時候會需要標籤呢?再往下一點是關於 chunk_target_size 的部分。如果你把這個設定為 1MB(這是合理的),這將試圖以 1MB 的壓縮大小來切割塊,這大約是 5MB 左右的未壓縮的日誌(可能多達 10MB,取決於壓縮)。如果你的日誌有足夠的容量在比max_chunk_age更短的時間內寫入 5MB,或者在這個時間範圍內有多的多的塊,你可能要考慮用動態標籤把它分成獨立的流。

你想避免的是將一個日誌檔案分割成流,這將導致塊被重新整理,因為流是空閒的或在滿之前達到最大年齡。從 Loki 1.4.0 開始,有一個指標可以幫助你瞭解為什麼要重新整理資料塊sum by (reason) (rate(loki_ingester_chunks_flushed_total{cluster="dev"}[1m]))

每個塊在重新整理時都是滿的,這並不關鍵,但它將改善許多方面的操作。因此,我們目前的指導思想是儘可能避免動態標籤,而傾向於過濾器表示式。例如,不要新增 level 的動態標籤,而用|= "level=debug"代替。

3. 標籤值必須始終是有界的

如果你要動態地設定標籤,千萬不要使用可以有無界值或無限值的標籤。這總是會給 Loki 帶來大問題。

儘量將值限制在儘可能小的範圍內。我們對 Loki 能處理的數值沒有完美的指導,但對於動態標籤來說,要考慮個位數,或者10 個數值。這對靜態標籤來說就不那麼重要了。例如,如果你的環境中有 1,000 臺主機,那麼有 1,000 個值的主機標籤就會很好。

4. 注意客戶端的動態標籤

Loki 有幾個客戶端選項。Promtail(也支援 systemd 日誌攝取和基於 TCP 的系統日誌攝取),FluentDFluent Bit,一個 Docker 外掛,以及更多!

每一個都有方法來配置用什麼標籤來建立日誌流。但要注意可能會用哪些動態標籤。使用 Loki 系列 API 來瞭解你的日誌流是什麼樣子的,看看是否有辦法減少流和 cardinality。系列 API 的細節可以在 這裡 找到,或者你可以使用 logcli 來查詢 Loki 的系列資訊。

5. 配置快取

Loki 可以對資料進行多層次的快取,這可以極大地提高效能。這方面的細節將在今後的文章中介紹。

6. 每條流的日誌必須按時間順序遞增(新版本預設接受無序日誌)

?Notes:

新版本預設接受無序日誌

許多人在使用 Loki 時遇到的一個問題是,他們的客戶端收到了錯誤的日誌條目。這是因為 Loki 內部有一條硬性規定。

  • 對於任何單一的日誌流,日誌必須總是以遞增的時間順序傳送。如果收到的日誌的時間戳比該流收到的最新日誌的時間戳大,該日誌將被放棄。

從這個宣告中,有幾件事需要剖析。首先,這個限制是針對每個流的。讓我們看一個例子:

{job=”syslog”} 00:00:00 i’m a syslog!
{job=”syslog”} 00:00:01 i’m a syslog!

如果 Loki 收到這兩行是針對同一流的,那麼一切都會好起來。但這種情況呢?

{job=”syslog”} 00:00:00 i’m a syslog!
{job=”syslog”} 00:00:02 i’m a syslog!
{job=”syslog”} 00:00:01 i’m a syslog!  <- 拒絕不符合順序的!

嗯,額。..... 但我們能做些什麼呢?如果這是因為這些日誌的來源是不同的系統呢?我們可以用一個額外的標籤來解決這個問題,這個標籤在每個系統中是唯一的。

{job=”syslog”, instance=”host1”} 00:00:00 i’m a syslog!
{job=”syslog”, instance=”host1”} 00:00:02 i’m a syslog!
{job=”syslog”, instance=”host2”} 00:00:01 i’m a syslog!  <- 被接受,這是一個新的流!
{job=”syslog”, instance=”host1”} 00:00:03 i’m a syslog!  <- 被接受,流 1 仍是有序的
{job=”syslog”, instance=”host2”} 00:00:02 i’m a syslog!  <- 被接受,流 2 仍是有序的

但是,如果應用程式本身產生的日誌是不正常的呢?嗯,這恐怕是個問題。如果你用類似 promtail 管道階段的東西從日誌行中提取時間戳,你反而可以不這樣做,讓 Promtail 給日誌行分配一個時間戳。或者你可以希望在應用程式本身中修復它。

但是我想讓 Loki 來解決這個問題!為什麼你不能為我緩衝資料流並重新排序?說實話,因為這將給 Loki 增加大量的記憶體開銷和複雜性,而正如這篇文章中的一個共同點,我們希望 Loki 簡單而經濟。理想情況下,我們希望改進我們的客戶端來做一些基本的緩衝和排序,因為這似乎是解決這個問題的一個更好的地方。

另外值得注意的是,Loki 推送 API 的批處理性質可能會導致收到一些順序錯誤的情況,這其實是誤報。(也許一個批處理部分成功了,並出現了;或者任何以前成功的東西都會返回一個失序的條目;或者任何新的東西都會被接受)。

7. 使用 chunk_target_size

這是在 2020 年早些時候我們 釋出 Loki v1.3.0 時新增的,我們已經用它實驗了幾個月。現在我們在所有的環境中都有chunk_target_size: 1536000。這指示 Loki 嘗試將所有的 chunks 填充到 1.5MB 的目標壓縮大小。這些較大的塊對 Loki 來說是更有效的處理。

其他幾個配置變數會影響到一個塊的大小。Loki 預設的 max_chunk_age 為 1 小時,chunk_idle_period 為 30 分鐘,以限制所使用的記憶體量,以及在程式崩潰時丟失日誌的風險。

根據使用的壓縮方式(我們一直使用 snappy,它的可壓縮性較低,但效能較快),你需要 5-10 倍或 7.5-10MB 的原始日誌資料來填充 1.5MB 的塊。記住,一個塊是每一個流,你把你的日誌檔案分成的流越多,在記憶體中的塊就越多,在它們被填滿之前,它們被擊中上述的超時的可能性就越大。

很多小的、未填充的塊目前是 Loki 的頑石。我們一直在努力改善這一點,並可能考慮在某些情況下使用壓縮器來改善這一點。但是,一般來說,指導原則應該保持不變:盡力填充塊。

如果你有一個應用程式,它的記錄速度足以迅速填滿這些塊(遠遠小於max_chunk_age),那麼使用動態標籤將其分解成獨立的資料流就變得更加合理。

總結

我最後再強調一次這個死馬當活馬醫的主意吧!

為了效能而使用並行化,而不是標籤和索引

對標籤要嚴格要求。靜態標籤通常是好的,但動態標籤應該少用。(如果你的日誌流以每分鐘 5-10MB 的速度寫入,那麼考慮一個動態標籤如何將其分成兩到三個流,這可以提高查詢效能。如果你的量比較少,堅持使用 過濾表示式

索引不一定是 Loki 的效能之路!首先要優先考慮並行化和 LogQL 查詢過濾。

請記住:與其他日誌儲存解決方案相比,Loki 需要一種不同的思維方式。我們正在對 Loki 進行最佳化,以獲得更少的資料流和更小的索引,這有助於填充更大的塊,更容易透過並行化進行查詢。

我們正在積極改進 Loki,並研究如何做到這一點。請務必繼續關注 Loki 故事的展開,我們都在琢磨如何將這個真正有效的工具發揮到極致!

Grafana 系列文章

Grafana 系列文章

三人行, 必有我師; 知識共享, 天下為公. 本文由東風微鳴技術部落格 EWhisper.cn 編寫.

相關文章