Ryu控制器实现跳数最短路径转发:SDN入门实例全解析
最近把实验笔记重新翻了一遍发现一个特别值得拿出来讲的SDN入门实例用Ryu控制器实现基于跳数的最短路径转发。几乎所有接触SDN的人都会做几次这个练习但它把控制器的三大核心能力——拓扑发现、路径计算、流表下发——完整地串了起来跑通一次等于把整个SDN转发控制链路摸了一遍。这篇文章就围绕这个实例从原理拆解、代码实现到验证排错完整复盘一遍适合正在学Ryu、准备写转发类应用的同学参考。1. 项目拆解跳数最短路径转发到底在算什么先把这个实例的核心逻辑捋清楚。所谓“基于跳数的最短路径”指的是数据包从源交换机到目的交换机之间经过的跳数最少的路径。跳数的定义很简单相邻两台交换机之间算一跳主机接入的交换机不算跳数。整个网络可以抽象成一张无向图节点是交换机边是链路每条边的权重都等于1那么最短路径就是节点数最少的那条通路。1.1 跳数最短路径的基本概念与直观逻辑用生活场景类比一下。你坐地铁换乘从A站到B站中间经过多少个站台就是跳数如果有一条线只经过3个站、另一条线要绕5个站绝大多数人会选3站那条。这里的“3站”就是跳数最短。SDN里的转发也一样网络里两台交换机之间的通路往往不止一条跳数最少的路径意味着端到端时延相对更低、中间故障点更少、占用的交换资源更小。需要注意“跳数最少”并不一定是网络中的最优路径。实际网络中某条跳数多的链路可能带宽更高、时延更低但在入门阶段的转发应用里跳数是最直观、最容易度量的指标。以它作为目标可以让我们把注意力集中在“控制器如何感知网络拓扑并做出决策”这件事情上而不用过早陷入复杂的路由算法优化。1.2 为什么用Ryu实现这种转发策略Ryu是Python编写的开源SDN控制器框架之所以选用它主要出于三个原因。第一Ryu对OpenFlow协议支持非常完整从1.0到1.3都能处理而且默认就走OpenFlow 1.3——现在绝大多数交换机、模拟器都支持这个版本不容易踩协议版本的坑。第二Ryu内置了拓扑发现模块。它通过LLDP链路层发现协议自动搜集交换机、链路、主机的位置信息并且把拓扑变化以事件的形式抛给你。这意味着我们不用自己实现一套链路发现机制只要监听事件即可应用代码能做得非常精简。第三Python写算法太方便了。最短路径计算、邻接表构建这类逻辑用Python十几行就能搞定。如果你的实验环境里装了NetworkX库甚至可以直接调用现成的最短路函数不过为了理解透彻手写一遍BFS会更踏实。1.3 整体工作流程的顶层设计这个实例的运行流程可以分成四个阶段。第一个阶段是控制器启动连接交换机后下发table-miss流表把所有未匹配的包都交给控制器处理。第二个阶段是拓扑学习控制器监听交换机和链路的添加事件把全网拓扑缓存在内存里。第三个阶段是主机学习当有数据包通过packet-in到达控制器时从以太网头部提取源MAC和入端口记录该主机在哪台交换机哪个端口下。第四个阶段是路径决策与流表下发当目的MAC已经被学习到时用BFS计算源交换机到目的交换机之间的最短路径然后沿着路径逐台下发流表让后续的同目的数据包直接在数据平面转发不再上送控制器。整个过程的关键点是控制器并不负责每个包的转发它只负责“第一次见面时指路”后续的包全部由交换机的流表处理。这也是SDN控制平面与数据平面分离的核心思想。2. 环境搭建与网络仿真拓扑设计动手写代码之前先把环境准备好。这个实例用到的工具不多但版本和配置细节会影响整个实验能否一次跑通。2.1 基础环境依赖清单我使用的基础环境分四块Python 3.x、Ryu框架、Mininet网络仿真器、Open vSwitch。Python 3.8及其以上版本均可Ryu官方目前对Python 3的支持比较成熟。安装Ryu可以用pip直接装装完之后验证一下pip install ryu ryu-manager --versionMininet推荐通过源码或者系统包管理器安装。它自带Open vSwitch和所有命令行工具。安装完成后用sudo mn --version检查版本。这里有个容易忽略的点Ryu和Mininet的虚拟机网络配置。如果是在虚拟机里做实验建议把控制器的监听地址设置为0.0.0.0Ryu默认就是这个Mininet里创建拓扑时用--controllerremote并指定控制器IP。如果Mininet跑在本机直接用--controllerremote,ip127.0.0.1就行。2.2 用Mininet构造线性与环形拓扑为了验证最短路径转发的效果推荐先用线性拓扑再用环形拓扑对比。线性拓扑的命令很简单sudo mn --topolinear,4 --controllerremote --switchovsk,protocolsOpenFlow13这条命令会创建4台交换机串联每台交换机下面挂1台主机拓扑是s1-h1、s2-h2、s3-h3、s4-h4这样的结构。因为链路呈一条直线任意两台主机之间的最短路径是唯一的非常适合第一轮验证。环形拓扑可以用自定义Python脚本也可以用--topolinear,4之后手动添加一条s1-s4链路。环形拓扑的特点是存在两条通路比如从h1到h4既可以走s1-s2-s3-s4也可以走s1-s4这时候才能看出控制器是否真的挑选了跳数最少的那条路。我在实际测试中习惯先线性再环形先保证基础转发没问题再验证路径计算逻辑。2.3 验证控制器与交换机的连接环境启动后第一件事不是急着ping而是确认交换机已经成功连接到控制器。两种验证方式。一种是看Ryu的运行日志。Ryu启动后会打印交换机的连接信息包括datapath ID、端口数量等。如果你看到类似“instantiating datapath”的日志说明连接成功。另一种是抓包验证。在控制器和交换机通信的网卡上抓包能看到OpenFlow协议的Hello消息和Feature Request/Reply交互。这能帮你确认OpenFlow版本协商是否正确尤其是当交换机默认使用OpenFlow 1.0而控制器只用1.3时两者协商失败会导致交换机一直处于“等待连接”状态。有一个常见配置坑Mininet创建的交换机默认协议版本跟控制器不一致。所以我上面的命令里写了protocolsOpenFlow13强制交换机走OpenFlow 1.3避免协议协商失败。3. 核心代码实现从拓扑发现到流表下发这一部分是整个实例的重点。我会给出可运行的Ryu应用代码并逐段解释其中的设计逻辑。核心代码总行数不多大概150行左右但每一段都有对应到SDN底层机制的考量。3.1 控制器骨架与事件监听先写控制器的基本框架from ryu.base import app_manager from ryu.controller import ofp_event from ryu.controller.handler import MAIN_DISPATCHER, CONFIG_DISPATCHER from ryu.controller.handler import set_ev_cls from ryu.ofproto import ofproto_v1_3 from ryu.lib.packet import packet, ethernet, arp from ryu.lib.packet import ether_types from ryu.topology import event from ryu.topology.api import get_switch, get_link class ShortestHopForwarding(app_manager.RyuApp): OFP_VERSIONS [ofproto_v1_3.OFP_VERSION] def __init__(self, *args, **kwargs): super(ShortestHopForwarding, self).__init__(*args, **kwargs) self.switches [] self.links [] self.host_map {} # mac地址 - (dpid, port) self.datapaths {} # dpid - datapath对象 set_ev_cls(event.EventSwitchEnter) def switch_enter_handler(self, ev): self.switches get_switch(self, None) self.datapaths {sp.dp.id: sp.dp for sp in self.switches} self.logger.info(switch enter, current switches: %s, list(self.datapaths.keys())) set_ev_cls(event.EventLinkAdd) def link_add_handler(self, ev): self.links get_link(self, None) self.logger.info(link add, current links: %d, len(self.links))这里有两个细节值得解释。第一OFP_VERSIONS [ofproto_v1_3.OFP_VERSION]声明当前应用只支持OpenFlow 1.3。这样Ryu在跟交换机协商时只会用1.3版本避免版本歧义。第二get_switch(self, None)的调用方式。Ryu的拓扑API在较新版本中第一个参数传入RyuApp实例即可第二个参数传None表示获取全部交换机和链路。EventSwitchEnter和EventLinkAdd两个事件的监听保证了拓扑信息能实时刷新。需要注意的是如果控制器启动时交换机已经全部连上了SwitchEnter事件可能只触发一次但LinkAdd事件会在每条链路被发现时触发一次。拓扑信息本身由Ryu的LLDP模块负责维护我们只需要做缓存同步。控制器的datapath对象用于后续向交换机发送流表消息必须单独保存。很多初学实例会把datapaths存在一个独立的字典里避免每次都要从topology API重新查一遍。3.2 用BFS算法计算跳数最短路径计算最短路径时很多人第一反应是Dijkstra算法但这里权重全是1用BFS就够了而且复杂度更低。BFS从源节点逐层向外扩展第一次到达目标节点时当前路径就是跳数最少的路径。from collections import deque def build_graph(self): graph {} for link in self.links: src link.src.dpid dst link.dst.dpid graph.setdefault(src, set()).add(dst) graph.setdefault(dst, set()).add(src) return graph def shortest_path_by_hop(self, src_dpid, dst_dpid): if src_dpid dst_dpid: return [src_dpid] graph self.build_graph() queue deque() queue.append((src_dpid, [src_dpid])) visited set() visited.add(src_dpid) while queue: current, path queue.popleft() for neighbor in graph.get(current, []): if neighbor in visited: continue new_path path [neighbor] if neighbor dst_dpid: return new_path visited.add(neighbor) queue.append((neighbor, new_path)) return None重点说一下build_graph里的双向处理。Ryu拓扑模块返回的link对象有src和dst两个端点但链路本身是无向的发包方向相反也能走通。所以必须在两个方向上同时加入邻接关系否则BFS会漏掉一半的可达路径。BFS返回的结果是一个交换机dpid列表比如s1 - s2 - s3 - s4对应的就是[1, 2, 3, 4]。这里我用1、2、3、4模拟Mininet线性拓扑中的dpid实际环境中dpid是交换机的datapath ID可能是一串数字。整个路径计算的时间复杂度是O(VE)V是交换机数量E是链路数量。在实验规模的网络里这个计算几乎是瞬时完成的。即使将来网络规模扩大到上百台交换机只要拓扑变化频率不高BFS也完全够用。3.3 Packet-In处理与主机位置学习控制器收到packet-in消息后首先解析以太网帧拿到源MAC和目的MAC然后更新主机位置表。set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER) def packet_in_handler(self, ev): msg ev.msg datapath msg.datapath dpid datapath.id ofproto datapath.ofproto parser datapath.ofproto_parser in_port msg.match[in_port] pkt packet.Packet(msg.data) eth pkt.get_protocols(ethernet.ethernet)[0] dst_mac eth.dst src_mac eth.src self.host_map[src_mac] (dpid, in_port) if eth.ethertype ether_types.ETH_TYPE_LLDP: return if dst_mac in self.host_map: dst_dpid, dst_port self.host_map[dst_mac] path self.shortest_path_by_hop(dpid, dst_dpid) if path: self.install_path_flows(path, dst_mac, dst_port) self.forward_packet(datapath, dpid, path, dst_port, msg, in_port) else: self.flood(datapath, in_port, msg.data, ofproto, parser)主机位置学习的逻辑很简单任何从某个端口进来的包它的源MAC一定连着这台交换机的这个端口所以直接把源MAC记录到host_map里。第一次收到h1发送的ARP请求时控制器还不知道h4在哪此时目的MAC是广播地址不在host_map中于是触发洪泛——把包发送到除了入端口之外的所有端口。洪泛是ARP处理的标准做法因为在主机位置未知时只能在全网范围内广播。等h4回了ARP响应控制器就同时学到了h4的MAC与位置。这里有个细节处理packet-in之前先检查以太网类型如果是LLDP包则直接丢弃。LLDP是Ryu拓扑发现模块自己发送的探测包如果我们也把它当作普通数据包处理会导致拓扑探测包被误转发甚至形成环路。3.4 沿路径安装双向流表的关键细节路径计算出来之后核心动作是安装流表。我的做法是沿着路径的每一跳都下发一条匹配目的MAC、指向下一跳端口的流表项def add_flow(self, datapath, dst_mac, out_port, priority10, idle_timeout30): parser datapath.ofproto_parser ofproto datapath.ofproto match parser.OFPMatch(eth_dstdst_mac) actions [parser.OFPActionOutput(out_port)] inst [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS, actions)] mod parser.OFPFlowMod( datapathdatapath, prioritypriority, matchmatch, instructionsinst, idle_timeoutidle_timeout ) datapath.send_msg(mod) def get_out_port_to_neighbor(self, dpid, neighbor_dpid): for link in self.links: if link.src.dpid dpid and link.dst.dpid neighbor_dpid: return link.src.port_no if link.dst.dpid dpid and link.src.dpid neighbor_dpid: return link.dst.port_no return None def install_path_flows(self, path, dst_mac, dst_port): for i in range(len(path) - 1): dpid path[i] neighbor_dpid path[i 1] out_port self.get_out_port_to_neighbor(dpid, neighbor_dpid) datapath self.datapaths.get(dpid) if datapath and out_port is not None: self.add_flow(datapath, dst_mac, out_port) last_dpid path[-1] last_dp self.datapaths.get(last_dpid) if last_dp: self.add_flow(last_dp, dst_mac, dst_port)这段代码里有三个必须注意的细节。第一个是get_out_port_to_neighbor函数。Ryu的link对象中src和dst都带有端口号信息。当我需要知道从dpid这台交换机出发、前往neighbor_dpid需要走哪个端口时必须根据dpid在link的src还是dst位置来判断返回哪个端口。如果不做这个判断很可能拿到反向链路的端口号导致转发错误。第二个是idle_timeout30。我故意给每条流表设置了30秒的空闲超时。原因很简单拓扑可能发生变化如果流表永久保留链路断开后旧路径还在工作数据包就会丢失。设置超时后长时间没有匹配的流表项会自动删除客观上降低了错误路径的存活时间同时也能让后续实验中的路径切换更快生效。实际生产环境中idle_timeout会结合业务需求设置比如长连接场景反而要设置足够长避免频繁触发packet-in。第三个是细节中的细节安装流表后当前触发packet-in的这个数据包并不会自动走新流表因为当交换机把包上送控制器时这个包已经被“消费”了。所以我单独写了一个forward_packet函数用OFPPacketOut把当前包直接送到正确的出端口保证第一个包也不丢def forward_packet(self, datapath, dpid, path, dst_port, msg, in_port): parser datapath.ofproto_parser ofproto datapath.ofproto if len(path) 1: next_dpid path[1] out_port self.get_out_port_to_neighbor(dpid, next_dpid) else: out_port dst_port actions [parser.OFPActionOutput(out_port)] out parser.OFPPacketOut( datapathdatapath, buffer_idofproto.OFP_NO_BUFFER, in_portin_port, actionsactions, datamsg.data ) datapath.send_msg(out)有同学可能会问既然流表已经下发那当前包能不能也交给流表去转发答案是不能。流表只对交换机后续收到的包生效而当前这个包已经在packet-in消息里被完整地送到了控制器交换机里已经没有它的副本了所以必须由控制器显式通过PacketOut送回去。这是SDN开发中最容易被忽略的机制之一很多新手写了流表下发却丢第一个包问题几乎都出在这里。回程路径的问题也统一说一下。当h4回复h1的包到达s4时控制器收到packet-in这时候host_map里已经有了h1的MAC和位置于是控制器同样会计算s4到s1的最短路径并下发流表。也就是说两个方向的路径是独立计算的但都由同一套逻辑支撑。实验中你会发现第一次ping时会有几个ARP和ICMP包经过控制器之后数据包就完全走交换机流水线了。3.5 洪泛的边界与广播风暴的预防前面的代码里未知名目的MAC触发洪泛。洪泛并不是无脑的“所有端口都发一遍”而是要排除入端口。交换机本身处理广播时会自动不向入端口发送但控制器通过PacketOut洪泛时动作列表里需要手动排除入端口def flood(self, datapath, in_port, data, ofproto, parser): actions [] for port in datapath.ports: if port ! in_port and port ! ofproto.OFPP_LOCAL: actions.append(parser.OFPActionOutput(port)) out parser.OFPPacketOut( datapathdatapath, buffer_idofproto.OFP_NO_BUFFER, in_portin_port, actionsactions, datadata ) datapath.send_msg(out)这里排除了OFPP_LOCAL端口通常是交换机上的管理端口可能连接本地协议栈避免把广播包发给控制器本身。实际上Ryu中datapath.ports属性保存了所有端口号和对应状态遍历时要特别注意排除掉入端口。洪泛在实验规模的小网络里问题不大但如果网络存在环路广播包会无限循环直接打满交换机CPU形成广播风暴。这也是SDN中为什么需要生成树协议或者控制器主动去环的原因。我们的跳数最短路径转发实例里不包含去环逻辑所以在环形拓扑上做实验时如果出现ARP洪泛风暴别惊讶这是正常现象后面我会讲怎么规避和扩展。4. 实操验证把转发案例完整跑起来代码写完环境备好接下来是实际验证。这一部分我会把启动命令、测试步骤、观察结果完整梳理一遍方便直接对照操作。4.1 启动控制器与网络的命令组合建议开两个终端窗口一个跑控制器一个跑Mininet。终端一启动控制器sudo ryu-manager shortest_hop_forwarding.py --observe-links--observe-links参数非常关键。它让Ryu主动通过LLDP探测链路状态并维护完整的拓扑视图。没有这个参数拓扑事件可能不完整路径计算就会失败。终端二创建网络sudo mn --topolinear,4 --controllerremote,ip127.0.0.1 --switchovsk,protocolsOpenFlow13启动后大约等3到5秒让LLDP完成链路发现。观察控制器日志会看到类似“link add, current links: 3”的输出说明4台线性交换机之间已经识别到3条链路。4.2 连通性测试与流表现场分析在Mininet命令行里执行mininet h1 ping -c 3 h4如果一切正常你会看到0% packet loss的结果。这个结果本身就是巨大的成功信号说明控制器成功计算了最短路径并安装了全部流表。接下来验证流表真实性。在Mininet外部再开一个终端执行sudo ovs-ofctl -O OpenFlow13 dump-flows s1 sudo ovs-ofctl -O OpenFlow13 dump-flows s2 sudo ovs-ofctl -O OpenFlow13 dump-flows s3 sudo ovs-ofctl -O OpenFlow13 dump-flows s4以s1为例你会看到一条匹配h4目的MAC、output到与s2相连端口通常是2号口的流表项。priority10idle_timeout30这些参数都能在流表信息中看到。s2和s3上也会有类似的流表指向下一跳。s4上的流表则直接output到h4所在端口。线性拓扑里路径是唯一的流表展示比较直观。为了真正体现“最短路径”的决策效果建议在环形拓扑上再做一轮测试sudo mn --topolinear,4 --controllerremote,ip127.0.0.1 --switchovsk,protocolsOpenFlow13 # 进入mininet后手动添加环形链路 mininet py net.addLink(s1, s4) mininet py net.build()这时候s1和s4之间多了一条直连链路相当于h1到h4存在两条可用路径。你可以用iperf或者ping测试验证。理论上控制器会为h1到h4选择s1 - s4这条两跳路径而不是s1 - s2 - s3 - s4。查看s1的流表你会发现output端口指向了与s4相连的端口这就直观证明了“跳数最短”的决策生效了。4.3 路径切换场景的验证方法再进一步可以测试链路故障后的行为。在环形拓扑中关闭某条链路mininet link s1 s4 down如果流表的idle_timeout还没触发s1到s4的旧流表项仍然存在数据包会继续从s1发往s4从而丢包。直到流表项超时删除后下一个packet-in才会触发控制器重新计算路径。这个实验能帮我们直观理解SDN里“拓扑变化感知与流表重定向”的时延来源。如果想做得更完整可以监听EventLinkDown事件在链路断开时主动删除相关流表set_ev_cls(event.EventLinkDown) def link_down_handler(self, ev): self.links get_link(self, None) # 删除所有流表项 for dpid, dp in self.datapaths.items(): parser dp.ofproto_parser ofproto dp.ofproto match parser.OFPMatch() mod parser.OFPFlowMod( datapathdp, commandofproto.OFPFC_DELETE, matchmatch, priority0 ) dp.send_msg(mod)这是扩展方向不是本次的基础目标但值得写下来。它说明了Ryu事件驱动模型的强大链路、交换机、主机的任何状态变化都会变成事件控制器可以在事件回调里做任何补救操作。5. 踩坑实录与问题排查速查表最后这部分我把实际操作中遇到的典型问题、排查思路整理成一个速查表。每一个问题都对应一类真实的开发困境按表排查比盲试高效得多。5.1 拓扑发现不全或端口错误现象控制器日志中有交换机连接记录但links始终为空路径计算直接返回None。排查步骤按顺序执行确认启动时加了--observe-links参数。Ryu默认不会主动探测链路没有这个参数拓扑模块可能在链路发现上表现不正常。检查交换机是否启用了LLDP。Mininet的ovsk交换机默认支持LLDP但有些普通Open vSwitch配置需要额外开启。确认EventSwitchEnter的监听器里正确调用了get_switch(self, None)。有些版本的Ryu拓扑API签名不同我习惯把self.switches和self.links都刷新一遍。端口信息错误是一个特别隐蔽的坑。link.src.port_no和link.dst.port_no对应的是链路两端的两个不同端口如果互换使用流表的出端口就会指向错误方向。所以get_out_port_to_neighbor函数里必须做双向判断。我最初写这个函数时只判断了link.src.dpid dpid这一种情况结果在非对称拓扑上流表全部指向了反向端口排查了大半天才发现。5.2 流表已下发但包仍上送控制器现象流表dump出来是正常的匹配规则也正确但抓包发现交换机和控制器之间仍有大量packet-in消息。这个问题的根源通常是流表优先级冲突。OpenFlow交换机匹配数据包时会从所有流表项中选出优先级最高且匹配的一项。如果我们下发的转发流表priority10但table-miss流表的priority也设置成了10那么在OpenFlow 1.3的默认处理逻辑里优先级相同的情况下匹配行为可能取决于流表的添加顺序和交换机实现。解决方法是保证转发流表优先级高于table-miss。table-miss的priority设成0转发流表设成10这样任何匹配到目的MAC的包都会优先命中有明确转发指令的流表只有完全未匹配的包才会上送控制器。另一个与此相关的点是流表的table_id。Ryu默认操作的是table 0如果交换机配置了多级流水线流表要放到对应的table里才能生效。实验环境通常只有一张流表不用特别处理但了解这个概念有助于理解OpenFlow的多级流表机制。5.3 ARP洪泛、环路与流表残留问题现象环形拓扑下执行ping网络卡顿控制器日志疯狂刷packet-in所有交换机CPU占用升高。这是典型的广播风暴。原因在于ARP请求的目的MAC是广播地址控制器不认识所以洪泛洪泛出去之后又被其他交换机收到再次触发packet-in再次洪泛。环形拓扑下广播包沿着环不断循环传播形成风暴。规避手段有几个思路。最简单的是在实验时避免环形拓扑先用线性拓扑验证核心逻辑。想要在环形拓扑下正常工作就需要引入去环机制比如在控制器层实现一个最小生成树算法只保留树上的链路用于转发或者接入Ryu自带的生成树模块。这个扩展很有趣但已经超出本文的基础范围后续可以单独写一篇生成树应用的实例。流表残留问题前面提到过。当拓扑变化、旧流表未删除时数据包会沿着已经失效的路径转发。应对方式就是在链路down事件中主动删除相关流表或者给所有流表设置一个合理的idle_timeout。我在代码里设置的30秒就是为这个场景兜底。5.4 协议版本协商失败现象Mininet交换机启动后Ryu日志显示连接被拒绝或者交换机一直处于“无控制器”状态。Open vSwitch默认支持多种OpenFlow协议版本但在早期版本中默认协议可能是OpenFlow 1.0。Ryu应用声明只支持1.3时双方协商可能失败。解决办法是创建网络时显式指定协议版本即--switchovsk,protocolsOpenFlow13。同理在自定义拓扑脚本里创建交换机时也要加上protocolsOpenFlow13参数。写在最后的一点体会这个实例做完之后最大的感受是SDN控制器开发的核心并不在“算法有多高级”而在于对数据面与控制面交互机制的理解。数据包如何触发packet-in、流表如何匹配与生效、PacketOut与FlowMod的配合、拓扑事件如何驱动决策……这些机制没有跑通一遍实际代码前读再多文档都只是纸面知识。基于跳数的最短路径转发恰好把所有关键环节都覆盖了一遍是一个性价比极高的入门项目。建议各位按“线性拓扑先跑通环形拓扑再验证链路故障后做扩展”的节奏去把所有细节亲手过一遍后面再接触负载均衡、多路径转发乃至基于时延或带宽的路由应用时就能快速套用这套控制器的底层框架。