这篇是服务器运维笔记,和 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 0warn 只有 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:15 个 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 把它写到了磁盘临时文件。
功能上没问题,响应还是会完整返回。但两个副作用:
- 慢——多了一次磁盘写读;
- 占空间——
/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 "(无此目录)"验证做对了没有:
- 改完配置后
nginx -t必须通过; nginx -s reload之后,新产生的日志里目标类型的条目不再增加(wc -l前后对比);- 站点本身
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 |
六、结论
三件事,按顺序:
- 先数级别,确认有没有
emerg/alert/crit——这三个才是「真的出事」; - 再聚合类型,一定要把
*连接序号去掉,否则同一类错误会被拆成几千行; - 按表判定,
access forbidden和bad key share看着吓人但没事,buffered to a temporary file看着温和但会撑爆磁盘。
日志里的级别和严重程度不是一回事。[error] 可能只是有人在扫你的站,[warn] 可能正在慢慢写满你的根分区。