一、前言
这两天看tomcat,查阅 tomcat 怎么承载高并发时,看到了backlog参数。我们知道,服务器端一般使用mq来减轻高并发下的洪峰冲击,将暂时不能处理的请求放入队列,后续再慢慢处理。其实操作系统已经帮我们做了一些类似的东西了,这个东西就是backlog。服务端一般通过 accept 调用,去获取socket。但是假设我们的程序处理不过来(比如因为程序bug,或者设计问题,没能及时地去调用 accept),那么此时的网络请求难道就直接丢掉吗?
当然不会!这时候,操作系统会帮我们放入 accept 队列,先暂存起来。等我们的程序缓过来了,直接调用 accept 去 队列取就行了,这就达到了类似mq的效果。
而 backlog,和另一个参数 /proc/sys/net/core/somaxconn 一起,决定了队列的容量,算法为:min(/proc/sys/net/core/somaxconn, backlog) 。
文章比较长,如果只需要结论,看第三章的总结即可,有时间的话,可以仔细看看正文、第四章的验证部分。 如果只想知道怎么设置这个值,直接跳到最后即可。
下面这篇文章,基础原理讲得很不错。但是是外国人写的,我这里简(tong)单(ku)翻译一下,我也会结合自己的理解,做一些补充。原文链接:http://veithen.io/2014/01/01/how-tcp-backlog-works-in-linux.html
正文之前,查了下linux中的说明。在linux下,执行 man listen,可以看到:
int listen(int sockfd, int backlog);
DESCRIPTION
listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2).The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow. If a connection request arrives when the queue is full, the client may receive an error with an indication of
ECONNREFUSED or, if the underlying protocol supports retransmission, the request may be ignored so that a later reattempt at connection succeeds.
The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for completely established sockets waiting to be accepted, instead of the number of incomplete connection
requests. The maximum length of the queue for incomplete sockets can be set using /proc/sys/net/ipv4/tcp_max_syn_backlog. When syncookies are enabled there is no logical maximum length and this setting is ignored. See
tcp(7) for more information.
我们着重看上面红色部分,“backlog 的意义从linux 2.2开始,发生了变化。现在,这个参数指定了已完成三次握手的 accept 队列的长度,而不是半连接队列的长度。半连接队列的长度可以通过 /proc/sys/net/ipv4/tcp_max_syn_backlog来设置”
所以,下次,如果面试官问你这个参数的意思是什么,那基本上答上面这句就没问题了。
但我们还是继续拓展下。下面我用渣英语翻译一下,权当锻炼了。
二、翻译正文
1、两种实现方式
当一个程序要进行监听时,需要调用listen函数,此时,需要制定backlog参数。该参数,通常指定socket连接队列的长度。
因为tcp连接的建立需要三次握手,因此,站在服务端的角度,一个到来的连接在变成established之前,需要经过一个中间状态SYN RECEIVED;
进入established状态后,此时如果服务端调用accept操作,即可返回该socket。这意味着,tcp/ip协议栈要实现backlog队列,有两种选择:
1、使用一个单独的队列,队列的长度由 listen 调用的 backlog 参数决定。当收到一个 syn 包时,给客户端返回 SYN/ACK,并将此链接加入到队列。
当对应的 ACK 到达后, 连接状态改变为 ESTABLISHED,然后即可移交给应用程序处理。 这意味着,队列可以包含两种状态的连接: SYN RECEIVED 和 ESTABLISHED。
只有处于 ESTABLISHED 状态的连接,才能返回给应用程序发起的 accept 调用。
2、使用两个队列,一个 SYN 队列(或者叫做 半连接队列) 和一个 accept 队列(或者叫做 完全连接队列)。 处于 SYN RECEIVED 状态的连接将被加入到 SYN 队列,后续当
状态变为 ESTABLISHED 状态时(比如三次握手中的最后一次 ACK 到达时),被移到 accept 队列。 就像 accept函数的名字所表示的那样, 实现 accept 调用时,只需要简单地从
accept 队列中获取连接即可。 在这种实现方式下, backlog 参数决定了 accept 队列的长度。
2、BSD 的选择
历史上, BSD 系统的 TCP 实现,使用第一种方式。 这种方式下,当 队列达到 backlog 指定的最大值时, 系统将不再给客户端发来的 SYN 返回 SYN/ACK 。 通常, TCP 实现会简单地丢弃 SYN 包(甚至不会返回 RST 包),因此客户端会触发重试。 这个在 W. Richard Stevens 老爷子的 TCP/IP 卷三种的14.5节有讲。值得注意的是, Stevens 老爷子解释了, BSD 实际上确实用了两个单独的队列, 但是它们表现的和一个单独的,具有backlog参数指定的长度的队列没什么差别。比如,BSD 逻辑上表现得和下面的表述一致:
队列的大小是半连接队列的长度 和 全连接队列的长度之和。(意思是 sum = 半连接队列长度 + 全连接队列长度)
3、Linux 的选择
在linux 上,事情不太一样,在 listen 调用的 man page 上(就是咱们前言那一段):
The behavior of the
backlog
argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length forcompletely established sockets waiting to be accepted,instead of the number of incomplete connection requests. The maximum length of the queue for incomplete sockets can be set using
/proc/sys/net/ipv4/tcp_max_syn_backlog
.
这意味着, Linux非要对着干,选了第二种方案: 一个 SYN 队列, 大小由 系统级别的参数指定 ; 一个 accept 队列, 大小由应用程序指定。
下面图的意思是,服务端收到 SYN 后,会把该socket 放入 syns queue ,当该 socket 的 ack到来时, 服务端将其从 syns queue取出来,移到 accept queue 中。应用程序调用 accept 时,其实就是去 accept 队列取。
4、linux实现中, accept 队列满了怎么办
有个问题是, 如果 accept 队列满了, 一个连接又需要从 SYN 队列移到 accept 队列时(比如收到了三次握手中的第三个包,客户端发来的 ack),linux 下的该种实现会如何表现呢?
这种场景下的代码处理在 net/ipv4/tcp_minisocks.c 中的
tcp_check_req
函数:
child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);
if (child == NULL)
goto listen_overflow;
对于 ipv4, 代码中第一行会最终调用net/ipv4/tcp_ipv4.c 中的 tcp_v4_syn_recv_sock:
ctcp_v4_syn_recv_sock的方法实现: if (sk_acceptq_is_full(sk))
goto exit_overflow;
这里,我们看到有对accept 队列的检测。 exit_overflow 后的代码,会进行一些清理工作, 更新 /proc/net/netstat中的 ListenOverflows 和 ListenDrops 统计信息 。 这会触发 tcp_check_req 中 listen_overflow的执行:
## 看起来像我们的监听者模式。。。
listen_overflow:
if (!sysctl_tcp_abort_on_overflow) {
inet_rsk(req)->acked = ;
return NULL;
}
这个什么意思呢? 意思是,除非 /proc/sys/net/ipv4/tcp_abort_on_overflow 设为 1 ,(这种情况下,会发送 RST 包),否则就什么都不做。
(emmmm 。。。。。。有点偷懒?)
总结一下, 如果 linux 下的tcp实现,在 accept 队列满的情况下,收到了 三次握手中的最后一次 ack 包, 它就直接无视这个包。 一开始,看起来有点奇怪,但是记得, SYN RECEIVED 状态下的 socket 有一个定时器。
该定时器的机制: 如果 ack 包没收到(或者被无视,就像我们上面描述的这个情况), tcp 协议栈 会重发 SYN/ACK 包。(重发次数由 /proc/sys/net/ipv4/tcp_synack_retries 指定)
译者这里补充下:
答案: 若 /proc/sys/net/ipv4/tcp_abort_on_overflow = 0,服务端直接忽略该ack,因为服务端一直处于 SYN RECEIVED,触发了定时器,该定时器会重传 SYN/ACK 给客户端,(不超过 /proc/sys/net/ipv4/tcp_synack_retries 指定的次数 );
如果 /proc/sys/net/ipv4/tcp_abort_on_overflow = 1, 则服务端会直接返回 RST,而不会重传 SYN/ACK。
通过下面的网络跟踪包(一个客户端试图连接到一个服务端的,队列已达到最大 backlog 值的监听 socket),我们看看会是神马情况:
0.000 127.0.0.1 -> 127.0.0.1 TCP > [SYN] Seq= Len=
0.000 127.0.0.1 -> 127.0.0.1 TCP > [SYN, ACK] Seq= Ack= Len=
0.000 127.0.0.1 -> 127.0.0.1 TCP > [ACK] Seq= Ack= Len=
0.000 127.0.0.1 -> 127.0.0.1 TCP > [PSH, ACK] Seq= Ack= Len=
0.207 127.0.0.1 -> 127.0.0.1 TCP [TCP Retransmission] > [PSH, ACK] Seq= Ack= Len=
0.623 127.0.0.1 -> 127.0.0.1 TCP [TCP Retransmission] > [PSH, ACK] Seq= Ack= Len=
1.199 127.0.0.1 -> 127.0.0.1 TCP > [SYN, ACK] Seq= Ack= Len=
1.199 127.0.0.1 -> 127.0.0.1 TCP [TCP Dup ACK #] > [ACK] Seq= Ack= Len=
1.455 127.0.0.1 -> 127.0.0.1 TCP [TCP Retransmission] > [PSH, ACK] Seq= Ack= Len=
3.123 127.0.0.1 -> 127.0.0.1 TCP [TCP Retransmission] > [PSH, ACK] Seq= Ack= Len=
3.399 127.0.0.1 -> 127.0.0.1 TCP > [SYN, ACK] Seq= Ack= Len=
3.399 127.0.0.1 -> 127.0.0.1 TCP [TCP Dup ACK #] > [ACK] Seq= Ack= Len=
6.459 127.0.0.1 -> 127.0.0.1 TCP [TCP Retransmission] > [PSH, ACK] Seq= Ack= Len=
7.599 127.0.0.1 -> 127.0.0.1 TCP > [SYN, ACK] Seq= Ack= Len=
7.599 127.0.0.1 -> 127.0.0.1 TCP [TCP Dup ACK #] > [ACK] Seq= Ack= Len=
13.131 127.0.0.1 -> 127.0.0.1 TCP [TCP Retransmission] > [PSH, ACK] Seq= Ack= Len=
15.599 127.0.0.1 -> 127.0.0.1 TCP > [SYN, ACK] Seq= Ack= Len=
15.599 127.0.0.1 -> 127.0.0.1 TCP [TCP Dup ACK #] > [ACK] Seq= Ack= Len=
26.491 127.0.0.1 -> 127.0.0.1 TCP [TCP Retransmission] > [PSH, ACK] Seq= Ack= Len=
31.599 127.0.0.1 -> 127.0.0.1 TCP > [SYN, ACK] Seq= Ack= Len=
31.599 127.0.0.1 -> 127.0.0.1 TCP [TCP Dup ACK #] > [ACK] Seq= Ack= Len=
53.179 127.0.0.1 -> 127.0.0.1 TCP [TCP Retransmission] > [PSH, ACK] Seq= Ack= Len=
106.491 127.0.0.1 -> 127.0.0.1 TCP [TCP Retransmission] > [PSH, ACK] Seq= Ack= Len=
106.491 127.0.0.1 -> 127.0.0.1 TCP 54 9999 > 53302 [RST] Seq=1 Len=0
由于 客户端的 tcp 协议栈收到了多个 SYN/ACK 包, 因此,它假设 ACK 包丢失了,于是进行重发。(可以看上面的 带有 TCP dup ACK 的行)。
如果服务端监听socket 的 backlog 值降低了 (比如,从 accept 队列消费了一个连接,因此队列变成未满),而且, SYN/ACK 重试次数没有达到最大值的情况下,那么, tcp 协议栈就可以最终处理 客户端发来的 ack 包, 将连接状态从 SYN RECEIVED 改为
ESTABLISHED, 并将其加入到 accept 队列中。 否则, 客户端最终将会拿到一个 RST 包。(上图标红那行)
5、问题延伸
上面的网络抓包,也展示出另一个有趣的方面。 从客户端的角度来说, 收到 服务端发来的 SYN/ACK 后,一直就处于 ESTABLISHED 状态。 如果它发生数据 (不等待服务端发来的数据,毕竟是全双工), 那么数据同样将会重传。 TCP 慢开始算法,会限制发出的包的数量。 (这里意思是, 慢开始算法下,一开始不会传很多包,可能只传一个,收到服务端的响应后,下一次传2个,再一次传4个,这样指数级增长,直到达到一个值后,进入线性增长阶段,因为服务端一直没响应,就不会增大发送的包的个数,避免浪费网络流量)
另一方面, 如果客户端一直等待服务端发送数据,但是服务端的 backlog 一直没有降低(一直没能 accept 该客户端), 那么最终结果是, 客户端该连接为 ESTABLISHED 状态,在服务端,该连接状态为 CLOSED。
还有一个我们没讨论的问题。 listen 的 man page 上说,每个 SYN 包将会添加到 SYN 队列(除非队列满了)。 这个不完全准确。理由是,在 net/ipv4/tcp_ipv4.c 中的 tcp_v4_conn_request 函数中 (该函数负责 SYN 包的处理):
/* Accept backlog is full. If we have already queued enough
* of warm entries in syn queue, drop request. It is better than
* clogging syn queue with openreqs with exponentially increasing
* timeout.
*/
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) {
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}
这个意味着,如果 accept 队列满了, 那么内核会隐式限制 SYN 包接收的速度。 如果收到了太多的 SYN 包, 部分会被丢弃。 在这种情况下, 由客户端决定 进行重发,然后我们最终表现就和在 BSD 下的实现一样。
总结下,为什么linux的设计,会比传统的BSD下的实现更为优越。 Stevens老爷子做了如下的观点(这个翻译太崩溃了。。。深奥。。。智商不行了。。。):
队列长度将会达到backlog 限定值,如果全连接队列满了的话(比如,服务器太忙,以至于进程无法足够快地调用 accept 进行处理,好方便从 accept 队列中腾出位置);或者,在半连接队列满了时,队列长度也会达到 backlog。 后者就是http服务器面临的问题,当客户端和服务端之间的往返时间较长时,(相对于什么较长?相对于 新连接的到达速率),因为一个新的 syn 包 会占据队列,长达客户端到服务端之间一次往返的时间。
当一个连接放入全连接队列时,它几乎总是空的, 因为当一个连接放入这个队列时, accept 调用就返回了, 然后 服务器将连接从队列中移除。
4、Stevens老爷子的建议
Stevens老爷子的建议是,增加backlog的值。 假设一个程序,打算对backlog 进行调优,不仅要考虑它怎么处理新建立的连接,也要考虑网络状况,比如客户端到服务器的往返时间。
Linux的实现有效地分离了这两个问题:
程序只需要负责调优 backlog,保证它能够尽快地调用 accept,避免堆满 accept 队列;
系统管理员可以基于 网络状况,对 /proc/sys/net/ipv4/tcp_max_syn_backlog 进行调优。
三、译文的测试验证
文章不太好理解,我查了些资料,参考https://www.cnblogs.com/xrq730/p/6910719.html后,我打算本地也进行一次验证。
主要是通过ss命令、以及wireshark抓包,观察这其中的细节。
1、服务端程序
首先,服务端程序为:
import java.net.ServerSocket; public class ServerSocketClass { public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(8888, 5); while (true) {
// server.accept();
}
} }
2、客户端程序
/**
* desc:
*
* @author : caokunliang
* creat_date: 2019/6/11 0011
* creat_time: 10:16
**/
import java.io.OutputStream;
import java.net.Socket;
import java.util.concurrent.locks.LockSupport; public class ClientSocketClass { private static Socket[] clients = new Socket[30]; public static void main(String[] args) throws Exception {
for (int i = 0; i < 1; i++) {
Socket socket = null;
socket = new Socket("192.168.19.13", 8888);
System.out.println("Client:" + socket + ", isConnected:" + socket.isConnected());
OutputStream outputStream = socket.getOutputStream();
outputStream.write('a');
} // 阻止程序退出,因为退出的话,程序会直接发送一个 RST 给服务器,不能观察 服务器的 ACK/SYN 重传
LockSupport.park();
} }
值得注意的是,这里,我们每次只发一次请求。
3、客户端请求发送5次,填满 accept 队列
观察下面的图,其中ss命令, 如果该条socket记录为监听端口,则Recv-Q 表示 accept 队列中元素的个数, Send-Q 表示 accept 队列中队列的容量。
Recv-Q Established: The count of bytes not copied by the user program connected to this socket. Listening: Since Kernel 2.6.18 this column contains the current syn backlog. Send-Q Established: The count of bytes not acknowledged by the remote host. Listening: Since Kernel 2.6.18 this column contains the maximum size of the syn backlog.
启动服务端程序,初始时,
每次我们执行客户端,这里便会加1。执行两次后:
4、再次发送连接请求
5次后,Recv-Q队列将会变满。如果此时再发送的话,按照参考博客中的说法,是会报错。但我这边没报错,看 wireshark 抓包:首先看服务端发给客户端的包,我们发现, 服务器确实会一直发送 SYN/ACK 给客户端,一共发了5次(即: /proc/sys/net/ipv4/tcp_synack_retries)。每次时间间隔加一倍。(参考退火算法)
可以看到,服务端一直给客户端发送 SYN/ACK,所以,客户端假设自己发出去的 ACK (三次握手的最后一次) 丢失了。于是会一直重发:
完整的交互如下:
我们发现,这里, 最后服务端会发送一个 RST ,但如果我们把客户端程序改改:
// OutputStream outputStream = socket.getOutputStream();
// outputStream.write('a');
再次请求,进行抓包的话,会发现不会发送 RST 了:
值得注意的是,在这种情况下,在客户端查看连接状态是 ESTABLISHED ,而在服务器端,查不到对应的连接信息。这也就验证了译文中 “问题延伸” 一节的猜想。
客户端:
服务器:
5、测试tcp_abort_on_overflow 参数
上面步骤都是在tcp_abort_on_overflow 为 false的情况下测试的, 这次我们打开后,再用下面程序测试。
sysctl -w net.ipv4.tcp_abort_on_overflow =
import java.net.Socket;
import java.util.concurrent.locks.LockSupport; public class ClientSocketClass { private static Socket[] clients = new Socket[30]; public static void main(String[] args) throws Exception {
for (int i = 0; i < 15; i++) {
Socket socket = null;
socket = new Socket("192.168.19.13", 8888);
System.out.println("Client:" + socket + ", isConnected:" + socket.isConnected());
} // 阻止程序退出,因为退出的话,程序会直接发送一个 RST 给服务器,不能观察 服务器的 ACK/SYN 重传
LockSupport.park();
} }
我们发起了15次连接,但是我们的 accept 队列为5,按理说只能成功 5 +1 = 6个连接,剩下的9个连接都会无效。tcp_abort_on_overflow 的作用是,在 accept 队列满时,返回 rst。下面测试:
上图可以看出,成功建立的只有6个,剩下的都被服务器返回了 RST 。
5、服务端正常accept时的连接情况
修改程序:将ServerSocketClass.java中的注释行打开,允许服务器调用accept;客户端循环次数改为20,看看服务器上的情况:
四、简单总结
backlog:该参数,每个程序可以在listen时自己设置,和另外一个参数( /proc/sys/net/core/somaxconn)一起,影响 全连接队列的容量。 具体算法是:min (backlog, /proc/sys/net/core/somaxconn ),最终可以建立的连接为 该值 + 1。
/proc/sys/net/ipv4/tcp_max_syn_backlog : 半连接队列的容量。(os层面,只能设一个,由所有程序共享)
/proc/sys/net/ipv4/tcp_synack_retries :分两种情况:
- tcp_abort_on_overflow = 0,服务端 accept 队列满了,客户端发来 ack , 服务端直接忽略该ack。因此服务端一直处于 SYN RECEIVED,触发了该状态下的定时器,该定时器会重传 SYN/ACK 给客户端,(不超过 /proc/sys/net/ipv4/tcp_synack_retries 指定的次数 ), 超过后,服务端不再重传,后续也不会再有任何动作;如果客户端此时传输数据的话,服务端会返回 RST;
- tcp_abort_on_overflow = 1,服务端 accept 队列满了,客户端发来 ack , 服务端直接返回 RST 。
ps: 查看、修改这些参数的简单方法:
#查看所有系统变量并查找
[root@localhost ~]# sysctl -a |grep somaxconn
net.core.somaxconn = # 设置系统变量
[root@localhost ~]# sysctl -w net.core.somaxconn=
net.core.somaxconn =
如何查看一个程序,最终生效的accept队列的大小:
五、 Tomcat 、nginx、redis中如何设置 backlog
1、tomcat
在tomcat 中, backlog 参数定义在org.apache.tomcat.util.net.AbstractEndpoint#backlog中,默认值为100。
/**
* Allows the server developer to specify the backlog that
* should be used for server sockets. By default, this value
* is 100.
*/
private int backlog = 100;
public void setBacklog(int backlog) { if (backlog > 0) this.backlog = backlog; }
但是在实际处理中, 会由 Digester 框架,去解析 server.xml,解析到 connector 时, 首先新建 org.apache.catalina.connector.Connector,
然后开始设置属性值:
当设置 acceptCount 时, 会调用 org.apache.catalina.connector.Connector#setProperty:
我们可以看看 replacements的定义:
protected static HashMap<String,String> replacements =
new HashMap<String,String>();
static {
replacements.put("acceptCount", "backlog");
replacements.put("connectionLinger", "soLinger");
replacements.put("connectionTimeout", "soTimeout");
replacements.put("rootFile", "rootfile");
}
所以,其实 connector 中 acceptCount 最终是 backlog 的值。
update于2020/03/12:
如果是spring boot内置的tomcat,我这边是
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.1.7.RELEASE</version>
<scope>compile</scope>
</dependency>
我发现,设置backlog的地方,在源码的如下位置:
org.apache.tomcat.util.net.AbstractEndpoint#acceptCount
默认值还是100。
如果要修改,可以直接:
https://www.cnblogs.com/Hleaves/p/11284316.html
2、nginx
server{
listen default_server backlog=;
}
3、redis
修改redis.conf
# TCP listen() backlog.
#
# In high requests-per-second environments you need an high backlog in order
# to avoid slow clients connections issues. Note that the Linux kernel
# will silently truncate it to the value of /proc/sys/net/core/somaxconn so
# make sure to raise both the value of somaxconn and tcp_max_syn_backlog
# in order to get the desired effect.
tcp-backlog 511
参考文档: