Docker 日志收集:Loki + Promtail 部署、Fluentd 配置与日志驱动对比
深入讲解 Docker 生产环境的日志收集方案:Loki + Promtail 轻量级日志栈的完整部署(与 Grafana 集成)、Fluentd/Fluent Bit 多路由日志聚合、Docker 日志驱动对比(json-file/journald/fluentd/loki),以及日志标签规范、日志保留策略、高基数标签的性能陷阱,和生产中常见的日志查询与告警配置。
容器的日志不像虚拟机那样持久存储,容器删除后日志默认消失。生产环境必须建立集中日志收集体系:将所有容器的日志汇聚到统一的存储,支持检索、告警和长期保留。本文讲解两套主流方案:Loki + Promtail(轻量级,与 Grafana 天然集成)和 Fluentd(功能强大,路由灵活)。
日志收集方案对比
方案 特点 适用场景
────────────────────────────────────────────────────────────────
Loki + Promtail 轻量、与 Grafana 一体 已有 Grafana 的环境
EFK(Elasticsearch + Fluentd + Kibana)
功能全面、全文索引 大规模日志分析
Fluent Bit 极轻量(<1MB 内存)、高性能 边缘/资源受限环境
journald 系统原生、零配置 单机 CentOS/RHEL
CloudWatch/SLS 托管服务、无需自建 云环境
一、Loki + Promtail 完整部署
Loki 不对日志内容建立全文索引,只索引标签(Labels),因此资源消耗极低,是中小规模的首选。
架构
容器日志(stdout/stderr)
↓ json-file 写到宿主机
Promtail(DaemonSet 模式,每个宿主机一个)
↓ tail 容器日志文件,打 Labels 后推送
Loki(日志存储与查询)
↑ Grafana LogQL 查询
部署
# docker-compose.logging.yml
services:
loki:
image: grafana/loki:3.2.0
container_name: loki
restart: unless-stopped
ports:
- "3100:3100"
volumes:
- ./loki/loki-config.yml:/etc/loki/config.yml:ro
- loki_data:/loki
command: -config.file=/etc/loki/config.yml
networks:
- monitoring
promtail:
image: grafana/promtail:3.2.0
container_name: promtail
restart: unless-stopped
volumes:
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./promtail/promtail-config.yml:/etc/promtail/config.yml:ro
command: -config.file=/etc/promtail/config.yml
networks:
- monitoring
volumes:
loki_data:
networks:
monitoring:
external: true
# loki/loki-config.yml
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
common:
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
instance_addr: 127.0.0.1
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: 720h # 日志保留 30 天
ingestion_rate_mb: 16
ingestion_burst_size_mb: 32
ruler:
alertmanager_url: http://alertmanager:9093
compactor:
working_directory: /loki/compactor
retention_enabled: true
retention_delete_delay: 2h
# promtail/promtail-config.yml
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml # 记录读取位置(重启后继续)
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
# 自动发现所有 Docker 容器的日志
- job_name: docker-containers
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
filters:
- name: status
values: ["running"]
relabel_configs:
# 使用容器名作为 job 标签
- source_labels: ['__meta_docker_container_name']
regex: '/(.*)'
target_label: 'container'
# 使用 compose service 名
- source_labels: ['__meta_docker_container_label_com_docker_compose_service']
target_label: 'service'
# compose 项目名
- source_labels: ['__meta_docker_container_label_com_docker_compose_project']
target_label: 'project'
# 镜像名
- source_labels: ['__meta_docker_container_image']
target_label: 'image'
# 日志文件路径
- source_labels: ['__meta_docker_container_id']
target_label: '__path__'
replacement: '/var/lib/docker/containers/$1/$1-json.log'
pipeline_stages:
# 解析 Docker json-file 格式
- json:
expressions:
log: log
stream: stream
time: time
- labels:
stream: # stdout/stderr 作为标签
- timestamp:
source: time
format: RFC3339Nano
- output:
source: log # 最终日志内容
# 解析 JSON 格式的应用日志(如果应用输出 JSON)
- match:
selector: '{service="api"}'
stages:
- json:
expressions:
level: level
msg: message
trace_id: trace_id
- labels:
level:
二、Grafana 中查询日志(LogQL)
# 基础查询(按 service 过滤)
{service="api"}
# 多条件过滤
{project="myapp", service="api"} |= "ERROR"
# 正则过滤
{service="nginx"} |~ "5[0-9]{2}" # 5xx 状态码
# 解析 JSON 日志并过滤字段
{service="api"} | json | level="error"
# 统计每分钟的错误数
count_over_time({service="api"} |= "ERROR" [1m])
# 错误率(错误数 / 总请求数)
rate({service="api"} |= "ERROR" [5m])
/
rate({service="api"} [5m])
* 100
# 解析延迟并计算 p99
{service="api"} | json | unwrap duration_ms | quantile_over_time(0.99, [5m])
# 最近 1 小时各 service 的错误日志数量对比
sum by (service) (
count_over_time({project="myapp"} |= "ERROR" [1h])
)
三、Loki 告警规则
# loki/rules/docker-log-alerts.yml
groups:
- name: docker-log-alerts
rules:
# 错误日志激增
- alert: ErrorLogSpike
expr: |
sum by (service) (
rate({project="myapp"} |= "ERROR" [5m])
) > 1
for: 2m
labels:
severity: warning
annotations:
summary: "服务 {{ $labels.service }} 错误日志激增"
description: "过去 5 分钟错误率 {{ $value | printf \"%.2f\" }}/s"
# 出现 PANIC 或 Fatal
- alert: ContainerPanic
expr: |
count_over_time({project="myapp"} |~ "(?i)panic|fatal" [5m]) > 0
for: 0m
labels:
severity: critical
annotations:
summary: "容器出现 panic/fatal:{{ $labels.service }}"
四、Fluentd / Fluent Bit 方案
Fluent Bit 比 Fluentd 更轻量(C 编写,内存 < 1MB),适合容器内嵌或边缘场景。
# docker-compose.logging.yml(Fluent Bit 方案)
services:
fluent-bit:
image: fluent/fluent-bit:3.1
container_name: fluent-bit
restart: unless-stopped
volumes:
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./fluent-bit/fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf:ro
- ./fluent-bit/parsers.conf:/fluent-bit/etc/parsers.conf:ro
networks:
- monitoring
# fluent-bit/fluent-bit.conf
[SERVICE]
Flush 5
Daemon Off
Log_Level info
Parsers_File parsers.conf
HTTP_Server On
HTTP_Listen 0.0.0.0
HTTP_Port 2020
# 输入:读取 Docker 容器日志
[INPUT]
Name tail
Tag docker.*
Path /var/lib/docker/containers/*/*-json.log
Parser docker
DB /var/log/flb_docker.db # 记录读取位置
Mem_Buf_Limit 50MB
Skip_Long_Lines On
Refresh_Interval 5
# 过滤:添加容器元数据
[FILTER]
Name docker_metadata
Match docker.*
Docker_Metadata_Dir /var/lib/docker/containers
# 过滤:解析 JSON 格式的应用日志
[FILTER]
Name parser
Match docker.*
Key_Name log
Parser json
Reserve_Data On
# 输出1:发送到 Loki
[OUTPUT]
Name loki
Match docker.*
Host loki
Port 3100
Labels job=fluent-bit, container=$container_name
Remove_Keys stream, time
# 输出2:同时发送到 Elasticsearch(双路由)
[OUTPUT]
Name es
Match docker.*
Host elasticsearch
Port 9200
Index docker-logs
Type _doc
Logstash_Format On
Logstash_Prefix docker
Time_Key @timestamp
# 输出3:错误日志单独发送到 Kafka(后续分析)
[OUTPUT]
Name kafka
Match docker.*
Brokers kafka:9092
Topics docker-errors
Message_Key_Field container_name
# fluent-bit/parsers.conf
[PARSER]
Name docker
Format json
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L%z
Time_Keep On
[PARSER]
Name json
Format json
Time_Key timestamp
Time_Format %Y-%m-%dT%H:%M:%S.%LZ
五、日志驱动直接发送(无 Sidecar)
# 容器直接使用 Loki 日志驱动(需要安装插件)
docker plugin install grafana/loki-docker-driver:latest \
--alias loki \
--grant-all-permissions
# 单容器使用
docker run -d \
--log-driver=loki \
--log-opt loki-url="http://loki:3100/loki/api/v1/push" \
--log-opt loki-batch-size=400 \
--log-opt loki-retries=3 \
--log-opt loki-timeout=10s \
--log-opt loki-labels="job=myapp,environment=production" \
myapp:v1
# 全局配置(daemon.json)
{
"log-driver": "loki",
"log-opts": {
"loki-url": "http://loki:3100/loki/api/v1/push",
"loki-batch-size": "400",
"loki-labels": "job=docker"
}
}
六、日志标签规范与性能陷阱
⚠️ Loki 高基数标签问题(High Cardinality)
错误做法:把请求 ID、用户 ID 当标签
{trace_id="abc123"} ← 每个请求都有唯一 trace_id
→ 产生数亿个 stream,内存爆炸
正确做法:标签应是低基数(有限个值)
{service="api"} ← 几十个服务
{environment="production"} ← 3-4 个环境
{level="error"} ← 几个级别
高基数信息放在日志内容(LogQL 过滤):
{service="api"} |= "trace_id=abc123" ← 正确
生产推荐标签设计:
必须:service(服务名)、environment(环境)
可选:version(版本)、level(日志级别,如果应用支持)
禁止:request_id、user_id、session_id 等唯一值
规范示例:
{service="api", environment="production", level="error"}
{service="nginx", environment="staging"}
小结
Docker 日志收集的两个核心选择:
Loki + Promtail:与 Prometheus + Grafana 同属一套技术栈,部署简单,资源消耗低,中小规模首选。核心优势是可以在 Grafana 同一界面关联日志和指标,排障效率高。
Fluentd / Fluent Bit:路由能力更强,可以同时将日志发往多个目标(ES + Kafka + Loki),适合已有 ELK 基础设施或需要复杂日志处理的场景。
生产关键实践:日志标签保持低基数(否则 Loki 性能严重下降);Promtail 记录读取位置(positions.yaml),容器/Promtail 重启后不丢失进度;设置合理的日志保留期(30 天基本够用,超出用 S3 归档)。
