从 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 中提取,其中命名组“映射”到提取的映射中。 提取的映射是一个键值存储,其中键是字段的名称,值是日志中的值。
  2. 变换阶段。 此阶段有两个选项:转换(我们在其中设置转换规则)和源(从提取的地图中进行转换的数据源)。 如果提取的地图中没有该字段,则会创建该字段。 这样就可以创建不基于提取的地图的标签。 在这个阶段,我们可以使用相当强大的功能来操作提取的地图中的数据 Go 模板。 此外,我们必须记住,提取的映射是在解析期间完全加载的,这使得可以检查其中的值:“{{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 中提取字段stream、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的所有日志,从源日志中提取log字段,并将其与row key一起放入提取的map中。

  - 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 收集日志

与 Prometheus 数据源相比,标准数据源 Loki 在功能上有所减少(例如,您无法更改图例),但 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(在我的测试中——大约是 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,但我们希望这篇文章能够帮助您做出选择。

包含本文中使用的代码的存储库位于 这里.

来源: habr.com

添加评论