问题描述
网络故障时 TCP 重传会持续非常非常久(重传定时器从 200ms 开始,不断翻倍直至 120s),此时虽然 socket 无法进行数据传输,但 socket 并不会感知到任何异常而必须依赖应用层的心跳机制主动关闭 socket,在某些场景下,我们很忌讳这个 120s 的定时器,可能会导致网络恢复正常之后 socket 之间的通信仍然中断 120s。
解决方案
TCP 重传定时器的超时时间是一个公比为 2 的等比数列,最大超时时间为 TCP_RTO_MAX
(120s),握手阶段的 tcp 重传处理与后两个阶段的处理有所差异。
参考如下代码(linux/net/ipv4/tcp_timer.c):
/* This function calculates a "timeout" which is equivalent to the timeout of a
* TCP connection after "boundary" unsuccessful, exponentially backed-off
* retransmissions with an initial RTO of TCP_RTO_MIN or TCP_TIMEOUT_INIT if
* syn_set flag is set.
*/
static bool retransmits_timed_out(struct sock *sk,
unsigned int boundary,
unsigned int timeout,
bool syn_set)
{
unsigned int linear_backoff_thresh, start_ts;
unsigned int rto_base = syn_set ? TCP_TIMEOUT_INIT : TCP_RTO_MIN;
if (!inet_csk(sk)->icsk_retransmits)
return false;
start_ts = tcp_sk(sk)->retrans_stamp;
if (unlikely(!start_ts))
start_ts = tcp_skb_timestamp(tcp_write_queue_head(sk));
if (likely(timeout == 0)) {
linear_backoff_thresh = ilog2(TCP_RTO_MAX/rto_base);
if (boundary <= linear_backoff_thresh)
timeout = ((2 << boundary) - 1) * rto_base;
else
timeout = ((2 << linear_backoff_thresh) - 1) * rto_base +
(boundary - linear_backoff_thresh) * TCP_RTO_MAX;
}
return (tcp_time_stamp - start_ts) >= timeout;
}
注意该函数计算重传多次之后的累计超时时间(而不是指单次的重传超时时间),如果超过阈值,此时 TCP 必须断链(即向 socket 层报错)。
入参 boundary
指定最大重传次数,timeout
指定绝对超时时间,syn_set
指定当前是否处于握手阶段(即 sk->sk_state
处于 TCPF_SYN_SENT
/ TCPF_SYN_RECV
阶段)。
握手阶段
重传定时器超时基准时间(rto_base
)为 TCP_TIMEOUT_INIT
(1s),最大超时时间为 TCP_RTO_MAX
(120s)。
boundary = icsk->icsk_syn_retries ? : sysctl_tcp_syn_retries;
timeout = 0;
syn_set = true;
其中 icsk->icsk_syn_retries
由 socket 选项 TCP_SYNCNT
控制,默认为 0;
sysctl_tcp_syn_retries
由系统参数 net.ipv4.tcp_syn_retries
控制,默认为 TCP_SYN_RETRIES
(6) 次。
在默认参数情况下,由于第 TCP_SYN_RETRIES
次重传的超时时间 1s * 2**TCP_SYN_RETRIES = 64s
仍然小于 TCP_RTO_MAX
(120),因此根据等比数列求和公式:
1s * (1 + 2**1 + 2**2 + .. + 2**6) = 1s * (2**7 - 1) = 127s
得到总的超时时间为 127s。
综上,要实现这个阶段(即客户端 connect)的超时时间可控,可以设置 socket 选项 TCP_SYNCNT
或者系统参数 net.ipv4.tcp_syn_retries
。
非握手阶段
结束握手阶段之后,重传定时器的基准时间在如下函数调用中设置(注意区分服务端和客户端):
net/ipv4/tcp_input.c
tcp_rcv_state_process
case TCP_SYN_SENT:
tcp_rcv_synsent_state_process
tcp_finish_connect
tcp_set_state(sk, TCP_ESTABLISHED)
tcp_init_metrics
case TCP_SYN_RECV:
tcp_set_state(sk, TCP_ESTABLISHED)
tcp_synack_rtt_meas
tcp_ack_update_rtt
tcp_set_rto
tcp_init_metrics
重传定时器超时基准时间(rto_base
)为 TCP_RTO_MIN
(0.2s),最大超时时间为 TCP_RTO_MAX
(120s)。
boundary = sysctl_tcp_retries2;
timeout = icsk->icsk_user_timeout;
syn_set = false;
其中 sysctl_tcp_retries2
由系统参数 net.ipv4.tcp_retries2
控制,默认为 TCP_RETR2
(15) 次;icsk->icsk_user_timeout
由 socket 选项 TCP_USER_TIMEOUT
控制,默认为 0。
在默认参数情况下,由于第 10 次重传的超时时间 0.2s * 2**10 = 204.8s
已大于 TCP_RTO_MAX
(120),因此只有前 9 次重传能使用等比数列求和公式:
0.2s * (1 + 2**1 + 2**2 + .. + 2**9) = 0.2s * (2**10 - 1) = 204.6s
再加上后 (TCP_RETR2 - 9) = (15 - 9) = 6
次的超时时间:
120s * 6 = 720s
得到总的超时时间为 204.6s + 720s = 924.6s
。
注意如果直接根据代码进行计算会得到超时时间为 1009.4s 的结果,实际测试发现第 1 次和 第 2 次重传的超时时间均为 0.2s,从第 3 次开始才倍增,重传次数会比 sysctl_tcp_retries2
控制的数目 + 1,总的超时时间控制在 928s 左右。
当设置系统参数 net.ipv4.tcp_retries2
为 11 时:
~# sysctl net.ipv4.tcp_retries2=11
重传次数为 11 + 1
= 12 次,总的超时时间为 446s 左右,与 retransmits_timed_out
的计算结果 529.4s 仍然差异比较大,但与理论值保持接近:
0.2s * (1 + 2**1 + 2**2 + .. + 2**9) + 0.2s + 120s * (11 - 9) = 0.2s * (2**10 - 1) + 0.2s + 240s = 444.8s
综上,要实现这个阶段(即客户端 connect)的超时时间可控,可以设置 socket 选项 TCP_USER_TIMEOUT
或者系统参数 net.ipv4.tcp_retries2
,如果设置 TCP_USER_TIMEOUT
,则总的超时时间完全由该参数控制,而不再受系统参数 net.ipv4.tcp_retries2
的控制。
注意
- 对于 TCP 选项而言,
net.ipv4.tcp_*
选项同样适用于 IPv6[2]; - 丢弃 SYN 报文的 iptables 规则如下:
~# iptables -A INPUT -p tcp -m tcp --sport 8181 --tcp-flags SYN SYN -j DROP
- 注意 socket 选项
TCP_USER_TIMEOUT
与SO_KEEPALIVE
的差异,TCP_USER_TIMEOUT
控制重传超时时关闭 TCP 连接,而SO_KEEPALIVE
控制闲时 TCP 链路的保活; - 使用 ss 而不是 netstat 可以查看 TCP 定时器相关的信息,以及增加过滤条件。
参考资料
[1] TCP Keepalive is a lie
http://codearcana.com/posts/2015/08/28/tcp-keepalive-is-a-lie.html
[2] Linux tcp settings for ipv6
https://stackoverflow.com/questions/22157566/linux-tcp-settings-for-ipv6
[3] setsockopt, SO_KEEPALIVE and Heartbeats
https://holmeshe.me/network-essentials-setsockopt-SO_KEEPALIVE/
[4] Improving HA Failures with TCP Timeouts
http://greenstack.die.upm.es/2015/03/02/improving-ha-failures-with-tcp-timeouts/
[5] TCP_USER_TIMEOUT does not work when connection is stalled on zero-window probes
https://bugzilla.redhat.com/show_bug.cgi?id=1189241
[6] When TCP sockets refuse to die
https://blog.cloudflare.com/when-tcp-sockets-refuse-to-die/
[7] SYN packet handling in the wild
https://blog.cloudflare.com/syn-packet-handling-in-the-wild/
[8] This is strictly a violation of the TCP specification
https://blog.cloudflare.com/this-is-strictly-a-violation-of-the-tcp-specification/
[9] The story of one latency spike
https://blog.cloudflare.com/the-story-of-one-latency-spike/
附件
其中 drop-data4.pcap 是设置系统参数 net.ipv4.tcp_retries2
为 11 的抓包结果,其它附件都为使用系统默认参数的抓包结果。
最后修改于 2019-03-05