openresty通过lua在access_by_lua_block阶段完成composer请求识别与灰度路由,不依赖php;基于ua提取composer/php版本、vendor等字段动态选择后端,并利用shared_dict+redis两级缓存提升性能。

OpenResty 本身不处理 Composer 的 composer.json 解析或包依赖计算,它只负责 HTTP 层的请求改写与路由决策;真正的镜像逻辑(如重写 packagist.org 域名、替换下载 URL)必须在请求进入后端前完成,且灰度路由不能依赖 PHP 运行时——得靠 Lua 在 access_by_lua_block 或 rewrite_by_lua_block 阶段干完。
Composer 请求识别:区分 metadata、download、dist 等不同路径
Composer 客户端发起的请求有明确模式,比如:
-
GET /packages.json(根元数据) -
GET /p/provider-2022%2B.json(provider 文件) -
GET /downloads/example/package/1.0.0/example-package-1.0.0.zip(dist 下载) -
GET /dist/example/package/1.0.0.0/abc123.zip(部分镜像实现用此路径)
这些路径不能靠 location ~ \.json$ 粗暴匹配,否则会漏掉无后缀的 /packages 或带 query 的 /p/provider-latest%2B.json?_t=xxx。建议用 ngx.re.match 提取关键段:
local path = ngx.var.uri
local m = ngx.re.match(path, [[^/p/(provider[^/]+)\.json$]])
if m then
ngx.var.composer_type = "provider"
ngx.var.provider_name = m[1]
end
注意:Composer 会加 _t 时间戳参数防缓存,ngx.var.args 里能取到,但匹配路径时应忽略 query string。
动态改写 Host 和 URI:绕过 packagist.org 直连限制
Composer 默认硬编码 packagist.org,但镜像服务通常用自定义域名(如 packagist.example.com)或内网 IP。改写必须发生在 DNS 解析前,所以不能只改 proxy_pass 地址——得同时伪造 Host 头并重写 URI:
- 用
proxy_set_header Host "packagist.org"欺骗后端服务认为请求来自原站 - 用
rewrite ^/(.*)$ /$1 break保持原始路径,避免因路径偏移导致 404 - 若镜像后端做了路径 rewrite(如把
/p/...映射为/api/provider/...),则需在 Lua 中主动修改ngx.var.uri,而非依赖 Nginx rewrite 指令
错误做法:仅改 proxy_pass http://mirror-backend 却不设 Host 头——后端服务收不到正确 host,可能返回 404 或跳转到原始域名。
灰度路由依据:支持按 Composer CLI 版本、PHP 版本或 vendor 名切流
Composer 请求的 User-Agent 是可靠信号源,格式类似 Composer/2.5.8 (Linux; 6.5.0-xx-generic; PHP 8.2.12),从中可提取:
-
Composer/2\.(\d+)→ 主版本号,用于兼容性灰度(如 v2.6+ 才支持新 dist 格式) -
PHP (\d+\.\d+)→ PHP 小版本,旧版 PHP 可能无法解压新 zip 结构 -
vendor/([^ ]+)→ 若请求头含自定义X-Composer-Vendor,可用于内部团队灰度
示例判断逻辑:
local ua = ngx.var.http_user_agent or ""
local _, _, php_ver = string.find(ua, "PHP (%d+%.%d+)")
if php_ver == "8.1" then
ngx.var.backend = "mirror_php81"
elseif php_ver == "8.2" then
ngx.var.backend = "mirror_php82"
else
ngx.var.backend = "mirror_stable"
end
注意:ngx.var.http_user_agent 是小写 key,Nginx 自动转换了 header 名;不要写成 ngx.var.http_User_Agent。
Redis 白名单 + shared_dict 缓存:避免每次请求都查 Redis
灰度用户 ID 或 token 列表存在 Redis 里是常见做法,但高频请求下直接 redis:get 会造成延迟毛刺。必须分层缓存:
- 先查
ngx.shared.dict(如gray_cache),key 为"ua:"..sha256(ua) - 未命中再查 Redis,成功后写入 shared_dict,TTL 设为 60s(太长会导致灰度开关延迟生效)
- Redis 查询失败时,**必须 fallback 到默认后端**,不能
ngx.exit(500)—— Composer 请求失败会导致整个 install 失败
容易被忽略的点:shared_dict 的容量单位是字节,不是条目数;lua_shared_dict gray_cache 5m 看似够用,但每条缓存含 UA hash + backend name + timestamp,实际撑不了几万条。生产环境建议设为 10m 起步,并监控 shdict_zone_miss 指标。











