为什么需要日志聚合
单台服务器上 tail -f /var/log/syslog 还能应付,多台服务器加几十个容器时就失控了。日志聚合系统解决三个核心问题:
- 集中检索:不用逐个登录服务器翻日志
- 结构化过滤:按服务、级别、时间范围快速定位
- 持久化存储:容器重启日志不丢失,支持长期保留
本文介绍用 Grafana Loki + Fluent Bit / Grafana Alloy 在内网环境搭建一套完整的日志聚合方案。
架构概览
┌──────────┐ ┌──────────┐ ┌──────────┐│ Server A │ │ Server B │ │ Server C ││ Fluent Bit│ │ Alloy │ │ Fluent Bit││ (采集器) │ │ (采集器) │ │ (采集器) │└─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │ │ └───────────────┬───────────────┘ │ Loki API ┌───────┴───────┐ │ Loki │ │ (日志存储/查询) │ └───────┬───────┘ │ ┌───────┴───────┐ │ Grafana │ │ (可视化) │ └───────────────┘Loki 是什么
Grafana Loki 是一套轻量级日志聚合系统,设计理念类似 Prometheus:只对标签(labels)建索引,日志正文以 chunk 形式压缩存储。相比 Elasticsearch,Loki 对资源的需求大幅降低。
| 特性 | Loki | Elasticsearch |
|---|---|---|
| 索引方式 | 仅索引标签 | 全文索引 |
| 存储效率 | 高(gzip/snappy 压缩) | 中等 |
| 内存占用 | 低 | 高(JVM heap) |
| 查询语言 | LogQL(类 PromQL) | Lucene/KQL |
| Kubernetes 集成 | 原生支持 | 需额外插件 |
| 适用规模 | 中小型到大型 | 大型到超大型 |
采集器对比:Fluent Bit vs Grafana Alloy
| 维度 | Fluent Bit | Grafana Alloy |
|---|---|---|
| 语言/运行时 | C(极低开销) | Go(Grafana Agent 继任者) |
| 吞吐量 | ~31,000 logs/s | ~15,700 logs/s |
| CPU 效率 | 0.26 核 @ 10k/s | 0.58 核 @ 10k/s |
| 内存占用 | ~78 MiB @ 10k/s | ~66 MiB @ 10k/s |
| 生态集成 | 广泛,支持多种输出 | LGTM 生态原生整合 |
| 功能范围 | 纯日志/指标采集 | 日志+指标+追踪统一采集 |
| 配置语法 | 经典 INI 格式 | River(类 HCL) |
数据来源:VictoriaMetrics 2026 年 Kubernetes 日志采集器基准测试(日志格式包含完整结构化字段)。Fluent Bit 吞吐量约 31k logs/s,CPU 占用 0.26 核 @10k logs/s;Grafana Alloy 吞吐量约 15.7k logs/s,CPU 占用 0.58 核 @10k logs/s,内存 66 MiB。该基准测试同时指出:不同采集器在日志交付正确性(丢失/重复)上存在显著差异,生产部署需关注此项。
选型建议:
- 纯日志采集,追求极致性能 → Fluent Bit
- 已使用 Grafana 生态,需要日志+指标+追踪统一采集 → Grafana Alloy
- 从 Promtail / Grafana Agent 迁移 → Grafana Alloy
- 已有 Fluent Bit 部署 → 继续使用,无需切换
系统要求
| 项目 | 最低要求 | 推荐配置 |
|---|---|---|
| CPU | 2 核 | 4 核+ |
| 内存 | 4 GB | 8 GB+ |
| 磁盘 | 50 GB SSD | 200 GB+ SSD(取决于日志量和保留期) |
| Docker | 20.10+ | 最新稳定版 |
| Loki | v3.x | v3.7+ |
| Fluent Bit | v4.0+ | v5.0+ |
| Alloy | v1.x | v1.15+ |
部署准备
创建项目目录结构:
mkdir -p ~/logging-stack/{loki,fluent-bit,alloy,grafana/datasources}目录说明:
logging-stack/├── docker-compose.yml # 主部署文件├── loki/│ └── loki-config.yaml # Loki 配置├── fluent-bit/│ ├── fluent-bit.conf # Fluent Bit 配置│ └── parsers.conf # 自定义解析器(可选)├── alloy/│ └── config.alloy # Alloy 配置└── grafana/ └── datasources/ └── loki-ds.yaml # Grafana 数据源Docker Compose 部署
以下 Docker Compose 文件同时包含 Fluent Bit 和 Alloy,实际使用时二选一即可。
services: loki: image: grafana/loki:3.7 container_name: loki volumes: - ./loki/loki-config.yaml:/etc/loki/config.yaml - loki-data:/loki command: -config.file=/etc/loki/config.yaml ports: - "3100:3100" restart: unless-stopped healthcheck: test: ["CMD-SHELL", "wget -q --spider http://localhost:3100/ready || exit 1"] interval: 10s timeout: 5s retries: 5
fluent-bit: image: fluent/fluent-bit:3.0 container_name: fluent-bit volumes: - ./fluent-bit/fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf - /var/log:/var/log:ro # 宿主机日志目录 - /var/lib/docker/containers:/var/lib/docker/containers:ro # Docker 容器日志 ports: - "2020:2020" # Fluent Bit 监控端点 - "24224:24224" # Forward 协议端口(供其他服务推送日志) restart: unless-stopped depends_on: loki: condition: service_healthy
# 若选择 Alloy 替代 Fluent Bit alloy: image: grafana/alloy:latest container_name: alloy volumes: - ./alloy/config.alloy:/etc/alloy/config.alloy - /var/log:/var/log:ro - /var/lib/docker/containers:/var/lib/docker/containers:ro - /run/systemd/journal:/run/systemd/journal:ro # systemd journal user: "0:0" # 需要 root 权限读取系统日志 restart: unless-stopped depends_on: loki: condition: service_healthy
grafana: image: grafana/grafana:latest container_name: grafana volumes: - ./grafana/datasources:/etc/grafana/provisioning/datasources - grafana-data:/var/lib/grafana environment: - GF_SECURITY_ADMIN_USER=admin - GF_SECURITY_ADMIN_PASSWORD=changeme ports: - "3000:3000" restart: unless-stopped depends_on: - loki
volumes: loki-data: grafana-data:Loki 配置详解
⚠️ 生产环境注意:下方配置使用
filesystem对象存储,仅适用于测试/POC。生产环境推荐使用 S3/GCS/MinIO 等对象存储(见下文 MinIO 配置示例),以支持水平扩展、数据高可用和更好的性能。
auth_enabled: false
server: http_listen_port: 3100
common: 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: ingestion_rate_mb: 16 ingestion_burst_size_mb: 32 max_streams_per_user: 10000 max_entries_limit_per_query: 5000 reject_old_samples: true reject_old_samples_max_age: 168h retention_period: 720h # 30 天,需与 compactor.retention_enabled 配合使用
compactor: working_directory: /loki/compactor retention_enabled: true retention_delete_delay: 2h delete_request_store: filesystem
query_range: results_cache: cache: embedded_cache: enabled: true max_size_mb: 100
analytics: reporting_enabled: false关键配置说明:
| 配置项 | 说明 |
|---|---|
schema: v13 | 推荐 schema 版本,使用 TSDB 索引 |
ingestion_rate_mb | 单租户每秒最大摄入量,内网环境可适当调高 |
max_streams_per_user | 最大 stream 数量上限,超过会被限流 |
reject_old_samples_max_age | 拒绝超过此时间的旧日志 |
retention_enabled: true | 开启自动日志过期删除 |
使用 MinIO 作为 S3 存储
当需要扩展存储或数据备份时,可将 filesystem 替换为 S3 兼容对象存储:
common: path_prefix: /loki storage: s3: endpoint: minio:9000 bucketnames: loki-data access_key_id: minioadmin secret_access_key: minioadmin insecure: true s3forcepathstyle: true对应的 MinIO 服务:
minio: image: minio/minio:latest container_name: minio command: server /data --console-address ":9001" environment: - MINIO_ROOT_USER=minioadmin - MINIO_ROOT_PASSWORD=minioadmin volumes: - minio-data:/data ports: - "9000:9000" - "9001:9001" restart: unless-stoppedFluent Bit 配置详解
[SERVICE] Flush 1 Log_Level info Daemon off Parsers_File parsers.conf
# ---- 系统日志 ----[INPUT] Name tail Path /var/log/syslog Tag system.syslog Parser syslog-rfc3164 Mem_Buf_Limit 5MB Skip_Long_Lines On
[INPUT] Name tail Path /var/log/auth.log Tag system.auth Mem_Buf_Limit 5MB
# ---- Docker 容器日志 ----[INPUT] Name tail Path /var/lib/docker/containers/*/*.log Tag docker.* Parser docker Mem_Buf_Limit 10MB Skip_Long_Lines On
# ---- Nginx 访问日志(示例)----[INPUT] Name tail Path /var/log/nginx/access.log Tag nginx.access Parser nginx Mem_Buf_Limit 5MB
# ---- Nginx 错误日志 ----[INPUT] Name tail Path /var/log/nginx/error.log Tag nginx.error Mem_Buf_Limit 5MB
# ---- 输出到 Loki ----[OUTPUT] Name loki Match * Host loki Port 3100 Labels job=fluent-bit Label_keys service,level structured_metadata container_id,node_name Line_format json Auto_kubernetes_labels off
Label_keys中的字段会作为 Loki 标签建索引。注意控制标签基数——不要把container_id、trace_id等唯一值设为标签,否则会导致性能问题。
自定义解析器
[PARSER] Name nginx Format regex Regex ^(?<remote>[^ ]*) - (?<user>[^ ]*) \[(?<time>[^\]]*)\] "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<code>[^ ]*) (?<size>[^ ]*)(?: "(?<referer>[^\"]*)" "(?<agent>[^\"]*)")?$ Time_Key time Time_Format %d/%b/%Y:%H:%M:%S %z
[PARSER] Name docker Format json Time_Key time Time_Format %Y-%m-%dT%H:%M:%S.%L
[PARSER] Name syslog-rfc3164 Format regex Regex ^\<(?<pri>[0-9]+)\>(?<time>[^ ]* {1,2}[^ ]* [^ ]*) (?<host>[^ ]*) (?<ident>[a-zA-Z0-9_\/\.\-]*)(?:\[(?<pid>[0-9]+)\])?(?:[^\:]*\:)? *(?<message>.*)$ Time_Key time Time_Format %b %d %H:%M:%SGrafana Alloy 配置详解
Alloy 使用 River 语法,配置更现代化,通过组件管道组合实现日志采集→处理→输出。
// ---- 发现日志文件 ----local.file_match "syslog" { path_targets = [{"__path__" = "/var/log/syslog"}]}local.file_match "auth" { path_targets = [{"__path__" = "/var/log/auth.log"}]}local.file_match "nginx_access" { path_targets = [{"__path__" = "/var/log/nginx/access.log"}]}local.file_match "nginx_error" { path_targets = [{"__path__" = "/var/log/nginx/error.log"}]}
// ---- Docker 容器日志 ----local.file_match "docker_logs" { path_targets = [{"__path__" = "/var/lib/docker/containers/*/*.log"}]}
// ---- systemd journal ----loki.source.journal "journal" { forward_to = [loki.write.default.receiver] relabel_rules = loki.relabel.journal.rules labels = {job = "systemd-journal"}}
loki.relabel "journal" { rule { source_labels = ["__journal__systemd_unit"] target_label = "unit" } rule { source_labels = ["__journal__hostname"] target_label = "host" } rule { source_labels = ["__journal_priority_keyword"] target_label = "level" }}
// ---- 日志文件采集 ----loki.source.file "system_logs" { targets = concat( local.file_match.syslog.targets, local.file_match.auth.targets, ) forward_to = [loki.write.default.receiver]}
loki.source.file "nginx_logs" { targets = concat( local.file_match.nginx_access.targets, local.file_match.nginx_error.targets, ) forward_to = [loki.write.default.receiver]}
loki.source.file "docker_containers" { targets = local.file_match.docker_logs.targets forward_to = [loki.process.docker_parse.receiver]}
// ---- Docker JSON 日志解析 ----loki.process "docker_parse" { stage.json { expressions = {log = "log", time = "time", stream = "stream"} } stage.timestamp { source = "time" format = "RFC3339" } stage.labels { values = {stream = ""} } forward_to = [loki.write.default.receiver]}
// ---- 写入 Loki ----loki.write "default" { endpoint { url = "http://loki:3100/loki/api/v1/push" } external_labels = {agent = "alloy"}}Alloy 支持通过
--config.format=alloy在启动时校验配置:alloy run --config.format=alloy /etc/alloy/config.alloy
Grafana 数据源配置
apiVersion: 1
datasources: - name: Loki type: loki access: proxy url: http://loki:3100 isDefault: true editable: true启动与验证
# 启动(二选一:注释掉不需要的采集器)docker compose up -d
# 检查各服务状态docker compose ps
# 验证 Loki 就绪curl -s http://localhost:3100/ready
# 检查 Fluent Bit 运行状态curl -s http://localhost:2020/api/v1/metrics | grep -E "input_bytes|output_bytes"
# 检查 Loki 接收到的日志量curl -s http://localhost:3100/metrics | grep log_entries_total打开浏览器访问 http://<服务器IP>:3000:
- 使用
admin / changeme登录 Grafana - 左侧菜单 → Explore → 数据源选择 Loki
- 输入 LogQL 查询日志:
{job="fluent-bit"} # 查看所有 Fluent Bit 采集的日志{job="fluent-bit"} |= "error" # 包含 "error" 的日志{job="systemd-journal"} |~ "(?i)fail" # systemd journal 中不区分大小写匹配 fail{stream="stdout"} # 容器的 stdout 日志生产环境调优
Loki 调优
| 参数 | 默认值 | 建议值 | 说明 |
|---|---|---|---|
ingestion_rate_mb | 4 | 16~32 | 提高日志摄入速率上限 |
ingestion_burst_size_mb | 8 | 32~64 | 突发流量缓冲区 |
max_streams_per_user | 不限 | 10000 | 防止标签爆炸 |
max_entries_limit_per_query | 5000 | 5000~10000 | 单次查询最大条目数 |
query_timeout | 1m | 1m~5m | 长时间范围查询允许超时 |
retention_period | 0(不过期) | 720h(30 天) | 日志保留期 |
Fluent Bit 调优
| 参数 | 默认值 | 建议值 | 说明 |
|---|---|---|---|
Flush | 1 | 1~5 | 刷新间隔(秒),低延迟用 1 |
Mem_Buf_Limit | 无限制 | 5MB~50MB | 单个 input 的内存缓冲区 |
storage.max_chunks_up | 128 | 128~256 | 同时上传的最大 chunk 数 |
net.connect_timeout | 10s | 5s | 连接超时 |
net.keepalive | on | on | 保持长连接 |
Alloy 调优
| 参数 | 建议值 | 说明 |
|---|---|---|
tail_from_end | true | 仅采集新日志(重启后不重复发送) |
max_log_size | "2MB" | 单行最大日志长度 |
sync_period | "10s" | 文件发现同步周期 |
batch.size | 16384 | 批量发送大小(字节) |
batch.timeout | "1s" | 批量发送超时 |
Nginx 反向代理(可选)
如果需要通过域名访问 Loki API 和 Grafana:
server { listen 80; server_name logs.example.local;
# Grafana location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
# Loki API(供采集器推送) location /loki/ { proxy_pass http://127.0.0.1:3100; proxy_set_header Host $host; }}常见问题
Q:Loki 提示 too many labels 或 max_streams_per_user 错误
避免将唯一值(如 container_id、trace_id、request_id)设为 Loki 标签。标签应保持低基数,可取值有限(如 service、level、host、namespace)。高基数字段用 structured_metadata 存储。
# Fluent Bit:使用 structured_metadata 替代 label_keys[OUTPUT] Name loki structured_metadata container_id,trace_id Label_keys service,levelQ:Fluent Bit 读取 Docker 日志出现乱码
Docker 使用 JSON 格式存储日志,配置 docker parser 解析:
[INPUT] Name tail Path /var/lib/docker/containers/*/*.log Parser dockerQ:Alloy 启动报权限错误
# 将 alloy 用户加入相关组sudo usermod -aG adm,systemd-journal alloy
# 或直接以 root 运行(docker-compose 中设置 user: "0:0")Q:Loki 占用磁盘空间过大
# 在 Loki 配置中启用自动压缩和保留compactor: retention_enabled: true retention_delete_delay: 2h
limits_config: retention_period: 720h # 30 天后自动删除Q:多台服务器的日志如何汇聚
在每台服务器上运行 Fluent Bit 或 Alloy,配置 output 指向中央 Loki 的 API 地址:
# Fluent Bit OUTPUT 配置[OUTPUT] Name loki Host <LOKI_SERVER_IP> Port 3100 Labels job=fluent-bit, host=server-aQ:Fluent Bit 日志中有 OOM 或内存不足
检查 Mem_Buf_Limit 设置,降低单个 input 的缓冲区。也可整体限制容器内存:
fluent-bit: deploy: resources: limits: memory: 512MQ:部分日志丢失,如何排查?
- 检查采集器缓冲区:
curl localhost:2020/api/v1/metrics | grep mem_buf_limit - 查看 Loki 限流指标:
sum(rate(loki_discarded_samples_total[5m])) by (reason) - 确认采集器与 Loki 之间的网络连通性,以及
depends_on健康检查是否生效 - 验证日志行长度是否超过 Loki 限制(默认 256KB),超长日志会被截断