searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

虚机热迁移mutlfd概率失败原因排查

2025-06-23 10:43:26
2
0

问题背景

虚机做热迁移mutlfd方式概率失败。失败的时候观测到源端无法连接到目的端,导致qemu内存数据无法传输。以下是问题发生时的现象:
79作为server端:

[root@cn-nm-region1-az3-compute-s7-10e8e105e79 ~]# netstat -antp | grep 49152
tcp6       3      0 :::49152                :::*                    LISTEN      709105/qemu-system-
tcp6       7      0 ::1:49152               ::1:50112               CLOSE_WAIT  -
tcp6      12      0 ::1:49152               ::1:49586               CLOSE_WAIT  -
tcp6       3      0 ::1:49152               ::1:39048               CLOSE_WAIT  709105/qemu-system-
tcp6       1      0 ::1:49152               ::1:45358               CLOSE_WAIT  -

80上连接79的49152端口,一直被拒绝。

[root@cn-nm-region1-az3-compute-s7-10e8e105e80 ~]# telnet 10.8.105.79 49152
Trying 10.8.105.79...
telnet: connect to address 10.8.105.79: Connection refused

问题排查

使用tcpdump在80(发送端)上抓包:
image.png

可见79(接收端)发送了RST直接中断了连接。
在79上采用bpftrace跟踪服务端处理流程:

bpftrace -e '
kprobe:tcp_v4_send_reset {
	printf("%s\\n\", kstack());
}
'

服务端处理流程如下:

tcp_v4_rcv
	...
	lookup:
	sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
			       th->dest, sdif, &refcounted);
	if (!sk)
		goto no_tcp_socket;

	// 正常流程会到 tcp_v4_do_rcv

	// 如果前面找不到sock
	no_tcp_socket:
	if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
		goto discard_it;

	tcp_v4_fill_cb(skb, iph, th);

	if (tcp_checksum_complete(skb)) {
csum_error:
		__TCP_INC_STATS(net, TCP_MIB_CSUMERRORS);
bad_packet:
		__TCP_INC_STATS(net, TCP_MIB_INERRS);
	} else {
		tcp_v4_send_reset(NULL, skb);	// 发送RST
	}

从上图可知,__inet_lookup_skb 查找不到监听的sock。继续跟踪 __inet_lookup_skb 流程:

tcp_v4_rcv
	__inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source, th->dest, sdif, &refcounted);
		__inet_lookup
			// 先在established的表里查找
			__inet_lookup_established		// hashinfo->ehash
			// 	established的表里查找不到,再到listener表里查找
			__inet_lookup_listener			// hashinfo->listening_hash

在hash表里找不到sock, 因此发送RST报文拒绝连接。

考虑到监听是ipv6的,怀疑不能通过ipv4连接,编写如下代码进行验证:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 61889
#define BACKLOG 5

void error_handling(const char *message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main() {
    int server_sock, client_sock;
    struct sockaddr_in6 server_addr, client_addr;
    socklen_t client_addr_size;

    // 创建 IPv6 套接字
    server_sock = socket(AF_INET6, SOCK_STREAM, 0);
    if (server_sock == -1) {
        error_handling("socket() error");
    }

    // 初始化服务器地址结构体
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin6_family = AF_INET6;
    server_addr.sin6_addr = in6addr_any;
    server_addr.sin6_port = htons(PORT);

    // 绑定套接字到指定地址和端口
    if (bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        error_handling("bind() error");
    }

    // 开始监听连接请求
    if (listen(server_sock, BACKLOG) == -1) {
        error_handling("listen() error");
    }

    client_addr_size = sizeof(client_addr);

    printf("Waiting for connections on port %d (IPv6)...\n", PORT);

    while (1) {
        // 接受新的连接请求
        client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &client_addr_size);
        if (client_sock == -1) {
            error_handling("accept() error");
        }

        char client_ip[INET6_ADDRSTRLEN];
        // 将客户端的 IPv6 地址转换为字符串形式
        inet_ntop(AF_INET6, &client_addr.sin6_addr, client_ip, INET6_ADDRSTRLEN);
        // 打印客户端连接信息
        printf("New connection from %s:%d\n", client_ip, ntohs(client_addr.sin6_port));

        // 这里可以添加处理客户端请求的代码
        // 例如读取客户端发送的数据、发送响应等

        // 关闭客户端套接字
        close(client_sock);
    }

    // 关闭服务器套接字
    close(server_sock);
    return 0;
}

发现是可以连接的
image.png

[root@cn-nm-region1-az3-compute-s7-10e8e105e80 ~]# telnet 10.8.105.79 61889
Trying 10.8.105.79...
Connected to 10.8.105.79.
Escape character is '^]'.
Connection closed by foreign host.

和虚拟化同事沟通,得知连不上的时候,他们添加了IPV6_V6ONLY标志:

image.png

于是修改上述服务端代码,在bind前添加setsockopt IPV6_V6ONLY:
image.png

成功复现了问题。
image.png

[root@cn-nm-region1-az3-compute-s7-10e8e105e80 ~]# telnet 10.8.105.79 61889
Trying 10.8.105.79...
telnet: connect to address 10.8.105.79: Connection refused

从上面可看出,服务端监听的时候使用 IPV6_V6ONLY,客户端用ipv4的协议去连接是不行的。因为在listener表里找不到。
咨询虚拟化同事为什么要用 IPV6_V6ONLY, 回答是虚拟迁移时,服务端如果发现端口被占用后,他们加上的这个标志重试。但如果加上了,后面用ipv4去连接肯定失败,就引发虚机迁移的问题了。
建议修改:发现端口占用时,更换新的端口去listen,不要加 IPV6_V6ONLY 标志(因为后续是用ipv4连接的)

IPV6_V6ONLY参数决定了一个 IPv6 套接字是否仅能处理 IPv6 流量,还是也能同时处理 IPv4 流量。当一个套接字被创建为 IPv6 套接字时,这个参数就会影响该套接字与 IPv4 地址之间的交互能力。

问题结果

建议虚机迁移逻辑修改:发现端口占用时,更换新的端口去listen,不要加 IPV6_V6ONLY 标志(因为后续是用ipv4连接的)

0条评论
0 / 1000
c****9
3文章数
0粉丝数
c****9
3 文章 | 0 粉丝
c****9
3文章数
0粉丝数
c****9
3 文章 | 0 粉丝
原创

虚机热迁移mutlfd概率失败原因排查

2025-06-23 10:43:26
2
0

问题背景

虚机做热迁移mutlfd方式概率失败。失败的时候观测到源端无法连接到目的端,导致qemu内存数据无法传输。以下是问题发生时的现象:
79作为server端:

[root@cn-nm-region1-az3-compute-s7-10e8e105e79 ~]# netstat -antp | grep 49152
tcp6       3      0 :::49152                :::*                    LISTEN      709105/qemu-system-
tcp6       7      0 ::1:49152               ::1:50112               CLOSE_WAIT  -
tcp6      12      0 ::1:49152               ::1:49586               CLOSE_WAIT  -
tcp6       3      0 ::1:49152               ::1:39048               CLOSE_WAIT  709105/qemu-system-
tcp6       1      0 ::1:49152               ::1:45358               CLOSE_WAIT  -

80上连接79的49152端口,一直被拒绝。

[root@cn-nm-region1-az3-compute-s7-10e8e105e80 ~]# telnet 10.8.105.79 49152
Trying 10.8.105.79...
telnet: connect to address 10.8.105.79: Connection refused

问题排查

使用tcpdump在80(发送端)上抓包:
image.png

可见79(接收端)发送了RST直接中断了连接。
在79上采用bpftrace跟踪服务端处理流程:

bpftrace -e '
kprobe:tcp_v4_send_reset {
	printf("%s\\n\", kstack());
}
'

服务端处理流程如下:

tcp_v4_rcv
	...
	lookup:
	sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
			       th->dest, sdif, &refcounted);
	if (!sk)
		goto no_tcp_socket;

	// 正常流程会到 tcp_v4_do_rcv

	// 如果前面找不到sock
	no_tcp_socket:
	if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
		goto discard_it;

	tcp_v4_fill_cb(skb, iph, th);

	if (tcp_checksum_complete(skb)) {
csum_error:
		__TCP_INC_STATS(net, TCP_MIB_CSUMERRORS);
bad_packet:
		__TCP_INC_STATS(net, TCP_MIB_INERRS);
	} else {
		tcp_v4_send_reset(NULL, skb);	// 发送RST
	}

从上图可知,__inet_lookup_skb 查找不到监听的sock。继续跟踪 __inet_lookup_skb 流程:

tcp_v4_rcv
	__inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source, th->dest, sdif, &refcounted);
		__inet_lookup
			// 先在established的表里查找
			__inet_lookup_established		// hashinfo->ehash
			// 	established的表里查找不到,再到listener表里查找
			__inet_lookup_listener			// hashinfo->listening_hash

在hash表里找不到sock, 因此发送RST报文拒绝连接。

考虑到监听是ipv6的,怀疑不能通过ipv4连接,编写如下代码进行验证:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 61889
#define BACKLOG 5

void error_handling(const char *message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

int main() {
    int server_sock, client_sock;
    struct sockaddr_in6 server_addr, client_addr;
    socklen_t client_addr_size;

    // 创建 IPv6 套接字
    server_sock = socket(AF_INET6, SOCK_STREAM, 0);
    if (server_sock == -1) {
        error_handling("socket() error");
    }

    // 初始化服务器地址结构体
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin6_family = AF_INET6;
    server_addr.sin6_addr = in6addr_any;
    server_addr.sin6_port = htons(PORT);

    // 绑定套接字到指定地址和端口
    if (bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        error_handling("bind() error");
    }

    // 开始监听连接请求
    if (listen(server_sock, BACKLOG) == -1) {
        error_handling("listen() error");
    }

    client_addr_size = sizeof(client_addr);

    printf("Waiting for connections on port %d (IPv6)...\n", PORT);

    while (1) {
        // 接受新的连接请求
        client_sock = accept(server_sock, (struct sockaddr *)&client_addr, &client_addr_size);
        if (client_sock == -1) {
            error_handling("accept() error");
        }

        char client_ip[INET6_ADDRSTRLEN];
        // 将客户端的 IPv6 地址转换为字符串形式
        inet_ntop(AF_INET6, &client_addr.sin6_addr, client_ip, INET6_ADDRSTRLEN);
        // 打印客户端连接信息
        printf("New connection from %s:%d\n", client_ip, ntohs(client_addr.sin6_port));

        // 这里可以添加处理客户端请求的代码
        // 例如读取客户端发送的数据、发送响应等

        // 关闭客户端套接字
        close(client_sock);
    }

    // 关闭服务器套接字
    close(server_sock);
    return 0;
}

发现是可以连接的
image.png

[root@cn-nm-region1-az3-compute-s7-10e8e105e80 ~]# telnet 10.8.105.79 61889
Trying 10.8.105.79...
Connected to 10.8.105.79.
Escape character is '^]'.
Connection closed by foreign host.

和虚拟化同事沟通,得知连不上的时候,他们添加了IPV6_V6ONLY标志:

image.png

于是修改上述服务端代码,在bind前添加setsockopt IPV6_V6ONLY:
image.png

成功复现了问题。
image.png

[root@cn-nm-region1-az3-compute-s7-10e8e105e80 ~]# telnet 10.8.105.79 61889
Trying 10.8.105.79...
telnet: connect to address 10.8.105.79: Connection refused

从上面可看出,服务端监听的时候使用 IPV6_V6ONLY,客户端用ipv4的协议去连接是不行的。因为在listener表里找不到。
咨询虚拟化同事为什么要用 IPV6_V6ONLY, 回答是虚拟迁移时,服务端如果发现端口被占用后,他们加上的这个标志重试。但如果加上了,后面用ipv4去连接肯定失败,就引发虚机迁移的问题了。
建议修改:发现端口占用时,更换新的端口去listen,不要加 IPV6_V6ONLY 标志(因为后续是用ipv4连接的)

IPV6_V6ONLY参数决定了一个 IPv6 套接字是否仅能处理 IPv6 流量,还是也能同时处理 IPv4 流量。当一个套接字被创建为 IPv6 套接字时,这个参数就会影响该套接字与 IPv4 地址之间的交互能力。

问题结果

建议虚机迁移逻辑修改:发现端口占用时,更换新的端口去listen,不要加 IPV6_V6ONLY 标志(因为后续是用ipv4连接的)

文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0