双 11 前夜的流量突袭,本质上是一场成本与意志的消耗战。凌晨一点半,服务器监控的告警短信像催命符一样响个不停,带宽曲线直接拉成 90 度角往上飙。我那个刚有点起色的在线 PDF 转换工具站,成了别人眼里的肥肉。对方用的手法很脏,不是爬虫,是直接用脚本伪造 Referer,模拟正常用户点击,调用核心转换 API。目的很简单,用海量垃圾请求挤爆我的服务器和第三方 API 额度,让我在流量高峰前直接瘫痪,账单爆炸。
这已经不是第一次了。上个月用 Python 写了个简单的频率限制中间件,放在 Flask 应用层,结果被对方用分布式 IP 池轻松绕过。应用层的限制太容易被探知和针对了,而且消耗的是我宝贵的应用服务器资源。这次我决定把战场前移,推到网络的最前沿——Nginx。
查日志,分析攻击模式。对方请求的 User-Agent 很杂,但有几个关键特征:Referer 集中来自几个垃圾站,请求的间隔时间极其均匀(毫秒级),而且只针对 `/api/v1/convert` 这个端点。在应用层看来,它们像“正常用户”,但在网络层,这种机器般的规律性就是最大的破绽。
我放弃了纯 IP 限制的思路,那会误伤正常用户。我要的是“智能”识别。Nginx 的 Lua 模块成了我的武器。核心逻辑不复杂,但要在 Nginx 的上下文中写出来,得抠细节。我建了一个共享内存字典,用来在 Nginx 的 Worker 之间同步状态。Key 不是 IP,而是“IP + 特定请求特征”的哈希值。具体来说,我提取了请求头中几个容易被伪造但组合起来就有概率特征的字段:User-Agent 的一部分、Referer 的域名,以及请求路径。
Lua 脚本的逻辑是这样的:当一个请求到达 `/api/v1/convert` 时,脚本计算它的特征哈希,然后在共享字典里查这个哈希出现的频率。如果单位时间内(比如 10 秒)超过阈值,比如 15 次,就直接在 Nginx 层返回 429(Too Many Requests),请求根本到不了后端 Flask。同时,把这个哈希加入一个“慢速冷却”列表,接下来的请求不仅被拒,还会被故意延迟处理几毫秒再返回错误,进一步消耗攻击方的资源。
真正的难点在于共享字典的清理和阈值的动态调整。我不能让这个字典无限膨胀,所以写了个后台的定时器函数(用 `ngx.timer.at` 实现),每 5 分钟扫描一次,清理掉超过冷却时间的记录。阈值也不是固定的,我让它和服务器整体负载挂钩:当服务器负载超过 70%,自动收紧阈值;凌晨低峰期,则适当放宽,避免影响偶尔爆发的真实用户。
部署脚本,重启 Nginx。看着监控面板上那条嚣张的带宽曲线,在几分钟内被硬生生地压平,然后稳定在一个正常的基线水平。告警停了。世界安静了。
那种亢奋感是生理性的,手有点抖,不是因为咖啡,是因为一种极致的、技术上的控制感。你躲在代码和配置文件的后面,用逻辑和算法构建了一道隐形的墙。你能感觉到墙那边机械的、不知疲倦的撞击,但你的墙纹丝不动。这种较量,比谈下一个客户、管好一个刺头员工,要纯粹一万倍。管理让人心累,是和人性的混沌作战;而这是和机器的确定性作战,你的代码赢了,就是赢了。
团队里的小孩第二天问我,昨晚服务器是不是被攻击了,怎么突然好了。我说,加了点规则。他们不会知道,在双 11 所有人盯着购物车的时候,我在和一场看不见的流量战争对线。这种独狼式的、在底层解决问题的快感,是组建公司后日益稀缺的东西。你大部分时间在开会、对账、安抚情绪,技术手感都钝了。但昨晚,那个 2016 年死磕爬虫和反爬、对流量机制着迷的独狼黑客,好像短暂地回来了一下。
只是亢奋过后,是更深的疲惫。我知道,对方可能会调整策略,这只是一轮交锋。技术攻防没有尽头。但至少今晚,我的服务器,能睡个好觉。














