背景
云盘业务需要与内部使用的大象 IM 融合,云盘负责大象 IM 的文件流转。大象原有协议中的文件消息包含静态链接,当时还没有鉴权。云盘设计为使用带鉴权信息和时效的动态链接。因此,需要设计一个同时包含鉴权信息并满足大象 IM 协议要求的 URL。
初始方案
方案一是让链接只包含必要的 IM ID 和 Cookie(或 Header)。请求到达服务端后,服务端完成鉴权和文件定位,再重定向到动态链接。该方案存在 App 端兼容问题,图片请求还会增加一次重定向,因此没有采用。
方案二是使用 Java 或 Go 转发流量。链接同样包含必要的 IM ID 和 Cookie(或 Header),请求先到 Java 服务完成鉴权和文件定位,再从 MSS 请求文件,并通过流对流返回客户端。这个方案有两个问题:
- 流量路径为
业务 nginx -> Java 服务 -> MSS nginx -> Swift 集群。扩容时,Java 服务和 Swift 集群都需要处理相应的流量;文件数据在内部多经过一次转发,增加网络开销和响应时间。 - 云盘在非核心链路中尝试过流对流方案,每 3W 次请求约有 1~2 次失败。链路节点增多后,网络、机器内存等任一环节异常都可能导致失败,无法满足 IM 图片消息的稳定性要求。
在 Nginx 完成鉴权和寻址
正常的文件请求路径是:请求 -> MSS nginx -> Swift 集群。方案二增加 Java 服务,只是为了完成鉴权和文件寻址。
根据此前的了解,这两项工作可以在 Nginx 中完成。与基础设施同学沟通后,确认现有 Nginx 支持 Lua 扩展,因此将鉴权和寻址逻辑放到 Lua 中。
最初考虑由 Lua 完成鉴权和寻址后读取文件,将文件内容写入 Nginx 响应。核心代码如下:
//前面是鉴权,寻址文件获得url
local http = require("socket.http");
local body, code = http.request("http://7xi576.com1.z0.glb.clouddn.com/01CE5B62-E4E2-4D84-99EC-BE70DA46269B.png")
if not body then ngx.say("获取不到文件");return; end
ngx.print(body);Lua 读取文件仍不如 Nginx 的 C 模块直接处理文件转发。因此最终使用 Nginx 的 proxy_pass:Lua 在每次请求中完成鉴权和文件寻址,动态设置 proxy_pass 使用的 URL、Content-Type 等参数,由 Nginx 代理文件。
文件流转路径变为:
client request file -> mss nginx(鉴权,文件寻址,代理到文件)-> mss server
该方案保持了云盘原有链路的稳定性和安全性,并兼容大象 IM 协议。核心 Lua 代码如下:
local http = require("socket.http");
local ltn12 = require("ltn12")
local cjson = require ("cjson");
local cjsonObj = cjson.new()
local var = ngx.var;
local request_body = [[ { "objectId" : "test","password" : "123" } ]];
local response_body = { }
local body, code, response_headers = http.request{
url = "http://127.0.0.1:8411/image",
method = "POST",
headers =
{
["Authorization"] = "Maybe you need an Authorization header?",
["Content-Type"] = "application/json",
["Content-Length"] = request_body:len(),
["Cookie"] = var.http_cookie,
},
source = ltn12.source.string(request_body),
sink = ltn12.sink.table(response_body)
}
if type(response_body) == "table" then
local resBodyStr = table.concat(response_body,"");
local resJsonObj = cjsonObj.decode(resBodyStr);
if code == 200 then
if not resJsonObj.error then
var.image_proxy_url = resJsonObj["filepath"];
else
ngx.say(resJsonObj.error.msg);
end
else
ngx.say("获取不到文件");
end
else
ngx.say("Not a table:", type(response_body))
end用内部 rewrite 分离模块
每次动态修改 proxy_pass 会影响 Nginx 性能,也会让模块耦合较紧。因此通过内部 rewrite 分离各模块:
auth、error、not-fount、proxy_pass。
该实现的前提是熟悉 Nginx 模块和扩展开发,并能确认鉴权、寻址和代理之间的数据传递方式。
