plantegg

java tcp mysql performance network docker Linux

如何追踪网络流量

背景

某些场景下通过监控发现了流量比较大,不太合理,需要知道这些流量都是哪些进程访问哪些服务触发的

方法

  1. 定位流量是由哪个进程触发的
  2. 定位流量主要是访问哪些ip导致的
  3. 定位具体的端口有较大的流量

工具

nethogs/iftop/tcptrack

定位进程

sudo nethogs 

image.png

从上图可以看到总的流量,以及每个进程的流量大小。这里可以确认流量主要是3820的java进程消耗的

定位ip

sudo iftop -p -n -B

image.png

通过上图可以看到流量主要是消耗在 10.0.48.1的ip上

定位端口

10.0.48.1 有可能是一个mapping ip,需要进一步查看具体

sudo tcptrack -r 5 -i eth0  //然后输入小写s,按流量排序
sudo tcptrack -r 5 -i eth0 host 10.0.48.1 //filter 语法和tcpdump一样

image.png

可以看到4355/4356端口上流量相对较大

软件安装

后续发布新镜像都会带上这三个软件的rpm安装包
目前可以手动下载这三个rpm安装包:

Docker宿主机磁盘爆掉的几种情况

磁盘爆掉的几种情况

  1. 系统磁盘没有空间,解决办法:删掉 /var/log/ 下边的带日期的日志,清空 /var/log/messages 内容
  2. 容器使用的大磁盘但是仍然空间不够,有三个地方会使用大量的磁盘
    • 容器内部日志非常大,处理办法见方法一
    • 容器内部产生非常多或者非常大的文件,但是这个文件的位置又通过volume 挂载到了物理机上,处理办法见方法二
    • 对特别老的部署环境,还有可能是容器的系统日志没有限制大小,处理办法见方法三

现场的同学按如下方法依次检查

方法零: 检查系统根目录下每个文件夹的大小

sudo du / -lh --max-depth=1 --exclude=overlay --exclude=proc

看看除了容器之外有没有其它目录使用磁盘特别大,如果有那么一层层进去通过du命令来查看,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#sudo du / -lh --max-depth=1 --exclude=overlay --exclude=proc
16K /dev
16K /lost+found
4.0K /media
17G /home
136M /boot
832K /run
1.9G /usr
75M /tmp
12K /log
8.5G /var
4.0K /srv
0 /proc
22M /etc
84G /root
4.0K /mnt
508M /opt
0 /sys
112G /

那么这个案例中应该查看 /root下为什么用掉了84G(总共用了112G), 先 cd /root 然后执行: sudo du . -lh –max-depth=1 –exclude=overlay 进一步查看 /root 目录下每个文件夹的大小

如果方法零没找到占用特别大的磁盘文件,那么一般来说是容器日志占用太多的磁盘空间,请看方法一

方法一: 容器内部日志非常大(请确保先按方法零检查过了)

在磁盘不够的物理机上执行如下脚本:

1
2
3
4
5
6
7
sudo docker ps -a -q >containers.list

sudo cat containers.list | xargs sudo docker inspect $1 | grep merged | awk -F \" '{ print $4 }' | sed 's/\/merged//g' | xargs sudo du --max-depth=0 $1 >containers.size

sudo paste containers.list containers.size | awk '{ print $1, $2 }' | sort -nk2 >real_size.log

sudo tail -10 real_size.log | awk 'BEGIN {print "\tcontainer size\tunit"} { print NR":\t" $0"\t kB" }'
执行完后会输出如下格式:
1
2
3
4
5
6
7
8
9
10
11
container     size	unit
1: 22690f16822f 3769980 kb
2: 82b4ae98eeed 4869324 kb
3: 572a1b7c8ef6 10370404 kb
4: 9f9250d98df6 10566776 kb
5: 7fab70481929 13745648 kb
6: 4a14b58e3732 29873504 kb
7: 8a01418b6df2 30432068 kb
8: 83dc85caaa5c 31010960 kb
9: 433e51df88b1 35647052 kb
10: 4b42818a8148 61962416 kb

第二列是容器id,第三列是磁盘大小,第四列是单位, 占用最大的排在最后面

然后进到容器后通过 du / –max-depth=2 快速发现大文件

方法二: 容器使用的volume使用过大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$sudo du -l /data/lib/docker/defaultVolumes --max-depth=1 | sort -rn
456012884 /data/lib/docker/defaultVolumes
42608332 /data/lib/docker/defaultVolumes/task_3477_g0_ark-metadb_miniDBPaaS-MetaDB_1
32322220 /data/lib/docker/defaultVolumes/task_3477_g0_dbpaas-metadb_dbpaas_1
27461120 /data/lib/docker/defaultVolumes/task_3001_g0_ark-metadb_miniDBPaaS-MetaDB_1
27319360 /data/lib/docker/defaultVolumes/task_36000_g0_ark-metadb_miniDBPaaS-MetaDB
27313836 /data/lib/docker/defaultVolumes/task_3600_g0_dbpaas-metadb_minidbpaas
27278692 /data/lib/docker/defaultVolumes/task_3604_g0_ark-metadb_miniDBPaaS-MetaDB_1
27277004 /data/lib/docker/defaultVolumes/task_3603_g0_ark-metadb_miniDBPaaS-MetaDB_1
27275736 /data/lib/docker/defaultVolumes/task_3542_g0_ark-metadb_miniDBPaaS-MetaDB
27271428 /data/lib/docker/defaultVolumes/task_3597_g0_ark-metadb_miniDBPaaS-MetaDB
27270840 /data/lib/docker/defaultVolumes/task_3603_g0_dbpaas-metadb_minidbpaas_1
27270492 /data/lib/docker/defaultVolumes/task_3603_g0_dbpaas-metadb_minidbpaas
27270468 /data/lib/docker/defaultVolumes/task_3600_g0_ark-metadb_miniDBPaaS-MetaDB
27270252 /data/lib/docker/defaultVolumes/task_3535_g0_ark-metadb_miniDBPaaS-MetaDB
27270244 /data/lib/docker/defaultVolumes/task_3538_g0_ark-metadb_miniDBPaaS-MetaDB
27270244 /data/lib/docker/defaultVolumes/task_3536_g0_ark-metadb_miniDBPaaS-MetaDB
25312404 /data/lib/docker/defaultVolumes/task_3477_g0_dncs-server_middleware-dncs_2

/data/lib/docker/defaultVolumes 参数是默认设置的volume存放的目录(一般是docker的存储路径下 –graph=/data/lib/docker) ,第一列是大小,后面是容器名

volume路径在物理机上也有可能是 /var/lib/docker 或者 /mw/mvdocker/ 之类的路径下,这个要依据安装参数来确定,可以用如下命令来找到这个路径:

sudo systemctl status docker -l | grep --color graph

结果如下,红色参数后面的路径就是docker 安装目录,到里面去找带volume的字眼:

找到 volume很大的文件件后同样可以进到这个文件夹中执行如下命令快速发现大文件:

du . --max-depth=2

方法三 容器的系统日志没有限制大小

这种情况只针对2017年上半年之前的部署环境,后面部署的环境默认都控制了这些日志不会超过150M

按照方法二的描述先找到docker 安装目录,cd 进去,然后 :

du ./containers --max-depth=2

就很快找到那个大json格式的日志文件了,然后执行清空这个大文件的内容:

echo '' | sudo tee 大文件名

一些其他可能占用空间的地方

  • 机器上镜像太多,可以删掉一些没用的: sudo docker images -q | xargs sudo docker rmi
  • 机器上残留的volume太多,删:sudo docker volume ls -q | xargs sudo docker volume rm
  • 物理文件被删了,但是还有进程占用这个文件句柄,导致文件对应的磁盘空间没有释放,检查: lsof | grep deleted  如果这个文件非常大的话,只能通过重启这个进程来真正释放磁盘空间

OverlayFS(overlay)的镜像分层与共享

OverlayFS使用两个目录,把一个目录置放于另一个之上,并且对外提供单个统一的视角。这两个目录通常被称作层,这个分层的技术被称作union mount。术语上,下层的目录叫做lowerdir,上层的叫做upperdir。对外展示的统一视图称作merged。   

如下图所示,Overlay在主机上用到2个目录,这2个目录被看成是overlay的层。 upperdir为容器层、lowerdir为镜像层使用联合挂载技术将它们挂载在同一目录(merged)下,提供统一视图。

图片

注意镜像层和容器层是如何处理相同的文件的:容器层(upperdir)的文件是显性的,会隐藏镜像层(lowerdir)相同文件的存在。容器映射(merged)显示出统一的视图。   overlay驱动只能工作在两层之上。也就是说多层镜像不能用多层OverlayFS实现。替代的,每个镜像层在/var/lib/docker/overlay中用自己的目录来实现,使用硬链接这种有效利用空间的方法,来引用底层分享的数据。注意:Docker1.10之后,镜像层ID和/var/lib/docker中的目录名不再一一对应。   创建一个容器,overlay驱动联合镜像层和一个新目录给容器。镜像顶层是overlay中的只读lowerdir,容器的新目录是可写的upperdir。

netstat 等网络工具

netstat 和重传– timer

经常碰到一些断网环境下需要做快速切换,那么断网后需要多久tcp才能感知到这个断网,并断开连接触发上层的重连(一般会连向新的server)

netstat -st命令中,tcp: 部分取自/proc/net/snmp,而TCPExt部分取自/proc/net/netstat,该文件对TCP记录了更多的统计。sysstat包也会采集/proc/net/snmp

keepalive

from: https://superuser.com/questions/240456/how-to-interpret-the-output-of-netstat-o-netstat-timers

The timer column has two fields (from your o/p above):

1
2
keepalive     (6176.47/0/0)  
<1st field> <2nd field>

The 1st field can have values:
keepalive - when the keepalive timer is ON for the socket

on - when the retransmission timer is ON for the socket

off - none of the above is ON

on - #表示是重发(retransmission)的时间计时

off - #表示没有时间计时

timewait - #表示等待(timewait)时间计时

Probe zerowindow

keepalive 是指在连接闲置状态发送心跳包来检测连接是否还有效(比如对方掉电后肯定就无效了,tcp得靠这个keepalive来感知)。如果有流量在传输过程中对方掉电后会不停地 retransmission ,这个时候看到的就是 on,然后重传间隔和次数跟keepalive参数无关,只和 net.ipv4.tcp_retries1、net.ipv4.tcp_retries2相关了。

keepalive 状态下的连接:

image.png

1
2
3
4
5
6
7
8
9
10
11
12
#netstat -anto |grep -E ":3048|Q" | awk '{ if($3>0) print $0 }'
Proto Recv-Q Send-Q Local Address Foreign Address State Timer
tcp 0 22207 1.2.3.134:3048 4.3.44.45:40100 ESTABLISHED probe (0.05/0/0)
tcp 0 56960 1.2.3.134:3048 4.3.7.8:40057 ESTABLISHED probe (0.06/0/0)
tcp 0 59808 1.2.3.134:3048 4.3.9.10:40085 ESTABLISHED on (0.21/0/0)
tcp 0 64080 1.2.3.134:3048 4.3.7.8:40055 ESTABLISHED on (0.19/0/0)
tcp 0 4788 1.2.3.134:3048 4.3.0.1:40075 ESTABLISHED probe (0.01/0/0)
tcp 0 44144 1.2.3.134:3048 4.3.7.8:40049 ESTABLISHED probe (0.05/0/0)
tcp 0 52688 1.2.3.134:3048 4.3.34.35:40068 ESTABLISHED probe (0.06/0/0)
tcp 0 4788 1.2.3.134:3048 4.3.4.5:40073 ESTABLISHED probe (0.18/0/0)
tcp 0 31894 1.2.3.134:3048 4.3.17.18:40072 ESTABLISHED probe (0.17/0/0)
tcp 0 44144 1.2.3.134:3048 4.3.12.13:40099 ESTABLISHED probe (0.02/0/0)

The 2nd field has THREE subfields:

(6176.47/0/0) -> (a/b/c)
a=timer value (a=keepalive timer, when 1st field=“keepalive”; a=retransmission timer, when 1st field=“on”)

b=number of retransmissions that have occurred

c=number of keepalive probes that have been sent

/proc/sys/net/ipv4/tcp_keepalive_time
当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时。

/proc/sys/net/ipv4/tcp_keepalive_intvl
当探测没有确认时,重新发送探测的频度。缺省是75秒。

/proc/sys/net/ipv4/tcp_keepalive_probes
在认定连接失效之前,发送多少个TCP的keepalive探测包。缺省值是9。这个值乘以tcp_keepalive_intvl之后决定了,一个连接发送了keepalive之后可以有多少时间没有回应

For example, I had two sockets opened between a client & a server (not loopback). The keepalive setting are:

KEEPALIVE_IDLETIME 30
KEEPALIVE_NUMPROBES 4
KEEPALIVE_INTVL 10

And I did a shutdown of the client machine, so at …SHED on (2.47/254/2)

1
2
3
4
tcp        0    210 192.0.0.1:36483             192.0.68.1:43881            ESTABLISHED on (1.39/254/2)  
tcp 0 210 192.0.0.1:36483 192.0.68.1:43881 ESTABLISHED on (0.31/254/2)
tcp 0 210 192.0.0.1:36483 192.0.68.1:43881 ESTABLISHED on (2.19/255/2)
tcp 0 210 192.0.0.1:36483 192.0.68.1:43881 ESTABLISHED on (1.12/255/2)

As you can see, in this case things are a little different. When the client went down, my server started sending keepalive messages, but while it was still sending those keepalives, my server tried to send a message to the client. Since the client had gone down, the server couldn’t get any ACK from the client, so the TCP retransmission started and the server tried to send the data again, each time incrementing the retransmit count (2nd field) when the retransmission timer (1st field) expired.

Hope this explains the netstat –timer option well.

RTO 重传

1
2
#define TCP_RTO_MAX ((unsigned)(120*HZ)) //HZ 通常为1秒 
#define TCP_RTO_MIN ((unsigned)(HZ/5))

Linux 2.6+ uses HZ of 1000ms, so TCP_RTO_MIN is ~200 ms and TCP_RTO_MAX is ~120 seconds. Given a default value of tcp_retries set to 15, it means that it takes 924.6 seconds before a broken network link is notified to the upper layer (ie. application), since the connection is detected as broken when the last (15th) retry expires.

2018-04-27-linux-tcp-rto-retries2.png

The tcp_retries2 sysctl can be tuned via /proc/sys/net/ipv4/tcp_retries2 or the sysctl net.ipv4.tcp_retries2.

查看重传状态

重传状态的连接:

image.png

前两个 syn_sent 状态明显是 9031端口不work了,握手不上。

最后 established 状态的连接, 是22端口给53795发了136字节的数据但是没有收到ack,所以在倒计时准备重传中。

net.ipv4.tcp_retries1 = 3

放弃回应一个TCP 连接请求前﹐需要进行多少次重试。RFC 规定最低的数值是3﹐这也是默认值﹐根据RTO的值大约在3秒 - 8分钟之间。(注意:这个值同时还决定进入的syn连接)

(第二种解释:它表示的是TCP传输失败时不检测路由表的最大的重试次数,当超过了这个值,我们就需要检测路由表了)

从kernel代码可以看到,一旦重传超过阈值tcp_retries1,主要的动作就是更新路由缓存。
用以避免由于路由选路变化带来的问题。这个时候tcp连接没有关闭

net.ipv4.tcp_retries2 = 15

**在丢弃激活(已建立通讯状况)**的TCP连接之前﹐需要进行多少次重试。默认值为15,根据RTO的值来决定,相当于13-30分钟(RFC1122规定,必须大于100秒).(这个值根据目前的网络设置,可以适当地改小,我的网络内修改为了5)

(第二种解释:表示重试最大次数,只不过这个值一般要比上面的值大。和上面那个不同的是,当重试次数超过这个值,我们就必须关闭连接了)

from:Documentation/networking/ip-sysctl.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
tcp_retries1 - INTEGER
This value influences the time, after which TCP decides, that
something is wrong due to unacknowledged RTO retransmissions,
and reports this suspicion to the network layer.
See tcp_retries2 for more details.

RFC 1122 recommends at least 3 retransmissions, which is the
default.

tcp_retries2 - INTEGER
This value influences the timeout of an alive TCP connection,
when RTO retransmissions remain unacknowledged.
Given a value of N, a hypothetical TCP connection following
exponential backoff with an initial RTO of TCP_RTO_MIN would
retransmit N times before killing the connection at the (N+1)th RTO.

The default value of 15 yields a hypothetical timeout of 924.6
seconds and is a lower bound for the effective timeout.
TCP will effectively time out at the first RTO which exceeds the
hypothetical timeout.

RFC 1122 recommends at least 100 seconds for the timeout,
which corresponds to a value of at least 8.

img

retries限制的重传次数吗

咋一看文档,很容易想到retries的数字就是限定的重传的次数,甚至源码中对于retries常量注释中都写着”This is how many retries it does…”

1
2
3
4
5
6
7
8
9
10
11
12
13
#define TCP_RETR1       3   /*
* This is how many retries it does before it
* tries to figure out if the gateway is
* down. Minimal RFC value is 3; it corresponds
* to ~3sec-8min depending on RTO.
*/

#define TCP_RETR2 15 /*
* This should take at least
* 90 minutes to time out.
* RFC1122 says that the limit is 100 sec.
* 15 is ~13-30min depending on RTO.
*/

那就就来看看retransmits_timed_out的具体实现,看看到底是不是限制的重传次数

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
35
36
37
38
39
/* This function calculates a "timeout" which is equivalent to the timeout of a
* TCP connection after "boundary" unsuccessful, exponentially backed-off
* retransmissions with an initial RTO of TCP_RTO_MIN or TCP_TIMEOUT_INIT if
* syn_set flag is set.
*/
static bool retransmits_timed_out(struct sock *sk,
unsigned int boundary,
unsigned int timeout,
bool syn_set)
{
unsigned int linear_backoff_thresh, start_ts;
// 如果是在三次握手阶段,syn_set为真
unsigned int rto_base = syn_set ? TCP_TIMEOUT_INIT : TCP_RTO_MIN;

if (!inet_csk(sk)->icsk_retransmits)
return false;

// retrans_stamp记录的是数据包第一次发送的时间,在tcp_retransmit_skb()中设置
if (unlikely(!tcp_sk(sk)->retrans_stamp))
start_ts = TCP_SKB_CB(tcp_write_queue_head(sk))->when;
else
start_ts = tcp_sk(sk)->retrans_stamp;

// 如果用户态未指定timeout,则算一个出来
if (likely(timeout == 0)) {
/* 下面的计算过程,其实就是算一下如果以rto_base为第一次重传间隔,
* 重传boundary次需要多长时间
*/
linear_backoff_thresh = ilog2(TCP_RTO_MAX/rto_base);

if (boundary <= linear_backoff_thresh)
timeout = ((2 << boundary) - 1) * rto_base;
else
timeout = ((2 << linear_backoff_thresh) - 1) * rto_base +
(boundary - linear_backoff_thresh) * TCP_RTO_MAX;
}
// 如果数据包第一次发送的时间距离现在的时间间隔,超过了timeout值,则认为重传超于阈值了
return (tcp_time_stamp - start_ts) >= timeout;
}

从以上的代码分析可以看到,真正起到限制重传次数的并不是真正的重传次数。
而是以tcp_retries1或tcp_retries2为boundary,以rto_base(如TCP_RTO_MIN 200ms)为初始RTO,计算得到一个timeout值出来。如果重传间隔超过这个timeout,则认为超过了阈值。
上面这段话太绕了,下面举两个个例子来说明

1
2
3
4
5
6
7
8
以判断是否放弃TCP流为例,如果tcp_retries2=15,那么计算得到的timeout=924600ms。

1. 如果RTT比较小,那么RTO初始值就约等于下限200ms
由于timeout总时长是924600ms,表现出来的现象刚好就是重传了15次,超过了timeout值,从而放弃TCP流

2. 如果RTT较大,比如RTO初始值计算得到的是1000ms
那么根本不需要重传15次,重传总间隔就会超过924600ms。
比如我测试的一个RTT=400ms的情况,当tcp_retries2=10时,仅重传了3次就放弃了TCP流

一些重传的其它问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>> effective timeout指的是什么?  
<< 就是retransmits_timed_out计算得到的timeout值

>> 924.6s是怎么算出来的?
<< 924.6s = (( 2 << 9) -1) * 200ms + (15 - 9) * 120s

>> 为什么924.6s是lower bound?
<< 重传总间隔必须大于timeout值,即 (tcp_time_stamp - start_ts) >= timeout

>> 那RTO超时的间隔到底是不是源码注释的"15 is ~13-30min depending on RTO."呢?
<< 显然不是! 虽然924.6s(15min)是一个lower bound,但是它同时也是一个upper bound!
怎么理解?举例说明
1. 如果某个RTO值导致,在已经重传了14次后,总重传间隔开销是924s
那么它还需要重传第15次,即使离924.6s只差0.6s。这就是发挥了lower bound的作用
2. 如果某个RTO值导致,在重传了10次后,总重传间隔开销是924s
重传第11次后,第12次超时触发时计算得到的总间隔变为1044s,超过924.6s
那么此时会放弃第12次重传,这就是924.6s发挥了upper bound的作用
总的来说,在Linux3.10中,如果tcp_retres2设置为15。总重传超时周期应该在如下范围内
[924.6s, 1044.6s)

RTO重传案例

我们来看如下这个51432端口向9627端口上传过程,十分缓慢,重传包间隔基本是122秒,速度肯定没法快

image.png

上图中垂直方向基本都是发出3-5个包,然后休息120秒,继续发3-5个 包,速度肯定慢,下图可以看到具体的包:

image.png

来看下到9627的RTT,基本稳定在245秒或者122秒,这RTT也实在太大了。可以看到:

  1. 网络质量很不好,丢包有点多;

  2. rtt高得离谱,导致rto计算出来120秒了,所以一旦丢包就卡120秒以上。

下图是RTT图

image.png

两个原因一叠加,就出现了奇慢无比.

正常情况下RTO是从200ms开始翻倍,实际上OS层面限制了最小RTO 200ms、最大RTO 120秒,由于RTT都超过120秒了,计算所得的RTO必定也大于120秒,所以最终就是我们看到的一上来第一个RTO不是常见的200ms,直接干到了120秒。

netstat -s

netstat -s统计,有两个和timestamp stamp reject相关的。

1
2
3
netstat -st | grep stamp | grep reject
18 passive connections rejected because of time stamp
1453 packets rejects in established connections because of timestamp

丢包统计:

netstat -s |egrep -i “drop|route|overflow|filter|retran|fails|listen”

nstat -z -t 1 | egrep -i “drop|route|overflow|filter|retran|fails|listen”

netstat -st命令中,Tcp: 部分取自/proc/net/snmp,而TCPExt部分取自/proc/net/netstat,该文件对TCP记录了更多的统计。sysstat包也会采集/proc/net/snmp

nc 测试

1
nc -v -u -z -w 3 10.101.0.1 53 //测试server 的53端口上的udp服务能否通

nc 6.5 快速fin

img

nc -i 3 10.97.170.11 3306 -w 4 -p 1234

-i 3 表示握手成功后 等三秒钟nc退出(发fin)

nc 6.5 握手后立即发fin断开连接,导致可能收不到Greeting,换成7.5或者mysql client就OK了

也就是用nc 6.5来验证mysql 服务是否正常可能会碰到nc自己断的太快,实际mysql还是正常的,从而像是mysql没有回复Greeting,从而产生误判。

nc 7.5的抓包,明显可以看到nc在发fin前会先等3秒钟:

img

ping

sudo ping -f ip 大批量的icmp包

ping -D 带时间戳 或者:ping -i 5 google.com | xargs -L 1 -I ‘{}’ date ‘+%Y-%m-%d %H:%M:%S: {}’ 或者 ping www.google.fr | while read pong; do echo “$(date): $pong”; done

ping -O 不通的时候输出:no answer yet for icmp_seq=xxx

或者-D + awk

ping -D 114.114.114.114 | awk ‘{ if(gsub(/[|]/, “”, $1)) $1=strftime(“[%F %T]”, $1); print}’

Linux 下直接增加如下函数:

1
2
3
4
5
6
7
ping.ts(){
if [ -t 1 ]; then
ping -D "$@" | awk '{ if(gsub(/\[|\]/, "", $1)) $1=strftime("[\033[34m%F %T\033[0m]", $1); print; fflush()}'
else
ping -D "$@" | awk '{ if(gsub(/\[|\]/, "", $1)) $1=strftime("[%F %T]", $1); print; fflush()}'
fi
}

mtr

若需要将mtr的结果提供给第三方,建议可以使用-rc参数,r代表不使用交互界面,而是在最后给出一个探测结果报告;c参数指定需要作几次探测(一般建议是至少200个包,可以配合-i参数减少包间隔来加快得到结果的时间)。

traceroute

和mtr不同的是,traceroute默认使用UDP作为四层协议,下层还是依靠IP头的TTL来控制中间的节点返回ICMP差错报文,来获得中间节点的IP和延时。唯一的区别是,在达到目标节点时,若是ICMP协议,目标大概率是会回复ICMP reply;如果是UDP协议,按照RFC协议规定,系统是要回复ICMP 端口不可达的差错报文,虽然三大平台Windows/macOS/Linux都实现了这个行为,但出于某些原因,这个包可能还是会在链路上被丢弃,导致路由跟踪的结果无法显示出最后一跳。所以建议在一般的情况下,traceroute命令可以加上-I参数,让程序使用ICMP协议来发送探测数据包。

dstat

dstat 监控

image-20210425082343156

dstat -cdgilmnrsy –aio –fs –lock –raw

参考资料

http://perthcharles.github.io/2015/09/07/wiki-tcp-retries/

tcpping2

关于TCP 半连接队列和全连接队列

最近碰到一个client端连接异常问题,然后定位分析发现是因为全连接队列满了导致的。查阅各种资料文章和通过一系列的实验对TCP连接队列有了更深入的理解

查资料过程中发现没有文章把这两个队列以及怎么观察他们的指标说清楚,希望通过这篇文章能说清楚:

  1. 这两个队列是干什么用的;

2)怎么设置和观察他们的最大值;

3)怎么查看这两个队列当前使用到了多少;

4)一旦溢出的后果和现象是什么

问题描述

场景:JAVA的client和server,使用socket通信。server使用NIO。

1.间歇性的出现client向server建立连接三次握手已经完成,但server的selector没有响应到这连接。
2.出问题的时间点,会同时有很多连接出现这个问题。
3.selector没有销毁重建,一直用的都是一个。
4.程序刚启动的时候必会出现一些,之后会间歇性出现。

分析问题

正常TCP建连接三次握手过程:

image.png

  • 第一步:client 发送 syn 到server 发起握手;
  • 第二步:server 收到 syn后回复syn+ack给client;
  • 第三步:client 收到syn+ack后,回复server一个ack表示收到了server的syn+ack(此时client的56911端口的连接已经是established)

从问题的描述来看,有点像TCP建连接的时候全连接队列(accept队列,后面具体讲)满了,尤其是症状2、4. 为了证明是这个原因,马上通过 netstat -s | egrep “listen” 去看队列的溢出统计数据:

667399 times the listen queue of a socket overflowed

反复看了几次之后发现这个overflowed 一直在增加,那么可以明确的是server上全连接队列一定溢出了

接着查看溢出后,OS怎么处理:

# cat /proc/sys/net/ipv4/tcp_abort_on_overflow
0

tcp_abort_on_overflow 为0表示如果三次握手第三步的时候全连接队列满了那么 server 扔掉 client 发过来的ack(在server端认为连接还没建立起来)

为了证明客户端应用代码的异常跟全连接队列满有关系,我先把tcp_abort_on_overflow修改成 1,1表示第三步的时候如果全连接队列满了,server发送一个reset包给client,表示废掉这个握手过程和这个连接(本来在server端这个连接就还没建立起来)。

接着测试,这时在客户端异常中可以看到很多connection reset by peer的错误,到此证明客户端错误是这个原因导致的(逻辑严谨、快速证明问题的关键点所在)

于是开发同学翻看java 源代码发现socket 默认的backlog(这个值控制全连接队列的大小,后面再详述)是50,于是改大重新跑,经过12个小时以上的压测,这个错误一次都没出现了,同时观察到 overflowed 也不再增加了。

到此问题解决,简单来说TCP三次握手后有个accept队列,进到这个队列才能从Listen变成accept,默认backlog 值是50,很容易就满了。满了之后握手第三步的时候server就忽略了client发过来的ack包(隔一段时间server重发握手第二步的syn+ack包给client),如果这个连接一直排不上队就异常了。

但是不能只是满足问题的解决,而是要去复盘解决过程,中间涉及到了哪些知识点是我所缺失或者理解不到位的;这个问题除了上面的异常信息表现出来之外,还有没有更明确地指征来查看和确认这个问题。

深入理解TCP握手过程中建连接的流程和队列

image.png

如上图所示,这里有两个队列:syns queue(半连接队列);accept queue(全连接队列)

三次握手中,在第一步server收到client的syn后,把这个连接信息放到半连接队列中,同时回复syn+ack给client(第二步);

题外话,比如syn floods 攻击就是针对半连接队列的,攻击方不停地建连接,但是建连接的时候只做第一步,第二步中攻击方收到server的syn+ack后故意扔掉什么也不做,导致server上这个队列满其它正常请求无法进来

第三步的时候server收到client的ack,如果这时全连接队列没满,那么从半连接队列拿出这个连接的信息放入到全连接队列中,同时将连接状态从 SYN_RECV 改成 ESTABLISHED 状态,否则按tcp_abort_on_overflow指示的执行。

这时如果全连接队列满了并且tcp_abort_on_overflow是0的话,server会扔掉三次握手中第三步收到的ack(假装没有收到一样),过一段时间再次发送syn+ack给client(也就是重新走握手的第二步),如果client超时等待比较短,就很容易异常了。其实这个时候client认为连接已经建立了,可以发数据或者可以断开,而实际server上连接还没建立好(还没能力)。

在我们的os中retry 第二步的默认次数是2(centos默认是5次):

net.ipv4.tcp_synack_retries = 2

如果TCP连接队列溢出,有哪些指标可以看呢?

上述解决过程有点绕,听起来蒙,那么下次再出现类似问题有什么更快更明确的手段来确认这个问题呢?

通过具体的、可见的东西来强化我们对知识点的理解和吸收

netstat -s

[root@server ~]#  netstat -s | egrep "listen|LISTEN" 
667399 times the listen queue of a socket overflowed  //全连接队列溢出
//以下两行是一个意思, netstat 版本不同导致显示不同,新版本显示为 dropped
667399 SYNs to LISTEN sockets ignored                 //含全连接/半连接队列溢出+PAWSPassive 等
905080 SYNs to LISTEN sockets dropped                 //含全连接/半连接队列溢出+PAWSPassive 等
 
//和本文无关的一些其它指标 
919614 passive connections rejected because of time stamp //tcp_recycle 丢 syn 包,对应/proc/net/netstat 中 PAWSPassive
TCPTimeWaitOverflow: 65000                                //tcp_max_tw_buckets 溢出

比如上面看到的 667399 times ,表示全连接队列溢出的次数,隔几秒钟执行下,如果这个数字一直在增加的话肯定全连接队列偶尔满了

ignored 和 dropped:

image-20240807112156872

这些指标都是从 /proc/net/netstat 中采集,含义可以参考 net-tool 工具(netstat 命令来源)中的源码

image-20240807112356787

ss 命令

[root@server ~]# ss -lnt
Recv-Q Send-Q Local Address:Port  Peer Address:Port 
0        50               *:3306             *:* 

上面看到的第二列Send-Q 值是50,表示第三列的listen端口上的全连接队列最大为50,第一列Recv-Q为全连接队列当前使用了多少

全连接队列的大小取决于:min(backlog, somaxconn) . backlog是在socket创建的时候传入的,somaxconn是一个os级别的系统参数

《Unix Network Programming》中关于backlog的描述

The backlog argument to the listen function has historically specified the maximum value for the sum of both queues.

There has never been a formal definition of what the backlog means. The 4.2BSD man page says that it “defines the maximum length the queue of pending connections may grow to.” Many man pages and even the POSIX specification copy this definition verbatim, but this definition does not say whether a pending connection is one in the SYN_RCVD state, one in the ESTABLISHED state that has not yet been accepted, or either. The historical definition in this bullet is the Berkeley implementation, dating back to 4.2BSD, and copied by many others.

关于 somaxconn 终于在2019年将默认值从128调整到了2048, 这个调整合并到了kernel 5.17中

SOMAXCONN is /proc/sys/net/core/somaxconn default value.

It has been defined as 128 more than 20 years ago.

Since it caps the listen() backlog values, the very small value has
caused numerous problems over the years, and many people had
to raise it on their hosts after beeing hit by problems.

Google has been using 1024 for at least 15 years, and we increased
this to 4096 after TCP listener rework has been completed, more than
4 years ago. We got no complain of this change breaking any
legacy application.

Many applications indeed setup a TCP listener with listen(fd, -1);
meaning they let the system select the backlog.

Raising SOMAXCONN lowers chance of the port being unavailable under
even small SYNFLOOD attack, and reduces possibilities of side channel
vulnerabilities.

这个时候可以跟我们的代码建立联系了,比如Java创建ServerSocket的时候会让你传入backlog的值:

ServerSocket()
	Creates an unbound server socket.
ServerSocket(int port)
	Creates a server socket, bound to the specified port.
ServerSocket(int port, int backlog)
	Creates a server socket and binds it to the specified local port number, with the specified backlog.
ServerSocket(int port, int backlog, InetAddress bindAddr)
	Create a server with the specified port, listen backlog, and local IP address to bind to.

(来自JDK帮助文档:https://docs.oracle.com/javase/7/docs/api/java/net/ServerSocket.html)

半连接队列的大小取决于:max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog)。 不同版本的os会有些差异

我们写代码的时候从来没有想过这个backlog或者说大多时候就没给他值(那么默认就是50),直接忽视了他,首先这是一个知识点的忙点;其次也许哪天你在哪篇文章中看到了这个参数,当时有点印象,但是过一阵子就忘了,这是知识之间没有建立连接,不是体系化的。但是如果你跟我一样首先经历了这个问题的痛苦,然后在压力和痛苦的驱动自己去找为什么,同时能够把为什么从代码层推理理解到OS层,那么这个知识点你才算是比较好地掌握了,也会成为你的知识体系在TCP或者性能方面成长自我生长的一个有力抓手

netstat 命令

netstat跟ss命令一样也能看到Send-Q、Recv-Q这些状态信息,不过如果这个连接不是Listen状态的话,Recv-Q就是指收到的数据还在缓存中,还没被进程读取,这个值就是还没被进程读取的 bytes;而 Send 则是发送队列中没有被远程主机确认的 bytes 数

$netstat -tn  
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address   Foreign Address State  
tcp    0  0 server:8182  client-1:15260 SYN_RECV   
tcp    0 28 server:22    client-1:51708  ESTABLISHED
tcp    0  0 server:2376  client-1:60269 ESTABLISHED

netstat -tn 看到的 Recv-Q 跟全连接半连接中的Queue没有关系,这里特意拿出来说一下是因为容易跟 ss -lnt 的 Recv-Q 搞混淆

所以ss看到的 Send-Q、Recv-Q是目前全连接队列使用情况和最大设置
netstat看到的 Send-Q、Recv-Q,如果这个连接是Established状态的话就是发出的bytes并且没有ack的包、和os接收到的bytes还没交给应用

我们看到的 Recv-Q、Send-Q获取源代码如下( net/ipv4/tcp_diag.c ):

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) {  //LISTEN状态下的 Recv-Q、Send-Q
	    r->idiag_rqueue = sk->sk_ack_backlog;
	    r->idiag_wqueue = sk->sk_max_ack_backlog; //Send-Q 最大backlog
    } else {						   //其它状态下的 Recv-Q、Send-Q
	    r->idiag_rqueue = max_t(int, tp->rcv_nxt - tp->copied_seq, 0);
	    r->idiag_wqueue = tp->write_seq - tp->snd_una;
    }
    if (info != NULL)
    	tcp_get_info(sk, info);
}

比如如下netstat -t 看到的Recv-Q有大量数据堆积,那么一般是CPU处理不过来导致的:

image.png

netstat看到的listen状态的Recv-Q/Send-Q

netstat 看到的listen状态下的Recv-Q/Send-Q意义跟 ss -lnt看到的完全不一样。上面的 netstat 对非listen的描述没问题,但是listen状态似乎Send-Q这个值总是0,这要去看netstat的代码了,实际上Listen状态它不是一个连接,所以肯定统计不到流量,netstat似乎只是针对连接的统计

从网上找了两个Case,server的8765端口故意不去读取对方发过来的2000字节,所看到的是:

$ netstat -ano | grep 8765  
tcp0  0 0.0.0.0:87650.0.0.0:*   LISTEN  off (0.00/0/0)  
tcp 2000  0 10.100.70.140:8765  10.100.70.139:43634 ESTABLISHED off (0.00/0/0)

第二个Case,8000端口的半连接满了(129),但是这个时候Send-Q还是看到的0

$ netstat -ntap | grep 8000 
tcp      129      0 0.0.0.0:8000            0.0.0.0:*               LISTEN      1526/XXXXX- 
tcp        0      0 9.11.6.36:8000          9.11.6.37:48306         SYN_RECV    - 
tcp        0      0 9.11.6.36:8000          9.11.6.34:44936         SYN_RECV    - 
tcp      365      0 9.11.6.36:8000          9.11.6.37:58446         CLOSE_WAIT  -  

案列:如果TCP连接队列溢出,抓包是什么现象呢?

image.png

如上图server端8989端口的服务全连接队列已经满了(设置最大5,已经6了,通过后面步骤的ss -lnt可以验证), 所以 server尝试过一会假装继续三次握手的第二步,跟client说我们继续谈恋爱吧。可是这个时候client比较性急,忙着分手了,server觉得都没恋上那什么分手啊。所以接下来两边自说自话也就是都不停滴重传

通过ss和netstat所观察到的状态

image.png

image.png

另外一个案例,虽然最终的锅不是TCP全连接队列太小,但是也能从重传、队列溢出找到根因

实践验证一下上面的理解

上面是通过一些具体的工具、指标来认识全连接队列,接下来结合文章开始的问题来具体验证一下

把java中backlog改成10(越小越容易溢出),继续跑压力,这个时候client又开始报异常了,然后在server上通过 ss 命令观察到:

Fri May  5 13:50:23 CST 2017
Recv-Q Send-QLocal Address:Port  Peer Address:Port
11         10         *:3306               *:*

按照前面的理解,这个时候我们能看到3306这个端口上的服务全连接队列最大是10,但是现在有11个在队列中和等待进队列的,肯定有一个连接进不去队列要overflow掉,同时也确实能看到overflow的值在不断地增大。

能够进入全连接队列的 Socket 最大数量始终比配置的全连接队列最大长度 + 1,结合内核代码,发现内核在判断全连接队列是否满的情况下,使用的是 > 而非 >=

Linux下发SIGSTOP信号发给用户态进程,就可以让进程stop不再accept,模拟accept溢出的效果

kill -19 pid 即可; kill -18 pid 恢复暂停进程

1
2
3
#define SIGKILL     9    /* Kill, unblockable (POSIX). */
#define SIGCONT 18 /* Continue (POSIX). */
#define SIGSTOP 19 /* Stop, unblockable (POSIX). */

tsar监控accept队列的溢出

tsar –tcpx -s=lisove -li 1

Tomcat和Nginx中的Accept队列参数

Tomcat 默认短连接,backlog(Tomcat里面的术语是Accept count)Ali-tomcat默认是200, Apache Tomcat默认100.

#ss -lnt
Recv-Q Send-Q   Local Address:Port Peer Address:Port
0       100                 *:8080            *:*

Nginx默认是511

$sudo ss -lnt
State  Recv-Q Send-Q Local Address:PortPeer Address:Port
LISTEN    0     511              *:8085           *:*
LISTEN    0     511              *:8085           *:*

因为Nginx是多进程模式,所以看到了多个8085,也就是多个进程都监听同一个端口以尽量避免上下文切换来提升性能

image.png

进一步思考 client fooling 问题

如果client走完第三步在client看来连接已经建立好了,但是server上的对应的连接有可能因为accept queue满了而仍然是syn_recv状态,这个时候如果client发数据给server,server会怎么处理呢?(有同学说会reset,还是实践看看)

先来看一个例子:

image.png

如上图,图中3号包是三次握手中的第三步,client发送ack给server,这个时候在client看来握手完成,然后4号包中client发送了一个长度为238的包给server,因为在这个时候client认为连接建立成功,但是server上这个连接实际没有ready,所以server没有回复,一段时间后client认为丢包了然后重传这238个字节的包,等到server reset了该连接(或者client一直重传这238字节到超时,client主动发fin包断开该连接,如下图)

image.png

这个问题也叫client fooling,可以看这个patch在4.10后修复了:https://github.com/torvalds/linux/commit/5ea8ea2cb7f1d0db15762c9b0bb9e7330425a071 ,修复的逻辑就是,如果全连接队列满了就不再回复syn+ack了,免得client误认为这个连接建立起来了,这样client端收不到syn+ack就只能重发syn。

从上面的实际抓包来看不是reset,而是server忽略这些包,然后client重传,一定次数后client认为异常,然后断开连接。

如果这个连接已经放入了全连接队列但是应用没有accept(比如应用卡住了),那么这个时候client发过来的包是不会被扔掉,OS会先收下放到接收buffer中,知道buffer满了再扔掉新进来的。

过程中发现的一个奇怪问题

[root@server ~]# date; netstat -s | egrep "listen|LISTEN" 
Fri May  5 15:39:58 CST 2017
1641685 times the listen queue of a socket overflowed  # 全连接队列溢出
1641685 SYNs to LISTEN sockets ignored                 # 半连接队列溢出

[root@server ~]# date; netstat -s | egrep "listen|LISTEN" 
Fri May  5 15:39:59 CST 2017
1641906 times the listen queue of a socket overflowed
1641906 SYNs to LISTEN sockets ignored

如上所示:
overflowed 和 ignored 总是一样多,并且都是同步增加,overflowed 表示全连接队列溢出次数,SYNs to LISTEN socket ignored 表示半连接队列溢出等等指标的次数,没这么巧吧。

翻看内核源代码(https://github.com/torvalds/linux/blob/v6.13-rc6/net/ipv4/tcp_ipv4.c#L1849 ):

image.png

可以看到overflow的时候一定会drop++(socket ignored),也就是drop一定大于等于overflow。

同时我也查看了另外几台server的这两个值来证明drop一定大于等于overflow:

server1
150 SYNs to LISTEN sockets dropped

server2
193 SYNs to LISTEN sockets dropped

server3
16329 times the listen queue of a socket overflowed
16422 SYNs to LISTEN sockets dropped

server4
20 times the listen queue of a socket overflowed
51 SYNs to LISTEN sockets dropped

server5
984932 times the listen queue of a socket overflowed
988003 SYNs to LISTEN sockets dropped

总结:SYNs to LISTEN sockets dropped(ListenDrops)表示:全连接/半连接队列溢出以及PAWSPassive 等造成的 SYN 丢包

ListenDrops 表示: SYNs to LISTEN sockets dropped

那么全连接队列满了会影响半连接队列吗?

来看三次握手第一步的源代码(http://elixir.free-electrons.com/linux/v2.6.33/source/net/ipv4/tcp_ipv4.c#L1249):

image.png

TCP 三次握手第一步的时候如果全连接队列满了会影响第一步drop 半连接的发生,流程的如下:

tcp_v4_do_rcv->tcp_rcv_state_process->tcp_v4_conn_request
//如果accept backlog队列已满,且未超时的request socket的数量大于1,则丢弃当前请求  
  if(sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_yong(sk)>1)
      goto drop;

半连接队列的长度

半连接队列的长度由三个参数指定:

  • 调用 listen 时,传入的 backlog
  • /proc/sys/net/core/somaxconn 默认值为 128
  • /proc/sys/net/ipv4/tcp_max_syn_backlog* 默认值为 1024

假设 listen 传入的 backlog = 128,其他配置采用默认值,来计算下半连接队列的最大长度

1
2
3
4
5
6
7
backlog = min(somaxconn, backlog) = min(128, 128) = 128
nr_table_entries = backlog = 128
nr_table_entries = min(backlog, sysctl_max_syn_backlog) = min(128, 1024) = 128
nr_table_entries = max(nr_table_entries, 8) = max(128, 8) = 128
nr_table_entries = roundup_pow_of_two(nr_table_entries + 1) = 256
max_qlen_log = max(3, log2(nr_table_entries)) = max(3, 8) = 8
max_queue_length = 2^max_qlen_log = 2^8 = 256

可以得到半队列大小是 256,以上计算方法:

1
2
3
4
5
6
7
8
backlog = min(somaxconn, backlog)
nr_table_entries = backlog
nr_table_entries = min(backlog, sysctl_max_syn_backlog)
nr_table_entries = max(nr_table_entries, 8)
// roundup_pow_of_two: 将参数向上取整到最小的 2^n,注意这里存在一个 +1
nr_table_entries = roundup_pow_of_two(nr_table_entries + 1)
max_qlen_log = max(3, log2(nr_table_entries))
max_queue_length = 2^max_qlen_log

没开启tcp_syncookies的话,到tcp_max_syn_backlog 75%水位就开始drop syn包了

总结

Linux内核就引入半连接队列(用于存放收到SYN,但还没收到ACK的连接)和全连接队列(用于存放已经完成3次握手,但是应用层代码还没有完成 accept() 的连接)两个概念,用于存放在握手中的连接。

全连接队列、半连接队列溢出这种问题很容易被忽视,但是又很关键,特别是对于一些短连接应用(比如Nginx、PHP,当然他们也是支持长连接的)更容易爆发。 一旦溢出,从cpu、线程状态看起来都比较正常,但是压力上不去,在client看来rt也比较高(rt=网络+排队+真正服务时间),但是从server日志记录的真正服务时间来看rt又很短。

另外就是jdk、netty等一些框架默认backlog比较小,可能有些情况下导致性能上不去,比如 @毕玄 碰到的这个 《netty新建连接并发数很小的case》
都是类似原因

希望通过本文能够帮大家理解TCP连接过程中的半连接队列和全连接队列的概念、原理和作用,更关键的是有哪些指标可以明确看到这些问题。

另外每个具体问题都是最好学习的机会,光看书理解肯定是不够深刻的,请珍惜每个具体问题,碰到后能够把来龙去脉弄清楚。

为什么 netstat 看到的 listen 状态的 SEND-Q 总是0

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=e7073830cc8b52ef3df7dd150e4dac7706e0e104

1
2
3
4
5
6
#netstat -ntap | grep 8000
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 129 0 0.0.0.0:8000 0.0.0.0:* LISTEN 1526/XXXXX- //第三列总是0
tcp 0 0 9.11.6.36:8000 9.11.6.37:48306 SYN_RECV -
tcp 0 0 9.11.6.36:8000 9.11.6.34:44936 SYN_RECV -
tcp 365 0 9.11.6.36:8000 9.11.6.37:58446 CLOSE_WAIT -

如下图代码,最开始 2 边一起支持了 LISTEN socket 显示 accept 队列当前长度,后来右边支持显示最大长度时,左边没有加。netstat 是读取的 /proc/net/tcp,然后 ss 走了 diag 接口去拿的:

image-20240506134119413


也就是改了 tcp_diag_get_info ,但是忘了改 get_tcp4_sock,但是写 man netstat 自己也没验证过就以讹传讹

d31d2480d9840f0d88739941bed0654d5b581dcb ?

参考文章

星球同学最详细的实践(代码、抓包等等,共13篇,含演示代码) https://xiaodongq.github.io/2024/05/30/tcp_syn_queue/ https://xiaodongq.github.io/2024/06/26/libbpf-trace-tcp_connect/

X推友实验:https://wgzhao.github.io/notes/troubleshooting/deep-in-tcp-connect/

张师傅:https://juejin.cn/post/6844904071367753736

详细的实验以及分析,附Go 实验代码:https://www.51cto.com/article/687595.html

http://veithen.github.io/2014/01/01/how-tcp-backlog-works-in-linux.html

http://www.cnblogs.com/zengkefu/p/5606696.html

http://www.cnxct.com/something-about-phpfpm-s-backlog/

http://jaseywang.me/2014/07/20/tcp-queue-%E7%9A%84%E4%B8%80%E4%BA%9B%E9%97%AE%E9%A2%98/

http://jin-yang.github.io/blog/network-synack-queue.html#

http://blog.chinaunix.net/uid-20662820-id-4154399.html

https://www.atatech.org/articles/12919

https://blog.cloudflare.com/syn-packet-handling-in-the-wild/

How Linux allows TCP introspection The inner workings of bind and listen on Linux.

https://www.cnblogs.com/xiaolincoding/p/12995358.html

从一次线上问题说起,详解 TCP 半连接队列、全连接队列–详细的实验验证各种溢出

案例三:诡异的幽灵连接,全连接队列满后4.10内核不再回复syn+ack, 但是3.10会回syn+ack

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
commit 5ea8ea2cb7f1d0db15762c9b0bb9e7330425a071
Author: Eric Dumazet <edumazet@google.com>
Date: Thu Oct 27 00:27:57 2016

tcp/dccp: drop SYN packets if accept queue is full

Per listen(fd, backlog) rules, there is really no point accepting a SYN,
sending a SYNACK, and dropping the following ACK packet if accept queue
is full, because application is not draining accept queue fast enough.

This behavior is fooling TCP clients that believe they established a
flow, while there is nothing at server side. They might then send about
10 MSS (if using IW10) that will be dropped anyway while server is under
stress.

- /* 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) {
+ if (sk_acceptq_is_full(sk)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}

Client 不断地 connect 建新连接:

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
35
36
37
38
39
40
41
42
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#define MAXLINE 4096

int main(int argc, char** argv)
{
int sockfd, n;
char recvline[4096], sendline[4096];
struct sockaddr_in servaddr;

memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(6666);
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);

for (n = 0; n < 100; n++)
{
if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
return 0;
}

//客户端不停的向服务端发起新连接,成功之后继续发,没成功会阻塞在这里 //--------------
if(connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
{
printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
return 0;
}

printf("connected to server: %d\n", n);
close(sockfd);
}

return 0;
}

server 故意不accept:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>

#define MAXLINE 4096

int main(int argc, char* argv[])
{
int listenfd, connfd;
struct sockaddr_in servaddr;
char buff[4096];
int n;

if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 )
{
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}

memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(6666);

if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1)
{
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 1;
}
//全连接队列设置为10 //--------------
if(listen(listenfd, 10) == -1)
{
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 2;
}

if( (connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1)
{
printf("accept socket error: %s(errno: %d)", strerror(errno), errno);
return 3;
}

printf("accepet a socket\n");

//服务端仅accept一次,之后就不再accept,此时全连接队列会被堆满 //----------------------------------
sleep(1000);

close(connfd);
close(listenfd);
return 0;
}

就是要你懂TCP–握手和挥手

看过太多tcp相关文章,但是看完总是不过瘾,似懂非懂,反复考虑过后,我觉得是那些文章太过理论,看起来没有体感,所以吸收不了。

希望这篇文章能做到言简意赅,帮助大家透过案例来理解原理

tcp的特点

这个大家基本都能说几句,面试的时候候选人也肯定会告诉你这些:

  • 三次握手
  • 四次挥手
  • 可靠连接
  • 丢包重传
  • 速度自我调整

但是我只希望大家记住一个核心的:tcp是可靠传输协议,它的所有特点都为这个可靠传输服务

那么tcp是怎么样来保障可靠传输呢?

tcp在传输过程中都有一个ack,接收方通过ack告诉发送方收到那些包了。这样发送方能知道有没有丢包,进而确定重传

tcp建连接的三次握手

来看一个java代码连接数据库的三次握手过程

image.png

三个红框表示建立连接的三次握手:

  • 第一步:client 发送 syn 到server 发起握手;
  • 第二步:server 收到 syn后回复syn+ack给client;
  • 第三步:client 收到syn+ack后,回复server一个ack表示收到了server的syn+ack(此时client的48287端口的连接已经是established)

握手的核心目的是告知对方seq(绿框是client的初始seq,蓝色框是server 的初始seq),对方回复ack(收到的seq+包的大小),这样发送端就知道有没有丢包了

握手的次要目的是告知和协商一些信息,图中黄框。

  • MSS–最大传输包
  • SACK_PERM–是否支持Selective ack(用户优化重传效率)
  • WS–窗口计算指数(有点复杂的话先不用管)

image.png

全连接队列(accept queue)的长度是由 listen(sockfd, backlog) 这个函数里的 backlog 控制的,而该 backlog 的最大值则是 somaxconn。somaxconn 在 5.4 之前的内核中,默认都是 128(5.4 开始调整为了默认 4096)

当服务器中积压的全连接个数超过该值后,新的全连接就会被丢弃掉。Server 在将新连接丢弃时,有的时候需要发送 reset 来通知 Client,这样 Client 就不会再次重试了。不过,默认行为是直接丢弃不去通知 Client。至于是否需要给 Client 发送 reset,是由 tcp_abort_on_overflow 这个配置项来控制的,该值默认为 0,即不发送 reset 给 Client。推荐也是将该值配置为 0

net.ipv4.tcp_abort_on_overflow = 0

这就是tcp为什么要握手建立连接,就是为了解决tcp的可靠传输

物理上没有一个连接的东西在这里,udp也类似会占用端口、ip,但是大家都没说过udp的连接。而本质上我们说tcp的连接是指tcp是拥有和维护一些状态信息的,这个状态信息就包含seq、ack、窗口/buffer,tcp握手就是协商出来这些初始值。这些状态才是我们平时所说的tcp连接的本质。

unres_qlen 和 握手

tcp connect 的本地流程是这样的:

1、tcp发出SYN建链报文后,报文到ip层需要进行路由查询

2、路由查询完成后,报文到arp层查询下一跳mac地址

3、如果本地没有对应网关的arp缓存,就需要缓存住这个报文,发起arp报文请求

4、arp层收到arp回应报文之后,从缓存中取出SYN报文,完成mac头填写并发送给驱动。

问题在于,arp层报文缓存队列长度默认为3。如果你运气不好,刚好赶上缓存已满,这个报文就会被丢弃。

TCP层发现SYN报文发出去3s(默认值)还没有回应,就会重发一个SYN。这就是为什么少数连接会3s后才能建链。

幸运的是,arp层缓存队列长度是可配置的,用 sysctl -a | grep unres_qlen 就能看到,默认值为3。

建连接失败经常碰到的问题

内核扔掉syn的情况(握手失败,建不上连接):

  • rp_filter 命中(rp_filter=1, 多网卡环境), troubleshooting: netstat -s | grep -i filter ;
  • snat/dnat的时候宿主机port冲突,内核会扔掉 syn包。 troubleshooting: sudo conntrack -S | grep insert_failed //有不为0的
  • 全连接队列满的情况
  • syn flood攻击
  • 若远端服务器的内核参数 net.ipv4.tcp_tw_recycle 和 net.ipv4.tcp_timestamps 的值都为 1,则远端服务器会检查每一个报文中的时间戳(Timestamp),若 Timestamp 不是递增的关系,不会响应这个报文。配置 NAT 后,远端服务器看到来自不同的客户端的源 IP 相同,但 NAT 前每一台客户端的时间可能会有偏差,报文中的 Timestamp 就不是递增的情况。nat后的连接,开启timestamp。因为快速回收time_wait的需要,会校验时间该ip上次tcp通讯的timestamp大于本次tcp(nat后的不同机器经过nat后ip一样,保证不了timestamp递增)
  • NAT 哈希表满导致 ECS 实例丢包 nf_conntrack full

tcp断开连接的四次挥手

再来看java连上mysql后,执行了一个SQL: select sleep(2); 然后就断开了连接

image.png

四个红框表示断开连接的四次挥手:

  • 第一步: client主动发送fin包给server
  • 第二步: server回复ack(对应第一步fin包的ack)给client,表示server知道client要断开了
  • 第三步: server发送fin包给client,表示server也可以断开了
  • 第四部: client回复ack给server,表示既然双发都发送fin包表示断开,那么就真的断开吧

image.png

除了 CLOSE_WAIT 状态外,其余两个状态都有对应的系统配置项来控制。

我们首先来看 FIN_WAIT_2 状态,TCP 进入到这个状态后,如果本端迟迟收不到对端的 FIN 包,那就会一直处于这个状态,于是就会一直消耗系统资源。Linux 为了防止这种资源的开销,设置了这个状态的超时时间 tcp_fin_timeout,默认为 60s,超过这个时间后就会自动销毁该连接。

至于本端为何迟迟收不到对端的 FIN 包,通常情况下都是因为对端机器出了问题,或者是因为太繁忙而不能及时 close()。所以,通常我们都建议将 tcp_fin_timeout 调小一些,以尽量避免这种状态下的资源开销。对于数据中心内部的机器而言,将它调整为 2s 足以:

net.ipv4.tcp_fin_timeout = 2

TIME_WAIT 状态存在的意义是:最后发送的这个 ACK 包可能会被丢弃掉或者有延迟,这样对端就会再次发送 FIN 包。如果不维持 TIME_WAIT 这个状态,那么再次收到对端的 FIN 包后,本端就会回一个 Reset 包,这可能会产生一些异常。

image.png

为什么握手三次、挥手四次

这个问题太恶心,面试官太喜欢问,其实大部分面试官只会背诵:因为TCP是双向的,所以关闭需要四次挥手……。

你要是想怼面试官的话可以问他握手也是双向的但是只需要三次呢?

我也不知道怎么回答。网上都说tcp是双向的,所以断开要四次。但是我认为建连接也是双向的(双向都协调告知对方自己的seq号),为什么不需要四次握手呢,所以网上说的不一定精准。

你再看三次握手的第二步发 syn+ack,如果拆分成两步先发ack再发syn完全也是可以的(效率略低),这样三次握手也变成四次握手了。

看起来挥手的时候多一次,主要是收到第一个fin包后单独回复了一个ack包,如果能回复fin+ack那么四次挥手也就变成三次了。 来看一个案例:

image.png

图中第二个红框就是回复的fin+ack,这样四次挥手变成三次了(如果一个包就是一次的话)。

我的理解:之所以绝大数时候我们看到的都是四次挥手,是因为收到fin后,知道对方要关闭了,然后OS通知应用层要关闭,这里应用层可能需要做些准备工作,可能还有数据没发送完,所以内核先回ack,等应用准备好了主动调close时再发fin 。 握手过程没有这个准备过程所以可以立即发送syn+ack(把这里的两步合成一步了)。 内核收到对方的fin后,只能ack,不能主动替应用来fin,因为他不清楚应用能不能关闭。

ack=seq+len

ack总是seq+len(包的大小),这样发送方明确知道server收到那些东西了

但是特例是三次握手和四次挥手,虽然len都是0,但是syn和fin都要占用一个seq号,所以这里的ack都是seq+1

image.png

看图中左边红框里的len+seq就是接收方回复的ack的数字,表示这个包接收方收到了。然后下一个包的seq就是前一个包的len+seq,依次增加,一旦中间发出去的东西没有收到ack就是丢包了,过一段时间(或者其他方式)触发重传,保障了tcp传输的可靠性。

三次握手中协商的其它信息

MSS 最大一个包中能传输的信息(不含tcp、ip包头),MSS+包头就是MTU(最大传输单元),如果MTU过大可能在传输的过程中被卡住过不去造成卡死(这个大小的包一直传输不过去),跟丢包还不一样

MSS的问题具体可以看我这篇文章: scp某个文件的时候卡死问题的解决过程

SACK_PERM 用于丢包的话提升重传效率,比如client一次发了1、2、3、4、5 这5个包给server,实际server收到了 1、3、4、5这四个包,中间2丢掉了。这个时候server回复ack的时候,都只能回复2,表示2前面所有的包都收到了,给我发第二个包吧,如果server 收到3、4、5还是没有收到2的话,也是回复ack 2而不是回复ack 3、4、5、6的,表示快点发2过来。

但是这个时候client虽然知道2丢了,然后会重发2,但是不知道3、4、5有没有丢啊,实际3、4、5 server都收到了,如果支持sack,那么可以ack 2的时候同时告诉client 3、4、5都收到了,这样client重传的时候只重传2就可以,如果没有sack的话那么可能会重传2、3、4、5,这样效率就低了。

来看一个例子:

image.png

图中的红框就是SACK。

知识点:ack数字表示这个数字前面的数据收到了

TIME_WAIT 和 CLOSE_WAIT

假设服务端和客户端跑在同一台机器上,服务端监听在 18080端口上,客户端使用18089端口建立连接。

如果client主动断开连接那么就会看到client端的连接在 TIME_WAIT:

1
2
3
# netstat -ant |grep 1808
tcp 0 0 0.0.0.0:18080 0.0.0.0:* LISTEN
tcp 0 0 192.168.1.79:18089 192.168.1.79:18080 TIME_WAIT

如果Server主动断开连接(也就是18080)那么就会看到client端的连接在CLOSE_WAIT 而Server在FIN_WAIT2:

1
2
3
# netstat -ant |grep 1808
tcp 0 0 192.168.1.79:18080 192.168.1.79:18089 FIN_WAIT2 --<< server
tcp 0 0 192.168.1.79:18089 192.168.1.79:18080 CLOSE_WAIT --<< client

TIME_WAIT是主动断连方出现的状态( 2MSL)

被动关闭方收到fin后有两种选择

如下描述是server端主动关闭的情况

1 如果client也立即断开,那么Server的这个连接会进入 TIME_WAIT状态

1
2
3
# netstat -ant |grep 1808
tcp 0 0 0.0.0.0:18080 0.0.0.0:* LISTEN --<< server还在
tcp 0 0 192.168.1.79:18080 192.168.1.79:18089 TIME_WAIT --<< server

2 client 坚持不断开过 Server 一段时间后(3.10:net.netfilter.nf_conntrack_tcp_timeout_fin_wait = 120, 4.19:net.ipv4.tcp_fin_timeout = 15)会结束这个连接但是client还是会 在CLOSE_WAIT 直到client进程退出

1
2
# netstat -ant |grep 1808
tcp 0 0 192.168.1.79:18089 192.168.1.79:18080 CLOSE_WAIT

CLOSE_WAIT

CLOSE_WAIT是被动关闭端在等待应用进程的关闭

通常,CLOSE_WAIT 状态在服务器停留时间很短,如果你发现大量的 CLOSE_WAIT 状态,那么就意味着被动关闭的一方没有及时发出 FIN 包,一般有如下几种可能:

  • 程序问题:如果代码层面忘记了 close 相应的 socket 连接,那么自然不会发出 FIN 包,从而导致 CLOSE_WAIT 累积;或者代码不严谨,出现死循环之类的问题,导致即便后面写了 close 也永远执行不到。
  • 响应太慢或者超时设置过小:如果连接双方不和谐,一方不耐烦直接 timeout,另一方却还在忙于耗时逻辑,就会导致 close 被延后。响应太慢是首要问题,不过换个角度看,也可能是 timeout 设置过小。
  • BACKLOG 太大:此处的 backlog 不是 syn backlog,而是 accept 的 backlog,如果 backlog 太大的话,设想突然遭遇大访问量的话,即便响应速度不慢,也可能出现来不及消费的情况,导致多余的请求还在队列里就被对方关闭了。

如果你通过「netstat -ant」或者「ss -ant」命令发现了很多 CLOSE_WAIT 连接,请注意结果中的「Recv-Q」和「Local Address」字段,通常「Recv-Q」会不为空,它表示应用还没来得及接收数据,而「Local Address」表示哪个地址和端口有问题,我们可以通过「lsof -i:」来确认端口对应运行的是什么程序以及它的进程号是多少。

如果是我们自己写的一些程序,比如用 HttpClient 自定义的蜘蛛,那么八九不离十是程序问题,如果是一些使用广泛的程序,比如 Tomcat 之类的,那么更可能是响应速度太慢或者 timeout 设置太小或者 BACKLOG 设置过大导致的故障。

server端大量close_wait案例

看了这么多理论,下面用个案例来检查自己对close_wait理论(tcp握手本质)的掌握。同时也可以看看自己从知识到问题的推理能力(跟文章最后的知识效率呼应一下)。

问题描述:

服务端出现大量CLOSE_WAIT 个数正好 等于somaxconn(调整somaxconn后 CLOSE_WAIT 也会跟着变成一样的值)

根据这个描述先不要往下看,自己推理分析下可能的原因。

我的推理如下:

从这里看起来,client跟server成功建立了somaxconn个连接(somaxconn小于backlog,所以accept queue只有这么大),但是应用没有accept这个连接,导致这些连接一直在accept queue中。但是这些连接的状态已经是ESTABLISHED了,也就是client可以发送数据了,数据发送到server后OS ack了,并放在os的tcp buffer中,应用一直没有accept也就没法读取数据。client于是发送fin(可能是超时、也可能是简单发送数据任务完成了得结束连接),这时Server上这个连接变成了CLOSE_WAIT .

也就是从开始到结束这些连接都在accept queue中,没有被应用accept,很快他们又因为client 发送 fin 包变成了CLOSE_WAIT ,所以始终看到的是服务端出现大量CLOSE_WAIT 并且个数正好等于somaxconn(调整somaxconn后 CLOSE_WAIT 也会跟着变成一样的值)。

如下图所示,在连接进入accept queue后状态就是ESTABLISED了,也就是可以正常收发数据和fin了。client是感知不到server是否accept()了,只是发了数据后server的os代为保存在OS的TCP buffer中,因为应用没来取自然在CLOSE_WAIT 后应用也没有close(),所以一直维持CLOSE_WAIT 。

得检查server 应用为什么没有accept。

image.png

结论:

这个case的最终原因是因为OS的open files设置的是1024,达到了上限,进而导致server不能accept,但这个时候的tcp连接状态已经是ESTABLISHED了(这个状态变换是取决于内核收发包,跟应用是否accept()无关)。

同时从这里可以推断 netstat 即使看到一个tcp连接状态是ESTABLISHED也不能代表占用了 open files句柄。此时client可以正常发送数据了,只是应用服务在accept之前没法receive数据和close连接。

TCP连接状态图

image.png

总结下

tcp所有特性基本上核心都是为了可靠传输这个目标来服务的,然后有一些是出于优化性能的目的

三次握手建连接的详细过程可以参考我这篇: 关于TCP 半连接队列和全连接队列

后续希望再通过几个案例来深化一下上面的知识。


为什么要案例来深化一下上面的知识,说点关于学习的题外话

什么是工程效率,什么是知识效率

有些人纯看理论就能掌握好一门技能,还能举三反一,这是知识效率,这种人非常少;

大多数普通人都是看点知识然后结合实践来强化理解理论,要经过反反复复才能比较好地掌握一个知识,这就是工程效率,讲究技巧、工具来达到目的。

对于费曼(参考费曼学习法)这样的聪明人就是很容易看到一个理论知识就能理解这个理论知识背后的本质。

肯定知识效率最牛逼,但是拥有这种能力的人毕竟非常少。从小我们周边那种不怎么学的学霸型基本都是这类,这种学霸都还能触类旁通非常快地掌握一个新知识。剩下的绝大部分只能拼时间(刷题)+方法+总结等也能掌握一些知识

非常遗憾我就是工程效率型,只能羡慕那些知识效率型的学霸。但是这事又不能独立看待有些人在某些方向上是工程效率型,有些方向就又是知识效率型(有一种知识效率型是你掌握的实在太多也就比较容易触类旁通了,这算灰色知识效率型)

使劲挖掘自己在知识效率型方面的能力吧,即使灰色地带也行啊。

就是要你懂TCP–wireshark-dup-ack-issue

问题:

很多同学学会抓包后,经常拿着这样一个抓包来问我是怎么回事:

在wireshark中看到一个tcp会话中的两台机器突然一直互相发dup ack包,但是没有触发重传。每次重复ack都是间隔精确的20秒

如下截图:

client都一直在回复收到2号包(ack=2)了,可是server跟傻了一样居然还发seq=1的包(按理,应该发比2大的包啊)

系统配置:

net.ipv4.tcp_keepalive_time = 20
net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_keepalive_intvl = 3

原因:

抓包不全的话wireshark有缺陷,把keepalive包识别成了dup ack包,看内容这种dup ack和keepalive似乎是一样的,flags都是0x010。keep alive的定义的是后退一格(seq少1)。

2、4、6、8……号包,都有一个“tcp acked unseen segment”。这个一般表示它ack的这个包,没有被抓到。Wirshark如何作出此判断呢?前面一个包是seq=1, len=0,所以正常情况下是ack = seq + len = 1,然而Wireshark看到的确是ack = 2, 它只能判断有一个seq =1, len = 1的包没有抓到。
dup ack也是类似道理,这些包完全符合dup ack的定义,因为“ack = ” 某个数连续多次出现了。

这一切都是因为keep alive的特殊性导致的。打开66号包的tcp层(见后面的截图),可以看到它的 next sequence number = 12583,表示正常情况下server发出的下一个包应该是seq = 12583。可是在下一个包,也就是68号包中,却是seq = 12582。keep alive的定义的确是这样,即后退一格。
Wireshark只有在抓到数据包(66号包)和keep alive包的情况下才有可能正确识别,前面的抓包中恰好在keep alive之前丢失了数据包,所以Wireshark就蒙了。

构造重现

如果用“frame.number >= 68” 过滤这个包,然后File–>export specified packets保存成一个新文件,再打开那个新文件,就会发现Wireshark又蒙了。本来能够正常识别的keep alive包又被错看成dup ack了,所以一旦碰到这种情况不要慌要稳

下面是知识点啦

Keepalive

TCP报文接收方必须回复的场景:

TCP携带字节数据
没有字节数据,携带SYN状态位
没有字节数据,携带FIN状态位

keepalive 提取历史发送的最后一个字节,充当心跳字节数据,依然使用该字节的最初序列号。也就是前面所说的seq回退了一个

对方收到后因为seq小于TCP滑动窗口的左侧,被判定为duplicated数据包,然后扔掉了,并回复一个duplicated ack

所以keepalive跟duplicated本质是一回事,就看wireshark能够正确识别了。

Duplication ack是指:

server收到了3和8号包,但是没有收到中间的4/5/6/7,那么server就会ack 3,如果client还是继续发8/9号包,那么server会继续发dup ack 3#1 ; dup ack 3#2 来向客户端说明只收到了3号包,不要着急发后面的大包,把4/5/6/7给我发过来

TCP Window Update

如上图,当接收方的tcp Window Size不足一个MSS的时候,为了避免 Silly Window Syndrome,Client不再发小包,而是发送探测包(跟keepalive一样,发一个回退一格的包,触发server ack同时server ack的时候会带过来新的window size)探测包间隔时间是200/400/800/1600……ms这样

正常的keep-alive Case:

keep-alive 通过发一个比实际seq小1的包,比如server都已经 ack 12583了,client故意发一个seq 12582来标识这是一个keep-Alive包

Duplication ack是指:

server收到了3和8号包,但是没有收到中间的4/5/6/7,那么server就会ack 3,如果client还是继续发8/9号包,那么server会继续发dup ack 3#1 ; dup ack 3#2 来向客户端说明只收到了3号包,不要着急发后面的大包,把4/5/6/7给我发过来

docker、swarm的Label使用

需求背景

广发银行需要把方舟集群部署在多个机房(多个机房组成一个大集群),这样物理机和容器vlan没法互相完全覆盖,

也就是可能会出现A机房的网络subnet:192.168.1.0/24, B 机房的网络subnet:192.168.100.0/24 但是他们属于同一个vlan,要求如果容器在A机房的物理机拉起,分到的是192.168.1.0/24中的IP,B机房的容器分到的IP是:192.168.100.0/24

功能实现:

  • 本质就是对所有物理机打标签,同一个asw下的物理机用同样的标签,不同asw下的物理机标签不同;
  • 创建容器网络的时候也加标签,不同asw下的网络标签不一样,同时跟这个asw下的物理机标签匹配;
  • 创建容器的时候使用 –net=driver:vlan 来动态选择多个vlan网络中的任意一个,然后swarm根据网络的标签要和物理机的标签一致,从而把容器调度到正确的asw下的物理机上。

分为如下三个改造点

1:

daemon启动的时候增加标签(其中一个就行):

上联交换机组的名称,多个逗号隔开 com.alipay.acs.engine.asw.hostname

2:
创建网络的时候使用对应的标签:

网络域交换机组asw列表的名称,多个逗号隔开 com.alipay.acs.network.asw.hostname
该VLAN网络是否必须显式指定,默认为0即不必须,此时当传入–net driver:vlan时ACS会根据调度结果自行选择一个可用的VLAN网络并拼装到参数中 com.alipay.acs.network.explicit

3:

Swarm manager增加可选启动选项netarch.multiscope,值为true

功能实现逻辑

  1. Swarm manager增加可选启动选项netarch.multiscope,当为1时,network create时强制要求必须指定label描述配置VLAN的ASW信息
  2. Swarm manager在创建容器时检查网络类型,VLAN网络时则将网络ASW的label放入过滤器中,在调度时按照机器的ASW标签过滤
  3. 如果使用者如果不关心具体使用哪个VLAN,则可以指定–net=”driver:vlan”,会自动查找driver=vlan的network,并根据调度结果(Node所关联的ASW)自动选择合适的network填入Config.HostConfig.NetworkMode传递给Docker daemon.

如果是现存的环境,修改zk来更新网络标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
[zk: localhost:2181(CONNECTED) 21] get /Cluster/docker/network/v1.0/network/c79e533e4444294ac9cb7838608115c961c6e403d3610367ff4b197ef6b981fc 
{"addrSpace":"GlobalDefault","enableIPv6":false,"generic":{"com.docker.network.enable_ipv6":false,"com.docker.network.generic":{"VlanId":"192"}},"id":"c79e533e4444294ac9cb7838608115c961c6e403d3610367ff4b197ef6b981fc","inDelete":false,"internal":false,"ipamOptions":{"VlanId":"192"},"ipamType":"default","ipamV4Config":"[{\"PreferredPool\":\"192.168.8.0/24\",\"SubPool\":\"\",\"Gateway\":\"192.168.8.1\",\"AuxAddresses\":null}]","ipamV4Info":"[{\"IPAMData\":\"{\\\"AddressSpace\\\":\\\"\\\",\\\"Gateway\\\":\\\"192.168.8.1/24\\\",\\\"Pool\\\":\\\"192.168.8.0/24\\\"}\",\"PoolID\":\"GlobalDefault/192.168.8.0/24\"}]","labels":{},"name":"vlan192-8","networkType":"vlan","persist":true,"postIPv6":false,"scope":"global"}
cZxid = 0x4100008cce
ctime = Fri Mar 09 12:46:44 CST 2018
mZxid = 0x4100008cce
mtime = Fri Mar 09 12:46:44 CST 2018
pZxid = 0x4100008cce
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 716
numChildren = 0

//注意上面的网络还没有标签,修改如下:

1
2
[zk: localhost:2181(CONNECTED) 28] set /Cluster/docker/network/v1.0/network/c79e533e4444294ac9cb7838608115c961c6e403d3610367ff4b197ef6b981fc {"addrSpace":"GlobalDefault","enableIPv6":false,"generic":{"com.docker.network.enable_ipv6":false,"com.docker.network.generic":{"VlanId":"192"}},"id":"c79e533e4444294ac9cb7838608115c961c6e403d3610367ff4b197ef6b981fc","inDelete":false,"internal":false,"ipamOptions":{"VlanId":"192"},"ipamType":"default","ipamV4Config":"[{\"PreferredPool\":\"192.168.8.0/24\",\"SubPool\":\"\",\"Gateway\":\"192.168.8.1\",\"AuxAddresses\":null}]","ipamV4Info":"[{\"IPAMData\":\"{\\\"AddressSpace\\\":\\\"\\\",\\\"Gateway\\\":\\\"192.168.8.1/24\\\",\\\"Pool\\\":\\\"192.168.8.0/24\\\"}\",\"PoolID\":\"GlobalDefault/192.168.8.0/24\"}]",**"labels":{"com.alipay.acs.network.asw.hostname":"238"},**"name":"vlan192-8","networkType":"vlan","persist":true,"postIPv6":false,"scope":"global"}

example:

创建网络://–label=”com.alipay.acs.network.asw.hostname=vlan902-63”
docker network create -d vlan –label=”com.alipay.acs.network.asw.hostname=vlan902-63” –subnet=11.162.63.0/24 –gateway=11.162.63.247 –opt VlanId=902 –ipam-opt VlanId=902 hanetwork2
跟daemon中的标签:com.alipay.acs.engine.asw.hostname=vlan902-63 对应,匹配调度

1
2
3
4
$sudo cat /etc/docker/daemon.json
{"labels":["com.alipay.acs.engine.hostname=11.239.142.46","com.alipay.acs.engine.ip=11.239.142.46","com.alipay.acs.engine.device_type=Server","com.alipay.acs.engine.status=free","ark.network.vlan.range=vlan902-63","com.alipay.acs.engine.asw.hostname=vlan902-63","com.alipay.acs.network.asw.hostname=vlan902-63"]}
//不指定具体网络,有多个网络的时候自动调度 --net driver:vlan 必须是network打过标签了
docker run -d -it --name="udp10" --net driver:vlan --restart=always reg.docker.alibaba-inc.com/middleware.udp

如何手动为docker daemon添加label

  1. 编辑或创建/etc/docker/daemon.json

  2. 将一个或多个lable以json格式写入文件,示例如下

1
2
# 为docker分配两个label,分别是nodetype和red
{"labels":["nodetype=dbpaas", "color=red"]}
  1. 重启docker daemon
1
service docker restart

4 /etc/docker/daemon.json 参考

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
{
"api-cors-header": "",
"authorization-plugins": [],
"bip": "",
"bridge": "",
"cgroup-parent": "",
"cluster-store": "",
"cluster-store-opts": {},
"cluster-advertise": "",
"debug": true,
"default-gateway": "",
"default-gateway-v6": "",
"default-runtime": "runc",
"default-ulimits": {},
"disable-legacy-registry": false,
"dns": [],
"dns-opts": [],
"dns-search": [],
"exec-opts": [],
"exec-root": "",
"fixed-cidr": "",
"fixed-cidr-v6": "",
"graph": "",
"group": "",
"hosts": [],
"icc": false,
"insecure-registries": [],
"ip": "0.0.0.0",
"iptables": false,
"ipv6": false,
"ip-forward": false,
"ip-masq": false,
"labels": ["nodetype=drds-server", "ark.ip=11.239.155.83"],
"live-restore": true,
"log-driver": "",
"log-level": "",
"log-opts": {},
"max-concurrent-downloads": 3,
"max-concurrent-uploads": 5,
"mtu": 0,
"oom-score-adjust": -500,
"pidfile": "",
"raw-logs": false,
"registry-mirrors": [],
"runtimes": {
"runc": {
"path": "runc"
},
"custom": {
"path": "/usr/local/bin/my-runc-replacement",
"runtimeArgs": [
"--debug"
]
}
},
"selinux-enabled": false,
"storage-driver": "",
"storage-opts": [],
"swarm-default-advertise-addr": "",
"tls": true,
"tlscacert": "",
"tlscert": "",
"tlskey": "",
"tlsverify": true,
"userland-proxy": false,
"userns-remap": ""
}

Daemon.json 指定 ulimit等参考

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
cat >> /etc/docker/daemon.json <<EOF
{
"data-root": "/var/lib/docker",
"log-driver": "json-file",
"log-opts": {
"max-size": "200m",
"max-file": "5"
},
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 655360,
"Soft": 655360
},
"nproc": {
"Name": "nproc",
"Hard": 655360,
"Soft": 655360
}
},
"live-restore": true,
"oom-score-adjust": -1000,
"max-concurrent-downloads": 10,
"max-concurrent-uploads": 10,
"storage-driver": "overlay2",
"storage-opts": ["overlay2.override_kernel_check=true"],
"exec-opts": ["native.cgroupdriver=systemd"],
"registry-mirrors": [
"https://yssx4sxy.mirror.aliyuncs.com/"
]
}
EOF

方舟环境容器调度

主要功能

  • 恢复宿主机死机或者断网后上面需要调度的所有容器
  • 恢复非正常的容器状态到正常
  • 调度的容器能够支持vlan网络和Host模式
  • 调度容器本身通过Leader-Follower的模式保证高可用性
  • 调度容器支持cron定时任务(精确到秒级)
  • 查询哪个节点是Leader
  • 停止或者打开调度(方便容器维护、正常启停)

通过 ark-schedule 镜像启动调度

必须在swarm manager节点上以 docker 容器的方式来启动,下面的 -e 参数对应后面的 export 参数和作用注释

1
docker run -d --restart=always --name=ark-schedule -e ACS_CLUSTER_SECURITY_GROUP=false -e ACS_CLUSTER_SCHEME=tcp -e ACS_CLUSTER_ENDPOINT=11.239.155.112:3376 -e ACS_NETWORK_NAME=vlan701 -e ACS_CRONTAB="7 * * * * *" -e ACS_PORT=3375 -e ACS_ADVERTISE=11.239.155.112:3375 -e ACS_NETWORK_STORE_CLUSTER=zk://11.239.155.112:2181,11.239.155.103:2181,11.239.155.97:2181/Cluster -e affinity:container==swarm-manager --net=host reg.docker.alibaba-inc.com/ark/ark-schedule:0.6-20180530-68e7bed /ark-schedule/ark-schedule --debug start

如果需要调度容器本身高可以用,需要在不同的宿主机上启动多个 ark-schedule 容器, 同时可以给调度容器自己增加调度标签

环境变量参数说明

1
2
3
4
5
6
7
export ACS_CLUSTER_ENDPOINT=10.125.14.238:3376; //跟自己在同一台宿主机的swarm-manager
export ACS_NETWORK_NAME=vlan192; //方舟网络名称 docker network ls 看到vlan开头的名字
export ACS_NETWORK_STORE_CLUSTER=zk://10.125.26.108:2181,10.125.14.238:2181,10.125.1.45:2181/Cluster; //方舟zk集群,同部署的ark.properties中的
export ACS_CRONTAB="*/7 * * * * *"
export ACS_PORT="3375" //schedule 自身api暴露端口
export ACS_ADVERTISE="10.125.14.238:3375" //宿主机ip+自身api暴露端口 多个schedule容器唯一
./ark-schedule --debug start

ark-schedule 容器默认占用3375端口,如果要用别的端口需要通过 -e ACS_PORT 参数传入

-e ACS_CRONTAB="7 * * * * *" (秒 分 时 天 月 星期)

这个参数如果没有,那么需要外部来触发调度API(见下面)

ACS_ADVERTISE=”10.125.26.108:3375” 这个参数是多容器选举用的,每个容器用自己的IP+PORT来标识

容器日志主要在 /root/logs/ark-schedule-container-2017-12-12.log 中, 可以映射到宿主机上,查看更方便

镜像版本

0.1 带cron功能,自动定时扫描并恢复容器
0.2-election 有多个ark-schedule节点选举功能,抢到主的开始cron,没有抢到或者失去主的stop cron
0.3-election 在0.2的基础上修复了docker/libkv的bug,能够在弱网络、断网的条件下正常运行
0.4-switch 增加查询leader节点和cron是否开始的API,增加对Leader的cron启停的API
0.5-labels 增加对restart/recreate 标签的支持
0.6 去掉了对多个zk的支持,简化启动参数
0.7 修复了重复endpoint导致的容器的域名不通、inspect notfound(集群多个同名容器的时候)等各种问题

所有需要调度的容器增加调度标志标签

在docker run中增加一个标签: –label “ark.labels.schedule=haproxy”

详细命令:

1
sudo docker update --label-add="ark.labels.schedule=haproxy" --label-add="ark.enable_restart=true" --label-add="ark.enable_recreate=true" 容器名1 容器名2

上述命令不需要重启容器,但是要重新调snapshot API 做一次快照,让他们生效

ark-schedule容器在调度容器的时候,先检查快照中的容器,如果容器不见了或者状态不是up,又包含如上标签,就会重新在其它机器上把这个容器拉起来

  • ark.enable_restart
    是否允许通过重启来恢复容器(默认是true)。true为可以,false不可以

  • ark.enable_recreate
    是否允许将消失的容器在其他宿主机重建(默认是true)。true为可以,false不可以

API (如下ip:10.125.14.238 在现场换成客户物理机IP)

  1. 中间件部署完毕,并检查无误,调用: curl -v “http://10.125.14.238:3375/schedule/snapshot“ 对中间件做快照,将来会按快照的状态来进行恢复,执行一次就可以
  2. 手动恢复容器不见了,调用 curl -v “http://10.125.14.238:3375/schedule/snapshot/restore“ 会将所有异常容器恢复回来
  3. schedule 容器本身的健康检查接口 curl http://10.125.14.238:3375/schedule/leader http code 值是 200,说明schedule容器是健康的
  4. 查询哪个节点是Leader curl 以及是否是停止调度(维护时): “http://10.125.14.238:3375/schedule/leader
  5. 停止调度,先查询谁是leader,然后调: “http://leader-ip:3375/schedule/stop

维护状态

通过调度容器API停止调度,所有容器都不再被调度了,维护完毕再调snapshot、start API恢复调度。

如果只想对某个容器进行维护,其它容器还是希望被调度监控、调度可以通过下面的方式来实现:

docker update --label-rm="ark.labels.schedule=haproxy" 容器1 容器2 //还可以跟多个容器名
然后调 snapshot API让刚刚的update生效

运维完毕,恢复运维后的容器进入可以调度状态,具体命令如下:

docker update --label-add="ark.labels.schedule=haproxy" 容器1 容器2 //还可以跟多个容器名

然后调 snapshot API让刚刚的update生效

image.png

升级ark-schedule步骤:

下载并导入新镜像

下载镜像:http://fzpackages.oss-cn-shanghai.aliyuncs.com/ark%2Fpatch%2Fark-schedule-0.6-20180530-68e7bed.tgz
sudo docker load -i ark-schedule-0.6-20180530-68e7bed.tgz

停止原来的ark-schedule

停止两个crontab(新的ark-schedule自带crontab,每分钟执行一次调度)

停止两个ark-schedule容器

启动新的ark-schdule

在停止的两个ark-schedule的两台机器上启动两个新的ark-schedule容器,启动参数需要修改参考前面的描述(用现场环境信息替换下面的信息)

1
2
3
4
5
6
7
export ACS_CLUSTER_ENDPOINT=10.125.14.238:3376; //跟自己在同一台宿主机的swarm-manager
export ACS_NETWORK_NAME=vlan192; //方舟网络名称 docker network ls 看到vlan开头的名字
export ACS_NETWORK_STORE_CLUSTER=zk://10.125.26.108:2181,10.125.14.238:2181,10.125.1.45:2181/Cluster; //方舟zk集群,同部署的ark.properties中的
export ACS_CRONTAB="*/7 * * * * *" ----不需要改
export ACS_PORT="3375" //schedule 自身api暴露端口----不需要改
export ACS_ADVERTISE="10.125.14.238:3375" //宿主机ip+自身api暴露端口 多个schedule容器唯一
./ark-schedule --debug start //----不需要改

检查调度日志

检查两个ark-schedule 谁是主: curl http://ark-schedule所在的宿主机-ip:3375/schedule/leader

进到是主的ark-schedule容器中看日志:cat /root/logs/ark-schedule-2018-日期.log

参考资料

如何打标签 http://panama.alibaba-inc.com/qa/faq?id=1124

磁盘爆掉的几种情况

  1. 系统磁盘没有空间,解决办法:删掉 /var/log/ 下边的带日期的日志,清空 /var/log/messages 内容
  2. 容器使用的大磁盘空间不够,又有三个地方会使用大量的磁盘
    • 容器内部日志非常大,处理办法见方法一
    • 容器内部产生非常多或者非常大的文件,但是这个文件的位置又通过volume 挂载到了物理机上,处理办法见方法二
    • 对特别老的部署环境,还有可能是容器的系统日志没有限制大小,处理办法见方法三

现场的同学按如下方法依次检查

方法零: 检查系统根目录下每个文件夹的大小

sudo du / -lh --max-depth=1 --exclude=overlay --exclude=proc

看看除了容器之外有没有其它目录使用磁盘特别大,如果有那么一层层进去通过du命令来查看,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#sudo du / -lh --max-depth=1 --exclude=overlay --exclude=proc
16K /dev
16K /lost+found
4.0K /media
17G /home
136M /boot
832K /run
1.9G /usr
75M /tmp
12K /log
8.5G /var
4.0K /srv
0 /proc
22M /etc
84G /root
4.0K /mnt
508M /opt
0 /sys
112G /

那么这个案例中应该查看 /root下为什么用掉了84G(总共用了112G), 先 cd /root 然后执行: sudo du . -lh –max-depth=1 –exclude=overlay 进一步查看 /root 目录下每个文件夹的大小

如果方法零没找到占用特别大的磁盘文件,那么一般来说是容器日志占用太多的磁盘空间,请看方法一

方法一: 容器内部日志非常大(请确保先按方法零检查过了)

在磁盘不够的物理机上执行如下脚本:

1
2
3
4
5
6
7
8
sudo docker ps -a -q >containers.list

sudo cat containers.list | xargs sudo docker inspect $1 | grep merged | awk -F \" '{ print $4 }' | sed 's/\/merged//g' | xargs sudo du --max-depth=0 $1 >containers.size

sudo paste containers.list containers.size | awk '{ print $1, $2 }' | sort -nk2 >real_size.log

sudo tail -10 real_size.log | awk 'BEGIN {print "\tcontainer size\tunit"} { print NR":\t" $0"\t kB" }'

执行完后会输出如下格式:
1
2
3
4
5
6
7
8
9
10
11
12
13
   	container     size	unit
1: 22690f16822f 3769980 kb
2: 82b4ae98eeed 4869324 kb
3: 572a1b7c8ef6 10370404 kb
4: 9f9250d98df6 10566776 kb
5: 7fab70481929 13745648 kb
6: 4a14b58e3732 29873504 kb
7: 8a01418b6df2 30432068 kb
8: 83dc85caaa5c 31010960 kb
9: 433e51df88b1 35647052 kb
10: 4b42818a8148 61962416 kb


第二列是容器id,第三列是磁盘大小,第四列是单位, 占用最大的排在最后面

然后进到容器后通过 du / –max-depth=2 快速发现大文件

方法二: 容器使用的volume使用过大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$sudo du -l /data/lib/docker/defaultVolumes --max-depth=1 | sort -rn
456012884 /data/lib/docker/defaultVolumes
42608332 /data/lib/docker/defaultVolumes/task_3477_g0_ark-metadb_miniDBPaaS-MetaDB_1
32322220 /data/lib/docker/defaultVolumes/task_3477_g0_dbpaas-metadb_dbpaas_1
27461120 /data/lib/docker/defaultVolumes/task_3001_g0_ark-metadb_miniDBPaaS-MetaDB_1
27319360 /data/lib/docker/defaultVolumes/task_36000_g0_ark-metadb_miniDBPaaS-MetaDB
27313836 /data/lib/docker/defaultVolumes/task_3600_g0_dbpaas-metadb_minidbpaas
27278692 /data/lib/docker/defaultVolumes/task_3604_g0_ark-metadb_miniDBPaaS-MetaDB_1
27277004 /data/lib/docker/defaultVolumes/task_3603_g0_ark-metadb_miniDBPaaS-MetaDB_1
27275736 /data/lib/docker/defaultVolumes/task_3542_g0_ark-metadb_miniDBPaaS-MetaDB
27271428 /data/lib/docker/defaultVolumes/task_3597_g0_ark-metadb_miniDBPaaS-MetaDB
27270840 /data/lib/docker/defaultVolumes/task_3603_g0_dbpaas-metadb_minidbpaas_1
27270492 /data/lib/docker/defaultVolumes/task_3603_g0_dbpaas-metadb_minidbpaas
27270468 /data/lib/docker/defaultVolumes/task_3600_g0_ark-metadb_miniDBPaaS-MetaDB
27270252 /data/lib/docker/defaultVolumes/task_3535_g0_ark-metadb_miniDBPaaS-MetaDB
27270244 /data/lib/docker/defaultVolumes/task_3538_g0_ark-metadb_miniDBPaaS-MetaDB
27270244 /data/lib/docker/defaultVolumes/task_3536_g0_ark-metadb_miniDBPaaS-MetaDB
25312404 /data/lib/docker/defaultVolumes/task_3477_g0_dncs-server_middleware-dncs_2

/data/lib/docker/defaultVolumes 参数是方舟默认volume存放的目录(一般是docker的存储路径下 –graph=/data/lib/docker) ,第一列是大小,后面是容器名

volume路径在物理机上也有可能是 /var/lib/docker 或者 /mw/mvdocker/ 之类的路径下,这个要依据安装参数来确定,可以用如下命令来找到这个路径:

sudo systemctl status docker -l | grep --color graph

结果如下,红色参数后面的路径就是docker 安装目录,到里面去找带volume的字眼:

image.png

找到 volume很大的文件件后同样可以进到这个文件夹中执行如下命令快速发现大文件:

du . --max-depth=2

方法三 容器的系统日志没有限制大小

这种情况只针对2017年上半年之前的部署环境,后面部署的环境默认都控制了这些日志不会超过150M

按照方法二的描述先找到docker 安装目录,cd 进去,然后 :

du ./containers --max-depth=2

就很快找到那个大json格式的日志文件了,然后执行清空这个大文件的内容:

echo '' | sudo tee 大文件名

一些其他可能占用空间的地方

  • 机器上镜像太多,可以删掉一些没用的: sudo docker images -q | xargs sudo docker rmi
  • 机器上残留的volume太多,删:sudo docker volume ls -q | xargs sudo docker volume rm
  • 物理文件被删了,但是还有进程占用这个文件句柄,导致文件对应的磁盘空间没有释放,检查: lsof | grep deleted 如果这个文件非常大的话,只能通过重启这个进程来真正释放磁盘空间

检查是否restart能支持只重启deamon,容器还能正常运行:

1
2
3
$sudo docker info | grep Restore
Live Restore Enabled: true

0%