技术博客

Docker 日志收集:Loki + Promtail 部署、Fluentd 配置与日志驱动对比

深入讲解 Docker 生产环境的日志收集方案:Loki + Promtail 轻量级日志栈的完整部署(与 Grafana 集成)、Fluentd/Fluent Bit 多路由日志聚合、Docker 日志驱动对比(json-file/journald/fluentd/loki),以及日志标签规范、日志保留策略、高基数标签的性能陷阱,和生产中常见的日志查询与告警配置。

Docker日志LokiPromtailFluentd日志收集生产运维可观测性

容器的日志不像虚拟机那样持久存储,容器删除后日志默认消失。生产环境必须建立集中日志收集体系:将所有容器的日志汇聚到统一的存储,支持检索、告警和长期保留。本文讲解两套主流方案: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 归档)。