???URL: https://grafana.com/blog/2020/04/21/how-labels-in-loki-can-make-log-queries-faster-and-easier/
??Description:
關于標簽在 Loki 中如何真正發(fā)揮作用,你需要知道的一切。它可能與你想象的不同
在我們從事 Loki 項目的第一年的大部分時間里,問題和反饋似乎都來自熟悉 Prometheus 的人。畢竟,Loki 就像 Prometheus--不過是針對日志的!"。
但是最近,我們看到越來越多的人嘗試使用 Loki,他們沒有 Prometheus 的經(jīng)驗,而且許多人來自于具有不同策略的系統(tǒng),以處理日志。這就帶來了很多關于 Loki 一個非常重要的概念的問題,即使是 Prometheus 專家也想了解更多:標簽 (Labels)!
這篇文章將涵蓋很多內(nèi)容,以幫助每一個剛接觸 Loki 的人和想要復習的人。我們將探討以下主題。
什么是標簽 (Label)?
標簽是鍵值對,可以被定義為任何東西!我們喜歡把它們稱為元數(shù)據(jù) (metadata),用來描述日志流。如果你熟悉 Prometheus,你會習慣性地看到一些標簽,比如job和instance,我將在接下來的例子中使用這些。
我們用 Loki 提供的刮削 (scrape) 配置也定義了這些標簽。如果你正在使用 Prometheus,在 Loki 和 Prometheus 之間擁有一致的標簽是 Loki 的超級優(yōu)勢之一,使你 非常容易將你的應用程序指標 (Metrics) 與你的日志 (Logs) 數(shù)據(jù)聯(lián)系起來。
Loki 如何使用標簽
Loki 中的標簽執(zhí)行一個非常重要的任務。它們定義了一個流。更確切地說,每個標簽的鍵和值的組合都定義了流。如果只有一個標簽值發(fā)生變化,就會產(chǎn)生一個新的流。
如果你熟悉 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 創(chuàng)建一個流。
現(xiàn)在讓我們把這個例子擴大一點:
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
現(xiàn)在我們正在跟蹤兩個文件。每個文件只得到一個標簽和一個值,所以 Loki 現(xiàn)在將存儲兩個數(shù)據(jù)流。
我們可以用幾種方式查詢這些流:
{job=”apache”} <- 顯示標簽 job 是 apache 的日志
{job=”syslog”} <- 顯示標簽 job 是 syslog 的日志
{job=~”apache|syslog”} <- 顯示標簽 job 是 apache **或** syslog 的日志
在最后一個例子中,我們使用了一個 regex 標簽匹配器來記錄使用標簽 job 的兩個值的流?,F(xiàn)在考慮一下如何也使用一個額外的標簽:
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
現(xiàn)在我們可以這樣做,而不是使用正則表達式:
{env=”dev”} <- 返回 env=dev 的所有日志,本例中包括兩個日志流
希望你現(xiàn)在開始看到標簽的力量。通過使用一個標簽,你可以查詢許多數(shù)據(jù)流。通過結(jié)合幾個不同的標簽,你可以創(chuàng)建非常靈活的日志查詢。
標簽是 Loki 的日志數(shù)據(jù)的索引。它們被用來尋找壓縮的日志內(nèi)容,這些內(nèi)容以塊形式單獨存儲。每個獨特的標簽和值的組合都定義了一個流,一個流的日志被分批壓縮,并作為塊存儲。
為了使 Loki 的效率和成本效益,我們必須負責任地使用標簽。下一節(jié)將更詳細地探討這個問題。
基數(shù) (Cardinality)
前面的兩個例子使用的是靜態(tài)定義的標簽,只有一個值;但是,有一些方法可以動態(tài)地定義標簽。讓我們用 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
這個詞組匹配日志行的每一個組件,并將每個組件的值提取到一個捕獲組中。在管道代碼中,這些數(shù)據(jù)被放置在一個臨時數(shù)據(jù)結(jié)構(gòu)中,允許在處理該日志行時將其用于多種用途(此時,這些臨時數(shù)據(jù)被丟棄)。關于這一點的更多細節(jié)可以在 這里 找到。
從該重合碼中,我們將使用兩個捕獲組,根據(jù)日志行本身的內(nèi)容動態(tài)地設置兩個標簽。
action(例如,action="GET",action="POST") status_code(例如, status_code="200", status_code="400")。
現(xiàn)在讓我們看幾個例子行:
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 中,將創(chuàng)建以下數(shù)據(jù)流:
{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"
這四條日志行將成為四個獨立的流,并開始填充四個獨立的塊。
任何符合這些標簽/值組合的額外日志行將被添加到現(xiàn)有的流中。如果有另一個獨特的標簽組合進來(例如 status_code="500"),就會創(chuàng)建另一個新的流。
現(xiàn)在想象一下,如果你為 ip 設置一個標簽。不僅每個來自用戶的請求都成為一個獨特的流。每個來自同一用戶的具有不同動作或狀態(tài)代碼的請求都將得到它自己的流。
做一些簡單的計算,如果有四個常見的動作(GET, PUT, POST, DELETE)和四個常見的狀態(tài)代碼(雖然可能不止四個?。?,這將是 16 個流和 16 個獨立的塊?,F(xiàn)在,如果我們用一個標簽來表示 ip,就把這個數(shù)字乘以每個用戶。你可以很快有幾千或幾萬個流。
這會導致很高的 cardinality。會殺死 Loki。
當我們談論 cardinality 時,我們指的是標簽和值的組合以及它們創(chuàng)造的流的數(shù)量。高 cardinality 是指使用具有大范圍可能值的標簽,如 ip,或結(jié)合許多標簽,即使它們有一個小而有限的值集,如使用 status_code 和 action。
高 cardinality 導致 Loki 建立一個巨大的索引(讀作:????????),并將成千上萬的小塊沖到對象存儲中(讀作:慢)。目前,Loki 在這種配置下表現(xiàn)很差,運行和使用起來將是最不劃算和最沒有樂趣的。
使用并行化 (parallelization) 的最佳 Loki 性能
現(xiàn)在你可能會問:如果使用大量的標簽或有大量數(shù)值的標簽是不好的,那么我應該如何查詢我的日志呢?如果沒有一個數(shù)據(jù)是有索引的,那查詢豈不是很慢?
當我們看到使用 Loki 的人習慣于使用其他索引重復的解決方案時,他們似乎覺得有義務定義大量的標簽,以便有效地查詢他們的日志。畢竟,許多其他的日志解決方案都是關于索引的,這也是常見的思維方式。
在使用 Loki 時,你可能需要忘記你所知道的東西,看看如何用并行化的方式來解決這個問題。Loki 的超能力是將查詢分解成小塊,并將其并行調(diào)度,這樣你就可以在小時間內(nèi)查詢大量的日志數(shù)據(jù)。
這種粗暴的方法聽起來可能并不理想,但讓我解釋一下為什么會這樣。
大型索引是復雜而昂貴的。通常情況下,你的日志數(shù)據(jù)的全文索引與日志數(shù)據(jù)本身的大小相同或更大。為了查詢你的日志數(shù)據(jù),你需要加載這個索引,而且為了性能,它可能應該在內(nèi)存中。這是很難擴展的,當你攝入更多的日志時,你的索引會很快變大。
現(xiàn)在讓我們來談談 Loki,它的索引通常比你攝入的日志量小一個數(shù)量級。因此,如果你能很好地保持你的數(shù)據(jù)流和數(shù)據(jù)流的流失,那么與攝取的日志相比,索引的增長非常緩慢。
Loki 將有效地保持你的靜態(tài)成本盡可能低(索引大小和內(nèi)存要求以及靜態(tài)日志存儲),并使查詢性能成為你可以在運行時控制的水平擴展。
為了了解這一點,讓我們回過頭來看看我們查詢特定 IP 地址的訪問日志數(shù)據(jù)的例子。我們不想用一個標簽來存儲 IP。相反,我們使用一個過濾器表達式來查詢它。
{job=”apache”} |= “11.11.11.11”
在幕后,Loki 會將該查詢分解成更小的片段(分片),并為標簽所匹配的流打開每個分片,開始尋找這個 IP 地址。
這些分片的大小和并行化的數(shù)量是可配置的,并基于你提供的資源。如果你愿意,你可以把分片的間隔配置到 5m,部署 20 個查詢器,在幾秒鐘內(nèi)處理幾十億字節(jié)的日志。或者你可以瘋狂地配置 200 個查詢器,處理 TB 級的日志。
這種較小的索引和平行的暴力查詢與較大/較快的全文索引之間的權衡,使得 Loki 能夠比其他系統(tǒng)節(jié)省成本。操作大型索引的成本和復雜性很高,而且通常是固定的--無論你是否查詢它,你都要一天 24 小時為它付費。
這種設計的好處是,你可以決定你想擁有多少查詢能力,而且你可以按需改變。查詢性能成為你想在上面花多少錢的一個函數(shù)。同時,數(shù)據(jù)被大量壓縮并存儲在低成本的對象存儲中,如 S3 和 GCS。這使固定的運營成本降到最低,同時還能實現(xiàn)令人難以置信的快速查詢能力
最佳實踐
這里有一些 Loki 目前最有效的標簽做法,可以給你帶來 Loki 的最佳體驗。
1. 推薦靜態(tài)標簽
像主機、應用程序和環(huán)境這些東西是很好的標簽。它們對于一個給定的系統(tǒng)/應用程序來說是固定的,并且有限定的值。使用靜態(tài)標簽可以使你更容易在邏輯上查詢你的日志(例如,給我看一個給定的應用程序和特定環(huán)境的所有日志,或者給我看一個特定主機上的所有應用程序的所有日志)。
2. 謹慎使用動態(tài)標簽
太多的標簽值組合會導致太多的數(shù)據(jù)流。在 Loki 中,這樣做的懲罰是一個大索引和存儲中的小塊,這反過來又會降低性能。
為了避免這些問題,在你知道你需要它之前,不要為某樣東西添加標簽。使用過濾表達式 ( |= "text", |~ "regex", ...) 并對這些日志進行暴力處理。這很有效--而且速度很快。
從早期開始,我們就使用 promtail 管道為level動態(tài)地設置了一個標簽。這對我們來說似乎很直觀,因為我們經(jīng)常想只顯示level="error"的日志;然而,我們現(xiàn)在正在重新評估這一點,因為寫一個查詢。{app="loki"} |= "level=error"對我們的許多應用來說,證明與{app="loki",level="error"}一樣快。
這似乎令人驚訝,但如果應用程序有中等至低容量,該標簽導致一個應用程序的日志被分成多達五個流,這意味著 5 倍的塊被存儲。而加載塊有一個與之相關的開銷。想象一下,如果這個查詢是{app="loki",level!="debug"}。這將不得不比{app="loki"} != "level=debug"}加載多的多數(shù)據(jù)塊。
上面,我們提到在你需要它們之前不要添加標簽,那么你什么時候會需要標簽呢?再往下一點是關于 chunk_target_size 的部分。如果你把這個設置為 1MB(這是合理的),這將試圖以 1MB 的壓縮大小來切割塊,這大約是 5MB 左右的未壓縮的日志(可能多達 10MB,取決于壓縮)。如果你的日志有足夠的容量在比max_chunk_age更短的時間內(nèi)寫入 5MB,或者在這個時間范圍內(nèi)有多的多的塊,你可能要考慮用動態(tài)標簽把它分成獨立的流。
你想避免的是將一個日志文件分割成流,這將導致塊被刷新,因為流是空閑的或在滿之前達到最大年齡。從 Loki 1.4.0 開始,有一個指標可以幫助你了解為什么要刷新數(shù)據(jù)塊sum by (reason) (rate(loki_ingester_chunks_flushed_total{cluster="dev"}[1m]))。
每個塊在刷新時都是滿的,這并不關鍵,但它將改善許多方面的操作。因此,我們目前的指導思想是盡可能避免動態(tài)標簽,而傾向于過濾器表達式。例如,不要添加 level 的動態(tài)標簽,而用|= "level=debug"代替。
3. 標簽值必須始終是有界的
如果你要動態(tài)地設置標簽,千萬不要使用可以有無界值或無限值的標簽。這總是會給 Loki 帶來大問題。
盡量將值限制在盡可能小的范圍內(nèi)。我們對 Loki 能處理的數(shù)值沒有完美的指導,但對于動態(tài)標簽來說,要考慮個位數(shù),或者10 個數(shù)值。這對靜態(tài)標簽來說就不那么重要了。例如,如果你的環(huán)境中有 1,000 臺主機,那么有 1,000 個值的主機標簽就會很好。
4. 注意客戶端的動態(tài)標簽
Loki 有幾個客戶端選項。Promtail(也支持 systemd 日志攝取和基于 TCP 的系統(tǒng)日志攝取),FluentD,Fluent Bit,一個 Docker 插件,以及更多!
每一個都有方法來配置用什么標簽來創(chuàng)建日志流。但要注意可能會用哪些動態(tài)標簽。使用 Loki 系列 API 來了解你的日志流是什么樣子的,看看是否有辦法減少流和 cardinality。系列 API 的細節(jié)可以在 這里 找到,或者你可以使用 logcli 來查詢 Loki 的系列信息。
5. 配置緩存
Loki 可以對數(shù)據(jù)進行多層次的緩存,這可以極大地提高性能。這方面的細節(jié)將在今后的文章中介紹。
6. 每條流的日志必須按時間順序遞增(新版本默認接受無序日志)
??Notes:
許多人在使用 Loki 時遇到的一個問題是,他們的客戶端收到了錯誤的日志條目。這是因為 Loki 內(nèi)部有一條硬性規(guī)定。
- 對于任何單一的日志流,日志必須總是以遞增的時間順序發(fā)送。如果收到的日志的時間戳比該流收到的最新日志的時間戳大,該日志將被放棄。
從這個聲明中,有幾件事需要剖析。首先,這個限制是針對每個流的。讓我們看一個例子:
{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! <- 拒絕不符合順序的!
嗯,額。..... 但我們能做些什么呢?如果這是因為這些日志的來源是不同的系統(tǒng)呢?我們可以用一個額外的標簽來解決這個問題,這個標簽在每個系統(tǒng)中是唯一的。
{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 仍是有序的
但是,如果應用程序本身產(chǎn)生的日志是不正常的呢?嗯,這恐怕是個問題。如果你用類似 promtail 管道階段的東西從日志行中提取時間戳,你反而可以不這樣做,讓 Promtail 給日志行分配一個時間戳。或者你可以希望在應用程序本身中修復它。
但是我想讓 Loki 來解決這個問題!為什么你不能為我緩沖數(shù)據(jù)流并重新排序?說實話,因為這將給 Loki 增加大量的內(nèi)存開銷和復雜性,而正如這篇文章中的一個共同點,我們希望 Loki 簡單而經(jīng)濟。理想情況下,我們希望改進我們的客戶端來做一些基本的緩沖和排序,因為這似乎是解決這個問題的一個更好的地方。
另外值得注意的是,Loki 推送 API 的批處理性質(zhì)可能會導致收到一些順序錯誤的情況,這其實是誤報。(也許一個批處理部分成功了,并出現(xiàn)了;或者任何以前成功的東西都會返回一個失序的條目;或者任何新的東西都會被接受)。
7. 使用 chunk_target_size
這是在 2020 年早些時候我們 發(fā)布 Loki v1.3.0 時添加的,我們已經(jīng)用它實驗了幾個月?,F(xiàn)在我們在所有的環(huán)境中都有chunk_target_size: 1536000。這指示 Loki 嘗試將所有的 chunks 填充到 1.5MB 的目標壓縮大小。這些較大的塊對 Loki 來說是更有效的處理。
其他幾個配置變量會影響到一個塊的大小。Loki 默認的 max_chunk_age 為 1 小時,chunk_idle_period 為 30 分鐘,以限制所使用的內(nèi)存量,以及在進程崩潰時丟失日志的風險。
根據(jù)使用的壓縮方式(我們一直使用 snappy,它的可壓縮性較低,但性能較快),你需要 5-10 倍或 7.5-10MB 的原始日志數(shù)據(jù)來填充 1.5MB 的塊。記住,一個塊是每一個流,你把你的日志文件分成的流越多,在內(nèi)存中的塊就越多,在它們被填滿之前,它們被擊中上述的超時的可能性就越大。
很多小的、未填充的塊目前是 Loki 的頑石。我們一直在努力改善這一點,并可能考慮在某些情況下使用壓縮器來改善這一點。但是,一般來說,指導原則應該保持不變:盡力填充塊。
如果你有一個應用程序,它的記錄速度足以迅速填滿這些塊(遠遠小于max_chunk_age),那么使用動態(tài)標簽將其分解成獨立的數(shù)據(jù)流就變得更加合理。
總結(jié)
我最后再強調(diào)一次這個死馬當活馬醫(yī)的主意吧!
為了性能而使用并行化,而不是標簽和索引
對標簽要嚴格要求。靜態(tài)標簽通常是好的,但動態(tài)標簽應該少用。(如果你的日志流以每分鐘 5-10MB 的速度寫入,那么考慮一個動態(tài)標簽如何將其分成兩到三個流,這可以提高查詢性能。如果你的量比較少,堅持使用 過濾表達式。
索引不一定是 Loki 的性能之路!首先要優(yōu)先考慮并行化和 LogQL 查詢過濾。
請記住:與其他日志存儲解決方案相比,Loki 需要一種不同的思維方式。我們正在對 Loki 進行優(yōu)化,以獲得更少的數(shù)據(jù)流和更小的索引,這有助于填充更大的塊,更容易通過并行化進行查詢。
我們正在積極改進 Loki,并研究如何做到這一點。請務必繼續(xù)關注 Loki 故事的展開,我們都在琢磨如何將這個真正有效的工具發(fā)揮到極致!
Grafana 系列文章
三人行, 必有我?guī)? 知識共享, 天下為公. 本文由東風微鳴技術博客 EWhisper.cn 編寫.