Listening TCP server ignores SYN or ACK for new connection handshake

Solution Verified - Updated

Environment

  • Red Hat Enterprise Linux
  • TCP listening process

Issue

  • Listening server ignores SYN for new TCP handshake
  • Listening TCP server ignores ACK for new connection
  • RHEL 5 and RHEL 6: The following stats are seen with the same value in netstat -s:
TcpExt:
    XXX times the listen queue of a socket overflowed
    XXX SYNs to LISTEN sockets ignored
  • or nstat -a:
TcpExtListenOverflows
TcpExtListenDrops
  • RHEL 7: The value of times the listen queue of a socket overflowed (TcpExtListenOverflows) may be larger than the value of SYNs to LISTEN sockets ignored (TcpExtListenDrops)

Resolution

Improve application accept() performance.

Increase the kernel maximum backlog (net.core.somaxconn) and application socket backlog as described in this article. Note that increasing this queue size will only help in the case of transient overloads. For continuous overload, higher connection latency will result, and connection requests will still be ignored.

Root Cause

Each listening TCP socket has a "listen backlog" which is the amount of un-handshaken connections which can sit waiting for the kernel to perform the TCP handshake.

The listen backlog is limited by the net.core.somaxconn kernel tunable, and the second parameter of the listen(int sockfd, int backlog) system call.

Each socket also has an "accept backlog" which is the amount of handshaken connections which can sit waiting for the application to call accept() and move the connection to a new socket file descriptor. If the application is busy and has not yet called accept() when the handshake completes then the connection may be queued if this backlog has not been reached.

There is no mechanism to set the size of the accept backlog, it is set to the same value as the listen backlog.

When a new connection request (i.e. a new SYN) comes in, the kernel checks to see if the accept backlog is full. If so, the new SYN is silently discarded.

On RHEL 5 and RHEL 6, the times the listen queue of a socket overflowed (TcpExtListenOverflows) and SYNs to LISTEN sockets ignored (TcpExtListenDrops) values are grown when the accept backlog overflows, but no counter is grown when a new SYN arrives and is silently discarded.

On RHEL 7, code review shows the times the listen queue of a socket overflowed (TcpExtListenOverflows) counter is grown when a SYN is discarded due to full accept backlog, so it could be expected for this counter to be higher on RHEL 7.

Diagnostic Steps

During this issue, inspect the socket backlog with ss -ntl.

The Send-Q column shows the socket backlog

The Recv-Q column shows the current number of connections in the backlog

If the Recv-Q value is approaching, equal to , or greater than the Send-Q value, this is confirmed to match the root cause.

The following example shows a process with a listen backlog of 5, and 6 connections waiting to be accepted, so the TCP server accept performance is too poor:

# ss -ntl '( sport = :9001 )'
State      Recv-Q Send-Q Local Address:Port               Peer Address:Port              
LISTEN     6      5           :::9001                    :::*                  

Silent discard of SYN in RHEL 5.11 kernel source:

net/ipv4/ip_input.c

199 static inline int ip_local_deliver_finish(struct sk_buff *skb)
200 {

236                         ret = ipprot->handler(skb);

241                         IP_INC_STATS_BH(IPSTATS_MIB_INDELIVERS);

251                         kfree_skb(skb);
net/ipv4/tcp_ipv4.c

1631 int tcp_v4_rcv(struct sk_buff *skb)
1632 {

1122         if (!sock_owned_by_user(sk)) {

1128                         ret = tcp_v4_do_rcv(sk, skb);

1143         return ret;
1010 int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
1011 {

1023         if (sk->sk_state == TCP_LISTEN) {
1024                 struct sock *nsk = tcp_v4_hnd_req(sk, skb);

1028                 if (nsk != sk) {
1029                         if (tcp_child_process(sk, nsk, skb))
1030                                 goto reset;
1031                         return 0;
650 int tcp_child_process(struct sock *parent, struct sock *child,
651                       struct sk_buff *skb)
652 {

656         if (!sock_owned_by_user(child)) {
657                 ret = tcp_rcv_state_process(child, skb, skb->h.th, skb->len);

672         return ret;
673 }                
net/ipv4/tcp_input.c

4422 int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
4423                           struct tcphdr *th, unsigned len)
4424 {

4442                 if(th->syn) {

4445                         if (icsk->icsk_af_ops->conn_request(sk, skb) < 0)
4446                                 return 1;

4465                         kfree_skb(skb);
4466                         return 0;
4467                 }
net/ipv4/tcp_ipv4.c

 739 int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
 740 {

 772         /* Accept backlog is full. If we have already queued enough
 773          * of warm entries in syn queue, drop request. It is better than
 774          * clogging syn queue with openreqs with exponentially increasing
 775          * timeout.    
 776          */
 777         if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
 778                 goto drop;

 884 drop:
 885         return 0;
 886 }
Components

This solution is part of Red Hat’s fast-track publication program, providing a huge library of solutions that Red Hat engineers have created while supporting our customers. To give you the knowledge you need the instant it becomes available, these articles may be presented in a raw and unedited form.