linux max open files限制过小导致openvswitch工作异常

上篇分析了linux内核的netdev_max_backlog默认设置导致了openvswitch在大量virtual port转发异常的问题,本篇再额外记录一处与大量virtual port相关的调优点。

自openvswitch 2.x版本开始,ovs引入了多线程的支持,在非dpdk即传统的ovs应用模式下,多线程主要用于处理内核fastpath lookup miss的流表,此过程称为upcall;并对这些流表进行统计、老化处理,此过程称为revalidate。

upcall和revalidate的线程数量计算,以openvswitch 2.3.x为例,代码中的判断是将物理核数约3/4分配给upcall,而revalidate则占1/4,这个分配直观上是合理的,毕竟upcall处理转发,要尽可能的快,处理能力也要更强。

而我们知道openvswitch datapath对用户态的每个virtual port都会在linux内核datapath中创建一个vport,内核datapath通过netlink发送消息给upcall,为了更快的处理每个vport的upcall请求,openvswitch将会为每个virtual port分配等同upcall线程数量的netlink socket,内核datapath根据flow hash将同一vport的upcall分发给不同的netlink socket,实现不同upcall线程间的并发处理,提升upcall性能。

从这里可以看出,openvswitch在优化性能方面确实做足了功夫,但这里确存在一个值得考虑的地方,我们知道linux process存在一个max open files的限制,当限制达到后,新的fd将无法被分配,导致建立文件、创建socket等需要用到fd资源的操作无法执行。

那么我们可以估计一个24 physical core的系统上,如果有2000个virtual port,openvswitch将会有多少个netlink socket,243/42000即36000,也即单音用于处理vport的netlink socket就占用了36000个fd,如果对openvswitch的max open files限制过小,很容易导致在大量virtual port下,出现各种工作异常。

对于此问题主要的优化思路如下:

  • 调整ovs-vswitchd进程的max open files limit上限,可按照前述算法估量
  • openvswitch提供了一个n-handler-threads的配置,可通过ovs-vsctl持久化配置,根据性能需求适当的减少upcall线程数量

linux内核netdev_max_backlog设置在极端情况下导致openvswitch转发不通

近期在调试问题时,遇到了一个从配置上看openvswitch无任何问题,但却导致转发不通的问题,特此记录下说,说明在openvswitch在大规模部署时,仍需要有较多调优之处。

设想如下openstack场景,大量tenant router通过linux namespace即仿真vrouter访问外网,此namespace中通过openvswitch internal port(或veth,如果不追求转发性能)连接到另一公网namespace,此时如果公网namespace中接口数量过多,比如说2000个(比如openstack中的qg-*接口),将有可能面临转发不通的情况。

从openvswitch的角度,我们知道openvswitch在内核态存在一个datapath,负责生成内核流表,实现高性能转发,对于一个拥有上千个port的ovs bridge来说,比如对应于上面的公网namespace,收到一个arp请求报文,由于是广播,报文通守openvswitch在userspace命中默认的normal流表后将推送广播至其他所有成员端口的内核流表,此后arp报文广播复制逻辑将在内核datapath模块处执行,最终命中以下内核代码:

:::c
/*
* enqueue_to_backlog is called to queue an skb to a per CPU backlog
* queue (may be a remote CPU queue).
*/
static int enqueue_to_backlog(struct sk_buff *skb, int cpu, unsigned int *qtail)
{
    struct softnet_data *sd;
    unsigned long flags;

    sd = &per_cpu(softnet_data, cpu);

    local_irq_save(flags);

    rps_lock(sd);
    if (skb_queue_len(&sd->input_pkt_queue) <= netdev_max_backlog) {
        if (skb_queue_len(&sd->input_pkt_queue)) {
enqueue:
	        __skb_queue_tail(&sd->input_pkt_queue, skb);
		     input_queue_tail_incr_save(sd, qtail);
             rps_unlock(sd);
             local_irq_restore(flags);
             return NET_RX_SUCCESS;
		 }

         /* Schedule NAPI for backlog device
          * We can use non atomic operation since we own the queue lock
          */
         if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state)) {
             if (!rps_ipi_queued(sd))
                 ____napi_schedule(sd, &sd->backlog);
         }
         goto enqueue;
    }

    sd->dropped++;
    rps_unlock(sd);

    local_irq_restore(flags);

    atomic_long_inc(&skb->dev->rx_dropped);
    kfree_skb(skb);
    return NET_RX_DROP;
}

可以注意到在上面的代码中有一个对sd->input_pkt_queue的检查过程,而sd是每个cpu核心net rx soft irq用于维护待处理报文的队列,netdev_max_backlog则是内核所提供的一个配置选项,默认为1000。

在ovs datapath复制报文的过程中,将对上千个port进行遍历处理,这个处理过程中不会进行cpu释放的操作,也即将持续调用enqueue_to_backlog(openvswitch datapath最初调用的是netif_rx,但最终调到enqueue_to_backlog),那么默认的1000限制将很快达到,假如上述的qg-*所公用的外网网关碰巧位于1000个以后,那么很显然,某个vrouter namespace发过来的ARP请求则无法被发送给公网外网网关接口,导致vrouter所对应的内网无法访问外网,如果是在生产环境中,势必影响客户业务。

对于此问题的修改,有如下两种思路:

  • 直接提高netdev_max_backlog数量,根据可能存在的接口数量合理设置
  • 通过sdn控制器配置arp或动态回复arp请求

由于目前项目中所采用的方案,我们通过方案1进行了改造,较好的解决了此问题。