网络服务中的定时器

    在网络服务中,双方的关系是临时创建的,而且这种关系不是基于彻底信任的基础上,服务器不能认为全部的客户端都是正常访问的客户端,客户端也不能彻底信任服务器能正确高效的作出请求的回应。因而就须要必定的机制判断出这种不正常。本文主要讨论网络服务中客户端与服务器之间的链接超时,发送超时,接受超时和idel超时,分别从系统层面上和应用层面上作出测试。nginx

  链接超时主要用于客户端,由于主动发起链接的一方被称为客户端,当调用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操做:网络

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,这样该事件就不会再被监听了。一样,这种方式也须要大量的定时器的建立和销毁操做,须要高效的定时器支撑。curl

  一样,对于空闲超时,也能够在每次处理完成一个请求以后就设置一个定时器,在定时器超时以后就关闭这个链接,执行一些清理工做。socket

  好了,上面说到了设置超时的缘由,重要性,实现方式,咱们不能光说不练,下面分别对这些系统调用在系统层面上的超时和应用软件(这里使用nginx)设置的超时,测试环境以下:tcp

     测试一:系统调用的超时ide

  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库中的超时设置:在指定时间内该链接的读写速度小于一个阀值以后该链接就会被关闭,这样能够更好的保护服务器。

  文中不足之处和疑问但愿可以被指出和解答,但愿你能从中获得收获...

相关文章
相关标签/搜索