1. API限流缺失的灾难性后果
上周排查一个线上故障时,发现某PHP接口被恶意刷了200万次请求,数据库CPU直接飙到100%。这让我想起五年前刚入行时犯的错——以为用Nginx挡在前端就万事大吉,结果凌晨三点被运维电话叫醒处理服务雪崩。今天我们就来聊聊PHP API不限流会引发的连锁反应,以及如何用Redis+Lua打造工业级限流方案。
当API接口没有限流措施时,最直接的表现为突发流量会像洪水般冲垮服务。我见过最典型的案例是:
- 某促销活动接口被脚本重复调用,导致MySQL连接数爆满
- 爬虫高频抓取数据接口,服务器带宽被打满
- 内部系统间调用失控,形成循环依赖的死亡链
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心防护机制解析
2.1 漏桶算法实战实现
在PHP中实现漏桶算法,我推荐使用Redis的INCR命令配合EXPIRE。这是经过多个千万级项目验证的稳定方案:
php复制$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$key = 'api_limit:' . $_SERVER['REMOTE_ADDR'];
$limit = 100; // 每分钟100次
$ttl = 60;
if ($redis->exists($key)) {
$count = $redis->incr($key);
if ($count > $limit) {
header('HTTP/1.1 429 Too Many Requests');
exit;
}
} else {
$redis->multi();
$redis->incr($key);
$redis->expire($key, $ttl);
$redis->exec();
}
关键技巧:使用Redis事务确保计数器和过期时间同步设置,避免键永久存活
2.2 分布式环境下的限流挑战
当你的PHP应用部署在多台服务器时,简单的Redis计数会出现偏差。去年我们电商大促时就踩过这个坑——因为Nginx轮询负载均衡,每台服务器的计数器不同步,实际放行了3倍于预期的请求量。
解决方案是采用Redis+Lua脚本原子化操作:
lua复制local key = KEYS[1]
local limit = tonumber(ARGV[1])
local ttl = tonumber(ARGV[2])
local current = redis.call('GET', key)
if current and tonumber(current) > limit then
return 0
else
redis.call('INCR', key)
if current == "1" then
redis.call('EXPIRE', key, ttl)
end
return 1
end
PHP调用示例:
php复制$script = <<<LUA
-- 上面Lua脚本内容
LUA;
$sha = $redis->script('load', $script);
$result = $redis->evalSha($sha, ['api_limit:'.$ip, 100, 60], 1);
3. Nginx层防护配置
在应用层限流之外,我强烈建议在Nginx配置请求限制。这是我们的生产环境配置:
nginx复制limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/m;
server {
location ~ ^/api/ {
limit_req zone=api_limit burst=50 nodelay;
limit_req_status 429;
try_files $uri /index.php$is_args$args;
}
}
这个配置实现了:
- 每个IP每分钟100次基础请求
- 允许突发50个请求(burst)
- 直接拒绝而非延迟处理(nodelay)
- 返回429状态码而非默认的503
4. 异常流量识别与熔断
去年我们遭遇过一次CC攻击,攻击者用10万个代理IP轮流请求。传统的IP限流完全失效,最终是靠以下组合拳解决的:
- 用户行为指纹识别:
php复制$fingerprint = md5($_SERVER['HTTP_USER_AGENT']
. $_SERVER['HTTP_ACCEPT_LANGUAGE']
. $_SERVER['HTTP_ACCEPT_ENCODING']);
- 敏感接口二次验证:
- 高频访问后要求滑动验证码
- 关键业务接口添加GraphQL复杂度限制
- 动态规则调整:
php复制// 根据实时负载自动调整限流阈值
$load = sys_getloadavg()[0];
$dynamic_limit = max(10, 100 - ($load * 20));
5. 监控与告警体系
没有监控的限流就是盲人摸象。这是我们使用的Prometheus监控指标示例:
php复制$registry = new Prometheus\CollectorRegistry(new Prometheus\Storage\Redis());
$requestsCounter = $registry->registerCounter(
'api',
'requests_total',
'Total API requests',
['method', 'endpoint']
);
$limitedCounter = $registry->registerCounter(
'api',
'limited_requests_total',
'Limited API requests',
['method', 'endpoint']
);
// 在限流逻辑中
$requestsCounter->inc(['GET', '/api/user']);
if ($isLimited) {
$limitedCounter->inc(['GET', '/api/user']);
}
配套的Grafana看板要重点关注:
- 请求拒绝率突增报警
- 同一IP的429响应占比
- 接口响应时间P99值
6. 实战中的血泪教训
-
不要依赖PHP会话限制
早期我们尝试用session_start()的锁机制来限流,结果发现:- 会话文件锁导致请求串行化
- 高并发时产生大量
session_start()阻塞 - 最终引发PHP-FPM进程耗尽
-
慎用sleep限流
见过有开发者这样写:php复制if ($requestCount > 100) { sleep(1); }这会导致:
- PHP进程被无用占用
- 快速耗尽FPM的pm.max_children
- 引发连锁雪崩效应
-
API网关的陷阱
使用Kong等网关时要注意:- 插件执行顺序影响限流效果
- 集群模式下redis连接数爆炸
- 配置热更新可能导致规则失效
真正可靠的限流应该像洋葱一样分层实现:
- 边缘网络层(Cloudflare/WAF)
- 负载均衡层(Nginx)
- 应用中间件层(PHP)
- 业务逻辑层(关键操作队列)
最后分享一个诊断脚本,当怀疑被攻击时可以快速分析Nginx日志:
bash复制awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -n 20
限流配置永远没有银弹,需要根据业务特性持续调优。我们现在的做法是每周做一次压力测试,观察限流阈值是否需要调整,毕竟业务增长的速度常常超出预期。
