Loki 日志聚合实战:Promtail 采集、LogQL 查询与生产部署
系统讲解 Grafana Loki 日志系统的生产部署:Loki 架构与存储原理(标签索引 vs 全文索引)、Promtail 多场景日志采集配置(Kubernetes/文件/系统日志)、LogQL 查询语言从基础到进阶、Loki 日志告警配置、Loki 分布式部署与存储配置(S3/阿里云 OSS 后端)。
Loki 是 Grafana Labs 开发的日志聚合系统,定位是“为日志设计的 Prometheus”。与 ELK 全文索引不同,Loki 只索引元数据标签(轻量),日志内容存到对象存储(便宜),查询时即时扫描(LogQL 过滤)。这使 Loki 的成本比 Elasticsearch 低 5-10 倍。
Loki vs ELK 架构对比
ELK(Elasticsearch + Logstash + Kibana):
Logstash 采集 → Elasticsearch(全文索引,每行都建索引)→ Kibana
优点:搜索任意字段超快(已建索引)
缺点:存储成本高、资源消耗大(Elasticsearch 内存吞噬机)
Loki(PLG 栈:Promtail + Loki + Grafana):
Promtail 采集 → Loki(只索引标签,内容存对象存储)→ Grafana
优点:存储成本低、资源消耗小、与 Prometheus/Grafana 原生集成
缺点:全文搜索慢(查询时扫描)
选型建议:
大规模生产、需要全文搜索 → ELK(或 OpenSearch)
云原生、成本敏感、Kubernetes 场景 → Loki(与 Prometheus 体系整合)
一、Loki 部署
单机部署(All-in-One 模式,适合开发/小规模)
# docker-compose.yml
version: '3'
services:
loki:
image: grafana/loki:3.2.0
ports:
- "3100:3100"
command: -config.file=/etc/loki/config.yml
volumes:
- ./loki-config.yml:/etc/loki/config.yml
- loki-data:/loki
promtail:
image: grafana/promtail:3.2.0
volumes:
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- ./promtail-config.yml:/etc/promtail/config.yml
command: -config.file=/etc/promtail/config.yml
grafana:
image: grafana/grafana:11.2.0
ports:
- "3000:3000"
environment:
- GF_DATASOURCE_PROMETHEUS_URL=http://prometheus:9090
volumes:
loki-data:
# loki-config.yml(单机配置)
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
common:
instance_addr: 127.0.0.1
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
limits_config:
retention_period: 30d # 日志保留 30 天
max_streams_per_user: 0 # 0 = 不限制
ingestion_rate_mb: 16 # 每秒最大摄入 16MB
生产部署(Simple Scalable 模式 + S3 存储)
# loki-config.yml(生产配置,使用阿里云 OSS)
auth_enabled: true
server:
http_listen_port: 3100
common:
path_prefix: /loki
storage:
s3:
endpoint: oss-cn-shanghai-internal.aliyuncs.com
bucketnames: my-loki-logs
access_key_id: ${AWS_ACCESS_KEY_ID}
secret_access_key: ${AWS_SECRET_ACCESS_KEY}
s3forcepathstyle: true
insecure: false
replication_factor: 3 # 3 副本高可用
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: s3
schema: v13
index:
prefix: loki_index_
period: 24h
limits_config:
retention_period: 90d # 保留 90 天
per_stream_rate_limit: 10MB # 每个日志流限速
ingestion_burst_size_mb: 32
max_query_length: 0 # 查询时间范围不限制
max_query_parallelism: 32 # 最大并行查询数
compactor:
working_directory: /loki/compactor
compaction_interval: 1h
retention_enabled: true
delete_request_store: s3
二、Promtail 日志采集配置
采集 Kubernetes Pod 日志
# promtail-config.yml(Kubernetes 场景)
server:
http_listen_port: 9080
positions:
filename: /tmp/positions.yaml # 记录读取位置,重启后续读
clients:
- url: http://loki:3100/loki/api/v1/push
batchwait: 1s # 积累 1 秒再批量发送
batchsize: 1048576 # 单批最大 1MB
timeout: 10s
scrape_configs:
# 采集 Kubernetes Pod 日志
- job_name: kubernetes-pods
kubernetes_sd_configs:
- role: pod
pipeline_stages:
# 1. 解析 Docker JSON 格式日志
- cri: {}
# 2. 过滤特定日志
- drop:
expression: '.*health.*check.*' # 丢弃健康检查日志
# 3. 提取字段作为标签(从日志内容)
- regex:
expression: '.*level=(?P<level>[a-zA-Z]+).*'
- labels:
level: # 把 level 字段变成 Loki 标签
relabel_configs:
# 只采集有 logging: "true" 注解的 Pod
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
# 把 Pod 元信息变成 Loki 标签
- source_labels: [__meta_kubernetes_pod_namespace]
target_label: namespace
- source_labels: [__meta_kubernetes_pod_name]
target_label: pod
- source_labels: [__meta_kubernetes_pod_container_name]
target_label: container
- source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_pod_name]
target_label: job
separator: /
replacement: $1
# 采集系统日志(/var/log/syslog)
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: syslog
host: ${HOSTNAME}
__path__: /var/log/syslog
# 采集应用日志(指定路径,带 glob 通配)
- job_name: nginx-access
static_configs:
- targets:
- localhost
labels:
job: nginx
__path__: /var/log/nginx/*access*.log
pipeline_stages:
# 解析 Nginx 日志格式
- regex:
expression: '^(?P<remote_addr>\S+) - (?P<remote_user>\S+) \[(?P<time_local>.*)\] "(?P<method>\S+) (?P<request>\S+) (?P<protocol>\S+)" (?P<status>\d+) (?P<bytes>\d+)'
- labels:
method:
status: # 把 HTTP 状态码变成标签(方便过滤)
三、LogQL 查询语言
LogQL 是 Loki 的查询语言,类似 PromQL 的风格。
日志查询(Log Queries)
# 基础查询:所有来自 production 命名空间的日志
{namespace="production"}
# 多标签过滤
{namespace="production", container="nginx"}
# 正则匹配标签
{namespace=~"production|staging"}
# 日志内容过滤(在标签流中过滤包含特定字符串的行)
{namespace="production"} |= "ERROR" # 包含 ERROR
{namespace="production"} != "DEBUG" # 不包含 DEBUG
{namespace="production"} |~ "timeout|error" # 正则匹配
{namespace="production"} !~ "health.*check" # 排除健康检查
# 多步骤过滤(pipeline)
{namespace="production", job="nginx"}
|= "status=5" # 先过滤 5xx 错误
|~ "GET|POST" # 再过滤 GET/POST 请求
| json # 解析 JSON 格式日志
| status >= 500 # 提取字段后再过滤
解析器(Parse Logs)
# JSON 格式日志
{job="app"} | json
# 自动提取 JSON 字段为标签,如 .level .msg .timestamp
# 指定字段提取
{job="app"} | json level, msg, timestamp
| level="error" # 提取后可以用字段过滤
# Logfmt 格式(key=value 格式,如 Go 应用)
{job="go-app"} | logfmt
| level="error"
# 正则提取
{job="nginx"}
| regexp `(?P<method>\w+) (?P<path>/[^ ]+) HTTP/(?P<version>[\d.]+)"`
| method="POST" # 只看 POST 请求
# 自定义 pattern
{job="app"}
| pattern `<timestamp> <level> <msg>`
| level="ERROR"
指标查询(Metric Queries)
# 每分钟的日志行数
count_over_time({namespace="production"}[1m])
# 每分钟的错误数
count_over_time({namespace="production"} |= "ERROR" [1m])
# 错误率(每分钟错误数 / 总行数)
sum(rate({namespace="production"} |= "ERROR" [5m]))
/
sum(rate({namespace="production"}[5m]))
# 按服务统计错误率(聚合)
sum by (container) (
rate({namespace="production"} |= "ERROR" [5m])
)
# 日志量排行(找出哪个服务日志最多)
topk(10,
sum by (container) (
bytes_over_time({namespace="production"}[1h])
)
)
# P99 响应时间(从日志中提取延迟)
quantile_over_time(0.99,
{job="nginx"}
| json
| request_time != ""
| unwrap request_time [5m]
) by (host)
四、Loki 日志告警
# Loki 告警规则(写入 Loki 配置中的 ruler 路径)
# /loki/rules/production/alerts.yml
groups:
- name: application-alerts
rules:
# 错误率告警
- alert: HighErrorRate
expr: |
sum(rate({namespace="production"} |= "ERROR" [5m])) by (namespace, container)
/
sum(rate({namespace="production"}[5m])) by (namespace, container)
> 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "{{ $labels.container }} 错误率过高"
description: "{{ $labels.namespace }}/{{ $labels.container }} 错误率 {{ $value | humanizePercentage }} 超过 5%"
# 日志停止(服务可能挂了)
- alert: NoLogsReceived
expr: |
sum(rate({namespace="production", container=~"api-server|worker"}[5m])) by (container) == 0
for: 5m
labels:
severity: warning
annotations:
summary: "{{ $labels.container }} 停止产生日志"
# 特定关键词告警
- alert: OutOfMemoryError
expr: |
count_over_time({namespace="production"} |= "OutOfMemoryError" [5m]) > 0
labels:
severity: critical
annotations:
summary: "检测到 OutOfMemoryError"
五、Loki 查询优化
# 优化技巧1:标签过滤放前面(减少扫描的日志流数量)
# 差:
{namespace="production"} |= "ERROR" | json | container="api"
# 好:(标签越精确,扫描越少)
{namespace="production", container="api"} |= "ERROR" | json
# 优化技巧2:控制时间范围(Loki 按时间分块存储)
# 避免查询超长时间范围(例如查 30 天),性能差
# 建议:精确指定时间段,或使用短时间窗口 + Recording Rules
# 优化技巧3:使用 Recording Rules 预计算高频指标
# 在 Loki ruler 中定义,预计算结果存到 Prometheus
groups:
- name: loki-recording
interval: 1m
rules:
- record: job:log_errors:rate5m
expr: sum by (namespace, container) (rate({namespace=~".+"}|= "ERROR"[5m]))
小结
Loki 的核心设计哲学是标签索引 + 内容即时扫描:只给日志打最重要的几个标签(namespace、container、job),日志正文不建索引(存对象存储很便宜),查询时用 LogQL pipeline 过滤(|=、| json、| regexp)。最佳实践:标签数量控制在 10 个以内(标签基数太高会导致 Loki 性能下降);把高基数字段(请求 ID、用户 ID)留在日志内容里,不要变成标签;用 Pipeline 阶段解析日志(| json、| logfmt),提取需要聚合的字段再做指标查询。
