目前群里很多同学做ovs研究,也有很多人来讨论如何自定义OVS匹配域的问题,所以今天的分享主题就围绕OVS匹配处理流程和拓展性展开,这和之前SDNLAB上发的自定义action,可称为姊妹篇。它们是去年研读OVS源码时候的一些收获和心得,今天拿出来和大家分享。
由于拓展匹配域更贴近OVS开发实践,难免会提到代码部分。但为了简洁明了,此次分享主要遵循两个目的:讲清楚其大体逻辑、然后点明需要源码添加的地方,提高匹配域拓展成功率。
一、整体思路
现在进入正题,今天的分享从三个方面进行:
- 匹配域相关的各个模块简单分析。
- 安装检验和调试
- 演示结果。
相比在ovs源码中添加自定义action,自定义匹配域显得关系更为复杂凌乱一些。为了让和匹配域相关的模块条理更加清楚明了,我尽量将要提到的相关模块关系化,防止漏掉和匹配域相关的部分。这里先给出总体架构图:
架构图中包含了将要分析的8大模块,每一个里面都有和匹配相关的内容,接下来会按照这个思路逐一分析。其实大家发现,这和流表从控制器下发后,数据包进入交换机的处理流程非常吻合,想必大家多多少少有一些认识。
二、各个模块分析
下来进入分享的重点。按照图中给出的思路,各个模块讲解顺序依次为:
1、匹配域定义
2、flowmod解析
3、用户层表项插入
4、内核层packet解析和匹配处理
5、Upcall接收和分类
6、用户层查找匹配处理
7、表项和packet的下发操作
8、内核层flow插入和packet执行
9、其他
1、匹配域定义
Ovs匹配域是基于OpenFlow协议的,因此,如果要添加一个新的匹配域,需要延续OF协议定义一个匹配域的逻辑,这样拓展出的新匹配才能较为容易的和其他OF已经定义的匹配域兼容起来,同时保障OVS的匹配处理逻辑不发生改变。
1)目前OF支持两种定义匹配域的格式,用的较多的是OXM格式,即TLV格式(类型,长度和值)。我们之后的讲解以TLV格式为基础进行。那么要想实现一个新的匹配域,代表类型的T和长度的L比不少,他们定义在枚举类型和宏定义中。
首先看枚举类型,目前OF在1.3协议中已经定义了40种匹配字段,它们枚举值定义在include\openflow\Openflow-1.2.h中,部分截图如下:
每一个匹配域有相应ENUM值,从in_port的0到IPV6_EXTHDR的39,因此对于新的匹配域,需要以这种格式进行添加即可,但ENUM值必须是目前还没有定义过的值。
2)除了要添加枚举值外,还需要添加一个TLV相关的宏定义。TLV头部如下(TL部分,相当于绑定了一个匹配字段的类型和长度):
对于一个新匹配域,只需要按照上面格式进行添加即可,注意4或是8指的是TLV中的L数值,表示匹配域值的长度。如对于inport则是4字节。之后OVS对flowmod中匹配域解析就全依赖这个枚举值和宏定义了,此外提一句,如果是在控制端也做匹配域添加,需要和这个枚举值和TL格式对应起来。
2、FlowMod消息解析
完成之前的新字段的TLV定义还远远不够,即将等待我们的是,OVS如何能够从Flowmod消息中准确提取出匹配域,并且能无排斥的插入原生的OVS流表中。接下来分析一下flowmod消息解析模块。
先上图:
图体现了大体思路: Flowmod消息的匹配域部分,最终是要按照TLV格式逐一解析出来,然后经过一系列依赖性和重复性检测等,最后才能将匹配域部分完整的解析放置在match结构体中。
Match是什么?是用来装载从flowmod消息中解析出来的匹配域。先来看看match结构体:
Match包含了flow和wc,前者装载字段值,后者标记字段掩码(深入会发现wc也是用flow结构体存储掩码)。Flow结构体包含了匹配域所有字段类型,因此对于新的字段,需要在此结构体中添加。
需要注意的是,匹配字段在flow中添加的前后位置要固定,因为后面添加相应源码时需要和这个位置一致。
2)说完了match,那如何从flowmod的匹配域中逐一解析出每一个字段呢?(其主要思想体现在函数nx_pull_raw()中)
大体是这样的,匹配域由多个TLV组成,每一个TLV是一个匹配字段。则OVS先会从flowmod匹配域中按照TLV中的L将每个OXM(TLV格式)切割出来。这样是不是就解析完了呢,显然不是,因为切割后的合法性无法保障(如长度是否符合定义,各个字段依赖是否正确等)。
这里就需要后面的工作了,通过分割出来的OXM的header(即TL部分),在匹配域哈希表mf_field(Hmap)中做哈希查找,然后查找到这个TL应该对应的mf_field结构体。mf_field是OVS已经声明定义好的匹配域信息集合,包含依赖性,名字,长度等信息,这些可以对分割出来的该字段进行检验。Ok,清楚了这些,下面给出匹配域字段解析的示意图:
刚才提到字段信息的集合mf_field,其以数组形式定义在mf_fields中,我们需要在此处写入新字段的信息:
如上面这个是inport字段信息集合,可以看到它包含了名字,字段长度和最开始提到的匹配域定义的enum OXM_OF_IN_PORT。这里注意,包含的第一个属性是mf_field的id号,一个mf_field有一个id,其定义在mf_field_id枚举类型中(对于新字段也需要在这里添加一个id,注意相对位置)。这个id号算是OVS自身识别匹配域类型的方式,之后匹配域合法性检测会都会用到这个id号。
3)接下来,会根据字段mf_field信息对分割的每个字段做依赖性检测、重复性检测和匹配域值的有效性检测等。
A、依赖性检测:如当设置ipv4匹配字段时,会检测match->flow的“二层协议匹配字段”是否已经是ip协议。如果新添加匹配字段有依赖性限制,则需要在函数mf_are_prereqs_ok中添加case进行检测。
B、重复性检测:因为匹配域字段是逐个解析的,为了防止当前字段类型已经在之前存在过,则需要进行重复性检测,对于新的字段,需要在函数mf_is_all_wild()添加代码进行检测。
C、匹配域值的有效性检测:对于一些匹配字段值是有规定的,如inport号是否大于最大范围等,对于新字段也需要在函数mf_is_value_valid()中完成检测。
检测完就可以安安心心的将解析的每个字段值赋给match结构体了,赋值时会分有掩码和无掩码情况,也需要添加相应新字段源码。
其实,令人欣慰的是,对于一个新字段需要在各处添加源码,看似繁杂,也基本就是照别的字段源码格式多写一个case的事情,照猫画虎也算是是个好方法。
3、流表项插入
完成flowmod的匹配域解析,那么剩下的就是依照flowmod要求进行流表项删除、添加等操作,这里对于一个新字段无需源码改动。
OVS有很多保障性能的方法,这里就有一处,简答提一下:Ovs定义了一个重要结构体cls_rule,其与匹配域信息、priority信息等相关,且cls_rule关联一个相应的流表项。当ovs向流表中插入新表项时,不是以表项全部内容进行重复性检测,而是通过cls_rule在分类器cls_calssifier中进行查找,这种对流表项分类查找方法可以大大提高工作效率,完成新表项的添加或是更新。
4、内核层packet解析和匹配处理
用户层表项解析与插入告一段落,下来就是当数据包进入交换机时,如何完成packet解析与匹配处理。(核心代码位于datapath文件夹下,数据包头解析和匹配旅程从ovs_vport_receive()开始)
我们知道,ovs为了提高效率,数据包会先在内核层datapath进行流表项匹配处理,对于匹配失败,或者是匹配到表项的action为发向用户层时,才会去用户层继续查找匹配。对于在用户层匹配成功的数据包会按照表项action相应处理,并向内核层下发一条匹配到的表项,方便以后类似数据包直接在内核层完成匹配转发。
这个过程将是要一一解释的关键点,无不和匹配域息息相关。先来说说数据包进入ovs内核层的处理过程。
1)当一个OVS端口接收到一个数据包,不是将整个数据包在内核层的流表中匹配查找,这样效率低下,而是需要对此数据包头字段进行解析,将解析出来的各个匹配字段值和端口号一起构造成查询key,然后用key在流表中进行匹配查找。
查询key,它是一个sw_flow_key结构体,如下,包含了各个匹配字段的类型,对于新字段也需要在这里进行添加。
此外,需要调用函数key_extract()依次从包头中提取各个字段放入key中。如果你构造了一个数据包新协议字段,就需要在这个函数中提取相应包头字段赋值给key即可,包头提取都是对linux的结构体操作,很方便快捷。
2)有了key,那就是内核层流表项匹配查找的事情了。由于此次分享围绕匹配域展开,内核中流表匹配查找阶段,不涉及具体的匹配字段,也无需做修改添加,因此不具体分析匹配查找流表项的具体过程。
查找结果无非两种,查找成功和查找失败。查找失败则构造upcall上交用户层继续查找处理,但这里需要注意,即使查找到也可能面临上交处理。因为有一些action无法在内核层执行,这种action在下发到内核层时已经标记为OVS_ACTION_ATTR_USERSPACE类型,此时也需要上交用户层进一步匹配处理。
上交用户层时(主要体现在queue_userspace_packet函数中),会构造上交的数据包user_skb(skb_buf结构体),然后通过generic netlink通信机制上交给用户层。
结构体skb_buf可以简单理解为这样的结构:Netlink头部+Attr+Attr+...,Attr是type+len+data结构。Attr主要分为三个类型信息:
- key:即由包头等构造的查询key,必不可少,数据类型type为OVS_PACKET_ATTR_KEY
- userdata:用于匹配成功却仍要走slow-path的数据包,标记了action参数(如原因),数据类型type为OVS_PACKET_ATTR_USERDATA
- packet:顾名思义,原始数据包。类型type为OVS_PACKET_ATTR_PACKET。
注意,在这里,有两部分内容和匹配域相关,添加新匹配域时候就需要在此处修改源码:
A是对于待上交key中含有的各个字段计算总长度(key_attr_size())
B上传数据user_skb中的key包含很多匹配字段。因此新字段也要从key中提取出来加入到待传输到用户层的数据体中(函数ovs_nla_put_flow()),提取时会用到各个匹配域数据的类型(enum ovs_key_attr枚举类型中定义)。
5、Upcall接收和分类
到这里,已经完成和匹配域相关的多大半内容,思路已经比较清晰,后面将加快进度。
上面说到,内核层会封装含有key、packet和action参数等内容的upcall消息上交用户层。那么用户层接收到upcall之后直接匹配表项即可,为什么还要分类呢?(其主要体现在函数read_upcalls()(ofproto-dpif-upcalls.c))。
先给一张图:
可以看到,用户层的upcall结构体有dupcall和miss两个成员,这就和ovs性能提升密切相关了。OVS将具有相同key的upcall归为一类,管理映射到同一个miss中。这样就完成了相似packet的分类工作,便于后期统一匹配处理,提高效率。
在上面这个过程中,需要从key提取出flow进行哈希查找和分类。Flow就是前面讲解到的用户层用于表示匹配域的结构体,OVS调用函数flow_extract()函数从packet与md(metadata元数据)中解析并构造flow赋值给miss->flow,在这里别忘了添加相应解析函数。
其实,分类还包括了对slow path原因的分类处理,因和匹配域无关,就不详述了
6、用户层查找匹配处理
完成upcall前期接收和分类工作,下来就是匹配处理了(主要体现在函数handle_upcalls()(ofproto-dpif-upcall.c))。
这里只有一处和新匹配域添加相关(odp_flow_key_from_flow__()函数),因此主要强调其工作原理。OVS会先分批(之前提到的,划分为同一个miss的数据包)完成用户层流表匹配查找,然后得到流表项action,并将用户层action翻译为内核层odp_action,并对属于slow_path的action数据包做特殊标记处理(miss.xout->slow),尤其对部分slow_path中slow_action的做help标记。之后就可以下发查找到的表项到内核层了,并将数据包发到内核层去执行流表项的action。
这个过程很合情合理,但标记做什么用呢?因为数据包匹配到的流表项,其action执行只能通过慢通道处理(最典型的就是Controller action,甚至是因为action过多或是数据量太大),因此标记后,就会将这些含有slow_path action的表项和packet 直接在用户层完成特殊处理,这基本和内核层关系就不大了,效率自然也不会高。
7、表项和Packet的下发操作
接下来的工作,就是将表项下发到内核层,并将packet通过netlink机制下发到内核层去执行action(主要体现在函数dpif_operate()中)。
由于之前提到的slow-path原因,OVS会采用两种形式下发,一种是和slow-path无关的统一处理下发,一种是和slow-path相关的单独特殊处理。
1)统一下发处理较为简单,就是批量以广播形式通过netlink机制下发到内核层,完成流表项在内核层的安装和packet在内核层action的执行。这里需要注意的是,如果自定义的新匹配域属于metadata类型,如inport这种,那么需要在odp_key_from_pkt_metadata()函数中,实现将元数据内容的取出放入request缓存后等待下发的功能。
2)特殊处理:对于一个需要slow-path处理的packet,其所有动作actions本应在用户层执行(即在odp_execute_actions__()函数),但是执行到OVS_ACTION_ATTR_OUTPUT类型action时,不言而喻其最后需要发送到内核层完成转发。那么这种含有slow_path的流表项是否需要下发到内核层?还记得之前的action翻译吗,这种表项会将action翻译为OVS_ACTION_ATTR_USERSPACE下发到内核层中。如下,用户层表项到内核层表项:
请注意,特殊处理中如果牵扯到set_field action,就需要在odp_execute_set_actio()添加新匹配域的set函数。
8、内核层flow插入和packet执行
转了一圈,又回到了内核层。在内核层完成flow的插入和packet action执行工作基本就大功告成了。这里面的原理比较简单,因此只提及在表项插入过程中与匹配域相关的地方。
OVS主要在用户层下发的表项数据中,对含有的匹配字段值进行解析和字段有效性检验,完成表项插入。匹配字段解析中包含字段长度解析(ovs_key_lens()函数)和字段掩码解析(ovs_key_from_nlattrs()函数),有效性检验(match_validate()函数)主要完成了匹配字段是否全初始化检验、掩码和值的一致性检验等,对于新匹配域,以上几个函数需要修改。
9、其他
围绕着OVS匹配域有关的处理流程,终于分析完了从表项解析、插入、匹配,执行等一系列过程。当然,新的匹配域可能还不能很好的运作,因为还差打印显示和手动插入等功能。这部分比较独立,简单提及函数即可。
打印密切相关:
miniflow_extract()
flow_format()
odp_flow_key_attr_len()
ovs_key_attr_to_string()
format_odp_key_attr()
其他一些:
mf_set_flow_value()(lib/meta-flow.c)
mf_get_value()(lib/meta-flow.c)
nx_put_raw()(nx-match.c)
parse_odp_key_mask_attr函数()(lib\odp-util.c)
序号数FLOW_WC_SEQ
二、安装检验和调试
对于添加一个自定义匹配域,源码修改就算完成了,虽然比较繁琐,但是每一处改动不大,基本照猫画虎即可。安装过程简单,采用常规安装OVS的方法即可。
如果安装后,采用ovs-ofctl命令可以正常添加一条带有自定义匹配域的流表项,并且数据包可以成功如愿以偿的匹配到这条表项,那基本就大功告成了。
如果安装失败或是匹配不能按照预想的效果,需要进行调试。调试一般采用两种方法,查看log信息和gdb工具调试:
1)log信息:匹配域的添加涉及用户层和内核层,ovs在用户层提供了相应log函数VLOG_WARN、VLOG_INFO、VLOG_DBG等,直接使用即可,用户层log信息一般位于/usr/local/var/log/openvswitch/ovs-vswitchd.log中查看;
内核层可以使用printk等函数添加log,并在/var/log/kern.log中查看即可。
2)采用dbg方式,比较准确高级的。
三、演示结果
说了这么多,没有实验结果都是不可靠的。因此,我下发了一条流表项,包含了一个新的匹配域,并且成功匹配到了数据包,达到预期效果。可以用ofctl查看用户层表项,用dpctl查看内核层表项。
为了能够正确添加自定义匹配域,上文对于ovs匹配字段的执行流程和基本原理做了分析说明,全部下来,也算是劳力劳神,辛苦大家了。不过,希望此次分享能有助于群友在OVS上的二次开发。
技术有限,讲解有误的地方欢迎指正,拍砖~,最后,再次感谢SDNLAB和大家的支持。
Q&A
Q1:如何保证压力下的性能
A1:ovs本身在查找匹配上下了功夫,现在ovs也支持dpdk,性能会有所提升。不过这和硬件交换机比起来,还是有较大的提升空间。
Q2:对于新增匹配类型,只需要在用户态修改,内核态不用修改?
A2:需要修改的,第四点中讲到了内核层数据包头解析和key修改等,都是内核态需要修改完成的。
Q3:请问你们现在ovs用在哪个方面的产品上?
A3:我们目前FNL实验室利用OVS和CNVP等搭建了SDN实验平台,提供给学生切虚网做实验。
Q4:内核态里面是精准匹配,用户态模糊匹配?
A4:内核态可以理解为“精确表项”。举个例子,比如用户层写某条表项action是Flood,那么给内核层下发的表项Action就是Output1,2,3..等等端口,内核层的action是用户层翻译过的。内核态具有提升性能等作用,这里就是之前提到的数据包匹配处理的slow-path和fast-path问题了。
Q5:你的意思是完全没有内核态应该也行吧?
A5:恩,记得安装时候,可以不构建安装内核态,即只跑用户态,但功能上会有缺失~,也其实是很少这么干的。
Q6:一个网络包第一次进入这个交换机后,要么被drop,要么被转发到控制器,内核是怎么判断的? 是转发到控制器还是drop?
A6:内核的判断全来自于用户层曾经的判断:一个网络包如果在内核层被匹配到,那就按照action执行,没有匹配到,就上传用户层进行用户层的匹配。用户层会将匹配结果和相应表项下发到内核层,便于以后类似数据包在内核层匹配可以直接匹配成功,提高效率。至于是否转发给控制器,这要看是否匹配到的表项action为Controller(of1.3)。
Q7:如果用户层没有匹配成功,用户层会下发流表为drop?
A7:在of1.3中会的,不过内核层表项生存时间很短的哦,防止后来用户向用户态插入新表项导致匹配可以成功了。当然,生存时间短也是一种节约资源的方式。
Q8:你的意思是,内核没有匹配成功,一定会发到用户态,用户态没有匹配成功会有两个动作1)转发到控制器,2)直接drop?
A8:在of1.3中,用户态如果匹配没有成功,数据包会drop,且给内核层发一个精确表项(action为drop),在of1.3之前,用户层没有匹配到,才会默认发给控制器的。
Q9:如果你做出去这么一套东西给客户,然后跑起来了,用户说断了或者网速慢,但是用户不会编程,怎么排除故障?
A9:个人觉得,SDN中对于故障解决问题还不成熟,当然对于控制器端故障,可以通过集群等方式解决,如果是数据层面,可以通过控制器端写业务应用来检测切换。
-----------------------------------------------------------------------------------------------------