網絡編程中超時時間是一個重要但又容易被忽略的問題,對其的設置須要仔細斟酌。在經歷了數次物理機宕機以後,筆者詳細的考察了在網絡編程(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的編程模式可讓應用層對這些事件作出響應。
網絡編程中超時時間是個重要但又容易被忽略的問題,這個問題只有在遇到物理機宕機等平時遇不到的現象時候纔會凸顯。筆者在經歷數次物理機宕機以後纔好好的研究了一番,但願本篇文章能夠對讀者在之後遇到相似超時問題時有所幫助。