TCP三次握手及其四次挥手详解:实战篇

FIN-WAIT-1过多实战

基础知识

对于FIN-WAIT-1是什么,有什么用,请查看tcpip的内核篇。这里主要介绍conntrack、nc命令以及iptables命令。

NC

使用nc需要安装yum install nmap-ncat包,其官方地址:http://netcat.sourceforge.net/ 主要的参数如下:

1
2
3
4
5
6
7
8
-l	监听模式
-p 开启指定端口
-u 默认是TCP,-u参数调整为udp.
-z 参数告诉netcat使用0 IO,连接成功后立即关闭连接, 不进行数据交换
-v 详细输出
-n 参数告诉netcat 不要使用DNS反向查询IP地址的域名
-w<超时秒数> 设置等待连线的时间
-s<来源地址> 设置本地主机送出数据包的IP地址。

扫描端口:

1
2
3
4
5
6
7
8
[root@localhost ~]# nc -z -v 192.168.1.60 22
Ncat: Version 7.50 ( https://nmap.org/ncat )
Ncat: Connected to 192.168.1.60:22.
Ncat: 0 bytes sent, 0 bytes received in 0.06 seconds.
[root@localhost ~]# nc -z -w 2 192.168.1.60 22 && echo $?
0
[root@localhost ~]# nc -z -w 2 192.168.1.60 2233 ; echo $?
1

服务器监听端口,先在server上开启一个端口:

1
[root@master ~]# nc -l -p 2000 < ipinfo.txt

在client上运行命令:

1
2
3
4
5
[root@localhost ~]# nc 192.168.1.60 2000
eth0 192.168.1.60 255.255.255.0 -

123
^C

这时可以看到client上面有输出了ipinfo.txt的内容,再在client输入123这个内容,在server上面也是有同步输出的。

iptables

iptables的LOG 将封包相关讯息纪录在 /var/log 中,进行完此处理动作后,将会继续比对其规则。例如: iptables -I OUTPUT -m state --state NEW -j LOG --log-prefix "[OUTPUT] new: "

在/var/log/message日志中就有以下日志:

1
Jun 14 04:28:47 master kernel: [OUTPUT] new: IN= OUT=eth0 SRC=192.168.1.60 DST=14.215.177.39 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=47485 DF PROTO=TCPSPT=36736 DPT=80 WINDOW=29200 RES=0x00 SYN URGP=0

conntrack

安装yum install conntrack-tools -y连接跟踪工具。安装完成之后,可以使用conntrack命令来查看系统的连接情况。iptables log其实是借用此功能实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 统计总的连接跟踪数 -L 表示列表,-o 表示以扩展格式显示
$ conntrack -L -o extended | wc -l
14289

# 统计 TCP 协议各个状态的连接跟踪数
$ conntrack -L -o extended | awk '/^.*tcp.*$/ {sum[$6]++} END {for(i in sum) print i, sum[i]}'
SYN_RECV 4
CLOSE_WAIT 9
ESTABLISHED 2877
FIN_WAIT 3
SYN_SENT 2113
TIME_WAIT 9283

# 统计各个源 IP 的连接跟踪数
$ conntrack -L -o extended | awk '{print $7}' | cut -d "=" -f 2 | sort | uniq -c | sort -nr | head -n 10
14116 192.168.0.2
172 192.168.0.96

跟内核相关的参数有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@master ~]# sysctl -a |& grep conntrack |egrep "max|count|buckets|timeout_"
net.netfilter.nf_conntrack_buckets = 16384
net.netfilter.nf_conntrack_count = 7
net.netfilter.nf_conntrack_expect_max = 256
net.netfilter.nf_conntrack_max = 65536
net.netfilter.nf_conntrack_tcp_max_retrans = 3
net.netfilter.nf_conntrack_tcp_timeout_close = 10
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 60
net.netfilter.nf_conntrack_tcp_timeout_established = 43200
net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 120
net.netfilter.nf_conntrack_tcp_timeout_last_ack = 30
net.netfilter.nf_conntrack_tcp_timeout_max_retrans = 300
net.netfilter.nf_conntrack_tcp_timeout_syn_recv = 60
net.netfilter.nf_conntrack_tcp_timeout_syn_sent = 120
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 120
net.netfilter.nf_conntrack_tcp_timeout_unacknowledged = 300
net.netfilter.nf_conntrack_udp_timeout_stream = 180
net.nf_conntrack_max = 65536
  • net.netfilter.nf_conntrack_count,表示当前连接跟踪数;
  • net.netfilter.nf_conntrack_max,表示最大连接跟踪数;
  • net.netfilter.nf_conntrack_buckets,表示连接跟踪表的大小。

如果message里面有看到 “nf_conntrack: table full” 的错误时,就表明 nf_conntrack_max 太小了。

一般情况下,连接跟踪对象大小为 376,链表项大小为 16,那么连接跟踪表占用的内存大小为:nf_conntrack_max* 连接跟踪对象大小 + nf_conntrack_buckets* 链表项大小=65536*376B + 16384*16B=23.75MB

另外可以看到nf_conntrack_tcp_timeout_fin_wait的时间为120秒,如果系统里面有大量的FIN-WAIT-1/-2的话,可以减少该值,其他的timeout类似。

实验部分

准备工作

在server、client分别安装yum install conntrack-tools -y连接跟踪工具。然后在server上面将conntrack tcp timeout设置得短点:sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=20,以及下放iptables规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
iptables -P INPUT ACCEPT
iptables -F
iptables -X
iptables -Z
# 在日志里记录INPUT chain里过来的每个报文的状态
iptables -A INPUT -p tcp -m state --state NEW -j LOG --log-prefix "[iptables] INPUT NEW: "
iptables -A INPUT -p TCP -m state --state ESTABLISHED -j LOG --log-prefix "[iptables] INPUT ESTABLISHED: "
iptables -A INPUT -p TCP -m state --state RELATED -j LOG --log-prefix "[iptables] INPUT RELATED: "
iptables -A INPUT -p TCP -m state --state INVALID -j LOG --log-prefix "[iptables] INPUT INVALID: "
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --dport 21 -j ACCEPT
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -p tcp --dport 8088 -m state --state NEW -j ACCEPT
iptables -A INPUT -p icmp --icmp-type 8 -j ACCEPT
iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
# 在日志里记录OUTPUT chain里过来的每个报文的状态
iptables -A OUTPUT -p tcp -m state --state NEW -j LOG --log-prefix "[iptables] OUTPUT NEW: "
iptables -A OUTPUT -p TCP -m state --state ESTABLISHED -j LOG --log-prefix "[iptables] OUTPUT ESTABLISHED: "
iptables -A OUTPUT -p TCP -m state --state RELATED -j LOG --log-prefix "[iptables] OUTPUT RELATED: "
iptables -A OUTPUT -p TCP -m state --state INVALID -j LOG --log-prefix "[iptables] OUTPUT INVALID: "
# iptables -A OUTPUT -m state --state INVALID -j DROP
iptables -P INPUT DROP
iptables -P OUTPUT ACCEPT
iptables -P FORWARD DROP
service iptables save
systemctl restart iptables.service

开始测试

在server端开启监听端口nc -l -p 8088,再打开一个tab页,运行tailf /var/log/messages |grep 8088来查看iptables的日志,然后在client运行nc 192.168.1.60 8088,最后在server运行:

1
2
3
4
5
6
7
8
9
10
11
12
[root@master ~]# conntrack -L |& grep 8088
tcp 6 7 ESTABLISHED src=192.168.1.61 dst=192.168.1.60 sport=43708 dport=8088 src=192.168.1.60 dst=192.168.1.61 sport=8088 dport=43708 [ASSURED] mark=0 use=1
[root@master ~]# lsof -p 29382 -P -n
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nc 29382 root cwd DIR 8,3 4096 537378689 /root
nc 29382 root rtd DIR 8,3 224 64 /
nc 29382 root txt REG 8,3 380216 806371454 /usr/bin/ncat
....
nc 29382 root 2u CHR 136,2 0t0 5 /dev/pts/2
nc 29382 root 5u IPv4 41116 0t0 TCP 192.168.1.60:8088->192.168.1.61:43708 (ESTABLISHED)
[root@master ~]# ss -tn |grep 8088
ESTAB 0 0 192.168.1.60:8088 192.168.1.61:43708

可以看到此连接已经是ESTABLISHED状态了。使用lsof也可以看到此 状态。

不要做任何操作,等待20秒。再运行命令:

1
2
3
4
5
6
[root@master ~]# conntrack -L |& grep 8088
[root@master ~]#
[root@master ~]# kill -9 29382
[root@master ~]# ss -tn |grep 8088
State Recv-Q Send-Q Local Address:Port Peer Address:Port
FIN-WAIT-2 0 0 192.168.1.60:8088 192.168.1.61:43708

可以看到,并没有conntrack里面没有8088端口的状态,可想而知,内核已经将此连接给断掉了,但是lsof还是显示ESTABLISHED状态。

ss -tn 的输出中可以看到状态变成了FIN-WAIT-2,为什么是这个状态呢?我们看一下messages日志:

1
2
3
4
5
6
7
8
[root@master ~]# tailf /var/log/messages |grep 8088
Jun 15 22:48:45 master kernel: [iptables] INPUT NEW: IN=eth0 OUT= MAC=02:11:32:26:f6:08:02:11:32:20:c1:30:08:00 SRC=192.168.1.61 DST=192.168.1.60 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=49894 DF PROTO=TCP SPT=43708 DPT=8088 WINDOW=29200 RES=0x00 SYN URGP=0
Jun 15 22:48:45 master kernel: [iptables] OUTPUT ESTABLISHEDIN= OUT=eth0 SRC=192.168.1.60 DST=192.168.1.61 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=8088 DPT=43708 WINDOW=28960 RES=0x00 ACK SYN URGP=0
Jun 15 22:48:45 master kernel: [iptables] INPUT ESTABLISHED:IN=eth0 OUT= MAC=02:11:32:26:f6:08:02:11:32:20:c1:30:08:00 SRC=192.168.1.61 DST=192.168.1.60 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=49895 DF PROTO=TCP SPT=43708 DPT=8088 WINDOW=229 RES=0x00 ACK URGP=0


Jun 15 22:50:51 master kernel: [iptables] OUTPUT INVALID: IN= OUT=eth0 SRC=192.168.1.60 DST=192.168.1.61 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=14265 DF PROTO=TCP SPT=8088 DPT=43708 WINDOW=227 RES=0x00 ACK FIN URGP=0
Jun 15 22:50:51 master kernel: [iptables] INPUT NEW: IN=eth0 OUT= MAC=02:11:32:26:f6:08:02:11:32:20:c1:30:08:00 SRC=192.168.1.61 DST=192.168.1.60 LEN=52 TOS=0x00 PREC=0x00 TTL=64 ID=49896 DF PROTO=TCP SPT=43708 DPT=8088 WINDOW=229 RES=0x00 ACK URGP=0

可以看到前三条是建立链接的请求。而是第四条的时候,此连接状态直接变成了OUTPUT INVALID,由于上面的iptable的iptables -A OUTPUT -m state --state INVALID -j DROP是注释的,包是可以发出去的,那为什么最后一条 内核又认为是一个新的请求呢?先思考下。

client是可以正常收到FIN包,之后会回复ACK确认包,但在server看来,这是一条新的请求过来了,但是我已经停止服务8088端口的,所以服务器就不到响应了。下面是抓包看下的内容:

1
2
3
4
5
6
23:25:01.808314 IP 192.168.1.61.43712 > 192.168.1.60.8088: Flags [S], seq 1414242879, win 29200, options [mss 1460,sackOK,TS val 398212597 ecr 0,nop,wscale 7], length 0
23:25:01.810707 IP 192.168.1.60.8088 > 192.168.1.61.43712: Flags [S.], seq 2138733076, ack 1414242880, win 28960, options [mss 1460,sackOK,TS val 398217386 ecr 398212597,nop,wscale 7], length 0
23:25:01.810830 IP 192.168.1.61.43712 > 192.168.1.60.8088: Flags [.], ack 1, win 229, options [nop,nop,TS val 398212600 ecr 398217386], length 0

23:27:09.126103 IP 192.168.1.60.8088 > 192.168.1.61.43712: Flags [F.], seq 1, ack 1, win 227, options [nop,nop,TS val 398344706 ecr 398212600], length 0
23:27:09.197216 IP 192.168.1.61.43712 > 192.168.1.60.8088: Flags [.], ack 2, win 229, options [nop,nop,TS val 398339921 ecr 398344706], length 0

为什么kill进程后socket一直处于FIN_WAIT_1状态,https://yq.aliyun.com/articles/704262,源码级别,有兴趣可以查看。

线上问题实例

在线上一台服务器出现了too many open files的报错(线上业务,对报错信息做了修改,其重点是socket的报错):

1
2
[2019/06/06 13:36:00] [E] syncHttpClient: Get https://1.1.1.1:59211/: dial tcp 1.1.1.1:59211: socket: too many open files
[2019/06/06 13:36:10] [E] syncHttpClient: Get https://1.1.1.1:59211/ dial tcp 1.1.1.1:59211: socket: too many open files

第一反应是去查看ulimit -n,发现设置的值是12W,并没有什么问题。但去查看tcp连接数的时候就看出异常出来了。FIN-WAIT-1数达到了6W多次,很不正常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[op@localhost ~]$ ss -s
Total: 65847 (kernel 66009)
TCP: 65795 (estab 65707, closed 64, orphaned 12, synrecv 0, timewait 63/0), ports 100

Transport Total IP IPv6
* 66009 - -
RAW 2 2 0
UDP 2 2 0
TCP 65731 60 65671
INET 65735 64 65671
FRAG 0 0 0

[op@localhost ~]$ ss -tn |awk '{a[$1]++}END{for(i in a)print i,a[i]}' |sort -k2 -rn |head -n 10
FIN-WAIT-1 61420
ESTAB 56
SYN-SENT 16
LAST-ACK 9
CLOSE-WAIT 2
State 1
[op@localhost ~]$ ss -tn |grep FIN-WAIT-1 |more
FIN-WAIT-1 0 1 ::ffff:2.2.2.2:22222 ::ffff:183.159.166.48:55020
FIN-WAIT-1 0 1 ::ffff:2.2.2.2:22222 ::ffff:116.24.95.50:16815
FIN-WAIT-1 0 1 ::ffff:2.2.2.2:22222 ::ffff:122.234.71.140:6033
FIN-WAIT-1 0 1 ::ffff:2.2.2.2:22222 ::ffff:112.97.52.91:56463
FIN-WAIT-1 0 1 ::ffff:2.2.2.2:22222 ::ffff:183.230.42.68:44926

为什么不正常呢?这是由于四次挥手时,谁主动发起FIN包,谁就进入了FIN-WAIT-1的状态。从上输出可以看到,都是服务器本身发起的FIN包(假设2.2.2.2为服务器的IP)。所以在怀疑有人在对22222端口做攻击,这时使用iptables来drop掉这个流量,服务器就恢复正常了。

C1M

所谓的C1M指的是服务器要达到100M的并发连接,在这一篇里面,全部参考的是http://www.blogjava.net/yongboy/archive/2013/04/09/397559.html,因为我不是开发,其里面的代码我是看不懂的,我从运维角度来记录一下我的实验过程。

准备篇

分为测试端和服务器端,都选用较为熟悉的64位Centos 7.1。

服务器端

服务器端程序依赖libev框架,需要提前编译,然后存放到相应位置。目前最新版本为4.25。注意,这个libev跟libevent是不一样的,是2个东西。

首先编译libev

1
2
3
4
5
6
wget http://dist.schmorp.de/libev/libev-4.25.tar.gz
tar zxvf libev-4.25.tar.gz
cd libev-4.25
./configure
make
make install

编译完成之后,先记录一下libev.a这个模块的位置。如果在make install没有找到位置,可以使用find来查看,如下是部署过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[root@master ~]# wget https://gist.githubusercontent.com/yongboy/5318930/raw/ccf8dc236da30fcf4f89567d567eaf295b363d47/server.c
[root@master ~]#
[root@master ~]# find / -name "libev.a"
/usr/local/lib/libev.a
/usr/local/src/libev-4.25/.libs/libev.a
[root@master ~]#
[root@master ~]# gcc server.c -o server /usr/local/lib/libev.a -lm
server.c: In function ‘main’:
server.c:137:5: warning: passing argument 2 of ‘getopt’ from incompatible pointer type [enabled by default]
while ((ch = getopt(argc, argv, "p:")) != -1) {
^
In file included from /usr/include/unistd.h:893:0,
from server.c:9:
/usr/include/getopt.h:151:12: note: expected ‘char * const*’ but argument is of type ‘const char **’
extern int getopt (int ___argc, char *const *___argv, const char *__shortopts)
^
[root@master ~]# ll server*
-rwxr-xr-x 1 root root 209488 Jun 8 21:14 server
-rw-r--r-- 1 root root 5176 Jun 8 10:25 server.c
[root@master ~]# ./server #运行server会输出内存信息
start free -m is
total used free shared buff/cache available
Mem: 1999 92 1490 8 416 1717
Swap: 4095 0 4095
[root@master ~]# netstat -tunpl |grep 8000
tcp 0 0 0.0.0.0:8000 0.0.0.0:* LISTEN 23798/./server

客户端篇

client程序使用libevent框架,因其使用简单,提供丰富易用接口,但需要提前下载,手动安装:

1
2
3
4
5
6
wget https://github.com/downloads/libevent/libevent/libevent-2.1.10-stable.tar.gz
tar xvf libevent-2.1.10-stable.tar.gz
cd libevent-2.1.10-stable
./configure
make
make install

安装完成之后,下载clinet1.c文件,需要再修改一下SERVERADDR的参数,就是服务端的IP,如 下是操作步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@localhost ~]# wget https://gist.githubusercontent.com/yongboy/5318994/raw/f0cc8650c3350df1578dd986a38f4700152cd976/client1.c
[root@localhost ~]# grep "define SERVERADDR" client1.c
#define SERVERADDR "192.168.1.60"
[root@localhost ~]#
[root@localhost ~]# gcc -o client1 client1.c -levent #会有很多warn信息,可以忽视
client1.c: In function ‘main’:
client1.c:47:27: warning: assignment from incompatible pointer type [enabled by default]
evhttp_connection = evhttp_connection_new(SERVERADDR, SERVERPORT);
^
。。。
[root@localhost ~]# ll client1*
total 28
-rwxr-xr-x 1 root root 13304 Jun 8 21:38 client1
-rw-r--r-- 1 root root 1878 Jun 8 20:57 client1.c
[root@localhost ~]# ./client1
./client1: error while loading shared libraries: libevent-2.1.so.6: cannot open shared object file: No such file or directory

运行client1之后发现找不到libevent-2.1.so.6,那就再find一下吧。

1
2
3
4
5
6
7
8
9
10
11
[root@localhost ~]# find / -name "libevent-2.1.so.6"
/usr/local/lib/libevent-2.1.so.6
/usr/local/src/libevent-2.1.10-stable/.libs/libevent-2.1.so.6
[root@localhost ~]# ln -s /usr/local/lib/libevent-2.1.so.6 /lib64/libevent-2.1.so.6
[root@localhost ~]# ./client1
Req: 192.168.1.60 -> /test/100
Req: 192.168.1.60 -> /test/200

Chunks: 126 Bytes: 63630 Closed: 0
Req: 192.168.1.60 -> /test/300
Req: 192.168.1.60 -> /test/400

开始测试篇

开始测试前的准备

我们需要先观察一下各类参数,server的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[root@master ~]# netstat -s |grep -i listen
[root@master ~]#
[root@master ~]# sysctl -a 2>/dev/null |grep ipv4.tcp |grep syn
net.ipv4.tcp_max_syn_backlog = 128
net.ipv4.tcp_syn_retries = 6
net.ipv4.tcp_synack_retries = 5
net.ipv4.tcp_syncookies = 1
[root@master ~]# ss -ltn
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:22 *:*
LISTEN 0 5 *:8000 *:*
LISTEN 0 128 :::22 :::*
[root@master ~]# ll /proc/3514/fd
total 0
lrwx------ 1 root root 64 Jun 8 23:04 0 -> /dev/pts/0
lrwx------ 1 root root 64 Jun 8 23:04 1 -> /dev/pts/0
lrwx------ 1 root root 64 Jun 8 23:04 2 -> /dev/pts/0
lrwx------ 1 root root 64 Jun 8 23:04 3 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Jun 8 23:04 4 -> anon_inode:[eventfd]
lrwx------ 1 root root 64 Jun 8 23:04 5 -> socket:[22456]
[root@master ~]# lsof -p 3514
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
server 3514 root cwd DIR 8,3 4096 537378689 /root
server 3514 root rtd DIR 8,3 224 64 /
server 3514 root txt REG 8,3 209488 537378712 /root/server
server 3514 root mem REG 8,3 2151672 43198 /usr/lib64/libc-2.17.so
server 3514 root mem REG 8,3 1137016 44006 /usr/lib64/libm-2.17.so
server 3514 root mem REG 8,3 163400 43191 /usr/lib64/ld-2.17.so
server 3514 root 0u CHR 136,0 0t0 3 /dev/pts/0
server 3514 root 1u CHR 136,0 0t0 3 /dev/pts/0
server 3514 root 2u CHR 136,0 0t0 3 /dev/pts/0
server 3514 root 3u a_inode 0,11 0 7197 [eventpoll]
server 3514 root 4u a_inode 0,11 0 7197 [eventfd]
server 3514 root 5u IPv4 22456 0t0 TCP *:irdmi (LISTEN)

client的参数如下:

1
2
3
4
5
6
7
[root@localhost ~]# netstat -s |grep -i listen
[root@localhost ~]#
[root@localhost ~]# sysctl -a 2>/dev/null |grep ipv4.tcp |grep syn
net.ipv4.tcp_max_syn_backlog = 128
net.ipv4.tcp_syn_retries = 6
net.ipv4.tcp_synack_retries = 5
net.ipv4.tcp_syncookies = 1

开始测试:文件打开数问题

在server服务器上面运行./server之后,同时在client端运行./client,可以 在server看到online user 1018之后就不在变化了,说明并发用户数是在1018个,如下是server的显示内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@master ~]# ./server
...
online user 1013
online user 1014
online user 1015
online user 1016
online user 1017
online user 1018

[root@master ~]# ss -tn |grep -v State |sed 's/:/ /g' |awk '{a[$(NF-1)"==="$1]++}END{for(i in a) printi,a[i]}'
192.168.1.1===ESTAB 3
192.168.1.61===ESTAB 1018
[root@master ~]# ss -ltn;ll /proc/3514/fd |wc -l;lsof -p 3514|wc -l;netstat -s |grep -i listen
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:22 *:*
LISTEN 0 5 *:8000 *:*
LISTEN 0 128 :::22 :::*
1025
1031
851 times the listen queue of a socket overflowed
1443 SYNs to LISTEN sockets dropped

可以看到结果是./server这个进程建立的连接数有1018个,而lsof -p看到的打开文件数是1031,开启服务,但没有人来访问的文件数是13,那1031-13=1018个,是可以对应上程序输出的。

在client端的输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[root@localhost ~]# ./client1
....
Chunks: 716 Bytes: 361580 Closed: 0
Req: 192.168.1.60 -> /test/900
Req: 192.168.1.60 -> /test/1000

Chunks: 724 Bytes: 365620 Closed: 0
Req: 192.168.1.60 -> /test/1100
Req: 192.168.1.60 -> /test/1200

Chunks: 875 Bytes: 441875 Closed: 182
Req: 192.168.1.60 -> /test/1300
Req: 192.168.1.60 -> /test/1400

[root@localhost ~]# lsof -p 619 |grep 192.168.1.60 |wc -l
1018
[root@localhost ~]# lsof -p 619 |more
client1 619 root 1021u IPv4 332040 0t0 TCP localhost.localdomain:52844->192.168.1.60:irdmi (ESTABLISHED)
client1 619 root 1022u IPv4 332041 0t0 TCP localhost.localdomain:52846->192.168.1.60:irdmi (ESTABLISHED)
client1 619 root 1023u IPv4 332042 0t0 TCP localhost.localdomain:52848->192.168.1.60:irdmi (ESTABLISHED)
...
[root@localhost ~]# ss -tn |grep -v State |sed 's/:/ /g' |awk '{a[$(NF-1)"==="$1]++}END{for(i in a) print i,a[i]}'
192.168.1.210===ESTAB 3
192.168.1.60===ESTAB 1018

可以看到跟server的连接为1018个,跟server的显示一样。同时看client的输出 ,在访问/test/1000时,close的连接为0,但是在访问/test/1200之后,close的数量就达到了182个,说明正常的连接有1200-182=1018个。

遇到2个问题点:

  1. 一直没有出现Too many open files这个提示,从server端则可以看到有SYN flooding攻击,其他再无异常了。
1
2
3
[root@master ~]# dmesg |grep request_sock_TCP
[ 887.499450] TCP: request_sock_TCP: Possible SYN flooding on port 8000. Sending cookies. Check SNMPcounters.
[37761.524570] TCP: request_sock_TCP: Possible SYN flooding on port 8000. Sending cookies. Check SNMPcounters.
  1. 在server端可以看到半连接以及全连接的数量在正常连接阶段都会增加,但是在client请求数增长到3000以上时,这2个值就不会发现变化了。

但不管怎么样,这次测试出来的连接数只是达到1018个,可以得出结论跟打开文件数有关,按前文对/etc/security/limits.conf设置之后,必须要重启服务器才能生效,使用ulimit -SHn设置并没有生效,这点请注意。

第二次测试:最大连接数问题

重启服务器之后,再次测试,看到最终的online user 28225,这其实对最大连接数有关系,如下,默认开启端口是32768~60999,这样就一共28231个连接,这是相对clinet来说的,如果不清楚的同学可以看前一篇文章,原理篇的内容。‬

1
2
[root@localhost ~]# sysctl -a 2>/dev/null |grep port |grep range
net.ipv4.ip_local_port_range = 32768 60999

所以要模拟大并发的情况,可以有2个方法,一是增加可用端口的范围,二是增加IP地址。由于要指定绑定源IP去通信,再下载一个测试程序,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@localhost ~]# wget https://gist.githubusercontent.com/yongboy/5324779/raw/f29c964fcd67fefc3ce66e487a44298ced611cdc/client2.c
[root@localhost ~]# gcc -o client2 client2.c -levent
[root@localhost ~]#
[root@localhost ~]# ifconfig eth0:2 192.168.1.62/24 up
# -h 要连接的服务器IP地址;-p 要连接的服务器端口;-m 本机IP地址需要绑定的随机端口数量;-o 本机所有可用的IP地址列表,注意所有IP地址都应该可用
[root@localhost ~]# ./client2 -h 192.168.1.60 -p 8000 -m 1000 -o 192.168.1.62,192.168.1.61
host is 192.168.1.60
port is 8000
max_conns is 1000
ori_ips is 192.168.1.62,192.168.1.61
Req: 192.168.1.62 -> /test/1000

Chunks: 233 Bytes: 117665 Closed: 0
Req: 192.168.1.61 -> /test/2000

Chunks: 349 Bytes: 176245 Closed: 0
[root@localhost ~]# ss -tn |grep -v State |sed 's/:/ /g' |awk '{a[$(NF-1)"==="$1]++}END{for(i in a) print i,a[i]}'
192.168.1.60===FIN-WAIT-1 3
192.168.1.60===FIN-WAIT-2 69
192.168.1.210===ESTAB 2
192.168.1.60===ESTAB 1993
192.168.1.60===SYN-SENT 7

可能还会遇到fs.file-max的问题的,根据需要进行调整。

tcp_mem问题

tcp的发送、接收都是有缓冲区的,在大并发的情况下,此参数必须优化。可以参考:http://www.blogjava.net/yongboy/archive/2013/04/11/397677.html

参考链接

100万并发连接服务器笔记系列:http://www.blogjava.net/yongboy/category/54842.html

0%