详解linux中的backlog

8,263 阅读6分钟

什么是backlog

backlog是linux下socket函数之listen的参数,当应用程序调用listen系统调用让一个socket进入LISTEN状态时,需要指定一个backlog参数。这个参数经常被描述为,新连接队列的长度限制。

由于TCP建立连接需要进行3次握手,一个新连接在到达ESTABLISHED状态可以被accept系统调用返回给应用程序前,必须经过一个中间状态SYN RECEIVED。这意味着,TCP/IP协议栈在实现backlog队列时,有两种不同的选择:

  • 仅使用一个队列,队列规模由listen系统调用backlog参数指定。当协议栈收到一个SYN包时,响应SYN/ACK包并且将连接加进该队列。当相应的ACK响应包收到后,连接变为ESTABLISHED状态,可以向应用程序返回。这意味着队列里的连接可以有两种不同的状态:SEND RECEIVED和ESTABLISHED。只有后一种连接才能被accept系统调用返回给应用程序。

  • 使用两个队列——SYN队列(待完成连接队列)和accept队列(已完成连接队列)。状态为SYN RECEIVED的连接进入SYN队列,后续当状态变更为ESTABLISHED时移到accept队列(即收到3次握手中最后一个ACK包)。顾名思义,accept系统调用就只是简单地从accept队列消费新连接。在这种情况下,listen系统调用backlog参数决定accept队列的最大规模。

对于linux操作系统,内核在2.2之后的版本,tcp/ip协议实现了第二种方案,即一个syn队列,一个accept队列,syn队列的长度由系统级别设置,accept队列的长度可以由应用级别设置,tcp建连交互流程如下图所示:

  1. client 端使用 connect() 向 server 端发起连接请求(发送 syn 包),此时 client 端的 TCP 的状态为 SYN_SENT。

  2. server 端在收到 syn 包后,将 TCP 相关信息放到 syn queue(半连接队列)中,同时向 client 发送 syn+ack,server 端 TCP 的状态为 SYN_RCVD。

  3. client 端收到 server 端的 syn+ack 后,向 server 端发送 ack,此时 client 端的 TCP 的状态为 ESTABLISHED。Server 端收到 ack 确认后,从 syn queue 里将 TCP 信息取出,并放到 accept queue(全连接队列)中,此时 server 端的 TCP 的状态为 ESTABLISHED。

经过以上三个过程,client 端和 server 端建立了连接,即所谓三次握手的过程。
由此,我们已经知道了什么是backlog,以及半连接队列、全连接队列的由来。下文当中提到的backlog均表示全连接队列长度。那怎么设置backlog的值呢?


如何设置backlog

backlog的大小有两方面因素决定,一是系统层面,二是应用层面
  1. 系统层面:somaxconn参数,可以通过编辑/proc/sys/net/core/somaxconn的值进行设置

  2. 应用层面:对于netty服务端来说,通过serverbootstrap的option进行设置,即option((ChannelOption.SO_BACKLOG,number),number即为要设置的大小,类型为int

backlog最终的取值为二者中的最小值,即min(backlog,somaxconn),在服务启动之后,我们可以通过,ss -tnlp进行查看,如下图所示:

在listen状态情况下:Recv-Q表示存在于backlog当中未被服务端应用程序accept的队列大小;Send-Q表示最大的backlog的大小,即我们设置的backlog的大小min(backlog,somaxconn)。那当设置backlog时应参考怎么的设置标准呢?

backlog设置标准

在设置backlog时,既不能太大,也不能太小,设置太大,当访问流量突然增加超过服务器的负载时,客户端不能快速失败,造成读取连接超时,对服务端来讲,会影响网络I/O,同时造成内存使用过大,cpu负载增加;如果设置太小,不能充分发挥服务端的负载能力,并且会客户端造成连接失败的情况。应根服务端能够承受的最大qps进行设置,backlog的大小,应设置成在服务端最大能够承受qps的1-1.5倍左右。


backlog使用分析

对于backlog队列的使用情况,我们可以通过netstat进行查询,如下图所示:

如图所示,我们可以看到当前backlog已使用的队列长度为1。由以上分析我们可以知道,处于backlog队列中的连接,还未被应用线程accept。也就是说我们可以通过netstat查询到当前连接,但该连接并未关联进程pid。分析图中所有连接,我们可以知道,端口号为34328未关联pid,代表此连接处于全连接队列当中。
当然我们也可以通过ss命令进行查询当前监听端口号的backlog使用情况,如下图所示:

如图所示,当前监听端口全连接队列长度为2,当全连接队列的使用长度为(backlog+1)时,代表次全连接队列已满,那当全连接队列已满的情况下,当又有新建连接请求进来时会怎么处理呢?
当再有新的建立连接的请求是,会根据服务端的/proc/sys/net/ipv4/tcp_abort_on_overflow设置而进行不同的处理。
  • 当tcp_abort_on_overflow=0,直接丢弃该ACK,通过tcpdump抓包,可以看到如下的交互流程

此时服务端处于【syn_rcvd】的状态,客户端处于【established】的状态:

  • 当tcp_abort_on_overflow=1,发送RST通知client,client会报connection reset by peer

抓包查看其流程如下所示:

当全连接队列溢出时,有哪些指标可以说明呢,我们又有那可有效的查询手段呢?我们可以通过netstat -s命令进行查询:

如图所示,4879 times表示全连接队列溢出的次数,隔几秒查询一次,如果这个数字一直在递增,说明全连接队列出现了溢出的状态。
我们又怎么确定到底是哪个监听端口出现了全连接队列溢出的情况呢?
  1. 通过ss -tnlp 查询监听端口全连接队列的使用情况。

  2. 通过netstat查询出连接状态处于established、但未关联进程号的连接,此连接对应着源端口以及目标端口,此连接的源端口就是我们应用程序监听的端口,即出现backlog队列溢出的端口号。

总之,对于我们日常工作当中,无论是长连接,短连接,以及各种的消息中间件,只要牵涉到监听端口,都会牵涉到backlog,当我们了解backlog之后,对我们处理网络问题,已经如何提交系统的高并发将大有裨益。


参考文献

http://veithen.io/2014/01/01/how-tcp-backlog-works-in-linux.html
https://www.jianshu.com/p/7fde92785056

作者简介

李双全,民生科技有限公司用户体验技术部Firefly移动金融开发平台Java开发工程师。