日志集中化实战:Loki采集与标签设计

一个人管十几台服务器的时候,最先被放弃的往往不是监控,而是日志。机器少的时候习惯 SSH 上去 tail 一下,机器一多就变成「出事了才挨个登上去找」。真正要命的是那种「三台机器都正常、合起来才不正常」的故障:负载均衡后端有一台 Timeout 稍高、PHP-FPM 在一台机器上偶发 502、某台机器磁盘写满导致写日志失败,任何一种在单机视角下都看不出来。

本文只讲个人站长规模(3~30 台)的日志集中化,用的是 Loki 加 Alloy(原 Promtail 的继任者),不涉及 ES 那套重型方案。重点不在「怎么装」,而在日志送到中心之后怎么不被自己淹死——这一步做不好,集中化反而比不集中更痛苦。

一、先想清楚要查什么,再决定收什么

绝大多数人第一次做日志集中都会犯同一个错:把所有日志全量送过去。结果是中心节点一周就撑爆,查一条日志比 SSH 还慢,最后整个系统被弃用。开始之前先写下三到五个真实场景,比如:

  • 用户报「刚才提交订单失败」,要在两分钟内翻出全站相关错误。
  • Nginx 出现 502,要立刻判断是后端挂了还是超时。
  • 某台机器磁盘告警,要先知道是哪个进程在写。

这三个场景对应的日志其实是不同的:第一个只需要应用层 ERROR 级日志,第二个需要 Nginx 的 error.log 而不需要 access.log 的全量,第三个需要的是系统层磁盘事件。按场景砍掉 access.log 的全量采集,往往能直接省掉 80% 的日志量——访问日志的正确存储位置是归档压缩文件,不是实时日志中心。

二、架构只有三个角色,别搞复杂

Loki 的设计思路和 Prometheus 一样:只索引标签,不索引正文。日志正文压成块存对象存储或本地磁盘,查询时先靠标签缩小范围再扫块。理解这一点,后面所有的性能问题都能自己推理出来。

应用 / Nginx / 系统日志
        ↓
   Alloy (每台机器一个)
        ↓  HTTP 批量推送
   Loki (单机即可)
        ↓
  Grafana (查询与告警)

三台以内的机器,Loki 用单机模式(`-target=all`)就够,配上本地文件存储完全可以跑下去。不要一上来就上微服务模式,那需要至少六个进程和一套对象存储,运维成本远超收益。

Alloy 的角色是采集加打标签。它从文件读日志、按规则提取标签、再批量推给 Loki。关键约束是:不要在客户端做重处理,客户端只做「贴标签」和「剔除明显无用行」两件事,复杂的解析交给 Loki 的查询语句,这样改了规则不用重新部署所有机器。

三、标签设计:决定这套系统能不能用

Loki 的性能几乎完全由标签基数(label cardinality)决定。每个不同的标签组合就是一个独立的流(stream),流太多会直接把内存吃光。站内最常见的翻车写法是把 URL、请求 ID、用户 ID 打成语义标签:

# 错误示范:一行日志一个流
labels:
  job: nginx
  path: /article/1234
  request_id: a8f3c2

一小时的访问量就是几十万个流,Loki 会开始报 `too many outstanding requests`,甚至 OOM。正确做法是只用低基数标签,通常不超过四个:

labels:
  job: nginx          # 哪类日志:nginx / php / syslog / app
  host: web-01        # 哪台机器
  env: prod           # 环境
  level: error        # 仅当日志本身带级别时使用

其余字段全部留在正文里,靠 LogQL 的过滤器在查询时匹配。判断一个字段能不能做标签,标准很简单:它的取值集合是固定的一二十个,还是随时在增长。前者可以做标签,后者一律放正文。

一个实用技巧是把「能预先枚举的东西」塞进标签,比如站点名、服务名、日志类型。这些值来自配置文件而不是日志内容,天然低基数,查询时又刚好是最常用的过滤条件。

四、采集配置:从一台机器开始的完整示例

Alloy 用 River 语法(新版本叫 Alloy 语法),配置文件和传统的 YAML 差别不小,但结构清晰。下面是采集 Nginx 错误日志和系统日志的完整配置:

local.file_match "nginx" {
  path_targets = [{
    __path__ = "/var/log/nginx/*error.log",
    job      = "nginx",
    host     = "web-01",
    env      = "prod",
  }]
}

loki.source.file "nginx" {
  targets    = local.file_match.nginx.targets
  forward_to = [loki.write.default.receiver]
}

loki.source.journal "syslog" {
  labels    = { job = "syslog", host = "web-01", env = "prod" }
  forward_to = [loki.write.default.receiver]
}

loki.write "default" {
  endpoint {
    url = "http://10.0.0.10:3100/loki/api/v1/push"
  }
}

三个容易踩的点:

  • `host` 标签手工写死是隐患,扩机器时忘改就会两台混成一台。正确做法是让 Ansible 模板渲染,或直接用 `sys.env("HOSTNAME")` 取值。
  • 系统日志不要用 `loki.source.file` 读 `/var/log/syslog`,`loki.source.journal` 读 journald 更稳,还能自动带上 `unit`、`priority` 字段。
  • `forward_to` 写错不会有明显报错,只是日志静默不送。配置改完一定要在 Grafana 里点一下 Explore 确认有数据进来。

五、真正让你放弃这套系统的是「查询太慢」

集中化上线两周后,最常见的抱怨是「点一下查询要等半分钟」。这几乎总是查询写法的问题,而不是 Loki 性能不行。三条规则能解决九成情况:

规则一:时间范围必须在语句里,且尽量窄。默认查询六小时和查七天,扫描的块数量差几十倍。给 Grafana 的 Explore 面板设默认时间为一小时,比任何调优参数都管用。

规则二:先过滤标签,再用 `|=` 过滤正文。下面的写法是慢的:

{job="nginx"} |~ "Timeout|Connection refused"

因为它要先扫出所有 nginx 流的所有日志再逐行匹配。加上主机和级别过滤后快得多:

{job="nginx", host="web-01"} |= "Timeout"

规则三:统计类查询用 `count_over_time` 而不是把日志拉回来自己数。要查「过去一小时每分钟 502 的数量」,正确写法是:

sum by (host) (
  count_over_time({job="nginx"} |= " 502 " [1m])
)

这条语句在 Grafana 里可以直接画成曲线,用来做告警数据源,比把日志拉到本地 grep 快两个数量级。

六、保留策略与磁盘规划

Loki 的保留策略按流配置,写在 `limits_config` 里:

limits_config:
  retention_period: 720h          # 30 天
  ingestion_rate_mb: 4
  ingestion_burst_size_mb: 8
  max_query_series: 500

三个参数都值得按实际情况调:

  • retention_period 是最重要的一个。个人站没必要留一年,30 天足够覆盖「上次改了什么导致的问题」。超过 30 天的日志应该去压缩归档,而不是留在 Loki 里。
  • ingestion_rate_mb 是保护自己。某台机器突然日志暴涨(比如 PHP 报错刷屏)时,不加限制会瞬间把中心节点写挂,连带其他机器的日志一起丢。
  • max_query_series 防止一条烂查询把内存打满,默认值在单机部署下往往偏大。

磁盘占用按「每天日志量 × 保留天数 × 压缩比」估算就够了。Loki 对文本日志的压缩比通常在 8 到 15 倍之间,一条每天 500MB 的日志流,30 天大约占 1~2GB,单机跑毫无压力。容量不够时优先砍保留期,其次砍采集量,别急着加机器。

七、告警:从「日志有人看」到「日志会主动叫你」

集中化真正的价值不是事后查,而是能把日志变成告警源。Loki 的 ruler 支持用 LogQL 写告警规则,配置在 `ruler` 段:

groups:
  - name: web
    rules:
      - alert: Nginx502Spike
        expr: |
          sum(count_over_time({job="nginx"} |= " 502 " [5m])) > 20
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "五分钟内 502 超过 20 次"

告警规则只写三条就够,多了就是噪音:

  • 错误量突增:某一类错误在五分钟内的数量超过基线三倍。
  • 日志断流:某台机器超过十分钟没有任何日志进来——这通常意味着采集器挂了或磁盘满了,比任何错误日志都值得警惕。
  • 关键词命中:OOM、`Disk quota exceeded`、`Too many open files` 这类明确的故障信号。

第二条最容易被忽略,但它是唯一能发现「监控自己也挂了」的手段。用 `absent_over_time` 就能写:

absent_over_time({job="nginx", host="web-01"} [10m])

命中时说明这台机器十分钟没送日志,直接告警。

八、从零上线的顺序

不要一次性把十几台机器全接上来。推荐的推进顺序是:先在一台机器上跑通 Loki 加 Alloy,确认数据能查;再接入采集量最大的那台,观察中心节点的内存和磁盘;稳定三天后按业务重要性分批接入;最后才配置告警规则。每一步都留出观察窗口,出问题时能立刻回退。

回退方案也要提前想好:Alloy 停掉就只是不推送,本地日志文件照旧在写,业务完全不受影响。这是这套架构最大的优点——日志链路是旁路,挂了不伤业务。相比之下,直接在应用里同步写远程日志接口就是给自己埋雷。

九、小结

个人站的日志集中化,难点从来不是部署,而是控制成本和查询体验。记住三件事:按场景砍采集量,标签只用低基数,查询必须带时间范围和标签过滤。做到这三点,一台 2 核 4G 的机器就能撑住几十台服务器的日志。做完之后再回头看「出事了挨个 SSH 上去 tail」,会觉得那是一种完全没有可比性的工作方式。

Last modification:November 4th, 2026 at 07:26 pm

Leave a Comment