一个人管十几台服务器的时候,最先被放弃的往往不是监控,而是日志。机器少的时候习惯 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」,会觉得那是一种完全没有可比性的工作方式。