在网络服务中,双方的关系是临时建立的,并且这种关系不是基于完全信任的基础上,服务器不能认为所有的客户端都是正常访问的客户端,客户端也不能完全信任服务器能正确高效的做出请求的回应。于是就需要一定的机制判断出这种不正常。本文主要讨论网络服务中客户端与服务器之间的连接超时,发送超时,接受超时和idel超时,分别从系统层面上和应用层面上做出测试。
连接超时主要用于客户端,因为主动发起连接的一方被称为客户端,当调用connect系统调用的时候,客户端发起了三次握手操作,只有完成三次握手之后连接才算是真正的建立。连接超时是指对于在发起连接开始,如果在指定时间内没有成功建立连接,需要执行将控制权从connect状态返回,通知调用者,避免不必要的等待。发送超时主要针对write操作,对于网络稍微了解的同学都知道,其实对于socket的write操作只是简单的将应用层数据拷贝到内核中这个TCP连接的发送缓冲区中,发送操作是由TCP/IP内核协议栈完成的,而发送方发送多少数据是根据接收方的接收窗口控制的。所以当连接的内核缓冲区被填满(发送缓冲区就相当于一个队列,只有在发送方将数据发送出去并且接收方回应ack之后发送出去的数据才会被丢弃,所以当本地的write操作较多并且由于网络原因导致的对端接受缓慢就很容易填满发送缓冲区),并且对端的接收窗口很小甚至为0就会导致write操作阻塞,发送超时主要针对write操作,如果在指定时间write操作不能返回就通知调用方,避免不必要的等待。读超时是针对read操作的,socket的read操作会等待着该连接的接收缓冲区有数据到达,TCP层保证了数据的顺序性和正确性,read会将内核缓冲区的数据拷贝到用户缓冲区中,但是当接收缓冲区为空(可能因为对端一直未发送数据)这个调用将会一直阻塞,从而影响接下来的流程,读超时就是指在指定时间内read操作不能读取(read一次只要读取到数据就会返回)到任何数据需要通知调用方,避免不需要的等待。空闲超时主要是针对服务器的,由于连接时客户端建立的,所以一般连接关闭操作也是由客户端控制的,但是每一个连接都需要占用服务器的资源,为了节约资源,服务器需要有一定的机制保证关闭空闲连接,一般情况下是指在客户端一个请求结束后在指定时间内没有其他请求,服务器需要执行一些必要的操作,例如关闭连接,从而保护自己。
从这几种超时的讨论可以看出,前三种超时都是系统层面上的,与系统调用息息相关,而空闲超时则完全是一种应用层的操作,连接超时一般用于客户端发起连接时设置,读写超时可以用于客户端和服务器在对数据进行读写时设置,空闲超时一般用于服务器对连接进行设置。可以看出无论是客户端还是服务器都只想着自己的利益,不信任对方,如果不设置超时,那么会导致客户端或者服务器的资源浪费(至少浪费一个连接,如果使用connection per thread模型,会导致该线程接下来的任务都无法进行),尤其是空闲连接,如果服务器不会主动关闭连接,那么恶意用户就可以通过大量建立连接而不发送数据,直到将服务器的资源消耗完。因此超时的设置是非常必要的。
但是对于应用程序怎么设置超时呢?也就是在实际编码的时候如何做呢?首先,你需要一个定时器管理单元,它能够提供定时的功能,在每一个注册的定时器超时之后通知调用方。对于前三种系统层面的超时,这些操作默认都是阻塞方式进行的,也就是说线程将会处于睡眠状态,直到这三种系统调用返回,这时候需要对这些系统调用进行一层封装,例如connect操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 |
int Connect( int
fd, struc sockaddr* addr, int
len) { int
state = 0; //创建定时器,回调函数用于唤醒该线程,并且将stata变量置为1表示超时 int
id = register_timer(thread_id , callback , &state); int
ret = connect(fd , addr , len); //定时器在函数返回之后被删除,回调函数只负责唤醒该线程 delete_timer(id); if (state != 0) { ret = TIMEOUT; } return
ret; } |
在注册定时器的时候指定回调函数,超时之后回调函数负责唤醒睡眠的线程(可以通过发送信号的方式,呼呼,线程间发送信号...),并将用户传入的私有参数值为1表示connect操作是由于超时返回的,对于阻塞式的read和write操作也需要这样的封装但是这样需要频繁的执行定时器的创建和删除,为了避免这些开销,可以为每一个线程绑定一个定时器,创建和删除操作转换成定时器的激活和暂停状态。
这些系统调用还可以使用非阻塞方式调用,这时候就需要将它们的fd注册到一种多路复用机制上监听,使用这种方式会导致这几个系统调用立即返回(如果未就绪就返回EAGAIN错误或者EINPROGRESS),而多路复用机制会在这些fd的事件就绪之后通知调用方,但是如果在指定时间内仍未就绪,就需要通知调用方超时,具体的实现方式也可以在调用之前注册定时器,在定时器的回调函数中将该事件从多路复用中delete,这样该事件就不会再被监听了。同样,这种方式也需要大量的定时器的创建和销毁操作,需要高效的定时器支撑。
同样,对于空闲超时,也可以在每次处理完成一个请求之后就设置一个定时器,在定时器超时之后就关闭这个连接,执行一些清理工作。
好了,上面说到了设置超时的原因,重要性,实现方式,我们不能光说不练,下面分别对这些系统调用在系统层面上的超时和应用软件(这里使用nginx)设置的超时,测试环境如下:
测试一:系统调用的超时
1. connect函数的超时
测试方式:使用C语言直接调用connect系统调用,server执行socket/bind/listen操作之后,阻塞在accept调用上,客户端执行socket操作之后调用connect函数,在启动client之前在server上使用iptables创建规则,丢弃server端口上接收到的所有数据,这包括丢弃所有的SYN报文。然后启动服务器和客户端。
通过观察可以看出客户端一直没有从connect函数返回,使用tcpdump抓包可以看出客户端在一直重试:
可以看出在客户端发送SYN报文之后如果在指定时间内没有收到对端的SYN+ACK,它就会重试(这是内核协议栈做的),重试的时间依次是1s/2s/4s/8s/16s/32s,最终在75秒之后connect返回:
可以看出,内核对于connect是有超时的,所以在应用层做超时需要低于内核对connect设置的超时时间(默认为75s),否则应用层的超时就没有任何意义了。
2. write函数的超时
测试方式:使用之前的程序,不过不需要在启动cllient之前启动server端的iptables,而是在connect之后sleep时间,然后循环调用write操作,每次写出16KB的数据,每次返回打印出已经写出去的字节数。在sleep时间内再启动server机器上的iptables,同样是丢弃所有的报文,这样write操作开始还能够正常返回,但是当本地的发送缓冲区被填充满之后该操作将被阻塞。client程序的反应如下:
前面的被省略了,可以看出write操作一共写出去了624KB的数据,由于接收方将所有的报文都丢弃了,所也不会发送ACK,这样发送方就当作是数据包丢失了,于是不断的重试,通过tcpdump转包可以证实这一点:
可以看出,第一次PSH发送方发送了16384byte的数据到对端,接着又发送了11815byte的数据,这两份数据都没有得到对方的ack,于是内核协议栈不断的进行重试,重试只针对第一份报文,可以看出间隔时间依次是0.2s/0.4s/0.8s/1.6s,成倍增长直到102.4秒之后,每隔2分钟重试一次。最终client进程的write操作还是返回的,write函数返回错误:Connection timed out,这个时间距离第一次发送数据的时间大概过去了15分钟30秒,可以看出该操作一共尝试了15次(从0.2s开始成倍增长直到102.4s,然后每隔2s重试一次,尝试了5次,在最后一次结束之后仍没有发送出去就返回错误)。所以系统层面上的写超时时间是这样的,应用程序不要设置大于这样的超时时间,否则就没有意义了。
但是通过netstat查看发送缓冲区发现一个不一致的情况。
在这里看到客户端的这个连接的发送缓冲区的大小为649889byte,比程序打印出来的write操作返回的总字节数少了1万多byte,不知道为什么,只能猜测对于套接字的write操作会将所有的数据全部拷贝之后才会返回?也就是能够保证write操作的原子性?只能猜测了...
3. read函数的超时、
由于默认情况下read操作是阻塞式的,它会阻塞直到套接字的接收缓冲区有任何数据到达,操作系统的协议栈不会对接收缓冲区为空做任何重试操作,所以read操作将会一直阻塞,除非在应用层有外力的作用,这里不再进行测试。
测试二:nginx作为代理服务器的各种超时
nginx有强大的代理转发功能,所以经常使用它作为反向代理服务器,并且可以通过它提供的负载均衡能力对后端的业务服务器进行负载均衡,作为代理服务器需要和后端的多个服务器建立多个连接,一般客户端会使用连接池管理连接,一般有两种实现方式:1. 每一个新的请求都创建一个连接并加入到池子中,等待池子满了从中选取一个发送请求;2. 尽可能的少创建连接,直到一个连接hold不住的时候再创建新的连接。不论哪种方式都需要多个请求复用一个tcp连接,这就需要对每一个请求进行标识,从而能够在得到回复之后继续该请求的流程。下面看一下nginx作为反向代理的超时机制,在nginx作为代理服务器时,提供了三种超时参数,分别是连接超时,读超时和发送超时。参数配置如下:
把这三个超时分别设置为30s/40s和50s,在后台服务器中有idle连接的超时时间,可以设置keepalive_timeout来设置服务器的空闲超时,这里设置为60s。
1. 连接超时
测试方法:在终端使用curl向代理服务器发送一个GET请求,代理服务器收到这个GET请求之后通过匹配location会将该请求转发给后台的nginx服务器,这两个服务器在一台主机上,分别配置两个nginx虚拟主机,端口号分别起6162和8181,前者是代理服务器,后者是后台服务器。在启动这两个服务器之后先使用iptables设置规则以丢弃所有发往8181端口的数据包,然后再执行curl命令,最终curl的结果如下:
代理服务器发生了超时,通过tcpdump抓包可以看出代理服务器一直在重试向后台服务器(8181)发送SYN报文:
一共尝试了5次,但是从nginx代理服务器的日志中可以看到这个请求的结束时间:
可以看出,从代理服务器第一次向8181服务器发送请求,到这个请求被回复正好经过了30秒,connect重试了5次,这也说明代理服务器中配置的连接超时起了作用。
2. 发送超时
测试方式:发送数据之前需要首先建立连接,但是iptables只能丢弃一个端口上的所有报文,而不能选择性的丢弃指定类型的报文(至少我没有找到),所以就只好使用了代理服务器的连接池,这样每次发送真正的数据之前首先发送一个成功的GET请求,使得代理服务器和后台服务器建立了连接,然后再发送一个真正的测试请求,为了填满代理服务器的发送缓冲区,我使用PUT请求上传一个大小为4MB的文件,这样复用之前的连接,数据并不能被真正发送到8181服务器上,最终curl仍然返回504 Gateway Time-out(使用curl命令,上传一个4MB大小的文件,使用PUT命令,但是第一次操作失败了,返回的错误是413 Request Entity Too Large,这是因为未设置代理服务器的client_max_body_size参数,以至于请求的主体部分过大,将这个参数设置为10MB,再次重试就可以了)。
tcpdump的抓包情况如下:
由于复用之前的连接,所以发送的报文序列号不再是从1开始的了,发送操作也会根据write调用的策略定期的重试,直到最后请求被返回,但是请求被返回的时间和最终8181关闭连接的时间不一样的,至于8181关闭连接的时间,是由于空闲超时时间决定的。
从日志中查看返回curl请求的时间和第一次发送数据的时间正好相差了50s,这说明之前配置的nginx发送超时生效了。
3. 读超时:
测试方式:测试读超时时需要丢弃所有发往代理服务器的包,但是这个端口号是在connect建立连接时随机选择的,所以需要首先复用之前建立的连接,然后获取该连接的客户端端口号,然后使用iptables根据丢弃所有发往这个端口的报文,在终端使用curl命令向代理服务器请求,最终得到的结果仍然是504 Gateway Time-out。tcpdump抓包如下:
发现两个方向都有数据流通,因为在curl之前就禁止了所有发往60762端口的报文,它会有两个影响:1.所有8181端口向它发送的PSH报文被丢弃;2. 所有8181端口向它发送的ACK保温被丢弃。因此8181将不断的重试向它的PSH操作,而60762虽然向8181端口发送数据成功(没有被丢弃)了,但是对端回复的ACK被丢弃了,这也会导致它不断的重试,所以看到双方都在重试的情况。但是最终这个请求被返回了,返回的时间可以根据代理服务器的日志获得:
可以计算出,最终恢复的时间距离该请求的读操作的时间为40秒,正好说明读超时有效。
4. 空闲超时
测试方式:在8181上配置keepalive_timeout为60s,然后清除所有的iptables的规则,使用curl发起一个GET请求,等待一段时间,观察tcpdump的输出:
可以看出在GET请求成功完成1分钟之后,这个连接被8181服务器主动关闭了,这也就说明了nginx的keepalive_timeout超时设置是有效的,当在这个时间内一个连接上无请求到达就关闭该连接。
上面,我们对四中超时进行了讨论,并且从系统调用层面上和应用层面上分别对它们进行测试和验证,从这里可以看出在设计网络程序中无论是客户端还是服务器,尤其是代理服务器作为客户端时需要对每一个连接甚至请求设置超时,这样保证资源部被浪费,请求能够最后得到回复。但是这样的超时还是有一些不够完善,例如一个连接上每隔1s中发送一个请求,这样不会造成写操作的超时,也不会造成服务器的空闲超时,但是对于这样缓慢的连接时应该被提前关闭的,所以可以进一步使用curl库中的超时设置:在指定时间内该连接的读写速度小于一个阀值之后该连接就会被关闭,这样可以更好的保护服务器。
文中不足之处和疑问希望能够被指出和解答,希望你能从中得到收获...
原文:http://www.cnblogs.com/coder-yu-love-chelsea/p/3665096.html