首页
关于本站
私人 DNS 加密设置教程
全部文章
推荐
加密 DNS 服务
DNS 实时战报
过滤规则订阅
搜索
网络与 DNS
加密 DNS 实操
路由器与家庭网络
自建服务
AdGuard Home
服务器运维
开发与工具
安全与隐私
登录
搜索
23-4.CN
累计撰写
59
篇文章
累计收到
3
条评论
首页
分类
网络与 DNS
加密 DNS 实操
路由器与家庭网络
自建服务
AdGuard Home
服务器运维
开发与工具
安全与隐私
页面
关于本站
私人 DNS 加密设置教程
全部文章
推荐
加密 DNS 服务
DNS 实时战报
过滤规则订阅
注册丨登录
搜索到
7
篇
分类为
服务器运维
的文章
2026-10-24
nginx error.log 里那 5 类 warn 到底要不要管
这篇是服务器运维笔记,和 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_sizeaccess 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] 可能正在慢慢写满你的根分区。相关阅读服务器磁盘满了怎么查:df / du / inode / 日志轮转的可执行排查顺序进程被 OOM Killer 杀掉之后:dmesg 怎么读、怎么防止再发生谁在占着这个端口:ss / lsof / /proc 三条路各自的盲区
2026年10月24日
1 阅读
0 评论
0 点赞
2026-10-24
谁在占着这个端口:ss / lsof / /proc 三条路各自的盲区
这篇是服务器运维笔记,和 DNS 无关。站内的加密 DNS 服务在dns.23-4.cn。启动服务报 bind: Address already in use,或者 EADDRINUSE——你要查「谁占了这个端口」。答案通常给三个:ss、lsof、netstat。但它们各自有盲区,而且在特定的情况下会给出「端口没被占用」这种完全错误的结论,让你去改代码。这篇把三条路都走一遍,重点是什么时候它们会骗你。下面所有输出都是我在一台 Ubuntu 20.04 上实际跑出来的。一、先看工具在不在for t in ss netstat lsof fuser; do printf "%-10s " $t command -v $t >/dev/null && echo yes || echo NO done实测这台机器:ss yes netstat NO lsof yes fuser yesnetstat 已经不在默认安装里了(net-tools 包)。这不是意外——ss 是它的替代品,而且更快。所以下面主要讲 ss。二、ss:默认选择,但有三个盲区最常用的一条:ss -tlnp参数含义:-t TCP、-l 只听 LISTEN、-n 不做名字解析、-p 显示进程。实测输出(截前几行):State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 511 0.0.0.0:2096 0.0.0.0:* users:(("nginx",pid=1253931,fd=27),("nginx",pid=1253930,fd=27),...) LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1253931,fd=6),...) LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=895990,fd=13)) LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=886753,fd=4))注意 0.0.0.0:2096 那一行有五个 nginx pid——master + 4 个 worker 都继承了同一个 fd。这很正常,不是重复占用。盲区 1:-p 需要权限不加 sudo 时,别人进程的 Process 列是空的:LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=886753,fd=4)) ← root 跑的,能看 LISTEN 0 100 0.0.0.0:9000 0.0.0.0:* ← 别的用户跑的,pid 看不到你看到端口被占了,但看不到是谁。先加 sudo 再下结论。盲区 2:UDP 和 TCP 是两个独立命名空间ss -tlnp 只查 TCP。如果你要查的端口是 UDP 的(DNS、QUIC、syslog 都可能是),这条命令会告诉你「没被占用」,而实际上是占着的。# UDP 要单独查 ss -ulnp这是最常见的误判来源:一个服务同时监听 TCP 和 UDP 的 53 端口,你只查了 TCP,以为端口空着,然后 bind UDP 失败。盲区 3:IPv4 和 IPv6 是两个独立 socket(除非开了 dual-stack)ss -tlnp | grep ':443 '可能只返回 0.0.0.0:443(纯 IPv4),而 [::]:443(IPv6)是另一个 socket。如果你 bind 的时候没指定地址族,可能碰到「IPv4 空着但 IPv6 被占」。ss -4 / ss -6 可以分开看:ss -4 -tlnp | grep ':443 ' ss -6 -tlnp | grep ':443 '三、lsof:信息更全,但更慢lsof -iTCP -sTCP:LISTEN -P -n-iTCP 只看 TCP;-sTCP:LISTEN 只看监听状态;-P 不把端口号转成服务名(必须加,否则 443 会显示成 https,grep 不到);-n 不做 IP 反解(不加会很慢,因为要对每个 IP 查 DNS)。实测:COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME nginx 2715 root 6u IPv4 38108 0t0 TCP *:80 (LISTEN) nginx 2715 root 7u IPv6 38109 0t0 TCP *:80 (LISTEN) nginx 2715 root 11u IPv4 10248389 0t0 TCP *:8443 (LISTEN)lsof 的优势:能看到同一个进程占的所有端口和文件,而且 IPv4/IPv6 分得清清楚楚(上面 *:80 出现了两次,一次 IPv4 一次 IPv6)。它的劣势:慢。lsof 会遍历 /proc 下所有进程的所有 fd,机器上进程多的时候要几秒。踩坑点:不加 -P 是新手最常见的错误。lsof -iTCP -sTCP:LISTEN -n | grep 443 # ← 可能一条都匹配不到 lsof -iTCP -sTCP:LISTEN -P -n | grep 443 # ← 正确因为默认会把端口 443 显示成 https,你 grep 443 自然匹配不上。四、/proc:不用任何工具,也不受权限限制(部分)这一条最值得学,因为它在容器里、精简系统里、ss 和 lsof 都没有的时候仍然可用。原理:/proc/net/tcp 里记录了所有 TCP socket,其中有一列是 inode。而每个进程的 /proc/PID/fd/* 是符号链接,指向 socket:[inode]。两边一对,就知道谁持有这个 socket。第 1 步:从 /proc/net/tcp 找到端口的 inode/proc/net/tcp 里端口是十六进制的。443 = 0x01BB,80 = 0x0050。换算命令(注意别踩下面的坑):for p in 80 443 853 53; do printf "%-5s -> %s\n" "$p" "$(awk -v p="$p" 'BEGIN{printf "%04X", p}')" done实测:80 -> 0050 443 -> 01BB 853 -> 0355 53 -> 0035踩坑点(我实际踩到的):不要写 printf "%04X" $p。$ printf "%04X" 443 0001 ← 错!sh 的 printf 把 443 当成十进制解析后没有按 %X 处理 $ awk 'BEGIN{printf "%04X", 443}' 01BB ← 对sh(dash)的内置 printf 处理 %X 时行为和 bash 不一致,会把数字截断成奇怪的结果(443 变成 0001,80 变成 0028)。用 awk 算十六进制最稳。反方向验证:$ echo $((0x01BB)) 443 $ echo $((0x0050)) 80第 2 步:反查谁持有这个 inodeinode=$(awk 'NR>1 && $2 ~ /:01BB$/ {print $10; exit}' /proc/net/tcp) echo "inode=$inode"inode=142089然后拿它去反查:for p in /proc/[0-9]*; do for fd in $p/fd/*; do [ -L "$fd" ] && [ "$(readlink $fd)" = "socket:[$inode]" ] && \ echo "持有者: $p $(cat $p/comm 2>/dev/null)" done done 2>/dev/null实测输出:持有者: /proc/1253928 nginx 持有者: /proc/1253929 nginx 持有者: /proc/1253930 nginx 持有者: /proc/1253931 nginx 持有者: /proc/2715 nginx五个 nginx 进程——master 加四个 worker,和 ss 的输出完全对得上。这条路的盲区/proc/PID/fd 里,别人进程的 fd 你读不到(需要 root)。所以非 root 跑这段,只会看到自己的进程。但和 ss -p 不同的是:你能确定「端口确实被占着」(inode 存在),只是不知道是谁。这已经足够区分「端口没被占」和「端口被占但我没权限看」——而这正是排查里最关键的一步。五、还有两种「端口被占」的假象假象 1:TIME_WAIT 让你以为端口还在用ss -tan | grep ':443 ' | awk '{print $1}' | sort | uniq -c如果输出里大量 TIME_WAIT,说明连接刚关闭,内核还在等 2MSL。这通常不影响 bind(除非你用了 SO_REUSEADDR 之外的选项),但会让 ss 的输出看起来很吓人。TIME_WAIT 不是「被占用」,是「等超时」。区别很重要:前者要杀进程,后者等 60 秒自己就没了。假象 2:SO_REUSEPORT 允许多个进程监听同一端口ss -tlnp | grep ':80 '如果看到多个不同的进程名都监听 :80,那不是异常,是用了 SO_REUSEPORT(nginx 多进程、某些负载均衡器会这么干)。内核会把新连接分给其中一个。这时「谁占着端口」这个问题的答案就是「不止一个,它们是设计好的」。六、对照表:什么时候用哪个场景用哪个理由快速看一眼ss -tlnp最快,默认就有看不到 pid加 sudo-p 需要权限查 UDPss -ulnp-tlnp 完全不覆盖 UDP查 IPv6ss -6 -tlnpv4/v6 是独立 socket要完整上下文lsof -iTCP -sTCP:LISTEN -P -n能看进程的所有资源精简系统 / 容器/proc/net/tcp + /proc/*/fd不依赖任何工具只想知道端口通不通nc -z host port不关心是谁,只关心能不能连七、一页排查脚本PORT=443 HEX=$(awk -v p="$PORT" 'BEGIN{printf "%04X", p}') echo "PORT=$PORT HEX=$HEX" echo "=== TCP 监听 ===" ss -tlnp | grep ":$PORT " echo "=== UDP 监听(别漏)===" ss -ulnp | grep ":$PORT " || echo "(UDP 无)" echo "=== IPv4 / IPv6 分开看 ===" echo -n "v4: "; ss -4 -tlnp | grep -c ":$PORT " echo -n "v6: "; ss -6 -tlnp | grep -c ":$PORT " echo "=== /proc 反查(不依赖工具)===" for inode in $(awk -v p=":$HEX" 'NR>1 && index($2,p) {print $10}' /proc/net/tcp); do for pr in /proc/[0-9]*; do for fd in $pr/fd/*; do [ -L "$fd" ] && [ "$(readlink "$fd")" = "socket:[$inode]" ] && \ echo " inode=$inode -> $pr $(cat $pr/comm 2>/dev/null)" done done 2>/dev/null done echo "=== 状态分布(TIME_WAIT 不是占用)===" ss -tan | grep ":$PORT " | awk '{print $1}' | sort | uniq -c实测这台机器对 443 跑完整脚本的输出:PORT=443 HEX=01BB === TCP 监听 === LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=1253931,fd=17),...,("nginx",pid=2715,fd=17)) LISTEN 0 511 [::]:443 [::]:* users:(("nginx",pid=1253931,fd=18),...,("nginx",pid=2715,fd=18)) === UDP 监听(别漏)=== (UDP 无) === IPv4 / IPv6 分开看 === v4: 1 v6: 1 === 状态分布 === 43 ESTAB 2 FIN-WAIT-2 2 LISTEN 7 TIME-WAIT注意 v4: 1 和 v6: 1——同一个 443 端口有两个独立的 LISTEN socket(0.0.0.0:443 和 [::]:443)。如果只查 IPv4,你会以为「只有一个」,而实际 bind 失败的可能是 IPv6 那一个。验证做对了没有:ss 和 /proc 两条路给出的进程列表应该完全一致(这里都是那 5 个 nginx pid)。如果不一致,说明你漏了 UDP 或者漏了 IPv6——那才是真正需要继续查的地方。八、一句话总结ss -tlnp 是起点,但它只覆盖 TCP + IPv4 + 你自己有权限看的进程。这三个限制之外的每一种情况,都会让它给你一个「端口空着」的错误答案。加 sudo、加 -u、加 -6,或者退回 /proc 自己查——这四步能覆盖几乎所有场景。最后补一个容易忽略的点:别拿 netstat 的肌肉记忆直接套 ss。两者参数名看着一样(netstat -tulnp / ss -tulnp),但 ss 对权限更敏感,而且参数的语义有细微差别。如果你在网上抄来的命令跑出来是空的,先 ss --help 确认这个参数在你的版本里存在——老版本 ss 遇到不认识的参数时,有些情况是静默返回空结果而不是报错。这又是一个「看起来端口没被占用、其实是工具骗了你」的来源。相关阅读进程被 OOM Killer 杀掉之后:dmesg 怎么读、怎么防止再发生服务器磁盘满了怎么查:df / du / inode / 日志轮转的可执行排查顺序nginx error.log 里那 5 类 warn 到底要不要管
2026年10月24日
2 阅读
0 评论
0 点赞
2026-10-24
进程被 OOM Killer 杀掉之后:dmesg 怎么读、怎么防止再发生
这篇是服务器运维笔记,和 DNS 无关。站内的加密 DNS 服务在dns.23-4.cn。进程突然消失,日志里没有任何异常,systemctl status 显示 killed 或者干脆 inactive (dead)——这时候最容易被误导的方向是「程序有 bug 崩了」。实际上很可能是内核把它杀了。我在一台 3.8 GB 内存的小机器上排查过好几次。最近一次的真实记录:[529021.651374] Out of memory: Killed process 1461869 (python3) total-vm:2542300kB, anon-rss:2529504kB, file-rss:0kB, shmem-rss:0kB, UID:0 pgtables:5000kB oom_score_adj:0一个 python3 进程吃到了 2.5 GB anon-rss,然后被内核杀掉。这台机器总共 3835 MB。这篇讲三件事:怎么确认是 OOM、那行日志每个字段什么意思、以及怎么让它别再发生。一、先确认到底是不是 OOM第一件事永远是查内核日志,不是查应用日志。dmesg | grep -i "killed process"实测这台机器的输出(最近 3 条):[529021.651374] Out of memory: Killed process 1461869 (python3) total-vm:2542300kB, anon-rss:2529504kB, file-rss:0kB, shmem-rss:0kB, UID:0 pgtables:5000kB oom_score_adj:0 [529099.143488] Out of memory: Killed process 1462964 (python3) total-vm:2285420kB, anon-rss:2272596kB, file-rss:0kB, UID:0 pgtables:4504kB oom_score_adj:0 [960123.301888] Out of memory: Killed process 281498 (python3) total-vm:2296232kB, anon-rss:2132456kB, file-rss:4kB, shmem-rss:0kB, UID:0 pgtables:4232kB oom_score_adj:0数一下次数:dmesg | grep -ci "out of memory"4四次。 全是 python3,全是 2.1~2.5 GB。踩坑点:dmesg 在某些发行版上普通用户读不到,会输出空。这时要么 sudo dmesg,要么读持久化日志:# systemd 系统 journalctl -k | grep -i "killed process" # 或按时间窗口 journalctl -k --since "1 day ago" | grep -i "out of memory"dmesg 是环形缓冲区,重启就没了。如果机器刚重启过,dmesg 里查不到,但 journalctl -k 可能还在(取决于 journald 的持久化配置)。二、那行日志每个字段什么意思以这条为例:Out of memory: Killed process 1461869 (python3) total-vm:2542300kB, anon-rss:2529504kB, file-rss:0kB, shmem-rss:0kB, UID:0 pgtables:5000kB oom_score_adj:0字段含义怎么用1461869PID已经不存在了,只能用来对日志时间(python3)进程名先确认是不是你的业务进程total-vm虚拟地址空间总量通常没用,虚拟内存可以远超物理内存anon-rss匿名页常驻内存这才是真实占用,看这个file-rss文件映射的常驻页一般很小,代码段和 mmap 的文件shmem-rss共享内存常驻页用了 shm/大页的会很大pgtables页表本身占的内存内存碎片化严重时会异常大oom_score_adj优先级调整值-1000 到 1000,越大越容易被杀关键判断:anon-rss 接近 total-vm,说明是真实的内存分配(堆上的数据),不是 mmap 映射出来的虚拟空间。上面三条都是这样(2.5 GB vs 2.54 GB),说明程序真的把 2.5 GB 数据读进内存了。如果反过来,total-vm 很大但 anon-rss 很小,那是正常的——很多运行时(JVM、Go、Python)会预留大量虚拟地址空间。pgtables:5000kB 也值得注意。5 MB 页表意味着进程有大量分散的内存页,通常是内存碎片。这种进程往往不是「用得多」,而是「用得散」。三、内核是依据什么挑中它的oom_score 决定了谁被杀。每个进程都有:for f in /proc/self/oom_score /proc/self/oom_score_adj /proc/self/oom_adj; do printf "%-28s " $f; cat $f done实测:/proc/self/oom_score 666 /proc/self/oom_score_adj 0 /proc/self/oom_adj 0oom_score(0~1000)是内核算出来的,主要由内存占用比例决定;oom_score_adj(-1000~1000)是你可以调的偏移量;oom_adj 是老的、已废弃的接口,别用。找当前最容易被杀的进程:for p in /proc/[0-9]*; do [ -r "$p/oom_score" ] || continue printf "%5s %8s %s\n" "$(cat $p/oom_score 2>/dev/null)" "$(cat $p/oom_score_adj 2>/dev/null)" "$(cat $p/comm 2>/dev/null)" done | sort -rn | head -10这个排序就是内核的候选名单顺序。排第一的基本就是下次会被杀的。四、让它别再发生:四种手段,按性价比排手段 1:给关键进程设 oom_score_adj = -1000(最省事)# 临时生效 echo -1000 > /proc/$(pidof sshd)/oom_score_adj # systemd 服务里永久生效 systemctl edit myservice在打开的编辑器里写:[Service] OOMScoreAdjust=-1000注意:-1000 表示「永不杀」。⛔ 别给业务进程设 -1000——那只是把被杀的对象换成了别人(可能是 sshd 或数据库)。这个值应该留给 sshd、systemd 本身这类「死了就登不上机器」的进程。手段 2:用 cgroup 给服务设内存上限(最推荐)systemd 245 支持 MemoryMax,超了只杀这个 cgroup 里的进程,不影响整机。先看当前值:systemctl show -p MemoryMax -p MemoryHigh -p OOMPolicy nginx实测(默认全是无限):OOMPolicy=stop MemoryHigh=infinity MemoryMax=infinity设置上限:systemctl edit myservice[Service] MemoryMax=512M MemoryHigh=400M OOMPolicy=stopMemoryHigh 是软限制:超过后内核会强力回收(让进程变慢),但不杀;MemoryMax 是硬限制:超过直接杀,且只杀这个 cgroup 内的;OOMPolicy=stop 表示 cgroup 内 OOM 时把整个服务停掉(而不是只杀一个进程,留下半死不活的状态)。这是最该用的手段,因为它把「整机 OOM」变成了「单个服务重启」,故障范围小了一个数量级。手段 3:加 swap(有争议,但对小机器有用)free -m实测这台机器: total used free shared buff/cache available Mem: 3835 1283 260 9 2291 2277 Swap: 0 0 0Swap 是 0。 没有 swap 意味着内存一旦用满,内核只有一条路:杀进程。加一个 2 GB 的 swap 文件:fallocate -l 2G /swapfile chmod 600 /swapfile mkswap /swapfile swapon /swapfile echo '/swapfile none swap sw 0 0' >> /etc/fstab验证:swapon --show free -m踩坑点:swap 不是万能药。如果进程是持续增长的(内存泄漏),加 swap 只是把「立刻被杀」变成「慢慢变卡,最后连 swap 也用满还是被杀」。swap 的正确用途是吸收突发峰值,不是掩盖泄漏。另外 vm.swappiness 默认 60,对小内存机器建议调到 10~20,让内核尽量不把热数据换出去:sysctl -w vm.swappiness=10手段 4:找根因(唯一真正的解法)上面三条都是「控制损失」,不解决「为什么它要吃 2.5 GB」。Python 进程吃到 2.5 GB,最常见的四个原因:原因特征怎么查一次性读入大文件anon-rss 突然涨到文件大小量级查代码里有没有 read() / readlines()无界缓存/字典缓慢单调增长,永不回落加 len(cache) 日志生成器误用成 listlist(gen) 把整个结果集物化搜 list( 包生成器的地方子进程/线程泄漏进程数或线程数持续涨`ls /proc/PID/task \wc -l`看进程内存的时间序列(不用装任何工具):PID=$(pidof myservice) while true; do echo "$(date +%H:%M:%S) $(awk '/VmRSS/{print $2, $3}' /proc/$PID/status)" sleep 5 doneVmRSS 单调上涨且不回落 = 泄漏。锯齿形(涨到某个值又掉下来)= 正常的批量处理峰值。五、怎么验证做对了改完配置,做一次受控压测,别等生产环境再出问题。# 1. 确认上限已生效 systemctl show -p MemoryMax -p MemoryHigh -p OOMPolicy myservice # 2. 确认 swap 已启用 swapon --show free -m # 3. 观察 PSI(内存压力),这是比 free 更准的指标 cat /proc/pressure/memory实测这台机器的 PSI:some avg10=0.00 avg60=0.00 avg300=0.00 total=138645986 full avg10=0.00 avg60=0.00 avg300=0.00 total=59592289some = 至少有一个任务因为等内存而卡住的时间占比;full = 所有任务都卡住的时间占比。full 长期大于 0 就说明内存真的不够了。这是比 free 好用得多的指标:free 里的 available 再大,只要 PSI 的 full 在涨,就说明实际有压力;反过来 free 看着很少但 buff/cache 可回收、PSI 为 0,那就是健康的。第 4 步,故意触发一次,确认隔离有效:# 在一个 cgroup 里跑一个吃内存的程序,确认只有它被杀 systemd-run --scope -p MemoryMax=100M python3 -c "a=bytearray(200*1024*1024)"实测输出:Running scope as unit: run-r258b23d3d64041bc9f1ce1255cf99392.scope Killed一个 200 MB 的分配,被 100 MB 的硬上限立刻截断,只杀这一个 scope——机器上其它服务完全不受影响。这就算验证通过了。对比一下不设上限时会发生什么:同样的分配会真的吃满 200 MB,如果整机剩余内存不够,内核就要在全局挑一个进程杀——可能就是你的数据库或 sshd。另外看一眼这台机器当前的候选名单(oom_score 降序前 5):for p in /proc/[0-9]*; do [ -r "$p/oom_score" ] || continue printf "%5s %8s %s\n" "$(cat $p/oom_score 2>/dev/null)" "$(cat $p/oom_score_adj 2>/dev/null)" "$(cat $p/comm 2>/dev/null)" done | sort -rn | head -5实测: 734 0 python3 708 0 AdGuardHome 706 0 python3 671 0 php-fpm 671 0 php-fpm排第一的 python3 就是下一次最可能被杀的。这份名单比任何监控面板都直接——它告诉你「如果再 OOM 一次,谁先死」。六、完整流程回顾# 1. 确认是不是 OOM(不是查应用日志) dmesg | grep -i "killed process" journalctl -k --since "1 day ago" | grep -i "out of memory" # 2. 读那行日志:重点看 anon-rss,不是 total-vm # anon-rss ≈ total-vm ⇒ 真实内存分配,不是虚拟空间 # 3. 看谁最容易被杀 for p in /proc/[0-9]*; do printf "%5s %s\n" "$(cat $p/oom_score 2>/dev/null)" "$(cat $p/comm 2>/dev/null)"; done | sort -rn | head # 4. 保护关键进程(只给 sshd 这种) echo -1000 > /proc/$(pidof sshd)/oom_score_adj # 5. 给业务服务设硬上限(最推荐) systemctl edit myservice # MemoryMax=512M / OOMPolicy=stop # 6. 有 swap 吗?没有就加,但别拿它掩盖泄漏 free -m; swapon --show # 7. 用 PSI 判断真实压力 cat /proc/pressure/memory # 8. 找根因:VmRSS 是否单调上涨 awk '/VmRSS/{print $2, $3}' /proc/$(pidof myservice)/statusOOM 不是一个「需要修 bug」的故障,它是一个容量与隔离问题。绝大多数「进程莫名消失」都能靠第 5 步(cgroup 硬上限)从「整机雪崩」降级成「单服务重启」——这是投入产出比最高的一步。相关阅读服务器磁盘满了怎么查:df / du / inode / 日志轮转的可执行排查顺序谁在占着这个端口:ss / lsof / /proc 三条路各自的盲区nginx error.log 里那 5 类 warn 到底要不要管SQLite 博客的备份与恢复:为什么不能 cp,该用 .backup 还是 VACUUM INTO
2026年10月24日
1 阅读
0 评论
0 点赞
2026-10-05
服务器磁盘满了怎么查:df / du / inode / 日志轮转的可执行排查顺序
这篇是服务器运维笔记,和 DNS 无关。站内的加密 DNS 服务在dns.23-4.cn。小服务器的根分区通常很小。我手上这台香港的小机器就是 1.2 GB 的根分区,日常只剩 300 多 MB。这种机器上「磁盘满」不是小概率事件,而是迟早的事——而且它挂起来的样子往往不像磁盘问题:nginx 报 500、PHP 写不了 session、数据库锁不上,你却在查代码。这篇是一份按顺序执行的排查清单,从「确认是磁盘」到「找到元凶」再到「防止复发」。一、先确认是不是磁盘,别急着查别的df -h看三件事:哪个挂载点是满的(Use% 接近或达到 100%);是不是根分区满(/),而不是数据分区——根分区满会连带影响 /tmp、日志、session,表现最杂;Available 是不是真的接近 0。# 记录一个基线,后面所有操作都拿它对比 df -h /还有一个容易被忽略的:inode 用满。df -iinode 满了的表现和磁盘满了几乎一样(写不进去文件),但 df -h 会显示还有空间。大量小文件(session 文件、缓存碎文件、邮件队列)是典型原因。二、找到是谁占的不要一上来 du -sh /*,那会遍历整个文件系统、很慢,还可能把挂载的外部存储也扫进去。用 -x 限制在同一个文件系统:# 第一层:看哪个顶层目录大 du -x -h --max-depth=1 / 2>/dev/null | sort -h # 锁定嫌疑目录后,再往里钻一层 du -x -h --max-depth=1 /var 2>/dev/null | sort -h du -x -h --max-depth=1 /var/log 2>/dev/null | sort -hsort -h 让结果按人类可读单位排序,一眼就能看到最大的。小服务器上最常见的四个元凶:目录原因处理/var/log日志没有轮转,或轮转配置没生效配 logrotate,或直接限制大小/var/lib/nginx/tmp 或类似nginx 把大的上游响应缓冲到磁盘见下面第四节/var/cache/apk(Alpine)包管理器的下载缓存apk cache clean/var/lib/docker镜像和容器层堆积docker system prune(确认后再执行)三、最阴的一种:文件被删了,空间没释放这是排查里最容易卡住的情况:du 加起来明明只有 200 MB,df 却说用掉了 1 GB。原因是有进程还持有已删除文件的句柄。文件在目录里已经看不见了,但 inode 没释放,空间就一直占着。# 找出所有被删除但仍被占用的文件 lsof +L1 2>/dev/null | head -30 # 没有 lsof 时的替代做法 ls -l /proc/*/fd 2>/dev/null | grep deleted | head -30输出里会看到类似 nginx 1234 ... 5u REG ... 500000000 /var/log/nginx/access.log (deleted)——一个 500 MB 的已删除日志。处理方式只有两种:重启那个进程(释放句柄,空间立刻回来);或者不要重启,用 truncate 清空当前文件(如果进程还在往里写新文件的话)。注意:rm 一个正在被写的日志文件不会释放空间,只是把它变成「不可见的占用」。正确做法是 truncate -s 0 file.log 或让 logrotate 用 copytruncate。四、nginx 把响应缓冲到了磁盘nginx 的 error.log 里会出现这种警告:[warn] an upstream response is buffered to a temporary file /var/lib/nginx/tmp/fastcgi/1/98/0000002981 while reading upstream这不是错误,但它说明上游(PHP-FPM)返回的响应超过了内存缓冲区,nginx 把它落盘了。如果上游返回很大的响应(比如一个几百 KB 的 HTML、或者一个大的 JSON 接口),这些临时文件会持续产生。两个方向:# 1. 让缓冲区能装下正常的响应(按你的实际响应大小调) fastcgi_buffers 16 16k; fastcgi_buffer_size 32k; # 2. 给临时目录设上限,避免无限增长 fastcgi_max_temp_file_size 64m; fastcgi_temp_path /var/lib/nginx/tmp/fastcgi 1 2;另外要确认临时目录会被清理。nginx 正常情况下会自己删,但如果进程被强杀过,残留文件会留在那里:du -sh /var/lib/nginx/tmp/* 2>/dev/null find /var/lib/nginx/tmp -type f -mtime +1 2>/dev/null | head五、日志轮转:一次配好,长期省心# /etc/logrotate.d/myapp /var/log/myapp/*.log { daily rotate 7 maxsize 50M # 单个文件超过 50M 就轮转,不等第二天 missingok notifempty compress delaycompress copytruncate # 关键:不重启进程也能释放空间 }maxsize 和 copytruncate 这两行是关键:maxsize 让「一天涨 2 GB」这种突发情况当天就被截住;copytruncate 避免「轮转后进程还拿着旧句柄」的问题——也就是第三节那个坑。Alpine 上 logrotate 不在默认安装里,要单独装:apk add logrotate # 确认 cron 里有这一行,否则 logrotate 根本不会跑 grep logrotate /etc/crontabs/root /etc/periodic/daily/* 2>/dev/null最容易犯的错是配好了 logrotate 但没验证它真的执行过。 用 logrotate -d(dry-run)先看一遍:logrotate -d /etc/logrotate.d/myapp六、给根分区留出安全垫在小分区上,与其等它满了再救,不如主动留垫:把日志目录单独挂载(或软链到数据盘),别让日志吃掉根分区;给关键服务设置自己的上限:journald 的 SystemMaxUse、docker 的 log-opts max-size、nginx 的 temp 上限;加一条最简单的告警,超过 85% 就提醒:#!/bin/sh # /etc/periodic/daily/check-disk THRESHOLD=85 USED=$(df / | awk 'NR==2 {gsub("%","",$5); print $5}') if [ "$USED" -ge "$THRESHOLD" ]; then echo "root filesystem ${USED}% full on $(hostname)" | \ mail -s "disk warning" you@example.com fi在改动前后都记一次 df -h /。这样你能明确知道「这次操作多占了多少」——尤其是往服务器上传东西的时候。七、完整流程回顾df -h # 1. 确认哪个分区满 df -i # 2. 顺便看 inode du -x -h --max-depth=1 / # 3. 找出大目录 lsof +L1 # 4. 查「已删除但被占用」的文件 # 5. 处理:清缓存 / 轮转日志 / 重启持有句柄的进程 # 6. 配 maxsize + copytruncate,防止复发 # 7. 加一个 85% 的告警顺序很重要:先量,再改。跳过第 3、4 步直接去删东西,很容易删掉不该删的,而真正的元凶还在那儿。相关阅读Alpine 轻量服务器初始化清单:时钟、Nginx、PHP 一篇配齐阿里云 2 元/月服务器防天价账单:流量监控脚本实操在 Alpine Linux 上从零搭一个 Typecho:nginx + php-fpm + SQLite 的完整清单
2026年10月05日
86 阅读
0 评论
0 点赞
2026-10-04
在 Alpine Linux 上从零搭一个 Typecho:nginx + php-fpm + SQLite 的完整清单
这篇是服务器搭建笔记,和 DNS 无关。站内的加密 DNS 服务在dns.23-4.cn。Alpine 做 Web 服务器有几个很实际的好处:镜像小、内存占用低、包管理干净。但它的目录结构、服务管理和多数人熟悉的 Debian 系不一样,第一次搭容易在几个固定位置上卡住。这篇是把一个 Typecho 博客在 Alpine 上跑起来的完整清单,按顺序执行即可,重点标出容易踩的坑。一、装包apk update apk add nginx \ php84 php84-fpm php84-session php84-json \ php84-pdo_sqlite php84-mbstring php84-openssl \ php84-curl php84-dom php84-fileinfo php84-iconv \ sqlite注意几点:Alpine 的 PHP 包按大版本命名(php84),不是 php。装之前先 apk search php84 确认版本号,Alpine 升级后包名会变;php84-pdo_sqlite 必须显式安装。Typecho 用 SQLite 时依赖它,缺了会在安装页报「无法连接数据库」,而错误信息不会告诉你缺的是哪个扩展;php84-session 也是必需的,Typecho 的登录和 CSRF 依赖它。二、服务管理是 OpenRC,不是 systemd这是 Debian 用户最容易懵的地方:rc-update add nginx default # 开机自启 rc-update add php-fpm84 default rc-service nginx start rc-service php-fpm84 start rc-service nginx status没有 systemctl,对应的是 rc-service / rc-update;日志不在 journalctl 里,而是各自的日志文件(nginx 在 /var/log/nginx/,PHP 在 /var/log/php84/error.log);服务的 init 脚本在 /etc/init.d/。三、PHP-FPM 监听确认 /etc/php84/php-fpm.d/www.conf 里的监听方式:listen = 127.0.0.1:9000 ; 或者用 unix socket,性能略好 ; listen = /run/php-fpm84/php-fpm.sock用 TCP 更省事,不用处理 socket 的属主和权限。用 socket 的话记得让 nginx 用户能读写。四、nginx 站点配置server { listen 80; server_name example.com; root /var/www/typecho; index index.php index.html; client_max_body_size 20m; location / { try_files $uri $uri/ /index.php$uri?$query_string; } location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { expires 7d; add_header Cache-Control "public"; try_files $uri =404; } location ~ .*\.php(\/.*)*$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; include fastcgi.conf; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_read_timeout 60s; } }这里有三个必须理解的点1. try_files ... /index.php$uri?$query_string —— 这是伪静态的关键。Typecho 的「重写」打开后,链接是 /archives/1/、/category/xxx/ 这种没有 .php 的形式。这些路径在磁盘上不存在,必须交给 index.php 处理。写成 /index.php?$query_string 会丢掉路径信息,结果是所有伪静态链接都变成首页或 404。2. fastcgi_split_path_info + PATH_INFO —— 少了它,Typecho 认不出路由。请求 /index.php/archives/1/ 时,PHP 需要知道 .php 后面那一段是「路径信息」。没有这两行,Typecho 拿不到 /archives/1/,就会走 404 分支。这个坑的症状是:首页正常,但所有文章页、分类页、标签页全部 404。3. location ~* \.(js|css|...)$ 这个正则块要放在 location / 后面、php 块前面。nginx 的匹配规则是「正则优先于前缀,正则之间按书写顺序」。顺序写错会让静态资源的 try_files 覆盖掉别的规则,或者反过来让 .php 请求走了静态分支——后者会把 PHP 源码当文本吐出来,非常危险。配完一定要测:# 如果这条命令返回了 PHP 源码,说明 php 块没匹配上 curl -s http://example.com/index.php | head -3五、装 Typecho 与 config.inc.php下载解压到 /var/www/typecho,然后走网页安装。SQLite 的配置在 config.inc.php 里长这样:$db = new \Typecho\Db('Pdo_SQLite', 'typecho_'); $db->addServer(array( 'file' => '/var/lib/typecho/typecho.db', ), \Typecho\Db::READ | \Typecho\Db::WRITE); \Typecho\Db::set($db);把数据库文件放在 Web 根目录之外(上面放在 /var/lib/typecho/)。放在根目录里的后果是:如果哪天 nginx 配置被人改错、或者多了一个不严谨的 location,.db 文件可能被直接下载——里面是所有用户的密码哈希。六、权限# Web 根目录给 web 用户,不要给 root chown -R nobody:nobody /var/www/typecho # 数据库目录只给 web 用户 chown -R nobody:nobody /var/lib/typecho chmod 750 /var/lib/typecho chmod 640 /var/lib/typecho/typecho.dbAlpine 上 nginx 的 worker 默认跑在 nginx 用户、PHP-FPM 的 pool 默认是 nobody。要让写文件的进程(PHP-FPM)对目录有写权限,否则上传附件、写 session、更新配置都会失败。查当前是谁在跑:ps aux | grep -E "nginx|php-fpm" | grep -v grep七、上 HTTPSapk add certbot certbot certonly --webroot -w /var/www/acme -d example.com -d www.example.com在 nginx 里加一个 location /.well-known/acme-challenge/ { root /var/www/acme; },然后 80 端口 301 到 443。续期一定要验证能跑通,别等三个月后证书过期才发现:certbot renew --dry-run八、上线前检查清单# 1. 伪静态是否生效(文章页应该 200,不是 404) curl -s -o /dev/null -w "%{http_code}\n" https://example.com/archives/1/ # 2. 分类 / 标签页 curl -s -o /dev/null -w "%{http_code}\n" https://example.com/category/default/ # 3. feed curl -s -o /dev/null -w "%{http_code}\n" https://example.com/feed/ # 4. PHP 没有被当源码吐出 curl -s https://example.com/index.php | head -1 # 5. 数据库文件不可下载(应该是 403 或 404) curl -s -o /dev/null -w "%{http_code}\n" https://example.com/typecho.db # 6. 根分区还剩多少 df -h /第 5 条尤其要做。小服务器上数据库就几十到几百 KB,很容易被随手放在根目录里。九、最容易踩的五个坑症状原因首页正常,其他页面全 404缺 PATH_INFO / fastcgi_split_path_info所有伪静态链接跳到首页try_files 写成了 /index.php?$query_string,丢了 $uri装 Typecho 时报连不上数据库没装 php84-pdo_sqlite上传附件 / 登录失败PHP-FPM 的运行用户对目录没有写权限浏览器直接下载到了 .db 文件数据库放在 Web 根目录里,且没有 deny 规则相关阅读Alpine 轻量服务器初始化清单:时钟、Nginx、PHP 一篇配齐服务器磁盘满了怎么查:df / du / inode / 日志轮转的可执行排查顺序AdGuard Home 迁移实录:从香港服务器搬到国内阿里云
2026年10月04日
17 阅读
0 评论
0 点赞
1
2