本文中主要是与大家分享一下LLDP在ODL中的实现以及其源码分析,主要内容涉及ODL控制器中LLDP帧的产生及发送。文章都是个人理解,希望能够帮助到大家,更希望可以一起讨论看法不一的地方。
1 LLDP简介
ODL使用LLDP实现链路检测。LLDP,即链路层发现协议,是一种数据链路层协议,网络设备可以通过在本地网络中发送LLDPDU(Link Layer Discovery Protocol Data Unit)来通告其他设备自身的状态,是一种能够使网络中的设备互相发现并通告状态、交互信息的协议。
LLDP的以太网类型为0x88cc,一个标准的LLDP帧格式如图1:
图1
图2
2 ODL中的LLDP
在ODL中,发送LLDP帧的工作由控制器来完成。如图3所示:
图3
在上图中,LLDP帧在控制器中生成,通过openflow协议packet_out消息发给交换机1,交换机1收到来自控制器的LLDP帧后,会转发给直邻交换机2,其直邻交换机2收到LLDP帧后,通过openflow的packet_in消息发给控制器,控制器据此做链路检测。
3 LLDP帧的产生和发送
LLDP帧产生和发送的源码位于ODL中openflowplugin子项目中,具体位置为:openflowplugin/applications/lldp-speaker,其源码目录如下图4:
图4
该模块主要分为两个功能:
- 监听MD-SAL中的inventory data tree中的端口信息,学习并检测端口信息的变化,产生与端口一一对应的LLDP帧,然后将其存入本地哈希nodeConnectorMap中,此哈希表键为端口IID,值为端口对应LLDP帧;
- 周期性遍历本地哈希表nodeConnectorMap,将哈希表中的LLDP帧通过packet_out消息发往相应的交换机。
3.1 监听端口信息
源码位于NodeConnectorInventoryEventTranslator.java中,首先构造监听节点的IID,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//监听inventory data tree 下openflow的端口状态 private static final InstanceIdentifier<State> II_TO_STATE = InstanceIdentifier.builder(Nodes.class) .child(Node.class) .child(NodeConnector.class) .augmentation(FlowCapableNodeConnector.class) .child(State.class) .build(); //监听inventory data tree 下openflow的端口 private static final InstanceIdentifier<FlowCapableNodeConnector> II_TO_FLOW_CAPABLE_NODE_CONNECTOR = InstanceIdentifier.builder(Nodes.class) .child(Node.class) .child(NodeConnector.class) .augmentation(FlowCapableNodeConnector.class) .build(); |
由IID在MD-SAL中注册监听器,实现数据监听,源码如下:
1 2 3 4 5 6 7 8 9 10 |
//监听端口 dataChangeListenerRegistration = dataBroker.registerDataChangeListener( LogicalDatastoreType.OPERATIONAL, II_TO_FLOW_CAPABLE_NODE_CONNECTOR, this, AsyncDataBroker.DataChangeScope.BASE); //监听端口状态 listenerOnPortStateRegistration = dataBroker.registerDataChangeListener( LogicalDatastoreType.OPERATIONAL, II_TO_STATE, this, AsyncDataBroker.DataChangeScope.SUBTREE); |
当监听到端口信息变化之后,需要依据端口状态,判定是否生成相应的LLDP帧。若需要生成LLDP帧,则生成之后存入本地哈希表nodeConnectorMap中,具体源码位于函数onDataChanged之中。即如下:
1 |
public void onDataChanged(AsyncDataChangeEvent<InstanceIdentifier<?>, DataObject> change) {...} |
其中对于change.getCreatedData(),其源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
for (Map.Entry<InstanceIdentifier<?>, DataObject> entry : change.getCreatedData().entrySet()) { InstanceIdentifier<NodeConnector> nodeConnectorInstanceId = entry.getKey().firstIdentifierOf(NodeConnector.class); if (compareIITail(entry.getKey(),II_TO_FLOW_CAPABLE_NODE_CONNECTOR)) { FlowCapableNodeConnector flowConnector = (FlowCapableNodeConnector) entry.getValue(); if (!isPortDown(flowConnector)) { notifyNodeConnectorAppeared(nodeConnectorInstanceId, flowConnector); //如果端口开启 } else { iiToDownFlowCapableNodeConnectors.put(nodeConnectorInstanceId, flowConnector); // } } } |
代码分析:对于change.getCreatedData(),即为datastore中新增端口,首先调用函数isPortDown(flowConnector)判断端口此时状态:若返回true,端口为UP,直接生成端口对应LLDP帧,然后存入哈希表nodeConnectorMap;若返回false,端口为DOWN,则将端口存入哈希表iiToDownFlowCapableNodeConnectors,之后如果端口状态变为UP,依然会生成对应LLDP帧,存入哈希表nodeConnectorMap中。
此处说明一下,端口为down,一般指端口或者端口处link存在问题,无法正常转发数据包。
对于change.getUpdatedData(),其源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// Iterate over updated node connectors (port down state may change) for (Map.Entry<InstanceIdentifier<?>, DataObject> entry : change.getUpdatedData.entrySet()) { InstanceIdentifier<NodeConnector> nodeConnectorInstanceId = entry.getKey().firstIdentifierOf(NodeConnector.class); if (compareIITail(entry.getKey(),II_TO_FLOW_CAPABLE_NODE_CONNECTOR)) { FlowCapableNodeConnector flowConnector = (FlowCapableNodeConnector) entry.getValue(); if (isPortDown(flowConnector)) { notifyNodeConnectorDisappeared(nodeConnectorInstanceId); } else { notifyNodeConnectorAppeared(nodeConnectorInstanceId, flowConnector); } } else if (compareIITail(entry.getKey(),II_TO_STATE)) { FlowCapableNodeConnector flowNodeConnector = iiToDownFlowCapableNodeConnectors.get(nodeConnectorInstanceId); if (flowNodeConnector != null) { State state = (State)entry.getValue(); if (!state.isLinkDown()) { FlowCapableNodeConnectorBuilder flowCapableNodeConnectorBuilder = new FlowCapableNodeConnectorBuilder(flowNodeConnector); flowCapableNodeConnectorBuilder.setState(state); notifyNodeConnectorAppeared(nodeConnectorInstanceId, flowCapableNodeConnectorBuilder.build()); iiToDownFlowCapableNodeConnectors.remove(nodeConnectorInstanceId); } } } } |
代码分析:对于更新数据为端口,调用函数isPortDown(flowConnector),判断端口状态:若端口为UP,则直接调用函数notifyNodeConnectorAppeared(nodeConnectorInstanceId, flowConnector);存入哈希表nodeConnectorMap;若端口为DOWN,则调用函数notifyNodeConnectorDisappeared(nodeConnectorInstanceId),从哈希表nodeConnectorMap中移除。
若更新数据为端口状态,则检查其对应端口是否在哈希表iiToDownFlowCapableNodeConnectors中,若存在且其端口链路状态为true,则生成此端口对应LLDP帧,存入哈希表nodeConnectorMap中,并且将其从哈希表iiToDownFlowCapableNodeConnectors中移出。
对应change.getRemovedPaths(),源码如下:
1 2 3 4 5 6 7 8 |
// Iterate over removed node connectors for (InstanceIdentifier<?> removed : change.getRemovedPaths()) { if (compareIITail(removed,II_TO_FLOW_CAPABLE_NODE_CONNECTOR)) { InstanceIdentifier<NodeConnector> nodeConnectorInstanceId = removed.firstIdentifierOf(NodeConnector.class); notifyNodeConnectorDisappeared(nodeConnectorInstanceId); } } } |
代码分析:对于删除的端口,则直接将其从哈希表nodeConnectorMap中删除即可。
3.2 LLDP帧生成
根据前文分析,当检测到新的端口时,会调用函数notifyNodeConnectorAppeared先生成对应LLDP帧,然后存入哈希表nodeConnectorMap中。其中生成LLDP帧源码如下:
1 2 3 4 5 6 |
// Generate packet with destination switch and port TransmitPacketInput packet = new TransmitPacketInputBuilder() .setEgress(new NodeConnectorRef(nodeConnectorInstanceId)) .setNode(new NodeRef(nodeInstanceId)) .setPayload(LLDPUtil.buildLldpFrame( nodeId, nodeConnectorId, srcMacAddress, outputPortNo, addressDestionation)).build(); |
代码分析:其中负责生成LLDP以太网帧的函数为
LLDPUtil.buildLldpFrame(nodeId, nodeConnectorId, srcMacAddress, outputPortNo, addressDestionation)
此函数在LLDPUtil.java文件中实现,主要逻辑是依次生成LLDPDU中的ChassisID TLV,PortID TLV,TTL TLV等字段,然后添加LLDP帧的以太网包头。由于代码逻辑比较清晰,此处不再详细叙述。
3.3 LLDP帧周期性发送
ODL中会周期性的发送LLDP帧,去检测网络设备间link的有效性。LLDP帧周期性发送在LLDPSpeaker.java中实现,采用以下代码实现周期性的执行任务:
1 2 3 |
private final ScheduledExecutorService scheduledExecutorService; private ScheduledFuture<?> scheduledSpeakerTask; scheduledSpeakerTask = this.scheduledExecutorService.scheduleAtFixedRate(this, LLDP_FLOOD_PERIOD,LLDP_FLOOD_PERIOD, TimeUnit.SECONDS); |
其中LLDP_FLOOD_PERIOD值为5,即每过5秒会将哈希表nodeConnectorMap中所有的LLDP帧发送给相应交换机。
函数setOperationalStatus将会设置LLDP机制的工作状态,RUN表示运行状态,即会周期性的发送LLDP帧,进行链路检测;STANDBY表示待命状态,即没有开启LLDP链路检测,不会周期性的发送LLDP帧。源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public void setOperationalStatus(final OperStatus operationalStatus) { LOG.info("Setting operational status to {}", operationalStatus); this.operationalStatus = operationalStatus; if (operationalStatus.equals(OperStatus.STANDBY)) { scheduledSpeakerTask.cancel(false); //参数false表示,如果任务在执行,则等待任务结束后再取消;若为参数true表示立即取消 } else if (operationalStatus.equals(OperStatus.RUN)) { //如果没有任务,或者任务已经取消,则开启定时任务 if (scheduledSpeakerTask == null || scheduledSpeakerTask.isCancelled()) { scheduledSpeakerTask = this.scheduledExecutorService.scheduleAtFixedRate(this, LLDP_FLOOD_PERIOD, LLDP_FLOOD_PERIOD, TimeUnit.SECONDS); } } } |
遍历哈希表nodeConnectorMap,发送LLDP帧的操作在run()函数完成中,其中是调用rpc方法transmit-packet发送LLDP帧,此rpc在packet-processing.yang中被定义。
1 2 3 4 5 6 7 8 9 10 11 12 |
@Override public void run() { if (OperStatus.RUN.equals(operationalStatus)) { LOG.debug("Sending LLDP frames to {} ports...", nodeConnectorMap.keySet().size()); //发送端口的数量 for (InstanceIdentifier<NodeConnector> nodeConnectorInstanceId : nodeConnectorMap.keySet()) { NodeConnectorId nodeConnectorId = InstanceIdentifier.keyOf(nodeConnectorInstanceId).getId(); LOG.trace("Sending LLDP through port {}", nodeConnectorId.getValue()); //发送端口的端口号 packetProcessingService.transmitPacket(nodeConnectorMap.get(nodeConnectorInstanceId)); //调用rpc方法transmit-packet发送LLDP包 } } } |
由于篇幅有限,源码只选择重要部分粘贴,完整源码请从以下链接下载:
https://github.com/opendaylight/openflowplugin/tree/stable/beryllium。
四、总结
上文已经具体分析了LLDP帧的生成和周期发送的逻辑以及关键代码。当LLDP帧发送,经过转发再次被发给控制器时,控制器需要从中提取出链路信息,并以此检测已经发现的链路是否老化,以及是否发现新的链路。这部分的源码将在下一篇文章《LLDP在ODL中的实现及源码分析(二)》中分析。
作者简介:徐志阳 2015.09-至今 于北京邮电大学信息光子学与光通信国家重点实验室攻读硕士研究生
鸣谢:本文是作者实习期间所做总结,感谢中国电信北京研究院SDN技术研发中心对本文的指导和支持。