在服务器运维中,硬件配置往往不是瓶颈,配置失衡才是。最近我对运行在 2核2G VPS(20M带宽) 上的 Typecho 博客进行了一次深度性能调优。通过对 PHP 8.4 环境的层层优化,见证了网站承载能力从“高延迟抗拒”到“稳如泰山”的转变。

本文将分享完整的优化逻辑、使用 Locust 进行的压测数据对比,以及最关键的 Nginx FastCGI Cache 配置方案。

1. 测试方法:Locust 阶梯压力测试

为了获得最真实的性能曲线,我弃用了简单的命令行工具wrk,改用现代化的分布式压测工具 Locust

  • 测试策略:阶梯式增加并发。分别测试 50、100、150 名虚拟用户。
  • 用户行为:模拟真实用户持续随机访问首页及文章页。
  • 孵化速率 (Spawn Rate):5 users/s(模拟用户平滑涌入)。
  • 环境基准:所有测试均已开启 PHP 8.4 的 Zend OPcache,对比项仅为“是否开启 Nginx FastCGI Cache”。

2. 初始状态:OPcache 开启但无页面缓存

在仅有 OPcache 的情况下,每一条请求仍需经过 PHP-FPM 解析并查询 MySQL 数据库。

压测数据分析(优化前):

  • Run #1 (50用户):RPS 约为 11.3,95%ile 延迟为 1700ms
  • Run #2 (100用户):响应延迟剧增,95%ile 飙升至 6000ms (6秒)
  • Run #3 (150用户)性能雪崩。RPS 依然卡在 11 左右无法增长,95%ile 延迟达到了恐怖的 11000ms (11秒)
    网站压力测试(优化前)

结论:2核2G 的服务器在处理动态 PHP 请求时,单机极限约在 11 RPS。当并发超过 50 后,请求在后台开始疯狂排队,用户感知的响应速度迅速恶化到无法使用的地步。


3. 优化坑点:Redis 插件的“负优化”

为了提速,我曾尝试安装 Typecho 的 Redis 缓存插件,结果却令人意外:

  • 无 Redis 插件:102.41 RPS (wrk 单口测试)
  • 有 Redis 插件:85.75 RPS (下降约 16%)
    避坑指南:对于结构简单的个人博客,MySQL 8.4 的主键查询效率极高。引入 Redis 带来的 PHP 序列化开销和网络通信开销,反而超过了节省的数据库时间。在小型博客场景下,减少链路比增加缓存层更有效。

备注:该项测试采用全新Typecho站点,搭配官方默认主题且仅有两篇纯文本的文章


4. 终极优化:Nginx FastCGI Cache 配置实战

FastCGI Cache 让 Nginx 直接缓存 PHP 输出的 HTML,请求无需再经过 PHP-FPM 和数据库,实现了真正的“静态化”。

第一步:定义全局缓存空间

在 Nginx 全局配置文件(http 块)加入:

# 定义缓存路径、层级、内存占用(100m)、失效时间(60m)
fastcgi_cache_path /tmp/nginx_cache levels=1:2 keys_zone=TYPECHO:100m inactive=60m max_size=1g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout invalid_header http_500;
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;

Nginx 全局配置文件完整示例:

http {
    include       mime.types;
    default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  main;
    access_log /dev/stdout main;
    
    server_tokens off;
    sendfile   on;
    tcp_nopush on;       
    tcp_nodelay on;  

    server_names_hash_bucket_size 512;
    client_header_buffer_size 32k;
    client_max_body_size 50m;
    keepalive_timeout 60;
    keepalive_requests 5000;

    gzip on;
    gzip_min_length  1k;
    gzip_buffers     4 16k;
    gzip_http_version 1.1;
    gzip_comp_level 2;
    gzip_types     text/plain application/javascript application/x-javascript text/javascript text/css application/xml;
    gzip_vary on;
    gzip_proxied   expired no-cache no-store private auth;
    gzip_disable   "MSIE [1-6]\.";

    limit_conn_zone $binary_remote_addr zone=perip:10m;
    limit_conn_zone $server_name zone=perserver:10m;

    # --- FastCGI Cache 全局定义开始 ---
    # 定义缓存目录、层级、内存名称及大小(100m)、有效期(60m)、最大硬盘占用(1g)
    fastcgi_cache_path /tmp/nginx_cache levels=1:2 keys_zone=TYPECHO:100m inactive=60m max_size=1g;
    fastcgi_cache_key "$scheme$request_method$host$request_uri";
    # 遇到后端错误时,允许返回过期的缓存给用户,保证可用性
    fastcgi_cache_use_stale error timeout invalid_header http_500;
    # 忽略后端传来的不缓存指令,强制缓存
    fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
    # --- FastCGI Cache 全局定义结束 ---
  
    include /usr/local/openresty/nginx/conf/conf.d/*.conf;
    include /usr/local/openresty/nginx/conf/default/*.conf;
    include /usr/local/openresty/1pwaf/data/conf/waf.conf;
}

第二步:网站 server 块控制逻辑

set $skip_cache 0;
# POST请求、带参数的搜索、后台页面、登录用户不走缓存
if ($request_method = POST) { set $skip_cache 1; }
if ($query_string != "") { set $skip_cache 1; }
if ($request_uri ~* "/admin/|/action/|config.inc.php") { set $skip_cache 1; }
if ($http_cookie ~* "typecho_") { set $skip_cache 1; }

第三步:PHP 块应用缓存

location ~ [^/]\.php(/|$) {
    fastcgi_pass 127.0.0.1:9000; 
    fastcgi_cache TYPECHO;                
    fastcgi_cache_valid 200 301 302 30m;  
    fastcgi_cache_bypass $skip_cache;     
    fastcgi_no_cache $skip_cache;         
    add_header X-Cache "$upstream_cache_status From $host"; # 方便调试
}

网站完整的Nginx配置文件示例:
(针对handsome主题PJAX加载文章的方式有单独优化)

server {
    listen 80 ; 
    listen 443 ssl ; 
    server_name www.dora520.top; 
    index index.php index.html index.htm default.php default.htm default.html; 
    access_log /www/sites/www.dora520.top/log/access.log main; 
    error_log /www/sites/www.dora520.top/log/error.log; 
    location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md) {
        return 404; 
    }
    location ^~ /.well-known/acme-challenge {
        allow all; 
        root /usr/share/nginx/html; 
    }
    if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
        return 403; 
    }
    error_page 404 /404.html; 
    root /www/sites/www.dora520.top/index; 
    # --- 缓存控制逻辑开始 ---
    set $skip_cache 0;
    # 1. POST 请求不缓存
    if ($request_method = POST) {
        set $skip_cache 1;
    }
    # 2. 处理查询参数
    # 先默认:只要有参数就跳过缓存(如搜索)
    if ($query_string != "") {
        set $skip_cache 1;
    }
    # 特例豁免:如果是 PJAX 请求(包含 _pjax=),我们允许缓存它
    if ($query_string ~* "_pjax=") {
        set $skip_cache 0;
    }
    # 3. 后台页面、安装页面、配置文件不缓存
    if ($request_uri ~* "/admin/|/action/|/install.php|config.inc.php") {
        set $skip_cache 1;
    }
    # 4. 登录用户不缓存
    if ($http_cookie ~* "typecho_") {
        set $skip_cache 1;
    }
    # --- 缓存控制逻辑结束 ---
    location ~ [^/]\.php(/|$) {
        fastcgi_pass 127.0.0.1:9000; 
        include fastcgi-php.conf; 
        include fastcgi_params; 
        # --- 新增缓存配置开始 ---
        fastcgi_cache TYPECHO;                   # 使用全局定义的缓存空间
        fastcgi_cache_valid 200 301 302 30m;     # 状态码 200、301、302 缓存 30 分钟
        fastcgi_cache_bypass $skip_cache;        # 满足不缓存条件时跳过查询缓存
        fastcgi_no_cache $skip_cache;            # 满足不缓存条件时不存入缓存
        add_header X-Cache "$upstream_cache_status From $host"; # 方便在浏览器查看命中状态
        # --- 新增缓存配置结束 ---
        set $real_script_name $fastcgi_script_name; 
        if ($fastcgi_script_name ~ "^(.+?\.php)(/.+)$" ) {
            set $real_script_name $1; 
            set $path_info $2; 
        }
        fastcgi_param SCRIPT_FILENAME $document_root$real_script_name; 
        fastcgi_param SCRIPT_NAME $real_script_name; 
        fastcgi_param PATH_INFO $path_info; 
    }
    http2 on; 
    ssl_certificate /www/sites/www.dora520.top/ssl/fullchain.pem; 
    ssl_certificate_key /www/sites/www.dora520.top/ssl/privkey.pem; 
    ssl_protocols TLSv1.3 TLSv1.2; 
    ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED; 
    ssl_prefer_server_ciphers off; 
    ssl_session_cache shared:SSL:10m; 
    ssl_session_timeout 10m; 
    error_page 497 https://$host$request_uri; 
    proxy_set_header X-Forwarded-Proto https; 
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; 
}

5. 最终成果:触碰物理带宽极限

开启 FastCGI Cache 后,再次运行 Locust 阶梯压测。

压测数据对比(优化后):

  • Run #1 (50用户):RPS 约为 15,95%ile 延迟仅 300ms
  • Run #2 (100用户):RPS 翻倍至 31.2,95%ile 依然稳在 300ms 左右。
  • Run #3 (150用户)吞吐量极限。RPS 冲到了 40-45 之间,响应时间开始平稳抬升至 1.5s 左右。
    网站压力测试(优化后)

深度对比分析:

  1. 吞吐量提升 400%:开启缓存后,RPS 从 11 暴涨至 45
  2. 延迟大幅下降:在 100 并发下,延迟从 6秒 降至 0.3秒,提升了 20倍 的用户体验。
  3. 触碰物理瓶颈

    • 平均页面大小约 105 KB
    • 计算:$45 \text{ RPS} \times 105 \text{ KB} \approx 4725 \text{ KB/s} \approx 37 \text{ Mbps}$。
    • 由于我的 VPS 拥有 20M 带宽及一定的峰值(Burst)余量,RPS 卡在 45 并不是因为 CPU 不行了,而是 20M 的带宽被彻底填满了。

6. 总结建议

对于 2核2G 级别的轻量级服务器,优化顺序应为:

  1. 必做项:开启 PHP OPcache,这是 PHP 环境的基石。
  2. 杀手锏:配置 Nginx FastCGI Cache。它让你的源站拥有了 CDN 般的吐息能力,是应对高并发的唯一最优解。
  3. 认知修正:当响应速度达到极限时,请查看网卡监控。如果带宽跑满,优化代码已无意义,此时应考虑开启 Gzip/Brotli 压缩 或接入 CDN 分流。

通过这套方案,我的 Typecho 博客在极低配置下实现了从“50人并发崩溃”到“100人并发秒开”的质变,彻底告别了性能焦虑。

最后修改:2026 年 03 月 15 日
如果觉得我的文章对你有用,请随意赞赏