Skip to content

Latest commit

 

History

History
666 lines (526 loc) · 34.7 KB

cbench.adoc

File metadata and controls

666 lines (526 loc) · 34.7 KB

マイクロベンチマークCbench

本格的なOpenFlowプログラミングの第一歩として、スイッチのフローテーブルを書き換えてみましょう。マイクロベンチマークツールCbenchを題材に、Packet InとFlow Modメッセージの使い方を学びます。

Cbenchベンチマークとは

CbenchはOpenFlow1.0コントローラのためのベンチマークです。このベンチマークの内容は、1秒あたりにコントローラが出せるFlow Modの数を計測するというものです。これはOpenFlowプロトコル全体のうちのごく一部の性能だけを対象にしているので、ベンチマークの中でもマイクロベンチマークに分類できます。

Cbenchは図 5-1のように動作します。まずcbenchプロセスはOpenFlowスイッチのふりをしてコントローラに接続し、コントローラにPacket Inを連続して送ります。コントローラはPacket Inを受け取るとcbenchプロセスにFlow Modを返します。cbenchプロセスは決められた時間の間に受け取ったFlow Modの数をカウントし、ベンチマークのスコアとします。つまり大量のPacket Inに反応し素早くFlow Modを返せるコントローラほど「速い」とみなします。

cbench overview
図 5-1: cbenchプロセスとコントローラの動作

インストール

Cbenchの実行にはopenflow.org [1] の配布するベンチマークスイートOflopsを使います。GitHubのtrema/cbenchリポジトリには、Oflops一式とCbenchコントローラのソースコードが入っています。次のコマンドでダウンロードしてください。

$ git clone https://github.com/trema/cbench.git

ダウンロードしたソースツリー上で bundle install --binstubs を実行すると、Tremaの ./bin/trema コマンドと Cbench の実行コマンド ./bin/cbench など必要な実行環境一式を自動的にインストールできます。

$ cd cbench
$ bundle install --binstubs

以上でCbenchとTremaのセットアップは完了です。

実行してみよう

さっそくCbenchを実行してみましょう。まず、コントローラを次のように起動します。

$ ./bin/trema run ./lib/cbench.rb

そして別ターミナルを開き、次のコマンドでcbenchプロセスを実行しベンチマークを開始します[2]

$ ./bin/cbench --port 6653 --switches 1 --loops 10 --ms-per-test 10000 --delay 1000 --throughput
cbench: controller benchmarking tool
   running in mode 'latency'
   connecting to controller at localhost:6653
   faking 1 switches :: 10 tests each; 10000 ms per test
   with 100000 unique source MACs per switch
   starting test with 1000 ms delay after features_reply
   ignoring first 1 "warmup" and last 0 "cooldown" loops
   debugging info is off
1   switches: fmods/sec:  807   total = 0.080652 per ms
1   switches: fmods/sec:  797   total = 0.079694 per ms
1   switches: fmods/sec:  799   total = 0.079730 per ms
1   switches: fmods/sec:  797   total = 0.079698 per ms
1   switches: fmods/sec:  801   total = 0.080003 per ms
1   switches: fmods/sec:  800   total = 0.079965 per ms
1   switches: fmods/sec:  802   total = 0.080159 per ms
1   switches: fmods/sec:  802   total = 0.080182 per ms
1   switches: fmods/sec:  806   total = 0.080549 per ms
1   switches: fmods/sec:  801   total = 0.080082 per ms
RESULT: 1 switches 9 tests min/max/avg/stdev = 79.69/80.55/80.01/0.26 responses/s

この例では、10秒間のベンチマークを10回実行しその結果を表示しています。fmods/sec の右側の数字が、実際に 1 秒間に打った Flow Mod の数です。実行環境によって値は変わりますが、Trema は秒間に数百回のFlow Modを打てることがわかります。

ソースコード解説

Cbenchが正しく実行できたところで、CbenchのソースコードからPacket InとFlow Modメッセージの処理方法を見ていきましょう。ファイルは lib/cbench.rb です。

lib/cbench.rb
link:vendor/cbench/lib/cbench.rb[role=include]

Cbench のソースコードを眺めると、いくつか見慣れない品詞や構文が登場していることに気付きます。この節では順にそれぞれを紹介していきますが、最初からすべてを覚える必要はありません。もし後でわからなくなったときには見直すようにしてください。

メソッド呼び出し

Cbench のソースコードにはいくつかのメソッド呼び出しがあります。

  • logger.info(…​) (3 章「Hello, Trema!」で解説)

  • ExactMatch.new(…​)

  • packet_in.buffer_id

  • SendOutPort.new(…​)

  • packet_in.in_port

このようにメソッドは普通、変数や定数の後にドットでつなげます。定数や変数が名詞なら、メソッドはちょうど動詞と同じです。

door.open

上の例では open がメソッドです。英語のopenは動詞なので、当然メソッドであるとも言えます。

ふつう、メソッド呼び出しによって何らかの動作をすると新しい結果が得られます。

'redrum'.reverse
#=> "murder"

この場合、文字が逆順になった新しい文字列が返ってきました。

メソッドは引数を取るものもあります。次の例は配列の各要素の間に指定した文字をはさんで連結 (join) します。

['M', 'A', 'S', 'H'].join('★')
#=> "M★A★S★H"

Rubyにはこのようなメソッドが何百種類もあります。それぞれの動作は名前から大体想像できるものがほとんどです。

startハンドラ

Cbench#start (lib/cbench.rb)
link:vendor/cbench/lib/cbench.rb[role=include]

前章と同じく、start ハンドラでコントローラの起動をログに書き込みます。引数は今回も使っていないので、名前を _args のようにアンダースコアで始めます。

packet_inハンドラ

コントローラに上がってくる未知のパケットを拾うには、Packet Inハンドラをコントローラクラスに実装します。Packet Inハンドラは次の形をしています。

def packet_in(datapath_id, packet_in)
  ...
end

packet_in ハンドラはその引数として、Packet Inを起こしたスイッチ(cbenchプロセス)の Datapath ID とPacket Inメッセージを受け取ります。

PacketIn クラス

packet_in ハンドラの2番目の引数はPacket Inメッセージオブジェクトで、PacketIn クラスのインスタンスです。この PacketIn クラスは主に次の3種類のメソッドを持っています。

  • Packet Inを起こしたパケットのデータやその長さ、およびパケットが入ってきたスイッチのポート番号などOpenFlowメッセージ固有の情報を返すメソッド

  • Packet Inを起こしたパケットの種別 (TCPかUDPか。VLANタグがついているかどうか、など)を判定するための ? で終わるメソッド

  • 送信元や宛先のMACアドレスやIPアドレスなど、パケットの各フィールドを調べるためのアクセサメソッド

PacketIn クラスは非常に多くのメソッドを持っており、またTremaのバージョンアップごとにその数も増え続けているためすべては紹介しきれません。そのかわり、代表的でよく使うものを表5-1に紹介します。

Table 1. PacketIn クラスのメソッド (一部)
メソッド 説明

:raw_data

パケットのデータ全体をバイナリ文字列で返す

:in_port

パケットが入ってきたスイッチのポート番号を返す

:buffered?

Packet Inを起こしたパケットがスイッチにバッファされているかどうかを返す

:buffer_id

バッファされている場合、そのバッファ領域の ID を返す

:total_length

パケットのデータ長を返す

:source_mac_address

パケットの送信元MACアドレスを返す

:destination_mac_address

パケットの宛先MACアドレスを返す

:ipv4?

パケットがIPv4である場合 true を返す

:ipv4_protocol

IPのプロトコル番号を返す

:ipv4_source_address

パケットの送信元IPアドレスを返す

:ipv4_destination_address

パケットの宛先IPアドレスを返す

:ipv4_tos

IPのToSフィールドを返す

:tcp?

パケットがTCPである場合 true を返す

:tcp_source_port

パケットのTCPの送信元ポート番号を返す

:tcp_destination_port

パケットのTCP宛先ポート番号を返す

:udp?

パケットがUDPである場合 true を返す

:udp_source_port

パケットのUDPの送信元ポート番号を返す

:udp_destination_port

パケットのUDPの宛先ポート番号を返す

:vlan?

パケットにVLANヘッダが付いている場合 true を返す

:vlan_vid

VLANのVIDを返す

:vlan_priority

VLANの優先度を返す

:ether_type

イーサタイプを返す

このようなメソッドは他にもたくさんあります。完全なメソッドのリストや詳しい情報を知りたい場合には、3 章「Hello, Trema!」で紹介した Trema ホームページを参照してください。

Flow Mod の送り方

Cbenchの仕様によると、コントローラからCbenchへと送るFlow Modメッセージは、次の内容にセットする必要があります。

  • マッチフィールド: Packet In メッセージのExactMatch (後述)

  • アクション: Packet In メッセージのin_portに+1したポートへ転送

  • バッファID: Packet In メッセージのバッファID

それぞれの指定方法を順に見ていきましょう。

マッチフィールド (OpenFlow 1.0)

マッチフィールドを指定するには、send_flow_mod_add の引数に match: オプションとしてマッチフィールドオブジェクト (Match.new(…​) または ExactMatch.new(…​)) を渡します。

send_flow_mod_add(
  datapath_id,
  match: Match.new(...), # (1)
  ...
)
  1. マッチフィールドを指定する match: オプション

マッチフィールドを作るには、Match.new に指定したい条件のオプションを渡します。たとえば、送信元 MAC アドレスが 00:50:56:c0:00:08 で VLAN ID が 3 というルールを指定したマッチフィールドを Flow Mod に指定するコードは、次のようになります。

send_flow_mod_add(
  datapath_id,
  match: Match.new(
           source_mac_address: '00:50:56:c0:00:08'
           vlan_vid: 3
         )
  ...

OpenFlow 1.0 において指定できるマッチフィールドは 12 種類です。Match.new のオプションには、以下の12種類の条件を指定できます(表5-2)。

Table 2. マッチフィールドを作る Match.new のオプション
オプション 説明

:in_port

スイッチの物理ポート番号

:source_mac_address

送信元MACアドレス

:destination_mac_address

宛先MACアドレス

:ether_type

イーサネットの種別

:source_ip_address

送信元IPアドレス

:destination_ip_address

宛先IPアドレス

:ip_protocol

IPのプロトコル種別

:tos

IPのToSフィールド

:transport_source_port

TCP/UDPの送信元ポート番号

:transport_destination_port

TCP/UDPの宛先ポート番号

:vlan_vid

VLAN IDの値

:vlan_priority

VLANのプライオリティ

Note

2章「OpenFlow の仕様」で説明したように、OpenFlow 1.3 でマッチフィールドは 40 種類に増えました。しかし、OpenFlow 1.3 での Match オブジェクトのオプションの指定方法は、OpenFlow 1.0 の場合と変わりません。詳しくは OpenFlow 1.3 を扱う 8章「OpenFlow1.3版ラーニングスイッチ」14章「ルータ (マルチプルテーブル編)」を参照してください。

Exact Matchの作り方 (OpenFlow 1.0)

マッチフィールドの中でもすべての条件を指定したものをExact Matchと呼びます。たとえばPacket Inとしてコントローラに入ってきたパケットとマッチフィールドが定義する12個の条件がすべてまったく同じ、というのがExact Matchです。

マッチフィールドを作る構文 Match.new にこの12種類の条件すべてを渡せば、次のようにExact Matchを作れます。

def packet_in(datapath_id, packet_in)
  ...
  send_flow_mod_add(
    datapath_id,
    match: Match.new(
             in_port: packet_in.in_port,
             source_mac_address: packet_in.source_mac_address,
             destination_mac_address: packet_in.destination_mac_address,
             ...

しかし、マッチフィールドを1つ作るだけで12行も書いていたら大変です。そこで、TremaではPacket InメッセージからExact Matchを楽に書ける次のショートカットを用意しています。

def packet_out(datapath_id, packet_in)
  send_flow_mod_add(
    datapath_id,
    match: ExactMatch.new(packet_in),
    ...

たった1行で書けました! Tremaにはこのようにコードを短く書ける工夫がたくさんあります。

Note

ExactMatch が使えるのは OpenFlow 1.0 のみです。OpenFlow 1.3 ではマッチフィールドの種類が増えたため、ExactMatch は廃止されました。

アクション (OpenFlow1.0)

アクションを指定するには、send_flow_mod_add の引数に actions: オプションとして単体のアクションまたはアクションのリストを渡します[3]

send_flow_mod_add(
  datapath_id,
  ...
  actions: アクション # (1)
)

または

send_flow_mod_add(
  datapath_id,
  ...
  actions: [アクション0, アクション1, アクション2, ...] # (2)
)
  1. actions: オプションでアクションを 1 つ指定

  2. actions: オプションにアクションを複数指定

たとえば、「VLAN ヘッダを除去しポート2番に転送」というアクションを Flow Mod に指定するコードは、次のようになります。

send_flow_mod_add(
  datapath_id,
  ...
  actions: [StripVlanHeader.new, SendOutPort.new(2)] # (1)
)
  1. アクションを 2 つ指定

アクションには表5-3の13種類のアクションを単体で、または組み合わせて指定できます。

Table 3. 指定できるアクション
アクション 説明

SendOutPort

指定したスイッチポートにパケットを出力する

SetEtherSourceAddress

送信元MACアドレスを指定した値に書き換える

SetEtherDestinationAddress

宛先MACアドレスを指定した値に書き換える

SetIpSourceAddress

送信元のIPアドレスを指定した値に書き換える

SetIpDstinationAddress

宛先のIPアドレスを指定した値に書き換える

SetIpTos

IPのToSフィールドを書き換える

SetTransportSourcePort

TCP/UDPの送信元ポート番号を書き換える

SetTransportDestinationPort

TCP/UDPの宛先ポート番号を書き換える

StripVlanHeader

VLANのヘッダを除去する

SetVlanVid

指定したVLAN IDをセットする、または既存のものがあれば書き換える

SetVlanPriority

指定したVLANプライオリティをセットする、または既存のものがあれば書き換える

Enqueue

指定したスイッチポートのキューにパケットを入れる

VendorAction

ベンダ定義の独自拡張アクションを実行する

まだ使っていないアクションについては、続く章で具体的な使い方を見ていきます。

send_flow_mod_add のオプション

バッファIDを指定するには、buffer_id: オプションを send_flow_mod_add の引数に指定します。たとえば次のコードは、バッファ ID に Packet Inメッセージのバッファ ID を指定する典型的な例です。

send_flow_mod_add(
  datapath_id,
  match: ...,
  actions: ...,
  buffer_id: packet_in.buffer_id # (1)
)
  1. Flow Mod のオプションにバッファ ID を指定

send_flow_mod_add で指定できるすべてのオプションは表5-4の通りです。

Table 4. send_flow_mod_addで指定できるオプション
オプション 説明

:match

フローエントリのマッチフィールドを指定する。本章で紹介した Match オブジェクトまたは ExactMatch オブジェクトを指定する

:actions

フローエントリのアクションを指定する。アクションには単体のアクションまたは複数のアクションを配列 (4章で解説) によって指定できる

:buffer_id

アクションが参照するパケットがバッファされている領域の ID を指定する

:idle_timeout

フローエントリが一定時間参照されなかった場合に破棄されるまでの秒数を指定する。デフォルトは0秒で、この場合フローエントリは破棄されない

:hard_timeout

フローエントリの寿命を秒数で指定する。デフォルトは0秒で、この場合フローエントリは破棄されない

:priority

フローエントリの優先度(符号なし16ビット、大きいほど優先度高)。Packet Inメッセージはこの優先度順にフローエントリのマッチフィールドと照らし合わされる。デフォルトは 0xffff (最高優先度)

:send_flow_removed

タイムアウトでフローエントリが消えるときに、Flow Removedメッセージをコントローラに送るかどうかを指定する。デフォルトは true

:check_overlap

true にセットすると、フローテーブルの中に同じ優先度で競合するものがあった場合、フローエントリを追加せずにエラーを起こす。デフォルトは false

:emerg

true にセットすると、フローエントリを緊急エントリとして追加する。緊急エントリはスイッチが何らかの障害でコントローラと切断したときにのみ有効となる。デフォルトは false

:cookie

任意の用途に使える64ビットの整数。使い道としては、同じクッキー値を持つフローエントリ同士をまとめて管理するなどといった用途がある

こうしたオプションも、続くいくつかの章で具体的な使い方を紹介します。

Note

OpenFlow1.0 では2章「OpenFlow の仕様」で解説したインストラクションは使いません。そのためインストラクションの代わりに、アクションを直接フローエントリに指定します。OpenFlow 1.3 でのインストラクションの使い方は、8章「OpenFlow1.3版ラーニングスイッチ」にて詳しく説明します。

マルチスレッド化する

Tremaはシングルスレッドで動作するので、実のところ Cbench の結果はあまり速くありません。シングルスレッドとはつまり、同時に1つの packet_in ハンドラしか実行できないという意味です。たとえ cbench プロセスがたくさん Packet In メッセージを送ってきても、Trema は順に 1 つひとつ処理するため遅くなります。

Trema がシングルスレッドである理由は、マルチスレッドプログラミング由来のやっかいなバグを避けるためです。たとえば次のようなマルチスレッドで動作する multi_threaded_packet_in というハンドラがあったとして、この中でスレッドセーフでない変数の内容を変更すると、タイミングや環境に起因するやっかいなバグが発生してしまいます。

def start(_args)
  @db = DB.new  # (1)
end

# マルチスレッド版 packet_in ハンドラ
def multi_threaded_packet_in(datapath_id, packet_in)  # (2)
  # !!! ここで @db の読み書きは危険 !!!
  return if @db.lookup(packet_in.in_port)
  @db.add packet_in.source_mac_address, packet_in.in_port
end
  1. スレッドセーフでないインスタンス変数

  2. 独立したスレッドで動く Packet In ハンドラ

マルチスレッドプログラミングでは、スレッド間で共有するリソースに競合が起こらないように、注意深くコードを書く必要があるのです。

排他制御

スレッド間の競合を解決する代表的な方法が Mutex による排他制御です。スレッド間で競合の起こる箇所を Mutex で囲むことで、その箇所へは同時に 1 つのスレッドしか入れなくなります。

def start(_args)
  @db = DB.new
  @mutex = Mutex.new  # (1)
end

def multi_threaded_packet_in(datapath_id, packet_in)
  @mutex.synchronize do  # (2)
    # この中で@dbを読み書きすれば安全
    return if @db.lookup(packet_in.in_port)
    @db.add packet_in.source_mac_address, packet_in.in_port
  end
end
  1. 排他制御用の Mutex

  2. do…​end の中には同時に 1 つのスレッドしか入れない

これでひとまず競合は回避できたので、packet_in をスレッド化してみましょう。次のように高速化したいハンドラメソッドの中身を別スレッドで起動し、インスタンス変数へのアクセスを Mutex で排他制御してやります。

def start(_args)
  @db = DB.new
  @mutex = Mutex.new
end

def packet_in(datapath_id, packet_in)
  Thread.start do  # (1)
    @mutex.synchronize do
      return if @db.lookup(packet_in.in_port)
      @db.add packet_in.source_mac_address, packet_in.in_port
    end
  end
end
  1. packet_in ハンドラの中身をスレッドで起動

しかし、これでもまだ問題は残ります。Thread.start によるスレッド起動はそれなりにコストのかかる処理なので、Packet In が届くたびに Tread.new でスレッドを作っていては速くなりません。

そこで、次のようにあらかじめいくつかワーカースレッドを作って待機させておく、いわゆるスレッドプールという手法が使えます。そして packet_in ハンドラが呼ばれるたびに待機中のスレッドに packet_in の処理をまかせます。

def start(_args)
  @db = DB.new
  @mutex = Mutex.new
  @work_queue = Queue.new  # (1)
  10.times { start_worker_thread }  # (2)
end

def packet_in(datapath_id, packet_in)
  @work_queue.push [datapath_id, packet_in]  # (3)
end

private

# ワーカースレッドを開始
def start_worker_thread  # (4)
  Thread.new do
    loop do
      datapath_id, packet_in = @work_queue.pop  # (5)
      @mutex.synchronize do
        next if @db.lookup(packet_in.in_port)
        @db.add packet_in.source_mac_address, packet_in.in_port
      end
    end
  end
end
  1. スレッドにまかせたい仕事を入れておくキュー

  2. スレッドプールに 10 個のスレッドを追加

  3. Packet In が届いたら datapath_id と Packet In をキューに入れる

  4. ワーカースレッドを起動するプライベートメソッド

  5. スレッドの中でキューから datapath_id と Packet In を取り出す。Queue クラスはスレッドセーフなので、@mutex.synchronize do …​ end の中に入れる必要はないことに注意

このスレッドプールを使った、最終的なマルチスレッド版 Cbench コントローラは次のようになります。

lib/multi_threaded_cbench.rb
# A simple openflow controller for benchmarking (multi-threaded version).
class MultiThreadedCbench < Trema::Controller
  def start(_args)
    @work_queue = Queue.new
    10.times { start_worker_thread }
    logger.info 'Cbench started.'
  end

  def packet_in(datapath_id, packet_in)
    @work_queue.push [datapath_id, packet_in]
  end

  private

  def start_worker_thread
    Thread.new do
      loop do
        datapath_id, packet_in = @work_queue.pop
        send_flow_mod_add(datapath_id,
                          match: ExactMatch.new(packet_in),
                          buffer_id: packet_in.buffer_id,
                          actions: SendOutPort.new(packet_in.in_port + 1))
      end
    end
  end
end

実際に性能を計測してみましょう。

$ ./bin/trema run lib/multi_threaded_cbench.rb

別ターミナルで Cbench を起動します。

$ ./bin/cbench --port 6653 --switches 1 --loops 10 --ms-per-test 10000 --delay 1000 --throughput
cbench: controller benchmarking tool
   running in mode 'throughput'
   connecting to controller at localhost:6653
   faking 1 switches :: 10 tests each; 10000 ms per test
   with 100000 unique source MACs per switch
   starting test with 1000 ms delay after features_reply
   ignoring first 1 "warmup" and last 0 "cooldown" loops
   debugging info is off
1   switches: fmods/sec:  748   total = 0.074746 per ms
1   switches: fmods/sec:  714   total = 0.071319 per ms
1   switches: fmods/sec:  705   total = 0.070448 per ms
1   switches: fmods/sec:  704   total = 0.070376 per ms
1   switches: fmods/sec:  718   total = 0.071747 per ms
1   switches: fmods/sec:  734   total = 0.073346 per ms
1   switches: fmods/sec:  739   total = 0.073763 per ms
1   switches: fmods/sec:  736   total = 0.073487 per ms
1   switches: fmods/sec:  732   total = 0.073146 per ms
1   switches: fmods/sec:  730   total = 0.072917 per ms
RESULT: 1 switches 9 tests min/max/avg/stdev = 70.38/73.76/72.28/1.25 responses/s

おや?シングルスレッド版よりも若干遅くなってしまいました。これには 2 つの原因があります。まず、Ruby のスレッドは OS のネイティブスレッドであるため、スレッド切り替えのオーバーヘッドが大きくかかります。しかも、Packet In 処理は一瞬で終わるため、マルチスレッド化しても並列性はあまり上がりません。これらの原因から、マルスレッド化によって新たにスレッド切り替えのオーバーヘッドがかかった分、元のバージョンより遅くなってしまったのです。

無理やり高速化する

ほかに高速化の方法はないでしょうか。実は cbench プロセスが送ってくる Packet In はすべて同じ中身なので、cbench プロセスへ送る Flow Mod メッセージを何度も使いまわすことで簡単に高速化できます。最初のコードでは Packet In ハンドラの中で send_flow_mod_add で毎回 Flow Mod メッセージを作り直していました。この無駄な処理をなくすために、一度作った Flow Mod メッセージをキャッシュしておいて、2 回目以降はキャッシュした Flow Mod を send_message で送るのです。

このキャッシュによって高速化したものがこちらです。ただしこれはただ Cbench のために無理やり高速化したコードなので、すべてを理解する必要はありません。

lib/fast_cbench.rb
# A simple openflow controller for benchmarking (fast version).
class FastCbench < Trema::Controller
  def start(_args)
    logger.info "#{name} started."
  end

  def packet_in(dpid, packet_in)
    @flow_mod ||= create_flow_mod_binary(packet_in) # (1)
    send_message dpid, @flow_mod # (2)
  end

  private

  def create_flow_mod_binary(packet_in)
    options = {
      command: :add,
      priority: 0,
      transaction_id: 0,
      idle_timeout: 0,
      hard_timeout: 0,
      buffer_id: packet_in.buffer_id,
      match: ExactMatch.new(packet_in),
      actions: SendOutPort.new(packet_in.in_port + 1)
    }
    FlowMod.new(options).to_binary.tap do |flow_mod| # (3)
      def flow_mod.to_binary
        self
      end
    end
  end
end
  1. 最初は @flow_modnil なので、最初だけ create_flow_mod_binary で Flow Mod メッセージを作る。二回目以降は呼ばれない

  2. キャッシュした Flow Mod メッセージを cbench プロセスに送る

  3. send_message のときに Trema が呼び出す FlowMod#to_binary を軽くするため、キャッシュしたバイナリを返す特異メソッドを定義

実行結果は次のようになります。秒間約 6000 発の Flow Mod が打てており、元のバージョンに比べて 10 倍近く高速化できました!

cbench: controller benchmarking tool
   running in mode 'throughput'
   connecting to controller at localhost:6653
   faking 1 switches :: 10 tests each; 10000 ms per test
   with 100000 unique source MACs per switch
   starting test with 1000 ms delay after features_reply
   ignoring first 1 "warmup" and last 0 "cooldown" loops
   debugging info is off
1   switches: fmods/sec:  6741   total = 0.674018 per ms
1   switches: fmods/sec:  6400   total = 0.639859 per ms
1   switches: fmods/sec:  6508   total = 0.650710 per ms
1   switches: fmods/sec:  6334   total = 0.633349 per ms
1   switches: fmods/sec:  6325   total = 0.632465 per ms
1   switches: fmods/sec:  6293   total = 0.629207 per ms
1   switches: fmods/sec:  6276   total = 0.627579 per ms
1   switches: fmods/sec:  6332   total = 0.633133 per ms
1   switches: fmods/sec:  6219   total = 0.621860 per ms
1   switches: fmods/sec:  6293   total = 0.629266 per ms
RESULT: 1 switches 9 tests min/max/avg/stdev = 621.86/650.71/633.05/7.77 responses/s
Warning
Cbenchの注意点

Cbench のスコアを盲信しないようにしてください。現在、OpenFlow コントローラの多くがその性能指標として Cbench のスコアを使っているのをよく見掛けます。たとえば Floodlight (http://www.projectfloodlight.org/) は 1 秒間に 100 万発の Flow Mod を打てると宣伝しています。実際にこれはなかなかすごい数字です。スレッドを駆使してめいっぱい I/O を使い切るようにしなければなかなかこの数字は出ません。でも、この数字はほとんど無意味です。

Flow Mod を 1 秒間に 100 万発打たなければならない状況を考えてみてください。それは、Packet In が 1 秒間に 100 万回起こるということになります。Packet In が 1 秒間に 100 万発起こるとはどういうことでしょうか。スイッチで処理できないパケットがすべてコントローラへやってくる、これが 1 秒間に 100 万回も起こるということです。明らかにフローテーブルの設計がうまく行っていません。

コントローラが Packet In を何発さばけるかという性能は、極端に遅くない限りは重要ではありません。データセンターのように、どこにどんなマシンがありどういう通信をするか把握できている場合は、フローテーブルをちゃんと設計していれば Packet In はそんなに起こらないからです。力技で Packet In をさばくよりも、いかに Packet In が起こらないフローテーブル設計をするかの方がずっと大事です。

Cbench のようなマイクロベンチマークでは、測定対象が何でその結果にはどんな意味があるかを理解しないと、針小棒大な結論を招きます。Cbench のスコアは参考程度にとどめましょう。

まとめ

Packet InとFlow Modの最初の一歩として、ベンチマークツールCbenchと接続できるコントローラを書きました。

  • フローエントリを追加するための send_flow_mod_add を使って、スイッチのフローテーブルを書き換える方法を学んだ

  • マッチフィールドの作り方と、指定できるルールを学んだ

  • SendOutPort アクションによるパケットの転送と、その他のアクションを学んだ

  • コントローラをマルチスレッド化する方法を学んだ

OpenFlow プログラミングの基礎はできたので、そろそろ実用的なツールを作ってみましょう。続く章では、遠隔操作可能なソフトウェアパッチパネルを作ります。もう、ネットワークケーブルを挿し替えるためだけにサーバルームまで出向く必要はなくなります。


2. cbench コマンドの初回実行時には、自動的に cbench コマンドのコンパイルが始まります。二回目以降の実行ではコンパイルは起こりません
3. OpenFlow1.0 にはインストラクションはありません。そのかわりパケットに適用したいアクションを、このように Flow Mod に直接指定します