为什么选择 Loki
传统 ELK(Elasticsearch + Logstash + Kibana)方案虽然功能强大,但存在两个核心痛点:
- 存储成本高:Elasticsearch 将日志全文索引化,每条日志的索引膨胀可达原始数据的 3-5 倍
- 运维复杂:ES 集群扩缩容、分片再平衡、索引生命周期管理复杂,生产集群维护成本高
Loki 由 Grafana Labs 开源,设计理念是"像 Prometheus 那样做日志"。它只对日志的标签(Labels)做索引,不对日志正文建索引,通过 LogQL 进行全文检索。这种设计使存储成本降低 10 倍以上。
本文基于 Loki 3.x,参考 Loki 官方文档
Loki vs ELK 对比
| 维度 | ELK (Elasticsearch) | Loki |
|---|---|---|
| 索引方式 | 全文倒排索引 | 仅索引标签,正文不索引 |
| 存储成本 | 高(索引膨胀 3-5x) | 低(标签索引 + 压缩正文) |
| 查询语言 | Lucene Query / KQL | LogQL(类 PromQL 语法) |
| 扩展性 | 水平扩展,分片复杂 | 微服务模式,组件独立扩展 |
| 适用场景 | 全文检索、复杂分析 | 日志监控、指标化查询、与 Grafana 联动 |
| 资源消耗 | 高(JVM,内存大) | 低(Go 编写,内存友好) |
Loki 并非要完全替代 ES。如果你的核心需求是全文检索和复杂文本分析,ES 仍是更好的选择。但对于 SRE 日志监控、指标告警、排障定位这类场景,Loki + Grafana 的组合在成本和效率上优势明显。
Loki 架构原理
日志存储模型
Loki 的核心设计:标签索引 + 压缩日志流。
┌─────────────────────────────────────────────┐
│ Label Index (倒排) │
│ app=nginx → stream_id_1, stream_id_2 │
│ env=prod → stream_id_1, stream_id_3 │
└──────────────────┬──────────────────────────┘
│ 查到 stream_id 后
▼
┌─────────────────────────────────────────────┐
│ Chunk Store (压缩日志块) │
│ stream_id_1: [ts] log line 1 │
│ [ts] log line 2 │
│ ... 压缩存储(Snappy/ZSTD) │
└─────────────────────────────────────────────┘
- Stream(日志流):由一组唯一标签集合定义,如
{app="nginx", env="prod"}。同一 Stream 的日志按时间排序追加。 - Chunk(日志块):Stream 中的日志被切成固定大小的 Chunk(默认 1MB 或 1h),压缩后存储到对象存储(S3/GCS/本地文件)。
- Label Index(标签索引):仅索引标签到 Stream 的映射关系,体积极小。
这种设计让索引数据量与标签基数成正比,而与日志量无关——每天 1TB 日志,只要标签不变,索引大小几乎不增长。
微服务架构组件
┌──────────┐
Promtail ──────► │ Distributor │ ── 写入 ──► Ingester ──► Chunk Store (S3)
└──────────┘ │
▼
Grafana ────► ┌──────────┐ Query Frontend
│ Querier │ ◄─── 读取 ──── Ingester + S3
└──────────┘
│
┌──────────┐
│ Compactor │ ─── 合并/过期清理
└──────────┘
| 组件 | 职责 |
|---|---|
| Distributor | 接收日志写入,校验标签,按 Stream 哈希分发给 Ingester |
| Ingester | 缓存最近写入的日志,满块后 flush 到对象存储 |
| Querier | 处理查询请求,先查 Ingester 缓存再查对象存储 |
| Query Frontend | 查询前置,拆分大查询、并行化、缓存结果 |
| Compactor | 合并 Chunk、执行日志保留策略(TTL) |
单机模式(Single Binary)将所有组件打包到一个进程,适合开发和小规模部署。微服务模式各组件独立部署,适合大规模生产环境。
Promtail 配置
Promtail 是 Loki 的日志采集 Agent,功能类似于 Logstash/Filebeat:采集、解析、打标签、推送到 Loki。
基础配置
# promtail-config.yaml
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml # 记录每个日志文件的读取位置
clients:
- url: http://loki:3100/loki/api/v1/push
tenant_id: default # 多租户场景下区分租户
scrape_configs:
# 采集 Nginx 访问日志
- job_name: nginx
static_configs:
- targets:
- localhost
labels:
job: nginx
app: nginx
env: prod
__path__: /var/log/nginx/*.log
# 采集 Docker 容器日志(按容器名打标签)
- job_name: docker
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
filters:
- name: label
values: ["logging=loki"]
relabel_configs:
- source_labels: ['__meta_docker_container_name']
regex: '/(.*)'
target_label: container_name
- source_labels: ['__meta_docker_container_log_stream']
target_label: stream
- source_labels: ['__meta_docker_container_label_com_docker_compose_service']
target_label: service
# 采集 Kubernetes Pod 日志
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_namespace]
target_label: namespace
- source_labels: [__meta_kubernetes_pod_name]
target_label: pod
- source_labels: [__meta_kubernetes_pod_label_app]
target_label: app
- source_labels: [__meta_kubernetes_pod_container_name]
target_label: container
日志解析与标签提取
通过 pipeline_stages 对原始日志行进行解析,提取结构化字段作为标签或日志内的结构化数据:
scrape_configs:
- job_name: app-json-logs
static_configs:
- targets: [localhost]
labels:
job: app
app: my-service
__path__: /var/log/app/*.log
pipeline_stages:
# 1. 解析 JSON 格式日志
- json:
expressions:
level: level
trace_id: trace_id
method: http.method
status: http.status_code
duration_ms: duration_ms
# 2. 将提取的字段设为标签
- labels:
level:
status:
method:
# 3. 只保留 level=error 和 status>=500 的日志
- match:
selector: '{job="app"} |~ "ERROR"'
action: keep
# 4. 对耗时字段做数值化处理
- template:
source: duration_ms
template: '{{ .ToFloat .duration_ms | printf "%.1f" }}'
# 5. 输出结构化输出
- output:
source: message
Promtail pipeline 配置详见 Promtail 文档
日志位置管理
positions.yaml 记录每个日志文件的已读位置(offset),Promtail 重启后从断点继续读取。生产环境建议将此文件放在持久化卷上:
positions:
filename: /data/promtail/positions.yaml
如果日志被轮转(如 logrotate 后旧文件被删除),Promtail 通过文件 inode 追踪,仍能正确续读新文件。
LogQL 查询语法
LogQL 是 Loki 的查询语言,语法设计参考 Prometheus 的 PromQL,分为日志查询和指标查询两类。
流选择器(Stream Selector)
用花括号选择日志流,类似 PromQL 的标签选择器:
# 精确匹配
{app="nginx", env="prod"}
# 正则匹配
{app=~"nginx|gateway|api.*"}
# 排除标签值
{namespace="default", container!="istio-proxy"}
日志管道(Log Pipeline)
在流选择器后追加管道操作符,对日志行进行过滤和解析:
# 包含关键词
{app="nginx"} |= "error"
# 排除关键词
{app="nginx"} != "timeout"
# 正则匹配
{app="app"} |~ "ERROR|WARN|PANIC"
# JSON 解析后按字段过滤
{app="my-service"}
| json
| level="error"
| status >= 500
# 日志格式化(提取字段后重组显示)
{app="my-service"}
| json
| line_format "{{.trace_id}} [{{.level}}] {{.message}}"
# 正则提取(非 JSON 日志)
{app="nginx"}
| regexp `(?P<method>\w+) (?P<path>\S+) (?P<status>\d+) (?P<duration>\d+)`
| status >= 500
指标查询(Metric Query)
将日志流转换为时间序列指标,这是 Loki 最强大的功能——直接从日志生成 Grafana 指标面板:
# 1. 日志行计数率(QPS)
sum(rate({app="nginx"}[5m])) by (status)
# 2. 提取字段后统计 P99 延迟
quantile_over_time(0.99,
{app="my-service"} | json | unwrap duration_ms [5m]
) by (method)
# 3. 错误率
sum(rate({app="my-service"} | json | level="error" [5m]))
/
sum(rate({app="my-service"} [5m]))
# 4. Top 10 耗时请求
topk(10,
sum by (path) (
rate({app="my-service"} | json | unwrap duration_ms [5m])
)
)
unwrap 关键字用于从日志中提取数值字段进行聚合计算,支持 rate、avg_over_time、quantile_over_time、sum_over_time 等聚合函数。
Grafana 集成
添加 Loki 数据源
在 Grafana → Configuration → Data Sources 中添加 Loki,URL 填 http://loki:3100。Grafana 10+ 支持自动检测 Loki 版本和功能。
日志面板
在 Dashboard 中新建 Log Panel,写入 LogQL:
{app="my-service", env="prod"}
| json
| line_format "{{.trace_id}} [{{.level}}] {{.method}} {{.path}} {{.duration_ms}}ms"
设置 Visualization 为 Logs,开启 Color lines based on field(按 level 字段着色)。
指标面板与日志面板联动
利用 Grafana 的 Dashboard 变量和面板链接,实现"指标异常 → 一键跳转日志"的排障工作流:
- 变量定义:创建
$trace_id变量,从日志查询中获取值列表
# Variable query
label_values({app="my-service"} | json | level="error", trace_id)
- 指标面板:展示 P99 延迟趋势
quantile_over_time(0.99,
{app="my-service"} | json | unwrap duration_ms [5m]
) by (path)
- 日志面板联动:在日志面板中使用变量过滤
{app="my-service"} | json | trace_id="$trace_id"
- 面板链接:在指标面板的 Data Links 中配置跳转链接,将当前时间范围和 Trace ID 传递给日志面板:
/d/abc123/my-dashboard?from=$__from&to=$__to&var-trace_id=$__field.trace_id
这样当指标面板上某个路径的 P99 飙高时,点击该数据点即可直接跳转到对应时间窗口的日志。
生产部署
完整 docker-compose.yml
以下是一个适合中小规模生产环境的完整部署方案,包含 Loki(单机模式)、Promtail、Grafana 和 MinIO(S3 兼容存储):
# docker-compose.yml
version: '3.8'
networks:
loki-net:
driver: bridge
volumes:
minio-data:
grafana-data:
promtail-data:
services:
# ===== MinIO: S3 兼容对象存储 =====
minio:
image: minio/minio:RELEASE.2024-01-01T00-00-00Z
ports:
- "9000:9000"
- "9001:9001"
environment:
- MINIO_ROOT_USER=loki
- MINIO_ROOT_PASSWORD=loki123456
command: server /data --console-address ":9001"
volumes:
- minio-data:/data
networks: [loki-net]
restart: unless-stopped
# 初始化 MinIO bucket
minio-init:
image: minio/mc:RELEASE.2024-01-01T00-00-00Z
depends_on: [minio]
networks: [loki-net]
entrypoint: >
/bin/sh -c "
sleep 5;
mc alias set local http://minio:9000 loki loki123456;
mc mb local/loki-chunks --ignore-existing;
mc mb local/loki-ruler --ignore-existing;
mc anonymous set download local/loki-chunks;
"
restart: "no"
# ===== Loki: 日志存储与查询 =====
loki:
image: grafana/loki:3.0.0
ports:
- "3100:3100"
command: -config.file=/etc/loki/loki-config.yaml
volumes:
- ./loki-config.yaml:/etc/loki/loki-config.yaml:ro
networks: [loki-net]
restart: unless-stopped
depends_on: [minio]
# ===== Promtail: 日志采集 =====
promtail:
image: grafana/promtail:3.0.0
ports:
- "9080:9080"
command: -config.file=/etc/promtail/promtail-config.yaml
volumes:
- ./promtail-config.yaml:/etc/promtail/promtail-config.yaml:ro
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- promtail-data:/data
networks: [loki-net]
restart: unless-stopped
# ===== Grafana: 可视化 =====
grafana:
image: grafana/grafana:11.0.0
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin123
- GF_INSTALL_PLUGINS=grafana-loki-datasource
volumes:
- grafana-data:/var/lib/grafana
networks: [loki-net]
restart: unless-stopped
depends_on: [loki]
Loki 配置文件
# loki-config.yaml
auth_enabled: false # 单租户模式
server:
http_listen_port: 3100
common:
path_prefix: /tmp/loki
storage:
filesystem:
chunks_directory: /tmp/loki/chunks
rules_directory: /tmp/loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
# 使用 S3 兼容存储(MinIO)
storage_config:
aws:
endpoint: minio:9000
bucketnames: loki-chunks
access_key_id: loki
secret_access_key: loki123456
insecure: true
sse_encryption: false
ruler:
storage:
type: local
local:
path: /tmp/loki/rules
# Schema 配置:定义日志的索引结构
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: aws
schema: v13
index:
prefix: index_
period: 24h
# 保留策略
limits_config:
retention_period: 720h # 日志保留 30 天
max_query_series: 5000
max_query_parallelism: 16
ingestion_rate_mb: 10 # 每租户每秒最大写入 10MB
ingestion_burst_size_mb: 20
reject_old_samples: true
reject_old_samples_max_age: 168h # 拒绝超过 7 天的旧日志
compactor:
working_directory: /tmp/loki/compactor
compaction_interval: 10m
retention_enabled: true
retention_delete_delay: 2h
retention_delete_worker_count: 150
delete_request_store: filesystem
ruler:
storage:
type: local
local:
path: /tmp/loki/rules
rule_path: /tmp/loki/rules-temp
alertmanager_url: http://alertmanager:9093
enable_api: true
Loki 微服务模式部署
大规模生产环境推荐微服务模式(Distributed),各组件独立扩展。Helm 部署:
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update
helm install loki grafana/loki-distributed \
--namespace observability \
--create-namespace \
--set storage.type=s3 \
--set storage.s3.endpoint=s3.us-east-1.amazonaws.com \
--set storage.s3.bucket=loki-chunks-prod
微服务模式下每个组件都可以独立水平扩展:
# values.yaml 关键配置
loki:
structuredConfig:
ingester:
max_chunk_age: 2h
chunk_idle_period: 30m
chunk_target_size: 1572864 # 1.5MB
query_scheduler:
max_outstanding_requests_per_tenant: 4096
# Ingester 副本数(写入吞吐瓶颈时扩容)
ingester:
replicas: 3
# Querier 副本数(查询并发瓶颈时扩容)
querier:
replicas: 4
# Query Frontend 拆分大查询
queryFrontend:
replicas: 2
S3 存储后端配置
生产环境使用 AWS S3 作为存储后端时,注意以下配置项:
storage_config:
aws:
endpoint: s3.us-east-1.amazonaws.com
bucketnames: loki-chunks-prod
access_key_id: ${S3_ACCESS_KEY} # 建议通过环境变量注入
secret_access_key: ${S3_SECRET_KEY}
region: us-east-1
insecure: false
sse_encryption: true # 启用 SSE 加密
http_config:
idle_conn_timeout: 90s
response_header_timeout: 30s
# 存储生命周期:利用 S3 Lifecycle 自动归档
# 在 S3 控制台配置 bucket lifecycle:
# - 30 天后转为 Glacier 归档
# - 90 天后删除
日志告警
Loki 内置 Ruler 组件,支持基于 LogQL 的告警规则:
# /tmp/loki/rules/alerts.yaml
groups:
- name: app-alerts
interval: 30s
rules:
# 错误日志突增
- alert: HighErrorRate
expr: |
sum(rate({app="my-service"} | json | level="error" [5m]))
by (app)
/
sum(rate({app="my-service"} [5m]))
by (app)
> 0.05
for: 5m
labels:
severity: critical
team: sre
annotations:
summary: "{{ $labels.app }} error rate > 5%"
description: "Current error rate: {{ $value }}"
# 特定关键词告警
- alert: OOMKilled
expr: sum(count_over_time({container="my-service"} |~ "OOMKilled|OutOfMemoryError" [5m])) > 0
labels:
severity: critical
annotations:
summary: "Container OOMKilled detected"
总结
Loki + Promtail + Grafana 构建了一套轻量高效的日志监控体系,与 Prometheus 指标监控形成互补。落地建议:
- 标签设计先行:标签基数(cardinality)直接影响索引大小和查询性能。避免将
user_id、request_id等高基数值作为标签,它们应放在日志正文中通过管道提取 - 控制标签基数:单个标签的不同值不超过数千。
container_name、pod_name在大规模 K8s 集群中基数可能很高,需评估 - 合理设置保留期:核心业务日志保留 30-90 天,访问日志保留 7-14 天,通过
limits_config.retention_period控制 - 日志结构化:应用输出 JSON 格式日志(含
level、trace_id、duration_ms等字段),Promtail 可直接解析提取,大幅提升查询效率 - 存储成本优化:S3 存储比 ES 存储成本低一个数量级。配合 S3 Lifecycle 策略(热数据标准存储 → 冷数据 Glacier 归档 → 自动删除),进一步压缩长期日志成本
- 告警联动:利用 Loki Ruler 将日志异常转为告警,与 Prometheus Alertmanager 统一管理告警分发
Grafana Loki 官方文档:https://grafana.com/docs/loki/latest/
参考资料与致谢
本文在撰写过程中参考了以下资料,感谢原作者的贡献:
- loki:3100 — loki:3100,参考了loki:3100相关内容
- [loki:3100
。Grafana](http://loki:3100。Grafana) — loki:3100。Grafana,参考了loki:3100。Grafana相关内容 - Loki 官方文档 — Grafana,参考了Loki 官方文档相关内容
- Promtail 文档 — Grafana,参考了Promtail 文档相关内容