TCP
TCP 头
选项
TCP 分段
在传输层(TCP协议)里,叫分段。在网络层(IP层),叫分片 (Fragmentation)
数据链路层,长度范围是 [64 byte, 1518 byte] 其中 18 byte 的数据是链路层本身的数据。具体格式:
6 byte 目标 MAC 地址 + 6 byte 源 MAC 地址 + 2 byte 类型 + 网络层的数据(IP 包) + 4 byte 校验和
留给网络层的数据长度是 [64 - 18, 1518 - 18] = [46, 1500]
。我们把 1500 这个值,称为 以太网 的 MTU :Maximum Transmission Unit 。
网络层的 IP 协议,也会自动填充协议头,协议头一般占用了 20 byte。也就是说留给传输层数据 只有:[46-20, 1500-20] = [26, 1480]
传输层的 TCP 协议头占了 20 byte, UDP 协议头占了 8 byte。TCP 可以传的用户数据实际上只有 [26 - 20, 1480 - 20] = [6, 1460]
对于 TCP 而言,MSS(Maxitum Segment Size)最大分段大小,它是 TCP 协议里面的一个概念。MSS 是 TCP 用来限制应用层最大的发送字节数。假设 MTU = 1500 byte,那么 MSS = 1500 - 20(IP Header) - 20 (TCP Header) = 1460 byte,如果应用层有 2000 byte 发送,那么需要两个切片才可以完成发送,第一个 TCP 切片 = 1460,第二个 TCP 切片 = 540。TCP 在建立连接的时候,会协商双方的 MSS 值,通常这个 MSS 会控制在 MTU 以内:最大 IP 包大小减去 IP 和 TCP 协议头的大小。(其最终目的:就是尽量避免 IP 分片)。这样 TCP 就可以在自己这一层,把用户发送的数据,预先分成多个大小限制在 MTU 里的 TCP 包。每个 TCP 的分片包,都完整了包含了 TCP 头信息,方便在接收方重组。如果某些情况导致:已经分好的 TCP 分片,还是大于了 MTU,那就在 IP 层中,再执行一次分片。这个时候如果数据丢了,那也只需要重传这一个 TCP 的分片,而不是整个原始的 50k 数据。
每次执行 TCP 发送消息的函数时,会重新计算一次 MSS,再进行分段操作。
IP 层会分片,TCP 还要分段的原因是丢包后,重传的是 TCP 层不分段的数据。如果TCP把这份数据,分段为 N 个小于等于MSS长度的数据包,到了IP层后加上IP头和TCP头,还是小于MTU,那么IP层也不会再进行分包。此时在传输路上发生了丢包,那么TCP重传的时候也只是重传那一小部分的MSS段。效率会比TCP不分段时更高。
UDP 本身不会分段,所以当数据量较大时,只能交给IP层去分片,然后传到底层进行发送。
TCP分段了,IP层就一定不会分片了吗?整个传输链路中,可能还会有其他网络层设备,而这些设备的 MTU 可能小于发送端的 MTU。此时虽然数据包在发送端已经分段过了,但是在IP层就还会再分片一次。如果链路上还有设备有更小的MTU,那么还会再分片,最后所有的分片都会在接收端处进行组装。
如何让 IP 层不再分片:整个链路上,最小的 MTU,就叫 PMTU(path MTU)。cat /proc/sys/net/ipv4/ip_no_pmtu_disc
如果是 0,说明开启了 Path MTU Discovery 功能。IP 数据报头中标红的标志位 DF(Don’t Fragment),当它置为 1,意味着这个IP报文不分片。当链路上某个路由器,收到了这个报文,当IP报文长度大于路由器的MTU时,路由器会看下这个IP报文的DF。如果为0(允许分片),就会分片并把分片后的数据传到下一个路由器;如果为1,就会把数据丢弃,同时返回一个ICMP
包给发送端,并告诉它数据不可达,需要分片,同时带上当前机器的MTU。发送端收到这个ICMP消息,会更新自己的MTU,同时记录到一个PMTU表中。因为TCP的可靠性,会尝试重传这个消息,同时以这个新MTU值计算出MSS进行分段,此时新的IP包就可以顺利被刚才的路由器转发。
本节参考动图图解!既然IP层会分片,为什么TCP层也还要分段?
三次握手
三次握手恰好可以保证 Client 和 Server 对自己的发送、接受能力做了一次确认。
connect
和bind
区别?
Socket 层面的,是如何和三次握手的过程对应的?
四次挥手
TIME_WAIT 状态的设计
为什么不直接进入 CLOSED 状态,而是中间有一个 TIME_WAIT 状态 ?
4元组 (客户端 IP、客户端 Port、服务器 IP、服务器 Port),无法区分出新连接还是老连接,这会导致老连接上的数据包会串到新的连接上来。
TCP/IP 定义了一个值 MSL,表示数据包在网络上最长逗留时间,默认是 120s。连接保持在 TIME_WAIT 状态,再等待 2 × MSL 时间就可以进入 CLOSED 状态了,防止串数据。
为什么是两倍的 MSL ?
因为上图第 4 次发送的数据包,服务器是否收到是不确定的。而如果服务器没有收到,那么服务器会重新发送第三次的数据包。所以整个的时间,最长是两个 MSL。
为何服务器收到第 4 个 ACK 之后,立刻进入 CLOSED 状态,而不是也进入 TIME_WAIT 状态 ?
没有必要,因为客户端处于 TIME_WAIT 状态,意味着四元组代表的这个连接要到 2 × MSL 时间之后才能重新启用,服务器即使想要立即连接也无法实现。
大量 TIME_WAIT 状态
如果存在大量的 TIME_WAIT
,往往是因为短连接太多,不断的创建连接,然后释放连接,从而导致很多连接在这个状态,可能会导致无法发起新的连接。解决的方式往往是:
- 打开
tcp_tw_recycle
和tcp_timestamps
选项; - 打开
tcp_tw_reuse
和tcp_timestamps
选项; - 程序中使用
SO_LINGER
,应用强制使用rst
关闭。
TCP 定时器
引用自 《TCP IP Protocol Suite 第4版》
多数 TCP 协议的实现都至少有如下 4 个定时器:
重传定时器
TCP 发送 segment 的时候,定时器开始计时,当超时(超时时间 RTO: retransmission time-out)后依然没有 ACK,那么会重新发送 segment,那么定时器重新开始计时。
计算 RTO 需要先计算 RTT。
Persistence 定时器
如果接受窗口大小变为 0,发送窗口将停止发送 segments,直到接受窗口返回一个非 0 的 ACK segment。不过这个 segment 可能会丢失。为了防止两边互相等待造成的死锁,当发送端收到了窗口置为 0 的 ACK 后,它启动 Persistence 定时器,当这个定时器 goes off 的时候,它会发送 1 字节的特殊的称为 probe 的 segment。probe 将会让接收端重新发送 ACK。
定时器的时间:
RTO -> 2*RTO -> 4*RTO -> 60s -> 60s -> 60s -> ...
Keepalive 定时器
Server 如果 2 小时之内没有收到任何来自 Client 的数据,Server 将会发送一个 probe
,10 个相隔 75s 的 probe
之后如果依然没有响应,那么会关闭这条连接。
TIME-WAIT 定时器
2MSL 定时器用于连接关闭的时候。
起始序列号 ISN
ISN 为什么不从 1 开始
client 第一次尝试建立连接: client 发送 1,2,3 数据包给 server,我们假设这个时候 3 这个数据包丢失了。紧接着 client 第二次尝试建立连接: client 发送 1,2 给 server,主观上并没有想发送 3 这个数据包,但是第一次的 3 数据包可能又会回来发送给 server,因此造成数据错误)。
起始序列号 ISN 如何计算
起始 ISN
是基于时钟的,每 4 毫秒加一,转一圈要 4.55 个小时。
TCP 初始化序列号不能设置为一个固定值,因为这样容易被攻击者猜出后续序列号,从而遭到攻击。 RFC1948 中提出了一个较好的初始化序列号 ISN 随机生成算法。
ISN = M + F (localhost, localport, remotehost, remoteport)
M
是一个计时器,这个计时器每隔 4 毫秒加 1。F
是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。
特点
滑动窗口
顺序问题、丢包问题、流量控制都是通过滑动窗口来解决的。
发送端维持如下数据结构:
接收端维持如下数据结构:
丢包算法
某个包发送出去,没有收到 ACK,那么应该怎么办?
一种方法就是超时重试,也即对每一个发送了,但是没有 ACK 的包,都有设一个定时器,超过了一定的时间,就重新尝试。但是这个超时的时间如何评估呢?这个时间不宜过短,时间必须大于往返时间 RTT,否则会引起不必要的重传。也不宜过长,这样超时时间变长,访问就变慢了。
估计往返时间,需要 TCP 通过采样 RTT 的时间,然后进行加权平均,算出一个值,而且这个值还是要不断变化的,因为网络状况不断地变化。除了采样 RTT,还要采样 RTT 的波动范围,计算出一个估计的超时时间。由于重传时间是不断变化的,我们称为自适应重传算法(Adaptive Retransmission Algorithm)。
每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢?
有一个可以快速重传的机制,当接收方收到一个序号大于下一个所期望的报文段时,就会检测到数据流中的一个间隔,于是它就会发送冗余的 ACK,仍然 ACK 的是期望接收的报文段。而当客户端收到三个冗余的 ACK 后,就会在定时器过期之前,重传丢失的报文段。
例如,接收方发现 6 收到了,8 也收到了,但是 7 还没来,那肯定是丢了,于是发送 6 的 ACK,要求下一个是 7。接下来,收到后续的包,仍然发送 6 的 ACK,要求下一个是 7。当客户端收到 3 个重复 ACK,就会发现 7 的确丢了,不等超时,马上重发。
还有一种方式称为 Selective Acknowledgment (SACK)。这种方式需要在 TCP 头里加一个 SACK 的东西,可以将缓存的地图发送给发送方。例如可以发送 ACK6、SACK8、SACK9,有了地图,发送方一下子就能看出来是 7 丢了。
流量控制
在对于包的确认中,同时会携带一个窗口的大小。如果接收方实在处理的太慢,导致缓存中没有空间了,可以通过确认信息修改窗口的大小,甚至可以设置为 0,则发送方将暂时停止发送。
如果这样的话,发送方会定时发送窗口探测数据包,看是否有机会调整窗口的大小。当接收方比较慢的时候,要防止低能窗口综合症,别空出一个字节来就赶快告诉发送方,然后马上又填满了,可以当窗口太小的时候,不更新窗口,直到达到一定大小,或者缓冲区一半为空,才更新窗口。
这就是我们常说的流量控制。
拥塞控制
TCP 的拥塞控制主要来避免两种现象,包丢失和超时重传。一旦出现了这些现象就说明,发送速度太快了,要慢一点。但是一开始我怎么知道速度多快呢,我怎么知道应该把窗口调整到多大呢?
一条 TCP 连接开始,cwnd
设置为一个报文段,一次只能发送一个;当收到这一个确认的时候,cwnd
加一,于是一次能够发送两个;当这两个的确认到来的时候,每个确认 cwnd
加一,两个确认 cwnd
加二,于是一次能够发送四个;当这四个的确认到来的时候,每个确认 cwnd
加一,四个确认 cwnd
加四,于是一次能够发送八个。可以看出这是指数性的增长。
涨到什么时候是个头呢?有一个值 ssthresh
为 65535
个字节,当超过这个值的时候,就要小心一点了,不能倒这么快了,可能快满了,再慢下来。
每收到一个确认后,cwnd 增加 1/cwnd,我们接着上面的过程来,一次发送八个,当八个确认到来的时候,每个确认增加 1/8,八个确认一共 cwnd 增加 1,于是一次能够发送九个,变成了线性增长。
但是线性增长还是增长,还是越来越多,直到有一天,水满则溢,出现了拥塞,这时候一般就会一下子降低倒水的速度,等待溢出的水慢慢渗下去。
拥塞的一种表现形式是丢包,需要超时重传,这个时候,将 sshresh
设为 cwnd/2
,将 cwnd
设为 1,重新开始慢启动。这真是一旦超时重传,马上回到解放前。但是这种方式太激进了,将一个高速的传输速度一下子停了下来,会造成网络卡顿。
前面我们讲过快速重传算法。当接收端发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,cwnd 减半为 cwnd/2
,然后 sshthresh = cwnd
,当三个包返回的时候,cwnd = sshthresh + 3
,也就是没有一夜回到解放前,而是还在比较高的值,呈线性增长。
就像前面说的一样,正是这种知进退,使得时延很重要的情况下,反而降低了速度。但是如果你仔细想一下,TCP 的拥塞控制主要来避免的两个现象都是有问题的。
- 第一个问题是丢包并不代表着通道满了,也可能是管子本来就漏水。例如公网上带宽不满也会丢包,这个时候就认为拥塞了,退缩了,其实是不对的。
- 第二个问题是 TCP 的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。其实 TCP 只要填满管道就可以了,不应该接着填,直到连缓存也填满。
为了优化这两个问题,后来有了 TCP BBR 拥塞算法。它企图找到一个平衡点,就是通过不断地加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样时延会增加,在这个平衡点可以很好的达到高带宽和低时延的平衡。
长肥管道问题
当网络的RTT非常大,已知光速大概30wkm/s,如果是跨国网络,距离非常远。RTT很大,这样一来。来回的ack时间就很长,如果出现丢包马上就会变成ssthresh
减半,慢启动状态。窗口恢复非常困难。
tcp 窗口大小就会是这样的曲线:
如何解决这种问题:
调大接收端的 tcp_rmem
和 发送端 tcp_wmem
中间那个缺省值, 让有足够的数据在tcp信道上飘着,就能充分利用信道带宽。
echo "4096 52420000 62910000" > /proc/sys/net/ipv4/tcp_rmem
然后再抓包发现初始化的接收窗口确实变大了,用wireshark
看起来没有变大是因为忽略了后面的WS字段,这个是窗口放大倍数,所以长肥管道为了保证有足够的数据在通信信道上跑,就要把接收端的recv buf
设置非常大。
异常情况如何响应
出现如下异常情况,TCP 会如何响应。
Server 端口没有打开
内核给 Client 响应一个 RST
Server 所在的机器都没有开机
Client 等待 6s 重发一个 SYN
,无响应则等待 24s 再发一个,若总共等待 75s 之后,则触发 ETIMEDOUT
。通过 setConnectTimeout()
来改善这个等待时间,使其可以在合理的毫秒以内返回。
Server 宕机
客户端持续重传,并且 read
调用会阻塞,一段时间后 TIMEOUT
,使用 setReadTimeout()
来改善这个阻塞时间。
Server 宕机又重启
收到了一个这个连接上不该有的,所以响应一个 RST
。
什么时候发送 RST 响应
当客户端收到 Connection Reset,往往是收到了 TCP 的 RST 消息,RST 消息一般在下面的情况下发送:
- 试图连接一个未被监听的服务端;
- 对方处于
TIME_WAIT
状态,或者连接已经关闭处于CLOSED
状态,或者重新监听 seq num 不匹配; - 发起连接时超时,重传超时,keepalive 超时
- 在程序中使用
SO_LINGER
,关闭连接时,放弃缓存中的数据,给对方发送 RST
RST 包一定有效吗?
RST 包的 seq
必须 in the window
,即是否位于接收方的滑动窗口中的接受窗口之内。
收到 RST,是否一定会立即终止连接?
会立即终止连接。
Linux 服务器最多能支撑多少个 TCP 连接?
Client 最大 TCP 连接
Client每次发起tcp连接请求时,除非绑定端口,通常会让系统选取一个空闲的本地端口(local port),该端口是独占的,不能和其他tcp连接共享。tcp端口的数据类型是unsigned short
,因此本地端口个数最大只有65536
,端口0有特殊含义,不能使用,这样可用端口最多只有65535
,所以在全部作为client端的情况下,最大tcp连接数为65535,这些连接可以连到不同的 server ip。
#vi /etc/sysctl.conf
net.ipv4.ip_local_port_range = 5000 65000
执行 sysctl -p
使之生效。
Server 端最大 TCP 连接
TCP 连接四元组是源IP地址、源端口、目的IP地址和目的端口。任意一个元素发生了改变,那么就代表的是一条完全不同的连接了。拿我的Nginx举例,它的端口是固定使用80。另外我的IP也是固定的,这样目的IP地址、目的端口都是固定的。剩下源IP地址、源端口是可变的。所以理论上我的Nginx上最多可以建立**2的32次方(ip数)×2的16次方(port数)**个连接。这是两百多万亿的一个大数字!!
(1) 每维持一条 TCP 连接,都需要创建一个文件对象:
进程每打开一个文件(linux下一切皆文件,包括socket),都会消耗一定的内存资源。如果有不怀好心的人启动一个进程来无限的创建和打开新的文件,会让服务器崩溃。所以linux系统出于安全角度的考虑,在多个位置都限制了可打开的文件描述符的数量,包括系统级、用户级、进程级。这三个限制的含义和修改方式如下:
- 系统级:当前系统可打开的最大数量,通过
fs.file-max
参数可修改 - 用户级:指定用户可打开的最大数量,修改
/etc/security/limits.conf
- 进程级:单个进程可打开的最大数量,通过
fs.nr_open
参数可修改
(2) 每一条连接,都需要 file, socket 内核对象,占用内存,一个空 TCP 连接就得消耗 3.3KB 左右
4GB 内存维持 100W 条空的长连接是没有问题的。
(3) 发送数据,就需要为 TCP 内核对象开启接受缓冲区,会增加内存开销
接收缓存区大小是可以配置的,通过 sysctl
命令就可以查看。
$ sysctl -a | grep rmem
net.ipv4.tcp_rmem = 4096 87380 8388608
net.core.rmem_default = 212992
net.core.rmem_max = 8388608
其中在 tcp_rmem
中的第一个值是为你们的TCP连接所需分配的最少字节数。该值默认是4K,最大的话8MB之多。也就是说你们有数据发送的时候我需要至少为对应的socket再分配4K内存,甚至可能更大。
TCP分配发送缓存区的大小受参数net.ipv4.tcp_wmem
配置影响。
$ sysctl -a | grep wmem
net.ipv4.tcp_wmem = 4096 65536 8388608
net.core.wmem_default = 212992
net.core.wmem_max = 8388608
在net.ipv4.tcp_wmem
中的第一个值是发送缓存区的最小值,默认也是4K。当然了如果数据很大的话,该缓存区实际分配的也会比默认值大。
超过最大连接数
如果你的项目中支持高并发,或者是测试过比较多的并发连接。那么相信你一定遇到过“Too many open files”这个错误。
这个错误的出现其实是正常的,因为每打开一个文件(包括socket),都需要消耗一定的内存资源。为了避免个别进程不受控制地打开了过多的文件而让整个服务器崩溃,Linux 对打开的文件描述符数量有限制。
其实在 Linux 上能打开多少个文件,限制有两种:
- 第一种,进程级别的,限制的是单个进程上可打开的文件数。具体参数是
soft nofile
和fs.nr_open
。它们两个的区别是soft nofile
可以不同用户配置不同的值。而fs.nr_open
在一台 Linux 上只能配一次。 - 第二种,系统级别的,整个系统上可打开的最大文件数,具体参数是
fs.file-max
。但是这个参数不限制 root 用户。
另外这几个参数之间还有耦合关系,因此还要注意以下三点:
- 1、如果你想加大
soft nofile
, 那么hard nofile
也需要一起调整。因为如果hard nofile
设置的低, 你的soft nofile
设置的再高都没用,实际生效的值会按二者里最低的来。 - 2、如果你加大了
hard nofile
,那么fs.nr_open
也都需要跟着一起调整。如果不小心把hard nofile
设置的比fs.nr_open
大了,后果比较严重。会导致该用户无法登陆。如果设置的是*
的话,那么所有的用户都无法登陆。 - 3、还要注意如果你加大了
fs.nr_open
,但是用的是echo "xx" > ../fs/nr_open
的方式,刚改完你可能觉得没问题。只要机器一重启你的fs.nr_open
设置就会失效,还是会无法登陆。
假如你想让你的进程可以打开 100 万个文件描述符,我觉得比较稳妥点的修改方法是干脆都直接用 conf
文件的方式来改。这样比较统一,也比较安全。
# vi /etc/sysctl.conf
fs.nr_open=1100000 //要比 hard nofile 大一点
fs.file-max=1100000 //多留点buffer
# sysctl -p
# vi /etc/security/limits.conf
* soft nofile 1000000
* hard nofile 1000000
查看当前活动的 TCP 连接数
$ ss -n | grep ESTAB | wc -l
1000024
TCP 是否丢包
请参考Linux 丢包那些事
攻击
Dos 攻击
DoS:是Denial of Service的简称,即拒绝服务,造成DoS的攻击行为被称为DoS攻击,其目的是使计算机或网络无法提供正常的服务。最常见的DoS攻击有计算机网络带宽攻击和连通性攻击。
SYN FLOOD
。- 利用 RST 位来实现。假设现在有一个合法用户(1.1.1.1)已经同服务器建立了正常的连接,攻击者构造攻击的TCP数据,伪装自己的IP为1.1.1.1,并向服务器发送一个带有RST位的TCP数据段。服务器接收到这样的数据后,认为从1.1.1.1发送的连接有错误,就会清空缓冲区中建立好的连接。这时,如果合法用户1.1.1.1再发送合法数据,服务器就已经没有这样的连接了,该用户就必须从新开始建立连接。攻击时,伪造大量的IP地址,向目标发送RST数据,使服务器不对合法用户服务。
DDos 攻击
DDoS即Distributed Denial of Service,DDoS攻击即是分布式DoS攻击。攻击者借助客户/服务器技术,控制并利用多个傀儡机(肉鸡),对一个或多个攻击目标发起DDoS攻击,从而成倍地提高DoS攻击的威力。
DDOS 只不过是一个概称,其下有各种攻击方式,比如“CC攻击、SYN攻击、NTP攻击、TCP攻击、DNS攻击等等”,现在DDOS发展变得越来越可怕,NTP攻击渐渐成为主流了,这意味着可以将每秒的攻击流量放大几百倍,比如每秒1G的SYN碎片攻击换成NTP放大攻击,就成为了200G或者更多。
- CC 攻击。CC 攻击是目前应用层攻击的主要手段之一,借助代理服务器生成指向目标系统的合法请求,实现伪装和 DDoS。CC 攻击模拟多个正常用户不停地访问如论坛这些需要大量数据操作的页面,造成服务器资源的浪费,CPU长时间处于100%,永远都有处理不完的请求,网络拥塞,正常访问被中止。这种攻击技术性含量高,见不到真实源IP,见不到特别大的异常流量,但服务器就是无法进行正常连接。
CC攻击中最重要的方式之一HTTP Flood,不仅会直接导致被攻击的Web前端响应缓慢,对承载的业务造成致命的影响,还可能会引起连锁反应,间接攻击到后端的Java等业务层逻辑以及更后端的数据库服务。
- DNS 攻击
- NTP 攻击
NTP是标准的基于UDP协议传输的网络时间同步协议,由于UDP协议的无连接性,方便伪造源地址。攻击者使用特殊的数据包,也就是IP地址指向作为反射器的服务器,源IP地址被伪造成攻击目标的IP,反射器接收到数据包时就被骗了,会将响应数据发送给被攻击目标,耗尽目标网络的带宽资源。一般的NTP服务器都有很大的带宽,攻击者可能只需要1Mbps的上传带宽欺骗NTP服务器,就可给目标服务器带来几百上千Mbps的攻击流量。
- ICMP Flood
ICMP(Internet控制报文协议)用于在IP主机、路由器之间传递控制消息,控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息,虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。通过对目标系统发送海量数据包,就可以令目标主机瘫痪,如果大量发送就成了洪水攻击。
DRDoS 攻击
DRDoS 即 Distributed Reflection Denial of Service,DRDoS攻击即是分布式反射DoS攻击。DRDoS攻击与DoS攻击、DDoS攻击不同,它不是直接对攻击目标发起请求,而是通过IP欺骗,发送大量源IP地址为攻击目标IP地址的数据包给攻击主机(第三方),然后攻击主机对IP地址源(即攻击目标)做出大量回应,从而形成DoS攻击。攻击者往往会选择那些响应包远大于请求包的第三方服务来利用,以达到放大反射攻击的效果,这样的服务包括DNS
、NTP
、SSDP
、Chargen
、Memcached
等。
SYN Flood
攻击者大量发送这种伪造源地址的 SYN 请求,服务器端将会消耗非常多的资源来处理这种半连接,保存遍历会消耗非常多的CPU时间和内存,何况还要不断对这个列表中的IP进行SYN+ACK
的重试。TCP是可靠协议,这时就会重传报文,默认重试次数为5次,重试的间隔时间从1s开始每次都番倍,分别为1s + 2s + 4s + 8s +16s = 31s,第5次发出后还要等32s才知道第5次也超时了,所以一共是31 + 32 = 63s。
也就是说一共假的syn报文,会占用TCP准备队列63s之久,而半连接队列默认为1024,系统默认不同,可 cat /proc/sys/net/ipv4/tcp_max_syn_backlog c查看。也就是说在没有任何防护的情况下,每秒发送200个伪造syn包,就足够撑爆半连接队列,从而使真正的连接无法建立,无法响应正常请求。
如何检测 Syn Flood 攻击 ?
首先使用 tcpdump
抓包:
$ tcpdump -i eth0 -n tcp port 80
09:15:48.287047 IP 192.168.0.2.27095 > 192.168.0.30: Flags [S], seq 1288268370, win 512, length 0
09:15:48.287050 IP 192.168.0.2.27131 > 192.168.0.30: Flags [S], seq 2084255254, win 512, length 0
...
-i
只抓取 eth0-n
不解析协议名和主机名tcp port 80
只抓取 tcp 协议端口号为 80 的网络帧
你能看到,此时,服务器端的 TCP 连接,会处于 SYN_RECEIVED
状态。这其实提示了我们,查看 TCP 半开连接的方法,关键在于 SYN_RECEIVED
状态的连接。我们可以使用 netstat ,来查看所有连接的状态,不过要注意,SYN_REVEIVED
的状态,通常被缩写为 SYN_RECV
。
$ netstat -n -p | grep SYN_REC
tcp 0 0 192.168.0.30:80 192.168.0.2:12503 SYN_RECV -
tcp 0 0 192.168.0.30:80 192.168.0.2:13502 SYN_RECV -
tcp 0 0 192.168.0.30:80 192.168.0.2:15256 SYN_RECV -
tcp 0 0 192.168.0.30:80 192.168.0.2:18117 SYN_RECV -
...
-n
表示不解析名字,-p
表示显示连接所属进程
从结果中,你可以发现大量 SYN_RECV
状态的连接,并且源 IP 地址为 192.168.0.2
。
如何抵抗 Syn Flood ?
可以使用 iptables
过滤来自 192.168.0.2
的包:
$ iptables -I INPUT -s 192.168.0.2 -p tcp -j REJECT
如果源 IP 不固定,那么可以限制 syn
的并发数:
# 限制 syn 并发数为每秒 1 次
$ iptables -A INPUT -p tcp --syn -m limit --limit 1/s -j ACCEPT
# 限制单个 IP 在 60 秒新建立的连接数为 10
$ iptables -I INPUT -p tcp --dport 80 --syn -m recent --name SYN_FLOOD --update --seconds 60 --hitcount 10 -j REJECT
多台机器同时发送 syn flood
如果是多台机器同时发送 SYN Flood,这种方法可能就直接无效了,因为数据量特别大,刚才那种数据量小。因为你很可能无法 SSH 登录(SSH 也是基于 TCP 的)到机器上去,更别提执行上述所有的排查命令。
比如,SYN Flood 会导致 SYN_RECV
状态的连接急剧增大。在上面的 netstat
命令中,你也可以看到 190 多个处于半开状态的连接。
不过,半开状态的连接数是有限制的,执行下面的命令,你就可以看到,默认的半连接容量只有 256:
$ sysctl net.ipv4.tcp_max_syn_backlog
net.ipv4.tcp_max_syn_backlog = 256
换句话说, SYN 包数再稍微增大一些,就不能 SSH 登录机器了。 所以,你还应该增大半连接的容量,比如,你可以用下面的命令,将其增大为 1024:
$ sysctl -w net.ipv4.tcp_max_syn_backlog=1024
net.ipv4.tcp_max_syn_backlog = 1024
另外,连接每个 SYN_RECV
时,如果失败的话,内核还会自动重试,并且默认的重试次数是 5 次。你可以执行下面的命令,将其减小为 1 次:
$ sysctl -w net.ipv4.tcp_synack_retries=1
net.ipv4.tcp_synack_retries = 1
TCP SYN Cookies
除此之外,TCP SYN Cookies 也是一种专门防御 SYN Flood 攻击的方法。SYN Cookies 基于连接信息(包括源地址、源端口、目的地址、目的端口等)以及一个加密种子(如系统启动时间),计算出一个哈希值(SHA1),这个哈希值称为 cookie。
然后,这个 cookie 就被用作序列号,来应答 SYN + ACK
包,并释放连接状态。当客户端发送完三次握手的最后一次 ACK 后,服务器就会再次计算这个哈希值,确认是上次返回的 SYN + ACK
的返回包,才会进入 TCP 的连接状态。
因而,开启 SYN Cookies 后,就不需要维护半开连接状态了,进而也就没有了半连接数的限制。
$ sysctl -w net.ipv4.tcp_syncookies=1
net.ipv4.tcp_syncookies = 1
注意,上述 sysctl 命令修改的配置都是临时的,重启后这些配置就会丢失。所以,为了保证配置持久化,你还应该把这些配置,写入 /etc/sysctl.conf
文件中。比如:
$ cat /etc/sysctl.conf
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_synack_retries = 1
net.ipv4.tcp_max_syn_backlog = 1024
不过要记得,写入 /etc/sysctl.conf
的配置,需要执行 sysctl -p
命令后,才会动态生效。
提升 TCP 性能
客户端建立连接
客户端发送 SYN 开启了三次握手,此时在客户端上用netstat 命令(后续查看连接状态都使用该命令)可以看到连接的状态是 SYN_SENT
。客户端在等待服务器回复的 ACK 报文。正常情况下,服务器会在几毫秒内返回 ACK,但如果客户端迟迟没有收到 ACK 会怎么样呢?客户端会重发 SYN,重试的次数由tcp_syn_retries
参数控制,默认是 6 次:
net.ipv4.tcp_syn_retries = 6
第 1 次重试发生在 1 秒钟后,接着会以翻倍的方式在第 2、4、8、16、32 秒共做 6 次重试,最后一次重试会等待 64 秒,如果仍然没有返回 ACK,才会终止三次握手。所以,总耗时是 1+2+4+8+16+32+64=127 秒,超过 2 分钟。
如果这是一台有明确任务的服务器,你可以根据网络的稳定性和目标服务器的繁忙程度修改重试次数,调整客户端的三次握手时间上限。比如内网中通讯时,就可以适当调低重试次数,尽快把错误暴露给应用程序。
服务器接受连接
当服务器收到 SYN 报文后,服务器会立刻回复 SYN+ACK 报文,既确认了客户端的序列号,也把自己的序列号发给了对方。此时,服务器端出现了新连接,状态是 SYN_RCV
,这个状态下,服务器必须建立一个 SYN 半连接队列来维护未完成的握手信息,当这个队列溢出后,服务器将无法再建立新连接。
新连接建立失败的原因有很多,怎样获得由于队列已满而引发的失败次数呢?netstat -s
命令给出的统计结果中可以得到。
# netstat -s | grep "SYNs to LISTEN"
1192450 SYNs to LISTEN sockets dropped
这里给出的是队列溢出导致 SYN 被丢弃的个数。注意这是一个累计值,如果数值在持续增加,则应该调大 SYN 半连接队列。修改队列大小的方法,是设置 Linux 的 tcp_max_syn_backlog
参数:
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,仅在队列满时再启用。
net.ipv4.tcp_syncookies = 1
当客户端接收到服务器发来的 SYN+ACK 报文后,就会回复 ACK 去通知服务器,同时己方连接状态从 SYN_SENT 转换为 ESTABLISHED,表示连接建立成功。服务器端连接成功建立的时间还要再往后,到它收到 ACK 后状态才变为 ESTABLISHED。
如果服务器没有收到 ACK,就会一直重发 SYN+ACK 报文。当网络繁忙、不稳定时,报文丢失就会变严重,此时应该调大重发次数。反之则可以调小重发次数。修改重发次数的方法是,调整 tcp_synack_retries
参数。
net.ipv4.tcp_synack_retries = 5
tcp_synack_retries
的默认重试次数是 5 次,与客户端重发 SYN 类似,它的重试会经历1、2、4、8、16 秒,最后一次重试后等待 32 秒,若仍然没有收到 ACK,才会关闭连接,故共需要等待 63 秒。
服务器收到 ACK 后连接建立成功,此时,内核会把连接从 SYN 半连接队列中移出,再移入 accept
队列,等待进程调用 accept
函数时把连接取出来。如果进程不能及时地调用 accept
函数,就会造成 accept
队列溢出,最终导致建立好的 TCP 连接被丢弃。
实际上,丢弃连接只是 Linux 的默认行为,我们还可以选择向客户端发送 RST
复位报文,告诉客户端连接已经建立失败。打开这一功能需要将 tcp_abort_on_overflow
参数设置为1。
net.ipv4.tcp_abort_on_overflow = 0
通常情况下,应当把 tcp_abort_on_overflow
设置为 0,因为这样更有利于应对突发流量。举个例子,当 accept 队列满导致服务器丢掉了 ACK,与此同时,客户端的连接状态却是 ESTABLISHED,进程就在建立好的连接上发送请求。只要服务器没有为请求回复ACK,请求就会被多次重发。如果服务器上的进程只是短暂的繁忙造成 accept 队列满,那么当 accept 队列有空位时,再次接收到的请求报文由于含有 ACK,仍然会触发服务器端成功建立连接。所以,tcp_abort_on_overflow
设为 0 可以提高连接建立的成功率,只有你非常肯定 accept 队列会长期溢出时,才能设置为 1 以尽快通知客户端。
那么,怎样调整 accept 队列的长度呢?listen 函数的 backlog
参数就可以设置 accept 队列的大小。事实上,backlog 参数还受限于 Linux 系统级的队列长度上限,当然这个上限阈值也可以通过 somaxconn
参数修改。
net.core.somaxconn = 128
当下各监听端口上的 accept 队列长度可以通过 ss -ltn
命令查看,但 accept 队列长度是否需要调整该怎么判断呢?还是通过 netstat -s
命令给出的统计结果,可以看到究竟有多少个连接因为队列溢出而被丢弃。
# netstat -s | grep "listen queue"
14 times the listen queue of a socket overflowed
如果持续不断地有连接因为 accept 队列溢出被丢弃,就应该调大 backlog 以及 somaxconn
参数。
半连接全连接队列完整图
Tcp Fast Open
三次握手建立连接造成的后果就是,HTTP 请求必须在一次 RTT(Round Trip Time,从客户端到服务器一个往返的时间)后才能发送。因此,Google 提出了 TCP fast open 方案,客户端可以在首个 SYN 报文中就携带请求,这节省了 1 个 RTT 的时间。
为了让客户端在 SYN 报文中携带请求数据,必须解决服务器的信任问题。因为此时服务器的 SYN 报文还没有发给客户端,客户端是否能够正常建立连接还未可知,但此时服务器需要假定连接已经建立成功,并把请求交付给进程去处理,所以服务器必须能够信任这个客户端。
TFO 到底怎样达成这一目的呢?它把通讯分为两个阶段,第一阶段为首次建立连接,这时走正常的三次握手,但在客户端的 SYN 报文会明确地告诉服务器它想使用 TFO 功能,这样服务器会把客户端 IP 地址用只有自己知道的密钥加密(比如 AES 加密算法),作为 Cookie 携带在返回的 SYN+ACK 报文中,客户端收到后会将 Cookie 缓存在本地。
之后,如果客户端再次向服务器建立连接,就可以在第一个 SYN 报文中携带请求数据,同时还要附带缓存的 Cookie。很显然,这种通讯方式下不能再采用经典的“先 connect
再 write
请求”这种编程方法,而要改用 sendto
或者 sendmsg
函数才能实现。
服务器收到后,会用自己的密钥验证 Cookie 是否合法,验证通过后连接才算建立成功,再把请求交给进程处理,同时给客户端返回 SYN+ACK。虽然客户端收到后还会返回 ACK,但服务器不等收到 ACK 就可以发送 HTTP 响应了,这就减少了握手带来的 1 个 RTT 的时间消耗。
当然,为了防止 SYN 泛洪攻击,服务器的 TFO 实现必须能够自动化地定时更新密钥。
Linux 下怎么打开 TFO 功能呢?这要通过 tcp_fastopen
参数。由于只有客户端和服务器同时支持时,TFO 功能才能使用,所以 tcp_fastopen
参数是按比特位控制的。其中,第1 个比特位为 1 时,表示作为客户端时支持 TFO;第 2 个比特位为 1 时,表示作为服务器时支持 TFO,所以当 tcp_fastopen
的值为 3 时(比特为 0x11)就表示完全支持 TFO 功能。
net.ipv4.tcp_fastopen = 3
主动关闭连接
互联网中往往服务器才是主动关闭连接的一方。这是因为,HTTP 消息是单向传输协议,服务器接收完请求才能生成响应,发送完响应后就会立刻关闭 TCP 连接,这样及时释放了资源,能够为更多的用户服务。
关闭连接有多种方式,比如进程异常退出时,针对它打开的连接,内核就会发送 RST
报文来关闭。RST 的全称是 Reset 复位的意思,它可以不走四次挥手强行关闭连接,但当报文延迟或者重复传输时,这种方式会导致数据错乱,所以这是不得已而为之的关闭连接方案。
安全关闭连接的方式必须通过四次挥手,它由进程调用 close
或者 shutdown
函数发起,这二者都会向对方发送 FIN
报文(shutdown
参数须传入 SHUT_WR 或者 SHUT_RDWR
才会发送 FIN
),区别在于 close
调用后,哪怕对方在半关闭状态下发送的数据到达主动方,进程也无法接收。
此时,这个连接叫做孤儿连接,如果你用 netstat -p
命令,会发现连接对应的进程名为空。而 shutdown
函数调用后,即使连接进入了 FIN_WAIT1
或者 FIN_WAIT2
状态,它也不是孤儿连接,进程仍然可以继续接收数据。关于孤儿连接的概念,下文调优参数时还会用到。
主动方发送 FIN 报文后,连接就处于 FIN_WAIT1
状态下,该状态通常应在数十毫秒内转为 FIN_WAIT2
。只有迟迟收不到对方返回的 ACK 时,才能用 netstat
命令观察到 FIN_WAIT1
状态。此时,内核会定时重发 FIN 报文,其中重发次数由 tcp_orphan_retries
参数控制(注意,orphan 虽然是孤儿的意思,该参数却不只对孤儿连接有效,事实上,它对所有 FIN_WAIT1
状态下的连接都有效),默认值是 0,特指 8次:
net.ipv4.tcp_orphan_retries = 0
如果 FIN_WAIT1 状态连接有很多,你就需要考虑降低 tcp_orphan_retries
的值。当重试次数达到 tcp_orphan_retries
时,连接就会直接关闭掉。
对于正常情况来说,调低 tcp_orphan_retries
已经够用,但如果遇到恶意攻击,FIN 报文根本无法发送出去。这是由 TCP 的 2 个特性导致的。
首先,TCP 必须保证报文是有序发送的,FIN 报文也不例外,当发送缓冲区还有数据没发送时,FIN 报文也不能提前发送。
其次,TCP 有流控功能,当接收方将接收窗口设为 0 时,发送方就不能再发送数据。所以,当攻击者下载大文件时,就可以通过将接收窗口设为 0,导致 FIN 报文无法发送,进而导致连接一直处于 FIN_WAIT1
状态。
解决这种问题的方案是调整 tcp_max_orphans
参数:
net.ipv4.tcp_max_orphans = 16384
顾名思义,tcp_max_orphans
定义了孤儿连接的最大数量。当进程调用 close 函数关闭连接后,无论该连接是在 FIN_WAIT1
状态,还是确实关闭了,这个连接都与该进程无关了,它变成了孤儿连接。Linux 系统为防止孤儿连接过多,导致系统资源长期被占用,就提供了 tcp_max_orphans
参数。如果孤儿连接数量大于它,新增的孤儿连接将不再走四次挥手,而是直接发送 RST 复位报文强制关闭。
当连接收到 ACK 进入 FIN_WAIT2 状态后,就表示主动方的发送通道已经关闭,接下来将等待对方发送 FIN 报文,关闭对方的发送通道。这时,如果连接是用 shutdown 函数关闭的,连接可以一直处于 FIN_WAIT2 状态。但对于 close 函数关闭的孤儿连接,这个状态不可以持续太久,而 tcp_fin_timeout
控制了这个状态下连接的持续时长。
net.ipv4.tcp_fin_timeout = 60
它的默认值是 60 秒。这意味着对于孤儿连接,如果 60 秒后还没有收到 FIN 报文,连接就会直接关闭。这个 60 秒并不是拍脑袋决定的,它与接下来介绍的 TIME_WAIT 状态的持续时间是相同的,我们稍后再来回答 60 秒的由来。
TIME_WAIT 是主动方四次挥手的最后一个状态。当收到被动方发来的 FIN 报文时,主动方回复 ACK,表示确认对方的发送通道已经关闭,连接随之进入 TIME_WAIT 状态,等待 60秒后关闭,为什么呢?我们必须站在整个网络的角度上,才能回答这个问题。
TIME_WAIT 状态的连接,在主动方看来确实已经关闭了。然而,被动方没有收到 ACK 报文前,连接还处于 LAST_ACK 状态。如果这个 ACK 报文没有到达被动方,被动方就会重发 FIN 报文。重发次数仍然由前面介绍过的 tcp_orphan_retries
参数控制。
如果主动方不保留 TIME_WAIT 状态,会发生什么呢?此时连接的端口恢复了自由身,可以复用于新连接了。然而,被动方的 FIN 报文可能再次到达,这既可能是网络中的路由器重复发送,也有可能是被动方没收到 ACK 时基于 tcp_orphan_retries
参数重发。这样,正常通讯的新连接就可能被重复发送的 FIN 报文误关闭。保留 TIME_WAIT 状态,就可以应付重发的 FIN 报文,当然,其他数据报文也有可能重发,所以 TIME_WAIT 状态还能避免数据错乱。
我们再回过头来看看,为什么 TIME_WAIT
状态要保持 60 秒呢?这与孤儿连接FIN_WAIT2 状态默认保留 60 秒的原理是一样的,因为这两个状态都需要保持 2MSL 时长。MSL 全称是 Maximum Segment Lifetime,它定义了一个报文在网络中的最长生存时间(报文每经过一次路由器的转发,IP 头部的 TTL 字段就会减 1,减到 0 时报文就被丢弃,这就限制了报文的最长存活时间)。
为什么是 2 MSL 的时长呢?这其实是相当于至少允许报文丢失一次。比如,若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT
状态的连接可以应对。为什么不是 4 或者 8 MSL 的时长呢?你可以想象一个丢包率达到百分之一的糟糕网络,连续两次丢包的概率只有万分之一,这个概率实在是太小了,忽略它比解决它更具性价比。
因此,TIME_WAIT
和 FIN_WAIT2
状态的最大时长都是 2 MSL,由于在 Linux 系统中,MSL 的值固定为 30 秒,所以它们都是 60 秒。
虽然 TIME_WAIT
状态的存在是有必要的,但它毕竟在消耗系统资源,比如 TIME_WAIT
状态的端口就无法供新连接使用。怎样解决这个问题呢?
Linux 提供了 tcp_max_tw_buckets
参数,当 TIME_WAIT
的连接数量超过该参数时,新关闭的连接就不再经历 TIME_WAIT
而直接关闭。
net.ipv4.tcp_max_tw_buckets = 5000
当服务器的并发连接增多时,相应地,同时处于 TIME_WAIT
状态的连接数量也会变多,此时就应当调大 tcp_max_tw_buckets
参数,减少不同连接间数据错乱的概率。
当然,tcp_max_tw_buckets
也不是越大越好,毕竟内存和端口号都是有限的。有没有办法让新连接复用 TIME_WAIT
状态的端口呢?如果服务器会主动向上游服务器发起连接的话,就可以把 tcp_tw_reuse
参数设置为 1,它允许作为客户端的新连接,在安全条件下使用 TIME_WAIT
状态下的端口。
net.ipv4.tcp_tw_reuse = 1
当然,要想使 tcp_tw_reuse
生效,还得把 timestamps
参数设置为 1,它满足安全复用的先决条件(对方也要打开 tcp_timestamps
):
net.ipv4.tcp_timestamps = 1
老版本的 Linux 还提供了 tcp_tw_recycle
参数,它并不要求 TIME_WAIT
状态存在 60秒,很容易导致数据错乱,不建议设置为 1。
net.ipv4.tcp_tw_recycle = 0
所以在 Linux 4.12 版本后,直接取消了这一参数。
被动关闭连接
当被动方收到 FIN 报文时,就开启了被动方的四次挥手流程。内核自动回复 ACK 报文后,连接就进入 CLOSE_WAIT 状态,顾名思义,它表示等待进程调用 close 函数关闭连接。
内核没有权力替代进程去关闭连接,因为若主动方是通过 shutdown 关闭连接,那么它就是想在半关闭连接上接收数据。因此,Linux 并没有限制 CLOSE_WAIT 状态的持续时间。
当然,大多数应用程序并不使用 shutdown
函数关闭连接,所以,当你用 netstat
命令发现大量 CLOSE_WAIT
状态时,要么是程序出现了 Bug,read 函数返回 0
时忘记调用 close
函数关闭连接,要么就是程序负载太高,close 函数所在的回调函数被延迟执行了。此时,我们应当在应用代码层面解决问题。
由于 CLOSE_WAIT 状态下,连接已经处于半关闭状态,所以此时进程若要关闭连接,只能调用 close 函数(再调用 shutdown 关闭单向通道就没有意义了),内核就会发出 FIN 报文关闭发送通道,同时连接进入 LAST_ACK 状态,等待主动方返回 ACK 来确认连接关闭。
如果迟迟等不到 ACK,内核就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retries
参数控制,这与主动方重发 FIN 报文的优化策略一致。
至此,由一方主动发起四次挥手的流程就介绍完了。需要你注意的是,如果被动方迅速调用close 函数,那么被动方的 ACK 和 FIN 有可能在一个报文中发送,这样看起来,四次挥手会变成三次挥手,这只是一种特殊情况,不用在意。
我们再来看一种特例,如果连接双方同时关闭连接,会怎么样?
此时,上面介绍过的优化策略仍然适用。两方发送 FIN 报文时,都认为自己是主动方,所以都进入了 FIN_WAIT1 状态,FIN 报文的重发次数仍由 tcp_orphan_retries
参数控制。
下来,双方在等待 ACK 报文的过程中,都等来了 FIN 报文。这是一种新情况,所以连接会进入一种叫做 CLOSING 的新状态,它替代了 FIN_WAIT2 状态。此时,内核回复 ACK确认对方发送通道的关闭,仅己方的 FIN 报文对应的 ACK 还没有收到。所以,CLOSING状态与 LAST_ACK 状态下的连接很相似,它会在适时重发 FIN 报文的情况下最终关闭。
调整拥塞控制
你可以根据网络状况和传输对象的大小,调整初始拥塞窗口的大小。调整前,先要清楚你的服务器现在的初始拥塞窗口是多大。你可以通过 ss
命令查看当前拥塞窗口:
# ss -nli | fgrep cwnd
cubic rto:1000 mss:536 cwnd:10 segs_in:10621866 lastsnd:1716864402 la
再通过 ip route change
命令修改初始拥塞窗口:
# ip route | while read r; do
ip route change $r initcwnd 10;
done
当然,更大的初始拥塞窗口以及指数级的提速,连接很快就会遭遇网络拥塞,从而导致慢启动阶段的结束。
慢启动、拥塞避免、快速重传、快速恢复,共同构成了拥塞控制算法。Linux 上提供了更改拥塞控制算法的配置,你可以通过 tcp_available_congestion_control
配置查看内核支持的算法列表:
net.ipv4.tcp_available_congestion_control = cubic reno
再通过 tcp_congestion_control
配置选择一个具体的拥塞控制算法:
net.ipv4.tcp_congestion_control = cubic
但有件事你得清楚,拥塞控制是控制网络流量的算法,主机间会互相影响,在生产环境更改之前必须经过完善的测试。
Linux 4.9 版本之后都支持 BBR 算法,开启 BBR 算法仍然使用 tcp_congestion_control
配置:
net.ipv4.tcp_congestion_control=bbr
粘包/拆包
看 Netty
的解决方案:
- FixedLengthFrameDecoder
- LineBasedFrameDecoder
- DelimiterBasedFrameDecoder
- LengthFieldBasedFrameDecoder