Skip to content

Latest commit

 

History

History
382 lines (270 loc) · 23.6 KB

learning_switch.adoc

File metadata and controls

382 lines (270 loc) · 23.6 KB

すべての基本、ラーニングスイッチ

データセンターのような複雑に入り組んだネットワークも、もしケーブルを抜いてバラバラにできたなら、スイッチやサーバなどの意外とシンプルな部品に分解できます。

learn

ラーニングスイッチとは何か

OpenFlowの世界では、コントローラとしてソフトウェア実装したイーサネットスイッチをよくラーニングスイッチと呼びます。なぜ、ラーニング(学習)スイッチと呼ぶのでしょうか。それは、イーサネットスイッチが次のように動くからです。

  • 学習:ホストから出たパケットに含まれる、ネットワーク上でのホストの位置情報を学習する

  • 転送:今まで学習してきた位置情報を使って、パケットを宛先のホストまで転送する

この「学習し、転送する」というラーニングスイッチの仕組みは応用が広く効きます。たとえば後半で紹介するいくつかのデータセンターネットワークも、基本はラーニングスイッチと同じく「学習し、転送する」という動作をします。自宅ネットワークであろうが最新鋭のデータセンターであろうが、基本的な考え方は同じなのです。

ラーニングスイッチを作れるようになれば、それをベースに改造することでいろいろなアプリケーションを作れるようになります。

ではさっそく、ネットワークの基本部品であるラーニングスイッチをTremaで実装してみましょう。まずは一般的なイーサネットスイッチの動作原理を理解し、次にOpenFlowでの実現方法を見ていきます。

イーサネットスイッチの仕組み

簡単なネットワークを例にしてイーサネットスイッチの動作を説明します(図 7-1)。

switch network
図 7-1: イーサネットスイッチ1台とホスト2台からなるネットワークとFDBの内容

イーサネットスイッチのポート1番と5番に、ホスト1と2をそれぞれ接続しています。また、それぞれのホストのネットワークカードは図に示したMACアドレスを持つとします。

イーサネットスイッチはホストから届いたパケットを宛先のホストまで転送するために、イーサネットスイッチにつながる各ホストの位置情報をためておくデータベースを持っています。これをフォワーディングデータベース(FDB)と呼び、FDBは「ホストのMACアドレス」+「ポート番号」の組を保持します。

ここでホスト2がホスト1へパケットを送信すると、イーサネットスイッチは図 7-2のようにパケットをホスト1まで転送します。

  1. 届いたパケットの宛先MACアドレス(00:11:11:11:11:11)を見て、このMACアドレスを持つホストがつながるポート番号をFDBから探す

  2. FDBには「MACアドレス00:11:11:11:11:11=ポート1」と学習しているので、ポート1にパケットを出力する

host2to1
図 7-2: FDBの情報からパケットをホスト1に届ける

ここまでの仕組みがわかれば、イーサネットスイッチの機能を実現するコントローラ (ラーニングスイッチ) をOpenFlowで実現するのは簡単です。

OpenFlow版イーサネットスイッチ(ラーニングスイッチ)の仕組み

OpenFlowによるイーサネットスイッチの構成は図 7-3のようになります。一般的なイーサネットスイッチとの違いは次の2つです。

  • FDBをソフトウェアとして実装し、コントローラがFDBを管理する

  • パケットの転送は、コントローラがフローテーブルにフローエントリを書き込むことで制御する

switch network openflow
図 7-3: OpenFlowによるイーサネットスイッチ(ラーニングスイッチ)の構成

なお、初期状態でのFDBとフローテーブルの中身はどちらも空です。

Packet Inからホストの位置情報を学習

この状態でホスト1がホスト2へパケットを送信すると、コントローラは次のようにホスト1のネットワーク上での位置情報を学習します(図 7-4)。

  1. フローテーブルはまだ空なので、パケットはPacket Inとしてコントローラまで上がる

  2. コントローラはPacket Inメッセージからパケットの送信元MACアドレスとパケットの入ってきたポート番号を調べ、「ポート1番にはMACアドレスが00:11:11:11:11:11のホストがつながっている」とFDBに保存する

host1to2 openflow
図 7-4: Packet Inの送信元MACアドレスとスイッチのポート番号をFDBに学習する

Packet Outでパケットを転送(フラッディング)

学習が終わると次はパケットの転送です。もちろん、パケットの宛先はまだ学習していないので、コントローラは次のようにパケットをばらまくことで宛先まで届けます。このばらまく処理をフラッディングと呼びます(図 7-5)。

  1. コントローラはPacket Inメッセージの宛先MACアドレスを調べ、FDBから送出先のポート番号を探す。しかし、ホスト2のMACアドレスとポート番号はまだFDBに入っていないのでわからない

  2. コントローラは出力ポートをフラッディングに指定したPacket Outメッセージでパケットをばらまくようにスイッチに指示する。その結果、ポート5につながるホスト2にパケットが届く

host1to2 flood openflow
図 7-5: 出力ポートがFDBから見つからないため、出力ポートをフラッディングに指定したPacket Outメッセージでパケットをばらまく

再び学習と転送(Flow ModとPacket Out)

この状態でホスト2がホスト1へパケットを送信すると次のようになります(図 7-6)。

  1. フローテーブルが空なためコントローラまで再びPacket Inメッセージが上がる

  2. コントローラはこのPacket Inメッセージから「ポート5番にはMACアドレスが00:22:22:22:22:22のホストがつながっている」とFDBに保存する

  3. Packet Inの宛先MACアドレスとFDBを照らし合わせ、出力先のポート番号を探す。すでに「ポート1=MACアドレス00:11:11:11:11:11」と学習しているので、出力ポートは1と決定できる

  4. 「ホスト2からホスト1へのパケットはポート1へ出力せよ」というフローエントリをFlow Modメッセージでフローテーブルに書き込む。加えて、Packet Outメッセージ(出力ポート = 1)でPacket Inを起こしたパケットをポート1へ出力する

host2to1 openflow
図 7-6: ホスト2のMACアドレスとポート番号をFDBに学習し、フローエントリを書き込むとともにパケットをホスト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 が入っています。

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.rblib/fdb.rb の 2 つからなります。まずはメインのソースコード (lib/learning_switch.rb) をざっと眺めてみましょう。 とくに、private の行よりも上のパブリックなメソッドに注目してください。

lib/learning_switch.rb
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_modpacket_out メソッドでそれぞれFlow ModとPacket Outメッセージを送っている。また、先述した「パケットをばらまく(フラッディング)」処理に対応する :flood も見つかる

ラーニングスイッチの心臓部は packet_in ハンドラだけで、その中身もたった 3 行のみと単純です。ラーニングスイッチの仕組みを思い出しながら、ソースコードを詳しく読み解いていきましょう。今回のポイントとなるのは、Packet In ハンドラでの次の処理です。

  • FDBの更新とポート番号の検索

  • ポート番号が見つかった場合の、Flow ModとPacket Outの処理

  • ポート番号が見つからなかった場合の、フラッディング処理

それでは、最初にPacket Inハンドラの内容から見ていきましょう。

未知のパケット(Packet In)の処理

知らないパケットがPacket Inとして入ってきたとき、ラーニングスイッチは次のようにFDBにホストの位置情報を学習し、宛先のポート番号を調べます。

  1. パケットの送信元MACアドレスとパケットが入ってきたポート番号をPacket Inメッセージから取り出し、FDB (@fdb) に保存する

  2. パケットの宛先MACアドレスとFDBから、パケットを出力するポート番号を調べる (@fdb.lookup メソッド)

LearningSwitch#packet_in, LearningSwitch#flow_mod_and_packet_out (lib/learning_switch.rb)
link:vendor/learning_switch/lib/learning_switch.rb[role=include]

宛先ポート番号が見つかった場合(FlowModとPacket Out)

もし宛先ポートが見つかった場合、以降は同じパケットは同様に転送せよ、というフローエントリをスイッチに書き込みます (flow_mod メソッド)。また、Packet Inを起こしたパケットも忘れずにそのポートへ出力します (packet_out メソッド)。

LearningSwitch#flow_mod_and_packet_out (lib/learning_switch.rb)
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の送信) メソッドを次のように呼び出します。

LearningSwitch#flow_mod, LearningSwitch#packet_out (lib/learning_switch.rb)
link:vendor/learning_switch/lib/learning_switch.rb[role=include]

Packet Out API

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メッセージとして入ってきたパケットをそのままスイッチのポートから送り出す場合がほとんどです。この場合、パケットの送信にスイッチのバッファを使う場合と使わない場合とで呼び出し方が変わります。

スイッチのバッファを使ってPacket Outする場合

パケットのデータがスイッチのバッファに乗っていることが期待できる場合には、次のように 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 の仕様」のコラムで説明したとおり、バッファの中身は観測不能でデータがいつ消えるかもわからないため、この方法は推奨しません。

スイッチのバッファを使わずにPacketOutする場合

スイッチのバッファを使わずに 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 したパケットをフラッディングします。

LearningSwitch#flow_mod_and_packet_out (lib/learning_switch.rb)
link:vendor/learning_switch/lib/learning_switch.rb[role=include]

FDB の実装

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 クラスを提供します。

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

FDB クラスは3つのメソッド lookuplearnage を持ちます。lookup メソッドを使うと MAC アドレスからポート番号を検索できます。逆に learn メソッドでは MAC アドレスとポート番号の組を学習できます。タイマで定期的に呼ばれる age メソッドでは、FDB に入っているすべてのエントリをエージングし、寿命を過ぎたもの (FDB::Entry#aged_out? で判定) を消します。

不要なパケットを転送しない

switch_ready ハンドラでは宛先 MAC アドレスが 01:80:C2:00:00:00 のパケットを落とすフローエントリを打ち込んでいました。

lib/learning_switch.rb
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 のマルチプルテーブル機能を使って実装します。パケットの処理内容ごとにフローテーブルを分けることで、コントローラをすっきりと設計できます。