引
生产环境的问题,按出现频率,大体可以分为两类:
- 高频问题
低频问题
- 周期性低频问题
无周期随机发生问题
- 单例发生
- 多例发生
一般,遇到 无周期随机发生问题
且 单例发生
的情况,是最难定位问题根源的。
前不久,我就遇到应用对外发起连接 无周期随机发生
且 单例发生
的情况 。你不可能在大流量的情况下,做 tcpdump 去分析连接情况的。而能透视内核状态的 eBPF/BPF 可能是个更合适的选择。
eBPF 可以在以下事件
发生时,抓取到 连接信息
:
- 应用发起
connect()
,连接进入SYN_SENT
状态时 - 在发送
SYN
一定时间后,无收到响应而重传SYN
时 - connect timeout 后,应用
close()
socket。连接从SYN_SENT
状态变为CLOSE
状态时
这里说的连接信息
,包括:
- 连接 4 元组: source_IP: local_port <-> destination_IP:dest_port
- 事发时的连接状态
而这些 事件
+ 连接信息
可以为进一步研判问题的方向,提供一个客观的事实证据。
应用场景例子
之前,我了一篇:特定条件下 Istio 发生 half-close 连接泄漏与出站连接失败 。其中模拟了一个场景:
应用程序出站outbound连接超时因为应用程序选择了一个与-15001outboundlistener-上的现有套接字冲突的临时端口
- App invoke syscall
connect(sockfd, peer_addr)
, kernel allocation aephemeral port
(44410 in this case) , bind the new socket to thatephemeral port
and sentSYN
packet to peer.
SYN
packet reach conntrack and it create atrack entry
inconntrack table
:$ conntrack -L tcp 6 108 SYN_SENT src=172.29.73.7 dst=172.21.206.198 sport=44410 dport=7777 src=127.0.0.1 dst=172.29.73.7 sport=15001 dport=44410
SYN
packet DNAT to127.0.0.1:15001
SYN
packet reach the already existingFIN-WAIT-2 127.0.0.1:15001 172.29.73.7:44410
socket, then sidecar reply aTCP Challenge ACK
(TCP seq-no is from the oldFIN-WAIT-2
) packet to App
大体意思是,如果应用在发起出站连接时,内核自动选择的临时本地端口如果与 “Envoy 在 15001 上的 FIN-WAIT-2
状态的 socket 的对端端口” 相同时,就会有问题。为了提高问题发生机率,我当时用 nc -p $临时本地端口号
的方法指定 临时本地端口号
,而不是让内核自动选择, 模拟端口号冲突的。
但实际应用是让内核自动选择临时本地端口号
的。那么问题来了,如果要证明在问题发生时, 内核自动选择的临时本地端口号
就是我设想的情况?
实时跟踪 TCP 连接失败与重试
我们知道,TCP 连接超时的处理过程,大概会有发下几个事件(函数)点:
应用调用
connect()
。连接进入SYN_SENT
状态- 对应内核的
tcp_connect()
- 对应内核的
在发送
SYN
一定时间后,因无收到响应而间歇重传SYN
- 对应内核的
tcp_retransmit_skb()
- 对应内核的
connect timeout 后,应用
close()
socket。连接从SYN_SENT
状态变为CLOSE
状态时- 对应内核的
inet_sk_state_store()
- 对应内核的
Talk is cheap, show you the code :
#!/usr/local/bin/bpftrace
#include
#include
#include
#include
#include
#include
// trace-poll-timeout.bt file
BEGIN
{
printf("Tracing IP TCP connect latency and SYN retry with stacks. Ctrl-C to end.\n");
@tcp_states[1] = "ESTABLISHED";
@tcp_states[2] = "SYN_SENT";
@tcp_states[3] = "SYN_RECV";
@tcp_states[4] = "FIN_WAIT1";
@tcp_states[5] = "FIN_WAIT2";
@tcp_states[6] = "TIME_WAIT";
@tcp_states[7] = "CLOSE";
@tcp_states[8] = "CLOSE_WAIT";
@tcp_states[9] = "LAST_ACK";
@tcp_states[10] = "LISTEN";
@tcp_states[11] = "CLOSING";
@tcp_states[12] = "NEW_SYN_RECV";
}
kprobe:tcp_connect
{
$sk = (struct sock *)arg0;
$inet = (struct inet_sock *)arg0;
@sk2connectUS[$sk] = nsecs;
}
kprobe:inet_sk_state_store
{
$sk = (struct sock *)arg0;
$inet = (struct inet_sock *)arg0;
$newstate = arg1;
if( $newstate == TCP_CLOSE ) {
if( $sk->__sk_common.skc_state == TCP_SYN_SENT && @sk2connectUS[$sk] ) {
$dport = $sk->__sk_common.skc_dport;
$dport = ($dport >> 8) | (($dport << 8) & 0x00FF00);
$sport = $inet->inet_sport;
$sport = ($sport >> 8) | (($sport << 8) & 0x00FF00);
$sk_family = $sk->__sk_common.skc_family;
time("%H:%M:%S ");
printf("TCP_SYN_SENT to CLOSE: %-6d %-16s %d ms %-3d ",
pid, comm, (nsecs - @sk2connectUS[$sk])/1000000,
$sk_family == AF_INET ? 4 : 6);
printf("%-15s:%-5d --> %-15s:%-5d\n", ntop($sk_family , $inet->inet_saddr), $sport,
ntop($sk_family, $sk->__sk_common.skc_daddr), $dport);
printf("kstack: %s\n", kstack);
printf("ustack: %s\n", ustack);
}
else {
}
delete(@sk2connectUS[$sk]);
}
}
kprobe:tcp_retransmit_skb
{
$sk = (struct sock *)arg0;
$inet_family = $sk->__sk_common.skc_family;
if ($inet_family == AF_INET || $inet_family == AF_INET6)
{
$state = $sk->__sk_common.skc_state;
if( $state == 2/*SYN_SENT*/ ) {
$daddr = ntop(0);
$saddr = ntop(0);
if ($inet_family == AF_INET)
{
$daddr = ntop($sk->__sk_common.skc_daddr);
$saddr = ntop($sk->__sk_common.skc_rcv_saddr);
}
else
{
$daddr = ntop(
$sk->__sk_common.skc_v6_daddr.in6_u.u6_addr8);
$saddr = ntop(
$sk->__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr8);
}
$lport = $sk->__sk_common.skc_num;
$dport = $sk->__sk_common.skc_dport;
// Destination port is big endian, it must be flipped
$dport = ($dport >> 8) | (($dport << 8) & 0x00FF00);
$statestr = @tcp_states[$state];
time("%H:%M:%S ");
printf("%-8d %14s:%-6d %14s:%-6d %6s\n", pid, $saddr, $lport,
$daddr, $dport, $statestr);
}
}
}
END
{
clear(@tcp_states);
}
运行 bpftrace ,执行上面脚本:
bpftrace trace-poll-timeout.bt
向一个不会回复的 ip 发送连接请求。且指定 10s 超时时长:
nc -w 10 1.1.1.146 22
这时,bpftrace 会出现跟踪信息:
$ bpftrace trace-poll-timeout.bt
Attaching 5 probes...
Tracing IP TCP connect latency and SYN retry with stacks. Ctrl-C to end.
21:47:55 0 192.168.1.14:42636 1.1.1.146:22 SYN_SENT
21:47:57 0 192.168.1.14:42636 1.1.1.146:22 SYN_SENT
21:48:01 0 192.168.1.14:42636 1.1.1.146:22 SYN_SENT
21:48:04 TCP_SYN_SENT to CLOSE: 23023 nc 10009ms(尝试连接用时) 4 192.168.1.14:42636(内核自动分配的本地临时端口号) --> 1.1.1.146:22
kstack:
inet_sk_state_store+1
__tcp_close+678
tcp_close+37
inet_release+69
__sock_release+63
sock_close+21
__fput+156
____fput+14
task_work_run+106
exit_to_user_mode_loop+343
exit_to_user_mode_prepare+160
syscall_exit_to_user_mode+39
do_syscall_64+105
entry_SYSCALL_64_after_hwframe+97
ustack:
0x7f1b1c6f8117
0x600000001
^C
就这样,应用对外发起连接 无周期随机发生
且 单例发生
的连接问题,就可以找到证据了。
谢谢阅读!