Php 机架轻量级流式HTTP代理(Ruby CPU light HTTP客户端库)

Php 机架轻量级流式HTTP代理(Ruby CPU light HTTP客户端库),php,ruby,asynchronous,rack,evented-io,Php,Ruby,Asynchronous,Rack,Evented Io,因此,我正在尝试一种情况,即我希望通过服务器将巨大的文件从第三方URL流式传输到请求的客户端 到目前为止,我已经尝试通过遵守“eachable”响应主体的标准机架实践,使用URB或Net::HTTP实现了这一点,如下所示: class StreamBody ... def each some_http_library.on_body do | body_chunk | yield(body_chunk) end end end 然而,我不能让这个系统使用

因此,我正在尝试一种情况,即我希望通过服务器将巨大的文件从第三方URL流式传输到请求的客户端

到目前为止,我已经尝试通过遵守“eachable”响应主体的标准机架实践,使用URB或Net::HTTP实现了这一点,如下所示:

class StreamBody
  ...
  def each
    some_http_library.on_body do | body_chunk |
      yield(body_chunk)
    end
  end
end
然而,我不能让这个系统使用少于40%的CPU(在我的MacBookAir上)。如果我尝试对Goliath做同样的操作,使用em同步(如Goliath页面上建议的),我可以将CPU使用率降低到大约25%的CPU,但是我无法刷新标头。我的流式下载“挂起”在请求的客户机中,当整个响应发送到客户机时,无论我提供什么头,头都会显示出来

我是否正确地认为这是Ruby非常糟糕的一种情况,而我不得不求助于世界上的能手和能手

相比之下,我们目前使用的是从CURL到PHP输出流的PHP流,这只需要很少的CPU开销

或者有没有上游代理解决方案,我可以要求处理我的东西?问题是——我想在整个函数体被发送到套接字之后可靠地调用Ruby函数,而nginx代理之类的东西对我来说是做不到的

更新:我尝试过为HTTP客户机做一个简单的基准测试,看起来大部分CPU使用的都是HTTP客户机LIB。RubyHTTP客户机有一些基准测试,但它们是基于响应接收时间的,而CPU使用率从未提及。在我的测试中,我执行了HTTP流式下载,将结果写入
/dev/null
,并获得了一致的30-40%的CPU使用率,这与通过任何机架处理程序流式传输时的CPU使用率大致匹配

更新:事实证明,大多数机架处理程序(Unicorn等)在响应体上使用write()循环,当无法足够快地写入响应时,可能会进入繁忙等待(CPU负载高)。这可以通过使用
rack.jackit
和使用
write\u nonblock
a
IO写入输出套接字来减轻。选择
(令人惊讶的是,服务器本身不会这样做)

lambda do|插座|
开始
机架|响应|主体。每个do |块|
开始
字节\写入=套接字。写入\非块(块)
#如果只能部分写入,请确保在下一次执行重试
#与剩余部分的迭代
如果写入的字节数小于chunk.bytesize
chunk=chunk[bytes\u write..-1]
升起错误号::EINTR
结束
rescue IO::WaitWritable,Errno::EINTR#输出套接字已饱和。
IO.select(nil,[socket])#然后让我们等待套接字再次可写
再试一次,我们就出发。。。
rescue Errno::EPIPE#在客户端中止连接时发生
返回
结束
结束
确保
socket.close错误
机架响应机身。如果机架响应机身。响应机身,则关闭(:关闭)
结束
结束

没有答案,但最终我们找到了解决方案。它非常成功,因为我们每天都在通过它传输数TB的数据。以下是关键要素:

  • 用户作为HTTP客户端。我会在答案下面解释这个选择
  • 健壮的线程Web服务器(如Puma)
  • 发送文件宝石
想要用Ruby构建这样的东西的主要问题是我称之为字符串搅动的东西。基本上,在VM中分配字符串不是免费的。在推送大量数据时,最终会为从上游源接收到的每个数据块分配一个Ruby字符串,如果无法
write()
将整个数据块分配到表示通过TCP连接的客户端的套接字,也可能会分配字符串。所以,在我们尝试过的所有方法中,我们都无法找到一个解决方案,让我们避免字符串搅动——在我们遇到赞助人之前,也就是说

事实证明,Patron是唯一一个允许在用户空间中直接写入文件的Ruby HTTP客户端。这意味着您可以通过HTTP下载一些数据,而无需为所提取的数据分配ruby字符串。用户有一个函数,可以打开
文件*
指针,并使用libCURL回调直接写入该指针。这发生在Ruby GVL解锁时,因为所有内容都被折叠到C级别。实际上,这意味着在“拉”阶段,Ruby堆中不会分配任何东西来存储响应体

请注意,另一个广泛使用的CURL绑定库curb没有这个特性——它将在堆上分配Ruby字符串并将它们提供给您,这与此目的背道而驰

下一步是将该内容提供给TCP套接字。正如它所发生的一样——同样——有三种方法可以做到这一点

  • 从下载到Ruby堆的文件中读取数据,并将其写入套接字
  • 编写一个薄的C垫片,为您执行套接字写入,避免Ruby堆
  • 使用
    sendfile()
无论哪种方式,您都需要使用TCP套接字,因此您需要获得完全或部分机架劫持支持(验证您的Web服务器文档是否有)

我们决定采用第三种选择
sendfile
是Unicorn和Rainbows作者的一颗神奇的宝石,它实现了这一点——给它一个Ruby File对象和
TCPSocket
,它将要求内核绕过尽可能多的机器将文件发送到套接字。同样,您不必将任何内容读入堆中。最后,我们采用了一种方法(伪代码ish,不处理边缘情况):

这允许我们为m服务
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
# Use Tempfile to allocate a unique file name
tf = Tempfile.new('chunk')

# Download a part of the file using the Range header 
Patron::Session.new.get_file(the_url, tf.path, {'Range' => '..-..'})

# Use the blocking sendfile call (for demo purposes, you can also send in chunks).
# Note that non-blocking sendfile() is broken on OSX
socket.sendfile(file, start_reading_at=0, send_bytes=tf.size)

# Make sure to get rid of the file
tf.close; tf.unlink