WordPress配Nginx FastCGI Cache,性能对标静态站

前言

WordPress 慢是刻板印象。慢的原因是没做缓存层。一旦叠满缓存,请求进 nginx 就直接返回,根本到不了 PHP——和 Hexo / Hugo 这类静态站走的是同一条路径。

本文给一份完整可用的 nginx 配置 + 主题侧清理钩子接入。目标:保留后台发文、评论、用户系统这些动态能力,首屏 TTFB 压到 10ms 以内。

缓存层叠满后的请求链路

没缓存的 WordPress:

请求 → nginx → PHP-FPM → WordPress 启动 → 插件加载 → 主题加载 → 数据库查询 → 模板渲染 → 返回

叠满缓存之后:

请求 → nginx 拿到缓存 → 返回

等价于 nginx 直接吐 HTML 文件。各层缓存职责:

  • Redis / Memcached Object Cache:缓存数据库查询
  • 主题层整页缓存:把 HTML 渲染结果存成文件,PHP 启动早期 die 返回
  • OPcache:PHP 脚本编译成字节码,省重复编译
  • Nginx FastCGI Cache:在 nginx 阶段短路掉 PHP-FPM 调用,请求根本不进 PHP

前三层是"减少 PHP 内部开销",FastCGI Cache 是"压根不进 PHP",这是最后一公里。

第一步:建目录并赋权

缓存目录选自己 WordPress 站点里的路径。推荐放在 wp-content/cache/ 下,和主题整页缓存同目录层,权限统一好管理。

把下面路径里的 /www/wwwroot/你的站点目录/ 换成自己站点实际根目录:

mkdir -p /www/wwwroot/你的站点目录/wp-content/cache/nginx
chown www:www /www/wwwroot/你的站点目录/wp-content/cache/nginx
chmod 755 /www/wwwroot/你的站点目录/wp-content/cache/nginx

nginx worker 以 www 用户运行,需要读写权限。如果是其他面板或自定义环境,把属主换成 nginx worker 实际运行的用户。

第二步:nginx.conf 定义缓存区

在 http 块尾部加全局配置。这一段属于整个 nginx,不归属单个站点。

同样把 /www/wwwroot/你的站点目录/ 换成自己的站点根目录:

# ============ FastCGI Cache ============
fastcgi_cache_path /www/wwwroot/你的站点目录/wp-content/cache/nginx
                     levels=1:2
                     keys_zone=WORDPRESS_CACHE:100m
                     max_size=500m
                     inactive=1d
                     use_temp_path=off;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout invalid_header updating http_500 http_503;
fastcgi_cache_lock on;
fastcgi_cache_lock_age 5s;
# ============ FastCGI Cache END ============

参数解释:

  • keys_zone=WORDPRESS_CACHE:100m:共享内存存缓存键元数据,100MB 能容纳几十万个 URL,单博客够用
  • max_size=500m:磁盘缓存总上限,超了按 LRU 淘汰
  • inactive=1d:1 天没访问就清掉
  • levels=1:2:目录层级,避免单目录文件过多
  • use_temp_path=off:直接写最终目录,省一次跨设备拷贝

第三步:站点 server 块配置

宝塔默认站点配置里 PHP 是通过 include enable-php-83.conf; 引用的,但 fastcgi_cache 指令必须和 fastcgi_pass 在同一个 location 块里,没法通过 include 注入。所以要把 PHP 段内联出来。

完整 server 块(SSL、HTTPS 跳转、伪静态这些省略,只看 fastcgi cache 相关):

server {
    listen 443 ssl;
    server_name your-domain.com;
    root /www/wwwroot/你的站点目录;

    # ============ 1. 缓存闸门 ============
    # 默认跳过缓存
    set $skip_cache 1;
    # 仅当原始请求 URI 为 "/" 时才允许缓存(首页)
    if ($request_uri = "/") {
        set $skip_cache 0;
    }
    # 仅 GET/HEAD 才缓存
    if ($request_method !~ ^(GET|HEAD)$) {
        set $skip_cache 1;
    }
    # 登录用户 / 评论作者 / 受密码保护文章一律跳过
    if ($http_cookie ~* "wordpress_logged_in|comment_author|wp-postpass") {
        set $skip_cache 1;
    }
    # 暴露缓存命中状态,便于调试
    add_header X-Cache-Status $upstream_cache_status;
    # ============ 缓存闸门 END ============

    # ============ 2. 禁止 web 访问缓存目录 ============
    location ^~ /wp-content/cache/nginx/ {
        deny all;
        return 403;
    }

    # ============ 3. PHP 段内联 + fastcgi cache ============
    location ~ [^/]\.php(/|$) {
        try_files $uri =404;
        fastcgi_pass unix:/tmp/php-cgi-83.sock;
        fastcgi_index index.php;
        include fastcgi.conf;
        include pathinfo.conf;

        # 缓存指令
        fastcgi_cache WORDPRESS_CACHE;
        fastcgi_cache_valid 200 301 302 1d;
        fastcgi_cache_valid 404 1m;
        fastcgi_cache_bypass $skip_cache;
        fastcgi_no_cache    $skip_cache;
        fastcgi_cache_use_stale error timeout invalid_header updating http_500 http_503;
        fastcgi_cache_lock on;
        fastcgi_cache_lock_age 5s;

        # 必须保留:忽略 PHP 输出的 Cache-Control / Expires / Set-Cookie
        fastcgi_ignore_headers Cache-Control Expires Set-Cookie;

        # 透传主题自定义响应头
        fastcgi_pass_header Cache-Control;
        fastcgi_pass_header Pragma;
        fastcgi_pass_header Expires;
        fastcgi_pass_header ETag;
        fastcgi_pass_header Last-Modified;
        fastcgi_pass_header Vary;
        fastcgi_pass_header Content-Length;
        fastcgi_pass_header X-Cache-Type;
    }
}

配置拆解

1. $skip_cache 闸门:决定"要不要缓存"

闸门判断三件事:

  • URI 必须是 /(首页)——本教程只缓存首页的核心规则
  • 必须是 GET 或 HEAD——POST 请求(评论提交、登录)一律放过
  • 不能带 wordpress_logged_in / comment_author / wp-postpass cookie——登录态、评论作者、受密码保护文章不缓存

这套闸门不只是防漏缓存,更防"登录态被缓存进首页、所有访客共享同一份缓存"这种灾难。

2. fastcgi_ignore_headers:决定"nginx 是否写入缓存"

WordPress 和大部分主题的 PHP 都会发 Cache-Control 头,类似:

Cache-Control: public, max-age=600, s-maxage=600, stale-while-revalidate=600, stale-if-error=600

nginx 拿到带 s-maxage 和 stale-while-revalidate 的头,会严格判断"这玩意只能被 CDN 缓存、不能被 FastCGI Cache 缓存",直接拒绝写入。

我配置时把这行删了测试,连续 5 次首页访问全部 MISS,没有一次 HIT。加回来立刻正常。缓存行为由 fastcgi_cache_valid 决定,不再受 PHP 头影响。

3. fastcgi_pass_header:决定"主题头是否透传"

fastcgi 默认不透传 X-* 自定义头。主题用来标记缓存状态的头(X-Cache-Type / X-Pix-Page-Cache 之类)需要显式 fastcgi_pass_header 才能在浏览器 DevTools 看到。响应头一字不差地透传是验证缓存命中正确性的关键。

第四步:主题侧清理机制对接

FastCGI Cache 弱点是 nginx 原生不支持按 URL 自动清理。首页内容更新后想立即看到,得有人主动清缓存。最顺手的办法是借用主题里"清除首页缓存"按钮的处理函数。

大部分主题都有一套自己的整页缓存清理逻辑,找到那个函数,在末尾追加一段递归删除 nginx 缓存目录的代码:

function clear_theme_page_cache() {
    // ... 主题原本的清理逻辑 ...

    // 同步清除 Nginx FastCGI Cache
    $cache_dir = WP_CONTENT_DIR . '/cache/nginx';
    if (!is_dir($cache_dir)) {
        return;
    }

    try {
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($cache_dir, FilesystemIterator::SKIP_DOTS),
            RecursiveIteratorIterator::CHILD_FIRST
        );
        foreach ($iterator as $file) {
            if ($file->isDir()) {
                @rmdir($file->getRealPath());
            } else {
                @unlink($file->getRealPath());
            }
        }
    } catch (Exception $e) {
        // 静默失败,避免影响主题清理流程
    }
}

路径用 WP_CONTENT_DIR 常量动态拼接,不写死绝对路径,迁移站点时不用改代码。

主题里所有调用清理函数的地方(清理按钮、关闭缓存开关、文章清理钩子)都会顺带清 nginx 缓存,不需要再单独加 transition_post_status / save_post 钩子,主题自身的清理流程已经覆盖这些场景。

踩坑记录

坑 1:duplicate 错误

报错:

nginx: [emerg] "fastcgi_cache_key" directive is duplicate in /www/server/panel/vhost/nginx/0.fastcgi_cache.conf:1

宝塔面板的"Nginx 缓存"插件会自动生成 0.fastcgi_cache.conf,里面已经定义了一组 fastcgi_cache 全局指令,和手动加的重复。

解决:在宝塔面板关掉那个开关,或直接改名备份:

mv /www/server/panel/vhost/nginx/0.fastcgi_cache.conf /www/server/panel/vhost/nginx/0.fastcgi_cache.conf.bak

坑 2:连续 MISS 不 HIT

5 次首页全部 MISS。原因就是上面说的 fastcgi_ignore_headers 被删了,主题输出 s-maxage 后 nginx 拒绝写入。加回来就好。

主题早期缓存和 FastCGI Cache 的关系

主题早期缓存(比如 HTTP 头里 X-Pix-Page-Cache: EARLY-HIT)在 PHP 启动早期 die 返回,和 FastCGI Cache 不冲突。两层叠加:

  • FastCGI Cache 命中:请求根本不到 PHP,早期缓存没机会跑
  • FastCGI Cache 未命中:进 PHP,早期缓存命中就 die,响应头里两个标记共存

扩展:从首页缓存到全站缓存

默认配置只缓存首页——单用户博客最常见的需求,游客为主,发文靠手动清理按钮。

想全站缓存:删掉 if ($request_uri = "/") { set $skip_cache 0; } 这段,闸门只剩 GET/HEAD 和登录态检查,所有 GET 请求都缓存。

想白名单缓存:把那个 if 改成 URI 正则:

if ($request_uri ~ "^/$|^/articles/|^/tags/") {
    set $skip_cache 0;
}

POST、登录态、带 query string 的请求自动 bypass,不用额外配置。

验证

# 1. 测试配置并重载
nginx -t
nginx -s reload

# 2. 清掉旧缓存
rm -rf /www/wwwroot/你的站点目录/wp-content/cache/nginx/*

# 3. 连续打 5 次首页
curl -I https://your-domain.com/   # MISS
curl -I https://your-domain.com/   # HIT
curl -I https://your-domain.com/   # HIT
curl -I https://your-domain.com/   # HIT
curl -I https://your-domain.com/   # HIT

预期:第 1 次 MISS,后 4 次 HIT。HIT 时响应头的 Cache-Control / ETag / Last-Modified / Vary 必须和 MISS 时完全一致,否则就是有头被 fastcgi 吞了。

其他场景必须 BYPASS:

  • 文章页:BYPASS
  • 带 query string(/?paged=2):BYPASS
  • 带 wordpress_logged_in cookie:BYPASS
  • 直接访问缓存目录(/wp-content/cache/nginx/):403

调优

  • fastcgi_cache_valid 200 301 302 1d;:1 天有效期,发文靠主题按钮清理兜底
  • max_size=500m:大概能存 200+ 个首页版本(单 HTML 50-80KB),单博客够用
  • open_file_cache max=1000 inactive=20s;:在 http 块加这一行,让 nginx 缓存文件元数据,每次读盘少一次 stat 系统调用,TTFB 再压 1-2ms
  • fastcgi_cache_use_stale error timeout ...:后端出错时返回旧缓存而不是 502,PHP-FPM 偶尔卡一下也不影响访客

不上 CDN,TTFB 稳定 20ms 以内,运气好 5-10ms。Hexo / Hugo 同档的速度,WordPress 的灵活度。

上一篇 WordPress 对象缓存选 APCu 还是 Redis
丙氨酸

丙氨酸管理员

与其等春来 不如追花去

本月创作热力图

文章目录