[转帖]k8s之udp丢包问题

k8s,udp,问题 · 浏览次数 : 0

小编点评

The provided context explains that the code is a program for testing a network performance issue related to the `conntrack` utility. Here's a summary of the key points: **Program functionality:** * It creates multiple threads to simulate network traffic. * Each thread creates an UDP socket and connects to a server. * The program updates the source port for each thread in a loop. * It uses the `conntrack` utility to monitor the number of insertions and drops on the server. * It prints the current number of insertions and drops for each thread. **Error handling:** * The code checks the return value of `conntrack` and prints an error message if it fails. **Performance optimization:** * The code uses multiple threads and updates the source port to reduce the number of socket connections. **Specific commands:** * `sudo ifconfig ens8 2.2.2.2/24` configures the IP address of the `ens8` interface to `2.2.2.2` with a subnet mask of `255.255.255.0`. * `arp -s 2.2.2.4 00:00:00:00:00:01` updates the ARP cache with the IP address of the server. **Overall, the code is intended to investigate a performance issue related to network traffic on a server running `conntrack`. It uses performance optimization techniques and handles errors gracefully.** **To reproduce the issue:** 1. Run the program with `sudo`. 2. Monitor the server's output for any error messages. 3. Check the server's `conntrack` configuration to ensure it's running correctly. **Note:** The provided context does not specify the server's configuration or the `conntrack` utility version, which could affect the results.

正文

https://www.jianshu.com/p/6a5faee4cc65

 


 
image.png

测试仪发送某种类型的报文进行性能测试,报文目的mac为VF的mac。对于发出去的每个报文,必须在3s内收到响应,否则认为丢包,会重新发送一次,此时会有retransmit的统计。

pod A有两个网卡,eth0(calico cni创建)和VF(sriov cni创建)。pod内部还运行着一个多线程进程,一个收包线程通过DPDK从VF收包,然后round robin分发到四个工作线程,这四个线程收到报文后,会封装一层udp(目的port固定,源port使用全局变量控制在一个范围内,四个线程每次获取一个port号后加一,注意此过程对全局变量操作没加锁),和一层ip(目的ip为service ip,源ip为pod A的eth0上的ip),然后调用原始套接字接口,将封装后的报文从eth0发给service ip。

pod X和pod Y是service的后端endpoint,通过socket从eth0收包处进行理。

问题描述:在测试仪上观察到大量retransmit计数。

问题分析:既然有retransmit,肯定是有丢包的,接下来重点分析为什么丢包,包被丢在什么位置。
a. 因为测试仪速率很大,最开始怀疑是达到性能瓶颈了,但是将速率降下来(每秒一百个报文左右)后发现仍然有retransmit计数,而且线程的cpupin都是没有问题的,不存在抢占问题,所以应该不是性能问题。
b. 降到速率后,观察到在VF处的收包统计,调用socket接口从eth0发包计数和测试仪是可以对上的,说明是在pod A的eth0发往service的路径上被丢弃的。
c. 查看drop计数。在pod A的eth0发往service的路径走的是kernel协议栈,既然丢包了大概率是有drop计数的,在报文路径上查看了如下几个drop计数,结果一无所获,很是遗憾。

    1. 网卡计数:ip -s link show dev
    2. 软中断计数: cat /proc/net/softnet_stat
    3. snmp中关于udp丢包计数(pod内部查看):cat /proc/net/snmp

d. 接下来就是抓包了,按照前面的拓扑图,抓包路径也比较清楚。分别在pod内部的eth0,worker的calixxx,tunl0和em1抓包。结果是在pod A的eth0和worker2的cali4cba4b78de8上能抓到两个报文(第一个为在后面流程被drop的,第二个为retransmit的),但是在worker2上的tunl0,em1和worker1上的pod中只能抓到retransmit的报文,说明报文肯定是在worker2上tunl0封装ipip报文之前被丢弃的。
e. 重点分析worker2上,报文从cali4cba4b78de8发出去到tunl0封装之间都有哪些流程。如下图,中间需要经过conntrack和nat模块处理。


 
image.png

报文从pod A经过eth0发出去后,会到达cali4cba4b78de8,然后进入worker2内核协议栈netif_rx_internal,在PREROUINT链上,报文首先经过连接跟踪模块ipv4_conntrack_in的处理,如果此报文是一条数据流的首包的话,查找全局链表肯定是找不到的,就会创建conntrack表项,并将表项添加到percpu的未确认链表上,然后经过nat模块时,查到dnat规则,修改目的ip和port,重新查找路由表。
在POSTROUTING链上,在ipv4_confirm函数中,会将conntrack表项从percpu的未确认链表上删除,同时添加到全局链表上。

这里有一个问题,假如数据包A和数据包B有相同的五元组,并且从两个cpu对应的线程发出,在ipv4_conntrack_in阶段,在全局链表查找不到表项,就会创建两个完全一样的conntrack表项(original和reply方向的tuple完全一样),后面经过dnat后,虽然reply的tuple可能会改变,但是original的tuple一直是不变的。在ipv4_confirm阶段,将两个tuple添加到全局链表时,如果数据包A的origianl和reply tuple先添加成功,那么当数据包B添加时,就会发现orginal的tuple已经存在了,就会丢弃数据包B,同时增加统计计数 insert_failed,代码如下

__nf_conntrack_confirm(struct sk_buff *skb)
    /* See if there's one in the list already, including reverse:
       NAT could have grabbed it without realizing, since we're
       not in the hash.  If there is, we lost race. */
    hlist_nulls_for_each_entry(h, n, &net->ct.hash[hash], hnnode)
        if (nf_ct_tuple_equal(&ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple,
                      &h->tuple) &&
            zone == nf_ct_zone(nf_ct_tuplehash_to_ctrack(h)))
            goto out;
    hlist_nulls_for_each_entry(h, n, &net->ct.hash[reply_hash], hnnode)
        if (nf_ct_tuple_equal(&ct->tuplehash[IP_CT_DIR_REPLY].tuple,
                      &h->tuple) &&
            zone == nf_ct_zone(nf_ct_tuplehash_to_ctrack(h)))
            goto out;

out:
    nf_ct_add_to_dying_list(ct);
    nf_conntrack_double_unlock(hash, reply_hash);
    NF_CT_STAT_INC(net, insert_failed);
    local_bh_enable();
    return NF_DROP;

再次分析下抓到的数据包发现每次丢包时,被丢的数据包和上一个数据包的五元组是完全相同的。因为同一个线程,源port是递加的,所以这俩个五元组完全一样的数据包应该是两个线程发送,正好符合上面的分析。

通过下面命令查看了下计数insert_failed,复现问题时果然在涨。

cat /proc/net/stat/nf_conntrack
或者
conntrack -S
[worker-2 ~]# cat /proc/net/stat/nf_conntrack
entries  searched found new invalid ignore delete delete_list insert insert_failed drop early_drop icmp_error  expect_new expect_create expect_delete search_restart
00000384  002f80e4 0f355850 00133416 0000dcc2 0006755f 0012688f 0006726d 00073df4 00000000 00000000 00000000 00000000  00000000 00000000 00000000 00000000
00000384  0003ae27 014d9677 0001d72b 000000e3 0006849d 00034c22 000318dd 0001a3e6 00000000 00000000 00000000 00000000  00000000 00000000 00000000 00000000
00000384  0003a791 014a3151 0001d96a 000000de 00068456 0002b175 00027d3c 0001a531 00000000 00000000 00000000 00000000  00000000 00000000 00000000 00000000
00000384  000393e7 0147cbbf 0001d4a6 000000ba 0006709a 00024252 00020edd 0001a131 00000000 00000000 00000000 00000000  00000000 00000000 00000000 00000000

现在肯定就是这个问题导致的,如何修改来验证呢?只要修改代码将四个线程发包时的源port写成固定,就不会有五元组完全一样的数据包了。

总结

此问题复现条件:

a. 多线程或者多进程,并且运行在不同的cpu上。只有不同cpu上的线程/进程才会真正并发执行,在链接跟踪表查找时才有可能出现冲突。
b. 相同的五元组。

后来上网查了下,这个问题在18年就被发现了,可以参考下此问题的root cause和如何fix
https://www.weave.works/blog/racy-conntrack-and-dns-lookup-timeouts
虽然fix了,但是计数insert_failed依然会增长,只不过不会丢包而已。

而且网上有如下两个说法

a. 只有udp才会出现此问题,tcp不会出现。这个说法也没错,因为在正常使用时,udp socket不用经过三次握手,每个线程可以直接调用udp socket发包,而tcp socket一般会在某个线程上创建,并且经过三次握手,此时会创建连接跟踪表项,所以即使有多个线程通过调用tcp socket发包,也不会有冲突。协议工作方式有关,而跟协议报文没关系。后面会使用原始套接字发送udp和tcp报文,都可以复现问题。
b. 多个线程同时调用同一个socket发包。这个说法的意思只是为了保证五元组是相同的,因为同一个socket源port和源ip是不变的。其实使用多个原始套接字同时发包也能出现此问题。

复现

如下代码,创建四个线程,每个线程创建一个udp类型的原始套接字同时发包,目的port为80,目的ip为2.2.2.4, 源ip为2.2.2.2,源port不断变化。

root@node2:~# cat udp_client.c
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <linux/if.h>
#include <linux/udp.h>
#include <linux/ip.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#define __USE_GNU
#include<sched.h>
#include<ctype.h>
#include<string.h>
#include<pthread.h>

#define THREAD_COUNT 4

struct sockaddr_in sin;
unsigned char sendBuf[sizeof(struct ethhdr) + 60];
int info[THREAD_COUNT];
int sockfd[THREAD_COUNT];
static int source_port;
static int source;
static int last_use[THREAD_COUNT];

unsigned short checksum(unsigned short *buf,int nword)
{
    unsigned long sum;

    for(sum=0;nword>0;nword--)
    {
        sum += *buf++;
        sum = (sum>>16) + (sum&0xffff);
    }
    return ~sum;
}

#define PORT_RANGE_START (40000)
#define PORT_RANGE_END   (65535)
#define PORT_RANGE_MASK  (PORT_RANGE_END - PORT_RANGE_START)

uint16_t get_port(uint32_t seed)
{
    return (seed & PORT_RANGE_MASK) + PORT_RANGE_START;
}

int create_socket(int index)
{
    //套接字
    sockfd[index] = socket(AF_INET, SOCK_RAW, IPPROTO_UDP);
    if (sockfd[index] == -1)
    {
        perror("create socket fail");
        return 0;
    }

    int daddr = inet_addr("2.2.2.4");

    sin.sin_family = AF_INET;
    sin.sin_port = htons(80);
    sin.sin_addr.s_addr = daddr;

    int val = 1;
    if (setsockopt(sockfd[index], IPPROTO_IP, IP_HDRINCL, &val, sizeof(val)) <  0)
    {
        printf("IP_HDRINCL\n"); return 0;
    }

    //填充IP头部
    struct iphdr ipheader;
    memset(&ipheader, 0, sizeof(struct iphdr));
    ipheader.version = 0x4;
    ipheader.ihl = 0x5;
    ipheader.tos = 0x00;
    ipheader.tot_len = htons(60);  //20 + 8 + 32
    ipheader.id = 0x1000;
    ipheader.frag_off = 0x0000;
    ipheader.ttl = 128;
    ipheader.protocol = 17;
    ipheader.check = 0;
    ipheader.saddr = inet_addr("2.2.2.2");
    ipheader.daddr = daddr;
    ipheader.check = checksum((void *)&ipheader, sizeof(struct iphdr));

    struct udphdr udpheader;
    udpheader.source = htons(1000);
    udpheader.dest = htons(80);
    udpheader.len = htons(40);
    udpheader.check = 0;

    memset(sendBuf, 0, sizeof(sendBuf));
    memcpy(sendBuf, &ipheader, sizeof(struct iphdr));
    memcpy(sendBuf + sizeof(struct iphdr), &udpheader, 8);
}

void *worker(void *arg)
{
    int *a = (int *)arg;
    int index = *a;
    printf("the index is: %d\n", index);  //显示是第几个线程

    cpu_set_t mask;  //CPU核的集合
    CPU_ZERO(&mask);    //置空
    CPU_SET(index, &mask);   //设置亲和力值
    if (sched_setaffinity(0, sizeof(mask), &mask) == -1)//设置线程CPU亲和力
    {
        printf("warning: could not set CPU affinity, continuing...\n");
    }

    create_socket(index);
    while (1) {
        if (source == last_use[index])
            continue;

        struct udphdr* udpheader = NULL;
        udpheader = (struct udphdr*)(sendBuf + sizeof(struct iphdr));
        last_use[index] = source;
        udpheader->source = htons(source);
        udpheader->check = 0;
        udpheader->check = checksum((void *)udpheader, 40);
        printf("source port is %d on thread %d socket: %d\n", udpheader->source, index, sockfd[index]);
        int len= sendto(sockfd[index], sendBuf, sizeof(sendBuf), 0, (const struct sockaddr*)&sin, sizeof(struct sockaddr));
        if (len > 0)
            printf("send success.\n");
        else
            perror("send fail");
    }
}

int main(void)
{
    int i = 0;

    //创建多个线程
    for (i = 0; i < THREAD_COUNT; i++) {
        pthread_t p_info = (pthread_t)malloc(sizeof(pthread_t));
        info[i] = i;
        int err = pthread_create(&p_info, NULL, &worker, (void*)&info[i]);
        if (err != 0) {
            printf("pthread create failed\n");
        }
    }

    //每隔一段时间就更新源port
    i = 0;
    while (i != 10000) {
        source = htons(get_port(source_port++));
        usleep(1000);
        i++;
    }

    return 0;
}

下面编译程序,开始复现

//配置ens8 ip为2.2.2.2/24
root@node2:~#ifconfig ens8 2.2.2.2/24
//2.2.2.4是不存在的,所以需要配置静态arp
root@node2:~#arp -s 2.2.2.4 00:00:00:00:00:01

//编译
root@node2:~#gcc -o udp udp_client.c -lpthread

//程序执行前,查看计数
root@node2:~# conntrack -S
cpu=0           found=0 invalid=57309 ignore=1171 insert=0 insert_failed=2636 drop=1730 early_drop=0 error=0 search_restart=3215
cpu=1           found=0 invalid=53922 ignore=1368 insert=0 insert_failed=3344 drop=2464 early_drop=0 error=0 search_restart=3048
cpu=2           found=0 invalid=61931 ignore=1591 insert=0 insert_failed=2573 drop=2035 early_drop=0 error=0 search_restart=2669
cpu=3           found=0 invalid=59014 ignore=383 insert=0 insert_failed=1124 drop=571 early_drop=0 error=0 search_restart=1790

//执行程序一段时间,比如一分钟
root@node2:~#./udp

//再次查看计数,可看到insert_failed已经在涨了
root@node2:~# conntrack -S
cpu=0           found=0 invalid=57309 ignore=1171 insert=0 insert_failed=2637 drop=1730 early_drop=0 error=0 search_restart=3215
cpu=1           found=0 invalid=53922 ignore=1368 insert=0 insert_failed=3345 drop=2464 early_drop=0 error=0 search_restart=3048
cpu=2           found=0 invalid=61931 ignore=1591 insert=0 insert_failed=2573 drop=2035 early_drop=0 error=0 search_restart=2669
cpu=3           found=0 invalid=59014 ignore=383 insert=0 insert_failed=1126 drop=571 early_drop=0 error=0 search_restart=1790

参考

此问题的复现方法和fix时的log
https://github.com/brb/conntrack-race

腾讯云-k8s网络疑难杂症排查分享
https://segmentfault.com/a/1190000020069368?utm_source=tag-newest

与[转帖]k8s之udp丢包问题相似的内容:

[转帖]k8s之udp丢包问题

https://www.jianshu.com/p/6a5faee4cc65 image.png 测试仪发送某种类型的报文进行性能测试,报文目的mac为VF的mac。对于发出去的每个报文,必须在3s内收到响应,否则认为丢包,会重新发送一次,此时会有retransmit的统计。 pod A有两个网卡,

[转帖]k8s之PV、PVC、StorageClass详解

https://zhuanlan.zhihu.com/p/128552232 导读 上一篇写了共享存储的概述以及一个简单的案例演示。这一篇就写一下PV和PVC。 PV是对底层网络共享存储的抽象,将共享存储定义为一种“资源”,比如Node也是容器应用可以消费的资源。PV由管理员创建和配置,与共享存储的

[转帖]10--k8s之数据持久化

https://www.cnblogs.com/caodan01/p/15136217.html 目录 一、emptDir 二、hostPath 三、pv 和 pvc 1.环境准备 2.创建pv 3.创建pvc 4.使用 5.综合案例 一、emptDir emptyDir类型的volume在pod分

[转帖]k8s 存储卷之 PV & PVC

https://cloud.tencent.com/developer/article/2070495?areaSource=&traceId= 文章目录 k8s 存储卷之 PV & PVC 高级存储 PV 创建 PV 实例 PVC Pod 挂载数据卷 生命周期 k8s 存储卷之 PV & PVC

[转帖]k8s发布Spring cloud+eureka架构服务优雅启动停止方案

本文转载自昆仑枫的简书https://www.jianshu.com/p/6d393cbb694a Spring cloud+eureka是目前微服务主流解决方案之一,kubernetes则是广泛应用的发布工具,两者结合使用很常见。而两者结合时如何优雅启停从而实现无感发布很关键。下面将从不做特殊处理

[转帖]K8S部署Redis Cluster集群(三主三从模式) - 部署笔记

https://www.cnblogs.com/cheyunhua/p/15619317.html 一、Redis 介绍 Redis代表REmote DIctionary Server是一种开源的内存中数据存储,通常用作数据库,缓存或消息代理。它可以存储和操作高级数据类型,例如列表,地图,集合和排序

[转帖]k8s部署有状态应用redis-cluster集群踩坑总结

https://segmentfault.com/a/1190000039196137 redis-cluster集群的部署网上一堆,用k8s部署也不在少数,但都是抄来抄去,问题不少,实际操作分享出来的还是太少。 1、redis启动配置文件,使用CofigMap来管理比较方便,redis-confi

[转帖]k8s-mtu设置不当引发的线上故障

https://www.cnblogs.com/zisefeizhu/p/16611626.html 背景 在部署新的paas平台线上环境时,突发consul和es中间件无法创建。 排查过程 以consul 通过查询k8s集群中pod状态发现原来3pod的consul集群,其中2个pod一直重启。

[转帖]K8S集群内部 无持久化Redis单节点配置文件

K8S集群内部 无持久化Redis单节点配置文件 apiVersion: apps/v1 kind: Deployment metadata: name: redis-deploy spec: selector: matchLabels: prod: redis replicas: 1 templa

[转帖]k8s证书永不过期

https://www.cnblogs.com/fengzi7314/p/15909220.html 第一步,在github上下载对应的版本,我的是1.22版本,所以选的是1.22版本 第二步,下载到本地解压 第三步,进入目录编辑文件,修改为100年 cd kubernetes-1.22.0/ vi