SDN网络的一大特点就是资源由控制器集中管理,控制器管理网络,最基本的当然需要知道网络的拓扑,而网络拓扑可能时时发生变化,所以控制器需要时时监测,对于整个网络来说,控制器担负了太多的计算任务,所以如果能够帮助控制器减压,则会提高整个网络的性能。本篇文章将以ryu控制器为例,首先介绍传统网络和现在SDN网络的拓扑发现原理,然后介绍改进算法,最后讲解改写后的代码逻辑。
一. LLDP拓扑发现原理
传统网络中的链路发现协议为LLDP(Link Layer Discovery Protocol),LLDP允许局域网中的结点告诉其他结点他自己的capabilities和neighbours。在传统以太网交换机中,交换机从自己的每个端口发送LLDP数据包,这个数据包不会被其他交换机转发,寿命只有一跳,LLDP负载被封装在以太网帧中,结构如下图,其中深灰色的即为LLDP负载,Chassis ID TLV, Port ID TLV和Time to live TLV三个是强制字段,分别代表交换机标识符(在局域网中是独一无二的),端口号和TTL。这个数据发出并被邻居结点收到之后进行解析,就可以知道这条链路的源目的交换机以及源目的接口。
二. ryu拓扑发现原理
OpenFlow的官方没有规定标准的拓扑发现方法,现在的OFDP(OpenFlow Discovery Protocol)利用的仍然是传统网络中的链路发现协议LLDP,接下来介绍ryu如何利用LLDP发现拓扑,假设现在有两个OpenFlow交换机连接在控制器上,如下图,简述拓扑发现步骤(以S1作为主体,S2的类似):
1. SDN控制器构造PacketOut消息向S1的三个端口分别发送上图所示的LLDP数据包,其中将Chassis ID TLV和Port ID TLV分别置为S1的dpid和端口号;
2. 控制器向交换机S1中下发流表,流表规则为:将从Controller端口收到的LLDP数据包从他的对应端口发送出去;
3. 控制器向交换机S2中下发流表,流表规则为:将从非Controller接收到LLDP数据包发送给控制器;
4. 控制器通过解析LLDP数据包,得到链路的源交换机,源接口,通过收到的PacketIn消息知道目的交换机和目的接口;
现在的ryu发现拓扑是对整个数据平面的所有交换机的所有端口发送PacketOut数据包,对于Fattree等网络来说,端口的数量是交换机数量的k倍,因此导致了很多资源的消耗,所以是否可以对这个拓扑发现的机制进行改进,让发送的PacketOut消息和交换机的数量相同?
三. 改进后的ryu拓扑发现机理
为了实现上面所提到的改进目标,需要将LLDP负载中的Port ID TLV进行改进,或者有其他的域和Port ID TLV一一映射也可以,这里提供一种解决办法,在LLDP数据包从交换机端口转发出去的时候,将这个以太网数据包的源MAC地址替换成为这个端口的MAC地址,而控制器在早先的配置阶段已经获得了关于交换机的端口的所有信息,所以对控制器来说,MAC地址和交换机的端口号是一一对应的,下面详细讲述改进方案。
1. 更新控制器的LLDP PacketOut消息数量,由一个端口一个,改为一个交换机一个PacketOut消息,LLDP数据包负载中的域Port ID TLV值置为零;
2. 控制器向流表下发一条规则:所有从端口Controller接收到的LLDP数据包,依次将其源MAC地址置为端口MAC地址,然后从相应的端口转发出去;
3. 更新控制器的PacketIn消息处理机制,根据LLDP数据包的来源,可以得到目的交换机,目的端口,通过解析LLDP数据包,得到源MAC和源交换机,通过源MAC地址查找对应的端口号;
4. 由于是修改的代码,所以不要忘了删除原来的以端口主导的相关代码。
四. 代码分析
首选需要添加的一些变量和类
* SwitchData类:包含了时间戳以及交换机所包含的LLDP数据Shell
- class SwitchData(object):
- #store the lldp information
- #send one LLDP information per switch
- def init (self, lldpdata):
- super(SwitchData, self).init()
- self.lldp_data = lldp_data
- self.timestamp = None
- self.sent = 0
- def lldp_sent(self):
- self.timestamp = time.time()
- self.sent += 1
- def lldp_received(self):
- self.sent = 0
- def lldp_dropped(self):
- return self.sent
- def clear_timestamp(self):
- self.timestamp = None
- def __str__(self):
- return 'SwitchData<timestamp=%s, sent=%d>' \
- % (self.timestamp, self.sent)
* SwitchDataState:类似于PortDataState类,继承自字典,保存从Switch类到SwitchData类的映射,维护了一个类似双向链表的数据结构Shell
- class SwitchDataState(dict):
- # dict: Switch class -> SwitchData class
- # slimed down version of OrderedDict as python 2.6 doesn't support it.
- _PREV = 0
- _NEXT = 1
- _KEY = 2
- def __init__(self):
- super(SwitchDataState, self).__init__()
- self._root = root = [] # sentinel node
- root[:] = [root, root, None] # [_PREV, _NEXT, _KEY]
- # doubly linked list
- self._map = {}
- def _remove_key(self, key):
- link_prev, link_next, key = self._map.pop(key)
- link_prev[self._NEXT] = link_next
- link_next[self._PREV] = link_prev
- def _append_key(self, key):
- root = self._root
- last = root[self._PREV]
- last[self._NEXT] = root[self._PREV] = self._map[key] = [last, root, key]
- def _prepend_key(self, key):
- root = self._root
- first = root[self._NEXT]
- first[self._PREV] = root[self._NEXT] = self._map[key] = [root, first, key]
- def _move_last_key(self, key):
- self._remove_key(key)
- self._append_key(key)
- def _move_front_key(self, key):
- self._remove_key(key)
- self._prepend_key(key)
- def add_switch(self, dp, lldp_data):
- if dp not in self:
- self._prepend_key(dp)
- self[dp] = SwitchData( lldp_data)
- def lldp_sent(self, dp):
- switch_data = self[dp]
- switch_data.lldp_sent()
- self._move_last_key(dp)
- return switch_data
- def lldp_received(self, dp):
- self[dp].lldp_received()
- def move_front(self, dp):
- switch_data = self.get(dp, None)
- if switch_data is not None:
- switch_data.clear_timestamp()
- self._move_front_key(dp)
- def get_switch(self, dp):
- return self[dp]
- def del_port(self, dp):
- del self[dp]
- self._remove_key(dp)
- def __iter__(self):
- root = self._root
- curr = root[self._NEXT]
- while curr is not root:
- yield curr[self._KEY]
- curr = curr[self._NEXT]
- def clear(self):
- for node in self._map.values():
- del node[:]
- root = self._root
- root[:] = [root, root, None]
- self._map.clear()
- dict.clear(self)
- def items(self):
- 'od.items() ->;gt; list of (key, value) pairs in od'
- return [(key, self[key]) for key in self]
- def iteritems(self):
- 'od.iteritems -> an iterator over the (key, value) pairs in od'
- for k in self:
- yield (k, self[k])
接着简述修改的核心代码,对应上面第三部分提到的四点
1. 更新控制器的LLDP PacketOut消息数量,由一个端口一个,改为一个交换机一个PacketOut消息,LLDP数据包负载中的域Port ID TLV值置为零;
- #construct LLDP packet for switch
- def switchadded(self, dp):
- lldpdata = LLDPPacket.lldppacket(
- dp.dp.id, 0, '00:00:00:00:00:00', self.DEFAULTTTL)
- self.switches.addswitch(dp, lldpdata)
2.控制器向流表下发一条规则:所有从端口Controller接收到的LLDP数据包,依次将其源MAC地址置为端口MAC地址,然后从相应的端口转发出去;
- if dp.ofproto.OFPVERSION>= ofprotov12.OFPVERSION:
- for portinfor in self.portstate[dp.id].values():
- if portinfor.name != "tap:":
- actions.append(dp.ofprotoparser.OFPActionSetField(ethsrc=portinfor.hwaddr))
- actions.append(dp.ofprotoparser.OFPActionOutput(portinfor.portno))
- #actions = [dp.ofprotoparser.OFPActionOutput(self.portstate[dp].portno)]
- out = dp.ofprotoparser.OFPPacketOut(
- datapath=dp, inport=dp.ofproto.OFPP_CONTROLLER,
- bufferid=dp.ofproto.OFPNOBUFFER, actions=actions,
- data=switchdata.lldpdata)
- dp.sendmsg(out)
- else:
- LOG.error('cannot send lldp packet. unsupported version. %x',
- dp.ofproto.OFPVERSION)
3.更新控制器的PacketIn消息处理机制,根据LLDP数据包的来源,可以得到目的交换机,目的端口,通过解析LLDP数据包,得到源MAC和源交换机,通过源MAC地址查找对应的端口号;
- for port in self.portstate[srcdpid].values():
- if port.hwaddr == srcmac:
- srcportno = port.portno
4.由于是修改的代码,所以不要忘了删除原来的以端口主导的相关代码。完整的代码见github。
五. 实验验证
用Mininet建立一个二层二叉树,s3作为根节点分别连接s1和s2。如下图:
写个简单的ryu应用来调用拓扑发现模块提供的API接口,应用为topo_learner.py,代码见github用wireshark抓取OpenFlow和LLDP数据包来进行验证
首先抓取交换机对控制器的响应消息,查看交换机的端口以及对应的MAC地址,从解析可以看到这是s3交换机,拥有四个端口(分别连接控制器,s1,s2,h3),下图是截取到的一个LLDP数据包,可以看出图中蓝色背景的LLDP的数据包的源MAC地址是s3交换机的3端口的MAC地址,说明前面的代码修改成功。