nginx error.log 里那 5 类 warn 到底要不要管

234
234
2026-10-24 / 0 评论 / 1 阅读 / 9746 字 / 正在检测是否收录...
这篇是服务器运维笔记,和 DNS 无关。站内的加密 DNS 服务在dns.23-4.cn。

/var/log/nginx/error.log 里堆着几万行 warn,你打开一看,第一条是「could not build optimal map_hash」——看不懂,也不敢动,就那么放着。

这篇的做法是:先按级别和类型统计,再逐个判定。不看统计直接读日志,会在同一个警告上浪费半小时。

下面所有数字都是我在一台跑了几个月的 Ubuntu 20.04 + nginx 1.18 上实际统计出来的。

一、先按级别数一遍

nginx 的日志级别从高到低:emerg > alert > crit > error > warn > notice > info。

for lv in emerg alert crit error warn notice info; do
  printf "%-8s " "$lv"
  grep -c "\[$lv\]" /var/log/nginx/error.log 2>/dev/null || echo 0
done

实测输出(有点反直觉):

emerg    0
alert    0
crit     1
error    504
warn     2
notice   1
info     0

warn 只有 2 条,error 有 504 条。

这就是第一个结论:别只盯着 warn。 很多人一搜「nginx error.log warn」就以为 warn 是主要噪音,实际上在这台机器上,真正值得看的是那 504 条 error。

二、按类型聚合(这一步最关键)

同一类错误会重复几万次,先把它们归并:

grep -E "\[error\]" /var/log/nginx/error.log \
  | sed -E 's/^.*\[error\] [0-9#]+: //; s/, client.*//; s/, server.*//' \
  | sort | uniq -c | sort -rn | head -12

实测这台机器的前几名:

    158 *9417356 access forbidden by rule
      5 *9439453 connect() failed (111: Connection refused) while connecting to upstream
      5 *9431818 connect() failed (111: Connection refused) while connecting to upstream
      5 *9364629 access forbidden by rule
      4 *9433151 connect() failed (111: Connection refused) while connecting to upstream

注意 *9417356 这种是 nginx 的连接序号,每次都不一样——所以同一个错误会被拆成很多行,uniq -c 数出来的次数是被稀释的。

想真正聚合,得把这个序号去掉:

grep -E "\[error\]" /var/log/nginx/error.log \
  | sed -E 's/^.*\[error\] [0-9#]+: \*[0-9]+ //; s/, client.*//; s/, server.*//' \
  | sort | uniq -c | sort -rn | head -10

实测这台机器(去掉序号后):

    190 access forbidden by rule
     23 connect() failed (111: Connection refused) while connecting to upstream
      3 recv() failed (104: Connection reset by peer) while reading response header from upstream
      2 upstream timed out (110: Connection timed out) while SSL handshaking to upstream
      2 FastCGI sent in stderr: "PHP message: PHP Warning: Undefined variable $index_result ...

对比一下:不去序号时 access forbidden 被拆成 158 + 5 = 163 行,去序号后是 190。 同一个错误被拆散了 27 份,你就很难判断它到底占多少比例。

这一步是整篇文章的核心——不看分布就去逐条读,等于在噪音里找信号。

三、五类常见 warn,逐个判定

类型 1:could not build optimal map_hash(可以忽略)

完整原文:

could not build optimal map_hash, you should increase either
map_hash_max_size: 8192 or map_hash_bucket_size: 64;
ignoring map_hash_bucket_size

判定:可以忽略,但值得顺手改掉。

它的意思很明确:map 指令建出来的哈希表不够优,nginx 用了备用方案。功能完全正常,只是查找时多比较几次。

为什么会有这个警告?因为你用了 map,而 map_hash_max_size 相对 map 的条目数偏小。看你有多少个 map 块:

grep -c "^ *map " /etc/nginx/nginx.conf /etc/nginx/conf.d/*.conf

实测:

/etc/nginx/nginx.conf:0
/etc/nginx/conf.d/00-dns-cors-map.conf:3
/etc/nginx/conf.d/typecho.conf:0
/etc/nginx/conf.d/zz-agh-doh.conf:1
/etc/nginx/conf.d/zz-sy-tls.conf:1

5 个 map 块。而配置里其实已经设过 map_hash_max_size:

/etc/nginx/nginx.conf:14:    #   nginx -t 报 "could not build optimal map_hash"。加大到 8192。
/etc/nginx/nginx.conf:15:    map_hash_max_size 8192;

但警告还在——因为这条指令写在 http 块里,而某些 map 块可能在 include 之后才被解析。正确做法是把它提到 http 块的最前面:

http {
    # 必须放在任何 map 之前
    map_hash_max_size 8192;
    map_hash_bucket_size 128;

    include /etc/nginx/conf.d/*.conf;
}

顺便说一个判据:这条警告会出现在 nginx -t 的输出里,所以不用等运行时才发现。

nginx -t

实测这台机器:

nginx: [warn] could not build optimal map_hash, you should increase either map_hash_max_size: 8192 or map_hash_bucket_size: 64; ignoring map_hash_bucket_size
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

注意:nginx -t 成功了,但 warn 还在。 这说明它确实是「配置能跑、只是不最优」,不是错误。

踩坑点:map_hash_max_size 和 map_hash_bucket_size 是两条不同的指令,调整哪个取决于 map 的条目数还是键长:

  • 条目多(几百上千)→ 加 map_hash_max_size;
  • 键很长(URL 路径做 key)→ 加 map_hash_bucket_size。

警告里说「increase either ... or ...」就是因为它不确定你该调哪个。盲目两个都加到很大只是浪费内存。

类型 2:an upstream response is buffered to a temporary file(要处理)

[warn] an upstream response is buffered to a temporary file
/var/lib/nginx/tmp/fastcgi/1/98/0000002981 while reading upstream

判定:要处理,但优先级低于它引发的磁盘问题。

它的意思是:上游(这里是 PHP-FPM)返回的响应大于 nginx 的缓冲区,nginx 把它写到了磁盘临时文件。

功能上没问题,响应还是会完整返回。但两个副作用:

  1. 慢——多了一次磁盘写读;
  2. 占空间——/var/lib/nginx/tmp 会持续增长,小根分区上可能撑爆。

处理方式(按响应大小调):

location ~ \.php$ {
    # 让缓冲区能装下正常响应
    fastcgi_buffers 16 16k;
    fastcgi_buffer_size 32k;
    fastcgi_busy_buffers_size 64k;

    # 给临时文件设上限,避免无限增长
    fastcgi_max_temp_file_size 0;
}

fastcgi_max_temp_file_size 0 表示完全禁用临时文件——如果响应超了缓冲区,直接报错而不是写盘。要不要这么激进取决于你的业务:

  • 响应通常不大 → 设 0,宁可报错也不占盘;
  • 有正常的大响应(比如导出文件)→ 保留一个上限,比如 64m。

实测这台机器:0 条这类 warn。 所以虽然它是最常被讨论的一类,但不一定是你的问题。

类型 3:access forbidden by rule(不是故障)

[error] *9417356 access forbidden by rule, client: 2409:8a5c:894:8170:...,
server: www.23-4.cn, request: "GET /dns-status/p2p/p2p/status HTTP/2.0"

判定:不是故障,是你在拦人。

这条是 deny all 或 location ~ /\. 之类的规则命中了。上面那条是有人在探测 /dns-status/p2p/p2p/status(一个不存在的路径),被规则挡了。

它记在 [error] 级别,但语义是「正常工作」——这正是为什么不能只看级别。

如果你不想让这类噪音占满日志,可以把那个 location 的日志关掉:

location ~ /\. {
    deny all;
    access_log off;
    error_log off;
}

⚠️ 这里有个版本陷阱,我实际踩到了。 网上流传的另一种写法是用 map 动态调日志级别:

# ⛔ 这段在 nginx 1.18 上不生效
map $request_uri $log_level {
    default        warn;
    ~^/dns-status/ crit;
}
error_log /var/log/nginx/error.log $log_level;

error_log 的第二个参数支持变量,是 nginx 1.19.5 之后才有的能力。先查你的版本:

nginx -v

实测这台机器:

nginx version: nginx/1.18.0 (Ubuntu)

1.18 —— 不支持。写了这段,nginx 会忽略变量(或者直接报配置错误),你以为降级了其实没有。

Ubuntu 20.04 的官方源就是 1.18,要这个特性得升到 1.20+ 或者用 nginx 官方源。在升级之前,老老实实用 error_log off 按 location 关。

别直接 error_log /dev/null——那会把整个 server 块(甚至整个 http 块)的真正问题也一起吞掉,包括上面那些 111 和 110。

类型 4:connect() failed (111: Connection refused) while connecting to upstream

[error] *9439453 connect() failed (111: Connection refused) while connecting to upstream

判定:要查,但通常是「后端没起来」而不是「nginx 配错了」。

111 是 ECONNREFUSED:连接被拒绝。含义是「目标端口上没有进程在监听」。

排查顺序:

# 1. 上游地址是什么?看 nginx 配置
grep -rn "proxy_pass\|fastcgi_pass" /etc/nginx/conf.d/*.conf | head -5

# 2. 那个端口有没有在监听
ss -tlnp | grep ':3000 '

# 3. 直接连一下试试
curl -sS -m 5 -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/

如果第 2 步是空的,就是后端没起来。别去改 nginx——它没错。

这台机器上的实例很有说服力。把 23 条 Connection refused 按上游地址聚合:

grep "Connection refused" /var/log/nginx/error.log \
  | grep -oE 'upstream: "[^"]+"' | sort | uniq -c | sort -rn | head -5

实测:

     14 upstream: "http://127.0.0.1:9100/ingest"
      4 upstream: "http://127.0.0.1:9100/chat_poll?since=3498"
      4 upstream: "http://127.0.0.1:9100/chat_poll?since=3497"
      1 upstream: "http://127.0.0.1:9100/chat_poll?since=3499"

全部指向 127.0.0.1:9100 的同一个后端。 结论很清楚:那个服务在某个时段挂了,nginx 正常地记录了连接失败。修法是让那个服务起来(或者加 systemd 自动重启),跟 nginx 配置一点关系都没有。

这条聚合命令的价值就在这里:它把「23 条 error」直接压缩成「1 个后端有问题」。

类型 5:SSL_do_handshake() failed ... bad key share

[crit] SSL_do_handshake() failed (SSL: error:141CF06C:SSL routines:
tls_parse_ctos_key_share:bad key share) while SSL handshaking,
client: 203.75.213.2, server: 0.0.0.0:443

判定:忽略。这是扫描器/老旧客户端。

bad key share 表示客户端在 TLS 1.3 握手里发了一个无效的 key share。正常浏览器不会这样,通常是漏洞扫描器或者不完整的 TLS 实现。

它记在 [crit] 级别——最高的实际级别(emerg/alert 都是 0)。看到 crit 很容易紧张,但这一条完全可以忽略。

怎么确认是扫描器:

grep "bad key share" /var/log/nginx/error.log | grep -oP 'client: \K[0-9.]+' | sort | uniq -c | sort -rn | head -5

如果同一个 IP 短时间内出现几十次,基本就是扫描。

四、判定流程(照着走)

LOG=/var/log/nginx/error.log

echo "=== 1. 级别分布(先看有没有 crit/alert/emerg)==="
for lv in emerg alert crit error warn; do
  printf "%-8s " "$lv"; grep -c "\[$lv\]" $LOG
done

echo "=== 2. 类型分布(去掉连接序号才能真正聚合)==="
grep -E "\[(error|crit|warn)\]" $LOG \
  | sed -E 's/^.*\[[a-z]+\] [0-9#]+: \*[0-9]+ //; s/, client.*//; s/, server.*//' \
  | sort | uniq -c | sort -rn | head -10

echo "=== 3. 有没有真影响业务的 ==="
grep -c "buffered to a temporary file" $LOG
grep -c "Connection refused" $LOG
grep -c "upstream timed out" $LOG

echo "=== 4. 临时目录有没有撑大 ==="
du -sh /var/lib/nginx/tmp 2>/dev/null || echo "(无此目录)"

验证做对了没有:

  1. 改完配置后 nginx -t 必须通过;
  2. nginx -s reload 之后,新产生的日志里目标类型的条目不再增加(wc -l 前后对比);
  3. 站点本身 curl -o /dev/null -w '%{http_code}' 仍然是 200。
# 记录基线
before=$(wc -l < $LOG)
# 做一次 reload
nginx -t && nginx -s reload
# 等 60 秒,制造一些流量,再看增量
sleep 60
after=$(wc -l < $LOG)
echo "新增 $((after - before)) 行"

五、一张判定表

warn / error 内容要管吗一句话
could not build optimal map_hash可忽略功能正常,只是哈希表不最优;顺手把 map_hash_max_size 提到 include 之前
an upstream response is buffered to a temporary file要管慢 + 占盘;调 fastcgi_buffers 或设 max_temp_file_size
access forbidden by rule不用管你在拦探测;嫌吵就单独关那个 location 的日志
connect() failed (111)要查后端没起来,不是 nginx 的问题
SSL_do_handshake() failed ... bad key share可忽略扫描器;看 client IP 分布确认
upstream timed out要查后端慢,先看后端自己的日志
worker_connections are not enough要管并发不够,调 worker_connections 或 worker_processes

六、结论

三件事,按顺序:

  1. 先数级别,确认有没有 emerg/alert/crit——这三个才是「真的出事」;
  2. 再聚合类型,一定要把 *连接序号 去掉,否则同一类错误会被拆成几千行;
  3. 按表判定,access forbidden 和 bad key share 看着吓人但没事,buffered to a temporary file 看着温和但会撑爆磁盘。

日志里的级别和严重程度不是一回事。[error] 可能只是有人在扫你的站,[warn] 可能正在慢慢写满你的根分区。

相关阅读

0个赞
取消