Grafana Loki 架构完全解读:微服务组件、读写路径与索引/块存储格式
【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki
Loki("Like Prometheus, but for logs")采用微服务架构设计,以水平可扩展的分布式系统形态运行;同时它把全部组件编译进同一个二进制或 Docker 镜像,通过-target启动参数决定进程扮演的角色。本文以 Loki 架构官方文档 为主线,结合本仓库的 组件文档、部署模式文档、TSDB 存储文档 以及pkg/chunkenc/memchunk.go等源码,系统讲解 Loki 的总体架构、存储与数据格式、写入路径、读取路径和多租户机制,帮助你从"能跑"进阶到"真正理解 Loki 为什么这么设计"。
一、总体架构:单二进制承载的微服务
Loki 是一个由众多微服务组成的分布式系统,但它采用了独特的构建模型:所有微服务都存在于同一个二进制中。无论你是用 Docker 镜像还是直接编译的可执行文件启动 Loki,都会发现可执行文件是同一份,区别只在于启动时传入的-target命令行标志:
-target=all:单进程内同时运行全部组件(monolithic / 单二进制模式);-target=read、-target=write、-target=backend:把组件逻辑分组为读、写、后端三部分(simple scalable deployment,简单可扩展部署,已被 HA Monolithic 模式取代);-target=<具体组件名>:只运行某一个组件(microservices 微服务模式)。
这种"一份二进制、多角色启动"的设计带来两个关键能力:
- 快速上手:以单二进制模式启动,全部组件在一个进程内同时运行,几分钟即可完成本地体验;
- 平滑演进:Loki 将"存储的数据"与"摄取/查询数据的软件"解耦,因此当你的需求变化时,可以几乎不修改配置(或仅做极小改动)就把集群切换到另一种部署模式。具体模式对比与切换要求见 部署模式,各组件职责详见 组件说明。
二、存储架构:单一对象存储后端
Loki 将所有数据存放在同一个对象存储后端中,例如:
- Amazon Simple Storage Service(S3)
- Google Cloud Storage(GCS)
- Azure Blob Storage
- 以及其他 S3 兼容存储
这种模式通过一个名为index shipper(简称 shipper)的适配器,把索引文件(TSDB)像 chunk 文件一样存放在对象存储中。该模式自Loki 2.0起正式 GA(generally available),具备快速、经济、简单的特点,也是当前及未来所有开发工作的落点。2.0 之前 Loki 为索引和 chunk 使用不同的存储后端,这类"遗留存储"(legacy storage)方案在新版本中已不再推荐。
三、数据格式:索引(Index)与块(Chunk)
Loki 有两种主要文件类型:
- index(索引):相当于一份"目录",记录了在特定标签集合下到哪里能找到日志;
- chunk(块):承载特定标签集合下日志条目的容器。
下图展示了 chunk 与 index 中存储内容的高层概览:
3.1 索引格式:TSDB(推荐)
当前通过 index shipper 单一存储支持的索引格式只有一种——TSDB(Time Series Database)。TSDB 是 Prometheus 维护者为时序(指标)数据开发的一种索引格式,可扩展性强,相比已被弃用的 BoltDB 索引具有诸多优势;Loki 的新存储特性仅在 TSDB 上可用。
TSDB 从 Loki v2.8 起成为推荐索引,详细说明见 Single Store TSDB 文档。在该文档中可以看到一个完整的启用示例(节选):
schema_config: configs: # 旧 boltdb-shipper schema,仅作参考,无需修改 - from: "2023-01-03" # <---- 过去的日期 index: period: 24h prefix: index_ object_store: gcs schema: v12 store: boltdb-shipper # 新的 TSDB schema - from: "2023-01-05" # <---- 未来的日期 index: period: 24h prefix: index_ object_store: gcs schema: v13 store: tsdb storage_config: tsdb_shipper: active_index_directory: /data/tsdb-index cache_location: /data/tsdb-cache index_gateway_client: server_address: dns:///index-gateway.<namespace>.svc.cluster.local:9095TSDB 还带来两个值得一提的特性:
- 动态查询分片(Dynamic Query Sharding):索引中额外记录了每个 chunk 的大小(KB)与行数,查询前端据此规划分片。默认目标为每个分片处理约 300–600MB 数据,由
limits_config中的tsdb_max_bytes_per_shard控制(默认 600MB),超过目标时 Loki 会将分片数翻倍。 - 无需索引缓存:TSDB 格式紧凑且经过优化,Loki 目前不为其使用索引缓存。
3.2 Chunk 格式
一个 chunk 是某个流(stream,即唯一标签集合)在特定时间范围内日志行的容器。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区段用于存储不重复的字符串,即来自结构化元数据的标签名与标签值;该区段内的标签字符串与长度信息是压缩存储的。
这些布局在源码中可以得到印证:pkg/chunkenc/memchunk.go定义了ChunkFormatV1到ChunkFormatV4四个版本、magicNumber = 0x12EE56A(即图中的 MagicNumber)、blocksPerChunk = 10(默认每 chunk 最多 10 个 block),并采用 CRC32-Castagnoli 多项式对区块计算校验和。结构化元数据从 ChunkFormatV4 引入,对应的 schema 版本号 ≥ 13,且要求索引类型为tsdb。
3.3 Block 格式
一个 block 由一系列 entry 组成,每个 entry 就是一条独立的日志行。block 的字节是压缩存储的,下图为其解压后的形态:
----------------------------------------------------------------------------------------------------------------------------------------------- | 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是日志条目的字节长度;- symbols(符号)保存对 chunk 中
structuredMetadata区段内实际标签名/标签值字符串的引用——即 block 内不重复存储完整字符串,而是用符号序号指向它们,从而大幅节省空间。
四、部署模式一览
Loki 的架构决定了你可以用三种方式部署(详见 部署模式):
| 维度 | Monolithic 单二进制 | HA Monolithic 高可用单二进制 | Microservices 微服务 |
|---|---|---|---|
| 数据持久性 | ✅ | ✅ | ✅ |
| 高可用 | ❌ | ✅ | ✅ |
| 执行路径分离 | ❌ | ❌ | ✅ |
| 运维复杂度 | 🟩 低 | 🟧 中 | 🟥 高 |
| 可扩展性 | 🟥 低 | 🟧 中 | 🟩 高 |
- Monolithic 模式(
-target=all):适合快速上手实验与约 20GB/天以内的小规模读写; - HA Monolithic 模式:通过共享对象存储 +
memberlist环 + 复制因子 3 实现高可用,取代了将在 Loki 4.0 移除的 Simple Scalable Deployment(SSD); - Microservices 模式:每个组件独立进程,扩展粒度最细,主要面向 Kubernetes 大集群。
想查看当前版本支持的全部 target,可以运行:
docker run docker.io/grafana/loki:3.2.1 -config.file=/etc/loki/local-config.yaml -list-targets组件与部署目标的映射
下表展示了各组件在不同-target下的归属(数据来自 组件文档):
| 组件 | 单独运行 | 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 | ||
| Pattern ingester 模式摄取器(可选) | x | x | x | ||
| Bloom Planner(实验性) | x | x | |||
| Bloom Builder(实验性) | x | x | |||
| Bloom Gateway(实验性) | x | x |
五、写入路径(Write Path)
从高层看,Loki 的写入路径按以下顺序工作:
- distributor(分发器)收到携带 streams(流)与日志行的 HTTP POST 请求;
- 分发器对请求中的每个 stream 做哈希,根据一致性哈希环(consistent hash ring)中的信息确定该送往哪个 ingester 实例;
- 分发器把每个 stream 发送给对应的 ingester 及其副本(副本数量由配置的复制因子 replication factor决定);
- ingester(摄取器)收到带日志行的 stream,为该 stream 创建新 chunk 或追加到已有 chunk(chunk 按租户 + 标签集合唯一);
- ingester 确认写入;
- 分发器等待**大多数(quorum)**ingester 确认写入;
- 若至少达到 quorum 的写入被确认,分发器返回成功(2xx 状态码);否则返回错误(4xx 或 5xx 状态码)。
写入路径涉及的关键机制(详见 组件文档 与 一致性哈希环文档):
- 复制因子(Replication Factor):通常为 3。quorum 定义为
floor(replication_factor / 2) + 1,即复制因子为 3 时至少需要 2 个 ingester 写入成功。若 3 个中只成功 2 个,可容忍丢失 1 个 ingester 但不能容忍丢失 2 个。 - WAL(Write Ahead Log):ingester 将写入持久化到磁盘的 WAL,即使进程崩溃,重启时可回放。复制因子保证滚动重启时写入不中断,WAL 保证单点崩溃不丢数据,两者互补。
- 一致性哈希:stream 用
租户 ID + 标签集合做哈希,在哈希环上顺时针找到第一个大于该哈希值的 token 所属 ingester;复制因子大于 1 时继续取后续属于不同 ingester 的 token。每个 token 负责一段哈希区间,从而实现数据在 ingester 间的均匀分布。 - quorum 一致性:所有分发器共享同一个哈希环,写请求可发给任意分发器;分发器等待"半数 + 1"个 ingester 的肯定响应后才向客户端确认(Dynamo 风格)。
六、读取路径(Read Path)
从高层看,Loki 的读取路径按以下顺序工作:
- query frontend(查询前端)收到携带 LogQL 查询的 HTTP GET 请求;
- 查询前端把查询拆分为多个子查询,交给 query scheduler(查询调度器);
- querier(查询器)从调度器拉取子查询;
- 查询器把查询发给所有 ingester以获取内存中的热数据;
- ingester 返回命中的内存数据(如有);
- 若 ingester 返回的数据不足或没有,查询器**惰性(lazily)**地从后端存储加载数据并执行查询;
- 查询器遍历所有收到的数据并去重,把子查询结果返回给查询前端;
- 查询前端等待一个查询的所有子查询完成并返回;
- 查询前端合并各子查询结果,得到最终结果返回给客户端。
关键点补充(详见 组件文档):
- 去重:由于复制因子的存在,查询器可能收到重复数据;查询器对"相同纳秒时间戳 + 相同标签集合 + 相同日志内容"的数据做内部去重。
- 查询前端是一个可选服务,负责拆分大查询、缓存(指标查询结果缓存、日志查询的空结果负缓存)、FIFO 排队与租户间公平调度;建议运行 2 个副本。
- 查询调度器同样可选,提供每租户独立队列以保证跨租户查询公平性。
- 索引网关(Index Gateway):仅用于单存储 TSDB,负责元数据查询——查询前端向它获取日志量以决定分片,查询器向它获取 chunk 引用以决定取哪些 chunk。
七、多租户(Multi-tenancy)
Loki 支持多租户隔离:无论是内存数据还是长期存储数据,都可以按tenant ID(租户 ID)分区。当 Loki 运行在多租户模式时,租户 ID 取自请求中的X-Scope-OrgIDHTTP 头;当 Loki未开启多租户模式时,该头会被忽略,租户 ID 固定为fake——这个 ID 会出现在索引与存储的 chunk 中。
在源码中可以找到大量对X-Scope-OrgID的读取与透传逻辑,例如pkg/ingester/flush.go、pkg/logcli/client/client.go等;各组件在 push、查询、flush 等路径上都会携带该头以完成租户路由与隔离。
八、深入阅读
围绕本文主题,可以继续在仓库中查阅以下一手资料:
- 组件详解:分发器的校验/预处理/限流、摄取器生命周期、查询前端缓存策略等细节
- 部署模式:三种模式对比与 HA Monolithic 配置要求
- 一致性哈希环:
common.ring与memberlist配置 - Single Store TSDB:TSDB schema 迁移与动态查询分片
- 结构化元数据:chunk 中
structuredMetadata区段的开启与查询方式 - WAL 说明、保留策略、日志删除
- 源码实现:chunk 编码与校验位于 pkg/chunkenc/memchunk.go,组件入口统一在 cmd/loki/main.go 中按
-target路由启动
【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考