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

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

事情是这样的。

前几天有读者跟我反馈,说博客变慢了。

不是偶尔卡一下,是点开文章以后,文字出来了,图片还要在原地转上好几秒。

我自己试了几次,确实不对劲。更奇怪的是,前不久我才压缩过图片。

一个纯静态博客,为什么会突然慢成这样?

1. CPU 没忙,3M 带宽却满了

我第一反应是服务器扛不住了。

毕竟这就是一台很普通的小服务器,2C2G。真遇上请求高峰,被打得喘不过气也不奇怪。

结果我打开监控一看,CPU 使用率还不到 10%。

机器闲得很。

真正出问题的,是带宽。

我这台服务器的公网出口只有 3 Mbps。正常情况下,机器的流出带宽很低,只有 70.793 Kbit/s

正常情况下,公网流出带宽只有几十 Kbit/s

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

大量请求出现后,公网流出带宽达到 3.289 Mbit/s

70.793 Kbit/s3.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
异常 IP75 个
返回 200475,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 在不到一秒内留下过这样的记录:

1
2
3
4
5
6
10:36:19.436  Chrome/127  /posts/kubernetes/58-dra-p4-my-dra-driver/
10:36:19.955  Chrome/110  /index.xml
10:36:20.029  Chrome/115  /tags/rtk/
10:36:20.075  Chrome/133  /posts/kubernetes/59-kubeclipper-release-1.6.0/
10:36:20.148  Chrome/120  /posts/cloudnative/01-metallb/
10:36:20.321  Chrome/109  /posts/kubernetes/57-dra-p3-workflow/

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

结合回放结果,我设置了两个独立限流区:

1
2
全部请求:120 次/分钟/IP
页面请求: 60 次/分钟/IP

页面请求同时计入两个区。HTML、分类、标签和 RSS 每分钟最多 60 次,CSS、JavaScript 等其他资源只受每分钟 120 次的兜底限制。

日志回放按固定自然分钟统计,后面使用的模块是滑动窗口,两者不会完全一致。这组阈值只对应当时的访问情况,后续还要根据 429 和误伤情况调整。

5. 在 Caddy 中启用 Rate Limit

Caddy v2.11.4 的官方标准二进制不包含 rate_limit,需要通过 xcaddy 构建自定义版本。本文使用的是第三方模块 mholt/caddy-ratelimit

为了以后换服务器还能复现,我固定了 Caddy、xcaddy 和插件提交:

1
2
3
4
5
go install github.com/caddyserver/xcaddy/cmd/xcaddy@v0.4.6

GOOS=linux GOARCH=amd64 CGO_ENABLED=0 \
  xcaddy build v2.11.4 \
  --with github.com/mholt/caddy-ratelimit@5625512f24f6f59d6f64fb3aafe5eecff0b286db

构建完成后确认模块已经包含在二进制中:

1
2
./caddy version
./caddy list-modules | grep -Fx http.handlers.rate_limit

限流配置如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
rate_limit {
  zone blog_all {
    key {remote_host}
    events 120
    window 1m
    ipv6_prefix 64
  }

  zone blog_pages {
    match {
      method GET HEAD
      path / /posts/* /tags/* /categories/* /about/* /index.xml
    }
    key {remote_host}
    events 60
    window 1m
    ipv6_prefix 64
  }

  log_key
}

将配置写入实际使用的 Caddyfile 后,先用新二进制校验配置,再替换当前版本并重启服务:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
install -m 0755 ./caddy /usr/local/bin/caddy.new

/usr/local/bin/caddy.new validate \
  --config /etc/caddy/Caddyfile

cp /usr/local/bin/caddy \
  /usr/local/bin/caddy.$(date +%Y%m%d%H%M%S).bak

mv /usr/local/bin/caddy.new /usr/local/bin/caddy
systemctl restart caddyd
systemctl status caddyd --no-pager

只替换磁盘上的二进制还不够,正在运行的旧进程不会自动加载新模块,因此这里需要重启 Caddy。

配置生效后,我从同一出口 IP 并发请求页面:

1
2
3
4
seq 1 65 | xargs -P 10 -I {} \
  curl --http1.1 -s -o /dev/null -w '%{http_code}\n' \
  https://www.lixueduan.com/posts/ \
  | sort | uniq -c

超过窗口额度后,客户端开始收到 429:

1
2
3
4
HTTP/1.1 429 Too Many Requests
Retry-After: 58
Server: Caddy
Content-Length: 0

看到 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。

指标上线前上线后实际变化
异常地址成功返回 HTML5,297819减少 4,478(-84.5%)
异常地址响应流量112.03 MB22.57 MB减少 89.46 MB(-79.9%)
异常地址返回 2005,664932减少 4,732(-83.5%)
异常地址返回 42901,287新增 1,287 次拦截
异常地址请求5,6842,223减少 3,461(-60.9%)
全站请求6,6373,071减少 3,566(-53.7%)

成功返回的 HTML 和异常响应流量分别下降 84.5% 和 79.9%,限流生效后有 1,287 次请求返回 429。全站请求量也下降了 53.7%,不过里面混着正常访问,只能作为参考。

单个小时还不能代表长期效果,后续仍要观察 429 来源以及搜索引擎是否被误伤。

7. 总结

文章发出来,本来就是给大家看的。有人搜索、订阅,甚至批量抓取文章,我其实都无所谓。我介意的是,持续的高频请求已经影响到正常读者打开文章和图片。

120 次/分钟60 次/分钟 不是通用答案,只是我根据这次日志回放选出的起点,后面还要继续观察是否存在误伤。

限流只是控制访问节奏,让真正来看文章的人能够顺畅打开页面。

0%