网络分层
TCP
协议承载HTTP、HTTPS
上层应用协议的数据传输
工作原理
连接
TCP
是通过端口号来保持所有这些连接的正确运行的。
[源IP地址、源端口号] < - - - > [目的IP地址、目的端口号]
通过这四个值唯一地定义了一条连接,两条不同的TCP连接不能拥有4个完全相同的地址组件值,不同连接的部分组件可以拥有相同的值。
举例如下:
连接 |
源地址IP【Client】 |
源端口 |
目的IP地址【Server】 |
目的端口 |
A |
172.16.110.1 |
8081 |
192.168.77.1 |
9090 |
B |
172.16.110.2 |
8077 |
192.168.77.1 |
9040 |
C |
172.16.110.2 |
8079 |
192.168.77.2 |
9050 |
D |
172.16.110.3 |
8044 |
192.168.77.2 |
9050 |
数据块
HTTP
或HTTPS
协议要传送一条报文时,会以流的形式将报文数据的内容通过一条打开的TCP连接按序传输。
TCP收到数据流之后,会将数据流砍成段的小数据块,并讲段封装在IP分组中,通过因特网进行传输。
每个TCP段都是由IP分组承载,从一个IP地址发送到另一个IP地址的。每个IP分组中都包括:
- 一个IP分组首部(通常20字节)
- 包含源地址、目标地址、长度和一些其他标记
- 一个TCP段首部(通常20字节)
- TCP端口号、TCP控制标记,以及用于数据排序和完整性的一些数字值
- 一个TCP数据块(0个或多个字节)
交互
交互流程
状态变迁
建立连接(三次握手)
问题汇总
- 为何三次握手而不是两次握手?
TCP是基于连接的双工协议,需要端到端双向彼此确认完成“握手”后,进入数据传输阶段,因此最少要经历SYN -> SYN+ACK -> ACK三个阶段 - SYN_SENT发送后没有返回ACK如何处理?
Linux参数/proc/sys/net/ipv4/tcp_synack_retries
控制重试次数,默认为5次,重试间隔时间按照二进制指数退让算法分别1s, 2s,4s,8s,16s, 32s,如果经历一分钟后还没有收到ACK则会关闭连接。 - SYN_QUEUE半连接队列控制?
Linux参数/proc/sys/net/ipv4/tcp_max_syn_backlog
控制,最大不要超过65536,一般为16000左右,除了半连接队列还要配合调整/proc/sys/net/core/somaxconn
全连接队列数、Nginx反向代理参数等才可以,这是一个全链路工程,不是单点修改的问题 - SYN FLOOD(SYN洪水攻击) 攻击原理?
利用TCP协议缺陷,发送大量伪造的TCP连接SYN请求,对服务器返回的SYN+ACK不进行响应,从而使得服务器半连接队列占用满无法接收其他正常连接请求,还有就是对于TCP栈不健壮的服务器来说,半连接队列存储和处理大量的连接会导致资源耗尽(CPU满负荷或内存不足),最终导致系统或服务器宕机 - SYN FLOOD攻击防范?
- 缩短SYN Timeout时间。由于SYN Flood攻击的效果取决于服务器上保持的SYN半连接数,这个值=SYN攻击的频度 x SYN Timeout,所以通过缩短从接收到SYN报文到确定这个报文无效并丢弃该连接的时间,例如设置为20秒以下,可以成倍的降低服务器的负荷。但过低的SYN Timeout设置可能会影响客户的正常访问
- 设置SYN Cookie。就是给每一个请求连接的IP地址分配一个Cookie,如果短时间内连续受到某个IP的重复SYN报文,就认定是受到了攻击,并记录地址信息,以后从这个IP地址来的包会被一概丢弃。这样做的结果也可能会影响到正常用户的访问
syn cookie值 |
功能 |
0 |
表示关闭该功能; |
1 |
表示仅当 SYN 半连接队列放不下时,再启用它 |
2 |
表示无条件开启功能 |
- SYN FLOOD是基于IP或者域名的,域名需要解析成IP其本质还是基于IP的,因此对于攻击者突然的流量导致的大量SYN,可以通过备用机器IP进行数据导流解析到不需要的服务器上,然后切换用户的真实访问IP或域名,这是一种被动的临时的解决方案,且在攻击者DNS解析的缓存有效期内可以做到一定的防护作用
- 全连接队列满了怎么办?
/proc/sys/net/ipv4/tcp_abort_on_overflow
控制全连接队列满后的处理方式,共有两个值:
tcp_abort_on_overflow值 |
功能 |
0 |
如果 accept 队列满了,那么 server 扔掉 client 发过来的 ack |
1 |
如果 accept 队列满了,server 发送一个 RST 包给 client,表示废掉这个握手过程和这个连接 |
- 如何查看服务端进程全连接队列的长度?
ss -lnt
- Recv-Q:当前accept 队列的大小,也就是当前已完成三次握手并等待服务端 accept() 的 TCP 连接
- Send-Q:accept 队列最大长度,上面的输出结果说明监听端口的 TCP 服务,accept队列的最大长度为128
断开连接(四次挥手)
- 为何四次挥手?
因为TCP是基于连接的双工工作机制,任何一端主动发起关闭请求后,需要等待对端返回确认后进入关闭,而对端被动关闭也是需要经过确认过程,而且在关闭之前的属于数据传输状态,网络情况、数据传输量是未知的,为了安全地、优雅地完成关闭动作,需要时间窗口以及双方确认,因为需要经历两个FIN -> ACK的四次挥手。 - 关闭TCP连接的方式
通常有两种,分别是RST报文关闭和FIN报文关闭。
如果进程异常退出了,内核就会发送RST报文来关闭,它可以不走四次挥手流程,是一个暴力关闭连接的方式。安全关闭连接的方式必须通过四次挥手,它由进程调用 close 和 shutdown 函数发起 FIN 报文,RST方式容易丢失交互信息 - 只有主动发起关闭的一方才有FIN_WAIT状态
- 发送FIN报文后没有及时收到ACK报文,如何处理?
Linux参数/proc/sys/net/ipv4/tcp_orphan_retries
控制,默认值是0,这里比较特殊,0代表重试8次,当重传次数大于该值则关闭连接。 - 如果遇到恶意攻击,FIN报文根本无法发送出去
这由 TCP 两个特性导致的:
- TCP必须保证报文是有序发送的。FIN报文也不例外,当发送缓冲区还有数据没有发送时,FIN报文也不能提前发送。
- TCP 有流量控制功能。当接收方接收窗口为0时,发送方就不能再发送数据。所以,当攻击者下载大文件时,就可以通过接收窗口设为0,这就会使得FIN报文都无法发送出去,那么连接会一直处于FIN_WAIT1状态。
- LInux参数
/proc/sys/net/ipv4/tcp_max_orphans
控制这种发送完FIN报文进入FIN_WAIT_1 状态的TCP连接,超过该参数值后直接走RST报文关闭TCP连接。
- FIN_WAIT_2状态的优化
linux参数/proc/sys/net/ipv4/tcp_fin_timeout
控制FIN_WAIT_2持续时间 - TIME_WAIT状态的优化
TIME-WAIT的状态尤其重要,主要是两个原因:
- 防止旧连接的数据包。防止具有相同<Client IP,Client PORT, Server IP, Server PORT>的旧的数据包被收到
- 保证连接正确关闭。保证被动关闭连接的一方能被正确的关闭,即保证最后的ACK能让被动关闭方接收,从而帮助其正常关闭
- TIME-WAIT没有等待时间或时间过短,断开连接会造成什么问题呢
如果TIME-WAIT时间设置过短,发起关闭的一端会立刻进入CLOSE状态,而发送给另一端的ACK报文可能丢失没有让被动关闭的一端收到,也就是说被动关闭的一端没有得到关闭连接的报文,一直处于LAST_ACK状态,迟迟无法进入CLOSE状态,当下一次新的TCP连接建立的时候,新连接建立请求会发送SYN报文,而未成功关闭的一方还停留在上一个工作未关闭状态直接返回RST报文,因此新的连接过程就被关闭了,受到影响了。 - 为什么是2MSL的时长?
这其实是相当于至少允许报文丢失一次。比如,若ACK在一个 MSL内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT状态的连接可以应对,这也是对网络糟糕情况的一个合理预估。 - TIME_WAIT状态优化
Linux参数/proc/sys/net/ipv4/tcp_max_tw_buckets
控制最大的TIME_WAIT连接数量,当超过该值则直接关闭。
性能问题
整个HTTP交互过程如下:
- DNS查询 即HTTP客户端需要对请求的URI进行IP、端口解析。一般HTTP客户端会通过DNS缓存保留常用或最近使用过的站点地址
- 连接 TCP建立连接握手的过程具有复杂性,有时间损耗
- 请求 服务报文请求
- 处理 对于请求的逻辑处理也有时间损耗
- 响应 服务报文响应
- 关闭 TCP连接关闭
性能问题主要聚焦在以下几部分,分别论述下
- TCP连接握手时延
- 用于捎带确认的TCP延迟确认算法
- TCP慢启动拥塞控制
- 数据聚集的Nagle算法、TCP Nodelay
- TIME_WAIT累积与端口耗尽
TCP连接握手时延
(1)(2)(3)是TCP建立连接的三次握手
- (1)SYN Client向Server发送一个小的TCP分组(40~60字节)。这个分组设置了一个特殊的SYN标记,说明这是一个连接请求。
- (2)SYN + ACK Server接受连接后,对连接参数进行计算,并向Client返回一个TCP分组,这个分组中SYN和ACK标记都被置位,说明请求已经被接受
- (3)ACK Client向Server发送一条确认信息,这个TCP分组中ACK标记被置位,表示已经建立了连接,且允许携带HTTP数据,这样可以减少一次请求
优化方案: 复用TCP连接
(3)(4)是HTTP数据传输
- (4) 响应Client请求,返回数据
用于捎带确认的TCP延迟确认算法
- 丢包问题。由于网络的不确定性和不可靠性,TCP分组传递过程中因路由器超负荷丢弃分组、网络中断等会出现丢失。
- 重发问题。 每个TCP分组都有一个序列号和数据完整性校验和。TCP分组接收者收到完整分组会给发送者一个确认,如果在指定时间窗口没有发出或发送者未收到这个确认,则会认为分组损坏或丢失而进行重发。
- 确认捎带&延迟确认算法。 由于收到完整TCP分组的反馈确认消息很小,为了节省网络资源会协同其他TCP分组进行捎带。这里有一个延迟确认算法,那就是将捎带信息存放在缓冲区同其他输出TCP分组一起发送,当在一个特定时间窗口(一般是100~200毫秒)内仍没有其他TCP分组则再进行单独确认消息的发送,也就是说最坏的情况下确认消息需要等待一个时间窗口才可以发送,因此会有可能产生延迟问题。
优化方案:根据实际情况调整TCP延迟确认算法参数
TCP慢启动拥塞控制
为了防止网络突发过载和拥塞,在TCP数据传输的启动节点会限制连接的最大速度,随着时间推移会逐渐提高传输速度,因此称之为TCP慢启动(Slow Start)。
TCP慢启动限制了一个TCP端点在任意时间可以传输的TCP分组数。举例,当有一组非常庞大的数据要进行发送,可能需要8个TCP分组发送才可以完成,起始阶段只允许发送1个TCP分组,当返回确认分组后,下次可以发送2个TCP分组,再返回确认分组后,下次就可以发送4个TCP分组,依次类推,窗口发送流量越来越大,因此称之为打开拥塞窗口。
由于存在这种拥塞控制特性,所以新连接的传输速度会比已经交换过一定数据量的、已调整的TCP连接慢一些。
优化方案:重用TCP连接
数据聚集的Nagle算法、TCP Nodelay
我们可以看到,每次数据发送都是以TCP分组形式进行的,对于上游的使用者来说,只有数据块是有效的,其他IP分组、TCP段等信息都是用来进行TCP通信交互的,因此尽可能多的填充数据块才可以更高效地利用网络传输,否则相同的数据块总量,每次发送的数据块较小,增加了TCP通信次数,大部分网络流量耗费在无用的底层数据封装上。
Nagle算法鼓励发送全尺寸(最大尺寸的分组约1500字节)的TCP分组,它也是先缓存不足全尺寸的TCP分组,当所有的TCP分组都确认了或缓存TCP包到一定程度,才进行不足全尺寸的TCP分组的发送,尽可能的最大程度利用每次TCP分组数据承载空间,减少通信交互。
Nagle算法 也带来一些性能问题,当HTTP报文无法填满一个分组时,可能会导致数据发送的延迟,且会与上面讲到的延迟确认算法出现时间窗口重合导致更严重的延迟问题。
优化方案:配置TCP_NODELAY来禁用Nagle算法,提高性能。这样的情况下要确保TCP分组中能保持写入较大的数据块使传输效率尽可能高。
TIME_WAIT累积与端口耗尽
当TCP端点关闭TCP连接的时候,TCP链接会进入TIME_WAIT状态,会在内存中维护一个空间,记录最近被关闭连接的IP和端口信息,且会维持一定的时间窗口,通常是最大使用周期的2倍(2MSL,一般为2分钟)左右,以确保这段时间内不会出现创建相同的IP和端口号。
服务器的可用端口是有限的,一般为60000个,如果在120秒(即2个MSL)内无法重新复用,因此当连接洪峰到达时,遇到极端情况最大的连接量为60000/120=500/秒。
优化方案:控制tcp_max_tw_buckets参数
参考
《HTTP权威指南》
time_wait状态产生的原因,危害,如何避免
TCP三次握手详解
近两万字 TCP 硬核知识,教你吊打面试官!
详解SYN Flood攻击原理与防范