openvswitch 系列第一篇 简介及基本数据结构

openvswitch 系列第一篇 简介及基本数据结构

1. openvswitch

1.1 基本描述

datapath为ovs内核模块,负责执行数据交换,也就是把从接收端口收到的数据包在流表中进行匹配,并执行匹配到的动作。内核中可以实现多个datapath(可以理解为桥,就是我们用ovs-vsctl看到的br0/br1之类), 一个datapath类似一个物理交换机,它可以对应多个vport(vport类似物理交换机的端口概念)。一个datapth关联一个flow table,一个flow table包含多个条目,每个条目包括两个内容:一个是flow的 match/key; 另一个是对应的action. 最常见的action是在不同vport中进行转发。

当一个数据报到达vport, 内核首先将它的flow key解析出来,之后在内核模块datapath的flow cache (大小为(sizeof(struct sw_flow)+ (nr_cpu_ids * sizeof(struct flow_stats ) = ( 1248 + 68 )=1296, 根据系统实际的CUP的数量多少会稍有不同)中的flow table中查找. 如果找到匹配的flow 规则,则去执行对应的action. 如果没有找到匹配的flow规则,则将数据报通过netlink的方式发送到用户空间的ovs-vswitchd处理,而用户空间的流表空间(65536)大的多,所以匹配到的几率也更高,通过在用户空间查找,并执行对应的action;如果找到,会通过netlink把用户空间的流表推送到datapath的flow cache中,后续的报文就可以直接在内核态处理.如果最后还是没有找到,那就使用默认的流表规则(丢掉这个包,或者其他).

用户空间有两个进程组成:ovs-vswitchd和ovsdb-server。
ovsdb-server保存了ovs-vswitchd的配置信息,ovsdb通常是一个文件, 并且保存在文件系统中,通常来说是/etc/openvswitch/conf.db
ovs-vswitchd是一个daemon,北向与Controller通过OpenFlow协议通信.南向与openvswitch内核模块通过netlink通信.东西向过OVSDB协议与ovsdb-server通信.

1.2 openvswitch 内核模块示例

1
2
3
4
5
6
7
8
# lsmod |grep openvswitch
openvswitch 114793 3
nf_nat_ipv6 14131 1 openvswitch
nf_defrag_ipv6 35104 2 openvswitch,nf_conntrack_ipv6
nf_nat_ipv4 14115 2 openvswitch,iptable_nat
nf_nat 26787 4 openvswitch,nf_nat_ipv4,nf_nat_ipv6,nf_nat_masquerade_ipv4
nf_conntrack 133095 8 openvswitch,nf_nat,nf_nat_ipv4,nf_nat_ipv6,xt_conntrack,nf_nat_masquerade_ipv4,nf_conntrack_ipv4,nf_conntrack_ipv6
libcrc32c 12644 4 xfs,openvswitch,nf_nat,nf_conntrack

1.3 通过openvswitch实现的bridge示例

1
2
# ovs-vsctl list-br
br0

1.4 通过openvswitch实现的bridge里面的voprt示例

1
2
3
# ovs-vsctl list-ports br0
vport1
vport2

1.5 通过openvswitch实现的bridge和vport的整体概览.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ovs-vsctl show
173a48d6-dbd3-420d-9433-384c437451a8
Bridge "br0"
fail_mode: secure
Port "br0"
Interface "br0"
type: internal
Port "vport1"
Interface "vport1"
type: internal
Port "vport2"
Interface "vport2"
type: internal
ovs_version: "2.0.0"

1.6 流表示例

1
2
3
# ovs-ofctl dump-flows br0
NXST_FLOW reply (xid=0x4):
cookie=0x0, duration=24.784s, table=0, n_packets=0, n_bytes=0, idle_age=24, ip,nw_src=200.200.200.0 actions=drop

2. 相对应的结构体示例

2.1 datapath的代码描述 (datapath 类似数据通路,其实是bridge的抽象)

1
2
3
4
5
6
7
8
9
10
11
struct datapath {
struct rcu_head rcu; // RCU回调, 用来负责推迟延迟销毁datapath
struct list_head list_node; // datapath链表,主要用来把datapath连接起来.
struct flow_table table; // datapath里面的流表
struct hlist_head *ports; // datapath里面的Switch ports. 是以哈希表的形式表示. %OVSP_LOCAL 这个端口一直在datapath建立的时候就存在. 用 ovs_mutex 和 RCU 来进行锁保护.
struct dp_stats_percpu __percpu *stats_percpu; // Pre-CPU的datapath状态信息
possible_net_t net; // datapat的网络命名空间的引用.
u32 user_features; // datapath用户所具有的能力.
u32 max_headroom; //留给datapath里面的所有vports使用的最大headroom.
struct hlist_head *meters; // datapath的meters, 参数之类.
};

2.2 vport的代码描述

1
2
3
4
5
6
7
8
9
10
11
struct vport {
struct net_device *dev; // 指向net_device的指针
struct datapath *dp; // 指向这个port所在datapath的指针,表示该端口是属于哪个datapath的
struct vport_portids __rcu *upcall_portids; //通过RCU机制保护的结构 'struct vport_portids'
u16 port_no; //在datapath里面所有端口数组的索引,唯一标识该端口.因为一个datapath上有多个端口,而这些端口都是用哈希链表来存储的,所以这是链表元素(里面没有数据,只有next和prev前驱后继指针,数据部分就是vport结构体中的其他成员)
struct hlist_node hash_node; //在设备hash表里面的元素
struct hlist_node dp_hash_node; //在datapath的hash表里面的元素
const struct vport_ops *ops; // 指向操作函数的指针,结构体里面存放了很多操作函数的函数指针
struct list_head detach_list; //用来在net-exit调用时撤销vport的链表
struct rcu_head rcu; //撤销datapath的RCU 回调函数头
};

2.3 流表flow_table (有五部分组成,一个是流表,另外是流表的实例,流表的内容,流表的key值,流表操作集和)

2.3.1 流表

1
2
3
4
5
6
7
8
struct flow_table { // 流表
struct table_instance __rcu *ti; // 具体流表实例
struct table_instance __rcu *ufid_ti; //包含unique flow identifier的流实例,
struct list_head mask_list; // 链表用来串联整个流表 (一般配合container_of使用来获取结构体的头指针)
unsigned long last_rehash; // 会初始化为当前的jiffies. 用来计间用
unsigned int count; // 具体流表的个数, ovs_flow_tbl_init流表初始化时,会置为0
unsigned int ufid_count; // 具体unique flow identifier流表的个数,ovs_flow_tbl_init流表初始化时,会置为0
};

2.3.2 流表的具体实例

1
2
3
4
5
6
7
8
struct table_instance { //流表的具体实例
struct flex_array *buckets; //哈希桶地址指针. 具体的流表项, 主要是方便处理.(真实的流表应该在这里面)
unsigned int n_buckets; // 哈希桶个数
struct rcu_head rcu;// 操作(撤销?)流表的RCU 回调函数头
int node_ver; //node_ver的存在使得我们可以控制sw_flow的哪个hlist_node链入到bucket中
u32 hash_seed; //哈希算法需要的种子
bool keep_flows;//是否保留流表项
};

2.3.3 流表里面所具体保存的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct sw_flow { // 流表里面所具体保存的内容.
struct rcu_head rcu; // rcu保护机制 (撤销流表项的RCU 回调函数头)
struct {
struct hlist_node node[2]; // 两个节点指针,用来链接作用,前驱后继指针
u32 hash; // hash值
} flow_table, ufid_table; // 为两种流表各定义对应的结构.
int stats_last_writer; // 最近一个CPU写操作的ID
struct sw_flow_key key; // 流表中的key值, 这个是个关键东东了,关系到报文要匹配那些流表key
struct sw_flow_id id; // 流表自身的ID
struct cpumask cpu_used_mask; // 也是流表中的key
struct sw_flow_mask *mask; // 要匹配的mask结构体
struct sw_flow_actions __rcu *sf_acts; // 相应的action动作. 使用了rcu机制保护的.
struct flow_stats __rcu *stats[]; /* 数据流的状态,每一个CPU上都有,第一个是流建立的时候, 其余的是在获取stats[0].lock锁之后,根据需求分配. 使用了rcu机制保护的.
};

2.3.4 流表中的key值,主要是保存数据包中协议相关信息,这是报文要进行流表匹配的关键结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
struct sw_flow_key {
u8 tun_opts[IP_TUNNEL_OPTS_MAX];
u8 tun_opts_len;
struct ip_tunnel_key tun_key; // 隧道的封装key.
struct {
u32 priority; // 包的Qos优先级
u32 skb_mark; // skb包的标记
u16 in_port; // 包进入的端口号或者DP_MAX_PORTS
} __packed phy; // Safe when right after 'tun_key'.
u8 mac_proto; // 链路层协议, 比如 Ethernet, ATM 等等
u8 tun_proto; // 封装协议, 比如GRE, VXLAN 等等
u32 ovs_flow_hash; // Datapath 所计算出的 hash 值
u32 recirc_id; // 转发的ID Recirculation ID.
struct {
u8 src[ETH_ALEN]; // Ethernet源mac地址
u8 dst[ETH_ALEN]; // Ethernet目的mac地址
struct vlan_head vlan; // vlan 的信息, 802.1q or 802.1ad 类型 以及vlan id
struct vlan_head cvlan; // vlan 的信息给Conntrack用的, 802.1q or 802.1ad 类型 以及vlan id
__be16 type; // 以太网帧类型
} eth; // 2 层的. 数据链路层的匹配信息.
u8 ct_state; // Conntrack的状态,有或者没有
u8 ct_orig_proto; //Conntrack 的原始路径的ip协议. 其实用来表示有没有包含original direction key 内容.
union {
struct {
__be32 top_lse; /* top label stack entry */
} mpls;
struct {
u8 proto; // IP包协议类型 TCP:6;UDP:17;ARP操作码类型用低8位表示
u8 tos; // IP包服务类型
u8 ttl; // IP包生存时间,经过多少跳路由
u8 frag; // 网桥中的OVS_FRAG_TYPE_*标记
} ip; // 3 层的. ip 层的匹配信息.
};
u16 ct_zone; // 追踪连接状态区
struct {
__be16 src; // TCP/UDP/SCTP的源端口,应用层发送数据的端口
__be16 dst; // TCP/UDP/SCTP的目的端口,也是指应用层传输数据端口
__be16 flags; // TCP 的标记
} tp; // 4 层的. 传输层的匹配信息
union {
struct {
struct {
__be32 src; // IP源地址
__be32 dst; // IP目的地址
} addr; // IP 的信息
union {
struct {
__be32 src; // Conntrack IP源地址
__be32 dst; // Conntrack IP目的地址
} ct_orig; // 追踪连接的原始目的区域
struct {
u8 sha[ETH_ALEN]; // ARP的源Mac地址
u8 tha[ETH_ALEN]; // ARP的目的Mac地址
} arp; // arp 的信息
};
} ipv4; // IPv4 的信息
struct {
struct {
struct in6_addr src; // IPv6 源地址
struct in6_addr dst; // IPv6 目的地址
} addr; // IPv6 的IP层信息
__be32 label; // IPv6 流的标示
union {
struct {
struct in6_addr src; // Conntrack IP源地址
struct in6_addr dst; // Conntrack IP目的地址
} ct_orig; // 追踪连接的原始目的区域
struct {
struct in6_addr target; // Neighbor Discovery 目标地址
u8 sll[ETH_ALEN]; // Neighbor Discovery 源链接层地址
u8 tll[ETH_ALEN]; // Neighbor Discovery 目标接层地址
} nd; //Neighbor Discovery (ND) protocol 是一个IPV6 的协议. 主机或者路由使用ND协议去侦测邻居的链路层地址,在必要的时候,可以及时清除无效的cache
};
} ipv6; // IPV6 信息.
struct ovs_key_nsh nsh; // 网络服务头
};
struct {
// Connection tracking fields 连接监测的信息, CT主要用来做网络连接状态的识别. OVS2.5版本开始支持. 涉及到有状态的防火墙和无状态的防火墙.Openstack则从M版开始,使用OVS的新特性,来实现“有状态防火墙”中的“Security Group”功能
struct {
__be16 src; /* CT orig tuple tp src port. 连接监测源端口.用于“有状态防火墙”的流识别
__be16 dst; /* CT orig tuple tp dst port. 连接监测目的端口.用于“有状态防火墙”的流识别
} orig_tp;
u32 mark;
struct ovs_key_ct_labels labels; // 这个是个32bit联合体,作为一个labels
} ct; // Connection tracking, 报文进来可能会先进入这里,如果有的话, 然后匹配自身的流表之后再去匹配datapath的流表,再之后执行action.

} __aligned(BITS_PER_LONG/8); // 主要是用来做数据对齐用的. 考虑到存取的效率, 在64bit机器上,就是8字节对齐

2.3.5 流表项操作, 也就是能对流表做些什么操作.

1
2
3
4
5
6
struct sw_flow_actions {
struct rcu_head rcu; // 要操作流表,肯定要个rcu来加锁同步了.
size_t orig_len; // 来自 flow_cmd_new netlink actions 操作的长度
u32 actions_len; // 操作的长度.
struct nlattr actions[]; //流表项的操作集合,这个是一个netlink的结构,一个就是数据包的长度,一个就是类型.
};

到此,第一部分就介绍完毕了. 后续会有网桥的操作,vport的操作,数据报的处理等等的介绍…