4

そのため、サーバーを介してサードパーティの URL から要求元のクライアントに巨大なファイルをストリーミングしたい状況を実験しています。

これまでのところ、次のように、"eachable" レスポンス ボディの標準的な Rack の慣例に従って、Curb または Net::HTTP でこれを実装しようとしました。

class StreamBody
  ...
  def each
    some_http_library.on_body do | body_chunk |
      yield(body_chunk)
    end
  end
end

ただし、このシステムの CPU 使用率を 40% 未満にすることはできません (私の MacBook Air では)。Goliath で同じことをしようとすると、em-synchrony を使用して (Goliath ページでアドバイスされているように)、CPU 使用率を約 25% の CPU に下げることができますが、ヘッダーをフラッシュすることはできません。要求しているクライアントでストリーミング ダウンロードが「ハング」し、指定したヘッダーに関係なく、応答全体がクライアントに送信されるとヘッダーが表示されます。

これは、Ruby が驚くほどうまくいかず、代わりに世界の go と nodejs に目を向けなければならないケースの 1 つであると考えるのは正しいでしょうか?

比較すると、現在、CURL から PHP 出力ストリームへの 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_nonblockIO.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
4

1 に答える 1

1

答えはありませんでしたが、最終的に解決策を見つけることができました。毎日テラバイト単位のデータを送り込んでいるため、これは非常に成功しています。主な成分は次のとおりです。

  • パトロンを HTTP クライアントとして使用します。答えの下に選択肢を説明します
  • 堅牢なスレッド Web サーバー (Puma など)
  • sendfile ジェム

このようなものを Ruby で構築しようとする主な問題は、私が文字列チャーンと呼んでいるものです。基本的に、VM での文字列の割り当ては無料ではありません。write()大量のデータをプッシュする場合、アップストリーム ソースから受信したデータのチャンクごとに Ruby 文字列を割り当てることになります。また、そのチャンク全体をソケットに割り当てることができない場合は、文字列を割り当てることになります。 TCP 経由で接続されたクライアント。そのため、私たちが試みたすべてのアプローチの中で、文字列のチャーンを回避できるソリューションを見つけることができませんでした-Patronに出くわす前に.

結局のところ、Patron は、ユーザー空間でファイルへの直接書き込みを許可する唯一の Ruby HTTP クライアントです。これは、プルするデータに ruby​​ String を割り当てることなく、HTTP 経由で一部のデータをダウンロードできることを意味します。Patron には、FILE*libCURL コールバックを使用して、ポインターを開き、そのポインターに直接書き込む関数があります。これは、Ruby GVL のロックが解除されているときに発生します。これは、すべてが C レベルに折りたたまれているためです。実際には、これは「プル」段階では、応答本文を格納するために Ruby ヒープに何も割り当てられないことを意味します。

他の広く使用されている CURL バインディング ライブラリである curb には、その機能がないことに注意してください。Ruby 文字列をヒープに割り当てて生成するため、目的に反します。

次のステップは、そのコンテンツを TCP ソケットに提供することです。繰り返しますが、それには 3 つの方法があります。

  • ダウンロードしたファイルから Ruby ヒープにデータを読み込み、ソケットに書き込みます
  • Rubyヒープを回避して、ソケット書き込みを実行するシンCシムを作成します
  • syscall を使用しsendfile()て、ファイルからソケットへの操作をカーネル空間で実行し、ユーザー空間を完全に回避します。

いずれにしても、TCP ソケットを取得する必要があります。そのため、Rack ハイジャックの完全または部分的なサポートが必要です (サポートがあるかどうかについては、Web サーバーのドキュメントを確認してください)。

3番目のオプションを使用することにしました。sendfileは Unicorn と Rainbows の作者によるすばらしい宝石であり、まさにそれを実現します。Ruby File オブジェクトとTCPSocket. 繰り返しますが、ヒープに何も読み取る必要はありません。したがって、最終的に、これが私たちが行ったアプローチです(疑似コードっぽい、エッジケースを処理しません):

# 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

これにより、非常に小さな CPU 負荷と非常に小さなヒープ プレッシャーで、イベントを発生させずに複数の接続を処理できます。何百人ものユーザーにサービスを提供しているボックスが、約 2% の CPU を使用しているのを定期的に確認しています。そして、Ruby GC は満足しています。基本的に、この実装で唯一気に入らない点は、MRI によって課せられるスレッドあたり 8MB の RAM オーバーヘッドです。ただし、これを回避するには、イベント サーバー (大量のスパゲッティ コード) に切り替えるか、独自の IO リアクターを記述して、多数の接続をより小さなスレッドのサルボに多重化する必要があります。多くの時間。

うまくいけば、これは誰かを助けるでしょう。

于 2016-05-16T16:12:17.650 に答える