OpenFlowの世界では、コントローラとしてソフトウェア実装したイーサネットスイッチをよくラーニングスイッチと呼びます。なぜ、ラーニング(学習)スイッチと呼ぶのでしょうか。それは、イーサネットスイッチが次のように動くからです。
-
学習:ホストから出たパケットに含まれる、ネットワーク上でのホストの位置情報を学習する
-
転送:今まで学習してきた位置情報を使って、パケットを宛先のホストまで転送する
この「学習し、転送する」というラーニングスイッチの仕組みは応用が広く効きます。たとえば後半で紹介するいくつかのデータセンターネットワークも、基本はラーニングスイッチと同じく「学習し、転送する」という動作をします。自宅ネットワークであろうが最新鋭のデータセンターであろうが、基本的な考え方は同じなのです。
ラーニングスイッチを作れるようになれば、それをベースに改造することでいろいろなアプリケーションを作れるようになります。
ではさっそく、ネットワークの基本部品であるラーニングスイッチをTremaで実装してみましょう。まずは一般的なイーサネットスイッチの動作原理を理解し、次にOpenFlowでの実現方法を見ていきます。
簡単なネットワークを例にしてイーサネットスイッチの動作を説明します(図 7-1)。
イーサネットスイッチのポート1番と5番に、ホスト1と2をそれぞれ接続しています。また、それぞれのホストのネットワークカードは図に示したMACアドレスを持つとします。
イーサネットスイッチはホストから届いたパケットを宛先のホストまで転送するために、イーサネットスイッチにつながる各ホストの位置情報をためておくデータベースを持っています。これをフォワーディングデータベース(FDB)と呼び、FDBは「ホストのMACアドレス」+「ポート番号」の組を保持します。
ここでホスト2がホスト1へパケットを送信すると、イーサネットスイッチは図 7-2のようにパケットをホスト1まで転送します。
-
届いたパケットの宛先MACアドレス(00:11:11:11:11:11)を見て、このMACアドレスを持つホストがつながるポート番号をFDBから探す
-
FDBには「MACアドレス00:11:11:11:11:11=ポート1」と学習しているので、ポート1にパケットを出力する
ここまでの仕組みがわかれば、イーサネットスイッチの機能を実現するコントローラ (ラーニングスイッチ) をOpenFlowで実現するのは簡単です。
OpenFlowによるイーサネットスイッチの構成は図 7-3のようになります。一般的なイーサネットスイッチとの違いは次の2つです。
-
FDBをソフトウェアとして実装し、コントローラがFDBを管理する
-
パケットの転送は、コントローラがフローテーブルにフローエントリを書き込むことで制御する
なお、初期状態でのFDBとフローテーブルの中身はどちらも空です。
この状態でホスト1がホスト2へパケットを送信すると、コントローラは次のようにホスト1のネットワーク上での位置情報を学習します(図 7-4)。
-
フローテーブルはまだ空なので、パケットはPacket Inとしてコントローラまで上がる
-
コントローラはPacket Inメッセージからパケットの送信元MACアドレスとパケットの入ってきたポート番号を調べ、「ポート1番にはMACアドレスが00:11:11:11:11:11のホストがつながっている」とFDBに保存する
学習が終わると次はパケットの転送です。もちろん、パケットの宛先はまだ学習していないので、コントローラは次のようにパケットをばらまくことで宛先まで届けます。このばらまく処理をフラッディングと呼びます(図 7-5)。
-
コントローラはPacket Inメッセージの宛先MACアドレスを調べ、FDBから送出先のポート番号を探す。しかし、ホスト2のMACアドレスとポート番号はまだFDBに入っていないのでわからない
-
コントローラは出力ポートをフラッディングに指定したPacket Outメッセージでパケットをばらまくようにスイッチに指示する。その結果、ポート5につながるホスト2にパケットが届く
この状態でホスト2がホスト1へパケットを送信すると次のようになります(図 7-6)。
-
フローテーブルが空なためコントローラまで再びPacket Inメッセージが上がる
-
コントローラはこのPacket Inメッセージから「ポート5番にはMACアドレスが00:22:22:22:22:22のホストがつながっている」とFDBに保存する
-
Packet Inの宛先MACアドレスとFDBを照らし合わせ、出力先のポート番号を探す。すでに「ポート1=MACアドレス00:11:11:11:11:11」と学習しているので、出力ポートは1と決定できる
-
「ホスト2からホスト1へのパケットはポート1へ出力せよ」というフローエントリをFlow Modメッセージでフローテーブルに書き込む。加えて、Packet Outメッセージ(出力ポート = 1)でPacket Inを起こしたパケットをポート1へ出力する
さて、ここまでの段階でフローテーブルには「ホスト2からホスト1へのパケットはポート1へ出力せよ」というフローエントリが入りました。もし、ホスト2がホスト1へ再びパケットを送信すると、今度はPacket Inがコントローラまで上がることはなく、スイッチ側だけでパケットを転送します。
残りのホスト1からホスト2へのフローエントリはどうでしょう。すでにFDBはすべてのホストのMACアドレスとポート番号を学習しています。もし、再びホスト1からホスト2へパケットを送信すると、図 7-6と同様にコントローラが「ホスト1からホスト2へのパケットはポート5へ出力せよ」というフローエントリを書き込みます。もちろん、それ以降の通信ではPacket Inはまったく上がらずにすべてスイッチ側だけでパケットを処理します。
今回も仮想ネットワークを使ってラーニングスイッチを起動してみます。ソースコードと仮想ネットワークの設定ファイルはGitHubのtrema/learning_switchリポジトリ (https://github.com/trema/learning_switch) からダウンロードできます。
$ git clone https://github.com/trema/learning_switch.git
ダウンロードしたソースツリー上で bundle install --binstubs
を実行すると、Tremaなどの実行環境一式を自動的にインストールできます。
$ cd learning_switch $ bundle install --binstubs
GitHubから取得したラーニングスイッチのソースリポジトリ内に、仮想スイッチ1台、仮想ホスト2台の構成を持つ設定ファイル trema.conf
が入っています。
link:vendor/learning_switch/trema.conf[role=include]
次のように trema run
の -c
オプションにこの設定ファイルを渡してラーニングスイッチを実行します。
$ ./bin/trema run ./lib/learning_switch.rb -c trema.conf
別ターミナルを開き、trema send_packets
コマンドを使ってhost1とhost2の間でテストパケットを送ってみます。
$ ./bin/trema send_packets --source host1 --dest host2 $ ./bin/trema send_packets --source host2 --dest host1
trema show_stats
コマンドでhost1とhost2の受信パケット数をチェックし、それぞれでパケットを受信していれば成功です。
$ ./bin/trema show_stats host1 Packets sent: 192.168.0.1 -> 192.168.0.2 = 1 packet Packets received: 192.168.0.2 -> 192.168.0.1 = 1 packet $ ./bin/trema show_stats host2 Packets sent: 192.168.0.2 -> 192.168.0.1 = 1 packet Packets received: 192.168.0.1 -> 192.168.0.2 = 1 packet
ラーニングスイッチの動作イメージがわかったところで、ソースコードの解説に移りましょう。
ラーニングスイッチのソースコードは lib/learning_switch.rb と lib/fdb.rb の 2 つからなります。まずはメインのソースコード (lib/learning_switch.rb) をざっと眺めてみましょう。 とくに、private
の行よりも上のパブリックなメソッドに注目してください。
require 'fdb'
# An OpenFlow controller that emulates an ethernet switch.
class LearningSwitch < Trema::Controller
timer_event :age_fdb, interval: 5.sec
def start(_argv)
@fdb = FDB.new
logger.info "#{name} started."
end
def switch_ready(datapath_id)
# Drop BPDU frames
send_flow_mod_add(
datapath_id,
priority: 100,
match: Match.new(destination_mac_address: '01:80:C2:00:00:00')
)
end
def packet_in(_datapath_id, packet_in)
@fdb.learn packet_in.source_mac, packet_in.in_port
flow_mod_and_packet_out packet_in
end
def age_fdb
@fdb.age
end
private
def flow_mod_and_packet_out(packet_in)
port_no = @fdb.lookup(packet_in.destination_mac)
flow_mod(packet_in, port_no) if port_no
packet_out(packet_in, port_no || :flood)
end
def flow_mod(packet_in, port_no)
send_flow_mod_add(
packet_in.datapath_id,
match: ExactMatch.new(packet_in),
actions: SendOutPort.new(port_no)
)
end
def packet_out(packet_in, port_no)
send_packet_out(
packet_in.datapath_id,
packet_in: packet_in,
actions: SendOutPort.new(port_no)
)
end
end
今までの知識だけでも、このソースコードからいろいろなことがわかります。
-
ラーニングスイッチの本体は
LearningSwitch
という名前のクラス -
起動時に呼ばれる
start
ハンドラでFDBのインスタンス変数を作っている。FDBの実装は別ファイルlib/fdb.rb
に分かれている -
スイッチ接続時に呼ばれる
swtich_ready
ハンドラでは、宛先 MAC アドレスが01:80:C2:00:00:00
のパケットを落とすフローエントリを打ち込んでいる -
packet_in
ハンドラで呼ぶflow_mod_and_packet_out
メソッドの中では、@fdb
を使ってポート番号を調べたり、flow_mod
とpacket_out
メソッドでそれぞれFlow ModとPacket Outメッセージを送っている。また、先述した「パケットをばらまく(フラッディング)」処理に対応する:flood
も見つかる
ラーニングスイッチの心臓部は packet_in
ハンドラだけで、その中身もたった 3 行のみと単純です。ラーニングスイッチの仕組みを思い出しながら、ソースコードを詳しく読み解いていきましょう。今回のポイントとなるのは、Packet In ハンドラでの次の処理です。
-
FDBの更新とポート番号の検索
-
ポート番号が見つかった場合の、Flow ModとPacket Outの処理
-
ポート番号が見つからなかった場合の、フラッディング処理
それでは、最初にPacket Inハンドラの内容から見ていきましょう。
知らないパケットがPacket Inとして入ってきたとき、ラーニングスイッチは次のようにFDBにホストの位置情報を学習し、宛先のポート番号を調べます。
-
パケットの送信元MACアドレスとパケットが入ってきたポート番号をPacket Inメッセージから取り出し、FDB (
@fdb
) に保存する -
パケットの宛先MACアドレスとFDBから、パケットを出力するポート番号を調べる (
@fdb.lookup
メソッド)
link:vendor/learning_switch/lib/learning_switch.rb[role=include]
もし宛先ポートが見つかった場合、以降は同じパケットは同様に転送せよ、というフローエントリをスイッチに書き込みます (flow_mod
メソッド)。また、Packet Inを起こしたパケットも忘れずにそのポートへ出力します (packet_out
メソッド)。
link:vendor/learning_switch/lib/learning_switch.rb[role=include]
この flow_mod
メソッドと packet_out
メソッドはそれぞれ send_flow_mod_add
(5 章「マイクロベンチマークCbench」で紹介) および send_packet_out
(Packet Outの送信) メソッドを次のように呼び出します。
link:vendor/learning_switch/lib/learning_switch.rb[role=include]
Packet OutはOpenFlowメッセージの1つで、スイッチの指定したポートからパケットを出力させるためのものです。TremaでPacket Outを送るためのメソッド send_packet_out
は、次の2つの引数を取ります。
send_packet_out(datapath_id, options)
それぞれの引数の意味は次のとおりです。
datapath_id
-
Packet Outメッセージの届け先となるスイッチのDatapath ID
options
-
Packet Outメッセージの中身を決めるためのオプション。アクションによるパケットの書き換えや出力するポートをハッシュテーブルで指定する。それぞれのオプションにはデフォルト値が設定されているので、必要なオプションのみを指定すればよい
Packet Outの使い道は、Packet Inメッセージとして入ってきたパケットをそのままスイッチのポートから送り出す場合がほとんどです。この場合、パケットの送信にスイッチのバッファを使う場合と使わない場合とで呼び出し方が変わります。
パケットのデータがスイッチのバッファに乗っていることが期待できる場合には、次のように buffer_id
オプションでバッファに乗っているパケットデータのIDを指定してやることでPacket Outできます。
send_packet_out(
datapath_id,
buffer_id: packet_in.buffer_id,
raw_data: packet_in.raw_data,
actions: SendOutPort.new(port_number)
)
この場合コントローラからスイッチへのパケットデータのコピーが起こらないため、若干のスピードアップが期待できます。ただし、2 章「OpenFlow の仕様」のコラムで説明したとおり、バッファの中身は観測不能でデータがいつ消えるかもわからないため、この方法は推奨しません。
スイッチのバッファを使わずに Packet Out する場合、次のように raw_data
オプションでパケットのデータを指定する必要があります。バッファに乗っているいないにかかわらず Packet Out できるので、若干遅くはなりますが安全です。
send_packet_out(
datapath_id,
raw_data: packet_in.raw_data,
actions: SendOutPort.new(port_number)
)
これは、次のように packet_in
オプションを使うことで若干短くできます (.raw_data
を書かなくてよくなります)。
send_packet_out(
datapath_id,
packet_in: packet_in,
actions: SendOutPort.new(port_number)
)
options
に指定できる主なオプションは次のとおりです。
buffer_id
-
スイッチでバッファされているパケットの ID を指定する。この値を使うと、スイッチでバッファされているパケットを指定して Packet Out できるので効率が良くなる (ただし、スイッチにバッファされていない時はエラーになる)
raw_data
-
Packet Out するパケットの中身を指定する。もし
buffer_id
オプションが指定されておりスイッチにバッファされたパケットを Packet Out する場合、この値は使われない packet_in
-
raw_data
およびin_port
オプションを指定するためのショートカット。Packet In ハンドラの引数として渡される Packet In メッセージを指定する actions
-
Packet Out のときに実行したいアクションの配列を指定する。アクションが 1 つの場合は配列でなくてかまわない
もし宛先ポートが見つからなかった場合、コントローラは Packet In したパケットをフラッディングしてばらまきます。これをやるのが flow_mod_and_packet_out
メソッドで、ポート番号に予約ポート番号の :flood
を指定して packet_out
メソッドを呼び出します。:flood
を指定した Packet Out メッセージをスイッチが受け取ると、Packet In したパケットをフラッディングします。
link:vendor/learning_switch/lib/learning_switch.rb[role=include]
learning_switch.rb の一行目の require 'fdb'
は、同じディレクトリ内の fdb.rb を読み込みます。require
はちょうど、C の #include
や Java の import
みたいなものと思ってください。Ruby では、たとえば fdb.rb というファイルを読み込みたいときは、拡張子の .rb を外して require 'fdb'
と書きます。読み込む対象のファイルは、lib/ ディレクトリを起点とした相対パスで書きます。たとえば lib/learning_switch/extensions.rb を読み込みたいときには require 'learning_switch/extensions'
と書きます。
fdb.rb もざっと目を通しておきましょう。このファイルは FDB の機能をカプセル化する FDB
クラスを提供します。
link:vendor/learning_switch/lib/fdb.rb[role=include]
FDB
クラスは3つのメソッド lookup
・learn
・age
を持ちます。lookup
メソッドを使うと MAC アドレスからポート番号を検索できます。逆に learn
メソッドでは MAC アドレスとポート番号の組を学習できます。タイマで定期的に呼ばれる age
メソッドでは、FDB に入っているすべてのエントリをエージングし、寿命を過ぎたもの (FDB::Entry#aged_out?
で判定) を消します。
switch_ready
ハンドラでは宛先 MAC アドレスが 01:80:C2:00:00:00
のパケットを落とすフローエントリを打ち込んでいました。
def switch_ready(datapath_id)
# Drop BPDU frames
send_flow_mod_add(
datapath_id,
priority: 100,
match: Match.new(destination_mac_address: '01:80:C2:00:00:00')
)
end
コードコメントにもあるように、ここで落としているのはスパニングツリーの制御フレームである BPDU フレームです。OpenFlow ではスイッチを集中制御できるため、ループを防ぎたい場合には分散アルゴリズムの一種であるスパニングツリーは不要だからです。OpenFlow でループを防ぐ方法について詳しくは、16 章「たくさんのスイッチを制御する」で解説します。
実用的なOpenFlowアプリケーションのベースとなるラーニングスイッチの動作と作り方を学びました。
-
コントローラは、Packet Inメッセージから送信元ホストのMACアドレスとホストのつながるスイッチポート番号をFDBに学習する
-
Packet Inの転送先がFDBからわかる場合、Flow Modで以降の転送情報をスイッチに書き込みPacketOutする
-
Packet Inの転送先がFDBからわからない場合は、入力ポート以外のすべてのポートにPacket Outでフラッディングする
続く章ではこのラーニングスイッチを OpenFlow 1.3 のマルチプルテーブル機能を使って実装します。パケットの処理内容ごとにフローテーブルを分けることで、コントローラをすっきりと設計できます。