在云原生时代,日志是可观测性(Observability)的三大支柱之一(另外两个是指标和链路追踪)。传统的日志系统(如 ELK Stack)虽然功能强大,但其高昂的存储成本和复杂的运维管理一直是挑战。Loki 作为一种新一代的日志聚合系统,以其独特的“只索引标签,不索引全文”的设计理念,提供了成本效益高、易于部署和管理的日志解决方案,尤其适合 Kubernetes 环境。

核心思想:
Loki 是一个受 Prometheus 启发而设计的日志聚合系统,它不索引日志内容本身,而是通过少量标签对日志流(log stream)进行索引。这种设计显著降低了存储和索引的成本,使得用户可以通过标签过滤日志流,然后进行 Grep 式的全文搜索。它是 Grafana Labs 家族产品的一员,与 Prometheus 和 Grafana 紧密集成,提供了统一的可观测性体验。


一、Loki 简介与核心理念

Loki 是 Grafana Labs 于 2018 年开源的日志聚合系统。其设计的核心理念与 Prometheus 有着异曲同工之处:

  • 受 Prometheus 启发:Loki 的数据模型、标签(labels)使用、查询语言(LogQL)和架构设计都深受 Prometheus 的影响。
  • 只索引标签 (Index Labels Only):这是 Loki 最核心的创新。与传统日志系统不同,Loki 不对日志的全文内容进行索引。它只提取和索引与日志流相关的少量元数据标签。这意味着:
    • 低存储成本:因为没有全文索引,索引数据量远小于传统方案。
    • 低运维复杂度:索引规模小,管理起来更简单。
    • 快速查询,但有局限性:查询首先通过标签快速筛选出相关的日志流,然后对这些日志流的原始内容进行 Grep 式的文本搜索。这意味着在没有特定标签的情况下进行大规模的全文搜索可能会很慢。

1.1 为什么要选择 Loki?

  1. 成本效益:显著降低日志存储和索引的成本,这一点对于大规模分布式系统尤为重要。
  2. 简单易用:部署和配置相对简单,尤其是对于熟悉 Prometheus 的用户。
  3. 与 Grafana 紧密集成:作为 Grafana Labs 的产品,Loki 与 Grafana 的集成达到了无缝对接的程度,提供了统一的指标、日志和追踪数据的可观测性平台。
  4. Prometheus 风格的查询语言 (LogQL):对于熟悉 PromQL 的用户,学习 LogQL 的曲线非常平缓。
  5. 云原生友好:为 Kubernetes 环境设计优化,可以方便地通过 DaemonSet 部署 Promtail 进行日志采集。

1.2 Loki 的术语与概念

  • 日志流 (Log Stream):由一个唯一的标签集(例如 {job="nginx", instance="web-01"})标识的一组时间排序的日志条目。Loki 的索引就是这些标签集。
  • 标签 (Labels):与 Prometheus 类似,标签是键值对,用于唯一标识一个日志流。它们是 Loki 查询的关键。
  • 条目 (Entry):日志流中的单个日志行,包含一个时间戳和一个原始日志文本。
  • LogQL:Loki 的查询语言,用于查询和聚合日志数据。它结合了标签选择器和文本过滤器。
  • Chunk (块):Loki 内部存储日志的基本单元。每个 Chunk 包含一个或多个日志流的日志条目,并以压缩格式存储。
  • Ingester (摄取器):Loki 的组件,负责接收 Promtail 等客户端发送的日志,进行标签处理,并将日志写入 Chunk,最终持久化到存储。
  • Querier (查询器):Loki 的组件,负责接收 LogQL 查询,从索引读取标签并找到相关的 Chunk,然后从存储中拉取 Chunk 进行文本搜索和返回结果。
  • Distributor (分配器):Loki 的前端服务,负责接收 Promtail 发送的日志,并将其负载均衡到 Ingester。
  • Pusher (推送器)Promtail 内部的组件,负责将日志推送到 Distributor

二、Loki 架构

Loki 的架构设计与 Prometheus 类似,通常采用微服务组件化,但在小规模部署时也可以作为单一二进制文件运行(monolithic 模式)。

主要组件:

  1. Clients (日志收集器)
    • Promtail (推荐):Grafana Labs 官方提供的日志收集代理,设计用于与 Loki 配合使用。它部署在每个节点上,负责从指定路径、Docker 日志等文件中抓取日志,并将其标签化(从 Kubernetes 元数据、文件名路径等提取),然后推送到 Loki 的 Distributor。
    • Fluent Bit / Fluentd:也可以用于收集日志并将其转发到 Loki。需要配置相应的 Loki 输出插件。
  2. Distributor (分配器)
    • 接收来自客户端的日志流。
    • 对接收到的日志进行验证和预处理。
    • 通过一致性哈希将日志流分配给 Ingester 进行处理,确保相同标签集的日志流被路由到同一个 Ingester。
  3. Ingester (摄取器)
    • 负责接收 Distributor 分配的日志流。
    • 将收到的日志条目压缩并构建成内存中的 Chunk。
    • 定期将 Chunk 刷写(flush)到 Chunk Store(如 S3)。
    • 将日志流的标签索引写入 Index Store(如 DynamoDB)。
  4. Querier (查询器)
    • 接收 LogQL 查询。
    • 首先通过 Index Store 查找与查询标签匹配的日志流。
    • 然后向所有 Ingester 查询内存中的 Chunk,以及向 Chunk Store 查询已持久化的 Chunk。
    • 对查询到的原始日志数据执行 Grep 式的文本过滤和处理。
    • 将结果返回给客户端 (Grafana)。
  5. Index Store (索引存储)
    • 存储日志流的标签索引,而非日志全文。这是 Loki 低成本的核心。
    • 支持多种后端,如 AWS DynamoDB, Google Bigtable, Apache Cassandra, BoltDB(单体模式)。
  6. Chunk Store (块存储)
    • 存储实际的原始日志数据 Chunk。
    • 支持多种后端,如 AWS S3, Google Cloud Storage, Azure Blob Storage, MinIO, 文件系统。
  7. Compactor (压缩器) (可选):在某些存储后端(如 S3)上,可以将小的 Chunk 合并成更大的 Chunk,以优化存储和查询效率。
  8. Ruler (规则管理器) (可选):允许用户基于 LogQL 表达式定义警报规则,并在满足条件时触发警报到 Alertmanager。

三、Promtail - 日志采集代理

Promtail 是 Loki 的官方日志收集代理,被设计成与 Loki 和 Kubernetes 环境紧密集成。

3.1 Promtail 的工作原理

  1. Tailing (跟踪):Promtail 像 tail -f 一样跟踪本地文件系统上的日志文件。
  2. 提取标签 (Scraping/Labeling):Promtail 从日志文件的路径、Kubernetes Pod 的元数据(如 Pod 名称、Namespace、容器名称)中提取标签。它还支持通过 relabel_configs 从日志内容中提取额外标签。
  3. 日志流化 (Stream Creation):每个独特的标签集构成了 Loki 中的一个日志流。
  4. 推送 (Pushing):Promtail 将带有标签的日志条目推送到 Loki 的 Distributor。

3.2 Promtail 配置示例 (promtail-config.yaml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
server:
http_listen_port: 9080
grpc_listen_port: 0

positions:
filename: /tmp/positions.yaml # 记录 Promtail 已处理的日志文件位置,防止重复收集

clients:
- url: http://loki.loki.svc.cluster.local:3100/loki/api/v1/push # Loki Distributor 的地址

scrape_configs:
# 监听 Kubernetes Pod 日志
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
relabel_configs:
# 获取 namespace 标签
- source_labels: [__meta_kubernetes_namespace]
action: replace
target_label: namespace
# 获取 Pod 名称标签
- source_labels: [__meta_kubernetes_pod_name]
action: replace
target_label: pod
# 获取容器名称标签
- source_labels: [__meta_kubernetes_container_name]
action: replace
target_label: container
# 从文件路径中获取 app 标签(假设日志路径是 /var/log/pods/<namespace>_<pod-name>/<container-name>/<app-name>*.log)
# 更常见的做法是从 k8s annotations 获取,这里简化
- source_labels: [__meta_kubernetes_pod_uid] # 确保唯一性,日志文件路径一般包含UID
action: replace
target_label: _temp_pod_uid

# 匹配日志文件的实际路径
# 通常 Kubernetes 日志路径格式为 /var/log/pods/<namespace>_<pod-name>_<pod-uid>/<container-name>/<replica-id>.log
- source_labels: [__meta_kubernetes_pod_annotation_kubernetes_io_config_hash] # 使用可以从pod annotation提取的稳定标识
regex: (.+)
target_label: _temp_app_name # 临时标签用于后续提取
action: replace

- source_labels: [__path__] # promtail 会默认添加 __path__ 标签
regex: .*\/pods\/(?:a-z0-9 [<sup>1</sup>](?:[-a-z0-9]*[a-z0-9])?_){1,}(?P<pod_name>a-z0-9 [<sup>1</sup>](?:[-a-z0-9]*[a-z0-9])?(?:_a-z0-9 [<sup>1</sup>](?:[-a-z0-9]*[a-z0-9])?)*)\/(?P<container_name>a-z0-9 [<sup>2</sup>]((_?[a-z0-9])*)?)(\/.*)?\.log
action: replace
target_label: __path__ # 保持原始 __path__

- source_labels: [__meta_kubernetes_pod_container_name]
regex: (.+)
target_label: container # 重置以确保准确性

# 提取 job 标签
- source_labels: [__meta_kubernetes_pod_label_app] # 假设 Pod 有 app 标签
action: replace
target_label: app
regex: (.+)

# 最终日志流标签:job, namespace, pod, container, app
# 重要的: drop 掉 Promtail 内部不需要的标签,只保留最终发送给 Loki 的实际标签
- action: labelmap
regex: __meta_kubernetes_pod_label_(.+) # 将所有 pod label 转换为日志标签
- action: labeldrop
regex: "__meta_kubernetes_.*"
- action: labeldrop
regex: "filename|path" # 避免将 filename 和 path 作为标签
- action: labeldrop
regex: "_temp_.*" # 删除临时标签

pipeline_stages:
# 解析 JSON 格式日志
- json:
expressions:
level: level # 将 JSON 字段 level 提取为 label level
msg: message # 将 JSON 字段 message 作为日志内容
# set_extra_label: { "log_type": "json" } # 也可以添加固定标签

# 或者解析 Regex 格式日志
- regex:
expression: '^(?P<time>\S+)\s+(?P<level>\S+)\s+(?P<caller>\S+)\s+(?P<message>.*)$'
- labels:
level: # 将正则提取的 level 字段作为标签
- timestamp:
format: RFC3339Nano
source: time # 将正则提取的 time 字段作为时间戳

# 定义要从哪些路径抓取日志
# 通常 Promtail 运行在 Kubernetes DaemonSet 中,会挂载 /var/log/pods
# 或 /var/lib/docker/containers/<container-id>/*.log 路径
- targets:
- __path__: /var/log/pods/*/*/*.log

注意:上述 Promtail 配置针对 Kubernetes 环境下的日志抓取和标签提取是一个复杂且高度可定制的过程。实际生产环境的 relabel_configspipeline_stages 会更复杂,需要根据具体的日志格式和 Kubernetes 资源标签进行调整。

四、LogQL - Loki 的查询语言

LogQL 是 Loki 的查询语言,其语法灵感来源于 Prometheus 的 PromQL。它允许你通过标签选择器过滤日志流,然后通过文本过滤器进一步筛选日志内容,甚至进行日志内容的聚合和转换。

LogQL 查询通常分为两个部分:

  1. 日志流选择器 (Log Stream Selector):用于匹配日志流的标签。
  2. 日志管道 (Log Pipeline):对匹配到的日志流中的日志内容进行过滤、解析和转换。

4.1 日志流选择器

与 PromQL 类似,使用 {} 来匹配标签。

1
2
3
4
5
6
7
8
# 匹配所有 job=nginx 的日志流
{job="nginx"}

# 匹配 namespace 为 default 且 pod 是以 web- 开头的日志流
{namespace="default", pod=~"web-.*"}

# 匹配容器名称不是 promtail 的所有日志流
{container!="promtail"}

4.2 日志管道 (Log Pipeline)

日志管道允许你对日志流中的文本条目进行进一步处理。

4.2.1 文本过滤器 (Line Filters)

用于 Grep 式的文本匹配。

  • |= (包含):查询包含指定字符串的日志行。
  • != (不包含):查询不包含指定字符串的日志行。
  • |~ (正则匹配):查询匹配指定正则表达式的日志行。
  • !~ (正则不匹配):查询不匹配指定正则表达式的日志行。
1
2
3
4
5
6
7
8
# 匹配包含 "error" 字符串的日志行
{job="nginx"} |= "error"

# 匹配不包含 "health check" 的日志行
{job="nginx"} != "health check"

# 匹配包含 "failed" 或 "denied" 的日志行
{job="nginx"} |~ "failed|denied"

4.2.2 解析器 (Parsers)

用于从日志行中提取结构化数据,可以是从 JSON、Logfmt 或者正则表达式中提取。

  • | json: 解析 JSON 格式日志,提取字段。
    1
    2
    3
    4
    # 假设日志是 {"level":"info", "message":"User logged in"}
    {job="my_app"} | json
    # 现在你可以通过 `level` 标签进行过滤,或者在 Metrics Query 中使用
    {job="my_app"} | json | level="info"
  • | logfmt: 解析 Logfmt 格式日志。
    1
    2
    3
    # 假设日志是 level=info msg="User logged in" user_id=123
    {job="my_app"} | logfmt
    {job="my_app"} | logfmt | level="info" | user_id="123"
  • | regexp "<regex>": 使用正则表达式提取字段。
    1
    2
    3
    4
    # 假设日志是 "time=2023-01-01 level=INFO msg=Request from 192.168.1.1"
    {job="my_app"} | regexp "msg=Request from (?P<client_ip>\\S+)"
    # 现在你可以通过 `client_ip` 标签进行过滤
    {job="my_app"} | regexp "msg=Request from (?P<client_ip>\\S+)" | client_ip="192.168.1.1"

4.2.3 标签过滤器 (Label Filters)

对解析器提取的临时标签进行过滤。

  • | <label_name> = "value"
  • | <label_name> != "value"
  • | <label_name> =~ "regex"
  • | <label_name> !~ "regex"
1
2
# 提取 level 字段后,过滤 level 为 error 的日志
{job="my_app"} | json | level="error"

4.2.4 格式化器 (Formatter)

  • | line_format "{{.level}} {{.message}}": 重新格式化输出的日志行。

4.2.5 行限制器 (Line Limiter)

  • | limit <number>: 限制返回的日志行数。

4.3 LogQL 度量查询 (Metrics Query)

LogQL 不仅仅用于查看原始日志,它还可以将日志数据转换为指标,这与 PromQL 的功能类似。这在 Grafana 中非常有用,可以将日志中的特定模式转换为可绘制的图表。

常用的度量聚合函数:

  • count_over_time(range_vector):计算在给定时间范围内每个日志流的日志条目数量。
  • sum_by(label_list) (range_vector):按标签求和。
  • rate(range_vector):计算每个日志流的每秒日志行数。
  • bytes_rate(range_vector):计算每个日志流的每秒字节数。
  • bytes_over_time(range_vector):计算在给定时间范围内每个日志流的总字节数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 统计过去 5 分钟内,每个 job 和 level 的日志出现次数
sum by (job, level) (
count_over_time({namespace="default"} | json | level=~"info|warn|error"[5m])
)

# 计算过去 1 分钟内,每个 pod 的错误日志速率
sum by (pod) (
rate({namespace="default"} |= "error"[1m])
)

# 计算 nginx 服务的 4xx 错误请求率(假设日志中包含 status_code 字段)
sum by (job) (
rate({job="nginx"} | json | status_code=~"4\\d{2}"[1m])
)
/
sum by (job) (
rate({job="nginx"}[1m])
) * 100

4.4 LogQL 警报规则

你可以在 Loki 的 Ruler 或 Grafana 中基于 LogQL 表达式创建警报规则。

1
2
3
4
5
6
7
8
9
10
11
12
# Loki Ruler 告警规则示例
groups:
- name: loki_alerts
rules:
- alert: HighErrorRate
expr: sum by (job) (rate({job="my_app"} |= "error"[5m])) > 10
for: 1m
labels:
severity: warning
annotations:
summary: "应用 {{ $labels.job }} 错误率过高"
description: "应用 {{ $labels.job }} 在过去 5 分钟内每秒错误日志数量超过 10 条。"

五、Grafana 与 Loki 的集成

Grafana 是 Loki 最常用的前端。它提供了直观的界面来查询和可视化 Loki 中的日志数据。

5.1 配置 Loki 数据源

  1. 在 Grafana 中,进入 Configuration -> Data Sources
  2. 点击 Add data source,选择 Loki
  3. URL 字段中输入 Loki Distributor 的地址,例如 http://localhost:3100http://loki.loki.svc.cluster.local:3100
  4. 点击 Save & Test

5.2 Explore 中使用 Loki

Grafana 的 Explore 功能专门用于探索和调试数据。

  1. 切换到 Explore 页面。
  2. 选择 Loki 数据源。
  3. 在查询编辑器中输入 LogQL 表达式。
  4. 你可以看到原始日志输出,并且在上方可以切换到 Metrics 模式,将 LogQL 度量查询结果以图表形式展示。

5.3 仪表盘中创建日志面板

可以像创建 Prometheus 面板一样,在 Grafana 仪表盘中创建基于 Loki 的面板。

  1. 创建一个新的 Dashborad 或编辑现有 Dashboard。
  2. 添加一个新的 Logs 面板。
  3. 选择 Loki 数据源,输入 LogQL 查询。
  4. Logs 面板会直接显示查询到的原始日志数据。
  5. 也可以添加 Time Series 面板,输入 LogQL 度量查询,将日志数据转换成图表。

5.4 关联指标与日志(Grafana Loki Links)

Grafana 提供了一个强大的功能,可以在指标面板和日志面板之间创建关联。当你在一个指标图上发现异常时,可以直接点击图上的点,跳转到相关的日志流,查看对应时间点的详细日志,极大地提高了故障排查效率。

配置步骤:

  1. 在 Prometheus 数据源配置中,添加一个 Data links

  2. 设置 TitleURL。URL 可以是 Loki Explore 页面的 URL,并使用模板变量 ($__range_from, $__range_to, $__interval_ms, $__series.labels.<LABEL_NAME>) 来传递标签和时间范围。

    1
    2
    # 示例 URL,会跳转到 Loki Explore 页面,并带上 job 标签和时间范围
    /explore?orgId=1&left=["now-1h","now","LokiDS",{"expr":"{job=\"$__series.labels.job\"}"},"Logs"]
  3. 在 Prometheus 指标面板中,当点击数据点时,就会出现跳转到 Loki 日志的链接。

六、部署与高可用

部署模式:

  • 单体模式 (Monolithic):所有 Loki 组件(Distributor, Ingester, Querier, Index, Chunk)都运行在一个进程中。适合小规模部署、测试和开发环境。使用 BoltDB 作为索引,文件系统作为 Chunk Store。
  • 微服务模式 (Microservices):每个组件独立运行,可以横向扩展。适合生产环境,可以使用云存储(S3/GCS)作为 Chunk Store,DynamoDB/Cassandra 等作为 Index Store。

高可用性:

  • Distributor/Querier:无状态,通过负载均衡器(如 Nginx, Load Balancer)进行多实例部署,实现高可用。
  • Ingester:有状态,通常使用 Kubernetes StatefulSet 部署。需要启用 HA Tracker(基于 Consul 或 Memberlist),确保多个 Ingester 副本之间数据的一致性,防止数据丢失(即当一个 Ingester 宕机时,其负责的日志,在未刷写(flush)到 Chunk Store 之前,由另一个 Ingester 接管)。Loki 默认有一个短期的 retention 窗口,Ingester 会在内存中保留一段时间的 Chunk,以保证 Ingester 故障时日志不丢失或仅丢失很少量。
  • 存储后端:通过选择高可用的 Index Store 和 Chunk Store(如云服务商提供的对象存储和 NoSQL 数据库)来保证数据的持久性和可用性。

七、Loki 的优缺点

优点:

  • 极高的成本效益:只索引标签的策略大幅降低了存储和索引成本。
  • 部署和运维简单:尤其是单体模式,非常轻量。
  • 与 Grafana 无缝集成:提供统一的可观测性平台。
  • Prometheus 风格的 LogQL:降低了学习曲线。
  • 云原生友好:专为 Kubernetes 和容器化环境设计。
  • 高效的数据压缩:原始日志数据存储效率高。

缺点:

  • 不适合大规模全文搜索:当需要在大范围(没有特定标签过滤)进行非结构化文本搜索时,性能可能低于传统日志系统。
  • 标签设计至关重要:查询性能严重依赖于优秀的标签设计。如果标签不够精细,查询可能需要扫描大量日志。
  • 数据模型局限性:不如 Elasticsearch 那样灵活,无法进行复杂的聚合和分析(例如,不能直接计算平均请求延迟,只能统计日志行的数量或字节数)。
  • 实时性:日志写入 Ingester 后有一段在内存中缓存的时间,才会刷入永久存储,这可能导致在 Ingester 故障时短时间内的数据丢失(尽管有 HA Tracker 机制来缓解)。

八、总结

Loki 作为云原生时代的新型日志聚合系统,以其独特的设计理念,成功地平衡了日志可观测性的成本与功能。它通过高度抽象的标签索引和 Grep 式的查询模式,为用户提供了一个轻量、高效且与 Prometheus 和 Grafana 完美集成的日志解决方案。虽然它在某些方面不如传统的 ELK Stack 强大,但在许多场景下,特别是对存储成本敏感且希望获得统一可观测体验的用户而言,Loki 是一个极具吸引力的选择。理解其架构、Promtail 的配置和 LogQL 的使用是成功部署和利用 Loki 的关键。