1. 这不是“重启大法”,而是故障树下的精准定位

你有没有过这样的经历:凌晨两点,手机突然弹出告警——公司官网打不开。你抓起电脑连上服务器, systemctl status nginx 一看,服务状态是 active (running),但 curl 测试返回 502;再查 Apache,日志里满屏 Permission denied ; df -h 显示磁盘 98% 满,可 du -sh /var/log/* 加起来才 2GB;最后发现是 /var 下某个被忽略的子目录(比如 /var/lib/php/sessions )被日志轮转脚本漏掉了清理规则,单个 session 文件因 PHP 配置错误膨胀到 1.2GB,直接撑爆了 /var 分区——而这个分区恰好是 Apache 的 DocumentRoot 所在挂载点。整个过程耗时 47 分钟,真正修复只用了 12 秒。

这就是 Linux Web 服务器故障排查最真实的日常: 表面症状千奇百怪,根因却往往藏在系统、服务、应用三层耦合的缝隙里 。它不是靠“先重启 Nginx,不行再重启 PHP-FPM,最后 reboot”这种暴力三连就能解决的。真正的排查,是一场基于证据链的逆向工程——你得像刑侦人员一样,从一个 HTTP 500 错误码出发,一层层剥开网络栈、进程状态、文件权限、资源配额、配置语法的外壳,最终锁定那个唯一能解释所有现象的变量。

我干这行十一年,维护过从 5 台 VPS 构成的博客集群,到 300+ 节点的 SaaS 平台后端,踩过的坑足够填满三本《Linux 故障字典》。我发现一个铁律: 90% 的“疑难杂症”,其实都属于“常见问题”的变体 ——只是它们披上了不同的报错外衣,混在了不同的日志位置,或者触发于特定的资源临界点。Apache 的 AH00072: make_sock: could not bind to address [::]:80 看似是端口冲突,实则可能是 systemd-socket 激活的旧实例在后台静默占着端口;Nginx 的 504 Gateway Timeout 常被归咎于 upstream 响应慢,但更大概率是 proxy_read_timeout 和 fastcgi_read_timeout 的单位被误写成毫秒而非秒; Connection refused 不一定代表服务没启,也可能是 SELinux 的 httpd_can_network_connect 被设为 off,或者防火墙的 nftables 规则链顺序错了。

所以这篇内容不叫“Linux 服务器排错指南”,它叫 “How To Troubleshoot Common Site Issues on a Linux Server” ——标题里那个 “How To” 是核心。它不罗列命令,而是教你构建一套可复用的思维框架:当网站打不开、页面空白、上传失败、响应超时、数据库连不上时,你应该 按什么逻辑顺序去问问题、去哪里找证据、如何交叉验证、怎样避免被表象误导 。接下来的每一节,都会围绕一个高频故障场景展开,从现象切入,展示完整的排查链条,最后落回到一个可立即执行的验证动作。你不需要记住所有命令,但必须理解每一步背后的“为什么”。


2. HTTP 请求卡在哪儿?用四层诊断法穿透网络与服务

网站打不开,第一反应往往是“Nginx 挂了?”——这是最危险的直觉。真正的排查,必须从 OSI 模型的底层开始,逐层向上验证,因为任何一层的断裂都会导致上层“不可用”。我把它总结为 “四层诊断法” :网络层 → 传输层 → 应用层 → 内容层。跳过任何一层,都可能让你在错误的方向上浪费数小时。

2.1 网络层:确认物理/逻辑连通性,排除路由与防火墙干扰

这不是 ping 一下就完事。 ping example.com 成功,只说明 DNS 解析和 ICMP 路由通,但 HTTP 流量走的是 TCP 80/443 端口,它可能被完全阻断。

第一步:用 telnet 或 nc 直接测试目标端口

# 测试 80 端口(HTTP)
nc -zv example.com 80
# 测试 443 端口(HTTPS)
nc -zv example.com 443

nc 的 -z 参数表示只扫描端口不发送数据, -v 输出详细信息。如果返回 Connection refused ,说明目标主机上没有服务监听该端口,或防火墙明确拒绝了连接。如果返回 timeout ,则是中间某处(云服务商安全组、本地 iptables/nftables、ISP 过滤)丢弃了包。

提示:很多新手会忽略云服务器的“安全组”配置。阿里云、腾讯云、AWS 的控制台里,安全组默认只放行 22 端口。即使你在服务器上 systemctl start nginx 成功,外部请求依然会被云平台的虚拟防火墙拦截。务必检查安全组规则是否包含 80/443 的入方向(Inbound)TCP 规则。

第二步:在服务器本地验证端口监听状态 登录服务器后,别急着看 Nginx 日志,先确认服务是否真在监听:

# 查看所有监听的 TCP 端口及对应进程
ss -tlnp | grep -E ':80|:443'
# 或使用 netstat(部分新系统已弃用)
netstat -tlnp | grep -E ':80|:443'

关键看输出中的 State 列(应为 LISTEN )和 PID/Program name 列。如果 ss 返回空,说明 Nginx/Apache 根本没启动,或者配置错误导致启动失败(此时 systemctl status nginx 会显示 failed )。如果看到 LISTEN ,但 nc -zv localhost 80 仍失败,则极可能是 SELinux 或 AppArmor 在作祟——它们会在内核层面拦截 socket 绑定。

注意: ss -tlnp 需要 root 权限才能看到 PID/Program name 。普通用户执行会显示 ??? 。如果你用 sudo ss -tlnp 看到 Nginx 在监听 *:80 ,但 curl http://localhost 返回 Connection refused ,那基本可以锁定是 SELinux 的 http_port_t 上下文被修改,或 sebool httpd_can_network_connect 被关闭。临时验证: sudo setsebool -P httpd_can_network_connect on ,然后重试。

2.2 传输层:确认 TCP 连接建立成功,揪出 SYN Flood 或连接池耗尽

nc -zv 成功,只代表三次握手能完成,不代表 HTTP 请求能被处理。这里有个经典陷阱: 连接能建,但请求发不出去,或响应收不回来 。

第三步:用 tcpdump 抓包,看真实流量走向

# 在服务器上抓取 80 端口的进出包(需 root)
sudo tcpdump -i any port 80 -w /tmp/http.pcap -c 50
# 然后在本地用浏览器访问,再停止抓包
# 用 Wireshark 分析 /tmp/http.pcap,或用命令行快速查看
sudo tcpdump -r /tmp/http.pcap -A | grep -i "GET /"

如果抓包里能看到 SYN 包进来,但没有 SYN-ACK 回去,说明服务进程没响应,可能已僵死或被 OOM Killer 杀掉。如果能看到 SYN-ACK ,但后续 ACK 后没有 HTTP GET ,说明客户端(浏览器)没发请求,问题在客户端或中间代理。如果能看到 GET 请求进来,但没有 HTTP/1.1 200 OK 响应,那问题就锁定了:应用层服务(Nginx/Apache/PHP)收到了请求,但没能生成响应。

实操心得:我曾遇到一个案例, ss -tlnp 显示 Nginx 在监听, nc -zv localhost 80 成功,但 curl 卡住。 tcpdump 显示请求进来了,响应却没出去。最后发现是 Nginx 配置里 sendfile on; 开启了,但后端 PHP-FPM 返回的 Content-Length 头与实际 body 长度不一致,导致内核 sendfile() 系统调用卡死。关掉 sendfile 立刻恢复。这说明,传输层的“通”,不等于应用层的“稳”。

2.3 应用层:验证 Web 服务器进程健康,区分“活着”与“可用”

systemctl status nginx 显示 active (running) ,不等于它能处理请求。进程可能处于僵尸态、CPU 占用 100% 无响应、或被 cgroup 限制了内存无法 fork 子进程。

第四步:用 ps 和 top 看进程真实状态

# 查看 Nginx 主进程和工作进程
ps aux | grep nginx
# 关键看 STAT 列:R=运行中,S=睡眠,Z=僵尸,< =高优先级,N=低优先级
# 如果看到大量 nginx: worker process 状态为 R,且 CPU% 持续 99%,说明它在忙死循环
# 如果看到 nginx: master process 状态为 S,但 worker 全是 Z,说明 master 没法回收僵尸 worker

# 实时监控资源占用
top -p $(pgrep -f "nginx: master") -p $(pgrep -f "nginx: worker")
# 按 Shift+M 按内存排序,Shift+P 按 CPU 排序

第五步:用 strace 追踪进程系统调用(终极手段) 当 top 显示进程在跑,但 curl 就是没响应, strace 是你的显微镜:

# 追踪一个 worker 进程的系统调用(-p 后跟 PID)
sudo strace -p $(pgrep -f "nginx: worker" | head -1) -e trace=network,io -s 100 -T
# -e trace=network,io 只跟踪网络和 I/O 相关调用
# -s 100 显示字符串长度上限,避免截断
# -T 显示每次系统调用耗时

你会看到类似:

recvfrom(12, "\21\0\0\0\3SELECT VERSION()", 4096, 0, NULL, NULL) = 23 <0.000021>
sendto(12, "HTTP/1.1 200 OK\r\n...", 128, 0, NULL, 0) = 128 <0.000015>

如果 recvfrom 后长时间没 sendto ,说明它卡在了数据库查询或文件读取上。如果 recvfrom 本身就卡住,说明它在等客户端发数据,但客户端网络有问题。

踩坑实录:某次线上事故, strace 显示 Nginx worker 卡在 open("/var/www/html/index.php", O_RDONLY) 上长达 30 秒。 ls -l /var/www/html/ 正常, cat /var/www/html/index.php 也秒开。最后发现是 /var/www 挂载的 NFS 服务器负载过高, open() 系统调用在内核 NFS 客户端里阻塞了。 strace 让我们 5 分钟内就定位到了 NFS 问题,而不是在 Nginx 配置里翻一整天。

2.4 内容层:确认响应内容正确,排除 PHP/Python/Node.js 层面的崩溃

Nginx 返回了 200 OK ,但浏览器显示空白页或 500 Internal Server Error ,问题就下沉到了应用代码层。这时,Web 服务器只是个“信使”,真正的“信”(HTML/JSON)没生成。

第六步:绕过 Web 服务器,直接调用应用解释器 对于 PHP:

# 直接用 CLI 模式运行 index.php,看是否报错
php -f /var/www/html/index.php
# 如果报错:Parse error: syntax error in /var/www/html/index.php on line 42
# 那就是代码语法错误,Nginx 日志里只会记一句 "PHP Parse error",根本找不到行号

对于 Python Flask/Django:

# 进入项目目录,用 Python 直接运行
cd /var/www/myapp
python app.py  # 或 python manage.py runserver 0.0.0.0:5000 --noreload
# 然后 curl http://localhost:5000,看是否正常

如果 CLI 模式能跑,但通过 Nginx 就不行,那一定是 Web 服务器与应用的通信环节出了问题:PHP-FPM socket 权限不对、uWSGI 配置的 socket 路径不匹配、或环境变量缺失(如 PYTHONPATH )。

关键经验:永远先用 curl -I (只获取 Header)和 curl -v (显示完整请求/响应过程)来区分问题层级。 curl -I http://localhost 返回 HTTP/1.1 500 Internal Server Error ,说明 Nginx 收到了上游错误;如果 curl -v 显示 * Connection #0 to host localhost left intact 但没 HTTP/1.1 行,说明连接被重置,是传输层问题;如果 curl -v 显示 HTTP/1.1 200 OK 但 curl http://localhost 输出为空,那 99% 是应用代码里 echo 或 print 没执行,或者输出缓冲区没 flush。


3. 日志不是“垃圾桶”,而是按时间戳排列的故障时间线

新手常犯的错误是:一出问题就 tail -f /var/log/nginx/error.log ,看到一堆 upstream timed out 就以为是后端慢,然后去优化数据库。结果修了一周,问题依旧。因为 error.log 只记录“发生了什么”,不记录“为什么发生”。真正的线索,往往散落在 系统日志、服务日志、应用日志、内核日志 四个地方,它们共同构成一条按纳秒精度排列的故障时间线。你得学会把它们“对齐”。

3.1 精确对齐日志时间戳:用 journalctl 作为中央时间轴

现代 Linux(systemd 系统)的日志统一由 journald 管理,它的时间戳精度是微秒级,且能关联所有服务。这是你排查的“主时间轴”。

第七步:用 journalctl 按时间范围拉取全量日志

# 查看最近 10 分钟所有服务的日志(含内核、systemd、nginx、php-fpm)
sudo journalctl --since "10 minutes ago" --all --no-pager | less
# 按服务过滤,看 nginx 和 php-fpm 在同一时刻的行为
sudo journalctl -u nginx -u php-fpm --since "2024-05-20 14:30:00" --no-pager

journalctl 的优势在于:它能把不同来源的日志按精确时间排序。比如你看到:

May 20 14:32:15 web01 nginx[1234]: *12345 connect() failed (111: Connection refused) while connecting to upstream
May 20 14:32:15 web01 php-fpm[5678]: ERROR: unable to bind listening socket for address '/run/php/php8.1-fpm.sock': No such file or directory
May 20 14:32:15 web01 systemd[1]: php8.1-fpm.service: Failed with result 'exit-code'.

这三行日志时间戳完全一致,立刻就能推断:Nginx 报错是因为 PHP-FPM 进程根本没起来,而 PHP-FPM 启动失败是因为它的 socket 文件路径 /run/php/php8.1-fpm.sock 对应的目录 /run/php/ 不存在( No such file or directory )。 journalctl 把因果关系用时间戳钉死了。

提示: journalctl 默认只保存最近 1-2 周日志。生产环境务必配置持久化存储:

# 编辑 /etc/systemd/journald.conf
Storage=persistent
SystemMaxUse=1G
# 然后重启 journald
sudo systemctl restart systemd-journald

3.2 Nginx 日志:读懂 access.log 里的“行为指纹”

access.log 不是流量统计工具,它是用户行为的原始录像带。每一行都包含 IP、时间、请求方法、URL、状态码、响应大小、Referer、User-Agent。 状态码是最高频的线索,但绝不能孤立看待 。

第八步:用 awk + sort 快速分析 access.log 异常模式

# 统计最近 1 小时 5xx 错误最多的 URL
sudo awk '$9 ~ /^5[0-9][0-9]$/ && $4 ~ /\[.*2024.*:..:..:../ {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -10

# 查看某个 IP 的全部请求(排查爬虫或攻击)
sudo awk '$1=="192.168.1.100" {print}' /var/log/nginx/access.log | tail -20

# 找出响应时间 > 5 秒的请求($NF 是最后一个字段,即 $request_time)
sudo awk '$NF > 5 {print $0}' /var/log/nginx/access.log | tail -10

关键洞察:如果 502 Bad Gateway 都集中在 /api/v1/users 这个 URL,而其他接口正常,那问题肯定在 users 服务,而不是 Nginx 本身。如果 413 Request Entity Too Large 突然暴增,那一定是前端上传组件改了,或者 Nginx 的 client_max_body_size 被意外改小了。

实战技巧:Nginx 的 log_format 可以自定义,强烈建议加入 $request_time (处理时间)、 $upstream_response_time (上游响应时间)、 $upstream_addr (上游地址)。这样一行日志就能告诉你:是 Nginx 自己慢,还是上游慢,还是网络慢。配置示例:

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                 '$status $body_bytes_sent "$http_referer" '
                 '"$http_user_agent" "$http_x_forwarded_for" '
                 '$request_time $upstream_response_time $upstream_addr';

3.3 Apache 日志:ErrorLog 里的“编译器错误”

Apache 的 error_log 比 Nginx 更“啰嗦”,但它会告诉你模块加载失败、语法错误、权限拒绝的每一个细节,像一个严格的 C 编译器。

第九步:用 grep 锁定 error_log 中的致命错误

# 查找最近的 FATAL、CRITICAL、CORE 错误(Apache 7.x+)
sudo grep -i "fatal\|critical\|core" /var/log/apache2/error.log | tail -10

# 查找模块加载失败(常见于 mod_php、mod_ssl)
sudo grep -i "module not found\|cannot load\|syntax error" /var/log/apache2/error.log | tail -10

# 查找权限相关错误(最常见!)
sudo grep -i "permission denied\|cannot access\|denied" /var/log/apache2/error.log | tail -10

一个典型例子: [core:error] [pid 1234] (13)Permission denied: AH00035: access to / denied (filesystem path '/var/www/html') because search permissions are missing on a component of the path 。这行日志明确指出: /var/www/html 目录的父目录(比如 /var/www )缺少 x (执行)权限,导致 Apache 进程无法 chdir 进入。 ls -ld /var/www 会显示 drw-r--r-- ,缺了 x 。修复: sudo chmod 755 /var/www 。

注意:Apache 的 DocumentRoot 权限检查是递归的。 /var/www/html 有 755 不够, /var 、 /var/www 都必须有 x 权限。 chmod 755 是安全底线, 775 或 777 是安全隐患。

3.4 内核日志:dmesg 里的“最后一声叹息”

当服务莫名退出、进程被杀、磁盘 IO 暂停, dmesg 往往是唯一的目击者。它记录内核事件,包括 OOM Killer 的“处决令”。

第十步:用 dmesg -T 查看带人类可读时间的内核日志

# 查看最近的 OOM 事件(内存不足被杀)
sudo dmesg -T | grep -i "killed process"

# 查看磁盘错误(SMART 告警、IO timeout)
sudo dmesg -T | grep -i "error\|fail\|timeout\|nvme\|sd"

# 查看 SELinux 拒绝记录(如果启用了 enforcing 模式)
sudo dmesg -T | grep -i "avc:.*denied"

OOM Killer 的日志非常有特征:

[Mon May 20 14:32:15 2024] Out of memory: Kill process 12345 (php-fpm) score 892 or sacrifice child
[Mon May 20 14:32:15 2024] Killed process 12345 (php-fpm) total-vm:2048000kB, anon-rss:1536000kB, file-rss:0kB, shmem-rss:0kB

它清楚地告诉你:哪个进程( php-fpm )被杀了,为什么( Out of memory ),以及它当时占了多少内存( anon-rss:1536000kB ≈ 1.5GB)。这比 free -h 的快照更有价值,因为它记录了“死亡瞬间”的状态。

关键提醒: dmesg 日志是环形缓冲区,重启后会清空。所以一旦发现服务异常,第一时间 dmesg -T > /tmp/dmesg.log 保存现场。生产环境建议配置 rsyslog 将 kern.* 日志转发到远程日志服务器。


4. 资源瓶颈:磁盘、内存、inode,三个沉默的杀手

“网站打不开”背后,有 30% 的概率不是代码或配置问题,而是服务器资源耗尽。它们不会报错,只会让一切变慢、超时、拒绝服务。磁盘空间、内存、inode 数量,这三个指标就像服务器的“血压、血糖、血氧”,必须持续监控。

4.1 磁盘空间: df 和 du 的战争,谁在说谎?

df -h 显示 /var 使用率 99%,但 du -sh /var/* 加起来才 10GB。这说明有“隐藏”的大文件或已删除但未释放的文件。

第十一步:找出“失踪”的磁盘空间

# 第一步:确认哪个挂载点满了
df -h | grep -E "(9[0-9]|100)%"

# 第二步:进入该目录,用 du 找大目录
cd /var
sudo du -sh * | sort -hr | head -10

# 第三步:如果 du 总和远小于 df,说明有被删除但进程还在占用的文件
# 查看所有被删除但仍被打开的文件(lsof -nP 会显示)
sudo lsof +L1 | grep "/var"  # +L1 表示链接数为 0(即已删除)
# 或更直接:查找大文件
sudo lsof +L1 | awk '{if($7>1000000) print $0}' | sort -k7 -nr

典型场景: /var/log/nginx/access.log 被 logrotate 重命名并压缩,但 Nginx 进程还拿着旧的文件描述符在写。 df 认为它还在, du 却找不到它(因为文件名变了)。 lsof 会显示:

nginx   1234  root    1w   REG   253,1 1234567890 123456 /var/log/nginx/access.log (deleted)

修复: sudo kill -USR1 $(pgrep nginx) 发送 USR1 信号,让 Nginx 重新打开日志文件,释放旧句柄。

实操心得:我见过最离谱的案例是 /var/lib/php/sessions 目录下,一个 PHP session 文件因 session_write_close() 没被调用,被锁住并不断追加数据,单个文件达 2.1GB。 du 扫描时跳过了被锁文件, df 却如实计算。 lsof 是唯一能揪出它的工具。

4.2 内存:不只是 free -h ,还有 slabtop 和 smaps

free -h 显示 available 有 2GB,但 top 里 PHP 进程 RSS 占用 1.8GB,系统就开始 swap。这是因为 available 是内核估算的“可立即分配”内存,而 RSS (Resident Set Size)是进程实际占用的物理内存。当 RSS 总和接近总内存,内核就会开始回收 page cache ,甚至 swap 。

第十二步:深入内存使用,定位“内存吞噬者”

# 查看各进程 RSS 占用(按内存排序)
ps aux --sort=-%mem | head -10

# 查看 slab 分配器使用(内核对象缓存,常被忽视)
sudo slabtop -o | head -20

# 查看某个进程的详细内存映射(如 PID 1234)
sudo cat /proc/1234/smaps | awk '/^Size:/ {sum += $2} END {print sum " kB"}'

slabtop 能揭示内核级内存泄漏。比如 dentry (目录项缓存)或 inode_cache 占用飙升,说明有程序在疯狂创建/删除文件,或 NFS 客户端有 bug。 smaps 则能告诉你进程的 Size (虚拟内存)、 RSS (物理内存)、 PSS (比例集大小,更公平的共享内存计量)。

关键参数: /proc/sys/vm/swappiness 控制内核使用 swap 的倾向。默认 60,意味着内存还剩 40% 就开始 swap。对于 Web 服务器,建议设为 10 或 1 :

echo 'vm.swappiness=1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

4.3 inode: df -i 被遗忘的维度

df -h 显示磁盘空间充足,但 touch test.txt 报错 No space left on device 。这时 df -i 会告诉你真相:inode 已用完(100%)。

第十三步:诊断和清理 inode

# 查看 inode 使用率
df -i

# 找出哪个目录 inode 最多(通常是日志、缓存、邮件队列)
sudo find /var -xdev -type f | cut -d "/" -f 2 | sort | uniq -c | sort -nr

# 清理 /var/spool/postfix/maildrop(Postfix 邮件队列积压)
sudo find /var/spool/postfix/maildrop -type f -mtime +7 -delete

# 清理 /var/log/journal(journald 日志)
sudo journalctl --vacuum-time=2weeks

inode 耗尽的典型场景: /var/log/journal 目录下有数百万个 .journal~ 临时文件; /var/spool/postfix/maildrop 里堆积了数万封待发邮件; /tmp 下有大量 PHP session 或 Composer 缓存文件。

提示:创建文件系统时, mkfs.ext4 的 -i 参数指定每多少字节分配一个 inode。默认是 -i 16384 (每 16KB 一个 inode)。如果预计要存海量小文件(如图片缩略图、session),应加大 inode 密度: mkfs.ext4 -i 4096 /dev/sdb1 (每 4KB 一个 inode)。


5. 配置陷阱:语法、路径、权限,三个看似简单却致命的雷区

90% 的 Web 服务器启动失败,源于配置文件里一个标点符号、一个空格、一个路径错误。它们不会在 systemctl start 时报错,而是让服务静默失败,或在第一次请求时崩溃。

5.1 语法校验: nginx -t 和 apachectl configtest 是你的编译器

永远不要跳过这一步。 nginx -t 不仅检查语法,还验证所有 include 的文件是否存在、所有 upstream 定义是否合法。

第十四步:执行配置校验,并解读错误信息

# Nginx
sudo nginx -t
# 如果报错:nginx: [emerg] invalid number of arguments in "proxy_pass" directive in /etc/nginx/sites-enabled/example.com:12
# 说明第 12 行 proxy_pass 写成了 proxy_pass http://backend/; (多了分号)

# Apache
sudo apachectl configtest
# 如果报错:Syntax error on line 42 of /etc/apache2/sites-enabled/example.com.conf:
# Invalid command 'SSLEngine', perhaps misspelled or defined by a module not included in the server configuration
# 说明 mod_ssl 没启用:sudo a2enmod ssl

nginx -t 的 -c 参数可以指定配置文件路径,用于测试新配置:

sudo nginx -t -c /tmp/new-nginx.conf

5.2 路径解析: root vs alias , DocumentRoot 的绝对与相对

Nginx 的 root 和 alias 是新手最大误区。 root 是拼接路径, alias 是替换路径。

第十五步:用 curl -v 验证路径映射是否正确 假设 Nginx 配置:

location /static/ {
    alias /var/www/static/;
}
location /assets/ {
    root /var/www;
}

那么:

  • curl http://example.com/static/logo.png → 实际读取 /var/www/static/logo.png
  • curl http://example.com/assets/logo.png → 实际读取 /var/www/assets/logo.png

如果 alias 末尾少了 / , /static/ 会映射到 /var/www/static ,导致 logo.png 被读取为 /var/www/staticlogo.png (少了个 / )。

Apache 的 DocumentRoot 必须是绝对路径,且 Apache 进程用户(通常是 www-data 或 apache )必须对该路径有 x 权限(如前所述)。

实操验证:在配置文件里加一行 return 200 "root: $document_root, request_uri: $request_uri"; ,然后 curl 看输出,就能直观看到 Nginx 如何解析路径。

5.3 权限模型:用户、组、umask,三位一体的枷锁

Web 服务器进程以什么用户身份运行,决定了它能访问哪些文件。 ps aux | grep nginx 显示 www-data ,那 www-data 用户就必须对 DocumentRoot 有 r-x (读和执行)权限,对 logs 目录有 w (写)权限。

第十六步:用 namei 诊断路径权限

# 诊断 /var/www/html/index.php 的每一级权限
sudo namei -l /var/www/html/index.php
# 输出:
# f: /var/www/html/index.php
# drwxr-xr-x root  root  /
# drwxr-xr-x root  root  var
# drwxr-xr-x root  root  www
# drwxr-xr-x www-data www-data html
# -rw-r--r-- www-data www-data index.php
# 如果某一级(如 /var/www)显示 drw-r--r--,缺少 x,namei 会标红提示。

namei 是权限诊断的终极武器,它会逐级检查 r (读)、 x (执行,对目录是进入权限)、 w (写)是否满足。

关键原则:文件权限 644 (所有者 rw,组 r,其他 r),目录权限 755 (所有者 rwx,组 rx,其他 rx)。 777 是安全灾难。 umask (默认 0022)决定了新创建文件的默认权限。确保 nginx 进程的 umask 是 0022 :在 /lib/systemd/system/nginx.service 的 [Service] 段添加 UMask=0022 。


6. 我的故障排查清单:一份可打印、可粘贴、可执行的行动纲领

写了这么多,你可能觉得步骤太多

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐