dRubyで分散負荷試験

# tl; dr
* 分散オブジェクトプログラミングなdRuby入門
* dRubyで分散負荷試験のサンプルコード

# はじめに
dRubyってご存知でしょうか。標準ライブラリ`drb`としてRubyに同梱されていて、

> 分散オブジェクトプログラミングのためのライブラリです。
> Ruby のプロセスから他のRubyプロセスにあるオブジェクトのメソッド を呼びだすことができます。他のマシン上のプロセスにも アクセスできます。

と説明されています[^1]。純Rubyな標準ライブラリなのでRubyさえ動けば簡単に分散プログラミングすることができます。

[^1]: http://docs.ruby-lang.org/ja/2.2.0/library/drb.html

dRubyを使って分散負荷試験をやってみたらとても楽ちんだったので紹介します。
RabbitMQの負荷試験をするためにdRubyを使いましたが、Webでもなんでも使える汎用的な仕組みです。

# 環境
この記事のコードは次の環境で動作確認をしています。

* MacOS X 10.11.1
* Ruby 2.2.3

# 経緯
普通のWebサービスならApache BenchなりJMeterなりを使えばいいです。
とあるサービスの開発で[RabbitMQ](https://www.rabbitmq.com/)の負荷試験をするために負荷試験用のプログラムを作る必要がありました。
RabbitMQはメッセージキューの中では性能は悪い方ですが、とはいえ1プロセスからでは負荷をかけきるのが難しい[^2]程度の性能はあります。そこでdRubyを使って複数のサーバから同時に負荷をかけることにしました。

[^2]: RabbitMQの性能上限に届く前に負荷をかけるクライアントの上限が来てしまう

# dRuby
## 基本的な使い方
簡単にdRubyの使い方を説明します。詳しい使い方は[ドキュメント](http://docs.ruby-lang.org/ja/2.2.0/library/drb.html)をみてください。[Ruby: dRuby(drb)](http://qiita.com/lnznt/items/e0d5a43fde5c5ecaba67)もわかりやすいです。

dRubyを使うと他プロセスのオブジェクトのメソッドを呼べるようになります。ここでは、他プロセスのオブジェクトを__リモートオブジェクト__と言い、リモートオブジェクトを提供するプロセスを__dRubyサーバ__と呼びます。

dRubyサーバは、1つのリモートオブジェクトを1つのURIで提供します。
例えばハッシュをリモートオブジェクトとして提供するdRubyサーバのコードは次のようになります。
(サーバ側ではリモートオブジェクトとして提供するオブジェクトを__フロントオブジェクト__といいます)
`irb`や`pry`を2つ起動すると簡単に試せます。

```ruby
require "drb"

front = { foo: "bar", hoge: "moge" }
uri = "druby://localhost:55331"

DRb.start_service(uri, front)
DRb.thread.join
```

`DRb.start_service`にサーバのURIとフロントオブジェクトを渡します。サーバはスレッドとして起動するため、メインスレッドが終了ないように`join`でサーバの終了を待つようにしておきます。

リモートオブジェクトを使う側のdRubyクライアントは次のようなコードになります。

```irb
>> require "drb"
=> true
>> remote_object = DRbObject.new_with_uri("druby://localhost:55331")
=> #<DRb::DRbObject:0x007fccdd8f2968 @ref=nil, @uri="druby://localhost:55331">
>> remote_object[:foo]
=> "bar"
>> remote_object[:aaa] = "bbb"
=> "bbb"
```

`DRbObject.new_with_uri`でURIからリモートオブジェクトを取得できます。リモートオブジェクトは`Hash`ではなく`DRb::DRbObject`というクラスのインスタンスになっており、こいつはリモートオブジェクトのproxyのようなものです。

## 分散オブジェクト
次のように任意のクラスのオブジェクトをリモートオブジェクトとして提供することが可能です。

```rb:server.rb
require "drb"

URI = "druby://localhost:55331"

class Server
  attr_reader :history

  def initialize
    @history = []
  end

  def multiply(x, y)
    (x * y).tap do |res|
      puts "#{x} * #{y} = #{res}"
      history.push res
    end
  end

  def perform(*args, &block)
    puts "Performing with arguments: #{args}"
    history[history.size] = block.call(args)
  end
end

DRb.start_service(URI, Server.new)
DRb.thread.join
```

使う側も簡単です。

```irb
>> require "drb"
=> true
>> remote = DRbObject.new_with_uri("druby://localhost:55331")
=> #<DRb::DRbObject:0x007fd0d3a674c0 @ref=nil, @uri="druby://localhost:55331">
>> remote.multiply(3, 4)
=> 12
>> remote.multiply(4, 4)
=> 16
>> remote.history
=> [12, 16]
```

## 参照渡しと値渡し
ここで少し注意しないといけないことがあります。サーバーからの`Server#history`の配列は値渡しになっています。つまり、クライアント側にはコピーされた配列が返ってきています。
dRubyではメソッド呼び出しの引数や返り値のオブジェクトは、マーシャリング可能な場合マーシャリングしてコピーされます。そのため、配列も通常だとコピーされて値渡しになります。

```
>> remote.history << 1
=> [12, 16, 1]
>> remote.history
=> [12, 16]
```

参照渡しにしたいときはマーシャリングできないようにする必要があります。dRubyでは`DRb::DRbUndumped`を`include`することでマーシャリング不可能にできます。

```rb:server.rb
class Server
  def initialize
    @history = [].extend(DRb::DRbUndumped)
  end
end
```

クライアント側ではリモートオブジェクトとして`DRb::DRbObject`のインスタンスが返ってきます。

```irb
>> remote = DRbObject.new_with_uri("druby://localhost:55331")
=> #<DRb::DRbObject:0x007fd0d4937b30 @ref=nil, @uri="druby://localhost:55331">
>> remote.multiply(3, 4)
=> 12
>> remote.multiply(4, 4)
=> 16
>> remote.history
=> #<DRb::DRbObject:0x007fd0d399bd70 @ref=70237500564640, @uri="druby://localhost:55331">
>> remote.history << 1
=> #<DRb::DRbObject:0x007fd0d4068158 @ref=70237500564640, @uri="druby://localhost:55331">
>> remote.history.join(", ")
=> "12, 16, 1"
```

## ブロック
dRubyはブロック付きのメソッド呼び出しにも対応しています。しかし、クライアント側でもdRubyサーバを起動する必要があります。`Proc`がマーシャリング不可能なため、参照を渡す必要があるからです。
クライアント側でサーバを起動しないとブロック付きメソッド呼び出しはエラーになります。

```irb
>> remote.perform(3, 4) { |x, y| x * y }
DRb::DRbConnError: DRb::DRbServerNotFound
from /Users/nownabe/.anyenv/envs/rbenv/versions/2.2.3/lib/ruby/2.2.0/drb/drb.rb:1718:in `current_server'
```

クライアント側でdRubyサーバを起動してやればうまくいきます。

```irb
>> DRb.start_service
=> #<DRb::DRbServer:0x007fd0d3a243a0 ...
>> remote.perform(3, 4) { |x, y| x * y }
=> 12
>> remote.history.join(", ")
=> "12, 16, 1, 12"
```

# 分散負荷試験
## RabbitMQ負荷試験ツール
前置きが長くなりましたが、いよいよ本題の負荷試験です。
まずは、RabbitMQの負荷試験をするために作ったツールについて説明します。図にするとこんな感じになります。何の変哲もないですね。

<img src="https://qiita-image-store.s3.amazonaws.com/0/43208/62a13a58-1100-5ea5-d492-f4e39306591e.png" height="400px">

それぞれのボックスが1台の仮想サーバと対応しています。

__Load Master__は次の役目を持つアプリケーションです。

- 設定ファイルのパース
- 負荷試験の進行
- 対象サーバ(RabbitMQ, LVS)のメトリクス収集、保存
- 対象サーバの状態リセット
- Load Slaveへの指示
- Load Slaveからの結果を保存
- メトリクスや結果の集計

Load Masterは次のようなYAMLで試験とパラメータを記述できるようになっています。

```yaml
connections:
  parameters:
    connections_count:
      - 10
      - 20
      - 30
queued_messages:
  parameters:
    message_size:
      - 256
      - 1024
    messages_count:
      - 100000
      - 200000
      - 300000
```

__Load Slave__は次の役目を持つアプリケーションです。

- 対象サーバへの各種負荷試験実施
- 対象サーバへの結果報告
- __dRubyで、負荷試験のインターフェイスとなるリモートオブジェクトの提供__

ベースクラスを継承した負荷クラスを追加することで様々な負荷をかけることができるようになっています。

```rb
module LoadSlave
  module Loads
    class QueuedMessages < Base
      def load
        # loads
      end
    end
  end
end
```

扱う仮想サーバが多いので、デプロイやプロセス起動・停止などにはCapistranoを使いました。

## 簡略版HTTP負荷ツール
ここからは、説明簡略化のために次のようなHTTPサーバに負荷をかけるシステムを考えます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/43208/a81d743e-6f0a-0133-3df4-18f8583df778.png" height="300px">

- Load MasterはSlaveへの指示と結果の取得・出力を行う
- Load SlaveはMasterから指示されると対象サーバに負荷をかけて結果をMasterへ報告する

## ファイル構成
Load Master、Load Slaveを構成するファイル群です。

```bash
$ tree .
.
├── bin # 実行ファイル
│   ├── master
│   └── slave
├── config
│   └── slaves.yml # Load SlaveのURI
└── lib
    ├── load_master
    │   ├── runner.rb
    │   ├── slaves.rb
    │   └── test.rb
    └── load_slave
        ├── loader.rb
        ├── runner.rb
        └── server.rb
```

## Load Slave
まずは実際に負荷をかけるLoad Slaveから実装していきます。
LoadSlaveはMasterから指示されるとローカルの`ab`コマンドを使って対象のHTTPサーバに負荷をかけ、かかった秒数を返します。

クラスは`LoadSlave::Loader`、`LoadSlave::Runner`、`LoadSlave::Server`の3つです。
それぞれのコードを紹介します。

### LoadSlave::Loader
実際にApache Benchで負荷をかけるクラスです。このクラスのインスタンスがリモートオブジェクトになります。
Load Masterが`LoadSlave::Loader#execute`を実行することになります。
負荷試験では途中経過の状態を持つこともあるので、各試験ごとでインスタンスを使い捨てることができるように設計してあります。

```ruby:lib/load_slave/loader.rb
require "drb"

module LoadSlave
  class Loader
    include DRb::DRbUndumped

    AB_BINARY = "/usr/sbin/ab"

    attr_reader :url, :requests, :concurrency

    def initialize(url, requests, concurrency)
      @url         = url
      @requests    = requests
      @concurrency = concurrency
    end

    def execute
      puts "Execute `#{command}`"

      start_time = Time.now
      `#{command}`
      time = Time.now - start_time

      puts "Finished ab (#{time}s)"
      time
    end

    private

    def command
      "#{AB_BINARY} -n #{requests} -c #{concurrency} #{url}"
    end
  end
end
```

### LoadSlave::Server
`LoadSlave::Server`のインスタンスがフロントオブジェクトになります。
Load Slaveのサーバプロセスはずっと起動しているので、このインスタンスはプロセスが終了するまで保持されることになります。
Load Masterは`LoadSlave::Server#generate_loader`を実行して`LoadSlave::Loader`のリモートオブジェクトを取得します。

```ruby:lib/load_slave/server.rb
require "load_slave/loader"

module LoadSlave
  class Server
    def generate_loader(url, requests, concurrency)
      Loader.new(url, requests, concurrency)
    end
  end
end
```

### LoadSlave::Runner
dRubyサーバーを起動するクラスです。ホストとポートを指定できます。

```ruby:lib/load_slave/runner.rb
require "drb"
require "load_slave/server"

module LoadSlave
  class Runner
    attr_reader :host, :port

    def initialize(host: "localhost", port: 55331)
      @host = host
      @port = port
    end

    def run
      puts "Starting LoadSlave server"
      DRb.start_service(uri, Server.new)
      puts "Waiting requests"
      DRb.thread.join
    end

    def uri
      @uri ||= "druby://#{host}:#{port}"
    end
  end
end
```

### 実行ファイル
実行ファイルです。

```ruby:bin/slave
#!/usr/bin/env ruby

if ARGV[0] =~ /-h/
  puts "Usage: bin/slave [port]"
  exit
end

$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require "load_slave/runner"

LoadSlave::Runner.new(port: ARGV[0] || 55331).run
```

## Load Master
簡略版ではかなりの機能をカットして、単純にLoad Slaveに指示を出して結果を受取るだけです。

クラスは`LoadMaster::Runner`、`LoadMaster::Slaves`、`LoadMaster::Test`の3つです。

### LoadMaster::Test
1つの負荷試験を表すクラスです。試験の条件を持ちます。

```ruby:lib/load_master/test.rb
Test = Struct.new(:url, :requests, :concurrency)
```

### LoadMaster::Slaves
Load Slaveのリモートオブジェクトを取得して、指示を出すクラスです。
[Parallel](https://github.com/grosser/parallel)を使って全Slaveに同時[^3]に指示を出します。

[^3]: 厳密に同時ではないです

```ruby:lib/load_master/slaves.rb
require "drb"
require "parallel"

module LoadMaster
  class Slaves
    def initialize(drb_uris)
      @slaves = drb_uris.map { |uri| DRbObject.new_with_uri(uri) }
    end

    def count
      @slaves.count
    end

    def load(test)
      Parallel.map(@slaves, in_threads: count) do |slave|
        slave.generate_loader(
          test.url,
          test.requests,
          test.concurrency
        ).execute
      end
    end
  end
end
```

LoadSlave側で`load_master/test`を`require`すれば`LoadSlave::Server#generate_loader`に`Test`オブジェクトをそのまま渡すこともできます。

### LoadMaster::Runner
負荷試験を実施するクラスです。YAMLの設定ファイルから各Load SlaveのURIを読み込みます。

```ruby:lib/load_master/runner.rb
require "yaml"
require "load_master/slaves"
require "load_master/test"

module LoadMaster
  class Runner
    attr_reader :test, :slaves_uris

    def initialize(url, requests, concurrency, config_file)
      @test = Test.new(url, requests, concurrency)
      @slaves_uris = YAML.load_file(config_file)
    end

    def run
      p slaves.load(test)
    end

    private

    def slaves
      @slaves ||= Slaves.new(slaves_uris)
    end
  end
end
```

### 実行ファイル
パラメータが多いです。

```ruby:bin/master
#!/usr/bin/env ruby

if ARGV[0] =~ /-h/
  puts "Usage: bin/master url requests concurrency config_file"
  exit
end

$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
require "load_master/runner"

url, requests, concurrency, config_file = ARGV
LoadMaster::Runner.new(
  url,
  requests,
  concurrency,
  config_file
).run
```

## 負荷かけてみる
### 準備
負荷をかける対象のHTTPサーバはあらかじめ用意しておいてください。めんどくさい場合はWEBrickでOKです。

```bash
echo "hello" > index.html
ruby -rwebrick -e 'WEBrick::HTTPServer.new(DocumentRoot: "./", Port: 3000).start'
```

また、Parallel gemが必要なのでインストールしておいてください。

```bash
gem install parallel
```

YAMLの設定ファイルも準備しておきます。今回Load Slavesは2つ起動することにします。

```yaml:config/slaves.yml
- druby://localhost:55331
- druby://localhost:55332
```

### 実施
まずはLoad Slaveを2プロセス起動します。今回はローカルで全て起動しますが、別のサーバで起動しても大丈夫です。

```bash:LoadSlave(1)
$ bin/slave 55331
Starting LoadSlave server
Waiting for requests
```

```bash:LoadSlave(2)
$ bin/slave 55332
Starting LoadSlave server
Waiting for requests
```

Load Slaveが起動できたら、Load Masterから指示を出して負荷をかけます。

```bash:LoadMaster
$ bin/master http://localhost:3000/ 1000 2 config/slaves.yml
[4.99813, 5.002736]
```

負荷をかけ終わると、Load Slaveの方は次のように出力されているはずです。

```bash:LoadSlave(1)
$ bin/slave 55331
Starting LoadSlave server
Waiting for requests
Execute `/usr/sbin/ab -n 1000 -c 2 http://localhost:3000/`
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests
Finished ab (4.99813s)
```

# おわりに
と、dRubyを使えばめちゃくちゃ簡単に分散負荷試験が可能です！ :satisfied::sparkles:
分散オブジェクトプログラミングが必要になったらぜひ使ってみてください :bulb:

LindaのRuby実装である[Rinda](http://docs.ruby-lang.org/ja/2.2.0/library/rinda=2frinda.html)などを使うとさらに便利になります:wink:

# 参考リンク
* [library drb (Ruby 2.2.0)](http://docs.ruby-lang.org/ja/2.2.0/library/drb.html)
* [Ruby: dRuby(drb)](http://qiita.com/lnznt/items/e0d5a43fde5c5ecaba67)
* [8章 Rinda](http://www.druby.org/ilikeruby/d208.html)