这里我们把遇到的坑都列一下,主要针对OVS-DPDK和OVS的区别处遇到的坑,OVS版本2.7.0,DPDK16.11.1版本。

hugepage配置问题


首先需要说明的是因为OVS-DPDK使用了hugepage,所以如果VM要和OVS-DPDK进行通信的话,也就必须使用hugepage。hugepage配置基本上有两种方案,2M和1G的,下面针对两个分别说一下:

  • 1G的需要在grub中指定默认为1G的hugepage,并且不支持创建小于1G内存的VM
  • 2M的hugepage,如果需要较大的内存,需要注意内存块映射的限制值,这块接下来会着重说一下,因为正好遇到了坑

我的环境内存为16Gx16=256G,此时我使用的是2M的hugepage,但是我发现只要nr_hugepages配置大了,OVS-DPDK启动就会失败,报错如下:

EAL: Failed to remap 2 MB pages
PANIC in rte_eal_init():
Cannot init memory

一下以nr_hugepages=40960为例进行分析为什么会失败。经过上面的报错我们直奔主题,去看代码

ovs-vswitchd.c的main-->bridge_run-->dpdk_init-->dpdk_init__-->rte_eal_init会调用DPDK的eal.c的rte_eal_memory_init-->rte_eal_hugepage_init

int rte_eal_hugepage_init(void)
{
    //此处循环是针对不同的page size的,即2M和1G,我们没有配置1G,只有2M的
    for (i = 0; i < (int)internal_config.num_hugepage_sizes; i ++){
        //此处是将所有的hugepage的内存文件进行映射
        //因为我们配置了40960个,所以需要全部映射
        //下面会对该函数进行分析
        pages_new = map_all_hugepages(&tmp_hp[hp_offset], hpi, 1);

        //重新映射,这里主要是为了尽量将内存物理地址连续的
        //也都映射为连续的虚拟地址
        if (map_all_hugepages(&tmp_hp[hp_offset], hpi, 0) !=
                    hpi->num_pages[0]) {
            RTE_LOG(ERR, EAL, "Failed to remap %u MB pages\n",
                        (unsigned)(hpi->hugepage_sz / 0x100000));
            goto fail;
        }

        //解除第一次的映射
        if (unmap_all_hugepages_orig(&tmp_hp[hp_offset], hpi) < 0)
          goto fail;

    }
}
static unsigned map_all_hugepages(struct hugepage_file *hugepg_tbl,
            struct hugepage_info *hpi, int orig)
{
    size_t vma_len = 0;

    for (i = 0; i < hpi->num_pages[0]; i++) {
        uint64_t hugepage_sz = hpi->hugepage_sz;

        //第一次映射都需要创建该结构
        //第二次映射此标识为0,忽略
        if (orig) {
            hugepg_tbl[i].file_id = i;
            hugepg_tbl[i].size = hugepage_sz;
            eal_get_hugefile_path(hugepg_tbl[i].filepath,
                        sizeof(hugepg_tbl[i].filepath), hpi->hugedir,
                        hugepg_tbl[i].file_id);
            hugepg_tbl[i].filepath[sizeof(hugepg_tbl[i].filepath) - 1] = '\0';
        }
        //第一次或者是当vma_len映射完毕后重新查看需要映射的物理地址的连续性
        else if (vma_len == 0) {
            unsigned j, num_pages;

            //检测物理地址是否连续
            for (j = i+1; j < hpi->num_pages[0] ; j++) {
                if (hugepg_tbl[j].physaddr !=
                            hugepg_tbl[j-1].physaddr + hugepage_sz)
                  break;
            }
            num_pages = j - i;
            vma_len = num_pages * hugepage_sz;

            //根据物理地址的连续性找到匹配的虚拟地址的连续长度
            vma_addr = get_virtual_area(&vma_len, hpi->hugepage_sz);
            if (vma_addr == NULL)
              vma_len = hugepage_sz;
        }

        //直接open hugepage
        fd = open(hugepg_tbl[i].filepath, O_CREAT | O_RDWR, 0600);

        //进行内存地址的映射
        //经过debug发现是第二次调用函数映射的时候出错
        //并且出错位置就在此处,errno为22
        virtaddr = mmap(vma_addr, hugepage_sz, PROT_READ | PROT_WRITE,
                    MAP_SHARED | MAP_POPULATE, fd, 0);
        if (virtaddr == MAP_FAILED) {
            RTE_LOG(DEBUG, EAL, "%s(): mmap failed: %s\n", __func__,
                        strerror(errno));
            close(fd);
            return i;
        }
    }
}

经过以上代码能够确认,虚拟地址够用,物理地址也存在,但是映射失败,并且每次运行崩溃的时机基本一致,都和循环次数有关,第二次的23000+的映射时报错,所以怀疑和系统的限制有关,经过群基友的提示,和进程的虚拟内存map的配置相关,即查看每个进程的map限制

$ cat /proc/sys/vm/max_map_count
65530

经过查看此值为65530,而我们第一次映射为40960,第二次映射时累加就会超过65530,所以会报错。这也就能解释为什么第二次映射时都是到固定的位置报错。而查看内核我们看到该全局变量sysctl_max_map_count是32位的,所以可以支持很大,我们配置大小为2048000之后,再运行OVS-DPDK就没有问题了。

sysctl -w vm.max_map_count=2048000

OVS-DPDK的conntrack不支持分片报文


问题描述

在VM中发送大于MTU的UDP报文,会发现网络不通。经过查看流表发现报文过conntrack的时候被标记为+inv标志,并且丢弃了。

问题代码分析

通过查看代码(路径为pmd_thread_main-->dp_netdev_process_rxq_port-->dp_netdev_input-->dp_netdev_input__-->dp_execute_cb-->conntrack_execute)

int
conntrack_execute(struct conntrack *ct, struct dp_packet_batch *pkt_batch,
            ovs_be16 dl_type, bool commit, uint16_t zone,
            const uint32_t *setmark,
            const struct ovs_key_ct_labels *setlabel,
            const char *helper)
{
    struct dp_packet **pkts = pkt_batch->packets;
    size_t cnt = pkt_batch->count;
    struct conn_lookup_ctx ctxs[KEY_ARRAY_SIZE];

    for (i = 0; i < cnt; i++) {
        //此处会解析报文头,然后计算hash值
        //解析过程中,如果是分片报文,则返回false
        //出场关于可以看到false时,报文设置为CS_INVALID
        //所以问题出现在这里,分片的报文期望也能找到conntrack
        if (!conn_key_extract(ct, pkts[i], dl_type, &ctxs[i], zone)) {
            write_ct_md(pkts[i], CS_INVALID, zone, 0, OVS_U128_ZERO);
            continue;
        }

        ...
    }

    ...

    return 0;
}

conntrack_execute-->conn_key_extract-->extract_l3_ipv4

static inline bool
extract_l3_ipv4(struct conn_key *key, const void *data, size_t size,
                const char **new_data, bool validate_checksum)
{
    //分片报文,返回值即为false,不管是首个还是之后的分片报文
    if (IP_IS_FRAGMENT(ip->ip_frag_off)) {
        return false;
    }
}

解决方案

方案1

学习kernel的方案,在conntrack匹配之前首先对报文进行重组,如果我们现在做了重组之后的好处是,报文只查一次,然后发报文之前可能还需要再分片,比较麻烦。

方案2

分片的报文不进行重组,只是分片的后续报文也需要匹配conntrack,我们采取的方案是分片首包除了需要创建之前的连接维护,还需要根据源、目的IP和ID进行hash值计算,根据将该hash和首包的conntrack信息进行映射,目前经过思考觉得该方案其实也挺消耗资源,如果每个报文都有分片,每个报文的分片都需要和conntrack建立映射,这样映射的建立和销毁也很频繁。

OVS-DPDK的NAT patch不支持冲突解决


问题描述

配置完NAT之后,两个vm同时ping一个IP,然后流表做SNAT,IP替换为宿主机的IP,发现偶尔会出现vm1的ping没有任何回复报文,而vm2 ping一次收到两个回复报文,怀疑是因为NAT没有做冲突查看和解决,当两个ping的ICMP层的ID值一致的时候,NAT的完出去的报文,收到回复报文的时候,没有办法区分两个UNNAT,所以都会给后面的那个vm做UNNAT。

OVS的NAT代码分析

__netif_receive_skb-->__netif_receive_skb_core-->netdev_frame_hook-->netdev_port_receive-->ovs_vport_receive-->ovs_dp_process_packet-->ovs_execute_actions-->do_execute_actions-->ovs_ct_execute首先调用handle_fragments进行报文重组,根据commit操作调用ovs_ct_commit进行conntrack创建,不是commnit则调用ovs_ct_lookup查找conntrack,这里我们先不管,ovs_ct_commitovs_ct_lookup都会调用__ovs_ct_lookup,前者是查找有没有冲突,后者是查找conntrack,我们主要关注这个函数。当查找到conntrack之后会判定conntrack是否有NAT信息,如果有的话调用ovs_ct_nat进行NAT操作。

ovs_ct_nat

  • untracked状态时以及没有执行commit的不进行NAT
  • 查看NAT类型,比如SNAT、DNAT、或者是reply报文的NAT,对源、目的需要做一个转换
  • 调用函数ovs_ct_nat_execute进行具体的NAT操作
  • 最后NAT完成之后需要调用ovs_nat_update_key更新flow key

ovs_ct_nat_execute

  • 根据不同的NAT类型,选择不同的hook点,SNAT选择LOCAL_IN,DNAT选择LOCAL_OUT,这里是NAT时需要的一个参数
  • 如果是reply报文,则需要调用ICMP的函数nf_nat_icmp_reply_translation进行翻译,使他看起来和之前的报文一样匹配conntrack,ipv6的是也调用类似的函数
  • 如果是新建的conntrack信息,则调用nf_nat_initialized初始化,调用nf_nat_setup_info进行安装NAT信息
  • 调用nf_nat_packet进行报文的NAT操作

nf_nat_setup_info

  • 调用nf_ct_nat_ext_add给conntrack创建NAT信息
  • 调用get_unique_tuple从NAT规则的范围中获取一个可用元组

get_unique_tuple

  • 如果源IP或者L4信息已经映射了,同样的映射符合范围内的一个唯一的元组,就使用他,主要是L3的in_range和L4的in_range函数处理,如果没有接着往下看
  • 在给定的范围内选择最少使用的IP或者L4信息
  • 根据L4的in_range进行验证并确定是唯一的元组nf_nat_used_tuple,然后就可以了,如果没有接着往下看
  • 调用L4的unique_tuple进行唯一性的处理

以上提到了好多次的L3和L4的一些函数指针,表示不同的协议有不同的操作,L3主要是包括ipv4和ipv6,L4支持的协议包括dccp、gre、icmp、icmpv6、sctp、tcp、udp、udplite、unkown。

icmp_unique_tuple

  • 如果指定了ICMP的ID或者范围,则进行循环选择并确定唯一性
  • 没有指定的话则随意选择

dccp_unique_tuple,sctp_unique_tuple,tcp_unique_tuple,udp_unique_tuple,udplite_unique_tuple都是调用的如下函数nf_nat_l4proto_unique_tuple

nf_nat_l4proto_unique_tuple

  • 如果没有指定范围,DNAT时目的端口不能改变,SNAT时源端口可以改变
  • 端口的变化范围有几个限制,端口是512以内的映射范围是1-512,端口是512-1024的映射范围是600-1024,1024以上的映射范围就是1024以上
  • 如果指定了端口的变化范围,那就按照指定的来
  • 如果是NF_NAT_RANGE_PROTO_RANDOM模式的话,调用L3的secure_port,根据源目的IP和需要修改的端口计算一个hash值。
  • 如果是NF_NAT_RANGE_PROTO_RANDOM_FULLY模式的话,直接计算随机数
  • 根据得到的值根据范围取余,再加上最小值就得到的端口,然后判定是否已用,用了的话加1再判定。

nf_nat_packet

  • 首先查找3层协议和4层协议
  • 调用3层协议的manip_pkt函数指针进行报文的操作,参数包括4层协议

nf_nat_ipv4_manip_pkt

  • 报文偏移到L3
  • 调用L4的manip_pkt函数指针进行报文操作
  • 重新计算csum
  • 修改源或者目的IP

icmp_manip_pkt

  • 报文偏移到L4
  • 重新计算csum
  • 修改ICMP的ID

udp_manip_pkt

  • 报文偏移到L4
  • 重新计算csum
  • 修改UDP的源或者目的端口

tcp_manip_pkt

  • 报文偏移到L4
  • 重新计算csum
  • 修改TCP的源或者目的端口

分片报文不能通过带五元组的精细流表


问题描述

问题同时存在与OVS和OVS-DPDK中,当vm中发出的报文是带分片的,但是我们的转发流表比较精细的话(比如UDP,流表匹配五元组),那么报文不能正常转发,当流表只是3元组,不包括源、目的端口的时候可以正常转发。

vhotuser重连的问题


问题描述见链接

新建虚拟机首包容易丢失的问题


问题描述

这个问题不只是DPDK版本的有,OVS如果是物理网口加到桥上,然后IP配置到桥上的场景中也可能会出现。必现场景是需要tunnel封装后需要发送的HOST的arp信息不存在时,报文就会丢失。这里说的arp信息是指ovs存储的arp信息,查看方式为ovs-appctl tnl/arp/show,我们只需要执行ovs-appctl tnl/arp/flush后,即可复现。

问题代码分析

handle_packet_upcall-->dp_netdev_upcall-->upcall_cb-->upcall_xlate-->xlate_actions-->do_xlate_actions-->xlate_output_action-->compose_output_action-->compose_output_action__-->build_tunnel_send是创建封装流表的时候,会调用tnl_neigh_lookup查询arp信息,如果查询不到则调用tnl_send_arp_request发送arp请求,然后返回。如果查找到了则会创建datapath流表。所以最开始的一个报文,没有创建datapath流表,这个问题不是很紧急,所以暂时还没有解决,基本思路就是先创建一个flood的datapath流表,他的hard_timeout很小,转发完报文就销毁,后续有了arp之后,报文就不会出现问题。

ovs-dpdk不能解封装geneve报文的问题


当有多个ovn-controller的时候,其中一个ovn-controller服务关掉之后,任何节点的ovs-dpdk都不能正确解封装报文,经过查看发现关闭ovn-controller服务的时候,ovs-vsctl show命令能看到和该宿主机的tunnel口删掉了,而删掉这个tunnel口的时候,发现监听端口也被删除了,这个通过命令ovs-appctl tnl/ports/show -v可以查看,现在需要查看代码,为什么会删除一个tunnel口就会删除监听,理论上应该是没有任何tunnel口之后才会删除监听。

port_construct-->tnl_port_add-->tnl_port_add__-->tnl_port_map_insert只有第一次才会添加,后续的就不再操作了。而通过map_insert_ipdev__-->map_insert看到引用计数只有一次,无论添加了多少个tunnel口。

port_destruct-->tnl_port_del-->tnl_port_del__-->tnl_port_map_delete直接减引用计数,然后删除tunnel口。

由以上我们可以看出,只要删除一个tunnel口,那么监听端口就删掉了,会影响解封装,导致网络异常。我们需要做的就是引用计数的处理,这个问题我暂时提交了一个patch

To Be Continued ...

最后修改:2021 年 08 月 18 日
如果觉得我的文章对你有用,请随意赞赏