RubyのNet::HTTPを速くする方法（Keep-Aliveを有効にする）

外部サービスとのインテグレーションをWebAPI経由で行う場合、何かしらのHTTPクライアントを利用しますよね。

細かい通信を繰り返す行う場合は、毎回コネクションを繋げて切ってを繰り返すよりもKeep-Aliveを使った実装にすると速くなります。

この記事ではRubyに標準で備わっている[Net::HTTP](http://docs.ruby-lang.org/ja/2.2.0/library/net=2fhttp.html)でKeep-Aliveを利用する方法をサンプルを使って簡単に説明します（接続先のサーバーでKeep-Aliveが有効になっていることが前提です）。

## HTTPのコネクションをプールする

こんな感じで接続先URLのホスト、ポート、プロトコルをキーにしたコネクションプールをさっくりと準備します。

```rb
class HTTPConnectionPool
  def initialize
    @connections = {}
  end
  def terminate
    @connections.values.each{|connection|
      connection.finish
    }
  end
  def build_http(uri)
    http = Net::HTTP.new(uri.host, uri.port, nil, nil)
    #http.set_debug_output($stderr)
    http.use_ssl = (uri.scheme == 'https')
    http.open_timeout = 1.5
    http.read_timeout = 1.5
    http.keep_alive_timeout = 5 # 気休め
    measure("new connection start ") {
      http.start
    }
    http
  end
  def http(uri)
    key = uri.host + uri.port.to_s + (uri.scheme == 'https').to_s
    @connections[key] = build_http(uri) if @connections[key].nil?
    @connections[key]
  end
end
```

通信の詳細を見たい場合はset_debug_output($stderr)を有効にします。

利用が終わったら#terminateを呼び出してコネクションを閉じます。Net::HTTP.finishは閉じた後に再度閉じようとするとIOErrorが発生します。

```
IOError: HTTP session not yet started
        from /usr/local/lib/ruby/2.2.0/net/http.rb:945:in `finish'
```

## HTTPクライアントの自作

次にHTTPコネクションプールを利用するHTTPクライアントをさっくりと自作します。コンストラクタで使いまわすHTTPコネクションプールを指定します。

```rb
class HTTPClient
  def initialize(uri, pool)
    @uri = URI.parse(uri)
    @http_connection_pool = pool
  end
  def get
    http = @http_connection_pool.http(@uri)
    request = Net::HTTP::Get.new(@uri.path)
    request['Connection'] = 'Keep-Alive'
    http.request(request)
  end
end
```

一応リクエストヘッダにKeep-Aliveを含めておきます。

## 速度に変化があるか測定してみる

Qiitaの記事なので、QiitaのCDNに設置してあるjs/png/cssを連続して取得して速度を比較して見ます。

```rb
require "net/http"
require "uri"

def measure(prefix = "")
  start_time = Time.now
  yield
  finish_time = Time.now
  puts prefix + (finish_time - start_time).to_s + " sec"
end

uris = [
  "http://cdn.qiita.com/assets/application-dbd069219bca837f99ecb3a2943fa1012766b50e59ff3841be447cf43546b69e.js",
  "http://cdn.qiita.com/assets/siteid-reverse-04252f9a0a01f3a6d03eefefb2a30602e854bf7a4d237969a35600c1bbc3f783.png",
  "http://cdn.qiita.com/assets/application-d8ee012bffd027f20800f6b42efae63131efef12cb945352928e7928a8f74395.css"
]

#全測定で同じコネクションプールを使いまわす
pool = HTTPConnectionPool.new
5.times{
  measure {
    uris.each{|url|
      HTTPClient.new(url, pool).get
    }
  }
}
pool.terminate

# 実行結果
# http start 0.12132032 sec
# 0.421523891 sec
# 0.259425779 sec
# 0.25834163 sec
# 0.27411833 sec
# 0.258259194 sec

#測定単位でコネクションプールを使いまわす
5.times{
  measure {
    pool = HTTPConnectionPool.new
    uris.each{|url|
      HTTPClient.new(url, pool).get
    }
    pool.terminate
  }
}

# 実行結果
# http start 0.138063059 sec
# 0.438925485 sec
# http start 0.12680652 sec
# 0.44297858 sec
# http start 0.068045523 sec
# 0.36733418 sec
# http start 0.145804413 sec
# 0.449172877 sec
# http start 0.128397024 sec
# 0.426856892 sec

# リクエスト単位でコネクションプールを作る
5.times{
  measure {
    uris.each{|url|
     pool = HTTPConnectionPool.new
      HTTPClient.new(url, pool).get
     pool.terminate
    }
  }
}

# 実行結果
# http start 0.068404971 sec
# http start 0.067623014 sec
# http start 0.101885186 sec
# 0.578651403 sec
# http start 0.150872889 sec
# http start 0.147071087 sec
# http start 0.152168537 sec
# 0.79008449 sec
# http start 0.121360849 sec
# http start 0.147465843 sec
# http start 0.073611708 sec
# 0.682813501 sec
# http start 0.154655283 sec
# http start 0.146802413 sec
# http start 0.068633179 sec
# 0.714932011 sec
# http start 0.116327055 sec
# http start 0.103016716 sec
# http start 0.102094246 sec
# 0.800242007 sec
```

コネクションを使いまわすとだいぶ速くなりますね。

ちなみにNet::HTTPはスレッドセーフではないので、キューに入れて複数スレッドから同時にNet::HTTPのインスタンスを利用するとエラーになります。

## まとめ

Ruby標準のHTTPライブラリではなく他ライブラリを利用する選択もありますが、できることなら依存性を減らせる標準ライブラリを使いたいですよね。

Net::HTTPは若干取っ付きづらい印象ですが機能が充実しているのでおススメです。

ちなみにApacheのKeep-Aliveはデフォルトで15秒、Nginxはデフォルト75秒と長めに設定されているんですね。