这两天看tomcat,查阅 tomcat 怎么承载高并发时,看到了backlog参数。咱们知道,服务器端通常使用mq来减轻高并发下的洪峰冲击,将暂时不能处理的请求放入队列,后续再慢慢处理。其实操做系统已经帮咱们作了一些相似的东西了,这个东西就是backlog。服务端通常经过 accept 调用,去获取socket。可是假设咱们的程序处理不过来(好比由于程序bug,或者设计问题,没能及时地去调用 accept),那么此时的网络请求难道就直接丢掉吗?html
固然不会!这时候,操做系统会帮咱们放入 accept 队列,先暂存起来。等咱们的程序缓过来了,直接调用 accept 去 队列取就好了,这就达到了相似mq的效果。java
而 backlog,和另外一个参数 /proc/sys/net/core/somaxconn 一块儿,决定了队列的容量,算法为:min(/proc/sys/net/core/somaxconn, backlog) 。linux
文章比较长,若是只须要结论,看第三章的总结便可,有时间的话,能够仔细看看正文、第四章的验证部分。 若是只想知道怎么设置这个值,直接跳到最后便可。nginx
下面这篇文章,基础原理讲得很不错。可是是外国人写的,我这里简(tong)单(ku)翻译一下,我也会结合本身的理解,作一些补充。原文连接:http://veithen.io/2014/01/01/how-tcp-backlog-works-in-linux.html面试
正文以前,查了下linux中的说明。在linux下,执行 man listen,能够看到:redis
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).springThe 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.apache
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.tomcat
咱们着重看上面红色部分,“backlog 的意义从linux 2.2开始,发生了变化。如今,这个参数指定了已完成三次握手的 accept 队列的长度,而不是半链接队列的长度。半链接队列的长度能够经过 /proc/sys/net/ipv4/tcp_max_syn_backlog来设置”
因此,下次,若是面试官问你这个参数的意思是什么,那基本上答上面这句就没问题了。
但咱们仍是继续拓展下。下面我用渣英语翻译一下,权当锻炼了。
当一个程序要进行监听时,须要调用listen函数,此时,须要制定backlog参数。该参数,一般指定socket链接队列的长度。
由于tcp链接的创建须要三次握手,所以,站在服务端的角度,一个到来的链接在变成established以前,须要通过一个中间状态SYN RECEIVED;
进入established状态后,此时若是服务端调用accept操做,便可返回该socket。这意味着,tcp/ip协议栈要实现backlog队列,有两种选择:
一、使用一个单独的队列,队列的长度由 listen 调用的 backlog 参数决定。当收到一个 syn 包时,给客户端返回 SYN/ACK,并将此连接加入到队列。
当对应的 ACK 到达后, 链接状态改变为 ESTABLISHED,而后便可移交给应用程序处理。 这意味着,队列能够包含两种状态的链接: SYN RECEIVED 和 ESTABLISHED。
只有处于 ESTABLISHED 状态的链接,才能返回给应用程序发起的 accept 调用。
二、使用两个队列,一个 SYN 队列(或者叫作 半链接队列) 和一个 accept 队列(或者叫作 彻底链接队列)。 处于 SYN RECEIVED 状态的链接将被加入到 SYN 队列,后续当
状态变为 ESTABLISHED 状态时(好比三次握手中的最后一次 ACK 到达时),被移到 accept 队列。 就像 accept函数的名字所表示的那样, 实现 accept 调用时,只须要简单地从
accept 队列中获取链接便可。 在这种实现方式下, backlog 参数决定了 accept 队列的长度。
历史上, BSD 系统的 TCP 实现,使用第一种方式。 这种方式下,当 队列达到 backlog 指定的最大值时, 系统将再也不给客户端发来的 SYN 返回 SYN/ACK 。 一般, TCP 实现会简单地丢弃 SYN 包(甚至不会返回 RST 包),所以客户端会触发重试。 这个在 W. Richard Stevens 老爷子的 TCP/IP 卷三种的14.5节有讲。值得注意的是, Stevens 老爷子解释了, BSD 实际上确实用了两个单独的队列, 可是它们表现的和一个单独的,具备backlog参数指定的长度的队列没什么差异。好比,BSD 逻辑上表现得和下面的表述一致:
队列的大小是半链接队列的长度 和 全链接队列的长度之和。(意思是 sum = 半链接队列长度 + 全链接队列长度)
在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 队列取。
有个问题是, 若是 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 = 1; 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 74 53302 > 9999 [SYN] Seq=0 Len=0 0.000 127.0.0.1 -> 127.0.0.1 TCP 74 9999 > 53302 [SYN, ACK] Seq=0 Ack=1 Len=0 0.000 127.0.0.1 -> 127.0.0.1 TCP 66 53302 > 9999 [ACK] Seq=1 Ack=1 Len=0 0.000 127.0.0.1 -> 127.0.0.1 TCP 71 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5 0.207 127.0.0.1 -> 127.0.0.1 TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5 0.623 127.0.0.1 -> 127.0.0.1 TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5 1.199 127.0.0.1 -> 127.0.0.1 TCP 74 9999 > 53302 [SYN, ACK] Seq=0 Ack=1 Len=0 1.199 127.0.0.1 -> 127.0.0.1 TCP 66 [TCP Dup ACK 6#1] 53302 > 9999 [ACK] Seq=6 Ack=1 Len=0 1.455 127.0.0.1 -> 127.0.0.1 TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5 3.123 127.0.0.1 -> 127.0.0.1 TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5 3.399 127.0.0.1 -> 127.0.0.1 TCP 74 9999 > 53302 [SYN, ACK] Seq=0 Ack=1 Len=0 3.399 127.0.0.1 -> 127.0.0.1 TCP 66 [TCP Dup ACK 10#1] 53302 > 9999 [ACK] Seq=6 Ack=1 Len=0 6.459 127.0.0.1 -> 127.0.0.1 TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5 7.599 127.0.0.1 -> 127.0.0.1 TCP 74 9999 > 53302 [SYN, ACK] Seq=0 Ack=1 Len=0 7.599 127.0.0.1 -> 127.0.0.1 TCP 66 [TCP Dup ACK 13#1] 53302 > 9999 [ACK] Seq=6 Ack=1 Len=0 13.131 127.0.0.1 -> 127.0.0.1 TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5 15.599 127.0.0.1 -> 127.0.0.1 TCP 74 9999 > 53302 [SYN, ACK] Seq=0 Ack=1 Len=0 15.599 127.0.0.1 -> 127.0.0.1 TCP 66 [TCP Dup ACK 16#1] 53302 > 9999 [ACK] Seq=6 Ack=1 Len=0 26.491 127.0.0.1 -> 127.0.0.1 TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5 31.599 127.0.0.1 -> 127.0.0.1 TCP 74 9999 > 53302 [SYN, ACK] Seq=0 Ack=1 Len=0 31.599 127.0.0.1 -> 127.0.0.1 TCP 66 [TCP Dup ACK 19#1] 53302 > 9999 [ACK] Seq=6 Ack=1 Len=0 53.179 127.0.0.1 -> 127.0.0.1 TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5 106.491 127.0.0.1 -> 127.0.0.1 TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5 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 包。(上图标红那行)
上面的网络抓包,也展现出另外一个有趣的方面。 从客户端的角度来讲, 收到 服务端发来的 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 调用就返回了, 而后 服务器将链接从队列中移除。
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抓包,观察这其中的细节。
首先,服务端程序为:
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(); } } }
/**
* 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();
}
}
值得注意的是,这里,咱们每次只发一次请求。
观察下面的图,其中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。执行两次后:
四、再次发送链接请求
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 ,而在服务器端,查不到对应的链接信息。这也就验证了译文中 “问题延伸” 一节的猜测。
客户端:
服务器:
上面步骤都是在tcp_abort_on_overflow 为 false的状况下测试的, 此次咱们打开后,再用下面程序测试。
sysctl -w net.ipv4.tcp_abort_on_overflow = 1
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 。
修改程序:将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 :分两种状况:
ps: 查看、修改这些参数的简单方法:
#查看全部系统变量并查找 [root@localhost ~]# sysctl -a |grep somaxconn net.core.somaxconn = 128 # 设置系统变量 [root@localhost ~]# sysctl -w net.core.somaxconn=129 net.core.somaxconn = 129
如何查看一个程序,最终生效的accept队列的大小:
在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。
若是要修改,能够直接:
http://www.javashuo.com/article/p-gicmkitc-mw.html
server{ listen 8080 default_server backlog=1024; }
修改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
参考文档: