Openvswitch源码阅读之flow匹配的实现

July 19, 2017 | 4 Minute Read

ovs datapath “UFID” 等概念的介绍

流表结构

struct datapath {
	struct rcu_head rcu;
	struct list_head list_node;

	/* Flow table. */
	struct flow_table table;     // 每个datapath有自己的流表,上来的包就是在这个流表里面搜索。但按照文档说法openflow有255个table的,不知道怎么从这个表跳到另外一个表。

	/* Switch ports. */
	struct hlist_head *ports;

	/* Stats. */
	struct dp_stats_percpu __percpu *stats_percpu;

	/* Network namespace ref. */
	possible_net_t net;

	u32 user_features;

	u32 max_headroom;
};



struct sw_flow {
	struct rcu_head rcu;
	struct {
		struct hlist_node node[2];
		u32 hash;
	} flow_table, ufid_table;
	int stats_last_writer;		/* CPU id of the last writer on
					 * 'stats[0]'.
					 */
	struct sw_flow_key key;         // 用来算hash查找匹配流的, 就是从网络包里面提取的以太网头部,tcp端口之类的信息,很大的一个结构
	struct sw_flow_id id;           // flow的UUID标识。底层有两个hash表,内核查找flow使用key来做哈希计算,用户空间用UFID计算哈希,在两个hash表里面走可以找到这个flow
	struct sw_flow_mask *mask;      // key的mask,类似ip地址的mask
	struct sw_flow_actions __rcu *sf_acts;
	struct flow_stats __rcu *stats[]; /* One for each CPU.  First one
					   * is allocated at flow creation time,
					   * the rest are allocated on demand
					   * while holding the 'stats[0].lock'.
					   */
};



struct table_instance {  // 第2步的哈希表的数据结构
	struct flex_array *buckets;
	unsigned int n_buckets;
	struct rcu_head rcu;
	int node_ver;
	u32 hash_seed;
	bool keep_flows;
};

struct flow_table {
	struct table_instance __rcu *ti;              // OVS_FLOW_ATTR_KEY 普通的哈希表
	struct table_instance __rcu *ufid_ti;         // OVS_FLOW_ATTR_UFID 如果flow有ufid就使用这个哈希表。 是一个UUID字符串,不知道怎么区别这两种
	struct mask_cache_entry __percpu *mask_cache;  // 每个cpu有个缓存mask cache,缓存这个flow使用的mask在mask_array里面的位置。是一个255个元素的哈希表
	struct mask_array __rcu *mask_array;      // 所有flow使用到的mask的一个列表
	unsigned long last_rehash;
	unsigned int count;
	unsigned int ufid_count;
};

ufid_ti 是供 “ovs-dpctl” 这些终端命令行通过指定flow的ufid(一个UUID)来操作flow用的? 一个flow应该同时在两个hash表里面。如果是网络包的查找
就用key 算hash找到flow,如果是命令行就根据指定的UUID找到对应的flow吧。这UFID主要是为了方便用户使用吧


mask_cache缓存的东西
struct mask_cache_entry {
	u32 skb_hash;        //  linux内核的flow id
	u32 mask_index;      //  这个linux flow(连接)应该使用的ovs哪一条flow的,那个flow的mask在mask_array里面的index
};


网络包进来的处理和查找flow
======================
flow.c 和 flow_table.c
```text
ovs_vport_receive
  ovs_flow_key_extract
    key_extract       // 这里会把skb的头部的mac地址ip地址端口等信息复制出来放到flow结构的key里面
  ovs_dp_process_packet
    ovs_flow_tbl_lookup_stats
      flow_lookup

flow的查找算法

	/* Look up flow. */
	flow = ovs_flow_tbl_lookup_stats(&dp->table, key, skb_get_hash(skb),
					 &n_mask_hit);

skb_get_hash是linux内核提供的函数,应该是linux内核提供的flow标识,应该是根据ip
tcp 端口这些计算生成的,同一个tcp连接的所有skb应该算出来是同一个hash值,表示
是属于同一个流的网络包。后面ovs里面应该用这个做流标识,用了缓存flow对应的mask的index。
参见内核源码  http://elixir.free-electrons.com/linux/latest/source/net/core/flow_dissector.c
/**
 * __skb_get_hash: calculate a flow hash
 * @skb: sk_buff to calculate flow hash from
 *
 * This function calculates a flow hash based on src/dst addresses
 * and src/dst port numbers.  Sets hash in skb to non-zero hash value
 * on success, zero indicates no valid hash.  Also, sets l4_hash in skb
 * if hash is a canonical 4-tuple hash over transport ports.
 */
void __skb_get_hash(struct sk_buff *skb)
{
	struct flow_keys keys;
	u32 hash;

	__flow_hash_secret_init();

	hash = ___skb_get_hash(skb, &keys, hashrnd);

	__skb_set_sw_hash(skb, hash, flow_keys_have_l4(&keys));
}
EXPORT_SYMBOL(__skb_get_hash);



/*
 * mask_cache maps flow to probable mask. This cache is not tightly
 * coupled cache, It means updates to  mask list can result in inconsistent
 * cache entry in mask cache.
 * This is per cpu cache and is divided in MC_HASH_SEGS segments.
 * In case of a hash collision the entry is hashed in next segment.
 */
struct sw_flow *ovs_flow_tbl_lookup_stats(struct flow_table *tbl,
					  const struct sw_flow_key *key,
					  u32 skb_hash,
					  u32 *n_mask_hit)
{
	struct mask_array *ma = rcu_dereference(tbl->mask_array);
	struct table_instance *ti = rcu_dereference(tbl->ti);
	struct mask_cache_entry *entries, *ce;
	struct sw_flow *flow;
	u32 hash;
	int seg;

	*n_mask_hit = 0;
	if (unlikely(!skb_hash)) {
		u32 mask_index = 0;

		return flow_lookup(tbl, ti, ma, key, n_mask_hit, &mask_index);
	}

	/* Pre and post recirulation flows usually have the same skb_hash
	 * value. To avoid hash collisions, rehash the 'skb_hash' with
	 * 'recirc_id'.  */
	if (key->recirc_id)
		skb_hash = jhash_1word(skb_hash, key->recirc_id);

	ce = NULL;
	hash = skb_hash;    // 这个linux内核的flow标识,同一个流比如tcp连接应该有一致的hash值
	entries = this_cpu_ptr(tbl->mask_cache);    // 每个cpu核心都有per cpu cache

  // 根据前面定义的MC_HASH_ENTRIES常量,mask_cache是一个255个元素大小的哈希表,
  // 也就是说最多只能缓存255个flow,一般来说255也足够了吧。如果linux同时有255个
  // 以上的tcp连接不停频繁切换就会出现缓存不命中需要遍历做mask array的搜索了。
  // 这个255大小的“非开放链表式”hash表,对cpu缓存比较友好吧。而且看样子mask array
  // 数目应该是对应ovs里面配置的flow的条数的。如果一个table配置的flow条数不多,
  // 遍历一下也就是多做几次hash查找而已。
  //
  // MC_HASH_SEGS 应该是4,把skb_hash的4个字节的每个字节做一次查找,就是说在表
  // 里面探测了4个位置。
  /* Find the cache entry 'ce' to operate on. */
  for (seg = 0; seg < MC_HASH_SEGS; seg++) {
    int index = hash & (MC_HASH_ENTRIES - 1);
    struct mask_cache_entry *e;

		e = &entries[index];
		if (e->skb_hash == skb_hash) { // 找到这个linux flow应该使用哪个mask(rule)
			flow = flow_lookup(tbl, ti, ma, key, n_mask_hit,
					   &e->mask_index);
			if (!flow)
				e->skb_hash = 0;
			return flow;
		}

		if (!ce || e->skb_hash < ce->skb_hash)
			ce = e;  /* A better replacement cache candidate. */

		hash >>= MC_HASH_SHIFT;
	}

	/* Cache miss, do full lookup. */
	flow = flow_lookup(tbl, ti, ma, key, n_mask_hit, &ce->mask_index);
	if (flow)
		ce->skb_hash = skb_hash;

	return flow;
}


/* Flow lookup does full lookup on flow table. It starts with
 * mask from index passed in *index.
 */
static struct sw_flow *flow_lookup(struct flow_table *tbl,
				   struct table_instance *ti,
				   const struct mask_array *ma,
				   const struct sw_flow_key *key,
				   u32 *n_mask_hit,
				   u32 *index)
{
	struct sw_flow_mask *mask;
	struct sw_flow *flow;
	int i;

	if (*index < ma->max) {  // 快速路径 : 直接根据第1步cache命中的index找到mask,就可以直接到哈希表里面找flow了
		mask = rcu_dereference_ovsl(ma->masks[*index]);
		if (mask) {
			flow = masked_flow_lookup(ti, key, mask, n_mask_hit);     // 第2步在哈希表里面找flow
			if (flow)
				return flow;
		}
	}

  // 慢路径: 前面的cache没有命中,只能遍历mask_array数组所有mask,每一个都查找一遍看看有匹配的没有
	for (i = 0; i < ma->max; i++)  {

		if (i == *index)
			continue;

		mask = rcu_dereference_ovsl(ma->masks[i]);
		if (!mask)
			continue;
    
    // 注意这个对每个mask的规则,都用mask 对 key做掩码之后才计算hash值,然后
    // 采用这个hash值去表里面找。
		flow = masked_flow_lookup(ti, key, mask, n_mask_hit);  // 第2步在哈希表里面找flow
		if (flow) { /* Found */
			*index = i;     // 如果找到了匹配的flow,就把mask的index返回上次缓存起来。
			return flow;
		}
	}

	return NULL;
}


这个查找方法有点复杂,openvswith把它叫做"Megaflows"技术吧, 查找分为两步
第1步: 在 mask_array数组里面找到flow对应的mask,这里有一个简单的hash缓存的优化,可以避免顺序遍历数组搜索
第2步: 在 hash table里面找到flow,根据key + mask做完掩码之后才计算hash值来做哈希表查找。

ovs为什么分为两级的搜索,而不是每个linux flow 直接对应hash表里面的一项,那样只
做一次查找就可以了。听说一开始ovs是这么实现的,后来才改成先查找mask的方式,最近
才又引入了mask的hash查找加速。想了一下,如果每个linux的flow都建一个hash项的话,
确实数目可能比较多,导致hash表比较大,消耗内存多性能也不见的好。它搞成这种mask
归类匹配的话,也是和上层配置流的通配符wilcard支持相匹配的,归类之后需要创建的flow
的hash项就少很多了。  比如  “192.168.1.0/24 port=*” 一条flow就能匹配多个192.168.1.1
192.168.1.2 等多个ip的tcp连接了。如果直接使用linux flow对应的方式就需要255 * 65535
条hash项。肯定还是基于这种mask通配符的匹配更好了,基本ovs里面配置多少条flow就
只需要创建多少条 flow+mask的匹配项而已。

2017-07-23补充
[The Design and Implementation of Open vSwitch](https://benpfaff.org/papers/ovs.pdf) 这篇论文详细介绍了
openvswitch实现这个megaflow的设计思想,以及开发人员为什么要这么设计的,还做了详细的性能测试、缓存命中率、和
linux bridge的性能比较等等,感兴趣的可以看一下


往表中插入flow比较容易理解
/* Must be called with OVS mutex held. */
int ovs_flow_tbl_insert(struct flow_table *table, struct sw_flow *flow,
			const struct sw_flow_mask *mask)
{
	int err;

	err = flow_mask_insert(table, flow, mask);   //  添加mask
	if (err)
		return err;
	flow_key_insert(table, flow);            // 添加key对应hash项
	if (ovs_identifier_is_ufid(&flow->id))
		flow_ufid_insert(table, flow);        // 添加ufid对应的hash项

	return 0;
}

ovs的resubmit这个action可以把包提交到另外table进行匹配时怎么实现的

ovs应该只是openflow标准,可以有255个table,应该可以在多个table串联
出复杂的pipeline处理流程。

https://networkheresy.com/2014/11/13/accelerating-open-vswitch-to-ludicrous-speed/
这个megaflow技术确实大大提高了ovs的性能。
看这里的说法openvswitch的flow的创建是在用户空间的vswitchd里面创建的,我之前
一篇文章也概读了一下那个xlate的代码了。但table的跳转,还没有看到代码是怎么实现的。
感觉会不会是xlate在生成flow的action的时候,已经把所有的255个table合并在一起了。
上层推算的时候就完全可以把table 的区分去掉了。

xlate_ofpact_resubmit 
  xlate_table_action
    xlate_resubmit_resource_check

看看这几个函数代码,好像有点像是跟猜测的那样实现的,底层内核确实可以做到不区分table_id了