SDN实验(四):测定链路时延

当一个 SDN 网络建立时,Ryu 控制器通过 LLDP 数据包与交换机通信,获取网络的各种信息。借助 LLDP 数据包上的时间戳,控制器可以测定两个交换机之间的传输时延,用于寻找数据传输的最短路径等。

在 Ryu 中维护端口时延

Ryu 维护的端口信息在ryu/ryu/topology/switch.py中定义为PortData类,可以修改其__init__来增加属性。当 LLDP 数据包规划从某个端口发送时,对应实例就会更新时间戳timestamp = time.time()

这里再加上一个时延属性

1
self.delay = 0

用于记录根据该端口转发出的 LLDP 包计算的时延。听来有些拗口,若用图示转发过程

Controller
Contro...
S1
S1
S2
S2
Viewer does not support full SVG 1.1

则 LLDP 包的生命周期是:(以绿色箭头所示为例)

  1. 由 Ryu 控制器构造,发送时对应 S2 交换机上某个端口的PortData实例更新时间戳。
  2. 被 S2 交换机转发给 S1 交换机。
  3. 被 S1 交换机发送给控制器,触发 Packet-In ,控制器收到后,可以解析出这个 LLDP 包的源端口是 S2 上的某个端口。

RyuApp 是运行于控制器的程序,维护的PortData也都在控制器端。第一步的时间戳是控制器发送 LLDP 包的时间,第三步的“当前时间”是控制器收到包的时间。因此,用当前时间减去时间戳,得到的就是 LLDP 数据包经过三步所用的总时延。这个时延维护在PortData实例的delay属性里。

为了用上述方法更新delay属性,还需要改动ryu/ryu/topology/switch.py中的Switches类,在它的lldp_packet_in_handler方法中更新self.ports[1]维护的所有端口。

用类似的方法可以得到反方向(红色箭头所示)的时延。

估计往返时延 RTT

网络链路的时延是实时变化的,只能进行估计式的测量。

LLDP 数据包被控制器发给交换机后,转发一次(一跳)就会被再次发回控制器。在上方图示的过程中,往返两次转发的总时延可以表示为 tlldp1+tlldp2t_{lldp1}+t_{lldp2} ,另一方面,这也是如下两项的和:

  • 交换机 S1 和 S2 之间的往返时延 RTT1,2RTT_{1,2}​​
  • 控制器到两台交换机的往返时延 techo1+techo2t_{echo1}+t_{echo2}

由此得到 RTT1,2=tlldp1+tlldp2(techo1+techo2)RTT_{1,2}=t_{lldp1}+t_{lldp2}-(t_{echo1}+t_{echo2})​​​​ ,可以用这个式子估计往返时延[2](忽略了构造、解析、转发数据包的时间)。​

OpenFlow 的 Echo Request/Reply 消息可以用于测量 techo1,techo2t_{echo1},t_{echo2}​ 两个时延,具体方法是:周期性发送以时间戳作为数据的 Echo Request ,时间戳会被 Echo Reply 回送,与当前时间相减即得时延。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@set_ev_cls(ofp_event.EventOFPEchoReply, MAIN_DISPATCHER)
def _echo_reply_handler(self, ev):
try:
# evaluate the echo delay from controller to `ev.msg.datapath`
self.echo_delay[ev.msg.datapath.id] = delay
except:
return

def _send_echo_request(self):
# run as a coroutine
while True:
for each datapath
# construct and send echo request
# sleep some time

网络同步对数据的影响

应用运行在控制器上,但数据需要等待网络传输。LLDP 时延和 Echo 往返时延都需要等待数据包传回,任何一个为空都不能计算 RTT ;即使这些数据都就位,也未必正确。

控制器端调用dp.send_msg,会把相应数据包送入消息队列。如果大量数据包同时发送,有些数据包可能等待一段时间,但附加在数据包中的时间戳是送入队列的时间,并非实际发出的时间。于是 Echo 往返时延有可能异常地长——例如数十毫秒——甚至比测出的 LLDP 时延还要长。依此计算,网络的拓扑图中将出现负权边,即到达在发送之前,这显然不可能。

那么,一方面需要让 Echo Request 较为均匀地发送,避免集中;另一方面需要检查算出的链路往返时延是否为负,避免构造出负权边。

协程调度问题

实验是在 ARPANET 网络拓扑上进行,一共有三个协程:

  • 消息处理协程(由 App Manager 启动的主协程)
  • 更新拓扑并下发生成树的协程(解决 ARP 广播风暴)
  • 发送 Echo Request 获取时延的协程

每个协程都要等待其他协程主动让出 CPU 才能执行,因此任何一个协程必须控制自己的执行时间,否则其他协程可能不能正常执行[3]。对于发送 Echo Request 的协程,每发送一轮之后应该hub.sleep主动休眠足够长的时间,所以最后的_send_echo_request是这样:

1
2
3
4
5
6
def _send_echo_request(self):
while True:
for dpid in self.id2dp:
# construct and send echo request
hub.sleep(0.1)
hub.sleep(1)

说明

SDN 课内实验共有四次,第一次的“自学习交换机”参考资料多如牛毛,我自不必赘述;最后一次实验介绍的 VeriFlow 工具,精华在于论文[4],对此我只有献丑之技,没有剖析之能,亦不再记录。

这几篇文章大多来自作业和实验报告,以提取思路、增进理解为主,增删过一些内容。时间所限,我并没有太多时间将实验的内容和相关技术资料认真整理成笔记,只借此简单提取、留档;也希望可以作为日后同学粗疏的参考。若有所助力,不胜荣幸。

Ryu 的资料主要有两个来源,均为官方维护人员编写:

注释

[1]^这是一个键为Port实例,值为PortData实例的字典。

[2]^思路来自实验指导书。

[3]^我的程序因此出现了各种奇怪的 Bug ,包括但不限于:时而正常工作时而卡在发送 Echo Request 的过程中、根据 Echo Reply 计算的时延很大(很小)、始终不能向所有的交换机发出 Echo Request ,等等。不外乎是影响了消息处理和更新拓扑的协程正常工作。

[4]^Ahmed Khurshid, Xuan Zou, Wenxuan Zhou, Matthew Caesar, P. Brighten Godfrey. VeriFlow: Verifying Network-Wide Invariants in Real Time. In NSDI