我的博客,被 47 万次请求刷慢了

事情是这样的。
前几天有读者跟我反馈,说博客变慢了。
不是偶尔卡一下,是点开文章以后,文字出来了,图片还要在原地转上好几秒。
我自己试了几次,确实不对劲。更奇怪的是,前不久我才压缩过图片。
一个纯静态博客,为什么会突然慢成这样?
1. CPU 没忙,3M 带宽却满了
我第一反应是服务器扛不住了。
毕竟这就是一台很普通的小服务器,2C2G。真遇上请求高峰,被打得喘不过气也不奇怪。
结果我打开监控一看,CPU 使用率还不到 10%。
机器闲得很。
真正出问题的,是带宽。
我这台服务器的公网出口只有 3 Mbps。正常情况下,机器的流出带宽很低,只有 70.793 Kbit/s。

但被大量请求刷起来以后,曲线完全变了。把粒度缩到 1 分钟,公网流出带宽直接冲到 3.289 Mbit/s,贴着 3M 上限跑。

从 70.793 Kbit/s 到 3.289 Mbit/s,相差超过 46 倍。
Caddy 返回静态页面几乎不费 CPU,所以没有收到报警信息。但出口一旦被占满,正常读者再打开一张图片,就只能在后面排队。
顺着带宽曲线往下查,我发现 Caddy Access Log 最近轮转得比平时快了不少。
我一开始还没太当回事。博客嘛,被搜索引擎和 RSS 阅读器抓一抓很正常。
直到我把 27 天的轮转日志全部合到一起,才发现里面有一批访问频率异常的请求。
2. 27 天日志里,到底藏了什么
这份日志快照实际覆盖 27.46 天,统计结果如下:
| 指标 | 数值 |
|---|---|
| 全站请求 | 1,132,456 |
| 异常请求 | 477,314 |
| 异常请求占比 | 42.15% |
| 异常响应流量 | 9.00 GB |
| 异常 IP | 75 个 |
| 返回 200 | 475,806 次 |
47 万是累计总量,短时间内的请求频率更值得看。
| 行为 | 观测结果 |
|---|---|
| 最高一天 | 25,936 次 |
| 最高一小时 | 6,864 次,占该小时全站请求 85.98% |
| 最高一分钟 | 627 次,全部来自同一个 IP |
| 最快 100 次 | 6.281 秒 |
| 最快 500 次 | 43.412 秒 |
| 最长连续突发 | 7 分 28 秒内请求 3,539 次 |
那段最长突发里,相邻请求没有一次停顿超过 1 秒。这段时间共切换了 45 种 User-Agent,成功请求文章页面 3,308 次。
从文章覆盖范围看,这些请求也不是偶尔访问几篇。
当前 sitemap.xml 中有 278 篇文章。这批 IP 访问了全部 278 篇,仅对这些文章就请求了 394,112 次。按篇数粗略折算,相当于把整个文章库读取了 1,418 遍。
这就离谱😂
单篇文章被读取的中位数是 1,163 次,152 篇文章超过 1,000 次,最高一篇达到 5,551 次。
不过,请求多并不能直接说明这些访问不正常。
搜索引擎、RSS 阅读器,甚至某个读者连续打开很多文章,都可能制造高峰。还需要继续看这些请求的具体特征。
3. 从日志识别异常抓取
先看 IP。
27 天里一共出现了 75 个相关地址,单个 IP 通常只活跃 1~6 天,随后换成同一地址空间里的另一个。
再看 User-Agent。同一个 IP 在不到一秒内留下过这样的记录:
| |
Chrome 100~144 一共 45 个版本全部出现,每个版本的请求量都在 9,441~10,045 次之间。排除每个 IP 的第一条记录后,97.77% 的相邻请求都会更换 User-Agent。
正常浏览器不会请求一次就换一个 Chrome 版本。
再看请求目标。这批地址主要请求 RSS 和文章 HTML,几乎不加载文章图片。
4. 从 IP 黑名单转向请求频率限流
我第一反应就是封 IP。
但对方 27 天换了 75 个 IP,逐个封禁只能短期止血,批量封禁又容易误伤正常用户。
继续判断请求来自谁没有太大意义,我只需要限制访问频率。
问题变成了,每分钟允许多少次,既能拦住抓取,又不影响正常读者?
为了避免阈值设得太紧,误伤正常访问,我先拿最近 24 小时的日志做了一次固定自然分钟回放:
| 阈值 | 回放结果 |
|---|---|
| 全部请求 60 次/分钟 | 命中 3 个主要异常 IP 和 1 个额外 IP |
| 全部请求 120 次/分钟 | 只命中 3 个主要异常 IP |
| 页面请求 30 次/分钟 | 命中 3 个主要异常 IP 和 1 个额外 IP |
| 页面请求 60 次/分钟 | 只命中 3 个主要异常 IP |
结合回放结果,我设置了两个独立限流区:
| |
页面请求同时计入两个区。HTML、分类、标签和 RSS 每分钟最多 60 次,CSS、JavaScript 等其他资源只受每分钟 120 次的兜底限制。
日志回放按固定自然分钟统计,后面使用的模块是滑动窗口,两者不会完全一致。这组阈值只对应当时的访问情况,后续还要根据 429 和误伤情况调整。
5. 在 Caddy 中启用 Rate Limit
Caddy v2.11.4 的官方标准二进制不包含 rate_limit,需要通过 xcaddy 构建自定义版本。本文使用的是第三方模块 mholt/caddy-ratelimit。
为了以后换服务器还能复现,我固定了 Caddy、xcaddy 和插件提交:
| |
构建完成后确认模块已经包含在二进制中:
| |
限流配置如下:
| |
将配置写入实际使用的 Caddyfile 后,先用新二进制校验配置,再替换当前版本并重启服务:
| |
只替换磁盘上的二进制还不够,正在运行的旧进程不会自动加载新模块,因此这里需要重启 Caddy。
配置生效后,我从同一出口 IP 并发请求页面:
| |
超过窗口额度后,客户端开始收到 429:
| |
看到 429 后,可以确认限流配置已经生效。
6. 限流上线后的同一时段
限流在 2026-07-16 10:55:50 随新 Caddy 进程正式生效。
前面的云监控截图中,带宽打满发生在 2026-07-15 20:29~21:28。我把这 1 小时作为上线前窗口,再取限流上线后相同钟点的 2026-07-16 20:29~21:28 做对比。
上线后的同一小时内,异常 IP 成功获取的 HTML 减少了 4,478 次,Caddy 少返回了 89.46 MB 响应数据,还有 1,287 次请求被直接挡在 429。
| 指标 | 上线前 | 上线后 | 实际变化 |
|---|---|---|---|
| 异常地址成功返回 HTML | 5,297 | 819 | 减少 4,478(-84.5%) |
| 异常地址响应流量 | 112.03 MB | 22.57 MB | 减少 89.46 MB(-79.9%) |
| 异常地址返回 200 | 5,664 | 932 | 减少 4,732(-83.5%) |
| 异常地址返回 429 | 0 | 1,287 | 新增 1,287 次拦截 |
| 异常地址请求 | 5,684 | 2,223 | 减少 3,461(-60.9%) |
| 全站请求 | 6,637 | 3,071 | 减少 3,566(-53.7%) |
成功返回的 HTML 和异常响应流量分别下降 84.5% 和 79.9%,限流生效后有 1,287 次请求返回 429。全站请求量也下降了 53.7%,不过里面混着正常访问,只能作为参考。
单个小时还不能代表长期效果,后续仍要观察 429 来源以及搜索引擎是否被误伤。
7. 总结
文章发出来,本来就是给大家看的。有人搜索、订阅,甚至批量抓取文章,我其实都无所谓。我介意的是,持续的高频请求已经影响到正常读者打开文章和图片。
120 次/分钟 和 60 次/分钟 不是通用答案,只是我根据这次日志回放选出的起点,后面还要继续观察是否存在误伤。
限流只是控制访问节奏,让真正来看文章的人能够顺畅打开页面。