TCP是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务器的内存里保存的一份关于对方的信息,如ip地址、端口号等。
TCP可以看成是一种字节流,它会处理IP层或以下的层的丢包、重复以及错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在TCP头部。
TCP提供了一种可靠、面向连接、字节流、传输层的服务,采用三次握手建立一个连接。采用4次挥手来关闭一个连接。
客户端和服务端通信前要进行连接,“3次握手”的作用就是双方都能明确自己和对方的收、发能力是正常的。
经历了上面的三次握手过程,客户端和服务端都确认了自己的接收、发送能力是正常的。之后就可以正常通信了。
在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:
服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应 SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。
不管是半连接队列还是全连接队列,都有最大长度限制,超过限制时,内核会直接丢弃,或返回 RST 包。
很遗憾,我们没有命令可以查看系统的半连接队列数量。但是我们可以抓住 TCP 半连接的特点,就是服务端处于 SYN_RECV 状态的 TCP 连接,就是 TCP 半连接队列。使用如下命令计算当前 TCP 半连接队列长度:
$ netstat |grep SYN_RECV |wc -l
1723
SYN_RECV 状态下,服务器必须建立一个 SYN 半连接队列来维护未完成的握手信息,当这个队列溢出后,服务器将无法再建立新连接。
模拟 TCP 半连接溢出场景不难,实际上就是对服务端一直发送 TCP SYN 包,但是不回第三次握手 ACK,这样就会使得服务端有大量的处于 SYN_RECV 状态的 TCP 连接。这其实也就是所谓的 SYN 洪泛、SYN 攻击、DDos 攻击。
实验环境:
本次实验使用 hping3
工具模拟 SYN 攻击:
$ hping3 -S -p 80 --flood 172.16.0.20
HPING 172.16.0.20 (eth0 172.16.0.20): S set, 40 headers + 0 data bytes
hping in flood mode, no replies will be shown
新连接建立失败的原因有很多,怎样获得由于队列已满而引发的失败次数呢?netstat -s 命令给出的统计结果中可以得到。
$ netstat -s|grep "SYNs to LISTEN"
1541918 SYNs to LISTEN sockets dropped
这里给出的是队列溢出导致 SYN 被丢弃的个数。注意这是一个累计值,如果数值在持续增加,则应该调大 SYN 半连接队列。修改队列大小的方法,是设置 Linux 的 tcp_max_syn_backlog 参数:
sysctl -w net.ipv4.tcp_max_syn_backlog = 1024
如果 SYN 半连接队列已满,只能丢弃连接吗?并不是这样,开启 syncookies 功能就可以在不使用 SYN 队列的情况下成功建立连接。syncookies 是这么做的:服务器根据当前状态计算出一个值,放在己方发出的 SYN+ACK 报文中发出,当客户端返回 ACK 报文时,取出该值验证,如果合法,就认为连接建立成功,如下图所示。
Linux 下怎样开启 syncookies 功能呢?修改 tcp_syncookies 参数即可,其中值为 0 时表示关闭该功能,2 表示无条件开启功能,而 1 则表示仅当 SYN 半连接队列放不下时,再启用它。由于 syncookie 仅用于应对 SYN 泛洪攻击(攻击者恶意构造大量的 SYN 报文发送给服务器,造成 SYN 半连接队列溢出,导致正常客户端的连接无法建立),这种方式建立的连接,许多 TCP 特性都无法使用。所以,应当把 tcp_syncookies 设置为 1,仅在队列满时再启用。
sysctl -w net.ipv4.tcp_syncookies = 1
在服务端可以使用 ss 命令,来查看 TCP 全连接队列的情况:但需要注意的是 ss 命令获取的 Recv-Q/Send-Q 在「LISTEN 状态」和「非 LISTEN 状态」所表达的含义是不同的。从下面的内核代码可以看出区别:
static void tcp_diag_get_info(struct sock *sk, struct inet_diag_msg *r,
void *_info)
{
const struct tcp_sock *tp = tcp_sk(sk);
struct tcp_info *info = _info;
if (sk->sk_state == TCP_LISTEN) {
r->idiag_rqueue = sk->sk_ack_backlog;
r->idiag_wqueue = sk->sk_max_ack_backlog;
} else {
r->idiag_rqueue = tp->rcv_nxt - tp->copied_seq;
r->idiag_wqueue = tp->write_seq - tp->snd_una;
}
if (info != NULL)
tcp_get_info(sk, info);
}
在「LISTEN 状态」时,Recv-Q/Send-Q 表示的含义如下:
$ ss -ltnp
LISTEN 0 1024 *:8081 *:* users:(("java",pid=5686,fd=310))
accept()
的 TCP 连接;在「非 LISTEN 状态」时,Recv-Q/Send-Q 表示的含义如下:
$ ss -tnp
ESTAB 0 0 172.16.0.20:57672 172.16.0.20:2181 users:(("java",pid=5686,fd=292))
实验环境:
ab是apache bench命令的缩写。ab是apache自带的压力测试工具。ab非常实用,它不仅可以对apache服务器进行网站访问压力测试,也可以对或其它类型的服务器进行压力测试。比如nginx、tomcat、IIS等。ab的原理:ab命令会创建多个并发访问线程,模拟多个访问者同时对某一URL地址进行访问。它的测试目标是基于URL的,因此,它既可以用来测试apache的负载压力,也可以测试nginx、lighthttp、tomcat、IIS等其它Web服务器的压力。ab命令对发出负载的计算机要求很低,它既不会占用很高CPU,也不会占用很多内存。但却会给目标服务器造成巨大的负载,其原理类似CC攻击。自己测试使用也需要注意,否则一次上太多的负载。可能造成目标服务器资源耗完,严重时甚至导致死机。
TCP 全连接队列的最大值取决于 somaxconn 和 backlog 之间的最小值,也就是 min(somaxconn, backlog)。从下面的 Linux 内核代码可以得知:
int __sys_listen(int fd, int backlog)
{
struct socket *sock;
int err, fput_needed;
int somaxconn;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
if ((unsigned int)backlog > somaxconn)
backlog = somaxconn;
err = security_socket_listen(sock, backlog);
if (!err)
err = sock->ops->listen(sock, backlog);
fput_light(sock->file, fput_needed);
}
return err;
}
所以测试环境的 TCP 全连接队列最大值为 min(128, 511),也就是 511,可以执行 ss
命令查看:
ss -tulnp|grep 80
tcp LISTEN 0 511 *:80 *:* users:(("nginx",pid=22913,fd=6),("nginx",pid=22912,fd=6),("nginx",pid=22911,fd=6))
tcp LISTEN 0 511 [::]:80 [::]:* users:(("nginx",pid=22913,fd=7),("nginx",pid=22912,fd=7),("nginx",pid=22911,fd=7))
客户端执行 ab 命令对服务端发起压力测试,并发 1 万个连接,发送10万个包:
-n表示总的请求数为10000
-c表示并发请求数为1000
ab -c 10000 -n 100000 http://172.16.0.20:80/
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 172.16.0.20 (be patient)
Completed 10000 requests
Completed 20000 requests
Completed 30000 requests
Completed 40000 requests
Completed 50000 requests
Completed 60000 requests
Completed 70000 requests
Completed 80000 requests
Completed 90000 requests
Completed 100000 requests
Finished 100000 requests
Server Software: nginx/1.20.1
Server Hostname: 172.16.0.20
Server Port: 80
Document Path: /
Document Length: 4833 bytes
Concurrency Level: 10000
Time taken for tests: 2.698 seconds
Complete requests: 100000
Failed requests: 167336
(Connect: 0, Receive: 0, Length: 84384, Exceptions: 82952)
Write errors: 0
Total transferred: 86399264 bytes
HTML transferred: 82392984 bytes
Requests per second: 37069.19 [#/sec] (mean)
Time per request: 269.766 [ms] (mean)
Time per request: 0.027 [ms] (mean, across all concurrent requests)
Transfer rate: 31276.86 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 129 151.5 106 1144
Processing: 39 121 37.7 114 239
Waiting: 0 23 51.8 0 159
Total: 142 250 152.4 224 1346
Percentage of the requests served within a certain time (ms)
50% 224
66% 227
75% 232
80% 236
90% 283
95% 299
98% 1216
99% 1228
100% 1346 (longest request)
其间共执行了两次 ss 命令,从上面的输出结果,可以发现当前 TCP 全连接队列上升到了 512 大小,超过了最大 TCP 全连接队列。
ss -tulnp|grep 80
tcp LISTEN 411 511 *:80 *:* users:(("nginx",pid=22913,fd=6),("nginx",pid=22912,fd=6),("nginx",pid=22911,fd=6))
ss -tulnp|grep 80
tcp LISTEN 512 511 *:80 *:* users:(("nginx",pid=22913,fd=6),("nginx",pid=22912,fd=6),("nginx",pid=22911,fd=6))
当超过了 TCP 最大全连接队列,服务端则会丢掉后续进来的 TCP 连接,丢掉的 TCP 连接的个数会被统计起来,我们可以使用 netstat -s 命令来查看:
netstat -s|grep overflowed
1233972 times the listen queue of a socket overflowed
上面看到的 1233972 times ,表示全连接队列溢出的次数,注意这个是累计值。可以隔几秒钟执行下,如果这个数字一直在增加的话肯定全连接队列满了。
netstat -s|grep overflowed
1292022 times the listen queue of a socket overflowed
从上面的模拟结果,可以得知,当服务端并发处理大量请求时,如果 TCP 全连接队列过小,就容易溢出。发生 TCP 全连接队列溢出的时候,后续的请求就会被丢弃,这样就会出现服务端请求数量上不去的现象。
实际上,丢弃连接只是 Linux 的默认行为,我们还可以选择向客户端发送 RST 复位报文,告诉客户端连接已经建立失败。
$ cat /proc/sys/net/ipv4/tcp_abort_on_overflow
0
tcp_abort_on_overflow 共有两个值分别是 0 和 1,其分别表示:
如果要想知道客户端连接不上服务端,是不是服务端 TCP 全连接队列满的原因,那么可以把 tcp_abort_on_overflow 设置为 1,这时如果在客户端异常中可以看到很多 connection reset by peer 的错误,那么就可以证明是由于服务端 TCP 全连接队列溢出的问题。通常情况下,应当把 tcp_abort_on_overflow 设置为 0,因为这样更有利于应对突发流量。所以,tcp_abort_on_overflow 设为 0 可以提高连接建立的成功率,只有你非常肯定 TCP 全连接队列会长期溢出时,才能设置为 1 以尽快通知客户端。
sysctl -w net.ipv4.tcp_abort_on_overflow = 0
根据上面提到的TCP 全连接队列的最大值取决于 somaxconn 和 backlog 之间的最小值,也就是 min(somaxconn, backlog)。我们现在调整somaxconn值:
$ sysctl -w net.core.somaxconn=65535
调整nginx配置:
server {
listen 80 backlog=65535;
...
最后要重启 Nginx 服务,因为只有重新调用 listen() 函数 TCP 全连接队列才会重新初始化。服务端执行 ss 命令,查看 TCP 全连接队列大小:
$ ss -tulntp|grep 80
tcp LISTEN 0 65535 *:80 *:* users:(("nginx",pid=24212,fd=6),("nginx",pid=24211,fd=6),("nginx",pid=24210,fd=6))
从执行结果,可以发现 TCP 全连接最大值为 65535。
ab -c 10000 -n 100000 http://172.16.0.20:80/
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 172.16.0.20 (be patient)
Completed 10000 requests
Completed 20000 requests
Completed 30000 requests
Completed 40000 requests
Completed 50000 requests
Completed 60000 requests
Completed 70000 requests
Completed 80000 requests
Completed 90000 requests
Completed 100000 requests
Finished 100000 requests
Server Software: nginx/1.20.1
Server Hostname: 172.16.0.20
Server Port: 80
Document Path: /
Document Length: 4833 bytes
Concurrency Level: 10000
Time taken for tests: 2.844 seconds
Complete requests: 100000
Failed requests: 178364
(Connect: 0, Receive: 0, Length: 89728, Exceptions: 88636)
Write errors: 0
Total transferred: 57592752 bytes
HTML transferred: 54922212 bytes
Requests per second: 35159.35 [#/sec] (mean)
Time per request: 284.419 [ms] (mean)
Time per request: 0.028 [ms] (mean, across all concurrent requests)
Transfer rate: 19774.64 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 130 18.3 130 172
Processing: 45 142 40.1 138 281
Waiting: 0 19 52.4 0 185
Total: 159 272 31.2 272 390
Percentage of the requests served within a certain time (ms)
50% 272
66% 274
75% 275
80% 276
90% 280
95% 358
98% 370
99% 375
100% 390 (longest request)
服务端执行 ss 命令,查看 TCP 全连接队列使用情况:
$ ss -tulnp|grep 80
tcp LISTEN 8 65535 *:80 *:* users:(("nginx",pid=24212,fd=6),("nginx",pid=24211,fd=6),("nginx",pid=24210,fd=6))
$ ss -tulnp|grep 80
tcp LISTEN 352 65535 *:80 *:* users:(("nginx",pid=24212,fd=6),("nginx",pid=24211,fd=6),("nginx",pid=24210,fd=6))
$ ss -tulnp|grep 80
tcp LISTEN 0 65535 *:80 *:* users:(("nginx",pid=24212,fd=6),("nginx",pid=24211,fd=6),("nginx",pid=24210,fd=6))
从上面的执行结果,可以发现全连接队列使用增长的很快,但是一直都没有超过最大值,所以就不会溢出,那么 netstat -s 就不会有 TCP 全连接队列溢出个数继续增加:
$ netstat -s|grep overflowed
1540879 times the listen queue of a socket overflowed
$ netstat -s|grep overflowed
1540879 times the listen queue of a socket overflowed
$ netstat -s|grep overflowed
1540879 times the listen queue of a socket overflowed
$ netstat -s|grep overflowed
1540879 times the listen queue of a socket overflowed
说明 TCP 全连接队列最大值从 512 增大到 65535 后,服务端抗住了 10 万连接并发请求,也没有发生全连接队列溢出的现象了。如果持续不断地有连接因为 TCP 全连接队列溢出被丢弃,就应该调大 backlog 以及 somaxconn 参数。
互联网中往往服务器才是主动关闭连接的一方。这是因为,HTTP 消息是单向传输协议,服务器接收完请求才能生成响应,发送完响应后就会立刻关闭 TCP 连接,这样及时释放了资源,能够为更多的用户服务。
我们来看断开连接的时候的状态时序图:
其实四次挥手只涉及两种报文:FIN 和 ACK。FIN 就是 Finish 结束连接的意思,谁发出 FIN 报文,就表示它将不再发送任何数据,关闭这一方向的传输通道。ACK 是 Acknowledge 确认的意思,它用来通知对方,你方的发送通道已经关闭。
大量处于 TIME_WAIT 状态的连接,它们会占用大量内存和端口资源。这时,我们可以优化与 TIME_WAIT 状态相关的内核选项,比如采取下面几种措施。
sysctl -w net.ipv4.tcp_max_tw_buckets=1048576
sysctl -w net.netfilter.nf_conntrack_max=1048576
sysctl -w net.ipv4.tcp_fin_timeout=15
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.ip_local_port_range="1024 65535"
sysctl -w fs.nr_open=1048576
sysctl -w fs.file-max=1048576
[1] 系统性能调优必知必会.陶辉.极客时间.
[2] TCP/IP详解卷二:实现