共計(jì) 8632 個(gè)字符,預(yù)計(jì)需要花費(fèi) 22 分鐘才能閱讀完成。
本篇文章為大家展示了 Linux 下 TCP 延遲確認(rèn) Delayed Ack 機(jī)制導(dǎo)致的時(shí)延問題怎么解決,內(nèi)容簡(jiǎn)明扼要并且容易理解,絕對(duì)能使你眼前一亮,通過這篇文章的詳細(xì)介紹希望你能有所收獲。
案例一:同事隨手寫個(gè)壓力測(cè)試程序,其實(shí)現(xiàn)邏輯為:每秒鐘先連續(xù)發(fā) N 個(gè) 132 字節(jié)的包,然后連續(xù)收 N 個(gè)由后臺(tái)服務(wù)回顯回來的 132 字節(jié)包。其代碼簡(jiǎn)化如下:
char sndBuf[132];
char rcvBuf[132];
while (1) { for (int i = 0; i N; i++){ send(fd, sndBuf, sizeof(sndBuf), 0);
...
}
for (int i = 0; i N; i++) { recv(fd, rcvBuf, sizeof(rcvBuf), 0);
...
}
sleep(1);
}
在實(shí)際測(cè)試中發(fā)現(xiàn),當(dāng) N 大于等于 3 的情況,第 2 秒之后,每次第三個(gè) recv 調(diào)用,總會(huì)阻塞 40 毫秒左右,但在分析 Server 端日志時(shí),發(fā)現(xiàn)所有請(qǐng)求在 Server 端處理時(shí)耗均在 2ms 以下。
當(dāng)時(shí)的具體定位過程如下:先試圖用 strace 跟蹤客戶端進(jìn)程,但奇怪的是:一旦 strace attach 上進(jìn)程,所有收發(fā)又都正常,不會(huì)有阻塞現(xiàn)象,一旦退出 strace,問題重現(xiàn)。經(jīng)同事提醒,很可能是 strace 改變了程序或系統(tǒng)的某些東西 (這個(gè)問題現(xiàn)在也還沒搞清楚),于是再用 tcpdump 抓包分析,發(fā)現(xiàn) Server 后端在回現(xiàn)應(yīng)答包后,Client 端并沒有立即對(duì)該數(shù)據(jù)進(jìn)行 ACK 確認(rèn),而是等待了近 40 毫秒后才確認(rèn)。經(jīng)過 Google,并查閱《TCP/IP 詳解卷一: 協(xié)議》得知,此即 TCP 的延遲確認(rèn)(Delayed Ack) 機(jī)制。
其解決辦法如下:在 recv 系統(tǒng)調(diào)用后,調(diào)用一次 setsockopt 函數(shù),設(shè)置 TCP_QUICKACK。最終代碼如下:
char sndBuf[132];
char rcvBuf[132];
while (1) { for (int i = 0; i N; i++) { send(fd, sndBuf, 132, 0);
...
}
for (int i = 0; i N; i++) { recv(fd, rcvBuf, 132, 0);
setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, (int[]){1}, sizeof(int));
}
sleep(1);
}
案例二:在營(yíng)銷平臺(tái)內(nèi)存化 CDKEY 版本做性能測(cè)試時(shí),發(fā)現(xiàn)請(qǐng)求時(shí)耗分布異常:90% 的請(qǐng)求均在 2ms 以內(nèi),而 10% 左右時(shí)耗始終在 38-42ms 之間,這是一個(gè)很有規(guī)律的數(shù)字:40ms。因?yàn)橹敖?jīng)歷過案例一,所以猜測(cè)同樣是因?yàn)檠舆t確認(rèn)機(jī)制引起的時(shí)耗問題,經(jīng)過簡(jiǎn)單的抓包驗(yàn)證后,通過設(shè)置 TCP_QUICKACK 選項(xiàng),得以解決時(shí)延問題。
延遲確認(rèn)機(jī)制
在《TCP/IP 詳解卷一:協(xié)議》第 19 章對(duì)其進(jìn)行原理進(jìn)行了詳細(xì)描述:TCP 在處理交互數(shù)據(jù)流 (即 Interactive Data Flow,區(qū)別于 Bulk Data Flow,即成塊數(shù)據(jù)流,典型的交互數(shù)據(jù)流如 telnet、rlogin 等) 時(shí),采用了 Delayed Ack 機(jī)制以及 Nagle 算法來減少小分組數(shù)目。
書上已經(jīng)對(duì)這兩種機(jī)制的原理講的很清晰,這里不再做復(fù)述。本文后續(xù)部分將通過分析 TCP/IP 在 Linux 下的實(shí)現(xiàn),來解釋一下 TCP 的延遲確認(rèn)機(jī)制。
1. 為什么 TCP 延遲確認(rèn)會(huì)導(dǎo)致延遲?
其實(shí)僅有延遲確認(rèn)機(jī)制,是不會(huì)導(dǎo)致請(qǐng)求延遲的 (初以為是必須等到 ACK 包發(fā)出去,recv 系統(tǒng)調(diào)用才會(huì)返回)。一般來說,只有當(dāng)該機(jī)制與 Nagle 算法或擁塞控制(慢啟動(dòng)或擁塞避免) 混合作用時(shí),才可能會(huì)導(dǎo)致時(shí)耗增長(zhǎng)。我們下面來詳細(xì)看看是如何相互作用的:
延遲確認(rèn)與 Nagle 算法
我們先看看 Nagle 算法的規(guī)則(可參考 tcp_output.c 文件里 tcp_nagle_check 函數(shù)注釋):
1)如果包長(zhǎng)度達(dá)到 MSS,則允許發(fā)送;
2)如果該包含有 FIN,則允許發(fā)送;
3)設(shè)置了 TCP_NODELAY 選項(xiàng),則允許發(fā)送;
4)未設(shè)置 TCP_CORK 選項(xiàng)時(shí),若所有發(fā)出去的包均被確認(rèn),或所有發(fā)出去的小數(shù)據(jù)包 (包長(zhǎng)度小于 MSS) 均被確認(rèn),則允許發(fā)送。
對(duì)于規(guī)則 4),就是說要求一個(gè) TCP 連接上最多只能有一個(gè)未被確認(rèn)的小數(shù)據(jù)包,在該分組的確認(rèn)到達(dá)之前,不能發(fā)送其他的小數(shù)據(jù)包。如果某個(gè)小分組的確認(rèn)被延遲了(案例中的 40ms),那么后續(xù)小分組的發(fā)送就會(huì)相應(yīng)的延遲。也就是說延遲確認(rèn)影響的并不是被延遲確認(rèn)的那個(gè)數(shù)據(jù)包,而是后續(xù)的應(yīng)答包。
1 00:44:37.878027 IP 171.24.38.136.44792 175.24.11.18.9877: S 3512052379:3512052379(0) win 5840 mss 1448,wscale 7
2 00:44:37.878045 IP 175.24.11.18.9877 171.24.38.136.44792: S 3581620571:3581620571(0) ack 3512052380 win 5792 mss 1460,wscale 2
3 00:44:37.879080 IP 171.24.38.136.44792 175.24.11.18.9877: . ack 1 win 46
......
4 00:44:38.885325 IP 171.24.38.136.44792 175.24.11.18.9877: P 1321:1453(132) ack 1321 win 86
5 00:44:38.886037 IP 175.24.11.18.9877 171.24.38.136.44792: P 1321:1453(132) ack 1453 win 2310
6 00:44:38.887174 IP 171.24.38.136.44792 175.24.11.18.9877: P 1453:2641(1188) ack 1453 win 102
7 00:44:38.887888 IP 175.24.11.18.9877 171.24.38.136.44792: P 1453:2476(1023) ack 2641 win 2904
8 00:44:38.925270 IP 171.24.38.136.44792 175.24.11.18.9877: . ack 2476 win 118
9 00:44:38.925276 IP 175.24.11.18.9877 171.24.38.136.44792: P 2476:2641(165) ack 2641 win 2904
10 00:44:38.926328 IP 171.24.38.136.44792 175.24.11.18.9877: . ack 2641 win 134
從上面的 tcpdump 抓包分析看,第 8 個(gè)包是延遲確認(rèn)的,而第 9 個(gè)包的數(shù)據(jù),在 Server 端 (175.24.11.18) 雖然早就已放到 TCP 發(fā)送緩沖區(qū)里面 (應(yīng)用層調(diào)用的 send 已經(jīng)返回) 了,但按照 Nagle 算法,第 9 個(gè)包需要等到第個(gè) 7 包 (小于 MSS) 的 ACK 到達(dá)后才能發(fā)出。
延遲確認(rèn)與擁塞控制
我們先利用 TCP_NODELAY 選項(xiàng)關(guān)閉 Nagle 算法,再來分析延遲確認(rèn)與 TCP 擁塞控制是如何互相作用的。
慢啟動(dòng):TCP 的發(fā)送方維護(hù)一個(gè)擁塞窗口,記為 cwnd。TCP 連接建立是,該值初始化為 1 個(gè)報(bào)文段,每收到一個(gè) ACK,該值就增加 1 個(gè)報(bào)文段。發(fā)送方取擁塞窗口與通告窗口 (與滑動(dòng)窗口機(jī)制對(duì)應(yīng)) 中的最小值作為發(fā)送上限(擁塞窗口是發(fā)送方使用的流控,而通告窗口則是接收方使用的流控)。發(fā)送方開始發(fā)送 1 個(gè)報(bào)文段,收到 ACK 后,cwnd 從 1 增加到 2,即可以發(fā)送 2 個(gè)報(bào)文段,當(dāng)收到這兩個(gè)報(bào)文段的 ACK 后,cwnd 就增加為 4,即指數(shù)增長(zhǎng):例如第一個(gè) RTT 內(nèi),發(fā)送一個(gè)包,并收到其 ACK,cwnd 增加 1,而第二個(gè) RTT 內(nèi),可以發(fā)送兩個(gè)包,并收到對(duì)應(yīng)的兩個(gè) ACK,則 cwnd 每收到一個(gè) ACK 就增加 1,最終變?yōu)?4,實(shí)現(xiàn)了指數(shù)增長(zhǎng)。
在 Linux 實(shí)現(xiàn)里,并不是每收到一個(gè) ACK 包,cwnd 就增加 1,如果在收到 ACK 時(shí),并沒有其他數(shù)據(jù)包在等待被 ACK,則不增加。
本人使用案例 1 的測(cè)試代碼,在實(shí)際測(cè)試中,cwnd 從初始值 2 開始,最終保持 3 個(gè)報(bào)文段的值,tcpdump 結(jié)果如下:
1 16:46:14.288604 IP 178.14.5.3.1913 178.14.5.4.20001: S 1324697951:1324697951(0) win 5840 mss 1460,wscale 2
2 16:46:14.289549 IP 178.14.5.4.20001 178.14.5.3.1913: S 2866427156:2866427156(0) ack 1324697952 win 5792 mss 1460,wscale 2
3 16:46:14.288690 IP 178.14.5.3.1913 178.14.5.4.20001: . ack 1 win 1460
......
4 16:46:15.327493 IP 178.14.5.3.1913 178.14.5.4.20001: P 1321:1453(132) ack 1321 win 4140
5 16:46:15.329749 IP 178.14.5.4.20001 178.14.5.3.1913: P 1321:1453(132) ack 1453 win 2904
6 16:46:15.330001 IP 178.14.5.3.1913 178.14.5.4.20001: P 1453:2641(1188) ack 1453 win 4140
7 16:46:15.333629 IP 178.14.5.4.20001 178.14.5.3.1913: P 1453:1585(132) ack 2641 win 3498
8 16:46:15.337629 IP 178.14.5.4.20001 178.14.5.3.1913: P 1585:1717(132) ack 2641 win 3498
9 16:46:15.340035 IP 178.14.5.4.20001 178.14.5.3.1913: P 1717:1849(132) ack 2641 win 3498
10 16:46:15.371416 IP 178.14.5.3.1913 178.14.5.4.20001: . ack 1849 win 4140
11 16:46:15.371461 IP 178.14.5.4.20001 178.14.5.3.1913: P 1849:2641(792) ack 2641 win 3498
12 16:46:15.371581 IP 178.14.5.3.1913 178.14.5.4.20001: . ack 2641 win 4536
上表中的包,是在設(shè)置 TCP_NODELAY,且 cwnd 已經(jīng)增長(zhǎng)到 3 的情況,第 7、8、9 發(fā)出后,受限于擁塞窗口大小,即使此時(shí) TCP 緩沖區(qū)有數(shù)據(jù)可以發(fā)送亦不能繼續(xù)發(fā)送,即第 11 個(gè)包必須等到第 10 個(gè)包到達(dá)后,才能發(fā)出,而第 10 個(gè)包明顯有一個(gè) 40ms 的延遲。
注:通過 getsockopt 的 TCP_INFO 選項(xiàng) (man 7 tcp) 可以查看 TCP 連接的詳細(xì)信息,例如當(dāng)前擁塞窗口大小,MSS 等。
2. 為什么是 40ms?這個(gè)時(shí)間能不能調(diào)整呢?
首先在 redhat 的官方文檔中,有如下說明:
一些應(yīng)用在發(fā)送小的報(bào)文時(shí),可能會(huì)因?yàn)?TCP 的 Delayed Ack 機(jī)制,導(dǎo)致一定的延遲。其值默認(rèn)為 40ms。可以通過修改 tcp_delack_min,調(diào)整系統(tǒng)級(jí)別的最小延遲確認(rèn)時(shí)間。例如:
# echo 1 /proc/sys/net/ipv4/tcpdelackmin
即是期望設(shè)置最小的延遲確認(rèn)超時(shí)時(shí)間為 1ms。
不過在 slackware 和 suse 系統(tǒng)下,均未找到這個(gè)選項(xiàng),也就是說 40ms 這個(gè)最小值,在這兩個(gè)系統(tǒng)下,是無法通過配置調(diào)整的。
linux-2.6.39.1/net/tcp.h 下有如下一個(gè)宏定義:
#define TCP_DELACK_MIN ((unsigned)(HZ/25)) /* minimal time to delay before sending an ACK */
注:Linux 內(nèi)核每隔固定周期會(huì)發(fā)出 timer interrupt(IRQ 0),HZ 是用來定義每秒有幾次 timer interrupts 的。舉例來說,HZ 為 1000,代表每秒有 1000 次 timer interrupts。HZ 可在編譯內(nèi)核時(shí)設(shè)置。在我們現(xiàn)有服務(wù)器上跑的系統(tǒng),HZ 值均為 250。
以此可知,最小的延遲確認(rèn)時(shí)間為 40ms。
TCP 連接的延遲確認(rèn)時(shí)間一般初始化為最小值 40ms,隨后根據(jù)連接的重傳超時(shí)時(shí)間(RTO)、上次收到數(shù)據(jù)包與本次接收數(shù)據(jù)包的時(shí)間間隔等參數(shù)進(jìn)行不斷調(diào)整。具體調(diào)整算法,可以參考 linux-2.6.39.1/net/ipv4/tcp_input.c, Line 564 的 tcp_event_data_recv 函數(shù)。
3. 為什么 TCP_QUICKACK 需要在每次調(diào)用 recv 后重新設(shè)置?
在 man 7 tcp 中,有如下說明:
TCP_QUICKACK
`Enable quickack mode if set or disable quickack mode if cleared. In quickack mode, acks are sent immediately, rather than delayed if needed in accordance to normal TCP operation. This flag is not permanent, it only enables a switch to or from quickack mode. Subsequent operation of the TCP protocol will once again enter/leave quickack mode depending on internal protocol processing and factors such as delayed ack timeouts occurring and data transfer. This option should not be used in code intended to be portable.`
手冊(cè)中明確描述 TCP_QUICKACK 不是永久的。那么其具體實(shí)現(xiàn)是如何的呢?參考 setsockopt 函數(shù)關(guān)于 TCP_QUICKACK 選項(xiàng)的實(shí)現(xiàn):
case TCP_QUICKACK:
if (!val) {
icsk- icsk_ack.pingpong = 1;
} else {
icsk- icsk_ack.pingpong = 0;
if ((1 sk- sk_state)
(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT)
inet_csk_ack_scheduled(sk)) {
icsk- icsk_ack.pending |= ICSK_ACK_PUSHED;
tcp_cleanup_rbuf(sk, 1);
if (!(val 1))
icsk- icsk_ack.pingpong = 1;
}
}
break;
其實(shí) linux 下 socket 有一個(gè) pingpong 屬性來表明當(dāng)前鏈接是否為交互數(shù)據(jù)流,如其值為 1,則表明為交互數(shù)據(jù)流,會(huì)使用延遲確認(rèn)機(jī)制。但是 pingpong 這個(gè)值是會(huì)動(dòng)態(tài)變化的。例如 TCP 鏈接在要發(fā)送一個(gè)數(shù)據(jù)包時(shí),會(huì)執(zhí)行如下函數(shù)(linux-2.6.39.1/net/ipv4/tcp_output.c, Line 156):
/* Congestion state accounting after a packet has been sent. */
static void tcp_event_data_sent(struct tcp_sock *tp,struct sk_buff *skb, struct sock *sk)
......
tp- lsndtime = now;
/* If it is a reply for ato after last received
* packet, enter pingpong mode.
*/
if ((u32)(now - icsk- icsk_ack.lrcvtime) icsk- icsk_ack.ato)
icsk- icsk_ack.pingpong = 1;
}
最后兩行代碼說明:如果當(dāng)前時(shí)間與最近一次接受數(shù)據(jù)包的時(shí)間間隔小于計(jì)算的延遲確認(rèn)超時(shí)時(shí)間,則重新進(jìn)入交互數(shù)據(jù)流模式。也可以這么理解:延遲確認(rèn)機(jī)制被確認(rèn)有效時(shí),會(huì)自動(dòng)進(jìn)入交互式。
通過以上分析可知,TCP_QUICKACK 選項(xiàng)是需要在每次調(diào)用 recv 后重新設(shè)置的。
4. 為什么不是所有包都延遲確認(rèn)?
TCP 實(shí)現(xiàn)里,用 tcp_in_quickack_mode(linux-2.6.39.1/net/ipv4/tcp_input.c, Line 197)這個(gè)函數(shù)來判斷是否需要立即發(fā)送 ACK。其函數(shù)實(shí)現(xiàn)如下:
/* Send ACKs quickly, if quick count is not exhausted
* and the session is not interactive.
*/
static inline int tcp_in_quickack_mode(const struct sock *sk)
const struct inet_connection_sock *icsk = inet_csk(sk);
return icsk- icsk_ack.quick !icsk- icsk_ack.pingpong;
}
要求滿足兩個(gè)條件才能算是 quickack 模式:
pingpong 被設(shè)置為 0。
快速確認(rèn)數(shù) (quick) 必須為非 0。
關(guān)于 pingpong 這個(gè)值,在前面有描述。而 quick 這個(gè)屬性其代碼中的注釋為:scheduled number of quick acks,即快速確認(rèn)的包數(shù)量,每次進(jìn)入 quickack 模式,quick 被初始化為接收窗口除以 2 倍 MSS 值(linux-2.6.39.1/net/ipv4/tcp_input.c, Line 174),每次發(fā)送一個(gè) ACK 包,quick 即被減 1。
5. 關(guān)于 TCP_CORK 選項(xiàng)
TCP_CORK 選項(xiàng)與 TCP_NODELAY 一樣,是控制 Nagle 化的。
打開 TCP_NODELAY 選項(xiàng),則意味著無論數(shù)據(jù)包是多么的小,都立即發(fā)送(不考慮擁塞窗口)。
如果將 TCP 連接比喻為一個(gè)管道,那 TCP_CORK 選項(xiàng)的作用就像一個(gè)塞子。設(shè)置 TCP_CORK 選項(xiàng),就是用塞子塞住管道,而取消 TCP_CORK 選項(xiàng),就是將塞子拔掉。例如下面這段代碼:
int on = 1;
setsockopt(sockfd, SOL_TCP, TCP_CORK, on, sizeof(on)); //set TCP_CORK
write(sockfd, ...); //e.g., http header
sendfile(sockfd, ...); //e.g., http body
on = 0;
setsockopt(sockfd, SOL_TCP, TCP_CORK, on, sizeof(on)); //unset TCP_CORK
當(dāng) TCP_CORK 選項(xiàng)被設(shè)置時(shí),TCP 鏈接不會(huì)發(fā)送任何的小包,即只有當(dāng)數(shù)據(jù)量達(dá)到 MSS 時(shí),才會(huì)被發(fā)送。當(dāng)數(shù)據(jù)傳輸完成時(shí),通常需要取消該選項(xiàng),以便被塞住,但是又不夠 MSS 大小的包能及時(shí)發(fā)出去。如果應(yīng)用程序確定能一起發(fā)送多個(gè)數(shù)據(jù)集合(例如 HTTP 響應(yīng)的頭和正文),建議設(shè)置 TCP_CORK 選項(xiàng),這樣在這些數(shù)據(jù)之間不存在延遲。為提升性能及吞吐量,Web Server、文件服務(wù)器這一類一般會(huì)使用該選項(xiàng)。
著名的高性能 Web 服務(wù)器 Nginx,在使用 sendfile 模式的情況下,可以設(shè)置打開 TCP_CORK 選項(xiàng):將 nginx.conf 配置文件里的 tcp_nopush 配置為 on。(TCP_NOPUSH 與 TCP_CORK 兩個(gè)選項(xiàng)實(shí)現(xiàn)功能類似,只不過 NOPUSH 是 BSD 下的實(shí)現(xiàn),而 CORK 是 Linux 下的實(shí)現(xiàn))。另外 Nginx 為了減少系統(tǒng)調(diào)用,追求性能極致,針對(duì)短連接(一般傳送完數(shù)據(jù)后,立即主動(dòng)關(guān)閉連接,對(duì)于 Keep-Alive 的 HTTP 持久連接除外),程序并不通過 setsockopt 調(diào)用取消 TCP_CORK 選項(xiàng),因?yàn)殛P(guān)閉連接會(huì)自動(dòng)取消 TCP_CORK 選項(xiàng),將剩余數(shù)據(jù)發(fā)出。
上述內(nèi)容就是 Linux 下 TCP 延遲確認(rèn) Delayed Ack 機(jī)制導(dǎo)致的時(shí)延問題怎么解決,你們學(xué)到知識(shí)或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識(shí)儲(chǔ)備,歡迎關(guān)注丸趣 TV 行業(yè)資訊頻道。