Trema/MyRoutingSwitch(5), Topology Change and Re-route(6)

はじめに

routing switch でリンクダウン発生によってトポロジが変化した際にどう対応していくか、というのを、この前の第1回第5回でためしてみました。
次はリンクアップに対してどう対応するかという方向に行きます。

…記事のタイトル、もうちょっとどうにかならんかなと思ったけどまあもういいや。

方針

リンクアップは、リンクダウンと違って、明示的に packet-in を起こすとか、そういう「特定のフロー操作」というのがやりにくいです。リンクダウンをした場合は、消すべきフローが特定できます。なにせもう落ちてしまったポートに対して output しているルールは意味がなくなるわけですから。フローを消すと明示的に packet-in が発生します。packet-in が発生すれば、そのフローの経路を書き換えてやれます。

リンクアップについてはこれがありません。既存の通信は別の経路で既にどうにかなっているので、特に問題が起きていないわけです。では、リンクアップによって選択可能な経路が増えたときにどうするか? 第2回にも書きましたが

  • フローのハードタイムアウト
  • トポロジ変化の検出時に環境内のすべてのスイッチのフローテーブルをクリア

というのは除外します。それ以外で、とりあえず以下の3パターンくらい考えてみました。

arp-table にある情報を元にフローを書き換える

環境内にいる通信ノード(endpoint)の情報が arp table には入っています。これを元に環境内のフローを書き換える…という話ですね。でもこれ、arp table だけだと特定のフローを指定できません。いまの Controller では、フローは MAC/IP で src/dst を指定しています。でも arp table には始点終点の情報がないわけです。arp table に入っている endpoint 間全組み合わせフルメッシュでフローを再計算する? 計算量的に無駄が多すぎますね…。みんながみんな相互に通信しているわけではないですし。

ということで却下。

OFSにあるフローテーブルの情報を元にフローを書き換える

トポロジ変化(リンクアップ)があったときに、今どういうフローが環境内で使われているか、というのがわかればよいわけです。なので、OFS にある情報を持ってきたらいいんじゃないか? というのが次の案。controller 側でスイッチのフロー情報を収集するというのも試してみたりしました。

フローの情報は集められます。が、1つの「フロー」に対して複数のスイッチに「フローエントリ」があります。個別に収集してきたエントリの情報を元に1つのフローを組み立て直す、というのが結構面倒くさいです。だって、もともと「(src/dst間通信を実現するための)フローをさばく必要がある」から、各スイッチに対してフローエントリを生成してばらまいているわけです。なぜわざわざ逆算しなければいけないのか。最初からフローの情報覚えておけばいいじゃないか。

ということで却下。

フローエントリ設定時にフローの情報をキャッシュしておく

そんなわけで、packet-in が起きて、フローエントリを各スイッチに対して配るときにフローの情報を記録しておくことにします。新しく FlowIndex というクラスをつくってそこに設定しているフロー情報を覚えさせます。arp table のフロー版…みたいなイメージですね。

テストシナリオ

こういうケースでどうなるのかを試してみます。

トポロジ


シナリオ
  • host1-host2, host1-host3 間で双方向に通信
  • リンクダウン→フロー片寄せ→リンクアップ→元に戻るか?
#!/bin/sh

. ./test-common.sh

# scenario

step 1
sendpackets_host1 host2
sendpackets_host1 host3
showstats host1 host2 host3
dumpflows sw1 sw2 sw3 sw4 sw5 sw6 sw7

step 2
linkdown sw2 1
dumpflows sw1 sw2 sw3 sw4 sw5 sw6 sw7

step 3
sendpackets_host1 host2
sendpackets_host1 host3
showstats host1 host2 host3
dumpflows sw1 sw2 sw3 sw4 sw5 sw6 sw7

step 4
linkup sw2 1
dumpflows sw1 sw2 sw3 sw4 sw5 sw6 sw7

step 5
sendpackets_host1 host2
sendpackets_host1 host3
showstats host1 host2 host3
dumpflows sw1 sw2 sw3 sw4 sw5 sw6 sw7

処理

FlowIndex に入れておく情報は、基本的に、環境内(各OFS)に持たせているフローエントリと整合性がとれている必要があります。つまり、新しく書き換えるときに更新して、使わない物は捨てる、という処理が必要です。

フローの削除

フローエントリを age out するようにしているのであれば(ソフトタイムアウト)、使われなくなったフローを削除したときに、キャッシュしてあるフロー情報も消してやる必要があります。が、これがまたちょっと厄介なところがあるのですが、説明が長くなります。次回で詳細解説。

リンク情報の取得タイミング

前にも書いているのですが、もともと、ruby_topology をベースに改造をしています。で、トポロジの情報…つまりスイッチ間リンクの情報は、LLDP Packet のやりとり (LLDP の packet-in) を元に組み立てられています。LLDP Packet Flooding は各スイッチで行われるので、1つのリンクに対して、その端点 2 スイッチ、双方向の情報があります。何を言いたいかというと、1リンクについて2個情報を受け取ることになっている、ということです。

リンクダウンの際は、port status 検出後すぐにフローを書き換えてしまうので、この lldp によるトポロジ情報収集にそれほど依存せずに処理が回ります。しかし、リンクアップの場合は、リンクが増えた後、lldp flooding を行って、リンクの情報が手に入ってはじめてちゃんとトポロジ情報が構築され、endpoint 間の経路計算ができるようになります。なので、リンクアップ〜port status〜lldp flooding のタイミングによっては、必要なリンクの情報が足りなくて上手くフローが計算できないことがありました。

  • MyRoutingSwitch::flow_mod_to_path
    • リンクの情報が見当たらない場合(片方しかリンク情報が取得できていない場合)はいったん warn メッセージ出して保留させます。
  def flow_mod_to_path start_dpid, dst_arp_entry, packet_in
    goal_dpid = dst_arp_entry.dpid
    goal_port = dst_arp_entry.port

    pred = @topology.path_between goal_dpid, start_dpid
    now_dpid = start_dpid
    path = []

    while pred[now_dpid]
      path << now_dpid
      next_dpid = pred[now_dpid]
      link = @topology.link_between(now_dpid, next_dpid)
      if link
        puts "flow_mod: dpid:#{now_dpid.to_hex}/port:#{link.port1} -> dpid:#{next_dpid}"
        flow_mod now_dpid, srcdst_match(packet_in), SendOutPort.new(link.port1)
        now_dpid = next_dpid
      else
        warn "[pred] link: not found"
        break
      end
    end
    # last hop
    flow_mod goal_dpid, dst_match(packet_in), SendOutPort.new(goal_port)

    return path # Notice: do not includes last-hop dpid
  end
  • MyRoutingSwitch::port_status
    • periodic_time_event で定期的に lldp flooding するようになっていますが、ポロジ変化を検出したときに集中的に lldp flooding してやれば良いわけです*1。というより、リンクアップ検出→パス計算→片方向の情報しか採れていなくて…というのの待ち時間は lldp flooding の間隔に依存するはずです。いちいち timer event での flooding を待たずに、トポロジ変化があった瞬間に、とっとと情報取得をしてしまえば良いわけです。
    • ここではリンクの状態変化(port_status)について書いていますが、スイッチの状態変化(switch_readh, switch_disconnected) についても同様ですね。
    • 環境全体で flooding させてしまうので、もうちょっと範囲を絞ってもいいのかも…と思ったのですが、新しいリンクが(スイッチが)増えたときに flooding する「範囲」ってどうやってわかるんだよって、わからないんですよね。トポロジ情報は flooding してはじめてわかるので。まあ当たり前の話なのですが。
  def port_status dpid, port_status
    info "[MyRoutingSwitch::port_status] switch:#{dpid.to_hex}"

    updated_port = port_status.port
    return if updated_port.local?

    @topology.update_port dpid, updated_port
    # update link info, asap!
    flood_lldp_frames
  end

これやっておけば、通常の lldp flooding timer event interval はもうちょっと大きくしていいです*2。Tremashark とかでイベント見てると lldp 周りのノイズがどうしても大きいしね…。

おわりに

リンクアップによるトポロジ変化についてどういう処理を行うかというのをやってみました。リンクが増えたときにどういうフロー情報の処理をしてフロー書き換えてやれば良いのか、というのは次回もうちょっと詳細に見ます。

*1:環境内が完全に OFS Hop-by-Hop になっているという前提をおいています。

*2:今回テストとか追加してますが、interval 30sec でも…つまりトポロジ変化の検出を periodic interval による flooding に(ほぼ)依存せずに…テスト回ってます。