因此,我正在尝试一种情况,即我想通过我的服务器将大型文件从第三方 URL 流式传输到请求客户端。
到目前为止,我已经尝试使用 Curb 或 Net::HTTP 来实现这一点,方法是遵循“eachable”响应体的标准 Rack 实践,如下所示:
class StreamBody
...
def each
some_http_library.on_body do | body_chunk |
yield(body_chunk)
end
end
end
但是我不能让这个系统使用少于 40% 的 CPU(在我的 MacBook Air 上)。如果我尝试对 Goliath 做同样的事情,使用 em-synchrony(如 Goliath 页面上的建议)我可以将 CPU 使用率降低到大约 25% CPU,但是我无法刷新标题。我的流式下载“挂起”在请求客户端中,并且在将整个响应发送到客户端后,无论我提供什么标头,标头都会显示。
我是否正确地认为这是 Ruby 非常糟糕的情况之一,而我不得不转向世界上的 go 和 nodejs ?
相比之下,我们目前使用 PHP 从 CURL 流式传输到 PHP 输出流,并且 CPU 开销很小。
还是有我可以要求处理我的东西的上游代理解决方案?问题是 - 一旦整个主体被发送到套接字,我想可靠地调用一个 Ruby 函数,而像 nginx 代理这样的东西不会为我做这件事。
更新:我尝试为 HTTP 客户端做一个简单的基准测试,看起来大部分 CPU 使用都是 HTTP 客户端库。Ruby HTTP 客户端有一些基准测试,但它们基于响应接收时间——而从未提及 CPU 使用率。在我的测试中,我执行了一个 HTTP 流式下载,将结果写入/dev/null
,并获得了一致的 30-40% 的 CPU 使用率,这与我通过任何 Rack 处理程序进行流式传输时的 CPU 使用率相匹配。
更新:事实证明,大多数 Rack 处理程序(Unicorn 等)在响应主体上使用 write() 循环,当无法足够快地写入响应时,它可能会进入繁忙等待(CPU 负载高)。这可以通过使用rack.hijack
和写入输出套接字来在write_nonblock
一定程度上缓解IO.select
(惊讶于服务器自己不这样做)。
lambda do |socket|
begin
rack_response_body.each do | chunk |
begin
bytes_written = socket.write_nonblock(chunk)
# If we could write only partially, make sure we do a retry on the next
# iteration with the remaining part
if bytes_written < chunk.bytesize
chunk = chunk[bytes_written..-1]
raise Errno::EINTR
end
rescue IO::WaitWritable, Errno::EINTR # The output socket is saturated.
IO.select(nil, [socket]) # Then let's wait on the socket to be writable again
retry # and off we go...
rescue Errno::EPIPE # Happens when the client aborts the connection
return
end
end
ensure
socket.close rescue IOError
rack_response_body.close if rack_response_body.respond_to?(:close)
end
end