日志系统全栈:Loki、EFK、Vector 与日志架构设计

系统性日志系统实战指南:日志采集架构设计(DaemonSet/Sidecar/应用直推)、Loki 架构(Distributor/Ingester/Querier/Compactor/Index Gateway)、LogQL 查询语言、Loki 对象存储后端与分片策略、EFK 栈(Elasticsearch/Fluentd/Kibana)生产部署、Vector(Rust 高性能日志采集器)配置与拓扑、日志结构化(JSON/OTLP)与解析(Regex/Grok/CSV)、日志压缩与保留策略、日志安全(脱敏/审计/权限)、日志与 Trace/Metrics 关联。附完整 Kubernetes 部署配置。

日志是可观测性中被低估最多的支柱。 在 Metrics 显示"支付错误率上升"、Traces 指出"银行 API 超时"后,是日志告诉你"具体是第 3 次重试时连接超时"、“当时的请求参数是什么”、“数据库中对应的订单状态”。一个设计良好的日志系统能让排查时间从小时缩短到分钟。


一、日志系统架构模式

1.1 采集模式对比

模式 A:DaemonSet(节点级采集)
┌─────────────────────────────────────────┐
│ Node                                    │
│  ┌────────────┐                         │
│  │ Fluent-bit │ ← 读取 /var/log,       │
│  │ (DaemonSet)│   /var/lib/docker      │
│  └──────┬─────┘                         │
│         │ 直推                          │
│         ↓                               │
│  ┌────────────┐                         │
│  │   Loki/    │                         │
│  │ Elasticsearch│                        │
│  └────────────┘                         │
└─────────────────────────────────────────┘
优点:与应用无关,资源隔离好
缺点:容器 stdout 需额外处理

模式 B:Sidecar(Pod 级采集)
┌─────────────────────────────────────────┐
│ Pod                                     │
│  ┌──────────┐  ┌────────────┐          │
│  │   App    │→ │ Fluent-bit│ → 直推    │
│  │ (stdout) │  │ (Sidecar) │           │
│  └──────────┘  └────────────┘          │
└─────────────────────────────────────────┘
优点:可按 Pod 定制配置
缺点:每个 Pod 多一个容器,资源开销

模式 C:应用直推(推荐)
┌─────────────────────────────────────────┐
│ Pod                                     │
│  ┌──────────┐                           │
│  │   App    │ → 直接 HTTP/gRPC 推送     │
│  │(SDK注入)  │   到 Collector            │
│  └──────────┘                           │
└─────────────────────────────────────────┘
优点:无需中间层,延迟最低
缺点:需修改应用代码

1.2 架构选型

场景推荐方案原因
Kubernetes 云原生Loki + Promtail/Fluent-bit轻量、低成本、与 Prometheus 生态集成
传统企业EFK (Elasticsearch + Fluentd + Kibana)功能丰富、生态成熟
高性能采集Vector + ClickHouse/S3Rust 编写、低资源、高吞吐
混合云OTel Collector + 多后端统一采集、灵活分发

二、Loki 架构

2.1 核心组件

Loki 架构:
┌─────────────────────────────────────────────────┐
│                    Distributors                   │
│  接收日志流 → 按 tenant + stream hash → Ingester │
└────────────────┬────────────────────────────────┘
                 │
        ┌────────┴────────┐
        ↓                 ↓
┌──────────────┐  ┌──────────────┐
│  Ingesters   │  │  Ingesters   │
│  ├─ WAL      │  │  ├─ WAL      │
│  └─ 内存索引  │  │  └─ 内存索引  │
└──────┬───────┘  └──────┬───────┘
       │                  │
       └────────┬─────────┘
                ↓
        ┌──────────────┐
        │  Compactor   │
        │  压缩+索引合并 │
        └──────┬───────┘
               ↓
        ┌──────────────┐
        │ Object Store │
        │  (S3/GCS/MinIO)│
        │  chunks + index │
        └──────┬───────┘
               ↓
        ┌──────────────┐
        │   Queriers   │
        │  从对象存储查  │
        │  Index Gateway │
        └──────────────┘

2.2 关键设计:无全文索引

Loki 与传统 Elasticsearch 的核心区别:

Elasticsearch:
  ├── 倒排索引所有字段
  ├── 搜索极快
  ├── 存储体积大(原始日志的 2-3x)
  └── 成本高

Loki:
  ├── 只索引标签(label)
  ├── 日志内容按时间顺序存储在对象存储中
  ├── 搜索时需抓取块并扫描
  ├── 存储体积小(接近原始日志)
  └── 成本低 10x+

权衡:
  - 已知标签过滤 → Loki 高效
  - 全文模糊搜索 → Loki 慢(需用 LogQL 管道过滤)

2.3 Kubernetes 部署

# loki-values.yaml (Helm)
loki:
  auth_enabled: false
  commonConfig:
    replication_factor: 1

  storage:
    type: s3
    s3:
      endpoint: s3.amazonaws.com
      region: us-east-1
      bucketnames: loki-chunks
      access_key_id: ${AWS_ACCESS_KEY_ID}
      secret_access_key: ${AWS_SECRET_ACCESS_KEY}

distributor:
  replicas: 1

ingester:
  replicas: 1
  persistence:
    enabled: true
    size: 10Gi

querier:
  replicas: 1
  persistence:
    enabled: true
    size: 10Gi

queryFrontend:
  replicas: 1

compactor:
  replicas: 1
  persistence:
    enabled: true
    size: 10Gi

2.4 LogQL 查询语言

# 基础标签选择
{app="payment-service"}

# 多标签过滤
{namespace="production", app=~"api-.*"}

# 管道过滤(在日志内容中搜索)
{app="api-gateway"} |= "error"
{app="api-gateway"} != "debug"
{app="api-gateway"} |~ "err(or)?|fatal"    # 正则匹配
{app="api-gateway"} !~ "health|ready"       # 反向正则

# JSON 解析
{app="order-service"}
  | json
  | level = "ERROR"
  | user_id = "user_123"
  | line_format "{{.timestamp}} [{{.level}}] {{.message}}"

# 日志速率统计
sum by (level) (rate({app="api"} |= "error" [1m]))

#  topk 统计
sum by (route) (count_over_time({app="api"} [5m]))

# 范围聚合
sum(rate({app="api"} |= "error" | json [5m]))

# 联合查询(日志→指标)
sum by (status_code) (
  rate(
    {app="api"}
    | json
    | status_code != ""
  [1m])
)

三、EFK 栈

3.1 Fluentd 配置

# fluentd.conf
<source>
  @type tail
  path /var/log/containers/*.log
  pos_file /var/log/fluentd.pos
  tag kubernetes.*
  <parse>
    @type json
    time_key time
    time_format %Y-%m-%dT%H:%M:%S.%NZ
  </parse>
</source>

<filter kubernetes.**>
  @type kubernetes_metadata
</filter>

<filter kubernetes.**>
  @type record_transformer
  <record>
    cluster ${ENV['CLUSTER_NAME']}
    environment production
  </record>
</filter>

<match kubernetes.**>
  @type elasticsearch
  host elasticsearch
  port 9200
  logstash_format true
  logstash_prefix k8s-logs
  flush_interval 10s
</match>

3.2 Elasticsearch 索引策略

// ILM (Index Lifecycle Management)
PUT _ilm/policy/logs_policy
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_size": "50GB",
            "max_age": "1d"
          }
        }
      },
      "warm": {
        "min_age": "3d",
        "actions": {
          "shrink": { "number_of_shards": 1 },
          "forcemerge": { "max_num_segments": 1 }
        }
      },
      "cold": {
        "min_age": "7d",
        "actions": {
          "freeze": {}
        }
      },
      "delete": {
        "min_age": "30d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}

四、Vector

4.1 为什么用 Vector

维度Fluent-bitFluentdVector
语言CRubyRust
性能极高
内存
拓扑简单复杂复杂(DAG)
函数处理有限丰富丰富(VRL)
推荐简单场景传统✅ 高性能场景

4.2 Vector 配置

# vector.yaml
api:
  enabled: true
  address: 0.0.0.0:8686

sources:
  kubernetes_logs:
    type: kubernetes_logs
    pod_annotation_fields:
      container_image: "container_image"
      container_name: "container_name"
      pod_labels: "pod_labels"

  host_metrics:
    type: host_metrics
    collectors: [cpu, memory, disk]

transforms:
  parse_json:
    type: remap
    inputs: [kubernetes_logs]
    source: |
      . = parse_json!(.message)
      .timestamp = to_timestamp!(.time)
      .level = downcase!(.level ?? "info")

  filter_errors:
    type: filter
    inputs: [parse_json]
    condition: '.level == "error" || .level == "fatal"'

sinks:
  loki:
    type: loki
    inputs: [parse_json]
    endpoint: http://loki:3100
    labels:
      app: "{{ kubernetes.pod_labels.app }}"
      namespace: "{{ kubernetes.pod_namespace }}"
      level: "{{ level }}"
    encoding:
      codec: json
    healthcheck:
      enabled: true

  elasticsearch:
    type: elasticsearch
    inputs: [filter_errors]
    endpoints: [http://elasticsearch:9200]
    index: "error-logs-%Y.%m.%d"
    bulk:
      index: "error-logs"

  console:
    type: console
    inputs: [filter_errors]
    encoding:
      codec: json

五、日志结构化

5.1 结构化日志格式

{
  "timestamp": "2026-08-13T10:32:01.234Z",
  "level": "ERROR",
  "service": "payment-svc",
  "version": "1.2.3",
  "trace_id": "abc123def456",
  "span_id": "span789",
  "message": "Payment processing failed",
  "error": {
    "type": "TimeoutError",
    "message": "Connection to bank API timed out after 30s",
    "stack": "..."
  },
  "http": {
    "method": "POST",
    "route": "/api/payment",
    "status_code": 504,
    "duration_ms": 30000
  },
  "user": {
    "id": "user_789",
    "tier": "premium"
  },
  "request": {
    "order_id": "order_456",
    "amount": 299.99,
    "currency": "CNY",
    "payment_method": "wechat_pay"
  },
  "context": {
    "retry_count": 3,
    "bank_api": "https://bank.example.com/v2/charge",
    "region": "ap-east-1"
  }
}

5.2 日志脱敏

// Go:自动脱敏敏感字段
func sanitizeFields(data map[string]interface{}) map[string]interface{} {
    sensitiveKeys := []string{"password", "token", "secret", "credit_card", "ssn"}
    for _, key := range sensitiveKeys {
        if _, ok := data[key]; ok {
            data[key] = "[REDACTED]"
        }
    }
    // 递归处理嵌套
    for k, v := range data {
        if nested, ok := v.(map[string]interface{}); ok {
            data[k] = sanitizeFields(nested)
        }
    }
    return data
}

// 或使用 Vector VRL
// . = redact!(., filters: ["credit_card", "ssn"])

六、日志与 Metrics/Traces 关联

数据关联:
├── 日志 → Trace
│   └── 日志中包含 trace_id / span_id
│   └── LogQL: {app="api"} |= "trace_id=abc123"
│
├── 日志 → Metrics
│   └── 从日志提取指标(LogQL rate/count_over_time)
│   └── Exemplar: 指标点附加 trace_id
│
├── Trace → 日志
│   └── Trace 查询时关联时间窗口内的日志
│   └── Grafana: 点击 Span → 查看对应日志
│
└── Metrics → 日志/Trace
    └── Exemplar 点击 → 跳转到 Trace/日志

七、日志系统 Checklist

检查项说明
结构化日志JSON 格式,便于解析
统一时间格式ISO 8601 / RFC 3339
TraceID 传播日志中包含 trace_id
采样策略避免日志洪泛
保留策略热 3d / 温 7d / 冷 30d / 删 90d
脱敏处理敏感字段自动替换
索引标签按 service/level/env 索引
告警集成错误日志触发告警
成本监控日志量/GiB 日志费用

参考与延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「infra」更多文章

  1. 可观测性数据存储选型:TSDB、列式存储、对象存储与成本优化
  2. 云原生 APM 与性能剖析:Continuous Profiling 与火焰图
  3. Kubernetes 可观测性实战:集群、Pod、网络、存储全链路监控