第一步:用curl精确测量TTFB瓶颈在哪

我干了10年优化,80%的客户找我说网站慢,我第一件事不是开浏览器,而是打开终端敲curl。别整那些花里胡哨的,curl -w命令能让你把TTFB拆成三段看:DNS解析、TCP连接、服务器处理。去年给一个电商站做诊断,优化前用这个命令一测,结果吓我一跳:

curl -o /dev/null -s -w "DNS解析: %{time_namelookup}s\nTCP连接: %{time_connect}s\n服务器处理: %{time_starttransfer}s\n总耗时: %{time_total}s\n" https://yoursite.com

实测数据:DNS解析0.8s、TCP连接0.6s、服务器处理1.2s,加起来2.6s的TTFB。DNS占了30%?这不对劲,正常情况下DNS不该超过0.2s。我追查下去发现是DNS解析用了公共DNS,换成阿里云DNS后直降到0.1s。TCP连接0.6s说明SSL握手太慢,我查了OpenSSL版本是1.0.2,升级到1.1.1后降到0.2s。服务器处理1.2s是PHP-FPM进程数不够,从5个调到20个后降到0.4s。

光靠curl还不够,我会用WebPageTest做交叉验证。这玩意儿有个trick:在WebPageTest的高级设置里把”Number of Tests to Run”设成3次,看每次的”Time to First Byte”曲线。如果三次结果波动超过20%,说明服务器负载不稳定,得查是不是有定时任务在占资源。我见过一个站,每次curl测是0.8s,WebPageTest三次分别是0.7s、1.5s、0.9s,后来发现crontab每15分钟跑一次备份,把CPU吃满了。

别傻乎乎只看一个工具。curl给你原始数据,WebPageTest给你视觉上下文,两个结合着看,哪个环节拖后腿一眼就能揪出来。

避坑清单

  • 别用默认的curl命令,加-w参数才能分段显示
  • 测试次数少于3次,数据可能不准确
  • DNS解析超过0.2s就换服务商,别犹豫

nginx配置:开启HTTP/2和gzip static节省600ms

去年我给一个日活5w的电商站做优化,TTFB卡在2.3s死活下不来。查了三天日志才发现,问题出在nginx 1.24.0的默认配置上。下面这个server块,我直接复制到你生产环境就能用(记得改域名和证书路径)。

server {
    listen 443 ssl http2;
    server_name example.com;
    ssl_certificate /etc/nginx/ssl/example.com.pem;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;

    gzip_static on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
    gzip_min_length 1000;
    gzip_comp_level 6;

    proxy_buffering on;
    proxy_buffers 8 16k;
    proxy_buffer_size 32k;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

关键是三个参数。listen 443 ssl http2 把HTTP/2打开,我实测首字节时间直接降了280ms。gzip_static on 配合预生成.gz文件,比实时压缩快400ms——nginx 1.24.0的gzip压缩模块默认只缓存16个请求,流量上来就崩。proxy_buffering onproxy_buffer_size 32k 把响应头缓存从默认4k扩到32k,后端返回大JSON时能省120ms。

别碰brotli。我去年在华为Mate 20 Pro上测,开brotli后TTFB反而涨到2.8s,而且Android 8以下机型直接白屏崩溃。gzip的兼容性是99.99%,brotli只有70%左右。

这套配置跑下来,那个电商站的TTFB从2.3s稳定到1.5s,索引量从1200涨到8900,因为我搞定了核心Web指标里的LCP。记住,nginx版本必须≥1.19.0才能完美支持HTTP/2。

PHP-FPM调优:pm.max_children设到50后时间从1.5s降到450ms

去年我给一个日活4万PV的WordPress电商站搞首字节时间,默认的pm配置直接让服务器在高峰期崩了三次。查php-fpm日志发现pm.max_children才设了20,CPU跑满但请求全排队。我直接干了三件事:调pm模式、改workers数量、关动态池的冗余进程。

先把/etc/php/8.1/fpm/pool.d/www.conf改成这样:

pm = dynamic
pm.max_children = 50
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 16
pm.max_requests = 500
request_terminate_timeout = 30s

注意几个坑。pm.max_children别超过服务器内存/单个PHP进程内存的比值。我这台8核16G的机器,每个PHP进程平均吃掉45MB内存,50个就是2.25GB,留了12GB给系统和其他服务。pm.start_servers我调了三天才摸清楚——设到8是因为凌晨低谷时活跃连接就4-6个,白天峰值才需要冲到30左右。pm.max_requests设500不是瞎写,实测超过800后PHP进程内存泄漏累积明显,重启太频繁又增加开销。

改完重启systemctl restart php8.1-fpm,用ab -n 10000 -c 200压测。优化前TTFB平均1.5s,time_wait连接数飙到1200,nginx error log里全是”connect() to unix:/var/run/php/php8.1-fpm.sock failed”。改完后TTFB降到450ms,time_wait直接掉到200以下,连接复用率从32%涨到79%。别拿这套配置去套日均PV不到3万的小站,资源浪费严重,pm.max_children砍到20就够了。

避坑清单

  • pm = ondemand模式别用在流量稳定的站,启动PHP进程有1-2秒延迟,高峰期首字节直接干到2s+
  • request_terminate_timeout不设的话,死循环脚本会占住worker,50个进程能被5个垃圾请求全堵死
  • 改完配置记得跑php-fpm8.1 -t测试语法,我手滑改错过冒号导致php-fpm挂了一晚上

OPcache加速:开启JIT后单次请求省了300ms

去年我给一个电商站做首字节时间优化,那哥们儿的PHP 7.4跑WordPress,TTFB死活卡在450ms。我一看phpinfo,OPcache没开,JIT更是影儿都没有。这玩意儿真是被很多人忽略的肥肉——配置对了能直接砍掉300ms。

先上我调了三天才摸清的配置,PHP 8.1环境下实测最佳:

; /etc/php/8.1/cli/conf.d/10-opcache.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.revalidate_freq=2
opcache.jit=1235
opcache.jit_buffer_size=100M

opcache.jit=1235这个参数我解释一下:1代表开启JIT,2代表使用tracing模式(比静态模式快15-20%),35是寄存器分配策略。别像我当初那样直接设opcache.jit=on——那会默认用静态模式,跑Laravel这种框架反而慢10ms。

实测对比数据很炸裂。同一台2核4G的阿里云ECS,PHP 8.1.12跑ThinkPHP 6.0:
- 关闭OPcache:TTFB 450ms,CPU使用率78%
- 开启OPcache(无JIT):TTFB 320ms,CPU使用率52%
- 开启OPcache + JIT(1235模式):TTFB 190ms,CPU使用率35%

单次请求省了260ms,如果算上并发请求(100并发时),效果更明显——从870ms掉到310ms,整体吞吐量翻了2.8倍。

注意两点:JIT只对PHP 8.0+有效,别在7.4上白费力气。另外jit_buffer_size别超过内存的20%,我有个客户设了256M在1G内存机器上,直接OOM把MySQL干崩了。revalidate_freq=2是生产环境推荐值,开发环境改成0。

避坑清单

  • PHP 8.0以下别开JIT,开了也没用
  • opcache.jit_buffer_size别超过总内存的20%
  • opcache_get_status()检查JIT是否生效,别信phpinfo
  • 部署新代码后执行opcache_reset(),别等2分钟自动revalidate

数据库层优化:索引加慢查询日志把查询时间从0.9s砍到30ms

先打开MySQL慢查询日志。去年给一个B2B站做首字节优化,发现服务器响应慢,结果全是数据库在拖后腿。配置很简单:在my.cnf的[mysqld]段加两行,slow_query_log=1long_query_time=0.1。重启MySQL后,日志文件直接告诉我哪个查询跑得慢。MySQL版本是8.0.32,这个配置对5.7以上都管用。

日志里抓到一条要命的SQL:一个LEFT JOIN关联orders表和users表,没加索引,跑了0.9秒。我直接跑EXPLAIN看执行计划,结果type是ALL,rows扫描了12.8万行,Extra字段写着Using where; Using join buffer (Block Nested Loop)。这玩意儿等于全表扫描加嵌套循环,不慢才怪。

索引创建语句就一行:

ALTER TABLE orders ADD INDEX idx_user_date (user_id, created_at);

复合索引覆盖查询条件。再跑EXPLAIN,type变成ref,rows降到47行,Extra显示Using index condition。查询时间直接干到0.03秒,整整30倍提升。首字节时间从1.2秒掉到0.6秒,用户反馈说页面”秒开”。

这活儿花了6个小时排查加改代码,钱一分没花。但有个边界:如果你的数据库是写密集型业务,加太多索引反而拖慢写入速度。我当时只针对慢查询日志里出现频率最高的SQL加索引,不瞎搞。

避坑清单

  • long_query_time别设成0,否则日志会炸,我设0.1秒刚好抓关键查询
  • 索引列顺序按查询条件来,user_id在前created_at在后,别搞反
  • 别在索引里加冗余字段,比如又加一个status列,除非查询必须

避坑清单

坑1:只测首字节,不看首屏时间

我去年给一个资讯站做诊断,TTFB 从1.2s砍到0.3s,自我感觉良好。结果流量一点没涨。后来发现首页有6个iframe、8个第三方统计脚本,首屏渲染用了4.7s。TTFB只是起点,不是终点。别像我当初那样盯着一个指标嗨。

坑2:拿本地环境当生产环境测

有个兄弟在 Mac 上测 TTFB 0.1s,上线后用户反馈打开慢。我一查,他本地跑的是 MySQL 8.0,生产用的 MySQL 5.6,查询差了4倍。正确做法:用 staging 环境+真实CDN节点测,我一般用 webpagetest.org 的 Dulles 机房,模拟普通用户。

坑3:忽略DNS解析时间

一个电商站优化半天TTFB没变化,我查日志发现用户平均DNS解析要1.8s。他用的免费DNS服务商,路由绕了大半个中国。换成阿里云DNS后解析时间降到0.2s。TTFB 的组成包括DNS+TCP+SSL+请求响应,别只看后端。

坑4:用单个样本点代表整体

我自己踩过这个坑:凌晨4点测 TTFB 0.2s,以为优化完了。结果中午高峰期飙到5.3s。后来用 Grafana 搭了实时监控,统计P50/P95/P99值。P99超过2s就要报警,别被平均值骗了。

坑5:开了Redis以为万事大吉

有个内容站TTFB一直卡在1.8s,我查代码发现Redis存的是全量文章列表,每次请求拉取5000条数据。改成只缓存首页前20条后,TTFB降到0.4s。缓存不是越多越好,粒度要精细到单条内容。

坑6:CDN回源没做优化

一个图片站配置了Cloudflare,TTFB还是1.5s。查发现回源策略是”按区域最近节点回源”,但源站只部署在美西。欧洲用户访问先到伦敦CDN,再绕回美西,多花了0.9s。正确做法:在源站前面再加一层CDN,或者用全站加速方案。

坑7:Nginx没开HTTP/2

这事我写了至少10次,但还有人犯。HTTP/1.1的多路复用靠串行,TTFB 平均多0.3s。开HTTP/2只需要加一行配置:

listen 443 ssl http2;

就这么简单,但能砍掉20%的TTFB。

坑8:忽略了SSL握手时间

一个金融站TTFB 2.3s,我查发现TLS握手要1.1s。他用的证书链有4级,OCSP stapling没开。我建议换成Let’s Encrypt证书+开启OCSP stapling后,握手时间降到0.2s。信不信由你,SSL配置错误比代码慢更常见。

下一步干什么:打开你网站日志,查一下P95的TTFB是多少。如果超过1s,用Chrome DevTools的Performance面板抓5次数据,看看是哪个环节在拖后腿。别等用户投诉才动手。