Loki 组件
Loki 是一个模块化系统,包含许多组件,这些组件可以一起运行(在目标为 `all` 的“单体二进制”模式下),也可以按逻辑分组运行(在目标为 `read`、`write`、`backend` 的“简单可扩展部署”模式下),或者单独运行(在“微服务”模式下)。有关更多信息,请参阅部署模式。
组件 | 独立 | all | read | write | backend |
---|---|---|---|---|---|
Distributor | x | x | x | ||
Ingester | x | x | x | ||
Query Frontend | x | x | x | ||
Query Scheduler | x | x | x | ||
Querier | x | x | x | ||
Index Gateway | x | x | |||
Compactor | x | x | x | ||
Ruler | x | x | x | ||
Bloom Planner (实验性) | x | x | |||
Bloom Builder (实验性) | x | x | |||
Bloom Gateway (实验性) | x | x |
本页描述了这些组件各自的职责。
Distributor
**Distributor** 服务负责处理来自客户端的传入推送请求。这是日志数据写入路径的第一步。Distributor 接收到 HTTP 请求中的一组流后,会对每个流进行正确性验证,并确保其在配置的租户(或全局)限制范围内。然后,每个有效的流会并行发送到 `n` 个 Ingester,其中 `n` 是数据的复制因子。Distributor 使用一致性哈希来确定将流发送到哪些 Ingester。
必须在 Distributor 前面放置负载均衡器,以便正确平衡传入流量。在 Kubernetes 中,服务负载均衡器提供此服务。
Distributor 是一个无状态组件。这使得扩展和尽可能多地分担 Ingester(写入路径中最关键的组件)的工作变得容易。独立扩展这些验证操作的能力意味着 Loki 也可以保护自身免受可能使 Ingester 过载的拒绝服务攻击。它还允许我们根据复制因子进行扇出写入。
验证
Distributor 执行的第一步是确保所有传入数据都符合规范。这包括检查标签是否是有效的 Prometheus 标签,以及确保时间戳不会太旧或太新,日志行不会太长等。
预处理
目前,Distributor 转换传入数据的唯一方法是规范化标签。这意味着使 `{foo="bar", bazz="buzz"}` 等效于 `{bazz="buzz", foo="bar"}`,换句话说,对标签进行排序。这使得 Loki 可以确定性地缓存和哈希它们。
速率限制
Distributor 还可以根据每个租户的最大数据摄入速率限制传入日志的速率。它通过检查每个租户的限制并将其除以当前的 Distributor 数量来实现。这使得可以在集群级别为每个租户指定速率限制,并允许我们根据需要扩展或缩减 Distributor 数量,并相应地调整每个 Distributor 的限制。例如,假设我们有 10 个 Distributor,租户 A 的速率限制为 10MB。每个 Distributor 在限制之前将允许高达 1MB/s 的速率。现在,假设另一个大型租户加入集群,我们需要再启动 10 个 Distributor。现在 20 个 Distributor 将把租户 A 的速率限制调整为 `(10MB / 20 个 Distributor) = 500KB/s`。全局限制就是这样让 Loki 集群的操作变得更简单、更安全。
注意
Distributor 在底层使用 `ring` 组件在同行中注册自身并获取活动 Distributor 的总数。这与 Ingester 在 Ring 中使用的“key”不同,并且来自 Distributor 自己的Ring 配置。
转发
一旦 Distributor 完成所有验证职责,它就会将数据转发给 Ingester 组件,Ingester 最终负责确认写入操作。
复制因子
为了降低在任何单个 Ingester 上丢失数据的几率,Distributor 会将写入转发到其对应的 _复制因子_ 的 Ingester 上。通常,复制因子为 `3`。复制允许在 Ingester 重启和滚动更新期间不失败写入,并为某些场景提供了额外的数据丢失保护。大致而言,对于推送到 Distributor 的每个标签集(称为 _流_),它会对标签进行哈希处理,并使用结果值在 `ring` 中查找 `replication_factor` 个 Ingester(`ring` 是一个暴露分布式哈希表的子组件)。然后它会尝试将相同的数据写入所有这些 Ingester。如果成功写入的数量少于 _quorum_,则会产生错误。Quorum 定义为 `floor( 复制因子 / 2 ) + 1`。因此,对于我们的 `replication_factor` 为 `3`,我们需要两个写入成功。如果成功写入的数量少于两个,Distributor 会返回错误,并且写入操作将被重试。
注意
如果 3 个 Ingester 中有 2 个确认了写入,我们可以容忍丢失一个 Ingester,但不能丢失两个,因为这将导致数据丢失。
复制因子并非唯一防止数据丢失的措施,它的主要目的是允许在滚动更新和重启期间写入操作不中断。现在 Ingester 组件包含一个预写日志 (WAL),它将传入的写入持久化到磁盘,只要磁盘未损坏,就能确保数据不会丢失。复制因子和 WAL 的互补性确保了除非这两种机制都出现严重故障(即多个 Ingester 死亡并丢失/损坏其磁盘),否则数据不会丢失。
哈希
Distributor 结合可配置的复制因子使用一致性哈希来确定哪个 Ingester 服务实例应该接收给定的流。
流是一组与租户和唯一标签集关联的日志。流使用租户 ID 和标签集进行哈希处理,然后使用哈希值查找要将流发送到的 Ingester。
哈希环通过使用 Memberlist 协议的点对点通信或存储在 Consul 等 Key-Value 存储中来维护,以实现一致性哈希;所有 Ingester 将自己及其拥有的令牌集注册到哈希环中。每个令牌都是一个随机的无符号 32 位数字。Ingester 除了注册一组令牌外,还将其状态注册到哈希环中。`JOINING` 和 `ACTIVE` 状态的 Ingester 都可以接收写入请求,而 `ACTIVE` 和 `LEAVING` 状态的 Ingester 可以接收读取请求。进行哈希查找时,Distributor 仅使用处于适当请求状态的 Ingester 的令牌。
为了进行哈希查找,Distributor 找到值大于流哈希值的最小的适当令牌。当复制因子大于 1 时,属于不同 Ingester 的后续令牌(在 Ring 中顺时针方向)也将包含在结果中。
这种哈希设置的效果是,Ingester 拥有的每个令牌都负责一个哈希范围。如果存在值为 0、25 和 50 的三个令牌,则哈希值为 3 的流将分配给拥有令牌 25 的 Ingester;拥有令牌 25 的 Ingester 负责哈希范围 1-25。
Quorum 一致性
由于所有 Distributor 共享访问同一个哈希环,写入请求可以发送到任何 Distributor。
为了确保查询结果的一致性,Loki 在读写操作上使用Dynamo 风格的 quorum 一致性。这意味着 Distributor 在响应发起发送的客户端之前,会等待至少一半加一个接收样本的 Ingester 返回确认响应。
Ingester
**Ingester** 服务负责在写入路径上持久化数据并将其发送到长期存储(Amazon Simple Storage Service、Google Cloud Storage、Azure Blob Storage 等),并在读取路径上返回最近摄取的内存中日志数据以供查询。
Ingester 包含一个 _lifecycler_,用于管理 Ingester 在哈希环中的生命周期。每个 Ingester 的状态可以是 `PENDING`、`JOINING`、`ACTIVE`、`LEAVING` 或 `UNHEALTHY`。
`PENDING` 是 Ingester 等待来自另一个 `LEAVING` 状态的 Ingester 进行handoff 时的状态。这仅适用于旧版部署模式。
注意
Handoff 是一种已弃用的行为,主要用于无状态的 Ingester 部署,不推荐使用。相反,建议使用有状态部署模型以及预写日志。
`JOINING` 是 Ingester 正在将自己的令牌插入 Ring 并初始化自己的状态。它可以接收其拥有的令牌的写入请求。
`ACTIVE` 是 Ingester 完全初始化的状态。它可以接收其拥有的令牌的写入和读取请求。
`LEAVING` 是 Ingester 正在关闭的状态。它可以接收内存中仍然存在的数据的读取请求。
`UNHEALTHY` 是 Ingester 心跳失败的状态。`UNHEALTHY` 由 Distributor 在定期检查 Ring 时设置。
Ingester 接收到的每个日志流都在内存中构建成许多“块”(chunk),并按可配置的时间间隔刷新到后端存储。
块在以下情况被压缩并标记为只读:
- 当前块达到容量上限(一个可配置的值)。
- 当前块长时间未更新。
- 发生刷新操作。
每当一个块被压缩并标记为只读时,一个新的可写块会取而代之。
如果 Ingester 进程崩溃或突然退出,所有尚未刷新的数据将会丢失。Loki 通常配置为复制每个日志的多个副本(通常为 3 个)以降低此风险。
当向持久存储提供商发生刷新时,块会根据其租户、标签和内容进行哈希处理。这意味着具有相同数据副本的多个 Ingester 不会将相同数据写入后端存储两次,但如果任何写入到某个副本失败,后端存储中会创建多个不同的块对象。请参阅Querier 了解数据如何进行去重。
时间戳顺序
Loki 默认配置为接受乱序写入。
当未配置为接受乱序写入时,Ingester 会验证摄入的日志行是否按顺序排列。当 Ingester 收到不符合预期顺序的日志行时,该行将被拒绝,并向用户返回错误。
Ingester 会验证日志行是否按时间戳升序接收。每条日志的时间戳都比前一条日志的时间晚。当 Ingester 收到不遵循此顺序的日志时,该日志行将被拒绝,并返回错误。
来自每个唯一标签集的日志都在内存中构建成“块”(chunk),然后刷新到后端存储。
如果 Ingester 进程崩溃或突然退出,所有尚未刷新的数据可能会丢失。Loki 通常配置有预写日志,可以在重启时进行 _重放_,并且配置了每个日志的 `replication_factor`(通常为 3)以降低此风险。
当未配置为接受乱序写入时,推送到 Loki 的给定流(标签的唯一组合)的所有日志行必须具有比之前接收的日志行更新的时间戳。然而,对于同一流中具有相同纳秒时间戳的日志,有两种处理情况:
如果传入行与之前接收的行完全匹配(时间戳和日志文本都匹配),则传入行将被视为完全重复并忽略。
如果传入行与前一行具有相同的时间戳但内容不同,则日志行将被接受。这意味着同一时间戳可能存在两条不同的日志行。
Handoff
警告
Handoff 是一种已弃用的行为,主要用于无状态的 Ingester 部署,不推荐使用。相反,建议使用有状态部署模型以及预写日志。
默认情况下,当 Ingester 关闭并尝试离开哈希环时,它会等待查看是否有新的 Ingester 尝试加入,然后进行刷新,并尝试发起 handoff。Handoff 会将退出 Ingester 拥有的所有令牌和内存中块转移给新的 Ingester。
在加入哈希环之前,Ingester 会在 `PENDING` 状态下等待 Handoff 发生。在可配置的超时后,未接收到转移的 `PENDING` 状态的 Ingester 将正常加入 Ring,插入一组新的令牌。
此过程用于避免在关闭时刷新所有块,这是一个缓慢的过程。
文件系统支持
虽然 Ingester 通过 BoltDB 支持写入文件系统,但这仅在单进程模式下有效,因为Querier 需要访问相同的后端存储,而 BoltDB 在任何给定时间只允许一个进程锁定 DB。
Query Frontend
**Query Frontend** 是一个**可选服务**,提供 Querier 的 API 端点,可用于加速读取路径。部署 Query Frontend 后,传入的查询请求应定向到 Query Frontend 而非 Querier。集群中仍需要 Querier 服务来执行实际查询。Query Frontend 内部执行一些查询调整,并将查询保存在内部队列中。在此设置中,Querier 充当工作进程,从队列中拉取任务,执行任务,然后返回给 Query Frontend 进行聚合。因此,需要使用 Query Frontend 地址配置 Querier(通过 `-querier.frontend-address` CLI 标志),以便它们连接到 Query Frontend。
Query Frontend 是**无状态的**。然而,由于内部队列的工作方式,建议运行少量 Query Frontend 副本以获得公平调度的优势。大多数情况下,两个副本就足够了。
队列管理
如果未使用独立的Query Scheduler 组件,Query Frontend 也会执行基本的查询队列管理。
确保可能导致 Querier 内存不足 (OOM) 错误的大型查询在失败时会重试。这使得管理员可以为查询分配较少的内存,或者乐观地并行运行更多小型查询,有助于降低总拥有成本 (TCO)。
- 通过使用先进先出 (FIFO) 队列将多个大型请求分发到所有 Querier,防止它们集中在一个 Querier 上。
- 通过公平地调度租户间的查询,防止单个租户对其他租户进行拒绝服务 (DOS) 攻击。
- 拆分
Query Frontend 支持缓存指标查询结果并在后续查询中重用。如果缓存结果不完整,Query Frontend 会计算所需的子查询并在下游 Querier 上并行执行。Query Frontend 可以选择将查询与其步长参数对齐,以提高查询结果的可缓存性。结果缓存兼容任何 Loki 缓存后端(目前支持 Memcached、Redis 和内存缓存)。
Query Frontend 还支持以负向缓存(negative cache)的形式缓存日志查询。这意味着 Loki 不缓存量化时间范围内的日志结果,而是仅缓存量化时间范围内的空结果。这比缓存实际结果更高效,因为日志查询有限制(通常 1000 个结果),而且如果您有一个长时间范围的查询只匹配少量行,并且只缓存实际结果,那么您仍然需要处理大量数据以及结果缓存中的数据,才能验证没有其他匹配项。
缓存
指标查询
索引统计查询
日志查询
Query Frontend 缓存索引统计查询结果,类似于指标查询结果。此缓存仅在使用单存储 TSDB 时适用。
日志量查询
Query Frontend 缓存日志量查询结果,类似于指标查询结果。此缓存仅在使用单存储 TSDB 时适用。
Query Scheduler
**Query Scheduler** 是一个**可选服务**,提供比Query Frontend 更高级的队列功能。在 Loki 部署中使用此组件时,Query Frontend 会将拆分后的查询推送到 Query Scheduler,Query Scheduler 会将它们放入内部内存队列中。每个租户都有一个队列,以确保所有租户之间的查询公平性。连接到 Query Scheduler 的 Querier 充当工作进程,从队列中拉取其任务,执行任务,然后返回给 Query Frontend 进行聚合。因此,需要使用 Query Scheduler 地址配置 Querier(通过 `-querier.scheduler-address` CLI 标志),以便它们连接到 Query Scheduler。
Query Scheduler 是**无状态的**。然而,由于内存队列的存在,建议运行多个副本来保持高可用性。大多数情况下,两个副本就足够了。
**Querier** 服务负责执行 Log Query Language (LogQL) 查询。Querier 可以直接处理来自客户端的 HTTP 请求(在“单体二进制”模式下,或作为“简单可扩展部署”读取路径的一部分),或者从 Query Frontend 或 Query Scheduler 拉取子查询(在“微服务”模式下)。
它从 Ingester 和长期存储中获取日志数据。Querier 会查询所有 Ingester 以获取内存数据,然后才会回退到对后端存储运行相同的查询。由于复制因子的存在,Querier 可能收到重复数据。为了解决这个问题,Querier 内部会**去重**具有相同纳秒时间戳、标签集和日志消息的数据。
Querier
Index Gateway
**Index Gateway** 服务负责处理和提供元数据查询。元数据查询是从索引中查找数据的查询。Index Gateway 仅由“shipper store”使用,例如单存储 TSDB 或单存储 BoltDB。
Index Gateway
Query Frontend 查询 Index Gateway 获取查询的日志量,以便决定如何对查询进行分片。Querier 查询 Index Gateway 获取给定查询的块引用,以便知道要获取和查询哪些块。
Index Gateway 可以运行在 `simple` 或 `ring` 模式下。在 `simple` 模式下,每个 Index Gateway 实例提供所有租户的所有索引。在 `ring` 模式下,Index Gateway 使用一致性哈希环在可用实例之间按租户分发和分片索引。
Compactor
Compactor
**Compactor** 服务由“shipper store”使用,例如单存储 TSDB 或单存储 BoltDB,用于将 Ingester 生成并发送到对象存储的多个索引文件合并为每天每个租户一个索引文件。这使得索引查找更高效。
为此,Compactor 会定期从对象存储下载文件,将它们合并为一个文件,上传新创建的索引,并清理旧文件。
在 Loki 部署中,Compactor 服务通常作为单个实例运行。
Ruler
Ruler
**Ruler** 服务管理并评估在规则配置中提供的规则和/或告警表达式。规则配置存储在对象存储中(或本地文件系统),可以通过 Ruler API 或直接将文件上传到对象存储进行管理。
另外,Ruler 还可以将规则评估委托给 Query Frontend。此模式称为远程规则评估,用于获得 Query Frontend 的查询拆分、查询分片和缓存的优势。
运行多个 Ruler 时,它们使用一致性哈希环在可用 Ruler 实例之间分发规则组。
警告
Bloom Planner
此功能是实验性功能。不提供工程和 On-call 支持。不提供 SLA。
Bloom Planner 服务负责规划 Bloom 的创建任务。它作为单例运行,并提供一个队列,Bloom Builder 从中拉取任务。规划定期运行,并考虑给定日期和租户已构建的 Bloom,以及需要新添加的序列。此服务也用于应用 Bloom 保留。
Bloom Builder
警告
Bloom Planner
Bloom Builder 服务负责处理 Bloom Planner 创建的任务。Bloom Builder 从日志条目的结构化元数据创建 Bloom 块。生成的 Bloom 会按给定日期分组到跨越多序列和多个块的 Bloom 块中。此组件还构建元数据文件,用于跟踪每个序列和 TSDB 索引文件可用的块。
该服务是无状态的,并且可水平扩展。
Bloom Gateway
警告
Bloom Planner
Bloom Gateway 服务负责处理和提供块过滤请求。Index Gateway 在计算块引用或给定查询的分片时,会查询 Bloom Gateway。网关服务接收一个块列表和一个过滤表达式,并将其与 Bloom 进行匹配,过滤掉与给定标签过滤表达式不匹配的任何块。
该服务可水平扩展。当运行多个实例时,客户端 (Index Gateway) 会根据引用的 Bloom 块的哈希将请求分片到各个实例。