侧边栏壁纸
  • 累计撰写 197 篇文章
  • 累计收到 2 条评论

为了提升 TTFB 给网站配了 Nginx fastcgi_cache,搜索快照却全乱套?排查 Set-Cookie 串味、URL 参数击穿与移动蜘蛛命中 PC 缓存我踩过的几个坑

2026-10-14 / 0 评论 / 6 阅读
AI 摘要由 AI 生成

为了优化网站性能,作者配置了Nginx的fastcgi_cache,但导致搜索引擎抓取到的快照出现管理员后台操作条和页面错乱等问题。分析发现,问题源于未过滤Set-Cookie和URL参数击穿,以及移动蜘蛛命中PC缓存。作者建议在Nginx中设置跳过缓存的逻辑,确保登录用户和包含Set-Cookie的缓存不被缓存。

看到百度站长平台和 Google Search Console 频繁发来抓取耗时提醒,显示页面平均响应时间(TTFB)一直在 700ms 到 1.2s 之间晃荡,我一度怀疑是那台 2 核 4G 的小云服务器撑不住并发了。

博客用的是经典的 PHP-FPM + MySQL 架构。每次搜索引擎蜘蛛跑来扫站,十几条并发线程顺着内链一顿狂抓,MySQL 的 CPU 占用率就直接往 85% 上面冲。PHP 每次都要解析模板、查分类、拉关联文章,哪怕 MySQL 开了查询缓存,也扛不住蜘蛛连续几千次的页面遍历。

当时最直接的想法就是在 Nginx 层做动态整页缓存。只要把生成好的 HTML 直接缓存在本地磁盘上,后来的请求全由 Nginx 静态文件模块直接吐出,根本不需要把请求打到后端的 PHP-FPM 进程池。

我很快配好了 fastcgi_cache。实测效果相当亮眼:首页和文章页的 TTFB 从原来的 800ms 瞬间压缩到 12ms 左右,用 ab 压测 500 并发,服务器负载几乎纹丝不动。

然而高兴了不到两周,我在搜索结果里随手点开百度快照,后背直接冒冷汗:快照里竟然带着我自己的管理员后台操作条,右上角清清楚楚挂着“编辑文章”、“退出登录”和站长昵称。再看站长平台的抓取数据,几篇新发布的文章连续十几天没被收录,移动端页面的抓取校验还莫名其妙挂了一堆“页面排版错乱”、“移动适配不达标”的警告。

把 Nginx 缓存当成单纯的性能优化开关,往往会掉进 SEO 的隐蔽暗坑里。爬虫可不会像普通浏览器那样有会话隔离,它的抓取逻辑机械生硬,一旦给它喂了脏缓存,搜索引擎对整个站点的抓取信任度会断崖式受挫。


坑点一:Set-Cookie 没过滤,快照串味泄露登录态与私有信息

这是整页缓存里最危险、也是后果最严重的一个坑。

很多 PHP 程序(不管是 WordPress、emlog、Typecho 还是自研系统),哪怕访客只是看一篇公开文章,PHP 内部在初始化 session 时,也会顺手在 HTTP 响应头里塞一个 Set-Cookie: PHPSESSID=...。如果恰好是管理员在浏览器里登录着后台,点开前台文章预览,PHP 吐出的 HTML 里不仅会渲染出管理员专用的编辑工具栏,响应头里更带着管理员的身份 Cookie。

Nginx 的默认机制是:如果上游后端返回了 Set-Cookie,或者包含特定防缓存的 Header,Nginx 往往会有两极化的表现:

  1. 要么它遵守标准直接放弃缓存(导致缓存命中率接近 0);
  2. 要么站长为了强行缓存,配置了 fastcgi_ignore_headers Set-Cookie;,结果 Nginx 把管理员刚才访问时生成的个性化 HTML、连同 Session 头,完完整整打包存进了磁盘缓存文件。

紧接着悲剧就发生了:百度蜘蛛(Baiduspider)顺着链接爬过来请求这篇文章。Nginx 一看缓存文件有效,直接 HIT,把包含着管理员界面、编辑链接甚至敏感 token 的静态页面吐给了爬虫。

爬虫不会执行客户端 JS 去二次鉴权,它只认服务端返回的这串原始 HTML。几天后搜索引擎建库建快照,抓取的就是这套“串味”的页面。更要命的是,爬虫把快照里的“编辑文章”、“删除评论”等管理链接提取出来当成站内链接,成百上千次向后台接口发起探测,直接触发了安全防护的报警风暴。

怎么排查?

拿 curl 不带任何 Cookie 模拟搜索引擎蜘蛛抓取,看看返回的内容和响应头:

curl -I -A "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)" \
  https://example.com/post-105.html

重点看两点:

  • 返回头里有没有莫名其妙的 Set-Cookie;
  • 查看返回的 HTML 正文(去掉 -I),搜一下有没有后台管理字段或者特定用户的私有信息。

怎么彻底解决?

在 Nginx 里必须守住两条红线:登录用户绝不走缓存,以及给未登录用户或爬虫吐出的缓存绝不包含 Set-Cookie。

首先在 http 块或 server 块里定义跳过缓存的逻辑:

# 默认走缓存
set $skip_cache 0;

# POST 请求不缓存
if ($request_method = POST) {
    set $skip_cache 1;
}

# 带有查询参数的特殊情况(如果是必须穿透的业务参数)
if ($query_string != "") {
    set $skip_cache 1;
}

# 带有登录 Cookie、评论者 Cookie 或后台 Session 的请求绝对绕过
if ($http_cookie ~* "comment_author|wordpress_logged_in|EM_AUTHCOOKIE|PHPSESSID") {
    set $skip_cache 1;
}

然后在对应的 location ~ \.php$ 块里,把忽略和隐藏 Header 做严密约束:

# 针对需要绕过的情况生效
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;

# 忽略后端上游发来的这些控制头,强制允许 Nginx 缓存公开页面
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;

# 关键:从发给客户端/爬虫的响应中彻底剔除 Set-Cookie,防止私有会话泄露
fastcgi_hide_header Set-Cookie;

# 显式透传当前缓存状态,方便用 curl 观察
add_header X-Cache-Status $upstream_cache_status;

这样一来,管理员登录后看到的是实时的动态后台,而爬虫和普通访客拿到的永远是纯净脱敏的公开静态版本,快照绝不会再串味。


坑点二:URL 追踪参数击穿 cache_key,爬虫大军直接打爆 PHP-FPM

很多技术文章在介绍 fastcgi_cache_key 时,随手给出的示例通常长这样:

fastcgi_cache_key "$scheme$request_method$host$request_uri";

表面看合情合理:协议、请求方式、域名、请求路径全包含进去了,每个 URL 对应一个唯一的缓存。

可一到搜索引擎实战场景,这个 key 就会引爆灾难。

搜索引擎爬虫(尤其是百度、搜狗以及各类海外聚合蜘蛛)在遍历网站时,往往会附带很多追踪参数或来源标识。例如百度抓取时偶尔带的 ?from=baidu、外部反向链接带的 ?utm_source=seo&utm_medium=rss,或者恶意扫描爬虫在 URL 尾部随机拼上的无序时间戳 ?t=173829104。

$request_uri 在 Nginx 内部代表的是完整的原始请求串,包含了问号及后面的所有参数。

这意味着:

  • /article/301.html
  • /article/301.html?from=baidu
  • /article/301.html?spm=1001.2014
  • /article/301.html?utm_source=twitter

在 Nginx 眼里,这是 4 个完全不同的页面。它会在磁盘上分别建 4 个单独的缓存文件。

当爬虫顺着大量带参链接批量扫站时,看似配置了缓存,实则每一个带参请求都在发生 MISS 穿透。原本静态缓存是为了保护后端,现在倒好:因为 cache_key 碎片化,磁盘里塞满了 99% 内容相同但 key 不同的垃圾缓存文件;更严重的是,成千上万个带参请求瞬间涌入 PHP-FPM,瞬间占满所有可用 worker。

结果就是服务排队超时,返回大量 502 Bad Gateway 和 504 Gateway Timeout。百度站长平台里的“抓取异常监控”曲线直接飙红,抓取配额被搜索引擎系统级削减。

怎么规范化 cache_key?

解决这个问题的逻辑是:区分真业务参数与无关参数。

在常规博客与资讯站里,绝大多数正文页根本不需要任何 query string;只有分页、标签筛选等少数场景需要参数。如果文章详情页本身就不依赖 query 参数,直接用清洗后的纯路径 $uri 作为 key 的主体;如果需要保留特定参数,可以通过 Nginx 的 map 指令把无关参数过滤掉。

在实际生产中,更稳妥的做法是只把真正影响页面输出的参数保留在 key 里:

# 在 http 块定义,提取不带参数的请求路径或者归一化参数
fastcgi_cache_key "$scheme$request_method$host$uri";

如果站点有分页需求,比如 /category/linux?page=2,此时 $uri 都是 /category/linux,如果直接忽略参数会导致第 2 页命中第 1 页的缓存。

对于这种情况,可以在 server 块里通过内置变量组合出一个安全的 key:

# 针对需要分页的参数进行捕获,其它无用追踪参数丢弃
set $cache_page "";
if ($arg_page ~* "^[0-9]+$") {
    set $cache_page "_p$arg_page";
}

# 组合后的 key:协议 + 请求方法 + 域名 + 路径 + 有效分页标识
fastcgi_cache_key "$scheme$request_method$host$uri$cache_page";

这样不管是 ?utm_source=google、?from=baidu 还是随机乱码参数,它们在 Nginx 眼里都会稳稳命中同一个干净的缓存文件,缓存命中率重回 95% 以上,再猛烈的爬虫遍历也不会把后端打崩。


坑点三:移动端与 PC 端缓存混杂,移动蜘蛛误判排版异常被降权

现在的搜索引擎全面推行移动优先索引。百度搜索里超过 80% 的抓取权重落在移动蜘蛛(Baiduspider-render)头上。

许多站点为了追求更极致的移动端性能,往往由服务端根据 UA 直接吐出两套完全不同的 HTML 模板:后端 PHP 读取 HTTP 请求头里的 User-Agent,如果是手机就渲染移动端模板(更精简的 DOM、适配小屏的图片尺寸、没有右侧边栏);如果是电脑就渲染完整宽屏版。

在这个架构下开启了常规的 fastcgi_cache,灾难立刻接踵而至。

因为你的 fastcgi_cache_key 里根本没有设备区分维度。

试想一下这个交替发生的真实抓取过程:

  1. 凌晨 02:00,Googlebot(PC 端)先爬了某篇文章,Nginx 发现没有缓存,于是向 PHP 索要,PHP 渲染了 1200px 宽的 PC 版 HTML,Nginx 将其存入缓存;
  2. 凌晨 02:05,百度的移动蜘蛛(Baiduspider-mobile)顺着内链爬到了同一个 URL;
  3. Nginx 命中刚才生成的 PC 版缓存,直接甩给百度移动蜘蛛;
  4. 百度移动渲染引擎解析这份 HTML,发现没有适配手机视口的 <meta name="viewport">,首屏文字极小,元素互相重叠遮挡;
  5. 百度搜索资源平台过两天直接发来警告:“页面存在移动浏览体验障碍”,移动搜索排名断崖式跌出前五页。

更搞的是,有时候手机用户先访问,缓存了移动版;电脑访客打开页面,突然发现排版变成了一条狭窄的单列手机布局,右侧边栏全没了,直到缓存过期才会恢复。

怎么排查?

分别用电脑和手机的 User-Agent 去抓同一个页面,对比返回的 HTML 大小和视口标签:

# 模拟移动蜘蛛
curl -s -A "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1 Baiduspider-mobile" \
  https://example.com/post-105.html | grep -i "viewport"

# 模拟 PC 蜘蛛
curl -s -A "Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)" \
  https://example.com/post-105.html | grep -i "viewport"

如果在两次请求里,后一次命中了前一次的 X-Cache-Status: HIT,并且返回的结构一模一样,说明设备端缓存已经彻底混成了一锅粥。

怎么做设备隔离?

服务端如果依赖 UA 输出不同的 HTML,就必须在 Nginx 层把设备指纹引入 cache_key。

首先在 http 块用 map 指令将 User-Agent 归类为 mobile 或 desktop:

map $http_user_agent $device_type {
    default                          "desktop";
    ~*(android|bb\d+|meego).+mobile  "mobile";
    ~*avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge\ |maemo|midp|mmp|mobile.+firefox|netfront|opera\ m(ob|in)i|palm(\ os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows\ ce|xda|xiino "mobile";
    ~*Baiduspider-mobile             "mobile";
}

接着,把 $device_type 拼进 fastcgi_cache_key 中:

fastcgi_cache_key "$scheme$request_method$host$device_type$uri$cache_page";

最后,非常关键的一步(不少人都会遗漏):
一定要在 Nginx 响应头中加上 Vary: User-Agent:

add_header Vary "User-Agent, Accept-Encoding";

Vary: User-Agent 的作用是明确通知百度蜘蛛、Googlebot 以及中间的 CDN 边缘节点:“这个 URL 在不同终端下有不同的内容呈现,请不要跨设备复用缓存”。只有把这个标识立住,搜索引擎才会针对移动和 PC 分别抓取、分别建索引,彻底消除排版错乱惩罚。


坑点四:发布更新后缺少主动 Purge,新文章陷入“孤岛”爬虫迟迟不抓

动态网站最大的优势是实时性:站长在后台一点“发布”,首页最新文章列表、分类列表立刻更新,爬虫一抓首页就能发现新链接并顺流而下完成收录。

但如果配置了整页缓存,比如常规设定的:

fastcgi_cache_valid 200 301 302 12h;

这就意味着首页在接下来的 12 个小时内全是死板的静态文本。

当你下午 2 点发了一篇重磅技术文章,跑到前台自己刷新(因为登录了,绕过了缓存)看得很开心;但搜索引擎爬虫下午 3 点来爬首页,命中的依然是早上 8 点生成的旧缓存文件。新文章的 URL 在首页和分类页里连个影子都找不着。

新文章直接退化成了“孤岛页面(Orphan Page)”。它没有任何从高权重页面流过来的站内入口,爬虫根本无从获知它的存在。直到半天甚至一天后首页缓存自然过期被重新激活,新文章才第一次露头。在讲究时效性的搜索算法里,原本一小时内能收录的内容,被硬生生拖延了一两天。

怎么解决更新同步问题?

有两个思路,视环境复杂度而定:

方案 A:引入 ngx_cache_purge 模块做精准主动刷新(推荐)

如果 Nginx 支持 ngx_cache_purge 模块(很多开源编译版本或运维面板自带),可以配置一个内部专用的 purge 接口:

location ~ /purge(/.*) {
    # 限制只有本机或者受信任的运维 IP 可以调用
    allow 127.0.0.1;
    deny all;

    # 对应清理缓存的 key 规则
    fastcgi_cache_purge CACHE_ZONE "$scheme$request_method$host$device_type$1";
}

在博客后台保存文章的钩子(Hook)里,用一段简单的本地调用:

curl -X PURGE http://127.0.0.1/purge/
curl -X PURGE http://127.0.0.1/purge/category/ops/

只要一发布,就把首页和分类页的缓存瞬时踢出,下一次爬虫来抓取,必然能秒级拿到包含最新文章的列表。

方案 B:对聚合列表页采用微缓存(Microcaching)

如果嫌编译安装 purge 模块麻烦,或者使用的是现成云托管环境,最管用的策略就是动静分离分级缓存:

  • *内容详情页(/post/.html)**:内容相对固定,给长缓存(比如 12 小时到 24 小时);
  • *首页与分类列表页(/、/category/)**:属于高频变化的中枢内链区,绝对不能配大几小时的长缓存,改用 3 到 5 分钟的微缓存(Microcaching)。
location = / {
    # 首页只给 3 分钟的微缓存
    fastcgi_cache_valid 200 3m;
    # 其它 fastcgi 基础配置...
}

location ~* ^/category/ {
    # 分类页给 5 分钟微缓存
    fastcgi_cache_valid 200 5m;
    # 其它 fastcgi 基础配置...
}

location ~* ^/post/ {
    # 详情页放心大胆给 12 小时长缓存
    fastcgi_cache_valid 200 12h;
    # 其它 fastcgi 基础配置...
}

3 分钟的微缓存对服务器来说已经足够挡住 99% 的瞬时并发冲击,把高并发下几百次 MySQL 查询直接压缩成一次;而对搜索引擎爬虫来说,3 分钟的时滞几乎可以忽略不计,新发布的文章很快就能被爬虫发现并顺着内链迅速收录。


避坑配置参考清单

把上述几个硬坑规避完之后,一份生产可用的 Nginx fastcgi_cache 核心配置大概长这样:

# 1. 在 http 块定义缓存路径与设备识别
fastcgi_cache_path /var/run/nginx-cache levels=1:2 keys_zone=WORDPRESS:100m inactive=60m max_size=1g;
fastcgi_cache_key "$scheme$request_method$host$device_type$uri$cache_page";

map $http_user_agent $device_type {
    default                          "desktop";
    ~*Baiduspider-mobile             "mobile";
    ~*(android|iphone|ipad|mobile)   "mobile";
}

server {
    listen 443 ssl http2;
    server_name example.com;

    # 2. 绕过缓存标记逻辑
    set $skip_cache 0;
    if ($request_method = POST) {
        set $skip_cache 1;
    }
    if ($http_cookie ~* "comment_author|wordpress_logged_in|EM_AUTHCOOKIE|PHPSESSID") {
        set $skip_cache 1;
    }

    # 3. 提取有效分页参数,丢弃追踪垃圾参数
    set $cache_page "";
    if ($arg_page ~* "^[0-9]+$") {
        set $cache_page "_p$arg_page";
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

        # 4. 绑定缓存区与状态码有效期
        fastcgi_cache WORDPRESS;
        fastcgi_cache_valid 200 301 302 60m;
        fastcgi_cache_valid 404 1m;

        # 5. 命中与绕过规则
        fastcgi_cache_bypass $skip_cache;
        fastcgi_no_cache $skip_cache;

        # 6. 安全与 SEO 关键防护:忽略并隐藏上游私有头
        fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
        fastcgi_hide_header Set-Cookie;

        # 7. 告知爬虫终端分流并暴露调试状态
        add_header Vary "User-Agent, Accept-Encoding";
        add_header X-Cache-Status $upstream_cache_status;
    }
}

配置改完以后,别忘了先用 nginx -t 测试语法是否合规,再执行 systemctl reload nginx 平滑重载。


整页缓存确实是小机器应对高并发、把 TTFB 从好几百毫秒压到十来毫秒的神器。但只要站点还要从搜索引擎拿自然流量,缓存策略就得跟着爬虫的脾气走。

从防范 Set-Cookie 泄露私有快照,到清洗无序参数保护后端的 PHP-FPM 进程池,再到给移动蜘蛛正确的视口隔离,这些细节稍微疏忽一个,就容易把原本想提升抓取效率的好事变成全站快照混乱的事故。把缓存的边界划清楚了,后端稳稳当当,爬虫抓得顺畅,收录和排名才能真正吃到页面提速的红利。

评论一下?

OωO
取消