网络编程中超时时间是一个重要但又容易被忽略的问题,对其的设置须要仔细斟酌。在经历了数次物理机宕机以后,笔者详细的考察了在网络编程(tcp)中的各类超时设置,因而就有了本篇博文。本文大部分讨论的是socket设置为block的状况,即setNonblock(false),仅在最后说起了nonblock socket(本文基于linux 2.6.32-431内核)。java
在讨论connectTimeout以前,让咱们先看下java和C语言对于socket connect调用的函数签名:linux
java: // 函数调用中携带有超时时间 public void connect(SocketAddress endpoint, int timeout) ; C语言: // 函数调用中并不携带超时时间 int connect(int sockfd, const struct sockaddr * sockaddr, socklen_t socklent)
操做系统提供的connect系统调用并无提供timeout的参数设置而java却有,咱们先考察一下原生系统调用的超时策略。编程
咱们观察一下此系统调用的kernel源码,调用栈以下所示:api
connect[用户态] |->SYSCALL_DEFINE3(connect)[内核态] |->sock->ops->connect
因为咱们考察的是tcp的connect,其socket的内部结构以下图所示:
最终调用的是tcp_connect,代码以下所示:网络
int tcp_connect(struct sock *sk) { ...... // 发送SYN err = tcp_transmit_skb(sk, buff, 1, sk->sk_allocation); ... /* Timer for repeating the SYN until an answer. */ // 因为是刚创建链接,因此其rto是TCP_TIMEOUT_INIT inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, inet_csk(sk)->icsk_rto, TCP_RTO_MAX); return 0; }
又上面代码可知,在tcp_connect设置了重传定时器以后return回了tcp_v4_connect再return到inet_stream_connect。咱们继续考察:socket
int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr, int addr_len, int flags) { ...... // tcp_v4_connect=>tcp_connect err = sk->sk_prot->connect(sk, uaddr, addr_len); // 这边用的是sk->sk_sndtimeo timeo = sock_sndtimeo(sk, flags & O_NONBLOCK); ...... inet_wait_for_connect(sk, timeo)); ...... out: release_sock(sk); return err; sock_error: err = sock_error(sk) ? : -ECONNABORTED; sock->state = SS_UNCONNECTED; if (sk->sk_prot->disconnect(sk, flags)) sock->state = SS_DISCONNECTING; goto out }
由上面代码可见,能够采用设置SO_SNDTIMEO来控制connect系统调用的超时,以下所示:tcp
setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);
若是不设置SO_SNDTIMEO,那么会由tcp重传定时器在重传超过设置的时候后超时,以下图所示:ide
这个syn重传的次数由:函数
cat /proc/sys/net/ipv4/tcp_syn_retries 笔者机器上是5
来决定。那么咱们就来看一下这个重传究竟是多长时间:spa
tcp_connect中: // 设置的初始超时时间为icsk_rto=TCP_TIMEOUT_INIT为1s inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
其重传定时器的回掉函数为tcp_retransmit_timer:
void tcp_retransmit_timer(struct sock *sk){ ...... // 检测是否超时 if (tcp_write_timeout(sk)) goto out; ...... // icsk_rto = icsk_rto * 2,因为syn阶段,因此isck_rto不会因为网络传输而改变 // 重传的时候会以1,2,4,8指数递增 icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX); // 重设timer inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, icsk->icsk_rto, TCP_RTO_MAX);out:; }
而计算tcp_write_timeout的逻辑则是在这篇blog中已经详细描述过,
https://my.oschina.net/alchemystar/blog/1936433
只不过在connect时刻,重传的计算以TCP_TIMEOUT_INIT为单位进行计算。而ESTABLISHED(read/write)时刻,重传以TCP_RTO_MIN进行计算。那么根据这段重传逻辑,咱们就能够计算出不一样tcp_syn_retries最终表现的超时时间。以下图所示:
那么整理下表格,对于系统调用,connect的超时时间为:
tcp_syn_retries | timeout |
---|---|
1 | min(so_sndtimeo,3s) |
2 | min(so_sndtimeo,7s) |
3 | min(so_sndtimeo,15s) |
4 | min(so_sndtimeo,31s) |
5 | min(so_sndtimeo,63s) |
上述超时时间和笔者的实测一致。
值得注意的是,linux自己官方发布的2.6.32源码对于tcp_syn_retries2的解释和RFC并不一致(至少笔者阅读的代码如此,这个细微的变化困扰了笔者很久,笔者下载了和机器对应的内核版本后才发现代码改了)。而redhat发布的2.6.32-431已经修复了这个问题(不清楚具体哪一个小版本修改的),并将初始RTO设置为1s(官方2.6.32为3s)。这也是,不一样内核小版本上的实验会有不一样的connect timeout表现的缘由(有的抓包到的重传SYN时间间隔为3,6,12......)。如下为代码对比:
========================>linux 内核版本2.6.32-431<========================#define TCP_TIMEOUT_INIT ((unsigned)(1*HZ)) /* RFC2988bis initial RTO value */static inline bool retransmits_timed_out(struct sock *sk, unsigned int boundary, unsigned int timeout, bool syn_set) { ...... unsigned int rto_base = syn_set ? TCP_TIMEOUT_INIT : TCP_RTO_MIN; ...... timeout = ((2 << boundary) - 1) * rto_base; ...... } ========================>linux 内核版本2.6.32.63<========================#define TCP_TIMEOUT_INIT ((unsigned)(3*HZ)) /* RFC 1122 initial RTO value */static inline bool retransmits_timed_out(struct sock *sk, unsigned int boundary { ...... timeout = ((2 << boundary) - 1) * TCP_RTO_MIN; ...... }
另外,tcp_syn_retries重传次数能够在单个socket中经过setsockopt设置。
如今咱们考察下java的connect api,其connect最终调用下面的代码:
Java_java_net_PlainSocketImpl_socketConnect(...){ if (timeout <= 0) { ...... connect_rv = NET_Connect(fd, (struct sockaddr *)&him, len); ..... }else{ // 若是timeout > 0 ,则设置为nonblock模式 SET_NONBLOCKING(fd); /* no need to use NET_Connect as non-blocking */ connect_rv = connect(fd, (struct sockaddr *)&him, len); /* * 这边用系统调用select来模拟阻塞调用超时 */ while (1) { ...... struct timeval t; t.tv_sec = timeout / 1000; t.tv_usec = (timeout % 1000) * 1000; connect_rv = NET_Select(fd+1, 0, &wr, &ex, &t); ...... } ...... // 从新设置为阻塞模式 SET_BLOCKING(fd); ...... } }
其和connect系统调用的不一样点是,在timeout为0的时候,走默认的系统调用不设置超时时间的逻辑。在timeout>0时,将socket设置为非阻塞,而后用select系统调用去模拟超时,而没有走linux自己的超时逻辑,以下图所示:
因为没有java并无设置so_sndtimeo的选项,因此在timeout为0的时候,直接就经过重传次数来控制超时时间。而在调用connect时设置了timeout(不为0)的时候,超时时间以下表格所示:
tcp_syn_retries | timeout |
---|---|
1 | min(timeout,3s) |
2 | min(timeout,7s) |
3 | min(timeout,15s) |
4 | min(timeout,31s) |
5 | min(timeout,63s) |
socket的write系统调用最后调用的是tcp_sendmsg,源码以下所示:
int tcp_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg, size_t size){ ...... timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT); ...... while (--iovlen >= 0) { ...... // 此种状况是buffer不够了 if (copy <= 0) { new_segment: ...... if (!sk_stream_memory_free(sk)) goto wait_for_sndbuf; skb = sk_stream_alloc_skb(sk, select_size(sk),sk->sk_allocation); if (!skb) goto wait_for_memory; } ...... } ...... // 这边等待write buffer有空间wait_for_sndbuf: set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);wait_for_memory: if (copied) tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH); // 这边等待timeo长的时间 if ((err = sk_stream_wait_memory(sk, &timeo)) != 0) goto do_error; ......out: // 若是拷贝了数据,则返回 if (copied) tcp_push(sk, flags, mss_now, tp->nonagle); TCP_CHECK_TIMER(sk); release_sock(sk); return copied; out_err: // error的处理 err = sk_stream_error(sk, flags, err); TCP_CHECK_TIMER(sk); release_sock(sk); return err; }
从上面的内核代码看出,若是socket的write buffer依旧有空间的时候,会立马返回,并不会有timeout。可是write buffer不够的时候,会等待SO_SNDTIMEO的时间(nonblock时候为0)。可是若是SO_SNDTIMEO没有设置的时候,默认初始化为MAX_SCHEDULE_TIMEOUT,能够认为其超时时间为无限。那么其超时时间会有另外一个条件来决定,咱们看下sk_stream_wait_memory的源码:
int sk_stream_wait_memory(struct sock *sk, long *timeo_p){ // 等待socket shutdown或者socket出现err sk_wait_event(sk, ¤t_timeo, sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN) || (sk_stream_memory_free(sk) && !vm_wait)); }
在write等待的时候,若是出现socket被shutdown或者socket出现错误的时候,则会跳出wait进而返回错误。在不考虑对端shutdown的状况下,出现sk_err的时间其实就是其write的timeout时间,那么咱们看下何时出现sk->sk_err。
物理机宕机后,tcp发送msg的时候,ack不会返回,则会在重传定时器tcp_retransmit_timer到期后timeout,其重传到期时间经过tcp_retries2以及TCP_RTO_MIN计算出来。其源码可见笔者的blog:
https://my.oschina.net/alchemystar/blog/1936433
tcp_retries2的设置位置为:
cat /proc/sys/net/ipv4/tcp_retries2 笔者机器上是5,默认是15
和上面ack超时有些许不同的是,一个逻辑是用TCP_RTO_MIN经过tcp_retries2计算出来的时间。另外一个是真的经过重传超过tcp_retries2次数来time_out,二者的区别和rto的动态计算有关。可是能够大体认为是一致的。
tcp_retries2 | buffer未满 | buffer满 |
---|---|---|
5 | 当即返回 | min(SO_SNDTIMEO,(25.6s-51.2s)根据动态rto定 |
15 | 当即返回 | min(SO_SNDTIMEO,(924.6s-1044.6s)根据动态rto定 |
java的sockWrite0没有设置超时时间的地方,同时也没有设置过SO_SNDTIMEOUT,其直接调用了系统调用,因此其超时时间和write系统调用保持一致。
ReadTimeout多是最容易致使问题的地方。咱们先看下系统调用的源码:
socket的read系统调用最终调用的是tcp_recvmsg, 其源码以下:
int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len, int nonblock, int flags, int *addr_len) { ...... // 这边timeo=SO_RCVTIMEO timeo = sock_rcvtimeo(sk, nonblock); ...... do{ ...... // 下面这一堆判断代表,若是出现错误,或者已经被CLOSE/SHUTDOWN则跳出循环 if(copied) { if (sk->sk_err || sk->sk_state == TCP_CLOSE || (sk->sk_shutdown & RCV_SHUTDOWN) || !timeo || signal_pending(current)) break; } else { if (sock_flag(sk, SOCK_DONE)) break; if (sk->sk_err) { copied = sock_error(sk); break; } // 若是socket shudown跳出 if (sk->sk_shutdown & RCV_SHUTDOWN) break; // 若是socket close跳出 if (sk->sk_state == TCP_CLOSE) { if (!sock_flag(sk, SOCK_DONE)) { /* This occurs when user tries to read * from never connected socket. */ copied = -ENOTCONN; break; } break; } ....... } ....... if (copied >= target) { /* Do not sleep, just process backlog. */ release_sock(sk); lock_sock(sk); } else /* 若是没有读到target本身数(和水位有关,能够暂认为是1),则等待SO_RCVTIMEO的时间 */ sk_wait_data(sk, &timeo); } while (len > 0); ...... }
上面的逻辑以下图所示:
重传以及探测定时器timeout事件的触发时机以下图所示:
若是内核层面ack正常返回并且对端窗口不为0,仅仅应用层不返回任何数据,那么就会无限等待,直到对端有数据或者socket close/shutdown为止,以下图所示:
不少应用就是基于这个无限超时来设计的,例如activemq的消费者逻辑。
java的超时时间由SO_TIMOUT决定,而linux的socket并无这个选项。其sockRead0和上面的java connect同样,在SO_TIMEOUT>0的时候依旧是由nonblock socket模拟,在此就再也不赘述了。
C系统调用:
tcp_retries2 | 对端无响应 | 对端内核响应正常 |
---|---|---|
5 | min(SO_RCVTIMEO,(25.6s-51.2s)根据动态rto定 | SO_RCVTIMEO==0?无限,SO_RCVTIMEO) |
15 | min(SO_RCVTIMEO,(924.6s-1044.6s)根据动态rto定 | SO_RCVTIMEO==0?无限,SO_RCVTIMEO) |
Java系统调用
tcp_retries2 | 对端无响应 | 对端内核响应正常 |
---|---|---|
5 | min(SO_TIMEOUT,(25.6s-51.2s)根据动态rto定 | SO_TIMEOUT==0?无限,SO_RCVTIMEO |
15 | min(SO_TIMEOUT,(924.6s-1044.6s)根据动态rto定 | SO_TIMEOUT==0?无限,SO_RCVTIMEO |
对端物理机宕机时对端内核也gg了(不会发出任何包通知宕机),那么本端发送任何数据给对端都不会有响应。其超时时间就由上面讨论的
min(设置的socket超时[例如SO_TIMEOUT],内核内部的定时器超时来决定)。
这时候若是设置了超时时间timeout,则在timeout后返回。可是,若是仅仅是在read等待,因为底层没有数据交互,那么其没法知道对端是否宕机,因此会一直等待。可是,内核会在一个socket两个小时都没有数据交互状况下(可设置)启动keepalive定时器来探测对端的socket。以下图所示:
大概是2小时11分钟以后会超时返回。keepalive的设置由内核参数指定:
cat /proc/sys/net/ipv4/tcp_keepalive_time 7200 即两个小时后开始探测cat /proc/sys/net/ipv4/tcp_keepalive_intvl 75 即每次探测间隔为75scat /proc/sys/net/ipv4/tcp_keepalve_probes 9 即一共探测9次
能够在setsockops中对单独的socket指定是否启用keepalive定时器(java也能够)。
和上面同理,也是在keepalive定时器超时以后,将链接close。因此咱们能够看到一个不活跃的socket在对端物理机忽然宕机以后,依旧是ESTABLISHED状态,过很长一段时间以后才会关闭。
若是仅仅是对端进程宕机的话(进程所在内核会close其所拥有的全部socket),因为fin包的发送,本端内核能够马上知道当前socket的状态。若是socket是阻塞的,那么将会在当前或者下一次write/read系统调用的时候返回给应用层相应的错误。若是是nonblock,那么会在select/epoll中触发出对应的事件通知应用层去处理。
若是fin包没发送到对端,那么在下一次write/read的时候内核会发送reset包做为回应。
设置为nonblock=true后,因为read/write都是马上返回,且经过select/epoll等处理重传超时/probe超时/keep alive超时/socket close等事件,因此根据应用层代码决定其超时特性。定时器超时事件发生的时间如上面几小节所述,和是否nonblock无关。nonblock的编程模式可让应用层对这些事件作出响应。
网络编程中超时时间是个重要但又容易被忽略的问题,这个问题只有在遇到物理机宕机等平时遇不到的现象时候才会凸显。笔者在经历数次物理机宕机以后才好好的研究了一番,但愿本篇文章能够对读者在之后遇到相似超时问题时有所帮助。