前回の続き。
いよいよ、負荷分散を行うControllerを実装してみます。
仕様(再掲)
DNSラウンドロビンという負荷分散のやり方がありますが、今回はDNSは使わずに、同一のIPアドレスを持った複数のHostに対して、他のHostからTCP接続を試みると順番に(ラウンドロビンで)別々のHostに接続する形にしてみます。
ネットワーク図(再掲)
これまでの記事(その1、その2)で構築したネットワークは以下のとおり。
ネットワーク構成はそのままで、このような形になるようにします。
大まかな手順
- NOXのインストール&動作確認(前回済み)
- Controllerの作成
- 動作確認
手順(続き)
- (負荷分散機能のある)Controllerの作成
- 動作確認
- ソースコード(myswitch.py)の解説
前回の記事で使用した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" },
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”とすれば、接続→ホスト名表示→切断が一行のコマンドでできます。
基本的に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つの辞書を、パケットの処理を判断するために使用しています。
キー | IPアドレス |
値 | インデックス値,MACアドレスの配列 |
説明 | ip_learning関数でMACアドレスとIPアドレスの対応をipmapに保存しています。今回は同じIPアドレスに対して複数のMACアドレスが存在しうるので、MACアドレスは配列で複数保存できるようにしています。インデックス値は、ラウンドロビンするMACアドレスを選ぶ際に使用するもので、SYNパケット受信毎に加算します。 |
キー | 送信元IPアドレス,送信元ポート番号,送信先IPアドレス,送信先ポート番号 |
値 | 変更前宛先MACアドレス,変更後宛先MACアドレス,出力ポート,入力ポート |
説明 | TCPセッション毎に1エントリーとなります。TCPセッションごとに出入りするパケットのMACアドレスを変えたりするのに使用しています。 |
おわりに
今回はすべてのパケットをController側で判断する実装になっているので、性能はよくないです。
実は、SwitchにFlowを登録して、Switch側である程度パケットを制御して効率化するやり方も試みたのですが、うまくいかなかったのでこのような実装になっています。
NOXを使わない、という手もありますが、OpenFlowに付属のControllerのソースを眺めてみた感じでは簡単にはいかないようです。
とはいえ、いろいろ改善の余地がありますので、腕に覚えのある方は改造してみてください。
OpenFlowはControllerがキモなのですが、簡単にはイメージ通りのControllerを作れないのが課題ですね。
最後に、本記事内容に誤り等がありましたら、ご指摘いただければ幸いです。