文章轉載參考自:https://jishuin.proginn.com/p/763bfbd2ac34
Loki 是 Grafana Labs 團隊最新的開源項目,是一個水平可擴展,高可用性,多租戶的日志聚合系統。它的設計非常經濟高效且易於操作,因為它不會為日志內容編制索引,而是為每個日志流配置一組標簽。項目受 Prometheus 啟發,官方的介紹就是:Like Prometheus, but for logs,類似於 Prometheus 的日志系統。
1. 概述
Loki 只會對你的日志元數據標簽(就像 Prometheus 的標簽一樣)進行索引,而不會對原始的日志數據進行全文索引。然后日志數據本身會被壓縮,並以 chunks(塊)的形式存儲在對象存儲(比如 S3 或者 GCS)甚至本地文件系統。一個小的索引和高度壓縮的 chunks 可以大大簡化操作和降低 Loki 的使用成本。
1.1 多租戶
Loki 支持多租戶模式,租戶之間的數據是完全分開的。多租戶是通過一個租戶 ID(用數字字母生成的字符串)實現的。當多租戶模式被禁用后,所有請求都會在內部生成一個**假的**
租戶 ID。
1.2 操作模式
Loki 可以在本地小規模運行也可以橫向擴展。Loki 自帶單進程模式,可以在一個進程中運行所有需要的微服務。單進程模式非常適合於測試 Loki 或者小規模運行。對於橫向擴展來說,Loki 的微服務是可以被分解成單獨的進程的,使其能夠獨立擴展。
1.3 組件
Distributor(分配器)
分配器服務負責處理客戶端寫入的日志。本質上它是日志數據寫入路徑中的第一站
。一旦分配器接收到日志數據,它就會把它們分成若干批次,並將它們並行地發送到多個采集器去。
分配器通過 gPRC 和采集器進行通信。它們是無狀態的,所以我們可以根據實際需要對他們進行擴縮容。
Hashing
分配器采用一致性哈希和可配置的復制因子結合使用,來確定哪些采集器服務應該接收日志數據。該哈希是基於日志標簽和租戶 ID 生成的。
存儲在 Consul 中的哈希環被用來實現一致性哈希;所有的采集器將他們自己的一組 Token 注冊到哈希環中去。然后分配器找到和日志的哈希值最匹配的 Token,並將數據發送給該 Token 的持有者。
一致性
由於所有的分配器都共享同一個哈希環,所以可以向任何分配器發送寫請求。
為了確保查詢結果的一致性,Loki 在讀和寫上使用了 Dynamo 方式的法定人數一致性。這意味着分配器將等待至少有一半以上的采集器響應,再向用戶發送樣本,然后再響應給用戶。
Ingester(采集器)
采集器服務負責將日志數據寫入長期存儲的后端(DynamoDB、S3、Cassandra 等等)。
采集器會校驗采集的日志是否亂序。當采集器接收到的日志行與預期的順序不一致時,該行日志將被拒絕,並向用戶返回一個錯誤。有關更多相關信息,可以查看時間戳排序部分內容。
采集器驗證接收到的日志行是按照時間戳遞增的順序接收的(即每條日志的時間戳都比之前的日志晚)。當采集器接收到的日志不按照這個順序,日志行將被拒絕並返回錯誤。
每一個唯一的標簽集數據都會在內存中構建成chunks
,然后將它們存儲到后端存儲中去。
如果一個采集器進程崩潰或者突然掛掉了,所有還沒有被刷新到存儲的數據就會丟失。Loki 通常配置成多個副本(通常為3個)來降低這種風險。
時間戳排序
一般來說推送到 Loki 的所有日志行必須比之前收到的行有一個更新的時間戳。然而有些情況可能是多行日志具有相同的納秒級別的時間戳,可以按照下面兩種情況進行處理:
-
如果傳入的行和之前接收到的行完全匹配(時間戳和日志文本都匹配),則傳入的行會被視為完全重復並會被忽略。
-
如果傳入行的時間戳和前面一行的時間戳相同,但是日志內容不相同,則會接收該行日志。這就意味着,對於相同的時間戳,有可能有兩個不同的日志行。
Handoff(交接)
默認情況下,當一個采集器關閉並視圖離開哈希環時,它將等待查看是否有新的采集器視圖進入,然后再進行 flush,並嘗試啟動交接。交接將把離開的采集器擁有的所有 Token 和內存中的 chunks 都轉移到新的采集器中來。
這個過程是為了避免在關閉時 flush 所有的 chunks,因為這是一個比較緩慢的過程,比較耗時。
文件系統支持
采集器支持通過 BoltDB 寫入到文件系統,但這只在單進程模式下工作,因為查詢器需要訪問相同的后端存儲,而且 BoltDB 只允許一個進程在給定時間內對 DB 進行鎖定。
Querier(查詢器)
查詢器服務負責處理 LogQL 查詢語句來評估存儲在長期存儲中的日志數據。
它首先會嘗試查詢所有采集器的內存數據,然后再返回到后端存儲中加載數據。
前端查詢
該服務是一個可選組件,在一組查詢器前面,來負責在它們之間公平地調度請求,盡可能地並行化它們並緩存請求。
Chunk(塊)存儲
塊存儲是 Loki 的長期數據存儲,旨在支持交互式查詢和持續寫入,無需后台維護任務。它由以下幾部分組成:
-
塊索引,該索引可以由 DynamoDB、Bigtable 或者 Cassandra 來支持。
-
塊數據本身的 KV 存儲,可以是 DynamoDB、Bigtable、Cassandra,也可以上是對象存儲,比如 S3。
與 Loki 的其他核心組件不同,塊存儲不是一個獨立的服務、任務或者進程,而是嵌入到需要訪問 Loki 數據的采集器和查詢器中的庫。
塊存儲依賴統一的 ”NoSQL“ 存儲(DynamoDB、Bigtable 和 Cassandra)接口,該接口可以用來支持塊存儲索引。該接口假設索引是由以下幾個 key 構成的集合:
-
哈希 KEY - 這是所有的讀和寫都需要的。
-
范圍 KEY - 這是寫的時候需要的,讀的時候可以省略,可以通過前綴或者范圍來查詢。
上面支持的這些數據庫中接口的工作原理有些不同:
-
DynamoDB 支持范圍和哈希 KEY。所以索引條目直接建模為 DynamoDB 的數據,哈希 KEY 為分布式 KEY,范圍為范圍 KEY。
-
對於 Bigtable 和 Cassandra,索引項被建模為單個的列值。哈希 KEY 成為行 KEY,范圍 KEY 成為列 KEY。
一些模式被用於對塊存儲的讀取和寫入時使用的匹配器和標簽集合映射到索引的適當操作中來。隨着 Loki 的發展也會增加一些新的模式,主要是為了更好地平衡些和提高查詢性能。
1.4 對比其他日志系統
EFK(Elasticsearch、Fluentd、Kibana)用於從各種來源獲取、可視化和查詢日志。
Elasticsearch 中的數據以非結構化 JSON 對象的形式存儲在磁盤上。每個對象的鍵和每個鍵的內容都有索引。然后可以使用 JSON 對象來定義查詢(稱為 Query DSL)或通過 Lucene 查詢語言來查詢數據。
相比之下,單二進制模式下的 Loki 可以將數據存儲在磁盤上,但在水平可擴展模式下,數據存儲需要在雲存儲系統中,如 S3、GCS 或 Cassandra。日志以純文本的形式存儲,並標記了一組標簽的名稱和值,其中只有標簽會被索引。這種權衡使其操作起來比完全索引更便宜。Loki 中的日志使用 LogQL 進行查詢。由於這種設計上的權衡,根據內容(即日志行內的文本)進行過濾的 LogQL 查詢需要加載搜索窗口內所有與查詢中定義的標簽相匹配的塊。
Fluentd 通常用於收集日志並轉發到 Elasticsearch。Fluentd 被稱為數據收集器,它可以從許多來源采集日志,並對其進行處理,然后轉發到一個或多個目標。
相比之下,Promtail 是為 Loki 量身定做的。它的主要工作模式是發現存儲在磁盤上的日志文件,並將其與一組標簽關聯的日志文件轉發到 Loki。Promtail 可以為在同一節點上運行的 Kubernetes Pods 做服務發現,作為 Docker 日志驅動,從指定的文件夾中讀取日志,並對 systemd 日志不斷獲取。
Loki 通過一組標簽表示日志的方式與 Prometheus 表示指標的方式類似。當與Prometheus 一起部署在環境中時,由於使用了相同的服務發現機制,來自Promtail 的日志通常與你的應用指標具有相同的標簽。擁有相同級別的日志和指標,用戶可以在指標和日志之間無縫切換,幫助進行根本性原因分析。
Kibana 被用於可視化和搜索 Elasticsearch 數據,並且在對這些數據進行分析時非常強大。Kibana 提供了許多可視化工具來做數據分析,例如地圖、用於異常檢測的機器學習,以及關系圖。也可以配置報警,當出現意外情況時,可以通知用戶。
相比之下,Grafana 是專門針對 Prometheus 和 Loki 等數據源的時間序列數據定制的。儀表板可以設置為可視化指標(即將推出的日志支持),也可以使用探索視圖對數據進行臨時查詢。和 Kibana 一樣,Grafana 也支持根據你的指標進行報警。
2 開始使用 Loki
2.1 Loki 在 Grafana 中的配置
Grafana 在 6.0 以上的版本中內置了對 Loki 的支持。建議使用 6.3 或更高版本,就可以使用新的LogQL功能。
-
登錄 Grafana 實例,如果這是你第一次運行 Grafana,用戶名和密碼都默認為
admin
。 -
在 Grafana 中,通過左側側邊欄上的圖標轉到 "
配置 > 數據源
"。 -
單擊
+ Add data source
按鈕。 -
在列表中選擇 Loki。
-
Http URL 字段是你的 Loki 服務器的地址,例如,在本地運行或使用端口映射的 Docker 運行時,地址可能是 http://localhost:3100。使用 docker-compose 或 Kubernetes 運行時,地址很可能是 https://loki:3100。
-
要查看日志,可以單擊側邊欄上的 "
探索
",在左上角下拉菜單中選擇 Loki 數據源,然后使用日志標簽按鈕過濾日志流。
3.2 Label 標簽
Label 標簽是一個鍵值對,可以定義任何東西,我們喜歡稱它們為描述日志流的元數據。如果你熟悉 Prometheus,那么一定對 Label 標簽有一定的了解,在 Loki 的 scrape 配置中也定義了這些標簽,和 Prometheus 擁有一致的功能,這些標簽非常容易將應用程序指標和日志數據關聯起來。
Loki 中的標簽執行一個非常重要的任務:它們定義了一個流。更具體地說,每個標簽鍵和值的組合定義了流。如果只是一個標簽值變化,這將創建一個新的流。
如果你熟悉 Prometheus,那里的術語叫序列,而且 Prometheus 中還有一個額外的維度:指標名稱。Loki 中簡化了這一點,因為沒有指標名,只有標簽,所以最后決定使用流而不是序列。
標簽示例
下面的示例將說明 Loki 中 Label 標簽的基本使用和概念。
首先看下下面的示例:
scrape_configs:
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: syslog
__path__: /var/log/syslog
這個配置將獲取日志文件數據並添加一個 job=syslog
的標簽,我們可以這樣來查詢:
{job="syslog"}
這將在 Loki 中創建一個流。現在我們再新增一些任務配置:
scrape_configs:
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: syslog
__path__: /var/log/syslog
- job_name: system
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 標簽值為 apache 或者 syslog 的日志。接下來我們看看如何使用額外的標簽:
scrape_configs:
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: syslog
env: dev
__path__: /var/log/syslog
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: apache
env: dev
__path__: /var/log/apache.log
要獲取這兩個任務的日志可以用下面的方式來代替 regex 的方式:
{env="dev"} -> 將返回所有帶有 env=dev 標簽的日志
通過使用一個標簽就可以查詢很多日志流了,通過組合多個不同的標簽,可以創建非常靈活的日志查詢。
Label 標簽是 Loki 日志數據的索引,它們用於查找壓縮后的日志內容,這些內容被單獨存儲為塊
。標簽和值的每一個唯一組合都定義了一個流
,一個流的日志被分批,壓縮,並作為塊進行存儲。
Cardinality(勢)
注:這個尚未實踐
前面的示例使用的是靜態定義的 Label 標簽,只有一個值;但是有一些方法可以動態定義標簽。比如我們有下面這樣的日志數據:
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
這個 regex 匹配日志行的每個組件,並將每個組件的值提取到一個 capture 組里面。在 pipeline 代碼內部,這些數據被放置到一個臨時的數據結構中,允許在處理該日志行時將其用於其他處理(此時,臨時數據將被丟棄)。
從該 regex 中,我們就使用其中的兩個 capture 組,根據日志行本身的內容動態地設置兩個標簽:
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"
這4行日志將成為4個獨立的流,並開始填充4個獨立的塊。任何與這些 標簽/值
組合相匹配的額外日志行將被添加到現有的流中。如果有另一個獨特的標簽組合進來(比如 status_code="500")就會創建另一個新的流。
比如我們為 IP 設置一個 Label 標簽,不僅用戶的每一個請求都會變成一個唯一的流,每一個來自同一用戶的不同 action 或 status_code 的請求都會得到自己的流。
如果有4個共同的操作(GET、PUT、POST、DELETE)和4個共同的狀態碼(可能不止4個!),這將會是16個流和16個獨立的塊。然后現在乘以每個用戶,如果我們使用 IP 的標簽,你將很快就會有數千或數萬個流了。
這個 Cardinality 太高了,這足以讓 Loki 掛掉。
當我們談論 Cardinality 的時候,我們指的是標簽和值的組合,以及他們創建的流的數量,高 Cardinality 是指使用具有較大范圍的可能值的標簽,如 IP,或結合需要其他標簽,即使它們有一個小而有限的集合,比如 status_code 和 action。
高 Cardinality 會導致 Loki 建立一個巨大的索引(????),並將成千上萬的微小塊存入對象存儲中(慢),Loki 目前在這種配置下的性能非常差,運行和使用起來非常不划算的。
Loki 性能優化
現在我們知道了如果使用大量的標簽或有大量值的標簽是不好的,那我應該如何查詢我的日志呢?如果沒有一個數據是有索引的,那么查詢不會真的很慢嗎?
我們看到使用 Loki 的人習慣了其他重索引的解決方案,他們就覺得需要定義很多標簽,才可以有效地查詢日志,畢竟很多其他的日志解決方案都是為了索引,這是之前的慣性思維方式。
在使用 Loki 的時候,你可能需要忘記你所知道的東西,看看如何用並行化
的方式來解決這個問題。Loki 的超強之處在於將查詢拆成小塊,並行調度,這樣你就可以在少量時間內查詢大量的日志數據了。
大型索引是非常復雜而昂貴的,通常情況下,你的日志數據的全文索引與日志數據本身的大小相當或更大。要查詢你的日志數據,需要加載這個索引,為了性能,可能在內存中,這就非常難擴展了,當你采集了大量的日志時,你的索引就會變得很大。
現在我們來談談 Loki,索引通常比你采集的日志量小一個數量級。所以,如果你很好地將你的流保持在最低限度,那么指數的增長和采集的日志相比就非常緩慢了。
Loki 將有效地使你的靜態成本盡可能低(索引大小和內存需求以及靜態日志存儲),並使查詢性能可以在運行時通過水平伸縮進行控制。
為了了解是如何工作的,讓我們回過頭來看看上面我們查詢訪問日志數據的特定 IP 地址的例子,我們不使用標簽來存儲 IP,相反,我們使用一個過濾器表達式來查詢它。
{job="apache"} |= "11.11.11.11"
在背后 Loki 會將該查詢分解成更小的碎片(shards),並為標簽匹配的流打開每個塊(chunk),並開始查找這個 IP 地址。
這些碎片的大小和並行化的數量是可配置的,並基於你提供的資源。如果你願意,可以將 shard 間隔配置到 5m,部署20個查詢器,並在幾秒內處理千兆字節的日志。或者你可以更加瘋狂地配置200個查詢器,處理 TB 級別的日志!
這種較小的索引和並行查詢與較大/較快的全文索引之間的權衡,是讓 Loki 相對於其他系統節省成本的原因。操作大型索引的成本和復雜度很高,而且通常是固定的,無論是是否在查詢它,你都要一天24小時為它付費。
這種設計的好處是,你可以決定你想擁有多大的查詢能力,而且你可以按需變更。查詢性能成為你想花多少錢的函數。同時數據被大量壓縮並存儲在低成本的對象存儲中,比如 S3 和 GCS。這就將固定的運營成本降到了最低,同時還能提供難以置信的快速查詢能力。