在一次升级线上应用服务之后,我们发现该服务的可用性变得时好时坏,一 段时间可以对外提供服务,一段时间突然又不可以,大家都百思不得其解。运维同学登录到服务所在的主机 上,使用netstat命令查看后才发现,主机上有成千上万处于TIME_WAIT状态的连接。
经过层层剖析后,我们发现罪魁祸首就是TIME_WAIT。为什么呢?我们这个应用服务需要通过发起TCP连接 对外提供服务。每个连接会占用一个本地端口,当在高并发的情况下,TIME_WAIT状态的连接过多,多到 把本机可用的端口耗尽,应用服务对外表现的症状,就是不能正常工作了。当过了一段时间之后, 处于 TIME_WAIT的连接被系统回收并关闭后,释放出本地端口可供使用,应用服务对外表现为,可以正常工 作 。这样周而复始,便会出现了一会儿不可以,过一两分钟又可以正常工作的现象。
那么为什么会产生这么多的TIME_WAIT连接呢? 这要从TCP的四次挥手说起。我在文稿中放了这样一张图。
TCP连接终止时,主机1先发送FIN报文,主机2进入CLOSE_WAIT状态,并发送一个ACK应答,同时,主机2 通过read调用获得EOF,并将此结果通知应用程序进行主动关闭操作,发送FIN报文。主机1在接收到FIN报 文后发送ACK应答,此时主机1进入TIME_WAIT状态。
主机1在TIME_WAIT停留持续时间是固定的,是最⻓分节生命期MSL(maximum segment lifetime)的两 倍,一般称之为2MSL。和大多数BSD派生的系统一样,Linux系统里有一个硬编码的字段,名称 为TCP_TIMEWAIT_LEN,其值为60秒。也就是说, Linux系统停留在TIME_WAIT的时间为固定的60秒 。
过了这个时间之后,主机1就进入CLOSED状态。为什么是这个时间呢?
只有发起连接终止的一方会进入TIME_WAIT状态。
首先,这样做是为了确保 最后的ACK能让被动关闭方接收 ,从而帮助其正常关闭。
TCP在设计的时候,做了充分的容错性设计,比如,TCP假设报文会出错,需要重传。在这里,如果图中主 机1的ACK报文没有传输成功,那么主机2就会重新发送FIN报文。
如果主机1没有维护TIME_WAIT状态,而直接进入CLOSED状态,它就失去了当前状态的上下文,只能回复 一个RST操作,从而导致被动关闭方出现错误。
现在主机1知道自己处于TIME_WAIT的状态,就可以在接收到FIN报文之后,重新发出一个ACK报文,使得 主机2可以进入正常的CLOSED状态。
第二个理由和连接“化身”和报文迷走有关系,为了 让旧连接的重复分节在网络中自然消失 。
我们知道,在网络中,经常会发生报文经过一段时间才能到达目的地的情况,产生的原因是多种多样的,如 路由器重启,链路突然出现故障等。如果迷走报文到达时,发现TCP连接四元组(源IP,源端口,目的IP, 目的端口)所代表的连接不复存在,那么很简单,这个报文自然丢弃。
我们考虑这样一个场景,在原连接中断后,又重新创建了一个原连接的“化身”,说是化身其实是因为这个 连接和原先的连接四元组完全相同,如果迷失报文经过一段时间也到达,那么这个报文会被误认为是连 接“化身”的一个TCP分节,这样就会对TCP通信产生影响。
所以,TCP就设计出了这么一个机制,经过2MSL这个时间,足以让两个方向上的分组都被丢弃,使得原来 连接的分组在网络中都自然消失,再出现的分组一定都是新化身所产生的。
第一是内存资源占用,这个目前看来不是太严重,基本可以忽略。
第二是对端口资源的占用,一个TCP连接至少消耗一个本地端口。要知道,端口资源也是有限的,一般可以开启的端口为32768〜61000 ,也可以通过net.ipv4.ip_local_port_range指定,如果TIME_WAIT状 态过多,会导致无法创建新连接。
一个暴力的方法是通过sysctl命令,将系统值调小。这个值默认为18000,当系统中处于TIME_WAIT的连接 一旦超过这个值时,系统就会将所有的TIME_WAIT连接状态重置,并且只打印出警告信息。这个方法过于 暴力,而且治标不治本,带来的问题远比解决的问题多,不推荐使用。
这个方法是一个不错的方法,缺点是需要“一点”内核方面的知识,能够重新编译内核
unix的哲学是一切皆文件,可以把socket看成是一种特殊的文件,而一些socket函数就是对其进行的操作api(读/写IO、打开、关闭)。我们知道普通文件的打开操作(open)返回一个文件描述字,与之类似,socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,
sockfd即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
在将一个地址绑定到socket的时候,需要先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于这个问题曾引发过不少血案,谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。
这里的主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:
listen函数的第一个参数即为要监听的socket描述字,第二个参数为socket可以接受的排队的最大连接个数。listen函数表示等待客户的连接请求。
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数去接收请求,这样连接就建立好了(在connect之后就建立好了三次连接),之后就可以开始进行类似于普通文件的网络I/O操作了。
如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与客户的TCP连接。
accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,类似于操作完打开的文件要调用fclose关闭打开的文件。
close一个TCP socket的缺省行为时把该socket标记为已关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数
close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:
客户端向服务器发送一个SYN J
服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
客户端再想服务器发一个确认ACK K+1
socket中TCP的四次握手释放连接详解
某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。一段时间之后,服务端调用close关闭它的socket。这导致它的TCP也发送一个FIN N;接收到这个FIN的源发送端TCP对它进行确认,这样每个方向上都有一个FIN和ACK。
为什么要三次握手
由于tcp连接是全双工的,存在着双向的读写通道,每个方向都必须单独进行关闭。当一方完成它的数据发送任务后就可以发送一个FIN来终止这个方向的连接。收到FIN只意味着这个方向上没有数据流动,但并不表示在另一个方向上没有读写,所以要双向的读写关闭需要四次握手,
3. time_wait状态如何避免?
首先服务器可以设置SO_REUSEADDR套接字选项来通知内核,如果端口忙,但TCP连接位于TIME_WAIT状态时可以重用端口。在一个非常有用的场景就是,如果你的服务器程序停止后想立即重启,而新的套接字依旧希望使用同一端口,此时SO_REUSEADDR选项就可以避免TIME_WAIT状态。
1.客户端连接服务器的80服务,这时客户端会启用一个本地的端口访问服务器的80,访问完成后关闭此连接,立刻再次访问服务器的
80,这时客户端会启用另一个本地的端口,而不是刚才使用的那个本地端口。原因就是刚才的那个连接还处于TIME_WAIT状态。
2.客户端连接服务器的80服务,这时服务器关闭80端口,立即再次重启80端口的服务,这时可能不会成功启动,原因也是服务器的连
接还处于TIME_WAIT状态。
实战分析:
状态描述:
CLOSED:无连接是活动的或正在进行
LISTEN:服务器在等待进入呼叫
SYN_RECV:一个连接请求已经到达,等待确认
SYN_SENT:应用已经开始,打开一个连接
ESTABLISHED:正常数据传输状态
FIN_WAIT1:应用说它已经完成
FIN_WAIT2:另一边已同意释放
ITMED_WAIT:等待所有分组死掉
CLOSING:两边同时尝试关闭
TIME_WAIT:另一边已初始化一个释放
LAST_ACK:等待所有分组死掉</pre>
命令解释:
如何尽量处理TIMEWAIT过多?
编辑内核文件/etc/sysctl.conf,加入以下内容:
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_fin_timeout 修改系默认的 TIMEOUT 时间</pre>
然后执行 /sbin/sysctl -p 让参数生效.
/etc/sysctl.conf是一个允许改变正在运行中的Linux系统的接口,它包含一些TCP/IP堆栈和虚拟内存系统的高级选项,修改内核参数永久生效。
简单来说,就是打开系统的TIMEWAIT重用和快速回收。
本文主要讲述了socket的主要api,以及tcp的连接过程和其中各个阶段的连接状态,理解这些是更深入了解tcp的基础!
欢迎分享,转载请注明来源:夏雨云
评论列表(0条)