Grafana Loki日志采集初探

  • 1. 基本介绍
  • 1.1 工作概述
  • 1.2 基本特性
  • 1.3 架构介绍
  • 1.3.1 多租户设计
  • 1.3.2 工作模式
  • 1.3.2.1 Monolithic mode
  • 1.3.2.2 Simple scalable deployment mode
  • 1.3.2.3 Microservices mode
  • 1.4 Loki的组件
  • 1.4.1 Distributor
  • 1.4.2 Ingester
  • 1.4.3 Query frontend(查询前端)
  • 1.4.4 Querier(查询器)
  • 1.5 其他概念
  • 1.5.1 Storage(存储)
  • 1.6 总结
  • 1.6.1 读取流程
  • 1.6.2 写入流程

1. 基本介绍

Grafana Loki是一组组件,可以组成一个功能齐全的日志采集工具。

与其他日志系统不同的是,Loki是围绕着只对日志元数据进行索引的想法构建的:labels(就像Prometheus标签一样)。然后,日志数据本身被压缩并以块的形式存储在对象存储库(如S3或GCS)中,甚至存储在文件系统的本地。小索引和高度压缩的块简化了操作,并显著降低了Loki的成本。

Grafana官方对Loki的定义就是"一个针对日志的Prometheus"

1.1 工作概述

Grafana Loki是一个日志聚合工具,它是一个功能齐全的日志堆栈的核心;

与其他日志系统不同,Loki索引是根据标签构建的,使原始日志消息不被索引;

Loki通过一个Agent(或者说是客户端),将日志转换为流,并通过HTTP API将流推送给Loki。Promtail代理是为Loki安装而设计的,但许多其他代理也可与Loki无缝集成。

如何在graylog里面监控日志_微服务

1.2 基本特性

  • 为日志建立索引而有效使用内存,使用更小的内存,减少操作成本
  • 多租户特性,Loki允许多个租户使用一个Loki实例。不同租户的数据完全与其他租户隔离。多租户是通过在代理中分配租户ID来配置的;
  • 特有的查询语言,如果用户使用过PromQL就会发现,LogQL对生成针对日志的查询很熟悉,而且很灵活。该语言还有助于从日志数据生成度量,这是一个远远超出日志聚合的强大特性。
  • Loki可以独立运行,所有组件都运行在一个进程中;同时也可以每个组件作为微服务进行运行,以进行灵活拓展;
  • 灵活拓展,许多代理(客户端)都有插件支持。这允许当前的可观察性结构添加Loki作为日志聚合工具,而不需要切换可观察性堆栈的现有部分;
  • Loki与Grafana无缝集成,提供了一个完整的可观察的解决方案;

1.3 架构介绍

1.3.1 多租户设计

当Grafana Loki在多租户模式下运行时,内存和长期存储中的所有数据都可以通过租户ID进行分区,从请求中的X-Scope-OrgID HTTP头中提取。当Loki不在多租户模式时,HTTP Header被忽略,并且租户ID被设置为"fake",它将出现在索引和存储块中;

1.3.2 工作模式

作为一个应用程序,Loki由许多组件微服务构建而成,并被设计成一个水平可伸缩的分布式系统。Loki的独特设计将整个分布式系统的代码编译成一个二进制或Docker镜像。该二进制文件的行为由-target命令行标志控制,并定义了三种操作模式其中之一;

每个已部署的二进制文件实例的配置将进一步指定它运行的组件的情况;Loki可以在管理员需要更改时,在不同的模式下轻松地重新部署集群,无需更改配置或只进行最小的配置更改。

1.3.2.1 Monolithic mode

此模式为服务一体的简单的操作模式,只需要设置-target=all。这也是默认运行模式,可以不需要指定。在这种运行模式下,在一个进程中以二进制或Docker镜像的形式运行Loki的所有微服务组件,架构如下:

如何在graylog里面监控日志_时间戳_02

  • 此模式对于快速开始使用Loki进行学习非常有用,对于每天大约100GB的小读/写量也非常有效;
  • 通过使用共享对象存储,并配置memberlist_config部分在所有实例之间共享状态,将Monlithic mode部署水平扩展到更多实例。
  • 高可用特性可以通过使用memberlist_config配置和一个共享对象存储运行两个Loki实例来配置。
  • 以轮训方式将流量路由到所有Loki实例。
  • 查询并行化受限于实例数量和定义的查询并行性

1.3.2.2 Simple scalable deployment mode

简单的可拓展部署模式,如果预估的日志量每天超过几百GB,或者需要将读写负载分开,那么此模式是一种解决方案;

如何在graylog里面监控日志_如何在graylog里面监控日志_03

在这种模式下,Loki的组件微服务被捆绑到两个target中:-target=read-target=write

将读写路径分开有以下好处:

  • 提供专门的节点以提供高可用能力;
  • 分离的可伸缩的读路径,可根据需要提高查询性能;

简单模式下,需要在Loki前面有一个负载均衡器,它将/ Loki /api/v1/push流量导向写节点。所有其他请求都转到读取节点。流量应该以轮训方式发送,此模式的规模可以扩展到每天几TB甚至更多的日志。

1.3.2.3 Microservices mode

微服务部署模式将Loki的组件实例化为不同的进程。每个进程被指定为对应的target调用:

  • ingester
  • distributor
  • query-frontend
  • query-scheduler
  • querier
  • index-gateway
  • ruler
  • compactor

如何在graylog里面监控日志_微服务_04

将组件作为单个微服务运行,可以通过增加微服务的数量来实现扩展。自定义集群具有更好的单个组件的可观察性。微服务模式部署是最高效的Loki安装。当然,维护难度和成本也会相应的提高;

  • 微服务模式推荐用于非常大的Loki集群,或者需要对伸缩性和集群操作进行更多控制的集群;
  • 微服务模式最适合Kubernetes部署。

1.4 Loki的组件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Dw3PM4Z-1642068033561)(https://grafana.com/docs/loki/latest/fundamentals/architecture/loki_architecture_components.svg)]

1.4.1 Distributor

分发服务负责处理客户端传入的流。它是日志数据写入路径中的第一站。Distributor接收到一组流后,将验证每个流的正确性,并确保其在配置的租户(或全局)限制内。然后有效的块被分割成批,并并行地发送给多个Ingester组件

Hashing

Distributor使用一致的散列治和可配置的复制因子来确定Ingester服务的哪些实例应该接收给定的流;流是一组与租户和唯一标签集相关联的日志。使用租户ID和标签集对流进行散列,然后使用散列查找要将流发送到的Ingester;

存储在Consul中的Hash环用于实现一致的散列;所有Ingester都使用自己拥有的一组令牌将自己注册到Hash环中。每个令牌是一个随机的无符号32位数字。除了一组令牌外,Ingester还将它们的状态注册到Hash环中。JOINING状态和ACTIVE状态都可能收到写请求,而ACTIVE状态和LEAVING的Ingester可能收到读请求。当进行哈希查找时,Distributor只对处于请求适当状态的Ingester使用令牌;

为了进行哈希查找,Distributor需要找到值大于流哈希值的最小的适当令牌。当复制因子大于1时,属于不同Ingester的下一个后续标记(环中顺时针方向)也将包含在结果中;

这个哈希设置的效果是,Ingester拥有的每个令牌负责一个哈希范围:

例如:如果有三个值分别为0、25和50的令牌,那么将给拥有令牌25的Ingester一个哈希值为3;拥有令牌25的Ingester负责1-25的散列范围。

Quorum consistency(集群一致性)

因为所有Distributor共享对同一个散列环的访问,所以写请求可以发送到任何分发服务器;为了确保查询结果的一致性,Loki在读写上使用dynamo风格的仲裁一致性。这意味着在响应发起发送的客户端之前,Distributor将等待至少一半的Ingester的积极响应,并且有一个Ingester发送了示例;

1.4.2 Ingester

Ingester负责将日志数据写入存储路径上的长期存储后端(DynamoDB、S3、Cassandra等),并为读路径上的内存查询返回日志数据;Ingester包含一个生命周期器,该生命周期器管理散列环中Ingester的生命周期。每个Ingester都有下列状态中的其中一个: PENDING, JOINING, ACTIVE, LEAVING, UNHEALTHY

  1. PENDING:此状态说明一个Ingester正在等待另一个LEAVING状态的Ingester的交接;
  2. JOINING:此状态说明一个Ingester正在将它的令牌插入到环中并初始化自己;此时它可能会收到对它拥有的令牌的写请求;
  3. ACTIVE:此状态说明一个Ingester已完全初始化。它可以接收对它所拥有的令牌的写和读请求;
  4. LEAVING:此状态说明一个Ingester正在关闭。它可以收到仍然在内存中的数据的读取请求;
  5. UNHEALTHY:此状态说明一个Ingester没有向Consul报告。UNHEALTHY状态由Distributor在定期检查环时设置。

Ingester接收到的每个日志流都在内存中构建成一组许多"chunks(块)",并按可配置的时间间隔刷新到存储后端。

在以下情况下,块被压缩并标记为只读:

  • 当前块已达到配置的容量(此值可配置);
  • 时间过得太久了,没有更新当前数据块;
  • 发生了Flush操作;

每当一个块被压缩并标记为只读时,一个可写块就会占据它的位置。

需要注意的是,如果一个Ingester进程崩溃或突然退出,所有尚未刷新的数据都将丢失。为了降低这种风险,Loki通常被配置为复制每个日志的多个副本(通常为3个);

当持久性存储提供方发生刷新时,将根据其租户、label和内容对块进行散列。这意味着具有相同数据副本的多个Ingester不会将相同的数据两次写入长期存储,但如果对其中一个副本的发生了任何写入失败,将在存储中创建多个不同的块对象。

时间戳排序

Loki可以配置为接受乱序写入,如果未配置为接受无序写操作,则Ingester将验证所摄取的日志行是否是有序的。Ingester验证日志行是否按照时间戳升序接收。每个日志都有一个时间戳,时间戳发生在比之前的日志晚的时间。当Ingester接收到不遵循此顺序的日志时,日志行将被拒绝,并返回一个错误。且对于给定流(唯一的label组合),所有推送到Loki的行必须有一个比之前接收到的行更新的时间戳。然而,对于具有相同纳秒时间戳的同一流,处理日志有两种情况:

  • 如果输入的行与之前接收的行完全匹配(同时匹配之前的时间戳和日志文本),则输入的行将被视为完全重复的行并被忽略;
  • 如果输入行的时间戳与前一行相同,但内容不同,则日志行被接受。这意味着对于同一个时间戳可以有两个不同的日志线

Handoff

默认情况下,当Ingester关闭并试图离开散列环时,它在Flush数据并尝试启动切换前将等待是否有新的Ingester试图进入。该切换将把离开Ingester的所有令牌和内存中的块转移给新的Ingester,在加入散列环之前,Ingester将处于PENDING状态等待切换的发生。在一个可配置的超时之后,处于PENDING状态的未接收到传输的Ingester将正常地加入到环中,插入一组新的令牌;此进程用于避免在关闭时刷新所有块,这是一个缓慢的进程。

文件系统支持

虽然Ingester支持通过BoltDB写入文件系统,但这只能在单进程模式下工作,因为查询器需要访问相同的后端存储,而BoltDB只允许一个进程在给定时间对数据库进行锁定。

1.4.3 Query frontend(查询前端)

Query frontend是一个可选的服务,提供Querier组件的API端点,可以用来加速读取路径。当Query frontend就位后,传入的查询请求应该被定向到Query frontend,而不是Querier。为了执行实际的查询,在集群中仍然需要Querier组件。

Query frontend在内部执行一些查询调整,并将查询保存在内部队列中。在这种设置中,Querier充当工人的角色,从队列中拉出作业,执行它们,并将它们返回到Query frontend进行聚合。Querier需要配置Query frontend组件的地址(通过-query .frontend-address 客户端标志),以便允许它们连接到Query frontend。

Query frontend是无状态的。然而,由于内部队列的工作方式,建议运行几个Query frontend副本,以获得公平调度的好处。在大多数情况下,两个副本应该足够了。

Queueing(队列)

Query frontend的队列机制用于达成以下效果:

  • 确保可能导致查询器中内存不足(OOM)错误的大型查询在失败时能够重试;
  • 通过使用FIFO队列方式将请求分布到所有的Querier组件上,防止多个大型请求在单个Querier上运行;
  • 通过合理地调度租户之间的查询,防止单个租户拒绝服务(DOSing)其他租户。

Splitting(分裂)

查询前端将较大的查询分割为多个较小的查询,在下游Querier上并行执行这些查询,并将结果再次拼接在一起。这可以防止大型(多天等)查询在单个Querier中导致内存不足的问题,并有助于更快地执行它们。

Caching(缓存)

Metric Queries

查询前端支持缓存指标查询的结果,并在后续查询中重用它们。如果缓存的结果不完整,查询前端计算所需的子查询,并在下游Querier上并行执行它们。查询前端可以选择将查询与其步骤参数对齐,以提高查询结果的可缓存性。结果缓存兼容任何后端Loki缓存(目前有memcached,redis,和一个内存缓存)。

Log Queries——官方说未来会有

1.4.4 Querier(查询器)

Querier服务使用LogQL查询语言处理查询,从Ingester和长期存储中获取日志。

在返回到对后端存储运行相同的查询之前(理解为从磁盘读取数据前),Querier会查询所有Ingester中的内存数据。由于复制因素,查询程序可能会接收到重复的数据。为了解决这个问题,Querier在内部对具有相同的纳秒时间戳、标签集和日志消息的数据进行重复数据删除,这样的操作最大化利用了内存的快速特性。

1.5 其他概念

1.5.1 Storage(存储)

Loki将所有数据存储在一个单一的对象存储后端。这种操作模式在Loki 2.0中普遍可用,而且快速、经济、简单。这种模式使用一个名为boltdb_shipper的适配器将索引存储在对象存储中(与存储块的方式相同)。

块存储是Loki的长期数据存储,旨在支持交互式查询和持续写入,而不需要后台维护任务,它由以下部分组成:

  • 块索引,这个索引可依赖于:
  • Amazon DynamoDB
  • Google Bigtable
  • Apache Cassandra
  • key-value (KV)存储块数据本身,可以是以下几种:
  • Amazon DynamoDB
  • Google Bigtable
  • Apache Cassandra
  • Amazon S3
  • Google Cloud Storage

与Loki的其他核心组件不同,块存储不是一个独立的服务、作业或进程,而是嵌入在两个需要访问Loki数据的服务(Ingester和Querier)中的库。

块存储依赖于"NoSQL"存储(DynamoDB、Bigtable和Cassandra)的统一接口,可用于支持块存储索引。这个接口假设索引是由以下键控项组成的集合:

  • 一个Hash Key,这对所有读写都是必需的。
  • 一个Range Key,这对于写是必需的,对于读可以省略,读可以通过前缀或范围进行查询。

一组模式用于将读取和写入块存储时使用的匹配器和标签集映射到索引上的适当操作。随着Loki的发展,模式被添加进来,主要是为了更好地实现写负载平衡和提高查询性能。

1.6 总结

1.6.1 读取流程

综上所述,读取流的工作原理如下:

  1. Querier接收到一个HTTP请求;
  2. Querier将查询传递给内存中数据的所有Ingester组件;
  3. Ingester接收读请求并返回与查询匹配的数据(如果有的话);
  4. 如果没有Ingester返回数据,Querier机会惰性地从长期存储中加载数据(这代表没在内存中检测到数据)并进行查询;
  5. Querier遍历所有接收到的数据并进行重复数据的删除,通过HTTP连接返回最终的一组数据。

1.6.2 写入流程

综上所述,写入流的工作原理如下:

如何在graylog里面监控日志_如何在graylog里面监控日志_05

  1. Distributor收到一个HTTP请求来为流存储数据;
  2. 每个流都使用散列环进行散列;
  3. Distributor将每个流发送到适当的Ingester及其副本(基于配置的复制因子);
  4. 每个Ingester将为流的数据创建一个块或向现有块追加数据。一个块对于每个租户和每个labels集是唯一的,具体原理见上图;
  5. Distributor通过HTTP连接响应一个成功代码;