!!!!!!!!!!!!
分类: LINUX
2010-12-16 19:56:14
网络编程常见问题总结 串讲(四)
为什么网络程序会没有任何预兆的就退出了
一般情况都是没有设置忽略PIPE信号
在我们的环境中当网络触发broken pipe (一般情况是write的时候,没有write完毕, 接受端异常断开了), 系统默认的行为是直接退出。在我们的程序中一般都要在启动的时候加上 signal(SIGPIPE, SIG_IGN); 来强制忽略这种错误
write出去的数据, read的时候知道长度吗?
严格来说, 交互的两端, 一端write调用write出去的长度, 接收端是不知道具体要读多长的. 这里有几个方面的问题
write 长度为n的数据, 一次write不一定能成功(虽然小数据绝大多数都会成功), 需要循环多次write
write虽然成功,但是在网络中还是可能需要拆包和组包, write出来的一块数据, 在接收端底层接收的时候可能早就拆成一片一片的多个数据包. TCP层中对于接收到的数据都是把它们放到缓冲中, 然后read的时候一次性copy, 这个时候是不区分一次write还是多次write的。所以对于网络传输中 我们不能通过简单的read调用知道发送端在这次交互中实际传了多少数据. 一般来说对于具体的交互我们一般采取下面的方式来保证交互的正确,事先约定好长度, 双方都采用固定长度的数据进行交互, read, write的时候都是读取固定的长度.但是这样的话升级就必须考虑两端同时升级的问题。特殊的结束符或者约定结束方式, 比如http头中采用连续的\r\n来做头部的结束标志. 也有一些采用的是短连接的方式, 在read到0的时候,传输变长数据的时候一般采用定长头部+变长数据的方式, 这个时候在定长的头部会有一个字段来表示后面的变长数据的长度, 这种模式下一般需要读取两次确定长度的数据. 我们现在内部用的很多都是这样的模式. 比如public/nshead就是这样处理, 不过nshead作为通用库另外考虑了采用 通用定长头+用户自定义头+变长数据的接口。
总的来说read读数据的时候不能只通过read的返回值来判断到底需要读多少数据, 我们需要额外的约定来支持, 当这种约定存在错误的时候我们就可以认为已经出现了问题. 另外对于write数据来说, 如果相应的数据都是已经准备好了那这个时候也是可以把数据一次性发送出去,不需要调用了多次write. 一般来说write次数过多也会对性能产生影响,另一个问题就是多次连续可能会产生延时问题,这个参看下面有关长连接延时的部分问题.
小提示
上面提到的都是TCP的情况, 不一定适合其他网络协议. 比如在UDP中 接收到连续2个UDP包, 需要分别读来次才读的出来, 不能像TCP那样,一个read可能就可以成功(假设buff长度都是足够的)。
如何查看和观察句柄泄露问题 一般情况句柄只有1024个可以使用,所以一般情况下比较容易出现, 也可以通过观察/proc/进程号/fd来观察。
另外可以采用valgrind来检查, valgrind参数中加上 --track-fds = yes 就可以看到最后退出的时候没有被关闭的句柄,以及打开句柄的位置
为什么socket写错误,但用recv检查依然成功?
首先采用recv检查连接的是基于我们目前的一个请求一个应答的情况对于客户端的请求,逻辑一般是这样 建立连接->发起请求->接受应答->长连接继续发请求
recv检查一般是这样采用下面的方式: ret = recv(sock, buf, sizeof(buf), MSG_DONTWAIT);
通过判断ret 是否为-1并且errno是EAGAIN 在非堵塞方式下如果这个时候网络没有收到数据, 这个时候认为网络是正常的
这是由于在网络交换模式下 我们作为一个客户端在发起请求前, 网络中是不应该存在上一次请求留下来的脏数据或者被服务端主动断开(服务端主动断开会收到FIN包,这个时候是recv返回值为0), 异常断开会返回错误. 当然这种方式来判断连接是否存在并不是非常完善,在特殊的交互模式(比如异步全双工模式)或者延时比较大的网络中都是存在问题的,不过对于我们目前内网中的交互模式还是基本适用的. 这种方式和socket写错误并不矛盾, 写数据超时可能是由于网慢或者数据量太大等问题, 这时候并不能说明socket有错误, recv检查完全可能会是正确的. 一般来说遇到socket错误,无论是写错误还读错误都是需要关闭重连.
为什么接收端失败,但客户端仍然是write成功
这个是正常现象, write数据成功不能表示数据已经被接收端接收导致,只能表示数据已经被复制到系统底层的缓冲(不一定发出), 这个时候的网络异常都是会造成接收端接收失败的.
长连接的情况下出现了不同程度的延时 在一些长连接的条件下, 发送一个小的数据包,结果会发现从数据write成功到接收端需要等待一定的时间后才能接收到, 而改成短连接这个现象就消失了(如果没有消失,那么可能网络本身确实存在延时的问题,特别是跨机房的情况下) 在长连接的处理中出现了延时,而且时间固定,基本都是40ms, 出现40ms延时最大的可能就是由于没有设置TCP_NODELAY 在长连接的交互中,有些时候一个发送的数据包非常的小,加上一个数据包的头部就会导致浪费,而且由于传输的数据多了,就可能会造成网络拥塞的情况, 在系统底层默认采用了Nagle算法,可以把连续发送的多个小包组装为一个更大的数据包然后再进行发送. 但是对于我们交互性的应用程序意义就不大了,在这种情况下我们发送一个小数据包的请求,就会立刻进行等待,不会还有后面的数据包一起发送, 这个时候Nagle算法就会产生负作用,在我们的环境下会产生40ms的延时,这样就会导致客户端的处理等待时间过长, 导致程序压力无法上去. 在代码中无论是服务端还是客户端都是建议设置这个选项,避免某一端造成延时。所以对于长连接的情况我们建议都需要设置TCP_NODELAY, 在我们的ub框架下这个选项是默认设置的.
小提示:
对于服务端程序而言, 采用的模式一般是
bind-> listen -> accept, 这个时候accept出来的句柄的各项属性其实是从listen的句柄中继承, 所以对于多数服务端程序只需要对于listen进行监听的句柄设置一次TCP_NODELAY就可以了,不需要每次都accept一次.
设置了NODELAY选项但还是时不时出现10ms(或者某个固定值)的延时 这种情况最有可能的就是服务端程序存在长连接处理的缺陷. 这种情况一般会发生在使用我们的pendingpool模型(ub中的cpool)情况下,在 模型的说明中有提到. 由于select没有及时跳出导致一直在浪费时间进行等待.
上面的2个问题都处理了,还是发现了40ms延时?
协议栈在发送包的时候,其实不仅受到TCP_NODELAY的影响,还受到协议栈里面拥塞窗口大小的影响. 在连接发送多个小数据包的时候会导致数据没有及时发送出去.
这里的40ms延时其实是两方面的问题:
对于发送端, 由于拥塞窗口的存在,在TCP_NODELAY的情况,如果存在多个数据包,后面的数据包可能会有延时发出的问题. 这个时候可以采用 TCP_CORK参数,
TCP_CORK 需要在数据write前设置,并且在write完之后取消,这样可以把write的数据发送出去( 要注意设置TCP_CORK的时候不能与TCP_NODELAY混用,要么不设置TCP_NODELAY要么就先取消TCP_NODELAY)
但是在做了上面的设置后可能还是会导致40ms的延时, 这个时候如果采用tcpdump查看可以注意是发送端在发送了数据包后,需要等待服务端的一个ack后才会再次发送下一个数据包,这个时候服务端出现了延时返回的问题.对于这个问题可以通过设置server端TCP_QUICKACK选项来解决. TCP_QUICKACK可以让服务端尽快的响应这个ack包.
这个问题的主要原因比较复杂,主要有下面几个方面
当TCP协议栈收到数据的时候, 是否进行ACK响应(没有响应是不会发下一个包的),在我们linux上返回ack包是下面这些条件中的一个
接收的数据足够多
处于快速回复模式(TCP_QUICKACK)
存在乱序的包
如果有数据马上返回给发送端,ACK也会一起跟着发送
如果都不满足上面的条件,接收方会延时40ms再发送ACK, 这个时候就造成了延时。
但是对于上面的情况即使是采用TCP_QUICKACK,服务端也不能保证可以及时返回ack包,因为快速回复模式在一些情况下是会失效(只能通过修改内核来实现)
目前的解决方案只能是通过修改内核来解决这个问题,STL的同学在 内核中增加了参数可以控制这个问题。
会出现这种情况的主要是连接发送多个小数据包或者采用了一些异步双工的编程模式,主要的解决方案有下面几种
对于连续的多个小数据包, 尽量把他们打到一个buffer中间, 不过会有内存复制的问题
采用writev方式发送多个小数据包, 不过writev也存在一个问题就是发送的数据包个数有限制,如果超过了IOV_MAX(我们的限制一般是1024), 依然可能会出现问题,因为writev只能保证在IOV_MAX范围内的数据是按照连续发送的。
writev或者大buffer的方式在异步双工模式下是无法工作,这个时候只能通过系统方式来解决。 客户端 不设置TCP_NODELAY选项, 发送数据前先打开TCP_CORK选项,发送完后再关闭TCP_CORK,服务端开启TCP_QUICKACK选项
采用STL修改的内核5-6-0-0,打开相关参数
网络编程常见问题总结 串讲(五)
TIME_WAIT有什么样的影响?
对于TIME_WAIT的出现具体可以参考<
可以通过/proc/sys/net/ipv4/ip_local_port_range看到可用端口的范围,我们的机器上一般是 32768 61000, 不足3W个,这样的结果就是导致如果出现500/s的短连接请求,就会导致端口不够用连接不上。 这种情况一般修改系统参数tcp_tw_reuse或者在句柄关闭前设置SO_LINGER选项来解决,也可以通过增大ip_local_port_range来缓解, 设置SO_LINGER后句柄会被系统立刻关闭,不会进入TIME_WAIT状态,不过在一些大压力的情况还是有可能出现连接的替身,导致数据包丢失。 系统参数/proc/sys/net/ipv4/tcp_tw_reuse设为1 会复用TIME_WAIT状态socket,如果开启,客户端在调用connect调用时,会自动复用TIME_WAIT状态的端口,相比SO_LINGER选项更加安全。
对于服务器端如果出现TIME_WAIT状态,是不会产生端口不够用的情况,但是TIME_WAIT过多在服务器端还是会占用一定的内存资源, 在/proc/sys/net/ipv4/tcp_max_xxx 中我们可以系统默认情况下的所允许的最大TIME_WAIT的个数,一般机器上都是180000, 这个对于应付一般程序已经足够了.但对于一些压力非常大的程序而言,这个时候系统会不主动进入TIME_WAIT状态而且是直接跳过, 这个时候如果去看dmsg中的信息会看到 "TCP: time wait bucket table overflow" , 一般来说这种情况是不会产生太多的负面影响, 这种情况下后来的socket在关闭时不会进入TIME_WAIT状态,而是直接发RST包, 并且关闭socket. 不过还是需要关注为什么会短时间内出现这么大量的请求。
小提示: 如果需要设置SO_LINGER选项, 需要在FD连接上之后设置才有效果
什么情况下会出现CLOSE_WAIT状态?
一般来说,连接的一端在被动关闭的情况下,已经接收到FIN包(对端调用close)后,这个时候如果接收到FIN包的一端没有主动close就会出现CLOSE_WAIT的情况。 一般来说,对于普通正常的交互,处于CLOSE_WAIT的时间很短,一般的逻辑是检测到网络出错,马上关闭。 但是在一些情况下会出现大量的CLOS_WAIT, 有的甚至维持很长的时间, 这个主要有几个原因:
没有正确处理网络异常, 特别是read 0的情况, 一般来说被动关闭的时候会出现read 返回0的情况。一般的处理的方式在网络异常的情况下就主动关闭连接句柄泄露了,句柄泄露需要关闭的连接没有关闭而对端又主动断开的情况下也会出现这样的问题。连接端采用了连接池技术,同时维护了较多的长连接(比如ub_client, public/connectpool),同时服务端对于空闲的连接在一定的时间内会主动断开(比如ub_server, ependingpool都有这样的机制). 如果服务端由于超时或者异常主动断开, 客户端如果没有连接检查的机制,不会主动关闭这个连接, 比如ub_client的机制就是长连接建立后除非到使用的时候进行连接检查,否则不会主动断开连接。 这个时候在建立连接的一端就会出现CLOSE_WAIT状态。这个时候的状态一般来说是安全(可控的,不会超过最大连接数). 在com 的connectpool 2中这种情况下可以通过打开健康检查线程进行主动检查,发现断开后主动close.
网络编程常见问题总结 串讲(六)
顺序发送数据,接收端出现乱序接收到的情况:
网络压力大的情况下,有时候会出现,发送端是按照顺序发送, 但是接收端接收的时候顺序不对.
一般来说在正常情况下是不会出现数据顺序错误的情况, 但某些异常情况还是有可能导致的.
在我们的协议栈中,服务端每次建立连接其实都是从accpet所在的队列中取出一个已经建立的fd, 但是在一些异常情况下,可能会出现短时间内建立大量连接的情况, accept的队列长度是有限制, 这里其实有两个队列,一个完成队列另一个是未完成队列,只有完成了三次握手的连接会放到完成队列中。如果在短时间内accept中的fd没有被取出导致队列变满,但未完成队列未满, 这个时候连接会在未完成队列中,对于发起连接的一端来说表现的情况是连接已经成功,但实际上连接本身并没有完成,但这个时候我们依然可以发起写操作并且成功, 只是在进行读操作的时候,由于对端没有响应会造成读超时。对于超时的情况我们一般就把连接直接close关闭了, 但是句柄虽然被关闭了,但是由于TIME_WAIT状态的存在, TCP还是会进行重传。在重传的时候,如果完成队列有句柄被处理,那么此时会完成三次握手建立连接,这个时候服务端照样会进行正常的处理(不过在写响应的时候可能会发生错误)。从接收上看,由于重传成功的情况我们不能控制,对于接收端来说就可能出现乱序的情况。 完成队列的长度和未完成队列的长度由listen时候的baklog决定((ullib库中ul_tcplisten的最后一个参数),在我们的 linux环境中baklog是完成队列的长度,baklog * 1.5是两个队列的总长度(与一些书上所说的两个队列长度不超过baklog有出入). 两个队列的总长度最大值限制是128, 既使设置的结果超过了128也会被自动改为128。128这个限制可以通过 系统参数 /proc/sys/net/core/somaxconn 来更改, 在我们 5-6-0-0 内核版本以后,STL将其提高到2048. 另外客户端也可以考虑使用SO_LINGER参数通过强制关闭连接来处理这个问题,这样在close以后就不启用重传机制。另外的考虑就是对重试机制根据业务逻辑进行改进。
连接偶尔出现超时有哪些可能?
主要几个方面的可能
服务端确实处理能力有限, cpu idel太低, 无法承受这样的压力, 或者 是更后端产生问题
accept队列设置过小,而连接又特别多, 需要增大baklog,建议设置为128这是我们linux系统默认的最大值 由/proc/sys/net/core/somaxconn决定,可以通过修改这个值来增大(由于很多书上这个地方设置为5,那个其实是4.2BSD支持的最大值, 而不是现在的系统, 不少程序中都直接写5了,其实可以更大, 不过超过128还是按照128来算)
程序逻辑问题导致accept处理不过来, 导致连接队列中的连接不断增多直到把accept队列撑爆, 像简单的线程模型(每个线程一个accept), 线程被其他IO一类耗时操作handle,导致accept队列被撑爆, 这个时候默认的逻辑是服务端丢弃数据包,导致client端出现超时, 但是可以通过打开/proc/sys/net/ipv4/tcp_abort_on_overflow开关让服务端立刻返回失败
当读超时的时候(或者其他异常), 我们都会把连接关闭,进行重新连接,这样的行为如果很多,也可能造成accept处理不过来
异常情况下,设置了SO_LINGER造成连接的ack包被丢失, 虽然情况极少,但大压力下还是有存在的.
当然还是有可能是由于网络异常或者跨机房耗时特别多产生的, 这些就不是用户态程序可以控制的。
另外还有发现有些程序采用epoll的单线模式, 但是IO并没有异步化,而是阻塞IO,导致了处理不及时.