在 Badoo,我们不断监控新技术并评估它们是否值得在我们的系统中使用。 我们想与社区分享其中一项研究。 它专用于 Loki,一个日志聚合系统。
Loki 是一个用于存储和查看日志的解决方案,该堆栈还提供了一个灵活的系统来分析日志并将数据发送到 Prometheus。 XNUMX月份,又发布了一个更新,得到了创作者的积极推动。 我们感兴趣的是 Loki 可以做什么,它提供什么功能,以及它可以在多大程度上替代我们现在使用的 ELK 堆栈。
洛基是什么
Grafana Loki 是用于处理日志的完整系统的一组组件。 与其他类似系统不同,Loki 基于仅索引日志元数据 - 标签(与 Prometheus 中相同)的思想,并将日志本身压缩为单独的块。
在我们讨论 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。
搜索界面如下所示:
请求本身由两部分组成:选择器和过滤器。 选择器是使用分配给日志的索引元数据(标签)进行搜索,过滤器是一个搜索字符串或正则表达式,用于过滤掉选择器定义的记录。 在给出的示例中:在花括号中有一个选择器,后面的所有内容都是过滤器。
{image_name="nginx.promtail.test"} |= "index"
由于 Loki 的工作方式,您无法在没有选择器的情况下进行查询,但标签可以根据您的喜好设置为通用的。
选择器是花括号中的键值对。 您可以组合选择器并使用运算符 =、!= 或正则表达式指定不同的搜索条件:
{instance=~"kafka-[23]",name!="kafka-dev"}
// Найдёт логи с лейблом instance, имеющие значение kafka-2, kafka-3, и исключит dev
过滤器是文本或正则表达式,它将过滤掉选择器接收到的所有数据。
可以根据度量模式下接收到的数据获得临时图。 例如,您可以了解包含字符串索引的条目在 nginx 日志中出现的频率:
功能的完整描述可以在文档中找到
日志解析
收集日志的方式有以下几种:
- 使用 Promtail,这是用于收集日志的堆栈的标准组件。
- 直接从 docker 容器使用
Loki Docker 日志驱动程序。 - 使用Fluentd或Fluent Bit,可以向Loki发送数据。 与 Promtail 不同的是,它们拥有适用于几乎任何类型日志的现成解析器,并且还可以处理多行日志。
通常使用Promtail进行解析。 它做了三件事:
- 查找数据源。
- 给它们贴上标签。
- 向 Loki 发送数据。
目前 Promtail 可以从本地文件和 systemd 日志中读取日志。 它必须安装在收集日志的每台计算机上。
与 Kubernetes 集成:Promtail 通过 Kubernetes REST API 自动识别集群状态并从节点、服务或 pod 收集日志,立即根据 Kubernetes 的元数据(pod 名称、文件名等)发布标签。
您还可以使用 Pipeline 根据日志中的数据悬挂标签。 Pipeline Promtail 可以由四种类型的阶段组成。 更多详情请参阅
- 解析阶段。 这是 RegEx 和 JSON 阶段。 在此阶段,我们将日志中的数据提取到所谓的提取地图中。 我们可以通过简单地将所需的字段复制到提取的映射中,或者通过正则表达式 (RegEx) 从 JSON 中提取,其中命名组“映射”到提取的映射中。 提取的映射是一个键值存储,其中键是字段的名称,值是日志中的值。
- 变换阶段。 此阶段有两个选项:转换(我们在其中设置转换规则)和源(从提取的地图中进行转换的数据源)。 如果提取的地图中没有该字段,则会创建该字段。 这样就可以创建不基于提取的地图的标签。 在这个阶段,我们可以使用相当强大的功能来操作提取的地图中的数据
Go 模板 。 此外,我们必须记住,提取的映射是在解析期间完全加载的,这使得可以检查其中的值:“{{if .tag}标签值存在{end}}”。 模板支持条件、循环和一些字符串函数,例如 Replace 和 Trim。 - 行动阶段。 此时您可以对提取的内容执行一些操作:
- 根据提取的数据创建一个标签,Loki 将对其进行索引。
- 从日志中更改或设置事件时间。
- 更改将发送给 Loki 的数据(日志文本)。
- 创建指标。
- 过滤阶段。 匹配阶段,我们可以将不需要的条目发送到 /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 的速度。 也就是说,您不应将 user_id 等内容放入索引中。 在文章中阅读更多相关内容“
日志可视化
Loki 可以使用 LogQL 作为 Grafana 图的数据源。 支持以下功能:
- 速率——每秒记录数;
- 随时间变化的计数 — 指定范围内的记录数。
还有聚合函数 Sum、Avg 等。 您可以构建相当复杂的图表,例如 HTTP 错误数量的图表:
与 Prometheus 数据源相比,标准数据源 Loki 在功能上有所减少(例如,您无法更改图例),但 Loki 可以作为 Prometheus 类型的源进行连接。 我不确定这是否是有记录的行为,但根据开发人员的反应来判断“
添加 Loki 作为 Prometheus 类型的数据源并添加 URL /loki:
我们可以制作图表,就像我们使用 Prometheus 的指标一样:
我认为功能上的差异是暂时的,开发人员将来会纠正这个问题。
指标
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 前缀的指标:
设置普罗米修斯。 添加职位简介:
- job_name: 'promtail'
scrape_interval: 10s
static_configs:
- targets: ['promtail:9080']
我们画一个图表:
例如,您可以通过这种方式找出四个最慢的查询。 您还可以设置对这些指标的监控。
缩放
Loki 可以处于单一二进制模式或分片模式(水平可扩展模式)。 第二种情况,它可以将数据保存到云端,块和索引分开存储。 1.5版本引入了存储在一个地方的能力,但还不建议在生产中使用它。
块可以存储在与 S3 兼容的存储中,并且可以使用水平可扩展的数据库来存储索引:Cassandra、BigTable 或 DynamoDB。 Loki 的其他部分——Distributors(用于写入)和 Querier(用于查询)——是无状态的,并且也可以水平扩展。
在 2019 年温哥华 DevOpsDays 会议上,与会者之一 Callum Styan 宣布,Loki 的项目拥有 PB 级日志,其索引不到总大小的 1%:“
Loki与ELK对比
索引大小
为了测试生成的索引大小,我从配置了上面 Pipeline 的 nginx 容器中获取了日志。 该日志文件包含 406 行,总大小为 624 MB。 日志在一小时内生成,每秒大约 109 个条目。
日志中的两行示例:
当由 ELK 建立索引时,索引大小为 30,3 MB:
对于 Loki,这导致大约 128 KB 的索引和大约 3,8 MB 的数据块。 值得注意的是,该日志是人为生成的,并没有大量的数据。 对带有数据的原始 Docker JSON 日志进行简单的 gzip 压缩可以达到 95,4%,考虑到只有清理后的 nginx 日志被发送到 Loki 本身,压缩高达 4 MB 是可以理解的。 Loki 标签的唯一值总数为 35 个,这解释了索引的大小。 对于 ELK,日志也被清除。 这样,Loki 将原始数据压缩了 96%,ELK 则压缩了 70%。
内存消耗
如果我们比较整个 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