tcp 相关总结

tcp 基础知识

这里写图片描述

  • CLOSED: 这个没什么好说的了,表示初始状态。
  • LISTEN: 这个也是非常容易理解的一个状态,表示服务器端的某个SOCKET处于监听状态,可以接受连接了。
  • SYN_RCVD: 这个状态表示接受到了SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三- 次握手会话过程中的一个中间状态,很短暂,基本上用netstat你是很难看到这种状态的,除非你特意写了一个客户端测试程序,故意将三次TCP握手过程中最后一个ACK报文不予发送。因此这种状态时,当收到客户端的ACK报文后,它会进入到ESTABLISHED状态。
  • SYN_SENT: 这个状态与SYN_RCVD遥想呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,因此也随即它会进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
  • ESTABLISHED:这个容易理解了,表示连接已经建立了。
  • FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。
  • FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接。
  • TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FIN_WAIT_1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
  • CLOSING: 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
  • CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。
  • LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。

三次握手

这里写图片描述

四次挥手

这里写图片描述

数据接收

这里写图片描述
数据包的接收,从下往上经过了三层:网卡驱动、系统内核空间,最后到用户态空间的应用。Linux内核使用sk_buff(socketkernel buffers)数据结构描述一个数据包。当一个新的数据包到达,NIC(networkinterface controller)调用DMAengine,通过RingBuffer将数据包放置到内核内存区。RingBuffer的大小固定,它不包含实际的数据包,而是包含了指向sk_buff的描述符。当RingBuffer满的时候,新来的数据包将给丢弃。一旦数据包被成功接收,NIC发起中断,由内核的中断处理程序将数据包传递给IP层。经过IP层的处理,数据包被放入队列等待TCP层处理。每个数据包经过TCP层一系列复杂的步骤,更新TCP状态机,最终到达recvBuffer,等待被应用接收处理。有一点需要注意,数据包到达recvBuffer,TCP就会回ACK确认,既TCP的ACK表示数据包已经被操作系统内核收到,但并不确保应用层一定收到数据(例如这个时候系统crash),因此一般建议应用协议层也要设计自己的确认机制。

数据发送

这里写图片描述
和接收数据的路径相反,数据包的发送从上往下也经过了三层:用户态空间的应用、系统内核空间、最后到网卡驱动。应用先将数据写入TCP sendbuffer,TCP层将sendbuffer中的数据构建成数据包转交给IP层。IP层会将待发送的数据包放入队列QDisc(queueingdiscipline)。数据包成功放入QDisc后,指向数据包的描述符sk_buff被放入RingBuffer输出队列,随后网卡驱动调用DMAengine将数据发送到网络链路上。

tcp queue

  1. 当 client 通过 connect 向 server 发出 SYN 包时,client 会维护一个 socket 等待队列,而 server 会维护一个 SYN 队列

  2. 此时进入半链接的状态,如果 socket 等待队列满了,server 则会丢弃,而 client 也会由此返回 connection time out;只要是 client 没有收到 SYN+ACK,3s 之后,client 会再次发送,如果依然没有收到,9s 之后会继续发送

  3. 半连接 syn 队列的长度为 max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog) 决定

  4. 当 server 收到 client 的 SYN 包后,会返回 SYN, ACK 的包加以确认,client 的 TCP 协议栈会唤醒 socket 等待队列,发出 connect 调用

  5. client 返回 ACK 的包后,server 会进入一个新的叫 accept 的队列,该队列的长度为 min(backlog, somaxconn),默认情况下,somaxconn 的值为 128,表示最多有 129 的 ESTAB 的连接等待 accept(),而 backlog 的值则由 int listen(int sockfd, int backlog) 中的第二个参数指定,listen 里面的 backlog 的含义请看这里。需要注意的是,一些 Linux 的发型版本会直接取 backlog 的值(本人目前接触到的都是正常的)。

  6. 当 accept 队列满了之后,即使 client 继续向 server 发送 ACK 的包,也会不被相应,此时,server 通过 /proc/sys/net/ipv4/tcp_abort_on_overflow 来决定如何返回,0 表示直接丢丢弃该 ACK,1 表示发送 RST 通知 client;相应的,client 则会分别返回 read timeout 或者 connection reset by peer。上面说的只是些理论,如果服务器不及时的调用 accept(),当 queue 满了之后,服务器并不会按照理论所述,不再对 SYN 进行应答,返回 ETIMEDOUT。

syn 半连接 accept 全连接

这里写图片描述

syn 半链接

三次握手协议中,服务器维护一个半连接队列,该队列为每个客户端的SYN包开设一个条目(服务端在接收到SYN包的时候,就已经创建了request_sock结构,存储在半连接队列中),该条目表明服务器已收到SYN包,并向客户发出确认,正在等待客户的确认包(会进行第二次握手发送SYN+ACK的包加以确认)。这些条目所标识的连接在服务器处于SYN_RECV状态,当服务器收到客户的确认包时,删除该条目,服务器进入ESTABLISHED状态。
该队列为SYN 队列,长度为 max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog),在机器的tcp_max_syn_backlog值在/proc/sys/net/ipv4/tcp_max_syn_backlog下配置。

: 新版的 linux 已经没有了 syn 队列,SYN_RECV 直接存入 ehash table(e代表establish,ehash即已连接hash队列) 里面 ,详情见https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git

查看SYN queue 溢出

netstat -s | grep LISTEN
102324 SYNs to LISTEN sockets dropped

accept 全链接

当第三次握手时,当server接收到ACK之后,会进入一个新的accept的队列,该队列的长度为min(backlog, somaxconn),默认情况下,somaxconn ()的值为 128,表示最多有 129 的 ESTAB 的连接等待 accept();backlog 的值则应该创建socket 的时候由 int listen(int sockfd, int backlog) 中的第二个参数指定,listen 里面的 backlog 可以有我们的应用程序去定义的。

查看Accept queue 溢出

netstat -s | grep TCPBacklogDrop
TCPBacklogDrop: 2334

业务配置

  • jdk serversocket 默认 50
  • netty 的 backlog 默认为 50
  • jetty 默认 50
  • tomcat backlog 是 AcceptCount参数,默认为100,Ali-Tomcat 为200.
  • nginx backlog 默认 511
    具体根据系统的并发量,吞吐量,以及长连接,短链接情况 进行设置.
    长连接 还要注意修改 fd 的上限(ulimit -n)

压测调优

慢慢调整io线程数,观察服务响应时间,第三方接口响应时间,关注io处理线程的状态:jstack 统计runnable的数量, 观察 cpu load (dstat,uptime,top),观察线程使用比例,观察listen等待队列是否有堆积, 观察 netstat -s |egrep listen 是否有增长. 结合各个参数的数据,和服务的具体功能,具体分析,慢慢优化参数.JAVA代码注意观察gc 的情况.
简单介绍几个场景的分析

  • 不依赖第三方,响应速度快

    • client 60 线程压测
    • 服务端 backlog 设置为 1024
    • 请求 是 http 短连接
    • 压力测试下,观察 io处理线程大多是wait 状态, listen 等待队列有积压, netstat -s accept 队列的overflowed次数没有增加,服务响应时间没有太大变化.cpu 使用率 不到40%,机器io,磁盘都没有到达瓶颈,gc正常
    • 对于响应很快的短连接服务 backlog 是很大的瓶颈 ,注意线程池不能盲目的给的很大,可能导致大量的 fetux 系统调用,和大量的cs , 中断 ,导致sys cpu 增加,反而影响性能.

    • 服务依赖第三方,响应快(小于2ms)
      TODO

    • 服务依赖第三方,响应慢 (大于50 ms)
      TODO
  • 不依赖第三方,响应慢(大于50ms)
    TODO

常见问题

syn rto, rto

  • 建立链接阶段,重传时间一般是1秒
  • 数据传输阶段,重传时间最小是200ms,windows是300ms,根据rtt动态变化,内网环境一般都是默认最小值
  • 可以通过ip route 修改rto, ip route change dev eth0 rto_min 20ms

syn flood 攻击

TIME_WAIT 导致连接数不够用

因为本地端口有限sysctl -a | grep net.ipv4.ip_local_port_range,
TIME_WAIT也有限sysctl -a | grep tcp_max_tw_buckets
常见都是打开tcp_tw_recycle,tcp_tw_reuse
- tcp_tw_reuse开启重用,重用生效条件
1)tcp_tw_reuse选项和tcp_timestamps选项也必须同时打开;
2)重用TIME_WAIT的条件是收到最后一个包后超过1s。
-tcp_tw_recycle 快速回收

一、net.ipv4.tcp_tw_recycle
功能:系统会假设对端开启了 tcp_timestamps,然后会去比较时间戳,如果时间戳变大了,就可以重用fd。开启后在 3.5*RTO 内回收,RTO 200ms~ 120s 具体时间视网络状况。内网状况比tw_reuse 稍快,公网尤其移动网络大多要比tw_reuse 慢,优点就是能够回收服务端的TIME_WAIT数量
打开后的副作用:如果对端是一个NAT网络的话(如:一个公司只用一个IP出公 网)或是对端的IP被另一台重用了,或者一个机器上多个docker Node,这个事就复杂了。建链接的SYN可能就被直接丢掉了(你可能会看到connection time out的错误) -- 这就是为什么一个公司连接在测试服务器时,会很慢,但是服务器本身负载很低的问题的原因。
建议:对外服务器不能打开该开关!!!对内服务器如果没有docker(Nat转换)的可以打开。
二、net.ipv4.tcp_timestamps
功能:记录数据包的发送时间,其为双向的选项,当一方不开启时,两方都将停用timestamps,默认打开
打开后的副作用:1.10字节的TCP header开销 2. allow you to guess the uptime of a target system
建议:保持打开状态

三、net.ipv4.tcp_tw_reuse
功能:表示开启重用。当从协议认为安全,则允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭。 协议什么情况下认为是安全的?从代码来看是收到最后一个包后超过1s :(sysctl_tcp_tw_reuse && get_seconds() - tcptw->tw_ts_recent_stamp > 1)
打开后的副作用:主动关闭方有可能在下次使用时收到上一次连接的数据包,包括关闭连接响应包或者正常通信的数据包(因为数据包需要经过MSL时间才会消失),主动关闭方会出现奇怪的表现。
建议:服务器应该打开该配置,因为内网网络状态一般比较好。

tcp_tw_recycle 导致syn抛弃

启用TIME-WAIT状态sockets的快速回收,这个选项不推荐启用。在NAT(Network Address Translation)网络下,会导致大量的TCP连接建立错误。当开启了tcp_tw_recycle选项后,当连接进入TIME_WAIT状态后,会记录对应远端主机最后到达分节的时间戳。如果同样的主机有新的分节到达,且时间戳小于之前记录的时间戳,即视为无效,相应的数据包会被丢弃.

半链接队列导致 syn丢包

在Linux内核2.2之后,分离为两个backlog来分别限制半连接(SYN_RCVD状态)队列大小和全连接(ESTABLISHED状态)队列大小。
SYN queue 队列长度由 /proc/sys/net/ipv4/tcp_max_syn_backlog 指定,默认为2048。
Accept queue 队列长度由 /proc/sys/net/core/somaxconn 和使用listen函数时传入的参数,二者取最小值。默认为128。在Linux内核2.4.25之前,是写死在代码常量 SOMAXCONN ,在Linux内核2.4.25之后,在配置文件 /proc/sys/net/core/somaxconn 中直接修改,或者在 /etc/sysctl.conf 中配置 net.core.somaxconn = 128 。溢出时,根据tcp_abort_on_overflow处理,tcp_abort_on_overflow指定为1(将返回RST),否则底层将不会做任何事情……这是一种委婉的退让策略,在服务端处理不过来时,让客户端误以为ACK丢失,继续重新发送ACK。这样,当服务端的处理能力恢复时,这条连接又可以重新被移动到ESTABLISHED队列中去。

CLOSE_WAIT 过多

CLOSE_WAIT 看状态图是被动关闭方的状态,在客户端关闭链接时,服务端没有做响应导致大量CLOSE_WAIT.

  • 因为closewait不会主动回收,一般只能通过重启,
  • keepalive 参数解决closewait问题,会定时心跳client,client关闭会返回rst关闭链接
  • 应用程序问题:如果在应用程序中忘记了 close 相应的TCP连接,那么Y也就不会发出 FIN 包,进而导致 CLOSE_WAIT;
  • 应用程序响应太慢:应用程序不能及时响应client的关闭请求也会导致CLOSE_WAIT状态的堆积。
  • Accept backlog太大:Accept 的 backlog太大,设想突然遭遇大访问量的话,即便响应速度不慢,也可能出现来不及消费的情况,导致多余的请求还在队列里就被对方关闭了。

Refrence

猜你喜欢

转载自blog.csdn.net/chen8238065/article/details/80457150