從 Loki 收集日誌

從 Loki 收集日誌

Badoo 不斷監控新技術並評估是否在我們的系統中使用它們。 我們想與社區分享其中一項研究。 它專用於 Loki,一個日誌聚合系統。

Loki 是一個用於存儲和查看日誌的解決方案,該堆棧還提供了一個靈活的系統來分析日誌並將數據發送到 Prometheus。 XNUMX月份,又發布了一個更新,得到了創作者的積極推動。 我們感興趣的是 Loki 可以做什麼、它提供什麼功能以及它可以在多大程度上替代我們現在使用的 ELK 堆棧。

洛基是什麼

Grafana Loki 是一套完整日誌系統的組件。 與其他類似系統不同,Loki 基於僅對日誌元數據 - 標籤進行索引的思想(就像在 Prometheus 中一樣),並將日誌本身並排壓縮成單獨的塊。

主頁, GitHub上

在介紹 Loki 的功能之前,我想澄清一下“僅索引元數據的想法”的含義。 讓我們使用 nginx 日誌中的一行示例來比較 Loki 方法和傳統解決方案(例如 Elasticsearch)中的索引方法:

172.19.0.4 - - [01/Jun/2020:12:05:03 +0000] "GET /purchase?user_id=75146478&item_id=34234 HTTP/1.1" 500 8102 "-" "Stub_Bot/3.0" "0.001"

傳統系統會解析整行,包括具有許多唯一 user_id 和 item_id 值的字段,並將所有內容存儲在大型索引中。 這種方法的優點是您可以快速運行複雜的查詢,因為幾乎所有數據都在索引中。 但你必須為此付出代價,因為索引會變大,這會轉化為內存需求。 因此,日誌的全文索引的大小與日誌本身的大小相當。 為了快速搜索,必須將索引加載到內存中。 而且日誌越多,索引增加的速度就越快,消耗的內存也就越多。

Loki方法要求只從字符串中提取必要的數據,其值的數量很少。 這樣我們就得到了一個小索引,並且可以通過按時間和索引字段過濾數據來搜索數據,然後使用正則表達式或子字符串搜索掃描其餘部分。 這個過程看起來並不是最快的,但 Loki 將請求分成幾個部分並並行執行,在短時間內處理大量數據。 分片的數量和其中的並行請求是可配置的; 因此,每單位時間可以處理的數據量線性取決於所提供的資源量。

這種大的快速索引和小的並行強力索引之間的權衡使 Loki 能夠控制系統的成本。 可以根據您的需求靈活配置和擴展。

Loki 堆棧由三個組件組成:Promtail、Loki、Grafana。 Promtail 收集日誌、處理日誌並將其發送給 Loki。 洛基保留了它們。 Grafana 可以向 Loki 請求數據並顯示出來。 一般來說,Loki 不僅可以用於存儲日誌和搜索日誌。 整個堆棧為使用 Prometheus 方式處理和分析傳入數據提供了絕佳的機會。
可以找到安裝過程的描述 這裡.

日誌搜索

您可以在 Grafana 的特殊界面 — Explorer 中搜索日誌。 查詢使用 LogQL 語言,這與 Prometheus 使用的 PromQL 非常相似。 原則上,它可以被認為是一個分佈式 grep。

搜索界面如下所示:

從 Loki 收集日誌

查詢本身由兩部分組成:選擇器和過濾器。 選擇器是通過分配給日誌的索引元數據(標籤)進行搜索,過濾器是一個搜索字符串或正則表達式,用於過濾掉選擇器定義的記錄。 在給定的示例中:大括號中 - 選擇器,後面的所有內容 - 過濾器。

{image_name="nginx.promtail.test"} |= "index"

由於 Loki 的工作方式,您無法在沒有選擇器的情況下發出請求,但標籤可以任意通用。

選擇器是大括號中的值的鍵值。 您可以組合選擇器並使用 =、!= 運算符或正則表達式指定不同的搜索條件:

{instance=~"kafka-[23]",name!="kafka-dev"} 
// Найдёт логи с лейблом instance, имеющие значение kafka-2, kafka-3, и исключит dev 

過濾器是一個文本或正則表達式,它將過濾掉選擇器接收到的所有數據。

可以根據指標模式下接收到的數據獲取臨時圖表。 例如,您可以找出包含索引字符串的條目在 nginx 日誌中出現的頻率:

從 Loki 收集日誌

功能的完整描述可以在文檔中找到 日誌查詢語言.

日誌解析

收集日誌的方式有以下幾種:

  • 借助 Promtail(用於收集日誌的堆棧標準組件)。
  • 直接從 docker 容器使用 Loki Docker 日誌驅動程序。
  • 使用 Fluentd 或 Fluent Bit 可以向 Loki 發送數據。 與 Promtail 不同的是,它們擁有適用於幾乎任何類型日誌的現成解析器,並且還可以處理多行日誌。

通常使用Promtail進行解析。 它做了三件事:

  • 查找數據源。
  • 給它們貼上標籤。
  • 向 Loki 發送數據。

目前 Promtail 可以從本地文件和 systemd 日誌中讀取日誌。 它必須安裝在收集日誌的每台計算機上。

與 Kubernetes 集成:Promtail 通過 Kubernetes REST API 自動查找集群的狀態,並從節點、服務或 pod 收集日誌,立即根據 Kubernetes 的元數據(pod 名稱、文件名等)發布標籤。

您還可以使用 Pipeline 根據日誌中的數據懸掛標籤。 Pipeline Promtail 可以由四種類型的階段組成。 更多詳情 - 在 官方文檔,我會立即註意到一些細微差別。

  1. 解析階段。 這是RegEx和JSON的階段。 在此階段,我們將日誌中的數據提取到所謂的提取地圖中。 您可以通過簡單地將我們需要的字段複製到提取的映射中或通過正則表達式 (RegEx) 來從 JSON 中提取,其中命名組“映射”到提取的映射中。 提取的map是一個鍵值存儲,其中key是字段的名稱,value是日誌中的字段值。
  2. 變換階段。 此階段有兩個選項:轉換(我們在其中設置轉換規則)和源(從提取的地圖中進行轉換的數據源)。 如果提取的地圖中沒有該字段,則會創建該字段。 因此,可以創建不基於提取的地圖的標籤。 在這個階段,我們可以使用相當強大的功能來操作提取的地圖中的數據 golang模板。 此外,我們必須記住,提取的映射在解析過程中已完全加載,這使得可以檢查其中的值:“{{if .tag}標籤值存在{end}}”。 模板支持條件、循環和一些字符串函數,例如 Replace 和 Trim。
  3. 行動階段。 在此階段,您可以對提取的內容執行一些操作:
    • 根據提取的數據創建一個標籤,Loki 將對其進行索引。
    • 從日誌中更改或設置事件時間。
    • 更改將發送給 Loki 的數據(日誌文本)。
    • 創建指標。
  4. 過濾階段。 匹配階段,我們可以將不需要的記錄發送到 /dev/null,或者發送它們以進行進一步處理。

通過處理普通 nginx 日誌的示例,我將展示如何使用 Promtail 解析日誌。

為了進行測試,我們採用一個修改後的 nginx jwilder/nginx-proxy:alpine 映像和一個可以通過 HTTP 作為 nginx-proxy 查詢自身的小守護進程。 該守護進程有多個端點,它可以向這些端點提供不同大小、不同 HTTP 狀態和不同延遲的響應。

我們將從 docker 容器收集日誌,這些日誌可以沿著路徑 /var/lib/docker/containers/ 找到/ -json.log

在 docker-compose.yml 中,我們設置 Promtail 並指定配置路徑:

promtail:
  image: grafana/promtail:1.4.1
 // ...
 volumes:
   - /var/lib/docker/containers:/var/lib/docker/containers:ro
   - promtail-data:/var/lib/promtail/positions
   - ${PWD}/promtail/docker.yml:/etc/promtail/promtail.yml
 command:
   - '-config.file=/etc/promtail/promtail.yml'
 // ...

將日誌的路徑添加到 promtail.yml (配置中有一個“docker”選項,在一行中執行相同的操作,但不會那麼明顯):

scrape_configs:
 - job_name: containers

   static_configs:
       labels:
         job: containerlogs
         __path__: /var/lib/docker/containers/*/*log  # for linux only

啟用此配置後,Loki 將接收來自所有容器的日誌。 為了避免這種情況,我們在 docker-compose.yml 中更改測試 nginx 的設置 - 將日誌記錄添加到標記字段:

proxy:
 image: nginx.test.v3
//…
 logging:
   driver: "json-file"
   options:
     tag: "{{.ImageName}}|{{.Name}}"

編輯 promtail.yml 並設置 Pipeline。 日誌如下:

{"log":"u001b[0;33;1mnginx.1    | u001b[0mnginx.test 172.28.0.3 - - [13/Jun/2020:23:25:50 +0000] "GET /api/index HTTP/1.1" 200 0 "-" "Stub_Bot/0.1" "0.096"n","stream":"stdout","attrs":{"tag":"nginx.promtail.test|proxy.prober"},"time":"2020-06-13T23:25:50.66740443Z"}
{"log":"u001b[0;33;1mnginx.1    | u001b[0mnginx.test 172.28.0.3 - - [13/Jun/2020:23:25:50 +0000] "GET /200 HTTP/1.1" 200 0 "-" "Stub_Bot/0.1" "0.000"n","stream":"stdout","attrs":{"tag":"nginx.promtail.test|proxy.prober"},"time":"2020-06-13T23:25:50.702925272Z"}

管道階段:

 - json:
     expressions:
       stream: stream
       attrs: attrs
       tag: attrs.tag

我們從傳入的 JSON 中提取流、attrs、attrs.tag 字段(如果有)並將它們放入提取的映射中。

 - regex:
     expression: ^(?P<image_name>([^|]+))|(?P<container_name>([^|]+))$
     source: "tag"

如果可以將標籤字段放入提取的映射中,則使用正則表達式我們提取圖像和容器的名稱。

 - labels:
     image_name:
     container_name:

我們分配標籤。 如果在提取的數據中找到鍵image_name和container_name,那麼它們的值將被分配給適當的標籤。

 - match:
     selector: '{job="docker",container_name="",image_name=""}'
     action: drop

我們丟棄所有沒有設置標籤image_name和container_name的日誌。

  - match:
     selector: '{image_name="nginx.promtail.test"}'
     stages:
       - json:
           expressions:
             row: log

對於 image_name 等於 nginx.promtail.test 的所有日誌,我們從源日誌中提取日誌字段,並將其與行鍵一起放入提取的映射中。

  - regex:
         # suppress forego colors
         expression: .+nginx.+|.+[0m(?P<virtual_host>[a-z_.-]+) +(?P<nginxlog>.+)
         source: logrow

我們用正則表達式清除輸入字符串,並提取 nginx 虛擬主機和 nginx 日誌行。

     - regex:
         source: nginxlog
         expression: ^(?P<ip>[w.]+) - (?P<user>[^ ]*) [(?P<timestamp>[^ ]+).*] "(?P<method>[^ ]*) (?P<request_url>[^ ]*) (?P<request_http_protocol>[^ ]*)" (?P<status>[d]+) (?P<bytes_out>[d]+) "(?P<http_referer>[^"]*)" "(?P<user_agent>[^"]*)"( "(?P<response_time>[d.]+)")?

使用正則表達式解析 nginx 日誌。

    - regex:
           source: request_url
           expression: ^.+.(?P<static_type>jpg|jpeg|gif|png|ico|css|zip|tgz|gz|rar|bz2|pdf|txt|tar|wav|bmp|rtf|js|flv|swf|html|htm)$
     - regex:
           source: request_url
           expression: ^/photo/(?P<photo>[^/?.]+).*$
       - regex:
           source: request_url
           expression: ^/api/(?P<api_request>[^/?.]+).*$

解析request_url。 在正則表達式的幫助下,我們確定請求的目的:靜態、照片、API,並在提取的地圖中設置相應的鍵。

       - template:
           source: request_type
           template: "{{if .photo}}photo{{else if .static_type}}static{{else if .api_request}}api{{else}}other{{end}}"

使用 Template 中的條件運算符,我們檢查提取的地圖中已安裝的字段,並為 request_type 字段設置所需的值:photo、static、API。 如果失敗則分配其他。 現在 request_type 包含請求類型。

       - labels:
           api_request:
           virtual_host:
           request_type:
           status:

我們根據我們設法放入提取的映射中的內容設置標籤 api_request、virtual_host、request_type 和 status(HTTP 狀態)。

       - output:
           source: nginx_log_row

改變輸出。 現在,從提取的映射中清理乾淨的 nginx 日誌將發送到 Loki。

從 Loki 收集日誌

運行上述配置後,您可以看到每個條目都根據日誌中的數據進行標記。

請記住,提取具有大量值(基數)的標籤會顯著減慢 Loki 的速度。 也就是說,您不應該放入索引,例如user_id。 在文章中閱讀更多相關內容Loki 中的標籤如何使日誌查詢更快更容易”。 但這並不意味著你不能在沒有索引的情況下通過 user_id 進行搜索。 搜索時需要使用過濾器(根據數據“抓取”),這裡的索引充當流標識符。

日誌可視化

從 Loki 收集日誌

Loki 可以使用 LogQL 作為 Grafana 圖表的數據源。 支持以下功能:

  • 速率 - 每秒記錄數;
  • 隨時間變化的計數 - 給定範圍內的記錄數。

還有聚合函數 Sum、Avg 等。 您可以構建相當複雜的圖表,例如 HTTP 錯誤數量的圖表:

從 Loki 收集日誌

Loki 的默認數據源的功能比 Prometheus 數據源稍差一些(例如,您無法更改圖例),但 Loki 可以作為 Prometheus 類型源進行連接。 我不確定這是否是記錄在案的行為,但根據開發人員的回應來判斷“如何將 Loki 配置為 Prometheus 數據源? · 問題 #1222 · grafana/loki”,例如,它是完全合法的,並且 Loki 與 PromQL 完全兼容。

添加 Loki 作為 Prometheus 類型的數據源並附加 URL /loki:

從 Loki 收集日誌

您可以製作圖表,就像我們使用 Prometheus 的指標一樣:

從 Loki 收集日誌

我認為功能上的差異是暫時的,開發人員將來會修復它。

從 Loki 收集日誌

指標

Loki 提供了從日誌中提取數字指​​標並將其發送到 Prometheus 的能力。 例如,nginx 日誌包含每個響應的字節數,並且通過對標準日誌格式進行一定修改,還包含響應所花費的時間(以秒為單位)。 可以提取該數據並將其發送到 Prometheus。

添加另一個部分到 promtail.yml:

- match:
   selector: '{request_type="api"}'
   stages:
     - metrics:
         http_nginx_response_time:
           type: Histogram
           description: "response time ms"
           source: response_time
           config:
             buckets: [0.010,0.050,0.100,0.200,0.500,1.0]
- match:
   selector: '{request_type=~"static|photo"}'
   stages:
     - metrics:
         http_nginx_response_bytes_sum:
           type: Counter
           description: "response bytes sum"
           source: bytes_out
           config:
             action: add
         http_nginx_response_bytes_count:
           type: Counter
           description: "response bytes count"
           source: bytes_out
           config:
             action: inc

該選項允許您根據提取的地圖中的數據定義和更新指標。 這些指標不會發送到 Loki - 它們出現在 Promtail /metrics 端點中。 Prometheus 必須配置為從該階段接收數據。 在上面的示例中,對於 request_type="api",我們收集直方圖指標。 使用這種類型的指標可以很方便地獲得百分位數。 對於靜態數據和照片,我們收集字節總和以及接收字節的行數來計算平均值。

閱讀有關指標的更多信息 這裡.

在 Promtail 上打開端口:

promtail:
     image: grafana/promtail:1.4.1
     container_name: monitoring.promtail
     expose:
       - 9080
     ports:
       - "9080:9080"

我們確保帶有 promtail_custom 前綴的指標已經出現:

從 Loki 收集日誌

設置普羅米修斯。 添加職位簡介:

- job_name: 'promtail'
 scrape_interval: 10s
 static_configs:
   - targets: ['promtail:9080']

並畫一個圖表:

從 Loki 收集日誌

例如,您可以通過這種方式找出四個最慢的請求。 您還可以配置對這些指標的監控。

縮放

Loki 可以處於單一二進制模式和分片(水平可擴展模式)。 第二種情況,它可以將數據保存到雲端,塊和索引分開存儲。 在1.5版本中,實現了一處存儲的功能,但還不建議在生產中使用。

從 Loki 收集日誌

塊可以存儲在與 S3 兼容的存儲中,為了存儲索引,請使用水平可擴展的數據庫:Cassandra、BigTable 或 DynamoDB。 Loki 的其他部分——Distributors(用於寫入)和 Querier(用於查詢)——是無狀態的,並且也可以水平擴展。

在 2019 年溫哥華 DevOpsDays 會議上,與會者之一 Callum Styan 宣布,Loki 的項目擁有 PB​​ 級日誌,其索引不到總大小的 1%:“Loki 如何關聯指標和日誌並為您節省資金“。

Loki與ELK對比

索引大小

為了測試生成的索引大小,我從配置了上面 Pipeline 的 nginx 容器中獲取了日誌。 該日誌文件包含 406 行,總大小為 624 MB。 日誌在一小時內生成,每秒大約109條記錄。

日誌中的兩行示例:

從 Loki 收集日誌

當由 ELK 建立索引時,索引大小為 30,3 MB:

從 Loki 收集日誌

對於 Loki,這提供了大約 128 KB 的索引和大約 3,8 MB 的數據塊。 值得注意的是,該日誌是人為生成的,並沒有包含廣泛的數據。 對帶有數據的原始 Docker JSON 日誌進行簡單的 gzip 壓縮可以達到 95,4%,並且考慮到只有清理後的 nginx 日誌被發送到 Loki 本身,壓縮到 4 MB 是可以理解的。 Loki 標籤的唯一值總數為 35 個,這解釋了索引的大小。 對於ELK來說,日誌也被清除了。 這樣,Loki 將原始數據壓縮了 96%,ELK 則壓縮了 70%。

內存消耗

從 Loki 收集日誌

如果我們比較 Prometheus 和 ELK 的整個堆棧,那麼 Loki 的“吃”量要少好幾倍。 很明顯,Go 服務消耗的內存比 Java 服務少,並且將 Elasticsearch JVM 堆的大小與 Loki 分配的內存進行比較是不正確的,但值得注意的是,Loki 使用的內存要少得多。 它的CPU優勢雖然不是那麼明顯,但也是存在的。

速度

洛基“吞噬”日誌的速度更快。 速度取決於很多因素——什麼樣的日誌、我們解析它們的複雜程度、網絡、磁盤等——但它絕對高於 ELK(在我的測試中——大約兩倍)。 這是因為 Loki 在索引​​中放入的數據要少得多,因此在索引上花費的時間也更少。 在這種情況下,搜索速度的情況正好相反:Loki 在處理大於幾 GB 的數據時明顯變慢,而對於 ELK,搜索速度不依賴於數據大小。

日誌搜索

Loki在日誌搜索能力上明顯不如ELK。 正則表達式的 grep 是個很強的東西,但是比不上成人數據庫。 缺乏範圍查詢、僅通過標籤進行聚合、無法在沒有標籤的情況下進行搜索——所有這些都限制了我們在 Loki 中搜索感興趣的信息。 這並不意味著使用 Loki 找不到任何內容,而是定義了處理日誌的流程:首先在 Prometheus 圖表上發現問題,然後使用這些標籤查找日誌中發生的情況。

接口

首先,它很漂亮(抱歉,無法抗拒)。 Grafana 的界面很漂亮,但 Kibana 的功能更強大。

洛基的優點和缺點

在優點中,值得注意的是 Loki 與 Prometheus 集成,我們分別獲得開箱即用的指標和警報。 它可以方便地收集日誌並將其存儲在 Kubernetes Pod 中,因為它具有繼承自 Prometheus 的服務發現功能並自動附加標籤。

缺點是 - 文檔很差。 有些東西,比如Promtail的特性和能力,我是在研究代碼的過程中才發現的,開源的好處。 另一個缺點是解析能力較弱。 例如,Loki 無法解析多行日誌。 此外,缺點還包括 Loki 是一項相對年輕的技術(1.0 版本於 2019 年 XNUMX 月發布)。

結論

Loki是一項百分百有趣的技術,適合中小型項目,可以讓你解決日誌聚合、日誌搜索、日誌監控和分析等諸多問題。

我們在 Badoo 不使用 Loki,因為我們有一個適合我們的 ELK 堆棧,並且多年來已經充斥著各種自定義解決方案。 對於我們來說,絆腳石是日誌中的搜索。 每天有近 100 GB 的日誌,對於我們來說,能夠找到所有內容以及更多內容并快速完成是非常重要的。 對於圖表和監控,我們使用根據我們的需求量身定制並相互集成的其他解決方案。 Loki 堆棧具有切實的好處,但它不會給我們帶來超出我們現有的東西,而且它的好處不會完全超過遷移的成本。

儘管經過研究後發現我們不能使用 Loki,但我們希望這篇文章能幫助您做出選擇。

包含本文中使用的代碼的存儲庫位於 這裡.

來源: www.habr.com

添加評論