必须用 Redis + Lua 做全局包级限流,因单机 limit_req 无法跨节点统计、IP 失真、静态资源不触发 access_by_lua,且需按 package_name/dist_type 而非 IP 限频以保护后端。

单机 limit_req 无法满足 Composer 镜像源的跨节点限频需求——用户通过 CDN 或多台 OpenResty 实例访问同一镜像后端时,IP 计数会分散,导致实际 QPS 翻倍甚至失控。必须用 Redis + Lua 做全局计数,且 key 要基于 Composer 的语义特征(如 package_name、dist_type)而非仅 IP。
为什么不能只用 limit_req_zone 控制 Composer 请求
Composer 客户端发起的请求高度集中于少数路径:/packages.json、/p/[vendor]/[package].json、/d/[hash].tar。这些请求携带明确的包标识,但 $binary_remote_addr 在 CDN 回源或集群部署下已失真;$http_x_forwarded_for 又易伪造。更关键的是,限频目标不是“防爬”,而是保护后端存储和构建服务不被高频下载压垮——比如某个热门包(如 monolog/monolog)被 CI 流水线反复拉取,应按包维度限频,而非按 IP。
- 单机
limit_req对/d/xxx.tar这类静态资源无效:Nginx 默认走static处理阶段,access_by_lua不触发 -
burst和nodelay在高并发下载场景下会造成突发流量堆积,反而加剧后端压力 - 无法区分“合法 CI 下载”和“恶意批量探测”,需结合
User-Agent(如Composer/2.7)与请求路径联合判断
resty.limit.count + Redis 实现包级时间窗口限流
使用 resty.limit.count 替代手写 INCR/EXPIRE,它原生支持原子性计数 + 自动过期,且能返回剩余配额,方便透传给客户端(如加 X-RateLimit-Remaining Header)。关键点在于构造唯一、低基数的限流 key:
- 对
/packages.json:key ="pkg:global",统一限频,防止元数据刷新风暴 - 对
/p/vend/name.json:key ="pkg:" .. ngx.var[1] .. "/" .. ngx.var[2](用rewrite捕获 vendor/name),每包独立限频 - 对
/d/[hash].tar:key ="dist:" .. ngx.var[1],按 dist hash 限频,避免同一 tar 包被重复压制 - 所有 key 设置 TTL = 60s,配合
count = 30,即“每分钟最多 30 次请求”
示例配置片段:
围绕关键发现、作用机制、临床相关性及研究局限性展开讨论。适用于撰写或优化任何生物医学论文的“讨论(Discussion)”部分——包括结果解读、与既往文献关联、阐释意外发现、界定研究局限性,以及撰写结论。当用户输入以下任一指令时也会自动触发该功能: - “write my discussion” - “help me discuss my findings” - “how do I compare to prior studies” - “write the limitations par
lua_shared_dict pkg_limit_store 10m;
location ~ ^/p/([^/]+)/([^/]+)\.json$ {
access_by_lua_block {
local limit = require "resty.limit.count".new("pkg_limit_store", 30, 60)
local key = "pkg:" .. ngx.var[1] .. "/" .. ngx.var[2]
local delay, err = limit:incoming(key, true)
if not delay then
if err == "rejected" then
ngx.status = 429
ngx.header["X-RateLimit-Limit"] = "30"
ngx.header["X-RateLimit-Window"] = "60"
ngx.exit(429)
end
end
}
}
绕过静态文件限流陷阱:用 content_by_lua 补位
/d/xxx.tar 类请求默认由 ngx_http_static_module 处理,access_by_lua 不生效。必须显式接管响应逻辑,否则限流形同虚设:
- 去掉
location /d/ { root /data/mirror; }这类纯静态配置 - 改用
location ~ ^/d/(\w+)\.tar$,在content_by_lua_block中:- 先执行限流检查(同上,key =
"dist:" .. ngx.var[1]) - 检查通过后,用
ngx.exec("@mirror_dist")跳转到内部命名 location - 该命名 location 再用
alias或root返回真实文件
- 先执行限流检查(同上,key =
- 注意:Lua 中调用
ngx.exec是内部重定向,不产生 HTTP 跳转,客户端无感知
Redis 连接失败时的降级策略必须显式编码
OpenResty 的 resty.limit.* 模块在 Redis 不可用时默认 fallback 到内存字典(lua_shared_dict),但该字典是**单机隔离**的,无法解决跨节点问题。你得主动控制降级行为:
- 初始化 limit 对象时传入
redis_timeout = 500和redis_pool_size = 20,避免连接卡死 - 捕获
limit:incoming()返回的err,若为"failed to connect to redis"或"redis timeout",则改用本地limit_reqzone(如zone=local_fallback:1m rate=5r/s) - 切勿依赖模块默认 fallback——它不会告诉你正在降级,线上出问题时难以定位
真正难的不是写 Lua,而是想清楚:哪些请求必须强限频(如元数据接口),哪些可以弱限频(如已缓存的 dist 文件),以及 Redis 故障时,你愿为可用性牺牲多少一致性。这三者没对齐,再漂亮的代码也扛不住 Composer 客户端的真实流量。

















