Async::HTTP::Client是Ruby生态中基于async生态体系构建的HTTP客户端,依托于事件循环实现了非常高效的并发请求能力。不过它的默认行为是按需创建连接,遇到大量并发请求时会同时打开大量TCP连接。如果没有对最大连接数做限制,轻则触发目标服务器的限流策略,重则耗尽进程的文件描述符,抛出Errno::EMFILE错误。这篇文章围绕如何在Async::HTTP::Client中限制最大连接数展开,先讲清楚连接池的底层机制,再给出几种可直接落地的实现方案。

一、理解Async::HTTP::Client的连接池机制
Async::HTTP::Client在内部维护了一个连接池,默认使用的是Async::HTTP::Protocol::HTTP1或HTTP2协议。连接池的核心逻辑是:当有请求进来时,优先从池中取出空闲连接复用;如果池中没有可用连接,就直接新建一个TCP连接。也就是说,连接的创建是惰性且无上限的,并发多少个请求理论上就可能同时建立多少个连接。
与一些语言中常见的阻塞式连接池不同,async环境下的连接并不是真正的池化等待模型,而更接近于一种连接缓存。当连接空闲后会被放回池中等待复用,但请求高峰期不会排队等待,而是立即开新连接。这一点可以从源码层面验证:客户端内部通过Async::Pool相关的资源管理类来管理连接生命周期,获取资源时如果池空则直接调用构造器创建新实例。
正因为如此,想限制最大连接数,通常不能指望一个现成的max_connections参数一步到位,而是要在请求发起的入口处做并发控制,或者在协议层面利用HTTP/2的多路复用能力减少连接数量。下面分别介绍这几种思路。
二、使用Async::Semaphore限制并发请求数
最直接、最常用的方案是用Async::Semaphore对并发请求做限流。信号量的语义非常适合这个场景:初始化N个许可,每个请求在发起前先获取许可,请求完成后释放许可。这样任意时刻正在执行的请求最多只有N个,对应的并发连接数自然也被限制在N以内。
下面是一个完整的示例,展示如何限制最大并发数为10:
require 'async'
require 'async/http/client'
require 'async/http/endpoint'
endpoint = Async::HTTP::Endpoint.parse('https://ipipp.com')
client = Async::HTTP::Client.new(endpoint)
# 创建一个最大许可数为10的信号量
semaphore = Async::Semaphore.new(10)
Async do
# 模拟100个并发请求
100.times.map do |i|
Async do
# 获取许可,超过10个并发时会在此处挂起等待
semaphore.acquire do
response = client.get("/api/items?page=#{i}")
puts "请求#{i}完成,状态码: #{response.status}"
response.finish
end
end
end
end这段代码的关键点在于semaphore.acquire接受一个块,块执行完毕后许可自动释放,即使块内抛出异常也能保证释放,避免了许可泄漏导致后续请求永久阻塞的问题。所有请求都复用同一个client实例,这样空闲连接会被自动复用,信号量只控制同时在途的请求数量。
这种方案的优点是实现简单、语义清晰,与连接复用机制配合良好;缺点是它限制的是请求数而不是严格的连接数,如果信号量许可数设置得比实际并发小,效果一致,但理解上要清楚二者的对应关系。一般情况下,一个在途请求最多占用一个连接,所以许可数等于最大连接数是成立的。
二、通过Endpoint和协议层控制连接规模
除了在应用层做限流,还可以从协议层面入手。HTTP/2天然支持多路复用,单个连接上可以同时跑大量并发请求。如果目标服务器支持HTTP/2,将客户端强制使用HTTP/2协议,就能用极少的连接承载高并发,从根本上降低连接数量。
require 'async'
require 'async/http/client'
require 'async/http/endpoint'
require 'async/http/protocol/http2'
endpoint = Async::HTTP::Endpoint.parse('https://ipipp.com',
alpn_protocols: Async::HTTP::Protocol::HTTP2.alpn_protocols
)
client = Async::HTTP::Client.new(endpoint,
protocol: Async::HTTP::Protocol::HTTP2
)
Async do
# 即使发起200个并发请求,底层也只维护少量HTTP/2连接
200.times.map do |i|
Async do
response = client.get("/api/data/#{i}")
response.finish
end
end
end需要注意的是,HTTP/2的多路复用虽然减少了连接数,但单个连接上的并发流数量受服务器设置的SETTINGS_MAX_CONCURRENT_STREAMS限制,超过限制的请求会在客户端排队等待。因此严格来说这并没有消除并发上限,只是把连接层面的上限转移到了流层面,由客户端库自动处理排队。
此外,还可以给Endpoint设置连接相关的参数,例如调整retries、超时时间等,避免连接异常时疯狂重连。在弱网环境下,合理的超时配合信号量限流,能显著降低连接风暴的风险。
三、封装可复用的受限连接池客户端
在实际项目中,建议把限流逻辑封装成独立的类,统一管理client生命周期和并发控制,避免信号量散落在各处。下面给出一个封装示例:
require 'async'
require 'async/http/client'
require 'async/http/endpoint'
class PooledHttpClient
def initialize(base_url, max_connections: 10)
@endpoint = Async::HTTP::Endpoint.parse(base_url)
@client = Async::HTTP::Client.new(@endpoint)
@semaphore = Async::Semaphore.new(max_connections)
end
# 通用请求方法,内部自动做并发控制
def request(method, path, headers: {}, body: nil)
@semaphore.acquire do
response = @client.send(method, path, headers, body)
# 注意:这里读取完body后再返回,确保连接能被正常复用
status = response.status
data = response.read
[status, data]
end
end
def close
@client.close
end
end
# 使用示例
Async do
http = PooledHttpClient.new('https://ipipp.com', max_connections: 5)
results = 50.times.map do |i|
Async do
http.request(:get, "/api/items/#{i}")
end
end.map(&:wait)
ensure
http&.close
end这个封装有几个细节值得注意。第一,务必调用response.read或response.finish把响应体读完,否则连接无法回到可复用状态,池中的连接会不断失效,实际新建的连接数反而会上升。第二,client实例必须全局复用,千万不要每个请求新建一个client,那样连接池就完全失去了意义。第三,在ensure块中关闭client,保证程序退出时连接被正确清理。
关于最大连接数应该设置成多少,一般建议参考两个维度:一是进程允许的文件描述符上限(可用ulimit -n查看),需要为每个连接预留一个描述符;二是目标服务器的承载能力,许多公共服务对单IP的并发连接有限制,设置过大会触发限流甚至封禁。保守起见,从10到20起步,结合压测逐步调整是比较稳妥的做法。
四、监控与验证连接数是否真的被限制住了
做了限流之后,还需要验证配置是否生效。最简单的办法是在请求前后统计系统的TCP连接数。Linux下可以借助lsof或读取/proc/net/tcp来观察,也可以在代码中周期性打印连接状态:
Async do
monitor = Async do |task|
loop do
# 每秒统计一次与目标主机的ESTABLISHED连接数
count = `lsof -nP -iTCP -sTCP:ESTABLISHED | grep ipipp.com | wc -l`.to_i
puts "当前活跃连接数: #{count}"
task.sleep 1
end
end
# 发起大批量请求...
monitor.stop
end压测时如果观察到连接数峰值稳定在设定值附近,说明限流生效;如果连接数持续增长超过许可数,通常是响应体没有读完、或者误用了多个client实例导致的,需要回到上面的封装检查代码。
总结一下,Async::HTTP::Client本身不提供现成的最大连接数参数,但通过Async::Semaphore做请求级限流、利用HTTP/2多路复用降低连接需求、再加上良好的封装和监控,完全可以实现可控的连接池行为。在生产环境中,把这三者结合起来使用,配合合理的超时与重试策略,就能让高并发HTTP请求既快又稳。
Ruby Async::HTTP::Client连接池最大连接数修改时间:2026-09-02 19:03:16