TCP重传次数与getsockopt为何无法及时感知连接断开


问题背景

在上次学府中学发现断线重连无法及时知道连接已断开,需要十来分钟的时间。原因是TCP会进行超时重传,而15分钟左右才能停止重传,将断开的TCP连接通知给上层(应用程序)。

而这次是问题背景是:西部的销售总监日常在云场馆使用中控屏幕时候,发现了设备无响应,且有死机问题。那天刚好断网,我初步判断是因为网络的原因,且云场馆的程序没有应用学府的最新网络检测模块。实际排查中发现了更深层次的一个BUG:在中控设备突然断网的时候,对中控设备接着进行体育设施设备操作,此时设备无响应;但是如果恢复网络后,中控设备会与MQTT服务器进行自动重连,此时之前的操作指令会接着发送出去。

这出现了设备会诡异的自动启动,同时如果设备之间有相互干扰的情况下,会出现设备与设备之间的碰撞。

出现这个问题后,我首先关闭了MQTT的持久会话功能,同时发布之前加入getsockopt函数的监测。对于MQTT的心跳包发送进行了重构,使用单独发布一个主题来进行心跳检测。如果发现无法发送心跳包的时候,会在两个心跳包的时间之后进行重连尝试。

为了使操作人员更为直观的感知到目前设备操控的状态,加入了MQTT未连接的提示框。但是这一步出现了问题,无法及时的感知连接断开,需要15分钟左右的时间。这情况是MQTT服务器那边在60秒的心跳时间过去后,会发现客户端(中控设备)已断开。但是客户端无法感知到自己已经断网了!

我的第一反应是getsockopt函数的问题,getsockopt函数的意义是:在TCP中,当一个套接字处于错误状态时,就会设置so_error选型,并将错误码存储在其中。我们可以使用getsockopt函数来获取这个选项的值。通过打印日志发现,在已经断网的情况下,心跳包正常发送。但这是不可能的事情,我去检查了下write函数,发现可以正常使用,没有进行报错。这时候就懵逼了。在断网的情况下,write函数应该是无法正常使用的才对。

这时重新回过头去梳理一下TCP连接,发现依旧是15分钟左右的超时重传机制导致的。那有没有办法能够加快这个时间呢?需要了解到Linux定义了两个参数来限定超时重传的次数。分别是tcp_retries1和tcp_retries2。

以下是源码中Documentation/networking/ip-sysctl.txt文档中的描述

c
tcp_retries1 - INTEGER
    This value influences the time, after which TCP decides, that
    something is wrong due to unacknowledged RTO retransmissions,
    and reports this suspicion to the network layer.
    See tcp_retries2 for more details.

    RFC 1122 recommends at least 3 retransmissions, which is the
    default.

tcp_retries2 - INTEGER
    This value influences the timeout of an alive TCP connection,
    when RTO retransmissions remain unacknowledged.
    Given a value of N, a hypothetical TCP connection following
    exponential backoff with an initial RTO of TCP_RTO_MIN would
    retransmit N times before killing the connection at the (N+1)th RTO.

    The default value of 15 yields a hypothetical timeout of 924.6
    seconds and is a lower bound for the effective timeout.
    TCP will effectively time out at the first RTO which exceeds the
    hypothetical timeout.

    RFC 1122 recommends at least 100 seconds for the timeout,
    which corresponds to a value of at least 8.

重传超过tcp_retries1会怎样

来看一下tcp_retries1相关的代码部分

c
// RTO timer的处理函数是tcp_retransmit_timer(),与tcp_retries1相关的代码调用关系如下  
tcp_retransmit_timer()
    => tcp_write_timeout()  // 判断是否重传了足够的久
        => retransmit_timed_out(sk, sysctl_tcp_retries1, 0, 0)  // 判断是否超过了阈值

// tcp_write_timeout()的具体相关内容  
...
if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
    // 如果超时发生在三次握手期间,此时有专门的tcp_syn_retries来负责限定重传次数
    ...
} else {    // 如果超时发生在数据发送期间
    // 这个函数负责判断重传是否超过阈值,返回真表示超过。后续会详细分析这个函数  
    if (retransmits_timed_out(sk, sysctl_tcp_retries1, 0, 0)) { 
        /* Black hole detection */
        tcp_mtu_probing(icsk, sk);  // 如果开启tcp_mtu_probing(默认关闭)了,则执行PMTU

        dst_negative_advice(sk);    // 更新路由缓存
    }
    ...
}

从以上的代码可以看到,一旦重传超过阈值tcp_retries1,主要的动作就是更新路由缓存。
用以避免由于路由选路变化带来的问题。

重传超过tcp_retries2会怎样

会直接放弃重传,关闭TCP流

c
// 依然还是在tcp_write_timeout()中,retry_until一般是tcp_retries2
...
if (retransmits_timed_out(sk, retry_until, syn_set ? 0 : icsk->icsk_user_timeout, syn_set)) {
    /* Has it gone just too far? */
    tcp_write_err(sk);      // 调用tcp_done关闭TCP流
    return 1;
}

所以我打算去修改tcp_retries2这个参数,它的默认值是15。

修改为5:

c
echo 5 > /proc/sys/net/ipv4/tcp_retries2

现在的超时返回时间为30s左右,极大的缓解了无法及时检测到连接断开的问题。

参考文献

  1. MQTT消息持久化
  2. 深入理解非阻塞TCP连接:getsockopt的关键作用
  3. C++ getsockopt so_error
  4. 如何使用C语言中的getsockopt()函数进行网络互联网服务器设置
  5. C语言的socket网络编程
  6. Linux上TCP的几个内核参数调优
  7. 聊一聊重传次数

文章作者: 冬瓜冬瓜排骨汤
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 冬瓜冬瓜排骨汤 !