Nginx日志降噪与分层采样:少而准

一、日志写得多,不代表看得见

很多站长的日志策略只有一条:把所有东西都记下来,出事了再 grep。这个策略在访问量小的时候没问题,等到每天几百万行、单文件几个 G 的时候,它就失效了——不是磁盘不够,而是信号被噪声淹没了。

我经历过一次典型事故:某天下午开始,所有静态资源请求的耗时都涨了 30%。但日志里每秒几百行正常的 200 记录,异常的那几十条夹在中间,肉眼完全看不出来。等发现的时候已经过去了四个小时,用户早就抱怨过一轮了。

这篇讲的是怎么用采样把日志量降下来,同时不丢掉真正重要的信息。核心思想一句话:采样要按“信息价值”分层,而不是按“概率”一刀切。

二、最容易犯的错:无差别随机采样

最简单的采样是在 Nginx 里加一段:

map $request_id $sampled {
    default 1;
    ~^[0-9a-f]  0;   # 示意:一半请求不记
}

问题立刻出现:错误请求也被同等概率地丢掉了。假设错误率 0.1%,采样率 50%,你每天丢一半的错误样本;更要命的是,如果某个故障只发生在特定时间窗,采样后的数据会呈现出完全不同的比例,事后做归因分析会得出错误结论。

所以第一条原则:错误永远全量记,成功请求才采样。

三、按状态码分层采样:可落地的 Nginx 配置

Nginx 的 map 能在日志阶段做条件判断,这是最轻量的分层手段:

map $status $loggable {
    ~^[45]      1;   # 4xx/5xx 全量记录
    ~^3         1;   # 3xx 也记,重定向异常很常见
    default     0;
}

# 采样变量:用 request_id 的哈希做稳定采样,保证同一请求多次出现时结果一致
split_clients "${remote_addr}${request_uri}" $sampled {
    10%     1;
    *       0;
}

server {
    access_log /var/log/nginx/access.log combined if=$loggable;
    access_log /var/log/nginx/sampled.log combined if=$sampled;
}

这段配置有两个细节值得说:

  • split_clients 用的是请求特征的哈希,不是随机数。这样做的好处是同一个客户端访问同一个路径,每次的采样结果一致,事后可以把某个用户的完整访问序列拼起来,而不是碎成一地。
  • 两个 log 分开写。全量的错误日志保持小体积、保留长周期;采样日志体积大但只留 3~7 天,用来做趋势和性能分析。这样几年下来,磁盘占用是可控的。

四、日志降噪:先砍掉没价值的那一大半

采样是“少记”,降噪是“本来就不该记”。按经验,一个普通站点的访问日志里,至少有 40% 是毫无价值的内容:

  • 健康检查:负载均衡每 5 秒来一次,一天一万七千条,全都是一样的内容。
  • 静态资源:CSS/JS/图片的 200 响应,除了算带宽几乎没用,性能分析看的是慢请求。
  • 扫描器与爬虫:不带任何意义的 404 狂刷,一晚上几十万条。
  • 监控探针:Uptime 检查、拨测节点。
map $request_uri $skip_log {
    default                                              0;
    ~^/healthz                                            1;
    ~^/favicon\.ico                                       1;
    ~^/(wp-json|xmlrpc\.php)                              1;
    ~*\.(css|js|png|jpg|jpeg|gif|webp|svg|woff2?|ico)$    1;
}
map $http_user_agent $ua_noise {
    default                                              0;
    ~*(zgrab|masscan|nmap|sqlmap|nikto|Go-http-client)    1;
}
map "$skip_log$ua_noise" $should_log {
    "00"  1;
    default 0;
}
access_log /var/log/nginx/access.log combined if=$should_log;

砍完之后日志量通常能降到原来的 30%~50%。这部分损失是值得的:你本来也不会去 grep 一条 favicon 的 200。

五、应用日志:分级 + 结构化 + 采样

Web 服务器的日志好处理,应用日志才是大头。我的做法是三步走。

第一步:分级别乱用

最常见的毛病是 INFO 当 DEBUG 用。比如每个请求都打一行“开始处理请求”“查询数据库”“返回响应”,一个请求三行 INFO,量就翻了三倍。生产环境的 INFO 应该只记录业务上的关键节点(订单创建、支付成功、任务完成),其它一律降到 DEBUG,而 DEBUG 在生产默认关闭。

PHP-FPM 场景下还有个具体的坑:catch 里既 error_log 又 trigger_error,同一条异常被打两遍。上线前 grep 一下 error_log( 和 trigger_error( 的重复,能省掉不少。

第二步:结构化,别再拼字符串

// 难分析
error_log("[WARN] user {$uid} failed login from {$ip} attempts={$n}");

// 好分析
error_log(json_encode([
    'level' => 'warn', 'event' => 'login_failed',
    'uid' => $uid, 'ip' => $ip, 'attempts' => $n,
], JSON_UNESCAPED_UNICODE));

结构化之后,统计“同一个 IP 失败次数”从一行复杂的正则变成了一次 JSON 解析。配合 jq 或者把日志丢进 Loki/ClickHouse,聚合查询的成本会降一个数量级。

第三步:按事件类型采样,不按时间采样

最有价值的采样维度是事件类型。我的默认规则表:

  • 错误、异常、安全事件:100% 记录,永不采样。
  • 慢请求(超过 1 秒):100% 记录,加一个单独的慢日志文件。
  • 普通成功请求:1%~5% 采样。
  • 定时任务的“正常完成”:只在状态变化时记录(成功→失败、失败→成功),连续成功不写日志。

最后一条特别有用。一个每分钟跑一次的任务,每天 1440 条“成功”日志,除了让日志文件变大以外没有任何意义。只在状态变化时记录是降噪里性价比最高的一个改动。

六、日志轮转:采样省下的空间别被轮转吃掉

采样做完之后,很多人忽略了轮转配置,结果磁盘占用并没有下来多少。原因是轮转保留太多份,而且压缩没开:

/var/log/nginx/*.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    create 0640 www-data adm
    sharedscripts
    postrotate
        [ -f /run/nginx.pid ] && kill -USR1 $(cat /run/nginx.pid)
    endscript
}

几个点需要注意:

  • delaycompress 配 compress:不延迟的话,压缩可能和写入同一份文件,造成最后一段日志丢失。
  • notifempty:空文件不轮转,避免产生一堆 0 字节的备份。
  • Nginx 用 USR1 重开日志文件,不是 reload;用 reload 会有几百毫秒的请求抖动。
  • 采样日志(体积大)给 7 天,错误日志(体积小)给 60 天。保留期应该按“信息密度”定,不是按“文件类型”一刀切。

七、验证采样没丢掉要紧的东西

采样配置上线后,必须做一次反向验证,否则你不知道自己丢了什么:

# 1. 抽样检查:错误日志里是否覆盖了所有状态码
awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head

# 2. 对比采样前后的量级,并确认 5xx 一条不少
grep -c ' 50[0-9] ' /var/log/nginx/access.log

# 3. 慢请求单独统计(假设日志里带了 request_time)
awk '{ if ($NF > 1) print }' /var/log/nginx/access.log | wc -l

第二条是关键:5xx 的数量在采样前后必须完全一致。如果采样后 5xx 变少了,说明采样条件把错误也一起筛掉了,配置有问题,立刻回滚。

八、成本核算:先把账算清再动手

降噪和采样是要付出代价的,动手前最好把三笔账算清楚,否则容易改到一半发现成本比收益还高。

第一笔是磁盘账。一条 combined 格式的 Nginx 日志约 250 字节,一百万条就是 250MB,压缩后约 25MB。所以先算出当前的日增长量:du -sh /var/log/nginx 除以保留天数,再乘上你想要的天数,就是这个目录的稳定占用。这个数字出来后,你会发现绝大多数站点的日志根本不是磁盘问题,而是查询问题——文件才 2GB,但 grep 一遍要几十秒,那该解决的是索引而不是体积。

第二笔是排查能力账。采样后最直接的影响是“数不出来”。原来可以精确统计某个错误码出现的次数,采样后只能估算。所以错误全量、成功采样这条分界线不能破,破了就等于主动放弃归因能力。如果某个成功请求的维度你确实需要精确统计(比如支付回调的耗时分布),就单独给它开一条全量日志,别指望从采样日志里还原。

第三笔是维护账。每多一个 map 和一条 log 指令,就多一处需要在换服务器、改架构时同步的配置。我见过最夸张的 Nginx 配置里有四十多个 map,最后没人说得清哪条规则还在生效。控制办法是所有降噪规则集中写在一个 include 文件里,主配置只留一行 include,规则文件里每条都写清注释和添加日期,半年没动过的规则定期清掉。

三笔账算完,通常得出的结论都是“先做降噪,采样只做成功请求的 5%”,这恰好就是前面几节给出的方案。之所以还要算一遍,是因为数字能让你在别人质疑“为什么要丢日志”的时候,拿出一个具体的、可复核的依据——能算清楚的配置,才守得住。

九、小结:采样的目标是“少而准”

把这一套做完,同一台机器上的日志量通常能降到原来的四分之一到三分之一,而排障能力不降反升——因为日志里剩下的都是有用信息,grep 的命中率高了。

三条原则,按优先级排:先降噪(删掉没价值的内容),再分层(错误全量、成功采样),最后轮转(按信息密度定保留期)。顺序很重要:先采样后降噪,等于在垃圾里做随机抽样,纯属浪费。

最后一句提醒:采样配置上线前,务必先跑一周“只降噪、不采样”,看看日志量能降到多少。很多时候降噪做完就已经够了,采样根本用不上——能用过滤解决的问题,不要用采样解决,因为过滤是无损的,采样是有损的。

Last modification:October 29th, 2026 at 09:25 pm

Leave a Comment