OVS - 数据包处理流程

在介绍OVS数据包的处理过程之前,首先得了解下OVS和SDN的相关概念

SDN

简介

SDN(Software-Defined Networking)是一种网络架构和理念,与传统网络架构最大的区别就在于将网络的控制平面从数据平面中分离出来,将网络决策集中在一个独立的软件控制器中,从而使网络更加灵活、可编程和易于管理。

在整体架构中,首先需要明确SDN控制器和网络设备两个概念:

  1. SDN控制器(SDN Controller):控制器是SDN的中央控制节点,它负责管理整个网络的行为和策略。控制器可以通过北向接口与上层应用程序或网络管理平台进行通信,通过南向接口与网络设备(如交换机和路由器)进行通信。控制器接收上层应用程序的指令,并将指令转化为底层网络设备的配置,从而实现对网络流量的控制。
  2. 网络设备(Switches/Router):SDN网络中的网络设备是传统交换机和路由器,但与传统网络不同的是,它们的数据转发平面和控制平面分离。网络设备在SDN架构中被称为数据面设备,负责根据SDN控制器下发的指令,进行数据包的转发和处理

具体的结构可参考下图:

在这里插入图片描述

OVS

简介

OVS(Open vSwitch)是一个开源的虚拟交换机软件, 是SDN架构中数据平面的一部分。通过使用OVS,可以起到以下作用:

  • 网络虚拟化: OVS 允许创建虚拟网络,这些网络可以在物理网络基础上进行划分,为不同的虚拟机、容器或租户提供独立的逻辑网络,从而增加了网络资源的灵活性和利用率。
  • 流量控制和管理: OVS 具有流表和流量匹配功能,可以根据不同的规则、条件和策略来控制和管理网络流量。这有助于实现负载均衡、流量隔离、QoS(服务质量)管理等。
  • 网络隔离和安全性: OVS 可以帮助实现虚拟网络之间的隔离,确保不同租户或应用之间的网络流量互不干扰。这有助于提高网络的安全性。
  • 网络监控和分析: OVS 支持流量镜像和抓包功能,可以方便地进行网络监控和分析,帮助排查问题、优化网络性能。
  • 灵活的网络配置: OVS 允许管理员动态地配置网络,添加、删除和修改网络规则,适应不同的应用需求,而无需依赖硬件更改。
  • 支持 SDN 控制器: OVS 可以与 SDN 控制器集成,通过控制器来统一管理和编程网络,实现集中式的网络控制和自动化。
  • 支持各种网络拓扑: OVS 支持多种网络拓扑,包括扁平网络、树状拓扑、多层拓扑等,使网络构建更加灵活。

主要组件

OVS中主要包含 ovs-vswtichd、ovsdb-server、openvswitch.ko 三个基本组件

  • ovs-vswtichd 是一个守护进程,是 ovs 的管理和控制服务,通过 unix socket 将配置信息保存到ovsdb,并通过 netlink 和内核模块交互
  • ovsdb 则是 ovs 的数据库,保存了 ovs 配置信息
  • openvswitch.ko 是 OVS 的内核模块,实现了 OVS 数据平面(Data Plane)和控制平面(Control Plane)之间的连接,其中还包含一个关键的部分就是datapath,通过 datapath 在内核层面完成数据包的转发、过滤和处理,以实现虚拟网络的各种功能。

各组件之间的关联关系可参考下图

在这里插入图片描述

图一

转发流程

通过上面的介绍,可以了解到 ovs 中包含一个名为 openvswith.ko 的内核模块,其中的 datapath 负责执行数据处理。那么接下来参考图一介绍当一个数据包到OVS后首次处理的流程:

  1. 当一个数据包到达系统,datapath 内核模块会拦截这个数据包进行处理
  2. datapath 内核模块会在流表中逐一匹配数据包,检查数据包的头部信息,例如源 MAC 地址、目标 MAC 地址、VLAN 标记、IP 地址等,以确定适用的流表规则,由于首次匹配不到则通过 upcall调用,将数据包以 netlink 协议上传到 vswitchd
  3. vswitchd通过 OpenFlow协议与控制器通信获取流表
  4. 用户态查询到的流表缓存至内核态的flow-table中
  5. vswitchd 通过reinject,使用netlink将包重新送回内核
  6. datapath 再根据数据包查询对应的动作
  7. 按照动作进行处理

由于第四步已经将该数据包的处理方式缓存到了内核中,那么当再有相同特征的数据包需要处理时,内核态就可直接处理。上述通过 ovs-vswitchd 查找 OpenFlow 实现转发的路径称为 slow-path,通过 OVS datapath直接转发的路径称为 fast-path,通过slow-path和fast-path的配合使用,完成网络数据的高效转发
在这里插入图片描述

图二

datapatch 中对应的转发规则可以通过 ovs-appctl dpif/dump-flows 进行查看

[root@ncspabdf00a0 ~]# ovs-appctl dpif/dump-flows <switch_name>
recirc_id(0),in_port(11),packet_type(ns=0,id=0),eth(dst=00:99:99:30:69:43),eth_type(0x8100),vlan(vid=1),encap(eth_type(0x0806)), packets:45033, bytes:2071518, used:11.008s, actions:5
...

代码分析

内核态

当包到网卡后, vport 注册的回调函数 netdev_frame_hook 会先调用到 ovs_vport_receive 处理接收报文,在 ovs_vport_receive 函数中从数据包中解析出用于匹配流表规则的字段信息,以便进行流表查找和匹配。最后调用 ovs_dp_process_packet 函数进入真正的 ovs 数据包处理。

/* Must be called with rcu_read_lock. */
void ovs_dp_process_packet(struct sk_buff *skb, struct sw_flow_key *key)
{
...
	/* Look up flow. */
	// 根据 mask和key进行匹配查找
	flow = ovs_flow_tbl_lookup_stats(&dp->table, key, skb_get_hash(skb),
					 &n_mask_hit);
    // 没有匹配到 flow, 需要发送到用户态进行慢速匹配
	if (unlikely(!flow)) {
		struct dp_upcall_info upcall;

		memset(&upcall, 0, sizeof(upcall));
		upcall.cmd = OVS_PACKET_CMD_MISS;
		upcall.portid = ovs_vport_find_upcall_portid(p, skb);
		upcall.mru = OVS_CB(skb)->mru;
		error = ovs_dp_upcall(dp, skb, key, &upcall, 0);  // 通过 upcall 发送到用户态 
		if (unlikely(error))
			kfree_skb(skb);
		else
			consume_skb(skb);
		// datapath 未匹配的包数
		stats_counter = &stats->n_missed;
		goto out;
	}
    
    // dp flow 中匹配的到,进行快速匹配
	ovs_flow_stats_update(flow, key->tp.flags, skb); // 更新流表状态信息 
	sf_acts = rcu_dereference(flow->sf_acts);
	error = ovs_execute_actions(dp, skb, sf_acts, key); // 执行 action
	if (unlikely(error))
		net_dbg_ratelimited("ovs: action execution error on datapath %s: %d\n",
							ovs_dp_name(dp), error);

	stats_counter = &stats->n_hit;

out:
	/* Update datapath statistics. */
	u64_stats_update_begin(&stats->syncp);
	(*stats_counter)++; //收包总数
	stats->n_mask_hit += n_mask_hit; //流表查询次数
	u64_stats_update_end(&stats->syncp);
}

先关注 upcall 调用,当没有找到匹配的流表时,内核通过 netlink 发送报文到用户态处理,对应代码中的 ovs_dp_upcall,该函数调用 queue_userspace_packet 函数构造发往用户层的 skb,通过 netlink 通信机制发送到用户态

int ovs_dp_upcall(struct datapath *dp, struct sk_buff *skb,
		  const struct sw_flow_key *key,
		  const struct dp_upcall_info *upcall_info,
		  uint32_t cutlen)
{
	...
    // 判断数据包是否分片
	if (!skb_is_gso(skb))
		err = queue_userspace_packet(dp, skb, key, upcall_info, cutlen);
	else
		err = queue_gso_packets(dp, skb, key, upcall_info, cutlen);
	if (err)
		goto err;
    ...
}

用户态

用户态对于 upcall 的处理函数 udpif_upcall_handler 会先在 udpif_start_threads 里面进行初始化

static void
udpif_start_threads(struct udpif *udpif, uint32_t n_handlers_,
                    uint32_t n_revalidators_)
{
    if (udpif && n_handlers_ && n_revalidators_) {
        /* Creating a thread can take a significant amount of time on some
         * systems, even hundred of milliseconds, so quiesce around it. */
        ovsrcu_quiesce_start();

        udpif->n_handlers = n_handlers_;
        udpif->n_revalidators = n_revalidators_;

        udpif->handlers = xzalloc(udpif->n_handlers * sizeof *udpif->handlers);
        for (size_t i = 0; i < udpif->n_handlers; i++) {
            struct handler *handler = &udpif->handlers[i];

            handler->udpif = udpif;
            handler->handler_id = i;
            // 创建 handler 线程
            handler->thread = ovs_thread_create(
                "handler", udpif_upcall_handler, handler);
        }
        ...
    }
}

udpif_upcall_handler 中通过 poll 的方式等待触发,如果有upcall请求,则进入 recv_upcalls 的处理函数中

/* The upcall handler thread tries to read a batch of UPCALL_MAX_BATCH
 * upcalls from dpif, processes the batch and installs corresponding flows
 * in dpif. */
static void *
udpif_upcall_handler(void *arg)
{
...
    while (!latch_is_set(&handler->udpif->exit_latch)) {

        if (recv_upcalls(handler)) {
            // 收到 upcall 调用,唤醒线程进行处理
            poll_immediate_wake();
        } else {
            // 未收到调用进行等待
            dpif_recv_wait(udpif->dpif, handler->handler_id);
            latch_wait(&udpif->exit_latch);
        }
        poll_block();
    }
...
}

至此,才是用户态对于 upcall 的真正处理过程,

static size_t
recv_upcalls(struct handler *handler)
{
    ...
    n_upcalls = 0;
    while (n_upcalls < UPCALL_MAX_BATCH) {
        ...
        // 通过 dpif_recv 将接收的数据放到 struct dpif_upcall 和 struct ofpbuf 中
        if (dpif_recv(udpif->dpif, handler->handler_id, dupcall, recv_buf)) {
            ofpbuf_uninit(recv_buf);
            break;
        }
        ...
		
        // 构造 struct upcall
        error = upcall_receive(upcall, udpif->backer, &dupcall->packet,
                               dupcall->type, dupcall->userdata, flow, mru,
                               &dupcall->ufid, PMD_ID_NULL);
		...
        // 开始处理
        error = process_upcall(udpif, upcall,
                               &upcall->odp_actions, &upcall->wc);
        ...
        n_upcalls++;
        continue;
    ...

    if (n_upcalls) {
        // 下发给 datapath
        handle_upcalls(handler->udpif, upcalls, n_upcalls);
        for (i = 0; i < n_upcalls; i++) {
            dp_packet_uninit(&dupcalls[i].packet);
            ofpbuf_uninit(&recv_bufs[i]);
            upcall_uninit(&upcalls[i]);
        }
    }

    return n_upcalls;
}

在 process_call 中会根据 upcall 的类型进行对应的处理,这里只关注 MISS_CALL 的处理,调用了 upcall_xlate 函数

static int
process_upcall(struct udpif *udpif, struct upcall *upcall,
               struct ofpbuf *odp_actions, struct flow_wildcards *wc)
{
    ...
    switch (upcall->type) {
    case MISS_UPCALL:
    case SLOW_PATH_UPCALL:
        // 交给 upcall_xlate 处理
        upcall_xlate(udpif, upcall, odp_actions, wc);
        return 0;
    ...
    }

    return EAGAIN;
}

upcall_xlate 最终调用到 classifier_lookup 查找到匹配的流表规则

/* Finds and returns the highest-priority rule in 'cls' that matches 'flow' and
 * that is visible in 'version'.  Returns a null pointer if no rules in 'cls'
 * match 'flow'.  If multiple rules of equal priority match 'flow', returns one
 * arbitrarily.
 *
 * If a rule is found and 'wc' is non-null, bitwise-OR's 'wc' with the
 * set of bits that were significant in the lookup.  At some point
 * earlier, 'wc' should have been initialized (e.g., by
 * flow_wildcards_init_catchall()).
 *
 * 'flow' is non-const to allow for temporary modifications during the lookup.
 * Any changes are restored before returning. */
const struct cls_rule *
classifier_lookup(const struct classifier *cls, ovs_version_t version,
                  struct flow *flow, struct flow_wildcards *wc)
{
    return classifier_lookup__(cls, version, flow, wc, true);
}

至此,用户态已经找到了可以处理数据包的流表,回到前面的 recv_upcalls 函数,接下来会调用 handle_upcalls 用于向 datapath 下发flow。

static size_t
recv_upcalls(struct handler *handler)
{
	...
    if (n_upcalls) {
        handle_upcalls(handler->udpif, upcalls, n_upcalls);
        for (i = 0; i < n_upcalls; i++) {
            dp_packet_uninit(&dupcalls[i].packet);
            ofpbuf_uninit(&recv_bufs[i]);
            upcall_uninit(&upcalls[i]);
        }
    }

    return n_upcalls;
}

handle_upcalls 最终会调用 dpif_operate 来下发flow,该接口针对不同的 dpif 实现,可以是 dpif_netdev_operate 或者 dpif_netlink_operate

static void
handle_upcalls(struct udpif *udpif, struct upcall *upcalls,
               size_t n_upcalls)
{
    /* Handle the packets individually in order of arrival.
     *
     *   - For SLOW_CFM, SLOW_LACP, SLOW_STP, SLOW_BFD, and SLOW_LLDP,
     *     translation is what processes received packets for these
     *     protocols.
     *
     *   - For SLOW_ACTION, translation executes the actions directly.
     *
     * The loop fills 'ops' with an array of operations to execute in the
     * datapath. */
	...
    dpif_operate(udpif->dpif, opsp, n_opsp, DPIF_OFFLOAD_AUTO);
	...
}

总结如上过程可如图

在这里插入图片描述

参考

  • https://blog.csdn.net/majieyue/article/details/52844738

  • https://www.sdnlab.com/my_sdnlab/wp-content/uploads/2017/02/cntctfrm_1a5b490b5708a374ad0d207df48ec29e_Openvswitch%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%E7%AC%94%E8%AE%B0.pdf