日志是可观测性中被低估最多的支柱。 在 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/S3 | Rust 编写、低资源、高吞吐 |
| 混合云 | 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-bit | Fluentd | Vector |
|---|---|---|---|
| 语言 | C | Ruby | Rust |
| 性能 | 高 | 中 | 极高 |
| 内存 | 低 | 高 | 低 |
| 拓扑 | 简单 | 复杂 | 复杂(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 日志费用 |
参考与延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。