Loki 架构
Grafana Loki 采用基于微服务的架构,设计为水平可伸缩的分布式系统。该系统包含多个可以独立并行运行的组件。Grafana Loki 的设计将所有组件的代码编译到一个单一二进制文件或 Docker 镜像中。`-target` 命令行标志控制该二进制文件作为哪些组件运行。
为了方便入门,您可以将 Grafana Loki 运行在所有组件同时运行在一个进程中的“单一二进制”模式下,或将组件分组为读、写和后端部分的“简单可伸缩部署”模式下。
Grafana Loki 设计为可以根据您的需求变化轻松地在不同模式下重新部署集群,无需配置更改或只需少量配置更改。
存储
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 中存储的数据和索引中存储的数据的高级概览。
索引格式
目前支持两种与 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 的写入路径工作流程如下:
- 分发器接收包含流和日志行的 HTTP POST 请求。
- 分发器对请求中包含的每个流进行哈希计算,以便根据一致性哈希环中的信息确定需要将其发送到的 ingester 实例。
- 分发器将每个流发送到相应的 ingester 及其副本(根据配置的复制因子)。
- ingester 接收包含日志行的流,并为该流的数据创建 Chunk 或追加到现有 Chunk。每个 Chunk 对于租户和标签集来说是唯一的。
- ingester 确认写入。
- 分发器等待大多数(法定数量)的 ingester 确认其写入。
- 如果收到至少法定数量的确认写入,分发器将返回成功(2xx 状态码);如果写入操作失败,则返回错误(4xx 或 5xx 状态码)。
参考组件以获取有关写入路径中涉及组件的更详细描述。
读取路径
从高层面看,Loki 的读取路径工作流程如下:
- 查询前端接收包含 LogQL 查询的 HTTP GET 请求。
- 查询前端将查询拆分为子查询,并将其传递给查询调度器。
- 查询器从调度器中拉取子查询。
- 查询器将查询传递给所有 ingester 以获取内存中的数据。
- ingester 返回匹配查询的内存中数据(如果有)。
- 如果 ingester 返回的数据为空或不足,查询器会延迟加载后端存储中的数据并对其运行查询。
- 查询器遍历所有接收到的数据并进行去重,然后将子查询结果返回给查询前端。
- 查询前端等待查询器的所有子查询完成并返回结果。
- 查询前端将单个结果合并为最终结果并将其返回给客户端。
参考组件以获取有关读取路径中涉及组件的更详细描述。
多租户
所有数据,无论是内存中的还是长期存储中的,都可以按租户 ID 进行分区,该 ID 从请求中的`X-Scope-OrgID` HTTP 请求头中获取,当 Grafana Loki 运行在多租户模式下。当 Loki **不**在多租户模式下运行时,该请求头会被忽略,并且租户 ID 被设置为`fake`,它将出现在索引和存储的 Chunk 中。