菜单
开源

Loki 架构

Grafana Loki 采用基于微服务的架构,设计为水平可伸缩的分布式系统。该系统包含多个可以独立并行运行的组件。Grafana Loki 的设计将所有组件的代码编译到一个单一二进制文件或 Docker 镜像中。`-target` 命令行标志控制该二进制文件作为哪些组件运行。

为了方便入门,您可以将 Grafana Loki 运行在所有组件同时运行在一个进程中的“单一二进制”模式下,或将组件分组为读、写和后端部分的“简单可伸缩部署”模式下。

Grafana Loki 设计为可以根据您的需求变化轻松地在不同模式下重新部署集群,无需配置更改或只需少量配置更改。

更多信息请参考部署模式组件

Loki components

存储

Loki 将所有数据存储在单一对象存储后端中,例如 Amazon Simple Storage Service (S3)、Google Cloud Storage (GCS)、Azure Blob Storage 等。这种模式使用一个名为 **index shipper**(简称 **shipper**)的适配器,以与在对象存储中存储 Chunk 文件相同的方式存储索引(TSDB 或 BoltDB)文件。这种操作模式在 Loki 2.0 版本中正式推出,它快速、经济且简单。所有当前和未来的开发都基于此。

在 2.0 版本之前,Loki 为索引和 Chunk 使用不同的存储后端。更多信息请参考传统存储

数据格式

Grafana Loki 主要有两种文件类型:**索引**和 **Chunk**。

  • **索引**是查找特定标签集日志的目录。
  • **Chunk**是特定标签集日志条目的容器。

Loki data format: chunks and indexes

上图展示了 Chunk 中存储的数据和索引中存储的数据的高级概览。

索引格式

目前支持两种与 Index Shipper 配合使用的单一存储索引格式:

  • TSDB (推荐)

    时序数据库(简称 TSDB)是索引格式,最初由Prometheus维护者开发,用于时序(指标)数据。

    它可扩展,并且相对于已弃用的 BoltDB 索引有许多优势。Loki 中的新存储功能仅在使用 TSDB 时可用。

  • BoltDB (已弃用)

    Bolt 是一个用 Go 语言编写的低级事务性键值存储。

Chunk 格式

Chunk 是特定时间范围内的流(唯一的标签集)的日志行的容器。

以下 ASCII 图详细描述了 Chunk 格式。

----------------------------------------------------------------------------
|                        |                       |                         |
|     MagicNumber(4b)    |     version(1b)       |      encoding (1b)      |
|                        |                       |                         |
----------------------------------------------------------------------------
|                      #structuredMetadata (uvarint)                       |
----------------------------------------------------------------------------
|      len(label-1) (uvarint)      |          label-1 (bytes)              |
----------------------------------------------------------------------------
|      len(label-2) (uvarint)      |          label-2 (bytes)              |
----------------------------------------------------------------------------
|      len(label-n) (uvarint)      |          label-n (bytes)              |
----------------------------------------------------------------------------
|                      checksum(from #structuredMetadata)                  |
----------------------------------------------------------------------------
|           block-1 bytes          |           checksum (4b)               |
----------------------------------------------------------------------------
|           block-2 bytes          |           checksum (4b)               |
----------------------------------------------------------------------------
|           block-n bytes          |           checksum (4b)               |
----------------------------------------------------------------------------
|                           #blocks (uvarint)                              |
----------------------------------------------------------------------------
| #entries(uvarint) | mint, maxt (varint)  | offset, len (uvarint)         |
----------------------------------------------------------------------------
| #entries(uvarint) | mint, maxt (varint)  | offset, len (uvarint)         |
----------------------------------------------------------------------------
| #entries(uvarint) | mint, maxt (varint)  | offset, len (uvarint)         |
----------------------------------------------------------------------------
| #entries(uvarint) | mint, maxt (varint)  | offset, len (uvarint)         |
----------------------------------------------------------------------------
|                          checksum(from #blocks)                          |
----------------------------------------------------------------------------
| #structuredMetadata len (uvarint) | #structuredMetadata offset (uvarint) |
----------------------------------------------------------------------------
|     #blocks len (uvarint)         |       #blocks offset (uvarint)       |
----------------------------------------------------------------------------

`mint` 和 `maxt` 分别描述了最小和最大 Unix 纳秒时间戳。

该`structuredMetadata` 部分存储不重复的字符串。它用于存储来自结构化元数据的标签名称和标签值。注意,`structuredMetadata` 部分内的标签字符串及其长度是压缩存储的。

块格式

块由一系列条目组成,每个条目都是一个独立的日志行。注意,块的字节是压缩存储的。以下是其解压后的形式

-----------------------------------------------------------------------------------------------------------------------------------------------
|  ts (varint)  |  len (uvarint)  |  log-1 bytes  |  len(from #symbols)  |  #symbols (uvarint)  |  symbol-1 (uvarint)  | symbol-n*2 (uvarint) |
-----------------------------------------------------------------------------------------------------------------------------------------------
|  ts (varint)  |  len (uvarint)  |  log-2 bytes  |  len(from #symbols)  |  #symbols (uvarint)  |  symbol-1 (uvarint)  | symbol-n*2 (uvarint) |
-----------------------------------------------------------------------------------------------------------------------------------------------
|  ts (varint)  |  len (uvarint)  |  log-3 bytes  |  len(from #symbols)  |  #symbols (uvarint)  |  symbol-1 (uvarint)  | symbol-n*2 (uvarint) |
-----------------------------------------------------------------------------------------------------------------------------------------------
|  ts (varint)  |  len (uvarint)  |  log-n bytes  |  len(from #symbols)  |  #symbols (uvarint)  |  symbol-1 (uvarint)  | symbol-n*2 (uvarint) |
-----------------------------------------------------------------------------------------------------------------------------------------------

`ts` 是日志的 Unix 纳秒时间戳,而 `len` 是日志条目的字节长度。

符号存储对Chunk 的 `structuredMetadata` 部分中包含标签名称和值的实际字符串的引用。

写入路径

从高层面看,Loki 的写入路径工作流程如下:

  1. 分发器接收包含流和日志行的 HTTP POST 请求。
  2. 分发器对请求中包含的每个流进行哈希计算,以便根据一致性哈希环中的信息确定需要将其发送到的 ingester 实例。
  3. 分发器将每个流发送到相应的 ingester 及其副本(根据配置的复制因子)。
  4. ingester 接收包含日志行的流,并为该流的数据创建 Chunk 或追加到现有 Chunk。每个 Chunk 对于租户和标签集来说是唯一的。
  5. ingester 确认写入。
  6. 分发器等待大多数(法定数量)的 ingester 确认其写入。
  7. 如果收到至少法定数量的确认写入,分发器将返回成功(2xx 状态码);如果写入操作失败,则返回错误(4xx 或 5xx 状态码)。

参考组件以获取有关写入路径中涉及组件的更详细描述。

读取路径

从高层面看,Loki 的读取路径工作流程如下:

  1. 查询前端接收包含 LogQL 查询的 HTTP GET 请求。
  2. 查询前端将查询拆分为子查询,并将其传递给查询调度器。
  3. 查询器从调度器中拉取子查询。
  4. 查询器将查询传递给所有 ingester 以获取内存中的数据。
  5. ingester 返回匹配查询的内存中数据(如果有)。
  6. 如果 ingester 返回的数据为空或不足,查询器会延迟加载后端存储中的数据并对其运行查询。
  7. 查询器遍历所有接收到的数据并进行去重,然后将子查询结果返回给查询前端。
  8. 查询前端等待查询器的所有子查询完成并返回结果。
  9. 查询前端将单个结果合并为最终结果并将其返回给客户端。

参考组件以获取有关读取路径中涉及组件的更详细描述。

多租户

所有数据,无论是内存中的还是长期存储中的,都可以按租户 ID 进行分区,该 ID 从请求中的`X-Scope-OrgID` HTTP 请求头中获取,当 Grafana Loki 运行在多租户模式下。当 Loki **不**在多租户模式下运行时,该请求头会被忽略,并且租户 ID 被设置为`fake`,它将出现在索引和存储的 Chunk 中。