setsockopt, TCP_NODELAY和連包
原文發表在:
ofollow,noindex"> https:// holmeshe.me/network-ess entials-setsockopt-TCP_NODELAY/一般情況下,系統瓶頸由 延時 決定,而不是 吞吐量 。然而 TCP 套接字預設開啟了所謂的" nagle演算法 ",會延緩發包時間,以便和後面(需要傳送)的網路包合併在一起傳送。這個演算法主要用於減少網路包的數量,從而減少TCP報文頭的吞吐量開銷。 setsockopt, TCP_NODELAY and Packet Aggregation I 一般情況下,系統瓶頸由 延時 決定,而不是 吞吐量 。然而 TCP 套接字預設開啟了所謂的" nagle演算法 ",會延緩發包時間,以便和後面(需要傳送)的網路包合併在一起傳送。這個演算法主要用於減少網路包的數量,從而減少TCP報文頭的吞吐量開銷。
鎖和阻塞操作歷來都是後臺程式設計的忌諱,所以我看到這個演算法的第一反應是:人家千辛萬苦就是要減少延時,這裡為啥反而要增加延時呢?於是便決定詳細瞭解一下。
軟體環境
客戶端作業系統:Debian 4.9.88
服務端作業系統(區域網和廣域網):Ubuntu 16.04
gcc:6.3.0
硬體(虛擬機器)環境
伺服器(區域網): Intel® Core™2 Duo CPU E8400 @ 3.00GHz × 2, 4GB
伺服器(廣域網): t2.micro, 1GB
nagle演算法對延時的影響
先打上碼
客戶端:
...include int main(int argc, char *argv[]) { int sfd, portno, n, delay; struct sockaddr_in srvaddr; struct hostent *host; char buffer[256] = "abc"; if (argc < 3) { fprintf(stderr,"usage: %s ip port delay\n", argv[0]); exit(0); } portno = atoi(argv[2]); sfd = socket(AF_INET, SOCK_STREAM, 0); if (sfd < 0) { perror("ERROR: socket()"); exit(0); } //int flags =1; //if (setsockopt(sfd, SOL_TCP, TCP_NODELAY, (void *)&flags, sizeof(flags))) { perror("ERROR: setsocketopt(), TCP_NODELAY"); exit(0); }; host = gethostbyname(argv[1]); if (host == NULL) { fprintf(stderr,"ERROR: host does not exist"); exit(0); } delay = atoi(argv[3]); bzero((char *) &srvaddr, sizeof(srvaddr)); srvaddr.sin_family = AF_INET; srvaddr.sin_port = htons(portno); bcopy((char *)host->h_addr, (char *)&srvaddr.sin_addr.s_addr, host->h_length); if (connect(sfd, (struct sockaddr *)&srvaddr, sizeof(srvaddr)) < 0) { perror("ERROR: connect()"); exit(0); } for (int i = 0; i < 1000; i++) { n = write(sfd, buffer, 4); if (n < 0) { perror("ERROR: writing()"); exit(0); } usleep(delay); } printf("finished\n"); return 0; }
服務端:
...include #define BUF_SIZE 256 int main(int argc, char *argv[]) { int sfd, rfd, portno, clilen; char buffer[BUF_SIZE]; struct sockaddr_in serv_addr, cli_addr; int n; if (argc < 2) { perror("ERROR: no port\n"); exit(1); } sfd = socket(AF_INET, SOCK_STREAM, 0); if (sfd < 0) { perror("ERROR: socket()"); exit(1); } bzero((char *) &serv_addr, sizeof(serv_addr)); portno = atoi(argv[1]); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(portno); if (bind(sfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) { perror("ERROR: bind()"); exit(1); } listen(sfd, SOMAXCONN); while (1) { clilen = sizeof(cli_addr); rfd = accept(sfd, (struct sockaddr *) &cli_addr, &clilen); if (rfd < 0) { perror("ERROR: accept()"); exit(1); } while (1) { n = read(rfd, buffer, BUF_SIZE); if (n <= 0) { printf("read() ends\n"); break; } } } return 0; }
簡單解釋一下。客戶端會發送1000個4位元組的網路包,每次發包的間隔由命令列引數決定。並且,如上所述,這段程式會使用TCP套接字的預設行為。伺服器端則僅僅實現了 拋棄伺服器 ,具體程式碼就不解釋了。
測一下。接下來我會記錄在不同的發包頻率下的連包的數量,並在區域網(RTT < 0.6ms)和廣域網(RTT ≈ 200ms)都做一遍。


如圖所示,連包的數量在發包間隔大於RTT時會趨近於0,這也符合<<TCP/IP Illustrated>>裡所描述的:
This algorithm says that a TCP connection can have only one outstanding small segment that has not yet been acknowledged. No additional small segments can be sent until the acknowledgment is received.
直接檢視 tcpdump 的輸出,我們可以發現無論程式以何種頻率呼叫 write(2)
, nagle演算法 都會把實際發包間隔設定為近似於 RTT,並將兩次實際發包的所有網路包合併。
… 18:34:52.986972 IP debian.53700 > ******.compute.amazonaws.com.6666: Flags [P.], seq 4:12, ack 1, win 229, options [nop,nop,TS val 7541746 ecr 2617170332], length 8 18:34:53.178277 IP debian.53700 > ******.amazonaws.com.6666: Flags [P.], seq 12:20, ack 1, win 229, options [nop,nop,TS val 7541794 ecr 2617170379], length 8 18:34:53.369431 IP debian.53700 > ******.amazonaws.com.6666: Flags [P.], seq 20:32, ack 1, win 229, options [nop,nop,TS val 7541842 ecr 2617170427], length 12 18:34:53.560351 IP debian.53700 > ******.amazonaws.com.6666: Flags [P.], seq 32:40, ack 1, win 229, options [nop,nop,TS val 7541890 ecr 2617170475], length 8 18:34:54.325242 IP debian.53700 > ******.amazonaws.com.6666: Flags [P.], seq 68:80, ack 1, win 229, options [nop,nop,TS val 7542081 ecr 2617170666], length 12 …
平均下來, nagle演算法 對每個網路包所增加的延時是 2 * RTT
ACK確認延遲
ACK確認延遲 則是另外一套相似的演算法,這裡我直接用<<TCP/IP Illustrated>>的描述:
TCP will delay an ACK up to 200 ms to see if there is data to send with the ACK.
TCP會延後(最多200毫秒)傳送ACK,以便將後續可能出現的資料包和ACK一起傳送。
解釋一下。在某些情況,比如說後臺接收到請求後並沒有及時返回的資料, ACK確認延遲 會等待另外一組請求,而這組請求卻在 nagle演算法 下等待上一波請求的ACK。於是,這兩套演算法成功的構成了 死鎖 。
由於在我的環境下無法用
flags = 0; flglen = sizeof(flags); getsockopt(sfd, SOL_TCP, TCP_QUICKACK, &flags, &flglen)
開啟 ACK確認延時 ,這個短暫死鎖的實際效果就沒測了。
寫在最後
在我寫這篇文章時,除了(過時的) telnet ,大部分的應用,包括前端的 ( Firefox , Chromium ),後端的 ( nginx , memcached ),以及 telnet 的繼任 ssh ,都用類似以下的程式碼禁用了 nagle演算法 ,
int flags =1; setsockopt(sfd, SOL_TCP, TCP_NODELAY, (void *)&flags, sizeof(flags));
說直白點,就是 不要阻塞 , 不要阻塞 , 不要阻塞。
… 18:22:38.983278 IP debian.43808 > 192.168.1.71.6666: Flags [P.], seq 1:5, ack 1, win 229, options [nop,nop,TS val 7358245 ecr 6906652], length 4 18:22:38.984149 IP debian.43808 > 192.168.1.71.6666: Flags [P.], seq 5:9, ack 1, win 229, options [nop,nop,TS val 7358246 ecr 6906652], length 4 18:22:38.985028 IP debian.43808 > 192.168.1.71.6666: Flags [P.], seq 9:13, ack 1, win 229, options [nop,nop,TS val 7358246 ecr 6906653], length 4 18:22:38.985897 IP debian.43808 > 192.168.1.71.6666: Flags [P.], seq 13:17, ack 1, win 229, options [nop,nop,TS val 7358246 ecr 6906653], length 4 18:22:38.986765 IP debian.43808 > 192.168.1.71.6666: Flags [P.], seq 17:21, ack 1, win 229, options [nop,nop,TS val 7358246 ecr 6906653], length 4 …
個人認為大家都用 TCP_NODELAY
的原因如下:
1) 頻寬的增加,所以 nagle演算法 越來越變成一個“過度優化” - 如果要讓現代的某一條通路飽和,需要上十萬條小包體,而
2)傳送這麼多小包體的應用,一般都對實時性(低延時)有很高的要求。
畢竟,玩慣了擼啊擼,80年代的回合(這裡是200ms一個回合)制RPG還是有點過時了。
