OpenFlowで、なんちゃって負荷分散を実装してみた[負荷分散実装編]

前回の続き。
いよいよ、負荷分散を行うControllerを実装してみます。

仕様(再掲)

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

ネットワーク図(再掲)

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

VMで作るOpenFlowネットワーク

VMで作るOpenFlowネットワーク

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

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

大まかな手順

  1. NOXのインストール&動作確認(前回済み)
  2. Controllerの作成
  3. 動作確認

手順(続き)

  1. (負荷分散機能のある)Controllerの作成
  2. 前回の記事で使用したpyswitchのソースコードを流用して、以下のようなControllerを作りました。
    このソースコードをmyswitch.pyという名前でpyswitchと同じ場所に保存してください。
    ~/nox/build/src/nox/coreapps/examples/myswitch.py
    ※テキスト版はこちら:myswitch.py

    from nox.lib.core     import *
    
    from nox.lib.packet.ethernet     import ethernet
    from nox.lib.packet.arp     import arp
    from nox.lib.packet.ipv4    import ipv4
    from nox.lib.packet.tcp    import tcp
    from nox.lib.packet.packet_utils import mac_to_str, mac_to_int, ip_to_str
    
    from twisted.python import log
    
    import logging
    from time import time
    from socket import htons
    from struct import unpack
    
    logger = logging.getLogger('nox.coreapps.examples.myswitch')
    
    # Global myswitch instance 
    inst = None
    
    # Timeout for cached MAC entries
    CACHE_TIMEOUT = 5
    
    # --
    # Given a packet, learn the source and peg to a switch/inport 
    # --
    def do_l2_learning(dpid, inport, packet):
        global inst 
    
        # learn MAC on incoming port
        srcaddr = packet.src.tostring()
        if ord(srcaddr[0]) & 1:
            return
        if inst.st[dpid].has_key(srcaddr):
            dst = inst.st[dpid][srcaddr]
            if dst[0] != inport:
                log.msg('MAC has moved from '+str(dst)+'to'+str(inport), system='myswitch')
            else:
                return
        else:
            log.msg('learned MAC '+mac_to_str(packet.src)+' on %d %d'% (dpid,inport), system="myswitch")
    
        # learn or update timestamp of entry
        inst.st[dpid][srcaddr] = (inport, time(), packet)
    
        # Replace any old entry for (switch,mac).
        mac = mac_to_int(packet.src)
    
    def ip_learning(macaddr,ipaddr,dpid,inport):
        global inst 
    
        if inst.ipmap.has_key(ipaddr):
            (index,macarr) = inst.ipmap[ipaddr];
            isexist = False
            for ipentry in range(len(macarr)):
                if macarr[ipentry][0] == macaddr:
                    inst.ipmap[ipaddr][1][ipentry] = (macaddr,time(),inport)
                    print 'Update mac:%s ip:%s' % (mac_to_str(macaddr),ip_to_str(ipaddr))
                    isexist=True
                    break
            if isexist == False:
                inst.ipmap[ipaddr][1].append((macaddr,time(),inport))
                print 'Append mac:%s ip:%s' % (mac_to_str(macaddr),ip_to_str(ipaddr))
        else:
            inst.ipmap[ipaddr]=(0,[(macaddr,time(),inport)])
            print 'Regist mac:%s ip:%s' % (mac_to_str(macaddr),ip_to_str(ipaddr))
            
    # --
    # If we've learned the destination MAC set up a flow and
    # send only out of its inport.  Else, flood.
    # --
    def forward_l2_packet(dpid, inport, packet, buf, bufid):    
        global inst
    
        print str(packet)
        actions = openflow.OFPP_FLOOD
        if isinstance(packet.next,arp):
            arppkt=packet.next
            print 'ARP: src-mac:%s src-ip:%s' % (mac_to_str(arppkt.hwsrc),ip_to_str(arppkt.protosrc))
            ip_learning(arppkt.hwsrc,arppkt.protosrc,dpid,inport)
        elif isinstance(packet.next,ipv4):
            ipv4pkt=packet.next
            ip_learning(packet.src,ipv4pkt.srcip,dpid,inport)
            if isinstance(ipv4pkt.next,tcp):
                tcppkt=ipv4pkt.next
                if tcppkt.flags == tcp.SYN and inst.ipmap.has_key(ipv4pkt.dstip):
                    (index,hosts) = inst.ipmap[ipv4pkt.dstip]
                    inst.tcpmap[(ipv4pkt.srcip,tcppkt.srcport,ipv4pkt.dstip,tcppkt.dstport)] = (packet.dst,hosts[index % len(hosts)][0],hosts[index % len(hosts)][2],inport)
                    actions = [[openflow.OFPAT_SET_DL_DST,hosts[index % len(hosts)][0]],[openflow.OFPAT_OUTPUT, [0,hosts[index % len(hosts)][2]]]]
                    inst.ipmap[ipv4pkt.dstip] = (index+1,hosts)
                else:
                    if inst.ipmap.has_key(ipv4pkt.dstip):
                        if inst.tcpmap.has_key((ipv4pkt.srcip,tcppkt.srcport,ipv4pkt.dstip,tcppkt.dstport)):
                            (orgmac,newmac,outport,srcport) = inst.tcpmap[(ipv4pkt.srcip,tcppkt.srcport,ipv4pkt.dstip,tcppkt.dstport)]
                            actions = [[openflow.OFPAT_SET_DL_DST,newmac],[openflow.OFPAT_OUTPUT, [0,outport]]]
                        elif inst.tcpmap.has_key((ipv4pkt.dstip,tcppkt.dstport,ipv4pkt.srcip,tcppkt.srcport)):
                            (orgmac,newmac,outport,srcport) = inst.tcpmap[(ipv4pkt.dstip,tcppkt.dstport,ipv4pkt.srcip,tcppkt.srcport)]
                            actions = [[openflow.OFPAT_SET_DL_SRC,orgmac],[openflow.OFPAT_OUTPUT, [0,srcport]]]
    
        inst.send_openflow(dpid, bufid, buf, actions, inport)
    
    # --
    # Responsible for timing out cache entries.
    # Is called every 1 second.
    # --
    def timer_callback():
        global inst
    
        curtime  = time()
        for dpid in inst.st.keys():
            for entry in inst.st[dpid].keys():
                if (curtime - inst.st[dpid][entry][1]) > CACHE_TIMEOUT:
                    log.msg('timing out entry'+mac_to_str(entry)+str(inst.st[dpid][entry])+' on switch %x' % dpid, system='myswitch')
                    inst.st[dpid].pop(entry)
    
        # ipmap
    #    for ipaddr in inst.ipmap.keys():
    #        for index in range(len(inst.ipmap[ipaddr][1])-1,-1,-1):
    #            if (curtime - inst.ipmap[ipaddr][1][index][1]) > 90:
    #                print 'timing out entry mac:%s ip:%s' % (mac_to_str(inst.ipmap[ipaddr][1][index][0]),ip_to_str(ipaddr))
    #                inst.ipmap[ipaddr][1].pop(index)
    #        if len(inst.ipmap[ipaddr][1]) == 0:
    #            inst.ipmap.pop(ipaddr)
    
        inst.post_callback(1, timer_callback)
    
        return True
    
    def datapath_leave_callback(dpid):
        logger.info('Switch %x has left the network' % dpid)
        if inst.st.has_key(dpid):
            del inst.st[dpid]
    
    def datapath_join_callback(dpid, stats):
        logger.info('Switch %x has joined the network' % dpid)
    
    # --
    # Packet entry method.
    # Drop LLDP packets (or we get confused) and attempt learning and
    # forwarding
    # --
    def packet_in_callback(dpid, inport, reason, len, bufid, packet):
    
        if not packet.parsed:
            log.msg('Ignoring incomplete packet',system='myswitch')
            
        if not inst.st.has_key(dpid):
            log.msg('registering new switch %x' % dpid,system='myswitch')
            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
    
    class myswitch(Component):
    
        def __init__(self, ctxt):
            global inst
            Component.__init__(self, ctxt)
            self.st = {}
            self.ipmap = {}
            self.tcpmap = {}
    
            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(myswitch)
    
    def getFactory():
        class Factory:
            def instance(self, ctxt):
                return myswitch(ctxt)
    
        return Factory()
    

    なお、このmyswitch.pyを使うには、同じディレクトリにあるmeta.jsonにmyswitchの記述を追加する必要があります。

    ~/nox/build/src/nox/coreapps/examples/meta.json
    のpyswitchの記述の下に以下を追加してください。

            {
                "name": "myswitch" ,
                "dependencies": [
                    "python"
                ],
                "python": "nox.coreapps.examples.myswitch"
            },
  3. 動作確認
  4. OpenFlow01にてControllerを次のように起動。

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

    次に、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やSwitchでエラーが出ていないようであれば、Host1から192.168.2.2宛にsshをしてみてください。

    $ ssh 192.168.2.2

    192.168.2.2に接続できたら、uname -n等で、ホスト名を確認し、Host2とHost3のどちらのVMに接続したか、確認してください。何回かsshをすれば、Host2とHost3で交互に接続されることを確認できると思います。

    $ uname -n
    OpenFlowHost2

    なお、”ssh 192.168.2.2 uname -n”とすれば、接続→ホスト名表示→切断が一行のコマンドでできます。

    実行例

  5. ソースコード(myswitch.py)の解説
  6. 基本的にpyswitchを流用したものなので、MACアドレスの学習機能(do_l2_learning)は同様です。
    ただ、pyswitchと大きく異なるのは、Switchに対してFlow定義を行わないで、すべてのパケットをController側で扱うことです。
    72行目からのforward_l2_packet関数では、Switchで受信したパケットについて、他のすべてのポートに送出する(リピータハブ相当の動作)か、もしくは送信先のMACアドレスを変更して送出(なんちゃって負荷分散)します。
    パケットをどのように送出するかは100行目のsend_openflow関数に渡す、actionsという変数で決まります。
    76行目のOFPP_FLOODのままですとリピータハブ相当の動作になります。89行目、95行目、98行目では配列でパケットの改変と特定ポートへの出力を行うよう指示しています。

    forward_l2_packet関数に渡されるpacket変数は、Etherフレームを表していますが、もしそのフレームがIPパケットならpacket.nextで返される値はIPパケットのクラスになり、ARPならARPのクラスになります。
    IPパケットのクラスも、next関数を呼び出すことで、UDPならUDPのクラス、TCPならTCPのクラスのインスタンスが得られます。
    77行目や81行目などでは受信したpacketがどんなプロトコルなのかをその仕組みを使って調べています。

    86行目のあたりでは、TCPの開始を表すSYNパケットを受信した場合に、そのTCPセッションのパケットの送出先を同一IPのいずれかのホストに固定し、次回以降、当該TCPセッションのパケットを受信した時にその固定したホストに向けて送出できるように、辞書に登録しています。

    myswitchでは、ipmapとtcpmapという2つの辞書を、パケットの処理を判断するために使用しています。

    ipmap
    キー IPアドレス
    インデックス値,MACアドレスの配列
    説明 ip_learning関数でMACアドレスとIPアドレスの対応をipmapに保存しています。今回は同じIPアドレスに対して複数のMACアドレスが存在しうるので、MACアドレスは配列で複数保存できるようにしています。インデックス値は、ラウンドロビンするMACアドレスを選ぶ際に使用するもので、SYNパケット受信毎に加算します。

    tcpmap
    キー 送信元IPアドレス,送信元ポート番号,送信先IPアドレス,送信先ポート番号
    変更前宛先MACアドレス,変更後宛先MACアドレス,出力ポート,入力ポート
    説明 TCPセッション毎に1エントリーとなります。TCPセッションごとに出入りするパケットのMACアドレスを変えたりするのに使用しています。

おわりに

今回はすべてのパケットをController側で判断する実装になっているので、性能はよくないです。
実は、SwitchにFlowを登録して、Switch側である程度パケットを制御して効率化するやり方も試みたのですが、うまくいかなかったのでこのような実装になっています。
NOXを使わない、という手もありますが、OpenFlowに付属のControllerのソースを眺めてみた感じでは簡単にはいかないようです。
とはいえ、いろいろ改善の余地がありますので、腕に覚えのある方は改造してみてください。
OpenFlowはControllerがキモなのですが、簡単にはイメージ通りのControllerを作れないのが課題ですね。
最後に、本記事内容に誤り等がありましたら、ご指摘いただければ幸いです。

コメントを残す

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

*