LoginSignup
15

More than 5 years have passed since last update.

rackレベルで速度制限をかける方法

Posted at

ダウンロードに時間がかかるときの挙動を確認したいことがあります。

一般的には、クライアントに速度制限アプリをいれたり、nginxのようなサーバでrate_limitのような速度制限を入れるのが一般的です。

本記事ではそういったアプローチではなく、rackレベルで速度制限をかける方法を紹介します。

当然、サーバ側のコードに手をいれるので、サーバアプリケーションの検証目的には使えませんが、クライアント側に速度制限アプリをいれる手間を省いたり、リクエストごとに通信速度を可変にするといったアプリ層ならではの柔軟性があったりします。

rack のレスポンス

railssinatrapadrinoすべてはrackの土台の上にできています。また、webrickpassengerunicornpumarackに対応しています。rackはこのようにWebフレームワークやWebアプリケーションとアプリケーションサーバの間にたち、データのやりとりを標準化しています。

その仕様は非常にシンプルで、ステータスコードと、レスポンスヘッダと、レスポンスボディの3つを返すことが定められています。

ステータスコードはto_iされた場合に100以上の数字になること、レスポンスヘッダはeachできて、keyとvalueをyieldすること、レスポンスボディはeachできて、Stringyieldすることと(一部はしょってますが)決められています。

Hijacking API

ヘッダでrack.で始まるヘッダは特別なヘッダとして定義されています。そのうちの一つにrack.hijack?というヘッダがあります。

これは、Rack 1.5 から導入された Hijacking APIで、このrack.hijack?がtrueなアプリケーションサーバは、Hijacking APIに対応しているということになります。

Hijacking APIでできることは、リクエストをハイジャックすることと、レスポンスをハイジャックする2通りがありますが、今回はダウンロードに時間をかけたいので、レスポンスをハイジャックする方法を説明します。

レスポンスをハイジャックするためには、rack.hijackというレスポンスヘッダを利用します。これは、callに反応できればいいので、通常はProcを使います。ちなみにこのやりかたは partial hijacking というレスポンスボディの返し方だけをハイジャックするやりかたです。

Hijacking API を利用してゆっくりデータを返すrackアプリケーションのコードは以下のようになります。

config.ru
require "rack"
require "thread"

class App
  def call(env)
   headers, body = { "Content-Type" => "text/plain", "Connection" => "close" }, nil

    if env["rack.hijack?"]
      headers["rack.hijack"] = lambda do |io|
        begin
          Thread.new do
            10.times do
              io.write("0123456789\n")
              io.flush
              sleep 1
            end
          end.join
        ensure
          io.close
        end
      end
    else
      body = 10.times.map { "0123456789\n" }
    end

    [ 200, headers, body ]
  end
end

use Rack::ContentLength
run App.new
$ rackup -s thin -p 3000
$ passenger start

thin と passenger でそれぞれサーバを起動し、http://localhost:3000にアクセスすると、thinは、hijackに対応していないため、すぐに結果が返り、passengerは10秒かけて結果がゆっくりかえってきていることがわかります。

Chromeのdev toolsでみると、Content Downloadが10秒かかっていますし、tcpdump とかで、port 3000をみてみると、(localhostをみているときは -i lo0 とか気をつけてくださいね)1秒ごとにchunckっぽいのが返っていることがわかります。

なお、これを最初はpassengerでなくて、pumaで試していたのですが、Broken Pipeと怒られてうまくうごきませんでした。

Rack::BodyProxy を利用する

先ほどのやり方は、pumaで動かなかったり、thinのようにhijackに対応していないアプリケーションサーバがあったりとなかなか未だこなれていない感があります。

そこで、レスポンスボディをラップするやりかたがあります。これがRack::BodyProxyです。なんとなく名前で想像付きそうな感じがしますね。

以下がRack::BodyProxyを利用をしたゆっくりとレスポンスを返すコードになります。

config.ru
require "rack"
require "thread"

class SlowResponse < Rack::BodyProxy
  def each(&block)
    Thread.new do
      @body.each {|body|
        sleep 1
        yield body
      }
    end.join
  end
end

class App
  def call(env)
    headers = { "Content-Type" => "text/plain" }
    body    = SlowResponse.new(10.times.map { "0123456789\n" }) {}

    [ 200, headers, body ]
  end
end

use Rack::ContentLength
run App.new

Rack::Responseを継承したSlowResponseというクラスのeachを上書きして、10秒ごとにbodyをyieldするようにしています。これだと、pumaunicornでも動作しました。

ちなみに、newするときに、渡しているブロックは、クローズの時に呼ばれるので、指定しないとエラーになります。

たぶんこの仕組でthinとかwebrickでも頑張ればできそうな気がするけど、ちょっと時間切れになってしまいました。

まとめ

  • Rack Hijacking API でやるやり方
  • Rack::BodyProxy を使うやり方

今回紹介したやり方はものすごいシンプルなコードなので、実際には何かしらのパラメータを渡して、例えばn[KB/s]の速度にするにはーとか、レスポンス時間がn[sec]にするにはーいうことを計算してやるのでもう少し複雑なコードになるかとは思います。

それでは楽しいスローライフを

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15