OpenFlowで、なんちゃって負荷分散を実装してみた[NOXインストール編]

OpenFlowでは、Controllerというソフトウェアで、対応したスイッチングハブが受け取ったすべてのパケットのコントロールができます。
Controllerを自作することで、スイッチングハブにいろんな機能を持たせることができます。
今まで、

  1. VMWare Serverで、OpenFlowの環境を作ってみた[1/2]
  2. VMWare Serverで、OpenFlowの環境を作ってみた[2/2]

とOpenFlowの基本的な環境を作りましたが、普通のスイッチングハブと同じ動作のサンプルなので、面白みがありません。
そこで、ちょっとした負荷分散機能をNOXというフレームワークを使って実装してみます。
(あくまでサンプルなので、実用性や性能は考慮していません。あしからず。)
なお、本記事ではControllerのプログラム言語にPythonを使用しています。

仕様

DNSラウンドロビンという負荷分散のやり方がありますが、今回はDNSは使わずに、同一のIPアドレスを持った複数のHostに対して、他のHostからTCP接続を試みると順番に(ラウンドロビンで)接続する形にしてみます。

ネットワーク図

これまでの記事(その1その2)で構築したネットワークは以下のとおり。

VMで作るOpenFlowネットワーク

VMで作るOpenFlowネットワーク

ネットワーク構成はそのままで、このような形になるようにします。

同一IPを使った負荷分散イメージ

大まかな手順

  1. NOXのインストール&動作確認
  2. Controllerの作成
  3. 動作確認

前提条件

  1. これまでの記事(VMWare Serverで、OpenFlowの環境を作ってみた[1/2]およびVMWare Serverで、OpenFlowの環境を作ってみた[2/2])の環境が構築済みである前提です。

手順

  1. 事前準備(sshサーバのインストール)
  2. 最終的にHost1からHost2およびHost3にsshで接続して動作確認を行うので、Host2とHost3にsshサーバをインストールしておきます。
    Host2とHost3の両方のVMにおいて、「VMWare Serverで、OpenFlowの環境を作ってみた[1/2]」の「手順2. OpenFlowをインストール」を参考にNetwork Adapterやプロキシを設定し、外部へ接続できる状態にしてから、以下を実行してください。

    $ sudo apt-get update
    $ sudo apt-get -y install ssh
    

    Host2とHost3のsshの公開鍵が異なると後で面倒なので、公開鍵をコピーして同じものにしておきます。
    Host2にある公開鍵をssh_keys.tarに保存

    $ sudo tar cvf ssh_keys.tar /etc/ssh/ssh_host*
    

    Host2で保存したssh_keys.tarをHost3にコピーした後、Host3にて展開

    $ cd /
    $ sudo tar xvf ~/ssh_keys.tar
    

    以上の作業を終えたら、Network Adapterの設定等を元に戻し、Host2、Host3をリブートしてください。

  3. NOXのインストール
  4. NOXとはOpenFlowのControllerを作るためのフレームワークです。C++やPythonを使って比較的容易にControllerを作ることができます。
    (が、あくまでも「比較的」。OpenFlowの知識とプログラミングの知識は必須です。)
    NOXはControllerになるので、インストールはControllerのVMで行います。インストール手順は、こちらのNOXのドキュメントのとおりですが、以下に詳細を記載します。

    • VMのネットワークの設定
    • 外部からパッケージを取得するので、ネットワーク設定を外部にアクセスできる状態にする必要があります。
      Controllerが動作するVMについて、「VMWare Serverで、OpenFlowの環境を作ってみた[1/2]」の「手順2. OpenFlowをインストール」の時のネットワーク設定(VMのNetwork AdapterやUbuntuのeth0の設定、プロキシ設定等)にしてください。

    • NOXのインストールに必要なもののインストール
    • $ cd /etc/apt/sources.list.d
      $ sudo wget http://openflowswitch.org/downloads/debian/nox.list
      $ sudo apt-get update
      $ sudo apt-get install nox-dependencies
      

      ※最後のnox-dependenciesのインストールで確認メッセージが出ますが、すべてYesで。

    • NOXのインストール
    • NOXのダウンロード

      $ cd ~
      $ git clone git://noxrepo.org/nox
      $ cd nox
      

      コンパイル

      $ ./boot.sh
      $ mkdir build/
      $ cd build/
      $ ../configure
      $ make -j 5
      

      確認

      $ cd src
      $ ./nox_core -h
      

      でヘルプメッセージが出力されればOK。
      なお、今回ビルドしたバージョンは以下の通り。

      $ ./nox_core -V
      NOX 0.9.0(zaku)~full~beta (nox_core), compiled Jul  9 2011 03:32:00
      Compiled with OpenFlow 0x01
      

      ※余談ですが、”zaku”とあるのはあのザクです。詳しくはこちら

  5. ネットワーク設定
  6. 基本的に「VMWare Serverで、OpenFlowの環境を作ってみた[2/2]」の「手順4.設定」とほぼ同じネットワーク設定ですが、同一IPで負荷分散したいので、次のような設定にしてください。

    Role VM名 Network Adapter IPアドレス
    Controller OpenFlow01 HostOnly 192.168.1.1/24
    Switch OpenFlow02 HostOnly 192.168.1.2/24
    HostOnly (2) no ip address
    HostOnly (3) no ip address
    HostOnly (4) no ip address
    Host1 OpenFlowHost1 HostOnly (2) 192.168.2.1/24
    Host2 OpenFlowHost2 HostOnly (3) 192.168.2.2/24
    Host3 OpenFlowHost3 HostOnly (4) 192.168.2.2/24
  7. NOXの動作確認
  8. すべてのVMを起動し、SwitchのVMでSwitchのソフトウェアを起動してください。

    Switchを起動:OpenFlow02にて2つの異なるターミナルで以下を実行

    $ sudo ofdatapath punix:/var/run/dp0.sock -i eth1,eth2,eth3 -v --no-local-port
    
    $ sudo ofprotocol unix:/var/run/dp0.sock tcp:192.168.1.1:6633 -v --out-of-band
    

    Controller実行

    $ cd ~/nox/build/src
    $ ./nox_core -v -i ptcp:6633 pyswitch
    

    ところが、

    00003|nox|ERR:Cannot change the state of 'python' to INSTALLED:
    'python' ran into an error:
            Unable to construct a Python component:
            Traceback (most recent call last):
              File "./nox/coreapps/pyrt/pyoxidereactor.py", line 364, in instance
    

    NOX実行エラー画面


    とエラーが出てしまいます。
    対処法はこの記事にあります。以下はこの記事通りの対処です。

    $ sudo vi +1125 /usr/lib/python2.6/dist-packages/twisted/internet/base.py
    

    をして、_handleSignals関数の定義の後(1125行目あたり)に、以下の関数定義を追加してください。(インデントに注意。)

        def _handleSigchld(self, signum, frame, _threadSupport=platform.supportsThreads()):
            from twisted.internet.process import reapAllProcesses
            if _threadSupport:
                self.callFromThread(reapAllProcesses)
            else:
                self.callLater(0, reapAllProcesses)
    

    対処ができたら、再度サンプルのControllerソフトウェアを起動してください。

    $ ./nox_core -v -i ptcp:6633 pyswitch
    

    エラーがでないようであれば、試しにHost2とHost3から同時に

    $ ping 192.168.2.1
    

    をやってみてください。
    Host2とHost3が一定間隔で交互にpingの応答を受け取る感じになっているかと思います。
    おそらく、Host1がARPしなおす時に切り替わるのかと思いますが、詳しいことは聞かないでください(^^;
    なお、pyswitchは普通のスイッチングハブと同様の動作なので、負荷分散的な動作はしません。
    試しにHost1から192.168.2.2に対して何回かsshをしてみれば、Host2かHost3いずれかのみにつながることを確認できると思います。

    $ ssh 192.168.2.2 uname -n
    

    実行結果

    (おまけ)pyswitch.pyの解説

    上記で起動したNOX付属のサンプル、pyswitchは、OpenFlowに付属するControllerソフトウェアのサンプルと同様の動作をします。すなわち、普通のスイッチングハブです。
    pyswitchのソースコードは、
    ~/nox/build/src/nox/coreapps/examples/pyswitch.py
    にあります。

    class pyswitch(Component):
    
        def __init__(self, ctxt):
            global inst
            Component.__init__(self, ctxt)
            self.st = {}
    
            inst = self
    
        def install(self):
            inst.register_for_packet_in(packet_in_callback)
            inst.register_for_datapath_leave(datapath_leave_callback)
            inst.register_for_datapath_join(datapath_join_callback)
            inst.post_callback(1, timer_callback)
    
        def getInterface(self):
            return str(pyswitch)
    
    
    • 161行目
    • Switchが受信した(かつSwitch側で処理されなかった)パケットがControllerに来た時に、”packet_in_callback”関数が呼ばれるようにする。

    • 162行目
    • SwitchとControllerの接続が切れた時に、”datapath_leave_callback”関数が呼ばれるようにする。

    • 163行目
    • SwitchとControllerの接続が確立されたときに、”datapath_join_callback”関数が呼ばれるようにする。

    def packet_in_callback(dpid, inport, reason, len, bufid, packet):
    
        if not packet.parsed:
            log.msg('Ignoring incomplete packet',system='pyswitch')
            
        if not inst.st.has_key(dpid):
            log.msg('registering new switch %x' % dpid,system='pyswitch')
            inst.st[dpid] = {}
    
        # don't forward lldp packets    
        if packet.type == ethernet.LLDP_TYPE:
            return CONTINUE
    
        # learn MAC on incoming port
        do_l2_learning(dpid, inport, packet)
    
        forward_l2_packet(dpid, inport, packet, packet.arr, bufid)
    
        return CONTINUE
    
    • 131行目
    • dpidはdatapathのID(SwitchとControllerの間の接続を表す)、inportはSwitchが受信したポート(スイッチングハブでいえばLANケーブルを差すインタフェースのこと。OpenFlowの仮想Switchではeth0等を表す)

    詳細は割愛しますが、”do_l2_learning”関数ではMACアドレスとポートの対応付けをpythonの辞書に登録しています。
    “forward_l2_packet”関数は”do_l2_learning”関数で登録された辞書を使って、Switch側に対してFlow定義を追加しています。
    Flow定義とは、Switch側で、条件に一致したパケットを指定された処理方法で自動で(Controllerを介さずに)処理させるためのものです。

    def forward_l2_packet(dpid, inport, packet, buf, bufid):    
        dstaddr = packet.dst.tostring()
        if not ord(dstaddr[0]) & 1 and inst.st[dpid].has_key(dstaddr):
            prt = inst.st[dpid][dstaddr]
            if  prt[0] == inport:
                log.err('**warning** learned port = inport', system="pyswitch")
                inst.send_openflow(dpid, bufid, buf, openflow.OFPP_FLOOD, inport)
            else:
                # We know the outport, set up a flow
                log.msg('installing flow for ' + str(packet), system="pyswitch")
                flow = extract_flow(packet)
                flow[core.IN_PORT] = inport
                actions = [[openflow.OFPAT_OUTPUT, [0, prt[0]]]]
                inst.install_datapath_flow(dpid, flow, CACHE_TIMEOUT, 
                                           openflow.OFP_FLOW_PERMANENT, actions,
                                           bufid, openflow.OFP_DEFAULT_PRIORITY,
                                           inport, buf)
        else:    
            # haven't learned destination MAC. Flood 
            inst.send_openflow(dpid, bufid, buf, openflow.OFPP_FLOOD, inport)
    
    • 90行目
    • 現在処理しているpacketから、Flow定義で使用する一致条件を”extract_flow”関数を使って抽出しています。すなわち現在処理しているpacketと同じ送信元&送信先MACアドレス等なら、Switch側で自動で処理されるようにする、というわけです。
      なお、”extract_flow”関数は
      ~/nox/build/src/nox/lib/util.py
      で定義されています。

    • 92行目
    • Flow定義で定義した条件に一致したpacketをどのように扱うのかを配列で指定しています。ここでは”prt[0]”、すなわち事前に辞書に登録されたポートに出力します。

    • 93行目
    • SwitchにFlow定義を追加する”install_datapath_flow”関数を呼び出しています。”install_datapath_flow”はNOXのドキュメントに(簡単な)説明があります。

    全体の動作を大まかにいうと、知らないMACアドレスからパケットが到着したら、それを辞書に登録します。次に辞書に登録されたMACアドレス宛のパケットがきたら、以降同様のパケットをSwitch側のみで処理できるようにFlow定義をSwitchに追加しています。
    これにより、いったんFlow定義したパケットをControllerに流さないでSwitchのみで効率的に処理できるようになります。
    なお、タイマーにより、使わなくなったMACアドレスの辞書エントリーを定期的に削除するようにしています。

  9. (負荷分散機能のある)Controllerの作成
  10. 長くなってしまいましたので、次回へ続きます。。

Excuse

私の英語能力の不足により、壮大な勘違いをしている箇所があるかもしれません。お気づきの点がありましたら、ご指摘いただければ大変ありがたいです。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

*