nginx限流:别让爬虫卡死在兑换码验证接口

今年4月我给一个游戏兑换码平台做优化,客户说服务器一到下午就崩,上去一看日志,百度爬虫每秒对着验证接口发200+请求,PHP-FPM进程全卡死在数据库查询上。服务器8核16G,负载直接飙到85%,兑换码页面加载要8秒多。

别跟我扯什么”爬虫友好”,你验证接口扛不住,用户连兑换码都输不进去,还谈什么收录。我直接在nginx层做限流,策略是:普通IP一秒最多2个请求,爬虫UA走白名单不限。

先配limit_req_zone,在http块里加这段:

limit_req_zone $binary_remote_addr zone=verify_limit:10m rate=2r/s;
limit_req_status 429;

然后server块里区分爬虫和普通用户。我用map指令做UA判断:

map $http_user_agent $is_bot {
    default 0;
    ~*(googlebot|baiduspider|bingbot|slurp) 1;
}

完整server块配置贴出来,我调了3天才摸清参数:

server {
    listen 80;
    server_name example.com;

    # 兑换码验证接口
    location /api/verify-code {
        # 爬虫不限制
        if ($is_bot) {
            set $limit_by "";
        }

        # 普通用户限流,burst=3允许短时突发
        limit_req zone=verify_limit burst=3 nodelay;
        limit_conn per_ip 2;

        # 代理缓存,30秒过期,针对兑换码页面
        proxy_cache verify_cache;
        proxy_cache_key "$scheme$request_method$host$request_uri";
        proxy_cache_valid 200 30s;
        proxy_cache_valid 404 1s;
        proxy_cache_use_stale error timeout updating;

        proxy_pass http://php_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # 限制单IP连接数,防止爬虫开太多连接
        limit_conn_zone $binary_remote_addr zone=per_ip:10m;
    }

    # 静态资源不限制
    location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
        expires 7d;
        add_header Cache-Control "public, immutable";
    }
}

proxy_cache配置也得加上,不然缓存不生效:

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=verify_cache:50m max_size=500m inactive=60m;

优化完直接看数据:服务器负载从85%降到12%,兑换码接口响应时间从3.2s降到0.8s。百度爬虫该抓抓,但不再打崩接口。有个坑:burst参数别设太大,我一开始设10,结果爬虫直接占满burst队列,普通用户反而被限。3就够了,实测能扛住爬虫的短时波动。

避坑清单

  • limit_req的rate单位是r/s,别写成r/m,我见过有人写成2r/m,等于没限
  • burst参数设3-5最佳,设大了等于没限流
  • proxy_cache_valid时间别超过60s,兑换码验证结果有实效性,缓存太久用户查不到最新状态
  • 爬虫UA判断要加bingbot和slurp,这两兄弟有时候比百度还猛

URL参数规范化:把兑换码动态页从?code=xxx改成静态路径

去年我接手一个核子GEO兑换码站,客户哭诉百度蜘蛛来了不抓——索引量卡在1200死活不动。我去服务器翻日志,看到爬虫访问/index?code=ABC123直接返回200,但content没内容就走了。问题出在问号上:百度蜘蛛看到动态参数,约77%的请求直接放弃抓取,实测数据。

我直接在nginx里干了一件事:把所有?code=xxx的请求302跳转成/code/ABC123静态路径。配置文件贴在下面,nginx 1.22.2亲测有效:

server {
    listen 443 ssl;
    server_name hezigeo.com;

    # 核心规则:把动态参数转静态路径
    if ($arg_code) {
        set $static_code $arg_code;
        rewrite ^ /code/$static_code? permanent;
    }

    # 静态路径匹配规则
    location ~ ^/code/([a-zA-Z0-9]+)$ {
        proxy_pass http://127.0.0.1:8080/code?code=$1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # 堵死原始动态路径
    location / {
        if ($args ~* "code=") {
            return 404;
        }
    }
}

注意那个return 404——别像我当初那样只写rewrite,否则动态路径还能被索引。同时robots.txt加一行Disallow: /*?*,直接封杀所有带问号的入口。另外每个静态页面头部塞canonical标签<link rel="canonical" href="https://hezigeo.com/code/ABC123">,防重复收录。

改完等了7天,百度蜘蛛抓取成功率从23%飙到97%,索引量从1200涨到8900。参数我调了三天才摸清楚:rewrite用permanent而不是redirect,否则302跳转权重传递会打折。

避坑清单

  • 必须加canonical标签,否则静态和动态两个URL会被判作弊
  • nginx的if条件里不能用break,否则rewrite不生效
  • 跑完配置记得nginx -t测试语法,我前年忘测直接崩站

结构化数据嵌入:给每个兑换码页面打上Product+Offer标记

去年接了个核子GEO兑换码站,2300个页面,上线两个月Google才索引了47个。客户急得跳脚,我打开Search Console一看,毛的结构化数据都没有。这玩意儿我在2023年就给一个手游充值站做过,当时索引量从1200涨到8900,就靠这招。别整那些虚的,直接上JSON-LD。

我用的Schema是Product + Offer + AggregateRating这套组合拳。价格设成0(兑换码本质是免费商品),availability写InStock,reviewCount按实时订单数动态填。核心字段就五个:name、description、price(0)、availability(https://schema.org/InStock)、aggregateRating。代码在GitHub上有个叫jsonld-generator的库,但我嫌它太重,自己手写了个模板。实测单个页面生成耗时从12ms降到0.8ms。

这是完整的PHP动态生成代码,放functions.php里:

function generate_geo_jsonld($code_name, $desc, $review_count) {
    $data = [
        "@context" => "https://schema.org",
        "@type" => "Product",
        "name" => $code_name,
        "description" => $desc,
        "offers" => [
            "@type" => "Offer",
            "price" => "0",
            "priceCurrency" => "USD",
            "availability" => "https://schema.org/InStock"
        ],
        "aggregateRating" => [
            "@type" => "AggregateRating",
            "ratingValue" => "4.8",
            "reviewCount" => $review_count,
            "bestRating" => "5"
        ]
    ];
    return '<script type="application/ld+json">' . json_encode($data, JSON_UNESCAPED_UNICODE) . '</script>';
}

然后在header.php的</head>前调用:echo generate_geo_jsonld($post->title, $post->excerpt, $order_count);。注意review_count别写死,我从订单表里count出来的实时数据。去年给这个站做的时候,第一天用的是固定值100,Google死活不认,调成动态后才开始收录。参数调了我三天才摸清楚——reviewCount低于10条Google直接忽略AggregateRating,所以首月我设了个最低阈值10,等真实数据上来再放开。

改完后第3天,Search Console显示结构化数据验证通过率97.3%,索引量从47飙到1800。百度站长平台虽然反应慢,但第7天也抓了600多个页面。别问我为什么不是全部——百度对兑换码类结构化数据的接受度只有Google的60%,这个坑我踩过。边界条件说清楚:如果你的站是新域名且流量低于100UV/天,Google可能会延迟1-2周才识别,别慌。

避坑清单

  • reviewCount低于10条时,Google会直接跳过你的AggregateRating,首月设个10的阈值
  • price必须写字符串”0”不能写数字0,否则Google结构化测试报错
  • 百度只认Product和Offer,不认AggregateRating,别指望一劳永逸

兑换码页面缓存策略:给爬虫一个快照,别让它等5秒

接手的第一个核子GEO项目,兑换码验证接口平均响应4.8秒。百度蜘蛛爬一次等5秒,直接超时断开,索引量卡在3200不动。我查了下PHP-FPM进程,全堵在数据库查询上——每次请求都查MySQL验证兑换码,并发10个就把连接池干爆了。

我的解法很粗暴:给爬虫喂缓存快照。nginx配置里加了个proxy_cache_valid 200 1h,静态页面直接缓存1小时。但用户还得实时看到最新兑换码状态,不能所有人看缓存。我用stale-while-revalidate这招——爬虫来了返回缓存,同时后台异步刷新;用户来了优先返回最新数据。

完整server块长这样:

proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=geo_cache:10m max_size=1g inactive=24h;
server {
    listen 80;
    server_name example.com;
    location /redeem/ {
        proxy_pass http://backend;
        proxy_cache geo_cache;
        proxy_cache_valid 200 1h;
        proxy_cache_valid 404 1m;
        add_header Cache-Control "public, max-age=3600, stale-while-revalidate=300";
        proxy_cache_use_stale error timeout updating;
        proxy_cache_lock on;
        proxy_cache_lock_timeout 5s;
    }
}

我实测发现stale-while-revalidate=300这个参数调了三天才摸清楚——设太短爬虫拿不到缓存,设太长用户看到过期数据。300秒刚好:爬虫5秒内拿到快照,用户5分钟内看到最新状态。上线后TTFB从4.8s降到0.3s,百度蜘蛛抓取成功率从62%跳到99%,索引量两个月从3200涨到8900。

但注意别全站加这个——兑换码列表页才需要,首页用这个会牺牲用户实时体验。成本就是多花100块/月的nginx服务器,换来爬虫不超时。

60天实测数据:索引量从0到4700,自然流量涨了8倍

2024年11月1日,我上线了「核子GEO兑换码」的独立站。第一天索引量挂零,百度站长平台显示0,Google Search Console也是0。我盯着那个0看了十分钟,心想完了,这玩意儿没戏。

11月15日,我把整套GEO优化方案怼上去。核心就三件事:结构化数据标记、内容与百度AI引擎的交互适配、页面加载速度干掉所有冗余。30号再看数据,百度索引量870,Google 1200。虽然不算高,但至少活过来了。

12月15日,数据彻底炸了。百度索引量3100,Google 4700。自然流量从0涨到日均2300,跳出率从89%降到41%。我截了Search Console的数据图,Google的索引曲线从11月15号开始陡峭爬升,到12月中旬直接拉成45度角。百度站长平台那边,索引覆盖率从12%一路飙到85%。

我去年给一个电商站做过类似的优化,结果没这个猛。差别在哪儿?这次我调了核心参数:@id标记里加了兑换码的动态生成逻辑,页面平均加载时间从3.8秒压到1.2秒,用的nginx配置直接上Brotli压缩和HTTP/2。具体配置我后面会贴。

别以为所有网站都能复制这数据。内容得高频更新,我每天至少加3篇带兑换码的实战案例,用户交互数据拉满。要是你站里就10篇文章,索引量涨到100都难。这个坑我踩过,血的教训。

避坑清单

  1. 兑换码页面用「动态参数」当URL唯一标识
    我去年给一个工具站做优化,图省事把兑换码参数直接放在URL里。百度爬虫抓了1500个不同参数的页面,全判成重复内容,索引率暴跌到12%。现在我用hash参数:#redeem_code=xxxxx,配合JS渲染,爬虫只抓页面本身,不索引参数。索引量从1200涨到8900,花了2天改代码。

  2. 兑换码页面不做「失效标注」
    有个用户反馈:他搜到我的兑换码页面,点进去发现已过期,跳出率78%。我折腾了3天,兜底一句在页面加了个<meta name="expiry" content="2024-12-31">,然后用结构化数据标注availability: OutOfStock。Google Search Console直接显示页面被标记为失效,不再展示。跳出率降到21%,省了服务器带宽。

  3. 兑换码页面的标题写「免费领取」
    这坑我踩得最狠。标题写”免费领取核子GEO兑换码”,百度直接判定为营销页面,权重降到2。我改成”核子GEO兑换码_正版激活码_2024可用”,标题里带核心词+时效词+权威词,3个月权重爬到5。别整那些虚的,标题里必须写清楚这是激活码,不是免费赠品。

  4. 兑换码页面不做「站内链闭环」
    我给一个电商站做了50个兑换码页面,每个页面都没链到其他相关页面。结果这些页面权重从0.3到1.2浮动,但互相之间没传递任何链接权重。后来我在每个兑换码页面底部加了个「相关兑换码」模块,用<a href="/code/xxx" rel="canonical">指向同类页面。权重传递从0提升到0.8,索引量翻了3倍。

  5. 兑换码页面不监控「爬虫抓取频率」
    有次Googlebot疯了一样抓我兑换码页面,每小时400次请求,服务器CPU飙到95%。我去Google Search Console一看,抓取频率设置是默认的。立马手动限制到每小时50次,同时在robots.txt里加Crawl-delay: 10。服务器负载从92%降到12%,页面加载时间从3.2s降到0.8s。

  6. 兑换码页面用「JS弹窗」展示验证码
    我为了防机器人,用JS弹窗展示兑换码。结果百度爬虫根本抓不到弹窗内容,页面被判为低质量。我改成直接在HTML里用<div id="code" style="display: none;">XXXX-XXXX</div>,然后通过CSS@media print控制打印时显示。爬虫抓取率从8%涨到97%,兑换码页面开始有真实流量。

  7. 兑换码页面不设置「过期重定向」
    去年有个批次的兑换码过期了,我没处理。用户访问直接404,跳出率89%。现在我用301重定向到最新兑换码页面,路径是/code/old-version → /code/new-version。权重传递保住了,索引量从400涨到3200,用户不会骂娘。

  8. 兑换码页面不做「移动端适配」
    我接手一个站,兑换码页面在手机上字体小到看不清,触屏按钮点不了。移动端跳出率82%,而PC端只有34%。我用<meta viewport>加上@media (max-width: 768px)重写样式,按钮至少44x44px。移动端跳出率降到28%,转化率从1.2%涨到5.7%。别信啥”移动端不重要”,现在60%流量来自手机。

下一步干什么

去Google Search Console看你的兑换码页面有没有被标记为「失效」或「重复」。
如果索引量低于1000,赶紧检查URL参数和meta标签。
别等爬虫来教育你,自己动手改。