网络编程常见问题总结 串讲(一)
网络编程常见问题总结
6 I& I! E- x8 Z+ p- U- B
在网络程序中遇到的一些问题进行了总结, 这里主要针对的是咱们经常使用的
TCP socket
相关的总结, 可能会存在错误, 有任何问题欢迎你们提出.
. e3 Y0 @* _- e1 G- B% R
对于网络编程的更多详细说明建议参考下面的书籍
《UNIX网络编程》 《TCP/IP 详解》 《Unix环境高级编程》 $ ^, `# d2 h9 r6 ~, b* Z: F. L' n
非阻塞IO和阻塞IO: % k, j L4 b1 q3 d9 m+ D x% I
在网络编程中对于一个网络句柄会遇到阻塞IO和非阻塞IO的概念, 这里对于这两种socket先作一下说明
5 /% b8 U! i; /) `
基本概念:
socket的阻塞模式意味着必需要作完IO操做(包括错误)才会返回。 非阻塞模式下不管操做是否完成都会马上返回,须要经过其余方式来判断具体操做是否成功。
设置:
通常对于一个socket是阻塞模式仍是非阻塞模式有两种方式 fcntl设置和recv,send系列的参数.
' J% f& o: ?; S$ w2 V) p
fcntl函数能够将一个socket句柄设置成非阻塞模式:
flags = fcntl(sockfd, F_GETFL, 0); fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
设置以后每次的对于sockfd的操做都是非阻塞的
6 B$ b8 i" _' k: U5 w$ B
recv, send函数的最后有一个flag参数能够设置成MSG_DONTWAIT
临时
将sockfd设置为非阻塞模式,而不管原有是阻塞仍是非阻塞。 recv(sockfd, buff, buff_size, MSG_DONTWAIT); send(scokfd, buff, buff_size, MSG_DONTWAIT);
* l( V- |' G1 U
区别:
读:
读本质来讲其实不能是读,在实际中, 具体的接收数据不是由这些调用来进行,是因为系统底层自动完成的,read也好,recv也好只
负责把数据从底层缓冲copy到咱们指定的位置
. 对于读来讲(read, 或者 recv) ,在阻塞条件下若是没有发现数据在网络缓冲中会一直等待,当发现有数据的时候会把数据读到用户指定的缓冲区,可是若是这个时候读到的数据量比较少,比参数中指定的长度要小,read并不会一直等待下去,而是马上返回。read的原则是数据在不超过指定的长度的时候有多少读多少,没有数据就会一直等待。因此通常状况下咱们读取数据都须要采用循环读的方式读取数据,
一次read完毕不能保证读到咱们须要长度的数据
,read完一次须要判断读到的数据长度再决定是否还须要再次读取。在非阻塞的状况下,read的行为是若是发现没有数据就直接返回,若是发现有数据那么也是采用有多少读多少的进行处理.
对于读而言, 阻塞和非阻塞的区别在于没有数据到达的时候是否马上返回.
recv中有一个 MSG_WAITALL的参数 recv(sockfd, buff, buff_size, MSG_WAITALL), 在正常状况下 recv是会等待直到读取到buff_size长度的数据,可是这里的WAITALL也只是尽可能读全,在有中断的状况下recv仍是可能会 被打断,形成没有读完指定的buff_size的长度。因此即便是采用recv + WAITALL参数仍是要考虑是否须要循环读取的问题,在实验中对于多数状况下recv仍是能够读完buff_size,因此相应的性能会比直接read 进行循环读要好一些。不过要注意的是这个时候的sockfd必须是处于阻塞模式下,不然WAITALL不能起做用。
写:
/ E/ m& A+ B+ r
写的本质也不是进行发送操做
,而是把用户态的数据copy到系统底层去
,
而后再由系统进行发送操做,返回成功只表示数据已经copy到底层缓冲,而不表示数据以及发出,更不能表示对端已经接收到数据
.
对于write(或 者send)而言,在阻塞的状况是会一直等待直到write彻底部的数据再返回.这点行为上与读操做有所不一样,究其缘由主要是读数据的时候咱们并不知道对端到底有没有数据,数据是在何时结束发送的,若是一直等待就可能会形成死循环,因此并无去进行这方面的处理;而对于write, 因为须要写的长度是已知的,因此能够一直再写,直到写完.不过问题是write是可能被打断形成write一次只write一部分数据, 因此write的过程仍是须要考虑循环write, 只不过多数状况下一次write调用就可能成功.
非阻塞写的状况下,是采用能够写多少就写多少的策略.与读不同的地方在于,有多少读多少是由网络发送的那一端是否有数据传输到为标准,可是对于能够写多少是由本地的网络堵塞状况为标准的,在网络阻塞严重的时候,网络层没有足够的内存来进行写操做,这时候就会出现写不成功的状况,阻塞状况下会尽量(有可能被中断)等待到数据所有发送完毕,对于非阻塞的状况就是一次写多少算多少,没有中断的状况下也仍是会出现write到一部分的状况.
网络编程常见问题总结 串讲(二)
超时控制: * Z5 a- [0 {, v: w
对于网络IO,咱们通常状况下都须要超时机制来避免进行操做的线程被handle住,经典的作法就是采用select+非阻塞IO进行判断,select在超时时间内判断是否能够读写操做,而后采用非堵塞读写,不过通常实现的时候读操做不须要设置为非堵塞,上面已经说过读操做只有在没有数据的 时候才会阻塞,select的判断成功说明存在数据,因此即便是阻塞读在这种状况下也是能够作到非阻塞的效果,就没有必要设置成非阻塞的状况了.
这部分的代码能够参考ullib中ul_sreado_ms_ex和ul_swriteo_ms_ex.
% G0 J d: g% C4
采用ul_sreado_ms_ex读数据也是不能保证返回大于0就必定读到指定的数据长度, 对于读写操做, 都是须要判断返回的读长度或者写长度是不是须要的长度, 不能简单的判断一下返回值是否小于0. 对于ul_sreado_ms_ex的状况若是出现了发送端数据发送一半就被close掉的状况就有可能致使接收端读不到完整的数据包.
errno 只有在函数返回值为负的时候才有效,若是返回0或者大于0的数, errno 的结果是无心义的. 有些时候 会出现read到0, 可是咱们认为是错误的状况而后输出errno形成误解,通常建议在这种状况要同时输出返回值和errno的结果,有些状况因为只有errno形成了对于问 题的判断失误。
; j; W& H* d6 _
8 |* J$ m. |$ n;
长链接和短链接的各类可能的问题及相应的处理 ' N9 C; f! {% R& ]" [
这里主要是发起链接的客户端的问题,这里列出的问题主要是在采用同步模型的状况下才会存在的问题.
短链接:
J/ E. u5 V: L
采用短链接的状况通常是考虑到下面的一些问题:
后端服务的问题, 考虑最简单的状况下一个线程一个链接, 若是这个链接采用了长链接那么就须要咱们处理链接的线程和后端保持一一对应,而后按照某些原则进行处理(n对n的关系), 但因为一方面服务器可能增长,这样致使须要先后端保持一致,带来了更多的麻烦,另外一方面线程数上不去对应处理能力也会产生影响,而短链接每次链接的时候只 须要关注当前的机器,问题相对会少一些. 其实这个问题能够采用链接池的方式来解决,后面会提到. 不须要考虑因为异常带来的脏数据。负载均衡方面能够简单考虑, 不管线程数是多少仍是后端服务器的数量是多少都没有关系, 每次考虑单个链接就能够了. 固然若是负载逻辑简单,而且机器相对固定,一个线程一个长链接问题也不大.
规避一些问题, 在过去有些状况下出现长链接大延时,数据没响应等问题, 测试的时候发现换短链接问题就解决了,因为时间关系就没有再继续追查, 事实上这些问题如今基本上都已经定位而且有相关的解决方案了.
不足:
效率不足, 因为链接操做通常会有50ns~200ns的时间消耗,致使短链接须要消耗更多的时间会产生TIME_WAIT问题,须要作更多的守护
长链接:
长链接相比短链接减小了链接的时间消耗, 能够承受更高的负载. 但在使用的时候须要考虑一些问题脏数据, 在一些特殊状况(特别是逻辑错误的状况下) 会存在一些咱们并不须要的数据. 这个时候的处理比较安全的方式是一旦检测到就关闭链接, 检测的方式在在发起请求前用前面为何socket写错误,但用recv检查依然成功? 介绍的方式进行检查. 不过有些程序会采用继续读把全部不须要的数据读完毕(读到 EAEGIN), 不过这种方式过度依赖逻辑了,存在了必定的风险. 不如直接断开来的简单 后端链接, 前面也提到了 在这种状况咱们通常会采用链接池的方式来解决问题好比(public/connectpool中就能够维护不一样的链接,使每一个线程均可以均匀的获取到句 柄) 服务端的处理这个时候须要考虑链接的数量,简单的方式就是一个长链接一个线程, 可是线程也不能无限增长( 增长了,可能形成大量的上下文切换使的性能降低). 咱们通常在长链接的状况采用pendingpool的模型, 经过一个异步队列来缓冲, 这样不须要考虑客户端和服务端的线程数问题,能够任意配置(能够经过线下测试选择合适的线程数)
一些特殊的问题, 主要是长链接的延时 在后面的FAQ中会有详细的说明.
2 A( }! ^5 ~1 O9 B+ V) /
通常来讲,对于咱们多数的内部业务逻辑都是能够采用长链接模式,不会产生太多的问题.
网络编程常见问题总结 串讲(三)
主要线程模型优缺点和注意事项
这里所列出的线程模型,目前在咱们的public/ub下都有相关的实现,在 ubFAQ中也有相关的说明,这里主要针对这些模 型的使用作相关的说明
# X9 s# ^! a! k( X( ^6 w
最简单的线程模型 1 P% c; W) N+ M* e8 L- x6 y 同时启动多个线程,
每一个线程都采用accept的方式进行阻塞获取链接(具体实现上通常是先select在accept, 一方面规避低内核的惊群效应,另外一方面能够作到优雅退出). 多个线程竞争一个链接, 拿到链接的线程就进行本身的逻辑处理, 包括读写IO所有都在一个线程中进行. 短链接每次从新accept, 长链接,第一次的时候accept而后反复使用.通常来讲在总链接数不多的状况下效果会比较好,相对适用于少许短链接(能够容许比线程数多一些)和不超过线程总数的长链接(超过的那些链接,除非 accept的链接断开,不然不可能会有线程对它进行accept).
& }( r# p3 ? Y+ ^8 A: ^9 t
但若是同一时候链接数过多会形成没有工做线程与
客户端进行链接,客户端会出现大量的链接失败, 由于这个时候线程可能存在不能及时accept形成超时问题, 在有重试机制的状况下可能致使问题更糟糕. 有些程序在出现几回超时以后会长时间一直有链接超时每每就是在这种状况下发生的.
3 x) V3 l! o1 c1 ^
这种模型的最大优势在于编写简单, 在正常状况下工做效果不错. 在public/ub中的xpool就是属于这种模型,建议针对链接数少的服务进行使用,好比一些一对一的业务逻辑.
" t' X- p) Z( u% c: @
生产者消费者模型
普通线程模型在长链接方面存在使用限制(须要对于线程数进行变化, 而线程又不是无限的), 短链接在处理同时大量链接(好比流量高峰期)的时候存在问题.
6 N" t9 m5 j" J0 C) p8 x- G
生产者消费者模型是能够把这种影响减小.
2 |5 v& p) b( h! M
对于有数据的活动链接放到异步队列中, 其余线程竞争这个队列获取句柄而后进行相关的操做. 因为accept是专门的线程进行处理, 出现被handle的状况比较少,不容易出现链接失败的状况.在大流量的状况下有必定的缓冲,虽然有些请求会出现延时,但只要在能够接受的范围内,服务还 是能够正常进行. 通常来讲队列的长度主要是考虑能够接受的延时程度.
这种模式也是咱们如今许多服务比较经常使用的模型.能够不用关心客户端和服务的线程数对应关系,业务逻辑上也是比较简单的。
但这种模式在编程的 时候,对于长链接有一个陷阱,判断句柄是否可读写之前通常采用的是select, 若是长链接的链接数比工做线程还少,当全部的链接都被处理了,有链接须要放回pool中,而这个时候若是正常创建链接的监听线程正好处于select状 态,这个时候必需要等到 select超时才能从新将链接放入select中进行监听,由于这以前被放入select进行监听的处理socket为空,不会有响应,这个时候因为时 间的浪费形成l长链接的性能降低。通常来讲某个链接数少,某个链接特别活跃就可能形成问题. 过去的一些作法是控制链接数和服务端的工做线程数以及经过监听一个管道fd,在工做线程结束每次都激活这个fd跳出此次select来控制。如今的2.6 内核中的epoll在判断可读写的时候不会存在这个问题(epoll在进行监听的时候,其它线程放入或者更改, 在epoll_wait的时候是能够立刻激活的), 咱们如今的服务多采用epoll代替select来解决这个, 可是主要的逻辑没有变化. ub_server中epool和public/ependingpool都是采用种模式
- g2 k& T) [! a7 h' H* O2 E- x
异步模型
这里只作一些简单的介绍。
上 面二者模型本质都是同步的处理业务逻辑,在一个线程中处理了读请求,业务逻辑和写回响应三个过程(不少业务更复杂,可是都是能够作相应的拆封的), 可是读和写这两个IO的处理每每须要阻塞等待, 这样形成了线程被阻塞, 若是要应付慢链接(好比外围抓取等待的时间是秒级的甚至更多), 在等待的时候其实CPU没有干多少事情, 这个时候就形成了浪费. 一种考虑是增长线程数,经过提升并发来解决这个问题, 可是咱们目前的线程数仍是有限的,不可能无限增长. 并且线程的增长会带来cpu对于上下文切换的代价,另外一方面多个线程从一个队列中获取可用链接, 这里存在互斥线程多的时候会致使性能降低,固然这里能够经过把一个队列改多队列减小互斥来实现.
, Q; R# Q' O4 j2 V0 E! K$ W v; ^! ?
引入异步化的处理, 就是把对于IO的等待采用IO复用的方式,专门放入到一个或者若干个线程中去, 处理主逻辑的程序能够被释放出来, 只有在IO处理完毕才进行处理, 这样能够提升CPU的使用率,减小等待的时间. 通常状况下几个线程(通常和CPU的核数至关)能够应付很大的流量请求 public/kylin , ub/ub(ub事件模型)都是基于纯异步思想的异步框架。而ub中的appool是简化版本将本来ub框架中网络IO处理进行了异步化,不过目前只支持 采用nshead头的模式。
网络编程常见问题总结 串讲(四)
为何网络程序会没有任何预兆的就退出了 , ~& |- h; d2 ^, }- Q; T- ^$ G: b
通常状况都是没有设置忽略PIPE信号 ,
在咱们的环境中当网络触发broken pipe (通常状况是write的时候,没有write完毕, 接受端异常断开了), 系统默认的行为是直接退出。在咱们的程序中通常都要在启动的时候加上 signal(SIGPIPE, SIG_IGN); 来强制忽略这种错误
write出去的数据, read的时候知道长度吗?
严格来讲, 交互的两端, 一端write调用write出去的长度, 接收端是不知道具体要读多长的. 这里有几个方面的问题
write 长度为n的数据, 一次write不必定能成功(虽然小数据绝大多数都会成功), 须要循环屡次write
0 }% M5 t/ }3 o7 ,
write虽然成功,可是在网络中仍是可能须要拆包和组包, write出来的一块数据, 在接收端底层接收的时候可能早就拆成一片一片的多个数据包. TCP层中对于接收到的数据都是把它们放到缓冲中, 而后read的时候一次性copy, 这个时候是不区分一次write仍是屡次write的。因此对于网络传输中 咱们不能经过简单的read调用知道发送端在此次交互中实际传了多少数据. 通常来讲对于具体的交互咱们通常采起下面的方式来保证交互的正确,事先约定好长度, 双方都采用固定长度的数据进行交互, read, write的时候都是读取固定的长度.可是这样的话升级就必须考虑两端同时升级的问题。特殊的结束符或者约定结束方式, 好比http头中采用连续的/r/n来作头部的结束标志. 也有一些采用的是短链接的方式, 在read到0的时候,传输变长数据的时候通常采用定长头部+变长数据的方式, 这个时候在定长的头部会有一个字段来表示后面的变长数据的长度, 这种模式下通常须要读取两次肯定长度的数据. 咱们如今内部用的不少都是这样的模式. 好比public/nshead就是这样处理, 不过nshead做为通用库另外考虑了采用 通用定长头+用户自定义头+变长数据的接口。
总的来讲read读数 据的时候不能只经过read的返回值来判断到底须要读多少数据, 咱们须要额外的约定来支持, 当这种约定存在错误的时候咱们就能够认为已经出现了问题. 另外对于write数据来讲, 若是相应的数据都是已经准备好了那这个时候也是能够把数据一次性发送出去,不须要调用了屡次write. 通常来讲write次数过多也会对性能产生影响,另外一个问题就是屡次连续可能会产生延时问题,这个参看下面有关长链接延时的部分问题.
& O ~9 E# T0 g, G% @, g% G
小提示
上面提到的都是TCP的状况, 不必定适合其余网络协议. 好比在UDP中 接收到连续2个UDP包, 须要分别读来次才读的出来, 不能像TCP那样,一个read可能就能够成功(假设buff长度都是足够的)。
0 q4 S' U4 W6 h! y) {6
如何查看和观察句柄泄露问题 通常状况句柄只有1024个可使用,因此通常状况下比较容易出现, 也能够经过观察/proc/进程号/fd来观察。
( Y1 b$ ]6 m/ N7 _
另外能够采用valgrind来检查, valgrind参数中加上 --track-fds = yes 就能够看到最后退出的时候没有被关闭的句柄,以及打开句柄的位置
为何socket写错误,但用recv检查依然成功?
7 J: s, F- `) r, I
首先采用recv检查链接的是基于咱们目前的一个请求一个应答的状况对于客户端的请求,逻辑通常是这样 创建链接->发起请求->接受应答->长链接继续发请求
$ O7 e9 j; M, T9 i6 Q$ B
recv检查通常是这样采用下面的方式: ret = recv(sock, buf, sizeof(buf), MSG_DONTWAIT);
经过判断ret 是否为-1而且errno是EAGAIN 在非堵塞方式下若是这个时候网络没有收到数据, 这个时候认为网络是正常的。
这是因为在网络交换模式下 咱们做为一个客户端在发起请求前, 网络中是不该该存在上一次请求留下来的脏数据或者被服务端主动断开(服务端主动断开会收到FIN包,这个时候是recv返回值为0), 异常断开会返回错误. 固然这种方式来判断链接是否存在并非很是完善,在特殊的交互模式(好比异步全双工模式)或者延时比较大的网络中都是存在问题的,不过对于咱们目前内网中的交互模式仍是基本适用的. 这种方式和socket写错误并不矛盾, 写数据超时多是因为网慢或者数据量太大等问题, 这时候并不能说明socket有错误, recv检查彻底可能会是正确的.
通常来讲遇到socket错误,不管是写错误还读错误都是须要关闭重连.
为何接收端失败,但客户端仍然是write成功
+ n. i/ B' N: T& g' M
这个是正常现象,
write数据成功不能表示数据已经被接收端接收致使,只能表示数据已经被复制到系统底层的缓冲(不必定发出), 这个时候的网络异常都是会形成接收端接收失败的.
长链接的状况下出现了不一样程度的 延时 在一些长链接的条件下, 发送一个小的数据包,结果会发现从数据write成功到接收端须要等待必定的时间后才能接收到, 而改为短链接这个现象就消失了(若是没有消失,那么可能网络自己确实存在延时的问题,特别是跨机房的状况下) 在长链接的处理中出现了延时,并且时间固定,基本都是40ms, 出现40ms延时最大的可能就是因为没有设置TCP_NODELAY 在长链接的交互中,有些时候一个发送的数据包很是的小,加上一个数据包的头部就会致使浪费,并且因为传输的数据多了,就可能会形成网络拥塞的状况, 在系统底层默认采用了Nagle算法,能够把连续发送的多个小包组装为一个更大的数据包而后再进行发送. 可是对于咱们交互性的应用程序意义就不大了,在这种状况下咱们发送一个小数据包的请求,就会马上进行等待,不会还有后面的数据包一块儿发送, 这个时候Nagle算法就会产生负做用,在咱们的环境下会产生40ms的延时,这样就会致使客户端的处理等待时间过长, 致使程序压力没法上去. 在代码中不管是服务端仍是客户端都是建议设置这个选项,避免某一端形成延时
。因此对于长链接的状况咱们建议都须要设置TCP_NODELAY
, 在咱们的ub框架下这个选项是默认设置的.
5 y# /" L) o: s& ^% h8 L7 _1 G
小提示: $ r) s/ X; n' z% Q. X: K% c3 e9 h
对于服务端程序而言, 采用的模式通常是
/ Q) h7 t% B7 p5 }5 B6 _9 |' f
bind-> listen -> accept, 这个时候accept出来的句柄的各项属性实际上是从listen的句柄中继承, 因此对于多数服务端程序只须要对于listen进行监听的句柄设置一次TCP_NODELAY就能够了,不须要每次都accept一次.
z: I, O) C+ w2 a3 _: y
设置了NODELAY选项但仍是时不时出现10ms(或者某个固定值)的延时 这种状况最有可能的就是服务端程序存在长链接处理的缺陷. 这种状况通常会发生在使用咱们的pendingpool模型(ub中的cpool)状况下,在 模型的说明中有提到. 因为select没有及时跳出致使一直在浪费时间进行等待.
上面的2个问题都处理了,仍是发现了40ms延时?
协议栈在发送包的时候,其实不只受到TCP_NODELAY的影响,还受到协议栈里面拥塞窗口大小的影响. 在链接发送多个小数据包的时候会致使数据没有及时发送出去.
这里的40ms延时实际上是两方面的问题:
: L) Z s# f0 G& Q7 B
对于发送端, 因为拥塞窗口的存在,在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也会一块儿跟着发送
* m. y3 y: u8 Q# ] @4 L3 c
若是都不知足上面的条件,接收方会延时40ms再发送ACK, 这个时候就形成了延时。
可是对于上面的状况即便是采用TCP_QUICKACK,服务端也不能保证能够及时返回ack包,由于快速回复模式在一些状况下是会失效(只能经过修改内核来实现)
目前的解决方案只能是经过修改内核来解决这个问题,STL的同窗在 内核中增长了参数能够控制这个问题。
会出现这种状况的主要是链接发送多个小数据包或者采用了一些异步双工的编程模式,主要的解决方案有下面几种
3 O( /! U$ O( n! Q6 `; A
对于连续的多个小数据包, 尽可能把他们打到一个buffer中间, 不过会有内存复制的问题
6 i8 D! h0 W. H8 k& H(
采用writev方式发送多个小数据包, 不过writev也存在一个问题就是发送的数据包个数有限制,若是超过了IOV_MAX(咱们的限制通常是1024), 依然可能会出现问题,由于writev只能保证在IOV_MAX范围内的数据是按照连续发送的。
& `8 O, e. K. P- K
writev或者大buffer的方式在异步双工模式下是没法工做,这个时候只能经过系统方式来解决。 客户端 不设置TCP_NODELAY选项, 发送数据前先打开TCP_CORK选项,发送完后再关闭TCP_CORK,服务端开启TCP_QUICKACK选项
采用STL修改的内核5-6-0-0,打开相关参数
网络编程常见问题总结 串讲(五)
5 q! C2 e, S' x; P+ E" {5 A TIME_WAIT有什么样的影响?
对于TIME_WAIT的出现具体能够参考<<UNIX网络编程>>中的章节,
总的来讲对于一个已经创建的链接若是是主动 close, 那么这个链接的端口(注意:不是socket)就会进入到TIME_WAIT状态,在咱们的机器上须要等待60s的时间(有些书上可能提到的是 2MSL,1MSL为1分钟,但咱们的linux实现是按照1分钟的). 在这一段时间内,这个端口将不会被释放,新创建的链接就没法使用这个端口
(链接的时候会报Cannot assign requested address的错误).
7 G% j5 b0 Q9 [* b I( Z1 Z! H# u
能够经过/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选项更加安全。
! f' H( V$ E* ^8 g' b- C1 m
对于服务器端若是出现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链接上以后设置才有效果
: O% B: j3 o/ A
什么状况下会出现CLOSE_WAIT状态? ' n4 P$ d1 K8 n7 X2 M" A
通常来讲,链接的一端在被动关闭的状况下,已经接收到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,致使了处理不及时.
网络编程常见问题总结 串讲(七)
8 c, ?9 X0 n: C" F% R; X
listen的时候的backlog有什么影响?
4 n1 b% K2 Y* V: I) I
backlog表明链接的队列, 这里对于内核中其实会维护2个队列
未完成队列, 这个是服务器端接收到链接请求后会先放到这里(第一次握手)这个时候端口会处于SYN_RCVD状态
已完成队列,完成三次握手的链接会放到这里,这个时候才是链接创建
在咱们 的linux环境中backlog 通常是被定义为已完成队列的长度, 为完成队列通常是按照以完成队列长度的一半来取, backlog为5, 那么已完成队列为5,未完成队列为3, 总共是8个。 若是这里的8个都被占满了,那么后面的链接就会失败,这里的行为能够由 /proc/sys/net/ipv4/tcp_abort_on_overflow 参数控制, 这个参数打开后队列满了会发送RST包给client端,client端会看到Connection reset by peer的错误(线上部份内核打开了这个参数), 若是是关闭的话, 服务端会丢弃此次握手, 须要等待TCP的自动重连, 这个时间通常比较长, 默认状况下第一次须要3秒钟, 因为咱们的链接超时通常都是很小的, client采用ullib库中的超时链接函数, 那么会发现这个时候链接超时了。
长链接和短链接混用是否会有问题?
虽然这种方式并不合适,但严格来讲若是程序中作好相关的守护操做(包括一些状况下系统参数的调整) 是不会出现问 题,基原本说在长短链接混用状况下出现的问题都是因为咱们的程序存在不一样程度上的缺陷形成的.
可能出现的问题:
2 D9 P" M* z# C2 X
只要有一端采用了短链接,那么就能够认为整体是短链接模式。
2 S# O1 /$ Q+ j/ o+ N* T! ]6 T
服务端长链接, 客户端短链接
客户端主动关 闭, 服务端须要接收到close的FIN包, read返回0 后才知道客户端已经被关闭。在这一段时间内其实服务端多维护了一个没有必要链接的状态。在同步模式(pendingpool,ub-xpool, ub-cpool, ub-epool)中因为read是在工做线程中,这个链接至关于线程多作了一次处理,浪费了系统资源。若是是IO异步模式(ub/apool或者使用 ependingpool读回调)则能够立刻发现,不须要再让工做线程进行处理
服务端若是采用普通线程模型(ub-xpool)那么在异常状况下FIN包若是没有及时到达,在这一小段时间内这个处理线程不能处理业务逻辑。若是出现问题的地方比较多这个时候可能会有连锁反应短期内不能相应。
服务端为长链接,对于服务提 供者来讲可能早期测试也是采用长链接来进行测试,这个时候accept的baklog可能设置的很小,也不会出现问题。 可是一旦被大量短链接服务访问就可能出现问题。因此建议listen的时候baklog都设置为128, 咱们如今的系统支持这么大的baklog没有什么问题。
2 `: @1 g! ~+ L; X! B8 |% r
每次老是客户端主动断开,这致使客户端出现了TIME_WIAT的状态,在没有设置SO_LINGER或者改变系统参数的状况下,比较容易出现客户端端口不够用的状况。
服务端短链接,客户端长链接这个时候的问 题相对比较少, 可是若是客户端在发送数据前(或者收完数据后)没有对脏数据进行检查,在写的时候都会出现大量写错误或者读错误,作一次无用的操做,浪费系统资源 通常的建议是采用长链接仍是短链接,两端保持一致, 但采用配置的方式并不合适,这个须要在上线的时候检查这些问题。比较好的方式是把采用长链接仍是短链接放到数据包头部中。客户端发送的时候标记本身是采用 短链接仍是长链接,服务端接收到后按照客户端的状况采起相应的措施,而且告知客户端。特别的若是服务端不支持长链接,也能够告知客户端,服务采用了短连 接
要注意的是,若是采用了一些框架或者库, 在read到0的状况下可能会多打日志,这个对性能的影响可能会比较大。
网络编程常见问题总结 串讲(八)
% D* h& /* ~, V7 i
select, epoll使用上的注意
5 {4 B( E! c; {/ {$ R$ }: [9 x
select, epoll实现上的区别能够参考, 本质上来讲 select, poll的实现是同样的,epoll因为内部采用了树的结构来维护句柄数,而且使用了通知机制,省去了轮询的过程,在对于须要大量链接的状况下在CPU上会有必定的优点.
select默认状况下能够支持句柄数 是1024, 这个能够看/usr/include/bits/typesizes.h 中的__FD_SETSIZE, 在咱们的编译机(不是开发机,是SCMPF平台的机器)这个值已经被修改成51200, 若是select在处理fd超过1024的状况下出现问题可用检查一下编译程序的机器上__FD_SETSIZE是否正确.
epoll在句柄数的限制没有像select那样须要经过改变系统环境中的宏来实现对更多句柄的支持
# Y" ^, K" W6 {5 T5 z
另外咱们发现有些程序在使用epoll的时候打开了边缘触发模式(EPOLLET), 采用边缘触发实际上是存在风险的,在代码中须要很当心,避免因为链接两次数据到达,而被只读出一部分的数据. EPOLLET的本意是在数据状况发生变化的时候激活(好比不可读进入可读状态), 但问题是这个时候若是在一次处理完毕后不能保证fd已经进入了不可读状态(通常来讲是读到EAGIN的状况), 后续可能就一直不会被激活. 通常状况下建议使用EPOLLET模式.一个最典型的问题就是监听的句柄被设置为EPOLLET, 当同时多个链接创建的时候, 咱们只accept出一个链接进行处理, 这样就可能致使后来的链接不能被及时处理,要等到下一次链接才会被激活.
% c! v2 q' E2 A/ Z' @
小提示: ullib 中经常使用的ul_sreado_ms_ex,ul_swriteo_ms_ex内部是采用select的机制,即便是在scmpf平台上编译出来也仍是受到 51200的限制,可用ul_sreado_ms_ex2,和ul_swriteo_ms_ex2这个两个接口来规避这个问题,他们内部不是采用 select的方式来实现超时控制的(须要ullib 3.1.22之后版本)
- q$ G6 r. T- Y+ k
一个进程的socket句柄数只能是1024吗?
, L. `) S. o2 R
答案是否认的, 一台机器上可使用的socket句柄数是由系统参数 /proc/sys/fs/file-max 来决定的.这里的1024只不 过是系统对于一个进程socket的限制,咱们彻底能够采用ulimit的参数把这个值增大,不过增大须要采用root权限,这个不是每一个工程师均可以采 用的.因此 在公司内采用了一个limit的程序,咱们的全部的机器上都有预装这个程序,这个程序已经经过了提权能够以root的身份设置ulimit的 结果.使用的时候 limit ./myprogram 进行启动便可, 默认是能够支持51200个句柄,采用limit -n num 能够设置实际的句柄数. 若是还须要更多的链接就须要用ulimit进行专门的操做.
( T% B# y. G, j4 F/ E' J/ r1 h# K9 v
另外就是对于内核中还有一个宏NR_OPEN会限制fd的作大个数,目前这个值是1024*1024
小提示: linux系统中socket句柄和文件句柄是不区分的,若是文件句柄+socket句柄的个数超过1024一样也会出问题,这个时候也须要limit提升句柄数.
ulimit对于非root权限的账户而言只能往小的值去设置, 在终端上的设置的结果通常是针对本次shell的, 要还原退出终端从新进入就能够了。
用limit方式启动,程序读写的时候出core?
0 M; G# S2 W v
这个又是另一个问题,前面已经提到了在网络程序中对于超时的控制是每每会采用select或者poll的方式.select的时候对于支持的FD其 实是有上限的,能够看/usr/inclue/sys/select.h中对于fd_set的声明,其实一个__FD_SETSIZE /(8*sizeof(long))的long数组,在默认状况下__FD_SETSIZE的定义是1024,这个能够看 /usr/include/bits/typesizes.h 中的声明,若是这个时候这个宏仍是1024,那么对于采用select方式实现的读写超时控制程序在处理超过1024个句柄的时候就会致使内存越界出 core .咱们的程序若是是线下编译,因为许多开发机和测试这个参数都没有修改,这个时候就会形成出core,其实不必定出core甚至有些状况下会出现有数据但 仍是超时的状况. 但对于咱们的SCMPF平台上编译出来的程序是正常的,SCMPF平台上这个参数已经进行了修改,因此有时会出现QA测试没问题,RD 自测有问题的状况。
一台机器最多能够创建多少链接?
理论上来讲这个是能够很是多的, 取决于可使用多少的内存.咱们的系统通常采用一个四元组来表示一个惟一的链接{客户端ip, 客户端端口, 服务端ip, 服务端端口} (有些地方算上TCP, UDP表示成5元组), 在网络链接中对于服务端采用的通常是bind一个固定的端口, 而后监听这个端口,在有链接创建的时候进行accept操做,这个时候全部创建的链接都只 用到服务端的一个端口.对于一个惟一的链接在服务端ip和 服务端端口都肯定的状况下,同一个ip上的客户端若是要创建一个链接就须要分别采用不一样的端,一台机器上的端口是有限,最多65535(一个 unsigned char)个,在系统文件/proc/sys/net/ipv4/ip_local_port_range 中咱们通常能够看到32768 61000 的结果,这里表示这台机器可使用的端口范围是32768到61000, 也就是说事实上对于客户端机器而言可使用的链接数还不足3W个,固然咱们能够调整这个数值把可用端口数增长到6W. 可是这个时候对于服务端的程序彻底不受这个限制由于它都是用一个端口,这个时候服务端受到是链接句柄数的限制,在上面对于句柄数的说明已经介绍过了,一个 进程能够创建的句柄数是由/proc/sys/fs/file-max决定上限和ulimit来控制的.因此这个时候服务端彻底能够创建更多的链接,这个 时候的主要问题在于如何维护和管理这么多的链接,经典的一个链接对应一个线程的处理方式这个时候已经不适用了,须要考虑采用一些异步处理的方式来解决, 毕竟线程数的影响放在那边
}$ u( [; s( N4 J9 d
小提示: 通常的服务模式都是服务端一个端口,客户端使用不一样的端口进行链接,可是其实咱们也是能够把这个过程倒过来,咱们客户端只用一个端可是服务端确是不一样的端 口,客户端作下面的修改原有的方式 socket分配句柄-> connect 分配的句柄 改成 socket分配句柄 ->对socket设置SO_REUSEADDR选项->像服务端同样bind某个端口->connect 就能够实现
不过这种应用相对比较少,对于像网络爬虫这种状况可能相对会比较适用,只不过6w链接已经够多了,继续增长的意义不必定那么大就是了.
' l- ?) D* @3 R, x) q7 Z' L' f
对于一个不存在的ip创建链接是超时仍是立刻返回?
8 a; k9 H. e7 o' m 这个要根据状况来看, 通常状况connect一个不存在的ip地址,发起链接的服务须要等待ack的返回,因为ip地址不存在,不会有返回,这个时候会一直等到超时才返回。如 果链接的是一个存在的ip,可是相应的端口没有服务,这个时候会立刻获得返回,收到一个ECONNREFUSED(Connection refused)的结果。 可是在咱们的网络会存在一些有限制的路由器,好比咱们一些机器不容许访问外网,这个时候若是访问的ip是一个外网ip(不管是否存在),这个时候也会立刻返回获得一个Network is unreachable的错误,不须要等待。