天天看点

客户端Connection reset by peer怎么办?——可能只是服务端挂了

cdn线网运营总会遇到有各种各样的奇怪问题,而导致这些问题却对应的各种各样的原因。有些原因查出来却总叫人哭笑不得,比如本案例所说的,服务端的程序挂了导致的connection reset by peer问题。这种最基础的原因却往往最能怀疑到,我个人觉得主要原因有以下两点。

一,定式思维总会让人最先排出掉最接近真相到原因。有点类似高中做题,总容易先入为主,陷入死胡同。

二,没有人对各种原因做归纳总结,不同的原因导致的虽然都是连接失败这一现象,但如果深入研究,一定有独特的特征。需要有人说出“白马”和“黑马”的各自独特性。网上搜索了下connection reset by peer关键字,发现只有应用层原因说明各种原因,并没有这种问题的tcp层的原因介绍。那就我来根据线网运营遇到的案例来分析具体tcp层发生来什么,各种connection reset by peer的原因在tcp层有啥不同的特征来区分。

废话不多扯,直接开始说事。下图是某下载业务在第三方竞速平台的错误点,错误类型是建立连接失败。幸好第三方测试平台自带错误点抓包功能,保留了错误发生的现场。

客户端Connection reset by peer怎么办?——可能只是服务端挂了

从图中可以看到,客户端没发送一个syn包都会回复一个reset包。很直观的感觉三次握手阶段,连接还没建立肯定还没有到应用层,必然跟应用层没有关系。并且如果应用层有问题,必然会导致大面积的连接失败,而第三方测试平台显示只是两个错误点。而我首先怀疑的是前一条连接的time_wait一直存在,导致的新链接被reset,因为我们从入门就有意无意的被灌输“前一条流会影响后一条流的建联,尤其是time_wait状态的连接总是充满着各种神秘”。

首先tcp层发送的reset是分为两种——active_reset和非active_reset, active_reset是调用tcp_send_active_reset()进行发送的,而非active_reset是调用tcp_v4_send_reset()进行发送。那从抓包来看怎么确认是哪种类型的reset呢? 关键是要看Win值是否有设置, tcp_send_active_reset()调用的是tcp_transmit_skb()进行发送的,在tcp_transmit_skb()会计算和设置接收窗口。而tcp_v4_send_reset()函数设置的Win是零。

void tcp_send_active_reset(struct sock *sk, gfp_t priority)

{
    struct sk_buff *skb;

    /* NOTE: No TCP options attached and we never retransmit this. */
    skb = alloc_skb(MAX_TCP_HEADER, priority);
    if (!skb) {
        NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTFAILED);
        return;
    }

    /* Reserve space for headers and prepare control bits. */  
    skb_reserve(skb, MAX_TCP_HEADER);
    tcp_init_nondata_skb(skb, tcp_acceptable_seq(sk),
                            TCPHDR_ACK | TCPHDR_RST);

    /* Send it off. */
    if (tcp_transmit_skb(sk, skb, 0, priority))
        NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTFAILED);

    TCP_INC_STATS(sock_net(sk), TCP_MIB_OUTRSTS);

}
           

wireshark上来看Win的值为0,所以是非active_reset。那查看下调用tcp_v4_send_reset()的位置,只有四处地方会调用。

1,监听套接口不存在

tcp层入口函数tcp_v4_rcv函数中,会调用__inet_lookup_skb找到对应的sock,如果连符合条件的listen状态的sock都没有找到,并且数据包校验和没问题就会发送reset包断开对端。

int tcp_v4_rcv(struct sk_buff *skb)

{
....
    sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);

    if (!sk)
        goto no_tcp_socket;
...
no_tcp_socket:
    if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
        goto discard_it;

    if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) { //校验和出错
csum_error:
        TCP_INC_STATS_BH(net, TCP_MIB_CSUMERRORS);
bad_packet:
        TCP_INC_STATS_BH(net, TCP_MIB_INERRS);
    } else {
        tcp_v4_send_reset(NULL, skb); //发送reset包
    }
}
           

2,四元组复用,但上一条流还未结束

使用四元组在__inet_lookup_skb查询找到一个time_wait的sock,也就是上一条流并没有结束,在tcp_timewait_state_process()调用后返回TCP_TW_RST,认为对端有错误发送reset关闭对端。但该case中客户端一共发送了三次syn包,如果第一次发送reset是有time_wait状态到sock存在,第二次和第三次syn包就不会响应reset,因为发送完第一次reset包就已经在time_wait的sock释放。

int tcp_v4_rcv(struct sk_buff *skb)
{
    ...
do_time_wait:
    ...
        switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {
        case TCP_TW_SYN: {
                struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev),
                                                        &tcp_hashinfo,
                                                        iph->saddr, th->source,
                                                        iph->daddr, th->dest,
                                                        inet_iif(skb));
                if (sk2) {
                        inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
                        inet_twsk_put(inet_twsk(sk));
                        sk = sk2;
                        goto process;
                }
                /* Fall through to ACK */
        }                       
        case TCP_TW_ACK:
                tcp_v4_timewait_ack(sk, skb);
                break;
        case TCP_TW_RST:
                tcp_v4_send_reset(sk, skb);
                inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
                inet_twsk_put(inet_twsk(sk));
                goto discard_it;
        case TCP_TW_SUCCESS:;
        }
        goto discard_it;
}
           

3,第三次握手检查发现设置了syn或者rst标志的包,并且并不是此前重传的syn包

struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
                           struct request_sock *req,
                           struct request_sock **prev,
                           bool fastopen)
{
...
        /* RFC793: "second check the RST bit" and
         *         "fourth, check the SYN bit"
         */
        if (flg & (TCP_FLAG_RST|TCP_FLAG_SYN)) {
                TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_ATTEMPTFAILS);
                goto embryonic_reset;
        }
...
embryonic_reset:
        if (!(flg & TCP_FLAG_RST)) {
                /* Received a bad SYN pkt - for TFO We try not to reset
                 * the local connection unless it's really necessary to
                 * avoid becoming vulnerable to outside attack aiming at
                 * resetting legit local connections.
                 */
                req->rsk_ops->send_reset(sk, skb);
        } else if (fastopen) { /* received a valid RST pkt */
                reqsk_fastopen_remove(sk, req, true);
                tcp_reset(sk);
        }
        if (!fastopen) {
                inet_csk_reqsk_queue_drop(sk, req, prev);
                NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_EMBRYONICRSTS);
        }
        return NULL;
}
           

4,TCP状态机处理

tcp_v4_do_rcv()函数中调用tcp_child_process()和tcp_rcv_state_process()返回值非零时,都会触发发送reset,tcp_child_process()是服务端三次握手完成才会调用,tcp_rcv_state_process传递进去的sk仍为listen状态的sk,所以只剩listen状态下收到ack包,或者含有syn标志,但是调用conn_request()返回值小于零。然而tcp_conn_request只会返回0。

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
...
        if (sk->sk_state == TCP_LISTEN) {
                struct sock *nsk = tcp_v4_hnd_req(sk, skb);
                if (!nsk)
                        goto discard;

                if (nsk != sk) {
                        sock_rps_save_rxhash(nsk, skb);
                        if (tcp_child_process(sk, nsk, skb)) {
                                rsk = nsk;
                                goto reset;
                        }
                        return 0;
                }
        } else
                sock_rps_save_rxhash(sk, skb);

        if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {
                rsk = sk;
                goto reset;
        }
        return 0;

reset:
        tcp_v4_send_reset(rsk, skb);
discard:
...
}
           
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
                          const struct tcphdr *th, unsigned int len)
{
...
        case TCP_LISTEN:
                if (th->ack)
                        return 1;

                if (th->rst)
                        goto discard;

                if (th->syn) {
                        if (th->fin)
                                goto discard;
                        if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)
                                return 1;

                        /* Now we have several options: In theory there is
                         * nothing else in the frame. KA9Q has an option to
                         * send data with the syn, BSD accepts data with the
                         * syn up to the [to be] advertised window and
                         * Solaris 2.1 gives you a protocol error. For now
                         * we just ignore it, that fits the spec precisely
                         * and avoids incompatibilities. It would be nice in
                         * future to drop through and process the data.
                         *
                         * Now that TTCP is starting to be used we ought to
                         * queue this data.
                         * But, this leaves one open to an easy denial of
                         * service attack, and SYN cookies can't defend
                         * against this problem. So, we drop the data
                         * in the interest of security over speed unless
                         * it's still in use.
                         */
                        kfree_skb(skb);
                        return 0;
                }
                goto discard;
...
}
           

上述四种情况,虽然不愿意相信,只剩第一种可能性最大。当查看服务端日志之后发现,该时刻服务端程序确实挂了。只是第三方测试的测试点发起请求的间隔比较大,并没有造成大面积的失败点。总结来说,服务端程序挂了,客户端报错connection reset by peer,在tcp层表现就为重试好几次都发生失败。

继续阅读