外部サービスとのインテグレーションをWebAPI経由で行う場合、何かしらのHTTPクライアントを利用しますよね。
細かい通信を繰り返す行う場合は、毎回コネクションを繋げて切ってを繰り返すよりもKeep-Aliveを使った実装にすると速くなります。
この記事ではRubyに標準で備わっているNet::HTTPでKeep-Aliveを利用する方法をサンプルを使って簡単に説明します(接続先のサーバーでKeep-Aliveが有効になっていることが前提です)。
HTTPのコネクションをプールする
こんな感じで接続先URLのホスト、ポート、プロトコルをキーにしたコネクションプールをさっくりと準備します。
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コネクションプールを指定します。
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を連続して取得して速度を比較して見ます。
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秒と長めに設定されているんですね。