为什么需要专门的日志方案
服务器上的日志天然长在两个地方:一个是本机磁盘上的文件,一个是标准输出。机器少的时候,ssh 上去 tail -f 就能搞定;一旦服务扩到几十台、上百个容器,登录每台机器翻日志这件事就破产了——你既不知道问题落在哪台机器上,也没法跨机器按时间线把一次请求拼起来。
一套专门的日志方案要解决三件事:
- 集中:所有服务、所有节点的日志都汇到同一个地方,有统一的时间线。
- 可查:能按服务、按错误级别、按 traceId 秒级捞出想要的那几行,而不是 grep 一整晚。
- 可告警:错误日志突增、关键字刷屏时,主动把人叫起来,而不是等用户投诉。
日志是系统的黑匣子。平时它沉默地占着磁盘,事故时它是唯一不会替你「记忆美化」的证人。
- 采集分工Fluent Bit / Filebeat 跑在节点侧,轻量、低侵入地把日志搬上管道。
- Loki 哲学只索引标签、不索引正文,用对象存储存原始日志,成本远低于 ES 全文索引。
- 结构化日志在产生时就写成 JSON,比事后正则解析可靠十倍。
- 成本按价值分层保留:ERROR 留 30 天、DEBUG 按天滚动,用生命周期策略自动清理。
采集端:Fluent Bit 与 Filebeat 的分工
采集端是整个日志链路里离业务最近的一环,它跑在每台机器或每个节点上,盯着日志文件的尾部,把新产生的行一行行读出来、加上标签、发往后端。两个主流选手:
- Fluent Bit:CNCF 毕业项目,C 语言编写,内存占用几 MB,单机万级日志吞吐毫无压力。Kubernetes 里通常以 DaemonSet 形式每节点一个。
- Filebeat:Elastic 出品,Go 编写,成熟稳定、配置生态丰富。吃内存比 Fluent Bit 大一些,但和 ELK 栈天然亲和。
两者干的事一样:tail → 解析 → 打标签 → 输出。下面是一段 Fluent Bit 配置,采集容器日志并打上 namespace、container 标签后发往 Loki:
[INPUT]
Name tail
Path /var/log/containers/*.log
Parser json
Tag kube.*
[FILTER]
Name parser
Match kube.*
Key_Name log
Reserve_Data On
Preserve_Key On
[OUTPUT]
Name loki
Match *
Host loki.gateway.svc
Port 3100
Labels job=fluentbit, namespace=$kubernetes['namespace_name'], container=$kubernetes['container_name']
采集端最容易被忽视的是背压处理:后端短暂不可用时,采集器必须把日志缓存在本地磁盘而不是直接丢。Fluent Bit 的 storage.type filesystem 就是干这个的——日志先落本地,后端恢复后自动补传,保证「不丢一条」。
云原生日志栈:Loki 的设计哲学
ELK 强大但重:Elasticsearch 要给日志正文建倒排索引,磁盘和内存都吃得很凶。Grafana Labs 推出的 Loki 走了一条相反的路——只给标签建索引,日志正文原样压缩存在对象存储里。
| 维度 | Elasticsearch | Loki |
|---|---|---|
| 索引对象 | 全文倒排索引 | 仅标签(labels) |
| 存储 | 本地磁盘 / SSD | S3 等对象存储,成本低一个量级 |
| 查询方式 | 全文 DSL | LogQL:先标签过滤,再正文匹配 |
| 擅长 | 复杂全文检索、聚合分析 | 按标签快速定位后 grep 正文 |
| 资源占用 | 重,需要大量堆内存 | 轻,可小规格起步 |
它的查询模型因此有个重要推论:标签设计就是查询设计。你不可能像在 ES 里那样对任意字段做全文聚合,必须在采集时就想清楚「未来我会按什么维度查日志」——服务名、环境、命名空间、实例 ID。把这些打进标签,正文里的细节靠 grep 过滤。
这也引出一条纪律:标签基数要克制。traceId、用户 ID 这类每个值都不同的东西绝对不能进标签,否则时序索引会爆炸——和 Prometheus 指标的高基数陷阱是同一个道理。
结构化与解析:把文本变成可查询数据
同一条错误,写成「支付超时,订单 SO10086 耗时 3000ms」和写成一行 JSON,事后分析的难度天差地别。前者要靠正则去猜字段,后者字段天然就有。
# 反例:纯文本日志,靠正则解析,脆弱
2026-10-14 09:12:33 ERROR order payment timeout orderId=SO10086 costMs=3000
# 正例:结构化 JSON(单行),字段直接可查
{"ts":"2026-10-14T09:12:33Z","level":"ERROR","service":"order","traceId":"a1b2c3","msg":"payment timeout","orderId":"SO10086","costMs":3000}
实在改不动老应用的输出格式时,才在采集端补一层解析——用正则或 Grok 把文本切成字段。但要明白:事后解析永远是次优解,正则会因为多一个空格就整个崩掉。
| 结构化日志要点 | 原因 |
|---|---|
| 时间戳用 ISO8601 / RFC3339 | 跨时区、跨机器可对齐,避免本地时区混乱 |
| 带 traceId | 跨服务串联同一次请求的全部日志 |
| 级别枚举固定 | DEBUG / INFO / WARN / ERROR,不要自创级别 |
| 错误对象单独字段 | 堆栈放 error 字段,msg 保持简短 |
| 不打印大对象 | 日志量翻倍的元凶,必要时只打 ID |
LogQL 查询与日志告警实战
Loki 的查询语言叫 LogQL,语法上和 PromQL 一脉相承,分成两段:先用花括号选标签流,再用管道对正文做过滤和指标聚合。
# 1. 选出 order 服务在 prod 环境的全部错误日志
{namespace="prod", container="order"} |= "ERROR"
# 2. 在上面基础上进一步过滤:只看支付超时
{namespace="prod", container="order"} |= "ERROR" |~ "payment.*timeout"
# 3. 把错误数转成指标:每分钟错误日志条数
sum(count_over_time({container="order"} |= "ERROR" [1m]))
# 4. 提取字段后再聚合:按 orderId 统计报错次数
sum by (orderId) (count_over_time({container="order"} | json | __error__=="" [5m]))
日志告警的思路和指标告警一致:先把日志流聚合成一个数值序列,再对数值设阈值。下面这条规则的意思是——order 服务每分钟错误日志超过 50 条就触发:
groups:
- name: log-alerts
rules:
- alert: OrderErrorSpike
expr: sum(count_over_time({container="order"} |= "ERROR" [1m])) > 50
for: 5m
labels:
severity: warning
annotations:
summary: "order 服务错误日志每分钟超过 50 条"
实践经验:日志告警别直接挂「含 ERROR 就报警」——那样一天响几百次。正确做法是对错误量做聚合、设速率阈值、加持续窗口,把偶发抖动挡在门外。
成本控制与保留策略
日志最大的隐性成本是保留太久。一条 INFO 级别的心跳日志,留一年就是纯浪费。成熟的做法是按价值分层保留:
- ERROR / WARN:保留 30~90 天,用于事故复盘与合规追溯。
- INFO:保留 7~14 天,覆盖一个迭代周期即可。
- DEBUG:默认关闭,排障时按服务临时打开,用完即关。
技术上用保留期策略自动过期:Loki 的 limits_config 里配置 retention_period,或用 compactor 定期清理过期 chunk。更进一步,可以给高频低价值的服务(比如业务侧的访问日志)单独建一条更短的保留链。
另一个容易忽视的成本来源是日志量本身。一个不注意打了大对象的循环,一晚上能吐几百 GB。在采集端加一个每服务的限速(rate_limit),既能保护后端,也能防止某个失控进程烧光你的存储预算。
一套最小可用的日志栈
理论讲完,落到能跑起来的最小组合。下面这套覆盖采集、存储、查询三件事,容器化半小时内能起:
| 组件 | 职责 | 备注 |
|---|---|---|
| Fluent Bit | 节点侧采集、打标签、转发 | DaemonSet 或单机 systemd |
| Loki | 日志接收、标签索引、查询 | 单机版可直接本地磁盘起步 |
| Grafana | 日志查询界面与仪表盘 | 和 Prometheus 共用一个面板 |
# docker compose 片段:一条命令起 Loki + Grafana
services:
loki:
image: grafana/loki:3.0.0
ports: ["3100:3100"]
command: -config.file=/etc/loki/local-config.yaml
grafana:
image: grafana/grafana:latest
ports: ["3000:3000"]
depends_on: [loki]
起步建议:先让应用把日志写成 JSON 打到 stdout,再用 Fluent Bit 收进 Loki,最后在 Grafana 里配一两个常用查询。不要一上来就追求全文检索能力——日志体系的价值在于「出事后能快速翻到那一行」,而不是「什么都能搜到」。把结构化做好、把保留期管好,比换更强的引擎有用得多。