问题背景
虚机做热迁移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(发送端)上抓包:
可见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;
}
发现是可以连接的
[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标志:
于是修改上述服务端代码,在bind前添加setsockopt IPV6_V6ONLY:
成功复现了问题。
[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连接的)