1. 项目概述:为什么我们需要lua-resty-http?
在OpenResty的生态里,发起HTTP请求是再常见不过的需求了。无论是作为API网关去调用后端微服务,还是作为反向代理去聚合多个上游接口,甚至是实现一个简单的爬虫或Webhook推送,都离不开HTTP客户端。你可能会问,OpenResty不是基于Nginx的吗,直接用Nginx的proxy_pass不就行了?确实,对于简单的反向代理,proxy_pass是首选。但当你需要动态决定请求的目标、在请求前后执行复杂的Lua逻辑、处理响应体、或者实现请求的流水线化时,原生的Nginx指令就显得力不从心了。
这时候,Lua代码的灵活性和强大就体现出来了。OpenResty提供了ngx.socket.tcp和ngx.socket.ssl这样的cosocket接口,理论上你可以用它们从零开始拼装一个HTTP客户端。但相信我,除非是教学目的,否则没人愿意每次都去手动处理HTTP协议的状态机、分块传输编码、连接复用这些繁琐的细节。这正是lua-resty-http库存在的意义——它封装了底层的cosocket操作,提供了一个符合直觉、功能完备的HTTP客户端,让你能像在Node.js或Python里使用axios、requests一样,在OpenResty中优雅地发起HTTP/HTTPS请求。
这个库由ledgetech团队维护,在GitHub上拥有超过2k的Star,是OpenResty社区里最成熟、最受信赖的HTTP客户端库,没有之一。它支持HTTP/1.0和1.1、完整的SSL/TLS(包括mTLS)、流式响应体处理、连接池与长连接、请求流水线、以及通过代理发起请求等高级特性。今天,我们就来深入探讨如何利用lua-resty-http模块,特别是针对HTTPS请求,构建高效、可靠的网络通信逻辑。我会结合我多年在网关和微服务架构中的实战经验,分享从基础使用到高级调优,再到避坑指南的全套心得。
2. 核心设计:两种模式与连接管理哲学
lua-resty-http的设计非常清晰,它提供了两种不同抽象层次的API,对应着两种编程模式:简单单次请求和流式请求。理解这两种模式及其背后的连接管理哲学,是高效使用这个库的关键。
2.1 简单单次请求模式:request_uri
这种模式对应httpc:request_uri(uri, params)方法。它的特点是“一站式”服务:你给它一个完整的URI和一些参数,它帮你完成DNS解析、建立连接(包括TCP和SSL握手)、发送请求、接收响应(包括状态行、头部和整个响应体),最后根据情况关闭连接或将其放回连接池。整个过程中,响应体会被完整地读取并缓存在内存中,以字符串形式返回。
适用场景:请求响应体不大(比如几KB到几百KB),且逻辑简单,不需要精细控制连接生命周期和内存使用的场景。例如,调用一个返回JSON配置的服务、验证一个Token、或者发送一个简单的监控心跳。
核心优势:代码极其简洁,无需关心连接的建立和关闭细节。库内部会智能地处理HTTP协议语义(比如根据Connection头决定是否保持连接),并利用OpenResty的连接池。
local httpc = require(“resty.http”).new() local res, err = httpc:request_uri(“https://api.example.com/v1/user", { method = “GET”, headers = { [“Authorization”] = “Bearer your_token_here”, [“Content-Type”] = “application/json”, }, ssl_verify = false, -- 注意:生产环境不建议关闭验证 keepalive_timeout = 60000, -- 连接池中保持60秒 }) if not res then ngx.log(ngx.ERR, “请求失败: “, err) return end -- res.body 已经是完整的响应字符串 local user_data = cjson.decode(res.body)注意:
request_uri会缓冲整个响应体。如果响应体非常大(比如几十MB的文件下载),会瞬间消耗大量内存,可能导致OpenResty工作进程内存暴涨甚至被OOM Killer终止。务必评估响应体大小。
2.2 流式请求模式:connect+request
这种模式将连接建立、请求发送、响应读取、连接关闭/复用这几个步骤解耦,给了开发者最大的控制权。你需要先调用httpc:connect(options)建立连接(包含可能的代理连接和SSL握手),然后使用httpc:request(params)发送请求,再通过res.body_reader迭代器流式读取响应体,最后手动调用httpc:set_keepalive()或httpc:close()管理连接。
适用场景:
- 处理大响应体:比如下载文件、处理流式API(如Server-Sent Events的初步读取)、或者你只想读取响应头部就丢弃Body。
- 请求流水线:在同一连接上连续发送多个请求(HTTP Pipelining),减少TCP和SSL握手的开销。
- 精细的超时控制:可以为连接、发送、接收分别设置不同的超时时间。
- 复用SSL会话:通过
ssl_session参数复用TLS会话,加速后续的HTTPS连接建立。
核心优势:内存使用可控、性能潜力更高(连接复用、流水线)、灵活性极强。
local httpc = require(“resty.http”).new() httpc:set_timeouts(5000, 5000, 30000) -- 连接5s,发送5s,读取30s -- 1. 建立连接(包含SSL握手) local ok, err, ssl_session = httpc:connect({ scheme = “https”, host = “api.example.com”, port = 443, ssl_verify = true, }) if not ok then ngx.log(ngx.ERR, “连接失败: “, err) return end -- 2. 发送请求 local res, err = httpc:request({ path = “/v1/large_file”, method = “GET”, headers = { [“Host”] = “api.example.com”, }, }) if not res then ngx.log(ngx.ERR, “请求失败: “, err) httpc:close() return end -- 3. 流式读取响应体 if res.status == 200 then local reader = res.body_reader local buffer_size = 16384 -- 16KB缓冲区 repeat local chunk, err = reader(buffer_size) if err then ngx.log(ngx.ERR, “读取响应体错误: “, err) break end if chunk then -- 处理每一块数据,例如写入临时文件或进行流式处理 ngx.ctx.file_handle:write(chunk) end until not chunk -- chunk为nil时表示读取完毕 else -- 非200响应,直接读取整个body(通常不大) local body, err = res:read_body() ngx.log(ngx.WARN, “请求失败,状态码:”, res.status, “, 响应:”, body) end -- 4. 将连接放回连接池以供复用 local ok, err = httpc:set_keepalive() if not ok then ngx.log(ngx.ERR, “设置keepalive失败: “, err) -- 可以选择关闭连接 httpc:close() end连接池的智能管理:这是lua-resty-http(特别是新版connect方法)的一大亮点。传统的connect(host, port)方法生成的连接池键名只包含host:port。这在纯HTTP下没问题,但在HTTPS或通过代理访问时就有大问题。想象一下,你通过一个HTTP代理访问https://bank.com和https://evil.com,如果它们都解析到同一个代理服务器的IP:Port,那么连接池可能会错误地复用这两个完全不同的安全会话!新版connect(options)方法通过将scheme(协议)、host、port、proxy_opts(代理配置)等信息共同哈希生成一个唯一的连接池名称,从根本上杜绝了这种不安全复用,这也是官方推荐使用新API的重要原因。
3. HTTPS请求实战:证书、验证与性能调优
发起一个简单的HTTPS请求不难,但要确保其安全、高效、稳定,就需要关注很多细节。下面我们围绕HTTPS,拆解几个核心实战要点。
3.1 SSL证书验证:安全的第一道防线
默认情况下,lua-resty-http的ssl_verify参数是true,这意味着它会尝试验证对端服务器的证书。验证失败,连接就会中止。这是保证通信安全、防止中间人攻击的关键。
常见错误与排查: 你在热词里看到的unexpected status 404 not found: unknown error, url: https://api.deepseek.com...这种错误,表面是404,但根源可能是SSL握手失败。OpenResty的Lua环境可能没有配置正确的CA证书包。
解决方案:
- 指定CA证书路径:在Nginx配置或
connect参数中,通过lua_ssl_trusted_certificate指令或ssl_ca_cert参数(如果底层cosocket支持)指定受信任的CA证书文件。最可靠的方法是在nginx.conf的http块中全局配置:
对于http { lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt; # 系统CA证书路径 lua_ssl_verify_depth 3; # ... 其他配置 }lua-resty-http,你可以在connect的options里设置(需要OpenResty版本支持):local ok, err = httpc:connect({ scheme = “https”, host = “api.example.com”, port = 443, ssl_verify = true, ssl_server_name = “api.example.com”, -- SNI扩展,必须与证书域名匹配 -- 注意:lua-resty-http 当前版本connect参数未直接暴露ssl_ca_cert, -- 依赖全局的`lua_ssl_trusted_certificate`配置。 }) - 生产环境切勿关闭验证:为了方便测试,很多人会设置
ssl_verify = false。这在生产环境是极其危险的,因为它使你的应用完全暴露在中间人攻击之下。测试环境可以使用自签证书,并让OpenResty信任你的自签CA。
自签证书场景:如果你在内网使用自签证书,需要将自签证书的CA公钥添加到信任链中。可以将CA公钥内容追加到lua_ssl_trusted_certificate指向的文件末尾。
3.2 超时设置:避免僵尸请求
网络是不稳定的。没有合理的超时设置,一个慢速的上游服务可能会拖死你所有的OpenResty工作进程。lua-resty-http提供了两套超时设置API。
set_timeout(time):设置统一的超时,适用于所有socket操作(连接、发送、接收)。简单,但不够精细。set_timeouts(connect_timeout, send_timeout, read_timeout):强烈推荐使用这个。你可以为不同阶段设置不同的超时。
connect_timeout:TCP连接和SSL握手阶段。对于HTTPS,SSL握手可能较慢,尤其是在会话不复用的情况下,可以设置稍长,如3000(3秒)。send_timeout:发送请求头和数据的时间。通常很快,2000(2秒)足够。read_timeout:从服务器读取响应数据(包括头部和body)的时间。这个值需要根据业务调整。如果下载大文件,需要设置得很长(如300000,5分钟);如果只是调用一个快速API,5000(5秒)即可。
local httpc = require(“resty.http”).new() -- 精细化的超时控制 httpc:set_timeouts(3000, 2000, 10000) -- 连接3秒,发送2秒,读取10秒 local res, err = httpc:request_uri(“https://api.slow.com/data”, { method = “GET”, }) if not res then -- 错误可能是超时,也可能是网络错误 ngx.log(ngx.ERR, “请求失败,可能原因:”, err) -- 可以根据err信息判断,例如”timeout” if string.find(err, “timeout”) then -- 处理超时逻辑,如重试或返回降级内容 ngx.status = 504 ngx.say(“{“code”: 504, “msg”: “Upstream timeout”}“) return end end3.3 连接复用与性能优化
HTTP/1.1的Keep-Alive和连接池是提升性能的利器。lua-resty-http完美地集成了OpenResty的cosocket连接池。
关键参数:
pool:自定义连接池名称。除非有特殊需求(如按用户隔离连接),否则让库自动生成是最安全的选择(特别是对于HTTPS)。pool_size:连接池的大小。默认是lua_socket_pool_size(通常为30)。这个值决定了每个工作进程对每个目标(scheme://host:port+ 代理等组合)最多保持多少个空闲连接。设置太小,无法充分利用长连接;设置太大,浪费内存。需要根据实际并发度和上游服务情况调整。keepalive_timeout:连接在池中空闲的最大时间(毫秒)。超过这个时间,连接会被关闭。默认是lua_socket_keepalive_timeout。
最佳实践:
- 总是尝试复用连接:在流式请求模式的最后,使用
set_keepalive()而不是close()。 - 理解
set_keepalive的返回值:它可能返回1(成功放入池中)、nil(出错)、或者2(因为协议要求必须关闭连接,如HTTP/1.0请求没有Connection: keep-alive头)。对于返回值2,这是正常行为,不是错误。 - 监控连接池:可以通过OpenResty的
ngx.status模块或定时打印日志来观察连接池的使用情况,避免连接泄漏(即创建了连接但未关闭或放回池中)。
连接泄漏排查:如果你发现工作进程的socket数量持续增长,很可能是连接泄漏。确保在所有代码路径(正常、异常、提前返回)上都正确调用了set_keepalive或close。可以使用get_reused_times()方法检查当前连接被复用的次数,如果一直是0,说明都是新建连接,可能复用没生效。
4. 高级特性与实战场景解析
掌握了基础,我们来看看lua-resty-http的一些高级玩法,这些特性能在特定场景下发挥巨大威力。
4.1 通过HTTP/HTTPS代理发起请求
在企业网络环境中,出站流量经常需要经过代理服务器。lua-resty-http通过set_proxy_options方法提供了原生支持。
local httpc = require(“resty.http”).new() -- 设置代理配置 httpc:set_proxy_options({ http_proxy = “http://proxy.corp.com:8080”, -- HTTP请求使用的代理 https_proxy = “http://proxy.corp.com:8080”, -- HTTPS请求使用的代理(CONNECT隧道) https_proxy_authorization = “Basic ” .. ngx.encode_base64(“user:pass”), -- 代理认证 no_proxy = “localhost,127.0.0.1,internal.api.corp.com”, -- 绕过代理的列表 }) -- 后续的connect和request会自动使用代理 local ok, err = httpc:connect({ scheme = “https”, host = “external.api.com”, port = 443, }) -- 此时,实际连接会先建立到 proxy.corp.com:8080,然后发起CONNECT请求建立到external.api.com:443的隧道重要提示:代理功能仅在使用了新的
connect(options)方法时才生效。使用旧的connect(host, port)签名无法自动启用代理。
4.2 请求流水线
HTTP Pipelining允许在同一个连接上,在前一个请求的响应被完全接收之前,就发送下一个请求。这可以减少延迟,特别是在高延迟网络上。lua-resty-http通过request_pipeline方法支持。
local httpc = require(“resty.http”).new() local ok, err = httpc:connect({ scheme = “https”, host = “api.example.com”, port = 443, }) if not ok then ngx.log(ngx.ERR, “连接失败:”, err) return end -- 一次性发送三个请求 local responses, err = httpc:request_pipeline({ { path = “/api/users/1”, method = “GET” }, { path = “/api/users/2”, method = “GET” }, { path = “/api/users/3”, method = “GET” }, }) if not responses then ngx.log(ngx.ERR, “流水线请求失败:”, err) httpc:close() return end -- 必须按顺序读取响应 for i, res in ipairs(responses) do if not res.status then ngx.log(ngx.ERR, “第”, i, “个请求读取失败,可能socket错误”) break -- 后续响应也无法读取了 end ngx.log(ngx.INFO, “用户”, i, “状态码:”, res.status) local body, err = res:read_body() -- 读取整个body -- 处理body... end httpc:set_keepalive()注意事项:
- 服务器必须支持HTTP Pipelining。虽然HTTP/1.1规范支持,但很多服务器和代理实现得不好,或者默认关闭。使用前需要测试。
- 响应必须严格按照请求发送的顺序读取。你不能跳过第一个响应直接读第二个。
- 如果某个请求的响应体没有被完整读取(比如只读了头部),那么该连接将处于不可用状态,无法用于后续请求。务必确保每个响应的body都被消费掉。
4.3 流式上传与下载
我们之前展示了流式下载。流式上传同样重要,特别是上传大文件时,可以避免在内存中组装整个请求体。
-- 假设我们有一个生成大文件内容的迭代器函数 local function large_file_reader(chunk_size) local offset = 0 local total_size = 1024 * 1024 * 100 -- 100MB return function() if offset >= total_size then return nil end local chunk = string.rep(“A”, math.min(chunk_size, total_size - offset)) offset = offset + #chunk return chunk end end local httpc = require(“resty.http”).new() httpc:set_timeouts(5000, 30000, 30000) -- 发送和读取超时设长 local ok, err = httpc:connect({ scheme = “https”, host = “upload.example.com”, port = 443, }) if not ok then ngx.log(ngx.ERR, “连接失败:”, err) return end -- 使用迭代器作为body,实现流式上传 local res, err = httpc:request({ path = “/upload”, method = “POST”, headers = { [“Host”] = “upload.example.com”, [“Content-Type”] = “application/octet-stream”, [“Transfer-Encoding”] = “chunked”, -- 必须使用分块编码 }, body = large_file_reader(8192), -- 8KB每块 }) if not res then ngx.log(ngx.ERR, “上传请求失败:”, err) httpc:close() return end -- 读取响应... local body, err = res:read_body() httpc:set_keepalive()关键点:当使用迭代器作为body时,必须设置Transfer-Encoding: chunked请求头,因为库无法预先知道body的总长度。服务器需要支持分块传输编码。
4.4 处理重定向
lua-resty-http本身不自动处理HTTP重定向(3xx状态码)。这是一个设计上的考量,因为自动重定向可能带来循环重定向、POST请求被错误地转为GET等问题。你需要手动实现。
local function request_with_redirect(httpc, uri, params, max_redirects) max_redirects = max_redirects or 5 local redirect_count = 0 while redirect_count < max_redirects do local res, err = httpc:request_uri(uri, params) if not res then return nil, err end if res.status >= 300 and res.status < 400 then local location = res.headers[“Location”] or res.headers[“location”] if not location then return nil, “redirect with no location header” end -- 处理相对路径和绝对路径 if not string.find(location, “^https?://”) then -- 简单的相对路径处理,实际应用可能需要更复杂的URL拼接 local parsed = httpc:parse_uri(uri) if string.find(location, “^/”) then location = parsed[1] .. “://” .. parsed[2] .. location else -- 处理相对路径,这里简化处理 return nil, “relative redirect not fully supported” end end ngx.log(ngx.INFO, “Following redirect to: “, location) uri = location -- 对于POST等非幂等方法,重定向时应转为GET(根据RFC) if params.method and params.method ~= “GET” and params.method ~= “HEAD” then if res.status == 301 or res.status == 302 or res.status == 303 then params.method = “GET” params.body = nil -- 移除请求体 if params.headers then params.headers[“Content-Length”] = nil end end end redirect_count = redirect_count + 1 else -- 不是重定向,返回结果 return res end end return nil, “too many redirects” end -- 使用示例 local httpc = require(“resty.http”).new() local res, err = request_with_redirect(httpc, “https://short.url/abc”, {method = “GET”})5. 生产环境避坑指南与性能调优
纸上得来终觉浅,绝知此事要躬行。下面这些坑,都是我或者我的团队在实际项目中踩过的,希望能帮你绕过去。
5.1 DNS解析问题
OpenResty的cosocket默认是阻塞式解析DNS的,并且不缓存DNS结果。这意味着每次connect(如果host是域名)都可能触发一次DNS查询,在高并发下会对DNS服务器造成压力,并增加请求延迟。
解决方案:
- 使用OpenResty的
resty.dns.resolver模块:这是一个非阻塞的DNS解析器,支持缓存。你可以先解析出IP,再用IP去连接。但这增加了代码复杂度,且需要处理DNS TTL过期和多个IP的负载均衡。 - 在Nginx层面配置
resolver指令并开启缓存:这是更推荐的方式。在nginx.conf的http、server或location块中配置:
配置后,cosocket会使用Nginx的异步DNS解析器,并享受缓存。http { resolver 8.8.8.8 114.114.114.114 valid=300s; # 指定DNS服务器,缓存300秒 resolver_timeout 5s; # ... 其他配置 }valid=300s表示缓存有效期300秒。 - 直接使用IP地址:对于内部服务,可以直接配置IP,绕过DNS。但失去了灵活性,且需要自己处理服务发现和负载均衡。
5.2 连接池竞争与“连接风暴”
假设你有一个突发流量场景,瞬间有1000个请求需要访问同一个上游HTTPS服务。如果连接池大小(pool_size)是30,那么前30个请求会创建新连接并放入池中。后面的970个请求,会尝试从池中获取空闲连接。如果前面的请求处理很慢,连接没有及时释放,那么大量请求会等待获取连接,导致排队延迟,甚至超时。
优化策略:
- 合理设置
pool_size:不要盲目设大。需要根据上游服务的并发处理能力和网络状况来定。可以通过压测找到一个平衡点。通常建议设置为每个工作进程允许的最大并发上游请求数。 - 设置
backlog参数:在connect的options中,可以设置backlog。当连接池满且无空闲连接时,新的connect调用会等待,直到有连接释放或超时。backlog指定了等待队列的长度。超过长度,connect会立即返回nil和错误connection pool is full。local ok, err = httpc:connect({ scheme = “https”, host = “api.example.com”, port = 443, pool_size = 50, backlog = 100, -- 等待队列长度 }) - 实现请求队列或降级:在应用层,当检测到上游连接池满或请求超时率升高时,可以实施限流、快速失败或返回缓存数据等降级策略。
5.3 响应头大小限制
OpenResty对单个请求或响应的头部大小有默认限制(通过lua_socket_buffer_size配置,默认是4k或8k)。如果上游服务器返回的响应头非常大(比如Set-Cookie很多),可能会导致头部读取不完整,解析错误。
解决方案:在nginx.conf中适当调大lua_socket_buffer_size。
http { lua_socket_buffer_size 128k; # 根据实际情况调整 # ... 其他配置 }5.4 错误处理与重试机制
网络请求天生不可靠。完善的错误处理和重试机制是保障系统韧性的关键。
常见错误类型:
- 连接错误:
connect失败,错误信息可能是connection refused,timeout,ssl handshake failed等。 - 发送错误:
request失败,可能是网络中断。 - 读取错误:
body_reader或read_body失败,可能是连接被对端重置。 - 协议错误:如无效的HTTP响应。
一个简单的带重试的封装函数:
local function request_with_retry(httpc, uri, params, max_retries) max_retries = max_retries or 3 local last_err for i = 1, max_retries do local res, err = httpc:request_uri(uri, params) if res then return res -- 成功,直接返回 end last_err = err ngx.log(ngx.WARN, “请求失败(尝试”, i, “/”, max_retries, “): “, err) -- 判断错误类型,决定是否重试 -- 例如,连接被拒绝、超时、SSL握手失败可以重试 -- 但像”host not found”这种DNS错误,重试可能没用 if string.find(err, “timeout”) or string.find(err, “connection refused”) or string.find(err, “ssl handshake”) then if i < max_retries then ngx.sleep(0.1 * i) -- 指数退避 end else -- 其他错误,不重试 break end end return nil, “all retries failed: “ .. (last_err or “unknown”) end更高级的策略:可以考虑结合断路器模式(如lua-resty-circuitbreaker),当上游服务失败率达到阈值时,自动熔断,直接返回降级内容,避免雪崩。
5.5 日志与监控
在生产环境,必须对HTTP客户端的调用情况进行监控。
- 日志记录:记录关键信息,如请求耗时、状态码、响应大小。可以使用
ngx.now()计算耗时。local start_time = ngx.now() local res, err = httpc:request_uri(“https://api.example.com”) local end_time = ngx.now() local cost = (end_time - start_time) * 1000 -- 毫秒 ngx.log(ngx.INFO, “上游调用: uri=”, uri, “, status=”, (res and res.status or “nil”), “, cost=”, cost, “ms, err=”, (err or “nil”)) - 指标上报:将耗时、状态码等信息上报到监控系统(如Prometheus)。可以定义几个关键的指标:
upstream_request_duration_seconds(直方图)、upstream_request_total(计数器,按状态码分类)、upstream_request_failed_total(计数器)。 - 链路追踪:在微服务架构中,将OpenResty发起的请求也纳入分布式追踪(如Jaeger、SkyWalking),注入Trace ID。
6. 综合案例:构建一个健壮的上游API调用模块
最后,我们整合以上所有知识点,编写一个用于生产环境的、健壮的上游API调用模块。
-- file: lib/resty/upstream_client.lua local http = require “resty.http” local cjson = require “cjson.safe” local _M = { _VERSION = ‘0.1’ } local mt = { __index = _M } function _M.new(self, opts) opts = opts or {} local client = { timeout_connect = opts.timeout_connect or 3000, timeout_send = opts.timeout_send or 5000, timeout_read = opts.timeout_read or 10000, max_retries = opts.max_retries or 2, keepalive_timeout = opts.keepalive_timeout or 60000, keepalive_pool = opts.keepalive_pool or 30, ssl_verify = opts.ssl_verify, proxy_opts = opts.proxy_opts, } return setmetatable(client, mt) end function _M.request(self, method, url, req_body, req_headers) local httpc = http.new() if not httpc then return nil, “failed to create http client” end -- 设置超时 httpc:set_timeouts(self.timeout_connect, self.timeout_send, self.timeout_read) -- 设置代理(如果有) if self.proxy_opts then httpc:set_proxy_options(self.proxy_opts) end local parsed = httpc:parse_uri(url) if not parsed then return nil, “invalid url: “ .. url end local scheme, host, port, path_with_query = unpack(parsed) local params = { method = method, headers = req_headers or {}, keepalive_timeout = self.keepalive_timeout, keepalive_pool = self.keepalive_pool, } if req_body then if type(req_body) == “table” then params.body = cjson.encode(req_body) params.headers[“Content-Type”] = “application/json” else params.body = req_body end params.headers[“Content-Length”] = #params.body end -- 合并SSL验证配置 local connect_opts = { scheme = scheme, host = host, port = port, ssl_verify = self.ssl_verify, } local last_err for retry = 1, self.max_retries do local res, req_err = httpc:request_uri(url, params) if res then -- 成功,处理响应 local ok, json_body if res.headers[“Content-Type”] and string.find(res.headers[“Content-Type”], “application/json”) then json_body, err = cjson.decode(res.body) if not json_body then ngx.log(ngx.ERR, “failed to decode json response: “, err, “ body: “, string.sub(res.body, 1, 500)) end end return { status = res.status, headers = res.headers, body = res.body, json = json_body, } else last_err = req_err ngx.log(ngx.WARN, string.format(“upstream request failed (retry %d/%d): %s”, retry, self.max_retries, req_err)) -- 判断是否可重试的错误 if self:_should_retry(req_err) and retry < self.max_retries then ngx.sleep(0.05 * (2 ^ (retry - 1))) -- 指数退避 else break end end end return nil, “request failed after retries: “ .. (last_err or “unknown”) end function _M._should_retry(self, err) -- 定义哪些错误需要重试 if not err then return false end if string.find(err, “timeout”) then return true end if string.find(err, “connection refused”) then return true end if string.find(err, “closed”) then return true end -- SSL握手错误可能重试也无用,但可以尝试一次 if string.find(err, “handshake”) and not string.find(err, “certificate”) then return true end return false end return _M使用示例:
local upstream_client = require “resty.upstream_client” local client = upstream_client:new({ timeout_connect = 2000, timeout_read = 8000, max_retries = 1, ssl_verify = false, -- 测试环境 }) local res, err = client:request(“POST”, “https://api.service.com/data”, { key = “value” }, { [“X-Api-Key”] = “your-api-key”, }) if not res then ngx.log(ngx.ERR, “调用上游服务失败: “, err) ngx.status = 502 ngx.say(“{“error”: “upstream_unavailable”}“) return end if res.status >= 200 and res.status < 300 then ngx.say(cjson.encode({ success = true, data = res.json })) else ngx.log(ngx.WARN, “上游服务返回错误: “, res.status, “ body: “, res.body) ngx.status = res.status ngx.say(res.body) end这个模块封装了超时、重试、JSON编解码、错误分类等通用逻辑,可以作为项目中进行上游HTTP调用的基础组件。当然,根据实际需求,你还可以为其增加熔断器、指标上报、更精细的日志等特性。