网站访问速度慢是什么原因?从服务器、数据库到前端全面排查

"网站慢"是一个模糊的表述,它可能意味着十几种完全不同的问题:用户点了链接三秒才出白屏,也可能页面很快出来了但图片一张张慢慢刷出来,还可能是只有部分地区用户慢、或者只有高峰期慢。这些情况对应的原因和优化手段完全不同,如果只是凭感觉去"优化",很容易把力气花在不相干的地方。

这篇文章给一套分层排查的方法:先用工具把慢的时间量化和分解,定位到具体是哪一层慢,再针对那一层找瓶颈。每一层都给出可执行的诊断命令和典型的修复思路,最后给出优化投入的顺序建议。

先别猜,先量化

排查的第一原则是:把"慢"变成一个数字,并把这个数字拆成几段。

一个 HTTP 请求从发起到完成,时间可以拆成 DNS 解析、TCP 连接、TLS 握手、服务端处理、内容传输这几段。搞清楚哪一段长,问题就缩小到一个具体方向。

用 curl 做时间分解

curl 的 -w 参数可以输出各阶段耗时:

curl -o /dev/null -sS -w 'DNS解析:   %{time_namelookup}s\n\
TCP连接:   %{time_connect}s\n\
TLS握手:   %{time_appconnect}s\n\
开始传输:  %{time_starttransfer}s\n\
总耗时:    %{time_total}s\n\
下载速度:  %{speed_download} B/s\n' https://www.example.com/

各字段的读法:

指标 含义 慢说明什么
time_namelookup DNS 解析耗时 DNS 服务器慢或域名链路配置有问题
time_connect TCP 连接建立完成(含 namelookup) 网络时延高或丢包重传
time_appconnect TLS 握手完成 TLS 握手慢、证书链校验慢、HTTP/2 未启用
time_starttransfer 收到第一个字节(约等于 TTFB) 服务端处理慢,重点查应用与数据库
time_total 全部完成 若远大于 starttransfer,说明内容传输慢,查资源体积与带宽

一个实用的判断方法:time_starttransfer 减 time_appconnect 大致就是服务端生成页面的时间。如果这一项超过几百毫秒,说明瓶颈在服务端(应用或数据库),而不是网络。

用浏览器 DevTools 看整体

curl 只看单个请求,页面整体表现要看浏览器的网络瀑布图。打开开发者工具的 Network 面板,刷新页面,重点看:

请求数量。 几十个小资源的请求在 HTTP/1.1 下会串行排队,启用 HTTP/2 能显著改善。

是否有请求在排队(Queueing / Stalled)时间长。 这通常说明浏览器对同一域名的并发连接数达到上限(HTTP/1.1 一般是六个左右)。

TTFB(Waiting for server response)。 服务端响应时间,与 curl 的 time_starttransfer 对应。

Content Download 占比。 如果下载时间远大于等待时间,说明资源体积过大。

DOMContentLoaded 与 Load 的时间差。 Load 明显更晚说明是图片、字体、第三方脚本拖慢了。

底部还有一行汇总:传输量、未压缩大小、请求数、完成时间。如果传输量与资源大小接近,说明没有启用压缩。

用 ping 和 mtr 看网络质量

ping -c 20 www.example.com

关注平均延迟和丢包率。丢包哪怕只有百分之几,也会因为 TCP 重传导致体验明显下降。

要定位是哪一段网络有问题,用 mtr(Debian、Ubuntu 上是 mtr-tiny 或 mtr 包,RHEL 系是 mtr 包):

sudo apt install -y mtr          # Debian、Ubuntu
sudo dnf install -y mtr          # Rocky、RHEL、CentOS
mtr -rwc 100 www.example.com

mtr 结合 traceroute 和 ping,能显示每一跳的延迟与丢包。哪一跳开始丢包或延迟突增,问题就在那段链路上。这对"只有部分地区用户慢"的问题特别有效。

分层排查框架

把可能的原因按层组织,逐层验证:

层级 典型症状 关键工具
DNS time_namelookup 大;首次访问慢后续快 dig、curl -w
网络 time_connect 大;有丢包;部分地区慢 ping、mtr、ss
服务器资源 所有站点一起慢;SSH 也卡 top、free、iostat、vmstat
Web 服务 TTFB 高但资源空闲;日志里 upstream 时间正常 Nginx 日志、access 日志分析
应用 $upstream_response_time 高 PHP 慢日志、应用性能分析
数据库 动态页面慢,静态页正常 慢查询日志、EXPLAIN、SHOW PROCESSLIST
前端 TTFB 正常但页面完整渲染慢 DevTools 瀑布图

DNS 层

常见原因:递归 DNS 服务器响应慢、域名配置了过多 NS 记录且部分不可用、DNSSEC 校验链路有问题、使用了境外 DNS 而解析绕远路。

诊断:

dig www.example.com A

输出里的 Query time 直接给出本次查询耗时,正常应在几十毫秒以内。如果持续超过两三百毫秒,换个公共 DNS 对比测试:

dig @1.1.1.1 www.example.com A
dig @223.5.5.5 www.example.com A
dig @8.8.8.8 www.example.com A

同时检查 NS 记录数量是否合理,以及是否有 NS 指向不可用的服务器:

dig example.com NS +short

优化:确保权威 DNS 服务商稳定;合理设置 TTL,太短会增加查询频率,太长影响变更生效;国内业务优先选择在国内有节点的 DNS 服务商。

网络层

常见原因:带宽被打满、跨境链路延迟高、丢包导致 TCP 重传、MTU 不匹配导致分片。

先看带宽是否被占满:

# 安装 sar / iftop 等工具查看实时流量
sudo apt install -y sysstat iftop      # Debian、Ubuntu
sudo dnf install -y sysstat iftop      # Rocky、RHEL、CentOS

sar -n DEV 1 5
sudo iftop -i eth0

如果出网带宽长期接近上限,先解决的应该是"为什么流量这么大",而不是急着加带宽。 常见的流量大户:未压缩的大图片、没有缓存头的静态资源、被频繁拉取的 API 响应、以及盗链。

MTU 问题比较少见但症状独特:小请求正常,大文件或 POST 卡住。可以用 ping 测试不分片大小:

ping -M do -s 1472 www.example.com     # Linux:禁止分片,测试 1500 字节 MTU

跨境访问的延迟是物理规律决定的,优化手段有限,主要靠把内容放到离用户更近的位置——这就是 CDN 的核心价值,而不是靠换机房解决。

服务器资源层

这一层出问题时的典型特征是:不只是网站慢,SSH 操作也卡。 这基本可以确定是整机资源问题。

CPU

top
htop     # 更直观,需安装

关注:us(用户态)高说明是应用程序消耗;sy(系统态)高可能是系统调用频繁或上下文切换过多;wa(iowait)高说明瓶颈在磁盘 IO,而不是 CPU 本身。

负载平均值(load average)要结合 CPU 核心数看。四核机器上 load 为 4 基本是满载,为 16 说明有严重排队。

内存与 swap

free -h
vmstat 1 5

重点关注 si 和 so 两列(换入换出),只要它们持续非零,说明系统正在使用 swap,性能会断崖式下降。磁盘比内存慢几个数量级,一旦开始换页,再怎么优化代码都无济于事。

常见的内存杀手:数据库缓冲池配置过大、pm.max_children 配得太多、Java 应用堆内存设置不合理。

磁盘 IO

iostat -x 1 5

关键列:

列 含义 判断参考
%util 设备繁忙程度 持续接近 100% 说明磁盘成为瓶颈
await IO 请求平均等待时间(毫秒) SSD 上超过 10 毫秒通常偏高,机械盘几十毫秒可接受
r/s、w/s 每秒读写次数 随机写多会明显拖慢机械盘和部分低端 SSD

如果 %util 长期打满,多半是数据库在做大量随机写或临时表落盘。可以考虑:把临时目录和数据目录放到更快的磁盘、优化查询减少临时表、或者干脆升级存储介质。

文件描述符与连接数

ulimit -n
cat /proc/sys/net/core/somaxconn
ss -s

高并发下如果文件描述符耗尽,会出现"偶发连接失败",日志里能看到 too many open files。

Web 服务层

资源都空闲但 TTFB 还是高,就要看 Web 服务本身的配置。

用 Nginx 日志定位

先定义一个包含耗时字段的日志格式:

log_format timed '$remote_addr [$time_local] "$request" '
                 '$status $body_bytes_sent '
                 'req=$request_time upstream=$upstream_response_time '
                 '$http_referer "$http_user_agent"';

access_log /var/log/nginx/access.log timed;

两个关键变量的区别:

变量 含义
$request_time 从收到请求到发送完响应,包含客户端接收慢导致的等待
$upstream_response_time 只统计后端(PHP-FPM 或代理)处理的时间

判断方法:

  • upstream 时间高 → 问题在后端应用或数据库
  • upstream 时间低但 request_time 高 → 后端很快,但传输慢或客户端慢。常见于慢速网络下的大文件传输、或者 Nginx 在等待客户端读完请求体
  • upstream 为 - → 请求没有转发给后端,可能是 Nginx 直接返回的静态文件或错误

找出最慢的请求:

awk '{ for(i=1;i<=NF;i++) if($i ~ /^req=/) print $i, $0 }' /var/log/nginx/access.log \
  | sort -rn | head -n 20

按 URL 聚合统计平均耗时也很有用,能快速找出最慢的几个接口。

压缩是否启用

curl -H 'Accept-Encoding: gzip,br' -I https://www.example.com/app.js

响应头里应有 content-encoding: gzip 或 br。Nginx 配置:

gzip on;
gzip_vary on;
gzip_comp_level 5;          # 1-9,越高压缩率越好但 CPU 消耗越大
gzip_min_length 1024;       # 太小的文件压缩收益不明显
gzip_types text/plain text/css application/javascript application/json application/xml image/svg+xml;

# Brotli 压缩率更高,但需要安装对应模块
# brotli on;
# brotli_comp_level 5;
# brotli_types text/css application/javascript application/json;

压缩对文本类资源(HTML、CSS、JS、JSON)效果极其显著,通常能减少百分之六十到八十的体积。这是投入产出比最高的优化之一,一行配置的事。

注意不要对已经是压缩格式的图片、视频、PDF 再做 gzip,既浪费 CPU 又几乎没效果。

静态资源缓存

没有缓存策略会导致用户每次访问都重新下载所有静态资源:

location ~* \.(jpg|jpeg|png|gif|webp|ico|css|js|woff2)$ {
    expires 30d;
    add_header Cache-Control "public";
    access_log off;
}

# 带内容哈希的构建产物可以设更长
location ^~ /assets/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    access_log off;
}

配合 etag 或 Last-Modified 做协商缓存,二次访问的验证请求也会很轻。

其他 Web 层配置

启用 HTTP/2(多路复用解决队头阻塞)、开启 keepalive(keepalive_timeout 一般 15 到 65 秒),以及确认 worker_processes auto 按 CPU 核心数设置。这些配置项单个收益有限但叠加起来明显。

应用层

PHP-FPM 是否成为瓶颈

# 查看进程池状态(需先在 pool 配置里开启 pm.status_path)
curl http://127.0.0.1/status

在 pool 配置里启用状态页:

pm.status_path = /status

关注几个字段:活跃进程数是否长期等于 pm.max_children(如果长期持平说明进程池太小,请求在排队)、以及 listen queue 是否有积压。

同时看慢日志,request_slowlog_timeout 设为 5 秒,超过这个时间的请求会记录调用栈:

slowlog = /var/log/php-fpm/www-slow.log
request_slowlog_timeout = 5s
tail -n 50 /var/log/php-fpm/www-slow.log

慢日志会直接告诉我们"哪一行函数卡住了",这是定位应用问题最快的工具。

常见应用瓶颈

OPcache 未启用。 PHP 每个请求都要重新解析编译源码,CPU 白白消耗一部分。检查一下:

php -i | grep -E 'opcache.enable|opcache.validate'

外部 HTTP 调用。 页面里同步调用第三方 API(支付查询、天气、短信、地图),第三方慢会直接拖慢你的页面。这类调用应该设短超时、加缓存、或者改为异步。这是"页面偶发很慢但服务器资源很闲"的常见原因。

未使用缓存层。 同样的复杂计算结果每次重算。引入 Redis 或 Memcached 做对象缓存,对动态站点效果显著。

数据库层

动态页面慢而静态页快,基本可以锁定数据库。

慢查询日志

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 'ON';

永久生效要写进配置文件:

[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1

风险提示:long_query_time 设得太小(如 0.1)在写入量大的系统上会产生大量日志,可能影响性能甚至占满磁盘。 建议从 1 秒起步,逐步收紧。

分析慢日志:

sudo apt install -y percona-toolkit         # Debian、Ubuntu
sudo dnf install -y percona-toolkit         # Rocky、RHEL、CentOS
pt-query-digest /var/log/mysql/slow.log | head -n 40

pt-query-digest 会把慢日志聚合成 Top N 查询,直接告诉我们应该优化哪几条 SQL。

即时查看当前在执行的语句

SHOW FULL PROCESSLIST;

看到某条语句 Time 很大且状态不是 Sleep,就是当前的阻塞源。如果大量连接卡在同一个状态(比如 Sending data 或 Locked),方向就很明确了。

锁等待排查:

SELECT * FROM information_schema.INNODB_TRX\G
SHOW ENGINE INNODB STATUS\G

常见数据库瓶颈

问题 症状 解决方向
缺索引 表数据量增长后突然变慢,rows examined 远大于返回行数 用 EXPLAIN 分析,补索引
索引失效 有索引但没走 检查是否在索引列上用了函数、隐式类型转换、或前导模糊查询
N+1 查询 列表页本该一次查询却执行了几十上百次 改用关联查询或批量加载
大偏移量分页 LIMIT 100000, 20 极慢 用游标式分页或覆盖索引优化
临时表落盘 Created_tmp_disk_tables 持续增长 优化 GROUP BY、ORDER BY,适当调大临时表内存阈值
缓冲池太小 磁盘 IO 高,命中率低 调大 InnoDB 缓冲池,但要给其他服务留内存
连接池耗尽 应用报连接超时 检查连接是否正确释放,考虑连接池中间件

用 EXPLAIN 看执行计划是必备技能:

EXPLAIN SELECT * FROM orders WHERE user_id = 123 ORDER BY created_at DESC LIMIT 20\G

重点看 type(最好到 ref 或 range,出现 ALL 说明全表扫描)、key(是否用上了索引)、rows(扫描行数)、以及 Extra 里有没有 Using filesort 或 Using temporary。

前端层

如果 TTFB 正常(几百毫秒内)但用户感觉慢,问题几乎都在前端。

常见问题清单

图片未优化。 这是最高频的前端性能问题。一个几 MB 的 PNG 截图、直接上传的原相机照片、用 JS 加载的整屏背景图,都会吃掉几秒时间。解决:压缩(有损或无损看场景)、转为 WebP 或 AVIF 格式、按显示尺寸提供图片而不是传原图、使用响应式 srcset。

未启用懒加载。 首屏之外的图片应在滚动到附近时才加载:

描述

width 和 height 属性要写上,避免布局抖动给用户造成"慢"的感觉。

渲染阻塞资源。 在 里的同步脚本会阻塞页面渲染。用 defer 或 async:


第三方脚本。 统计、客服、广告、社会化分享这类脚本往往是首屏最大的时间开销,它们的服务器不在你控制范围内。评估每一个第三方脚本是否真的必要,能延迟加载的就延迟。

重定向链。 例如 http://example.com → https://example.com → https://www.example.com 形成两次跳转,每次都是一个完整的往返。应该在第一步就跳到位。

字体。 大体积的字体文件会阻塞文本渲染。使用 font-display: swap 让文本先用系统字体显示,同时精简字体子集。

CDN 的作用

CDN 把静态资源缓存到离用户更近的节点,对地理距离远的用户效果最明显。启用 CDN 后要注意:正确配置缓存规则(静态资源长缓存,HTML 短缓存或不缓存)、确认资源 URL 带版本号以便更新时刷新、以及 CDN 回源本身要快(如果回源要走跨境链路,效果会打折扣)。

常见瓶颈速查表

现象 可能原因 优先优化
首次访问慢,后续快 DNS 慢、未建立缓存、无 HTTP/2 换 DNS、启用 keepalive 与 HTTP/2
所有站点同时慢,SSH 也卡 整机资源耗尽(内存换页或磁盘打满) 查 free、iostat、磁盘占用
TTFB 高,静态页也慢 PHP-FPM 池太小、Web 服务配置问题 查进程池状态、Nginx 错误日志
动态页慢,静态页快 数据库慢查询 开慢查询日志,用 EXPLAIN 分析
偶发性极慢 外部 API 调用、锁等待、定时任务抢资源 查应用日志、MySQL PROCESSLIST、crontab
页面加载出来后图片慢慢刷 图片体积大、未压缩、无懒加载 压缩转 WebP、懒加载、CDN
只有部分地区慢 区域网络链路、CDN 调度 mtr 追踪、调整 CDN 配置
高峰期慢,平时正常 并发能力不足、连接池耗尽 扩容、加缓存、削峰

优化顺序建议

面对一堆待优化项,按下面的顺序做,性价比最高:

第一梯队(配置改动,几小时内完成,收益明显):启用 gzip 或 brotli 压缩;配置静态资源缓存头;启用 OPcache;打开 HTTP/2;压缩并转换图片格式;消除重定向链。

第二梯队(需要定位,一到数天,收益最大):开启数据库慢查询日志,用 pt-query-digest 找出 Top 查询,补索引、改写法;处理 N+1 查询;给热点数据加 Redis 缓存。

第三梯队(架构调整,周级):引入 CDN;静态资源与数据库分离;把外部 API 调用改为异步或加缓存;必要时升级硬件配置或拆分服务。

关键提醒:不要跳过测量直接优化。 先记下当前的关键指标(首页 TTFB、完整加载时间、慢查询条数),每做完一项优化就复测一次,这样才能知道哪项改动真正有效。很多时候直觉上以为的瓶颈(比如服务器配置不够)其实完全不是问题所在。

同时要建立长期监控:服务端响应时间、错误率、数据库慢查询数量、磁盘使用率这些基础指标,配上告警阈值。性能优化不是一次性工作,而是持续观察的过程。

常见问题

TTFB 多少算正常?

静态页面或启用缓存后在一两百毫秒内算不错;未缓存的动态页面在几百毫秒通常可接受。如果超过一秒,用户体验会有明显感知。这还要看网络位置,跨洲访问的物理延迟本身就有几百毫秒。

服务器配置不够是主要原因吗?

多数情况下不是。常见的慢站点瓶颈在未经优化的图片、未压缩的资源、缺失的索引和 N+1 查询上,升级配置只会掩盖问题并延缓它的爆发。先把压测量出来再做决定。

加了 CDN 就一定快吗?

对远距离用户通常有明显改善,但如果瓶颈在源站的动态页面生成(TTFB 高),CDN 无法解决——它缓存的是静态内容。动态内容要么做页面缓存,要么优化源站。

数据库索引是不是越多越好?

不是。索引会加快查询但减慢写入,还占用磁盘和内存。应该针对实际慢查询创建必要的索引,并定期检查是否有从未被使用的冗余索引。

怎么判断是前端慢还是后端慢?

看 curl 的 time_starttransfer。它小(比如 200 毫秒)但页面 Load 时间很长,就是前端问题;它本身就大(比如三秒),重点查后端。

慢查询日志开了会影响性能吗?

影响很小,通常可以忽略。long_query_time 设得太小导致日志量爆炸才是真正的风险,同时要注意给慢日志文件配置 logrotate 避免占满磁盘。

总结

  • 排查的第一步是把"慢"量化并分段:用 curl -w 分解 DNS、TCP、TLS、TTFB、传输各阶段,用浏览器 DevTools 看整体瀑布图,先确定是网络慢、服务端慢还是前端慢。
  • Nginx 日志加 $request_time 和 $upstream_response_time 两个字段是最有价值的长期投入:前者高后者低说明瓶颈在传输,两者都高说明瓶颈在后端应用或数据库。
  • 服务器资源层看三件事:si/so 是否非零(内存换页)、%util 是否持续满(磁盘打满、await 高)、以及 load 与核心数的比值;SSH 也卡基本可以确定是整机资源问题。
  • 数据库是动态站点最常见的瓶颈源,慢查询日志加 EXPLAIN 加索引优化通常比升级硬件有效得多,注意 N+1 查询和大偏移量分页这两个隐形杀手。
  • 前端问题集中在图片体积、懒加载、渲染阻塞脚本和第三方脚本上,这些优化不需要改业务逻辑,成本低收益高。
  • 优化按"先看数据再动手"推进:配置类(压缩、缓存头、OPcache、HTTP/2)先做,数据类(慢查询、索引、缓存层)次之,架构类(CDN、分离部署、扩容)最后,每步都要复测验证效果。