限速规则拒掉的第4个请求是robots.txt,全站SEO抓取会停12小时
本文目录
- 一条限速规则拒掉的,为什么会是robots.txt?
- 实测:爬虫一轮抓取的第5、第6个请求
- robots.txt拿到503的代价,官方写得很具体
- 想把robots.txt摘出配额,会先撞上一个不存在的指令
- 403、429、503,Google分别怎么算账?
- 用403挡爬虫来“减负”,是完全无效的
- 降速的幅度和范围,比想象的大
- 还有个时间上限
- 有没有不用状态码的限速办法?
- 那大家还在写吗?写给谁?
- nginx默认用哪个码拒绝?它带Retry-After吗?
- 拒绝时带不带Retry-After?实测是不带
- 加了却发不出去:always的那个坑
- Retry-After写什么值,服务器不管
- 现网到底有多少拒绝带了Retry-After?
- 换个出口IP,四分之一的站换个状态码
- 同一个URL、同一个UA,两地对照
- 这意味着什么
- 顺带看到的:谁在拒绝
- 拒绝一次只花百分之二的字节,为什么还是亏?
- 那为什么还是亏
- 拒绝页面会不会被缓存住?
- 按IP和按UA限速,为什么对爬虫几乎没用?
- 按IP:换一个IP就是一份新配额
- 按UA:Googlebot有不止一个UA
- 需要限速的时候,怎么配才不误伤?
- 第一件事:把机器专属端点摘出配额
- 第二件事:状态码和头,一次配对
- 第三件事:把观测补上
- 第四件事:三条判据
- 常见问题解答
- 限速拒绝该用429还是503?
- robots.txt返回429会不会比503好一点?
- 返回503期间,页面的标题和描述会更新吗?
- 为什么我curl自己的站看到Retry-After,日志里的爬虫却像没收到?
- 用403挡住爬虫,能减轻服务器压力吗?
- 限速只配在某个接口上,会影响其他页面吗?
- 怎么确认自己的站有没有误拦搜索引擎?
- burst能避免拒绝吗?
- 权威参考资料
摘要:限速这件事只有一个对外的出口——状态码。保哥把nginx的
limit_req按最常见的抄法配在server级,然后模拟爬虫抓一轮:第4个请求开始被拒,而爬虫那一轮排在第5、第6位的正好是robots.txt和sitemap.xml,两个都吃到503。按Google官方文档的规矩,robots.txt取不到会让它前12小时停止抓取整站。更麻烦的是想把这两个端点摘出来时才发现,limit_req off这条指令根本不存在,配置直接报错。另外实测到:nginx拒绝时默认一个Retry-After都不发;add_header不写always的话,这个头会在200上发出、在429上消失;503一旦带上max-age就会被边缘缓存存住,站恢复了边缘还在发503。
先把一件事说在前面:本文不反对限速。服务器扛不住的时候必须有人踩刹车,这没得商量。
本文要拆的是,踩刹车这个动作在爬虫那边被翻译成了什么——因为你和爬虫之间只有一条信道,就是HTTP响应,而拒绝这件事在这条信道上只能用状态码来表达。这门语言只有几个词,每个词的代价都不一样,而大多数人是在完全不知道代价表的情况下选的词。
一条限速规则拒掉的,为什么会是robots.txt?
从一个最普通的配置说起。搜“nginx防采集”,排在前面的写法基本都是这个形状:
limit_req_zone $binary_remote_addr zone=sz:1m rate=1r/s;
server {
limit_req zone=sz burst=2 nodelay;
...
}写在server块里,整站一把限。这个位置很关键——limit_req写在哪一层,配额就在哪一层共享。写在server级,意味着这个站上所有的东西共用一份配额:HTML页面、图片、CSS、robots.txt、sitemap.xml,全部记在同一个账本上。
实测:爬虫一轮抓取的第5、第6个请求
保哥在服务器上另起了一个nginx实例(端口18448,不碰生产),用上面那份配置,然后按爬虫一轮抓取的典型顺序连着发6个请求,UA用Googlebot:
| 顺序 | 请求 | 状态码 |
|---|---|---|
| 1 | GET / | 200 |
| 2 | GET /deep.html | 200 |
| 3 | GET /deep.html | 200 |
| 4 | GET /deep.html | 503 |
| 5 | GET /robots.txt | 503 |
| 6 | GET /sitemap.xml | 503 |
速率1r/s加burst=2,也就是“瞬时最多放3个”,第4个开始拒。前三个额度被普通页面用掉了,轮到robots.txt的时候账上已经空了。
把access_log打开(加了$limit_req_status变量之后):
200 "GET / HTTP/1.1" lrs=PASSED
200 "GET /deep.html HTTP/1.1" lrs=PASSED
200 "GET /deep.html HTTP/1.1" lrs=PASSED
503 "GET /deep.html HTTP/1.1" lrs=REJECTED
503 "GET /robots.txt HTTP/1.1" lrs=REJECTED
503 "GET /sitemap.xml HTTP/1.1" lrs=REJECTEDrobots.txt拿到503的代价,官方写得很具体
Google在robots.txt规范那份文档里,对robots.txt取不到时的行为分了三段:
- 前12小时:Google stops crawling the site but keeps trying to fetch the robots.txt file.——停止抓取整站;
- 之后30天:用最后一份能用的旧版本,同时继续尝试取新的。原文还补了一句A 503 (service unavailable) error results in fairly frequent retrying;
- 30天之后:如果站点整体是可访问的,就当作没有robots.txt处理。
注意第一段的措辞:不是“这个URL抓不到”,是stops crawling the site。一个几百字节的文本文件取不到,代价是整站停抓半天。
Google在暂停线上业务那份文档里把这条又单独强调了一遍:Don't return a 503 HTTP response status code for the robots.txt file because this blocks all crawling.
这两份文档讲的都是“你主动关站”的场景,而上面那个实验说明了另一件事:你没打算关站,一条限速规则也能把robots.txt打成503,而且触发条件低得离谱——爬虫抓到第4个页面就够了。
想把robots.txt摘出配额,会先撞上一个不存在的指令
知道问题之后,第一反应通常是这样写:
location = /robots.txt { limit_req off; }这条配置过不了nginx -t:
[emerg] invalid parameter "off" in /tmp/d84_nginx2.conf:42limit_req没有off这个开关。它有burst、有nodelay、有delay,就是没有关掉自己的写法。而nginx的指令继承规则是“本层没写才继承上层”,所以你也没法靠“在location里写一个更宽松的limit_req”来绕——那只会换一个zone继续限。
正解藏在limit_req模块文档里一句很不起眼的话:Requests with an empty key value are not accounted.——键值为空的请求不计入。于是做法变成用map把特定路径的键置空:
map $uri $exempt_key {
default $binary_remote_addr;
~^/robots\.txt$ "";
~^/sitemap.*\.xml$ "";
~^/llms\.txt$ "";
}
limit_req_zone $exempt_key zone=sz2:1m rate=1r/s;换成这份配置之后,同样的6个请求:
| 顺序 | 请求 | 状态码 | access_log里的$limit_req_status |
|---|---|---|---|
| 1到3 | 页面 | 200 | PASSED |
| 4 | 页面 | 503 | REJECTED |
| 5 | /robots.txt | 200 | - |
| 6 | /sitemap.xml | 200 | - |
最后两行的-和第1到3行的PASSED是两个不同的意思:PASSED是“进了账本、这次放行”,-是“压根没记账”。前者会消耗后面请求的额度,后者不会。
顺带一提,这个“空键不计数”的技巧在拦AI爬虫又不误伤搜索引擎那套配置里同样管用——只不过那边判的是UA,这边判的是路径,机制是同一个。
403、429、503,Google分别怎么算账?
既然只能用状态码说话,那就得把这几个词的代价表看清楚。Google在HTTP状态码、网络与DNS错误那份文档里把每一档都写明白了,整理成表是这样:
| 状态码 | 对抓取速率 | 对已索引的URL | 官方原话要点 |
|---|---|---|---|
| 403 / 401 | 没有影响 | 从索引中移除 | The 4xx status codes, except 429, have no effect on crawl rate. |
| 404 / 410 | 没有影响 | 从索引中移除 | 与其他4xx同等对待 |
| 429 | 降速 | 先保留,最终丢弃 | treats the 429 status code as a signal that the server is overloaded, and it's considered a server error |
| 500 / 502 / 503 / 504 | 降速 | 先保留,最终丢弃 | already indexed URLs are preserved in the index, but eventually dropped |
这张表里最值得盯的是第一行。
用403挡爬虫来“减负”,是完全无效的
Google在同一份文档里写了一句几乎是点名的话:Don't use 401 and 403 status codes for limiting the crawl rate.——别用401和403来限制抓取速率。
原因就在表里:4xx(429除外)对抓取速率没有任何影响。你返回403,爬虫不会因此少来,它只会认为这个URL不该被索引,然后把它从索引里删掉。你付出了索引,什么也没换回来。
这条特别值得说,因为403是防护类中间件最爱用的码。WAF拦截、IP黑名单、UA黑名单、Bot管理的默认动作,绝大多数是403。而它在抓取速率这件事上的效果是零。
降速的幅度和范围,比想象的大
两条容易被忽略的细则,都在降低Google抓取速率那份文档里:
第一条是幅度。文档说500的情况下The decrease in crawl rate is proportionate to the number of individual URLs that are returning a server error.——降速幅度与出错的URL数量成正比。所以偶发一两个5xx影响很小,而限速规则一旦生效通常是成片触发,出错URL数会瞬间拉高。
第二条是范围,这一条杀伤力最大:
The reduced crawl rate affects the whole hostname of your site (for example, subdomain.example.com), both the crawling of the URLs that return errors, as well as the URLs that return content.
降速作用在整个主机名上,而且明确包括那些返回正常内容的URL。
这句话把很多“精细化限速”的方案直接判了死刑。比如常见的“只给搜索接口限速,其他路径不限”——搜索接口被爬出来的那些参数URL吃到503,降速会连着把你的商品页、文章页一起压下去。分面导航产生的海量参数URL之所以危险,除了浪费额度,还有这一层:它是最容易触发限速的那类流量,而触发之后代价由全站承担。
还有个时间上限
Google对“用错误码换喘息”这件事给了个明确的时间窗:
If you need to urgently reduce the crawl rate for short period of time (for example, a couple of hours, or 1-2 days), then return 500, 503, or 429 HTTP response status code instead of 200.
紧跟着就是警告:
We don't recommend that you do this for a long period of time (meaning, longer than 1-2 days)... if Googlebot observes these status codes on the same URL for multiple days, the URL may be dropped from Google's index.
一两天是官方给的安全窗口。超过之后URL开始掉索引。而限速规则的特点是一旦配上就长期在那儿,它不会像故障那样被人记得去关掉。
有没有不用状态码的限速办法?
看到这里应该会有一个自然的念头:既然状态码这门语言每个词都有代价,能不能换一条路,直接跟爬虫“打个招呼”说慢点来?
有这么一条路,而且历史比429还老:robots.txt里的Crawl-delay。
坏消息在Google的robots.txt规范文档里,一句话带过:
Google supports the following fields (other fields such as crawl-delay aren't supported): user-agent, allow, disallow, sitemap.
支持的字段只有四个,crawl-delay被点名放在括号里当反例。也就是说,唯一一条不靠状态码的限速通道,对最需要限的那个爬虫是关闭的。
那大家还在写吗?写给谁?
保哥把那批站的robots.txt也一并拉了下来,93个能拿到200的,逐份解析:
| 统计项 | 数量 | 占比 |
|---|---|---|
写了Crawl-delay | 24个 | 25.8% |
写了已废弃的Request-rate | 1个 | 1.1% |
写了Sitemap | 82个 | 88.2% |
四分之一的站还在写这个字段。取值分布也挺整齐:10出现34次,1出现16次,剩下的5、4、2、0.5各一两次。
真正有意思的是这些Crawl-delay挂在哪个User-agent段下面:
| 挂在哪个UA段下 | 出现次数 |
|---|---|
| ahrefsbot | 11 |
| ahrefssiteaudit | 10 |
| mj12bot | 9 |
| pinterest / pinterestbot | 13 |
*(所有爬虫) | 7 |
| facebookbot / omgilibot / megaindex / rogerbot / baidu | 各1 |
绝大多数是写给SEO工具爬虫的——Ahrefs、Majestic、Moz这一批。挂在*下的只有7次。
这张表其实在讲一件挺合理的事:大家早就知道Crawl-delay对Google没用,所以只拿它去挡那些真的会读它的爬虫。Ahrefs和Bing确实遵守,Google不遵守。于是这个字段在今天的实际角色,是“SEO工具限速开关”,而不是“抓取速率控制器”。
顺带看了一眼这批robots.txt对AI爬虫的点名情况:GPTBot出现在12.9%的站里,ClaudeBot和PerplexityBot各8.6%,Google-Extended7.5%,CCBot6.5%,Bytespider5.4%。拦AI爬虫的三层选型里robots.txt那一层的实际覆盖率,大概就是这个量级。
结论回到主线:声明这条路对搜索引擎是断的,所以本文剩下的篇幅只能继续在状态码上做文章。
nginx默认用哪个码拒绝?它带Retry-After吗?
先说默认值。limit_req模块文档里写着limit_req_status的默认值是503。也就是说,如果你没显式配过,nginx拒绝超速请求时说的是“服务不可用”,而不是语义上更准确的“你请求太多了”。
在Google的账本里这两个码是等价的(都算服务器错误、都降速),所以这个默认值不算错。但在别的消费者那里区别不小——很多HTTP客户端库对429有专门的退避逻辑,对503没有。
拒绝时带不带Retry-After?实测是不带
RFC 6585定义429的时候写了一句MAY include a Retry-After header indicating how long to wait before making a new request;RFC 9110对503的描述是The server MAY send a Retry-After header field to suggest an appropriate amount of time for the client to wait before retrying。两处都是MAY,不是MUST。
nginx对这个MAY的选择是:不发。实验台上把limit_req打到超限,把响应头里的Retry-After抓出来:
| 配置 | 超限时的状态码 | Retry-After |
|---|---|---|
limit_req zone=rz1;(默认) | 503 | 无 |
limit_req_status 429; | 429 | 无 |
加add_header Retry-After 60 always; | 429 | 60 |
要有这个头,得自己加。这不算意外,nginx一贯的风格就是不替你做决定。
加了却发不出去:always的那个坑
问题出在怎么加。add_header的默认行为是只对一小撮成功类状态码生效,不带always的话,5xx和429上根本不会出现。实验台把两种写法并排跑,同一个zone、同一个速率、同一批请求:
| 写法 | 第1次(200) | 第2次(429) | 第3次(429) | 第4次(429) |
|---|---|---|---|---|
add_header Retry-After 60; | RA=60 | RA=空 | RA=空 | RA=空 |
add_header Retry-After 60 always; | RA=60 | RA=60 | RA=60 | RA=60 |
第一行的行为足够荒诞:Retry-After发给了唯一不需要它的那些请求(成功的200),唯独在需要它的那些请求(被拒的429)上消失。
更糟的是这个错误的自检成本。你写完配置,curl -I一下首页,看到Retry-After: 60好好地在那儿,于是打勾收工。可首页那次请求是200,它走的正是add_header默认生效的那条路。要发现这个问题,你得先把自己限到超速,再去看头——而这恰恰是没人会做的一步。
Retry-After写什么值,服务器不管
顺手测了三种写法,nginx一律原样发出,不做任何校验:
| 配置里写的值 | 响应头里的值 | 是否合法 |
|---|---|---|
120 | Retry-After: 120 | 合法(延迟秒数) |
Wed, 21 Oct 2026 07:28:00 GMT | 原样 | 合法(HTTP日期) |
soon | Retry-After: soon | 不合法,照发 |
RFC 9110第10.2.3节只允许两种形式:HTTP日期或者非负十进制整数秒。第三行那种写法客户端解析不出来,行为等同于没有这个头,而你的配置检查、语法检查、健康检查全都不会拦它。
Google那边的建议倒是很务实:Use the retry-after HTTP header with a best effort date or duration. Use static HTML.——尽力给个值就行,页面用静态HTML。后半句的意思是别让错误页本身再去查一次数据库,那纯属在服务器已经扛不住的时候再补一刀。
现网到底有多少拒绝带了Retry-After?
实验台归实验台,真实世界什么样?保哥对164个电商与SaaS域名的首页各发了一轮请求,只看非200的那些响应里带没带Retry-After。
然后同一批URL、同一个UA、几乎同一时段,从一台机房服务器又跑了一遍。两组结果放一起看:
| 出口位置 | 非200的响应 | 带Retry-After的 | 占比 |
|---|---|---|---|
| 家用宽带 | 65个 | 23个 | 35.4% |
| 机房服务器 | 65个 | 1个 | 1.5% |
差距大得离谱。翻明细就明白了:家宽那侧的23个全部是Cloudflare返回的429,值一律是60,一个不多一个不少;机房那侧唯一的那个,是一个301上挂的Retry-After: 0,属于噪声。
换句话说,Retry-After在今天的公网上基本只是Cloudflare挑战页的一个副产品。自建服务器返回的403、500、301,一个都不带。至于更新一些的RateLimit-Limit、RateLimit-Remaining、RateLimit-Reset这组头,164个站里出现了1个,覆盖率0.6%。
换个出口IP,四分之一的站换个状态码
上面那两组数据摆在一起时,保哥先怀疑的是自己:家宽那侧22个429,会不会是自己把人家打限速了?
机房那一轮正好回答了这个问题——而且答案比“是”或“不是”有意思得多。
同一个URL、同一个UA,两地对照
| 家宽看到 | 机房看到 | 域名数 |
|---|---|---|
| 200 | 200 | 70 |
| 403 | 403 | 23 |
| 429 | 301 | 14 |
| 连不上 | 连不上 | 14 |
| 301 | 301 | 12 |
| 429 | 200 | 8 |
| 200 | 403 | 8 |
| 其他组合 | — | 15 |
两地状态码相同的122个,占74.4%。也就是说,四分之一的站对同一个请求给出了不同的答案,而唯一的变量是请求从哪个IP发出去的。
家宽侧那22个429,在机房侧一个都没复现——14个变成301,8个变成200。机房侧自己的429只有2个,来自另外两个站。反过来,有8个站在家宽给200、在机房给403。
把UA那一维也加进来:家宽侧换成Googlebot UA后状态码改变的比例是15.9%,机房侧只有6.7%。
这意味着什么
两条结论,都挺重要:
第一,你测到的那个429,主要是关于你自己的,不是关于那个站的。它不是速率超限的证据,而是“你这个出口在对方风控模型里的评分”的证据。同一份配置对不同来源给不同答案,本来就是Bot管理产品的正常工作方式。
第二,也是更实用的那条:“我的站有没有误拦搜索引擎”这个问题,从你自己的网络位置是测不出来的。你在办公室curl一下,看到200,什么也证明不了——Googlebot从谷歌的IP段来,和你的出口是两个世界。这件事只有两个可靠的验证路径:从日志侧看(按反向DNS验证过的真Googlebot统计它拿到的状态码分布),或者从Search Console的抓取统计信息里看响应类型分布。拿UA字符串模拟爬虫去测自己的站能验证UA规则那一层,但验证不了IP信誉那一层。
顺带看到的:谁在拒绝
机房那一轮按Server头分组:
| Server | 站数 | 首页200率 | 其中403 |
|---|---|---|---|
| cloudflare | 80 | 43.8% | 22 |
| 没有Server头 | 33 | 27.3% | 5 |
| nginx | 10 | 100.0% | 0 |
| netlify | 6 | 50.0% | 0 |
| vercel | 5 | 60.0% | 0 |
| akamai | 5 | 0.0% | 4 |
| Amazon S3 / tengine | 各2 | 100.0% | 0 |
规律很清楚:越是“把磁盘上的文件按规矩发出去”的那一类,越是照单全收;越是在前面加了一层Bot管理的,拒得越狠。这不是在批评谁——挡住恶意流量本来就是这些产品的价值所在。但它确实意味着,把站放到这类平台后面之后,“谁能拿到我的页面”这个决定权就不完全在你手里了,而托管平台悄悄拦掉爬虫这种事,出问题时监控是不会报警的。
拒绝一次只花百分之二的字节,为什么还是亏?
把拒绝的成本算清楚,有助于理解它为什么是笔亏本买卖。
实验台上量了几种响应的字节数(响应头加正文):
| 响应 | 正文字节 | 响应头字节 | 合计 |
|---|---|---|---|
| 正常页面(200) | 23187 | 234 | 23421 |
| nginx默认503页 | 190 | 170 | 360 |
| nginx默认429页 | 162 | 156 | 318 |
| 自定义维护页(保持503) | 171 | 191 | 362 |
现网那批站也是一样的量级:正常首页过线字节中位数65755,非200响应中位数2085,比值3.17%(家宽那一侧算出来是1.71%)。
而且拒绝还很快。实测nginx拒绝一个请求用0.6毫秒左右,跟发一个静态文件是同一个量级——因为它连磁盘都不用碰。
那为什么还是亏
因为抓取预算有两本账,拒绝在其中一本上几乎免费,在另一本上是全价。
第一本是字节账:压缩没配全会让字节账凭空翻几倍,而条件请求做对了能让没变过的页面只花百来字节。在这本账上,一次503确实只花2%到3%。
第二本是次数账。爬虫这一轮打算抓多少个URL是有数的,你回一个503,这次机会就用掉了——它拿到的不是内容,是一个“稍后再来”。省下的那97%字节,买到的是零。
更要命的是第三笔,前面已经讲过:这次503不只是浪费了一次机会,它还会把整个主机名后续的抓取速率一起压下去,包括那些本来能正常返回的URL。一笔360字节的开销,结算在全站的抓取容量上。
拒绝页面会不会被缓存住?
还有一个更隐蔽的代价:这次拒绝可能不止发生一次。
RFC 9110第15.1节列了默认可以被启发式缓存的状态码:200、203、204、206、300、301、308、404、405、410、414、501。503不在这个名单里,404反而在。所以按规范,一个不带缓存指令的503不该被存下来。
实验台上挂了一层nginx做的边缘缓存,对同一个URL连打3次,看第2、3次是HIT还是MISS:
| 上游响应 | 第1次 | 第2次 | 第3次 |
|---|---|---|---|
| 裸503(无Cache-Control) | MISS | MISS | MISS |
503 + Cache-Control: no-store | MISS | MISS | MISS |
503 + Cache-Control: public, max-age=600 | MISS | HIT | HIT |
裸503,但边缘配了proxy_cache_valid any 5m | MISS | HIT | HIT |
前两行是好消息:默认行为是对的,不带指令的503不会被缓存。
后两行是两条不同的路,通向同一个坑:
- 第三行——很多站会在全站统一加一条
Cache-Control: public, max-age=600,通常是在某个“提速”教程里学的。这条头会跟着503一起发出去,于是这个503被存进边缘节点,十分钟内所有人(包括爬虫)拿到的都是它。 - 第四行——
proxy_cache_valid any 5m这条配置的意思是“任何状态码都缓存5分钟”,它同样是提速教程里的常客,而any这个词把5xx也包括进去了。
两种情况的后果一样:你的服务器早就恢复正常了,边缘节点还在按缓存时长继续发503。你在源站怎么测都是200,用户和爬虫拿到的是503,而这个时间差正好是你配的那个缓存时长。
顺带一个意外的发现:PHP里只要调了session_start(),它会自动塞一组Cache-Control: no-store, no-cache, must-revalidate。如果你的维护页是个PHP脚本并且开了会话,这组头会把上面第三行那个坑意外地堵上。这大概是全文唯一一个默认值帮上忙的地方。
按IP和按UA限速,为什么对爬虫几乎没用?
还有一个方向的浪费:限速规则的分区键。
limit_req_zone的第一个参数就是分区键,教程里的标准写法是$binary_remote_addr,也就是按客户端IP各算一份配额。这个默认选择对付单机采集器很有效,对付搜索引擎爬虫则基本落空。
按IP:换一个IP就是一份新配额
实验台上配了一个按请求头里的伪造IP分区的zone,速率1r/s,然后打两轮:
| 打法 | 12次连打的状态码 |
|---|---|
| 12次都用同一个IP | 200,然后429×11 |
| 12次各用一个不同IP | 200×12 |
这个结果毫不意外,但把它和爬虫的实际形态对上就有意思了:Googlebot从一整段IP池里出来,Bingbot同理,各家AI爬虫更是分散。按IP限速对它们的实际约束,等于把配额乘上了它们的IP数量。
按UA:Googlebot有不止一个UA
那按UA限速呢?实验台把分区键换成$http_user_agent,用Googlebot的桌面版和移动版两个UA交替请求(两组之间留够冷却时间):
桌面UA第1次 → 200
移动UA第1次 → 200
桌面UA第2次 → 429
移动UA第2次 → 429两个UA各自拿到了一份完整的配额。这不是bug,是分区键的定义使然——UA字符串不一样,就是两个不同的键。而Google公开的抓取器UA列表里,光是Googlebot这一族就有好几个变体,再加上AdsBot、Google-InspectionTool这些,按UA限速的实际约束力被同样地稀释了。
结论不是“限速没用”,而是:按IP或按UA分区的限速,真正被限死的是单IP单UA的那批客户端——也就是真实用户和小型爬虫;而你真正想控制的大厂爬虫,恰好是最不受这套分区约束的。这大概能算本文里最尴尬的一条。
需要限速的时候,怎么配才不误伤?
把前面所有实测拧成一份可以直接对着改的清单。
第一件事:把机器专属端点摘出配额
必须摘的是robots.txt——它被拒一次的代价是整站停抓12小时,这个代价和任何限速收益都不成比例。建议一起摘的还有sitemap.xml系列、llms.txt、RSS输出。做法就是前面那个map置空键的写法,别去找limit_req off。
这些端点有个共同特征:只有机器会访问它们,所以它们出问题时没有任何人类会先发现。
第二件事:状态码和头,一次配对
| 要做的 | 怎么配 | 不这么配会怎样 |
|---|---|---|
| 拒绝码用429 | limit_req_status 429; | 默认是503,语义不准,客户端库的退避逻辑对不上 |
| 带Retry-After | add_header Retry-After 60 always; | 不写always就只在200上发,在拒绝时消失 |
| 拒绝页不可缓存 | add_header Cache-Control "no-store" always; | 全站统一的max-age会跟着发出去,被边缘存住 |
| 边缘别缓存5xx | 检查有没有proxy_cache_valid any | any包含5xx,故障会被固化成缓存时长那么久 |
| 拒绝页用静态HTML | 指向一个静态文件 | 动态错误页会在服务器最扛不住的时候再压一次数据库 |
| 状态码别被洗掉 | 检查有没有error_page 503 =200 | 爬虫拿到的是一个正常的、可缓存的“维护中”页面 |
第三件事:把观测补上
默认的combined日志格式看不出限速有没有生效。至少要把$limit_req_status加进log_format,它的取值能直接区分三种情况:PASSED(记账了、放行)、DELAYED(记账了、排了队)、REJECTED(记账了、拒了)、空(没进账本)。
加完之后有个立刻能做的自查:按UA分组统计REJECTED的数量,看看被拒的里面有多少是搜索引擎。这个数字应该是0。不是0的话,前面那些代价你已经在付了。
第四件事:三条判据
- 拒绝码只有429和5xx会降抓取速率,403和404不会——但它们会让URL掉出索引。如果你的目的是“让爬虫少来”,403是最差的选项:没有换来减速,还赔了索引。
- 任何长期挂着的限速规则,都在长期地小幅压低你的抓取容量。官方给的安全窗口是1到2天。限速规则和临时故障不一样,它不会有人记得去关。
- 自检拒绝相关的配置,必须在“已经被拒”的状态下做。正常请求下看到的响应头,和被拒时看到的完全是两套。这条对Retry-After、Cache-Control、自定义错误页全部成立。
保哥给一个做宠物用品的客户处理过一次,现象是Search Console里“robots.txt无法读取”隔三差五冒一次,每次持续不到一小时,运维查过去总是正常的。最后是在access_log里加了$limit_req_status才对上:站上有个按小时跑的图片同步任务,跑起来的时候会把同一个IP的额度吃掉一大半,如果Googlebot恰好在那几分钟来取robots.txt,就被拒了。触发条件太窄,人工复现基本不可能,但它每次触发的代价是按小时算的。
这件事和本文开头那个实验是同一个形状:限速规则不知道自己拒的是谁,也不知道被拒的那个URL在别人的规则里意味着什么。它只认速率。而恰好,那个几百字节的文本文件,是整个抓取流程里唯一一个“取不到就全停”的单点。
常见问题解答
限速拒绝该用429还是503?
在Google那边两者等价——429被明确写成it's considered a server error,和5xx走同一套降速与索引保留逻辑。区别在别的消费者身上:很多HTTP客户端库和AI爬虫对429有专门的退避实现,对503没有。所以推荐limit_req_status 429;,语义准确,行为更可预测。但别指望换个码就能规避降速。
robots.txt返回429会不会比503好一点?
不会。Google对robots.txt的4xx处理有一句Google's crawlers treat all 4xx errors, except 429, as if a valid robots.txt file didn't exist——429被明确排除在“当作不存在”之外,它和5xx一样落入“取不到”那条分支,同样触发前12小时停抓。robots.txt的正确答案是别让它被限速拦到,而不是换个码。
返回503期间,页面的标题和描述会更新吗?
不会。Google在暂停线上业务那份文档里写着,页面返回503时没有办法刷新标题、描述、元数据和结构化数据。所以用503做长时间维护,除了降抓取速率和最终掉索引,还会让搜索结果里的摘要一直停在旧版本上。
为什么我curl自己的站看到Retry-After,日志里的爬虫却像没收到?
八成是add_header没写always。这个指令默认只在一小撮成功类状态码上生效,你curl首页拿到的是200,头正常出现;而被拒的那些请求是429或503,头不会出现。实测同一份配置,200上RA=60、429上RA为空。自检必须在被拒的状态下做。
用403挡住爬虫,能减轻服务器压力吗?
能减轻这一次的压力,但减不了后续的量。Google文档原文是Don't use 401 and 403 status codes for limiting the crawl rate. The 4xx status codes, except 429, have no effect on crawl rate.——403对抓取速率没有影响,爬虫不会因此少来。代价那边倒是实打实:4xx会让URL从索引里被移除。
限速只配在某个接口上,会影响其他页面吗?
会。Google写得很明确:降速affects the whole hostname of your site,而且包括那些返回正常内容的URL。只要出错的URL数量起来了,整个主机名的抓取速率一起降,商品页文章页无一幸免。所以“只限某个路径”这种精细化方案,精细的只是触发条件,代价范围一点也不精细。
怎么确认自己的站有没有误拦搜索引擎?
不能靠从自己的网络位置去测。实测同一批164个站,家宽出口和机房出口看到的状态码有25.6%不一样,其中14个站在家宽是429、在机房是301。可靠的路径只有两条:一是从access_log里按反向DNS验证过的真爬虫统计它拿到的状态码分布,被拒的应该是0;二是看Search Console抓取统计信息里的响应类型分布。用UA字符串在本地模拟只能验证UA规则那一层,验证不了IP信誉那一层。
burst能避免拒绝吗?
能把拒绝换成排队,换不掉代价。不带nodelay时超出速率的请求会被压在队列里等,实测速率1r/s下每个请求要等约0.98秒,而基线是0.6毫秒——状态码全是200,但服务器为这批请求占用的连接时长翻了一千多倍。而抓取容量上限盯的正是这个时长。排队是把明面上的拒绝换成了看不见的降速,这一层展开在服务器变慢时那些不进日志的故障形态里。
权威参考资料
本文标题:《限速规则拒掉的第4个请求是robots.txt,全站SEO抓取会停12小时》
本文链接:https://zhangwenbao.com/nginx-rate-limit-robots-txt-crawl-rate-seo.html
版权声明:本文原创,转载与引用请注明作者与原文链接。许可协议: CC BY 4.0