TCP接收緩存大小的動態調整


引言 
 

TCP中有擁塞控制,也有流控制,它們各自有什么作用呢?

擁塞控制(Congestion Control) — A mechanism to prevent a TCP sender from overwhelming the network.

流控制(Flow Control) — A mechanism to prevent a TCP sender from overwhelming a TCP receiver.

 

下面是一段關於流控制原理的簡要描述。

“The basic flow control algorithm works as follows: The receiver communicates to the sender the maximum

amount of data it can accept using the rwnd protocol field. This is called the receive window. The TCP sender

then sends no more than this amount of data across the network. The TCP sender then stops and waits for

acknowledgements back from the receiver. When acknowledgement of the previously sent data is returned to

the sender, the sender then resumes sending new data. It's essentially the old maxim hurry up and wait. ”

由於發送速度可能大於接收速度、接收端的應用程序未能及時從接收緩沖區讀取數據、接收緩沖區不夠大不能

緩存所有接收到的報文等原因,TCP接收端的接收緩沖區很快就會被塞滿,從而導致不能接收后續的數據,發送端

此后發送數據是無效的,因此需要流控制。TCP流控制主要用於匹配發送端和接收端的速度,即根據接收端當前的

接收能力來調整發送端的發送速度。

 

TCP流控制中一個很重要的地方就是,TCP接收緩存大小是如何動態調整的,即TCP確認窗口上限是如何動態調整的?

本文主要分析TCP接收緩存大小動態調整的原理和實現。

 

原理
 

早期的TCP實現中,TCP接收緩存的大小是固定的。隨着網絡的發展,固定的TCP接收緩存值就不適應了,

成為TCP性能的瓶頸之一。這時候就需要手動去調整,因為不同的網絡需要不同大小的TCP接收緩存,手動調整不僅

費時費力,還會引起一些問題。TCP接收緩存設置小了,就不能充分利用網絡。而TCP緩存設置大了,又浪費了內存。

如果把TCP接收緩存設置為無窮大,那就更糟糕了,因為某些應用可能會耗盡內存,使其它應用的連接陷入飢餓。

所以TCP接收緩存的大小需要動態調整,才能達到最佳的效果。

動態調整TCP接收緩存大小,就是使TCP接收緩存按需分配,同時要確保TCP接收緩存大小不會成為傳輸的限制。

linux采用Dynamic Right-Sizing方法來動態調整TCP的接收緩存大小,其基本思想就是:通過估算發送方的擁塞窗口

的大小,來動態設置TCP接收緩存的大小。

 

It has been demomstrated that this method can successfully grow the receiver's advertised window at a pace

sufficient to avoid constraining the sender's throughput. As a result, systems can avoid the network performance

problems that result from either the under-utilization or over-utilization of buffer space.

 

實現
 

下文代碼基於3.2.12內核,主要源文件為:net/ipv4/tcp_input.c。

struct tcp_sock {
...
u32 rcv_nxt; /* What we want to receive next,希望接收的下一個序列號 */
u32 rcv_wnd; /* Current receiver window,當前接收窗口的大小*/
u32 copied_seq; /* Head of yet unread data,應用程序下次從這里復制數據 */
u16 advmss; /* Advertised MSS,接收端通告的MSS */
u32 window_clamp; /* Maximal window to advertise,通告窗口的上限*/

/* Receiver side RTT estimation */
struct {
u32 rtt;
u32 seq;
u32 time;
} rcv_rtt_est; /* 用於接收端的RTT測量*/

/* Receiver queue space */
struct {
int space;
u32 seq;
u32 time;
} rcvq_space; /* 用於調整接收緩沖區和接收窗口*/

/* Options received (usually on last packet, some only on SYN packets). */
struct tcp_options_received rx_opt; /* TCP選項*/
...
};

struct sock {
...
int sk_rcvbuf; /* TCP接收緩沖區的大小*/
int sk_sndbuf; /* TCP發送緩沖區大小*/
unsigned int ...
sk_userlocks : 4, /*TCP接收緩沖區的鎖標志*/
...
}; 
 

RTT測量
 

在發送端有兩種RTT的測量方法(具體可見前面blog),但是因為TCP流控制是在接收端進行的,所以接收端也需要

有測量RTT的方法。

 

(1)沒有時間戳時的測量方法

static inline void tcp_rcv_rtt_measure(struct tcp_sock *tp)
{
/* 第一次接收到數據時,需要對相關變量初始化*/
if (tp->rcv_rtt_est.time == 0)
goto new_measure;

/* 收到指定的序列號后,才能獲取一個RTT測量樣本*/
if (before(tp->rcv_nxt, tp->rcv_rtt_est.seq))
return;

/* RTT的樣本:jiffies - tp->rcv_rtt_est.time */
tcp_rcv_rtt_update(tp, jiffies - tp->rcv_rtt_est.time, 1);

new_measure:
tp->rcv_rtt_est.seq = tp->rcv_nxt + tp->rcv_wnd; /* 收到此序列號的ack時,一個RTT樣本的計時結束*/
tp->rcv_rtt_est.time = tcp_time_stamp; /* 一個RTT樣本開始計時*/
}
此函數在接收到帶有負載的數據段時被調用。

此函數的原理:我們知道發送端不可能在一個RTT期間發送大於一個通告窗口的數據量。那么接收端可以把接收一個

確認窗口的數據量(rcv_wnd)所用的時間作為RTT。接收端收到一個數據段,然后發送確認(確認號為rcv_nxt,通告

窗口為rcv_wnd),開始計時,RTT就是收到序號為rcv_nxt + rcv_wnd的數據段所用的時間。

很顯然,這種假設並不准確,測量所得的RTT會偏大一些。所以這種方法只有當沒有采用時間戳選項時才使用,而內核

默認是采用時間戳選項的(tcp_timestamps為1)。

下面是一段對此方法的評價:

If the sender is being throttled by the network, this estimate will be valid. However, if the sending application did not

have any data to send, the measured time could be much larger than the actual round-trip time. Thus this measurement

acts only as an upper-bound on the round-trip time.

 

(2)采用時間戳選項時的測量方法

static inline void tcp_rcv_rtt_measure_ts(struct sock *sk, const struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
/* 啟用了Timestamps選項,並且流量穩定*/
if (tp->rx_opt.rcv_tsecr && (TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq >=
inet_csk(sk)->icsk_ack.rcv_mss))
/* RTT = 當前時間 - 回顯時間*/
tcp_rcv_rtt_update(tp, tcp_time_stamp - tp->rx_opt.rcv_tsecr, 0);
}
雖然此種方法是默認方法,但是在流量小的時候,通過時間戳采樣得到的RTT的值會偏大,此時就會采用

沒有時間戳時的RTT測量方法。

 

(3)采樣處理

不管是沒有使用時間戳選項的RTT采樣,還是使用時間戳選項的RTT采樣,都是獲得一個RTT樣本。

之后還需要對獲得的RTT樣本進行處理,以得到最終的RTT。

/* win_dep表示是否對RTT采樣進行微調,1為不進行微調,0為進行微調。*/
static void tcp_rcv_rtt_update(struct tcp_sock *tp, u32 sample, int win_dep)
{
u32 new_sample = tp->rcv_rtt_est.rtt;
long m = sample;

if (m == 0)
m = 1; /* 時延最小為1ms*/

if (new_sample != 0) { /* 不是第一次獲得樣本*/
/* If we sample in larger samples in the non-timestamp case, we could grossly
* overestimate the RTT especially with chatty applications or bulk transfer apps
* which are stalled on filesystem I/O.
*
* Also, since we are only going for a minimum in the non-timestamp case, we do
* not smooth things out else with timestamps disabled convergence takes too long.
*/
/* 對RTT采樣進行微調,新的RTT樣本只占最終RTT的1/8 */
if (! win_dep) {
m -= (new_sample >> 3);
new_sample += m;

} else if (m < new_sample)
/* 不對RTT采樣進行微調,直接取最小值,原因可見上面那段注釋*/
new_sample = m << 3;

} else {
/* No previous measure. 第一次獲得樣本*/
new_sample = m << 3;
}

if (tp->rcv_rtt_est.rtt != new_sample)
tp->rcv_rtt_est.rtt = new_sample; /* 更新RTT*/
}
對於沒有使用時間戳選項的RTT測量方法,不進行微調。因為用此種方法獲得的RTT采樣值已經偏高而且收斂

很慢。直接選擇最小RTT樣本作為最終的RTT測量值。

對於使用時間戳選項的RTT測量方法,進行微調,新樣本占最終RTT的1/8,即rtt = 7/8 old + 1/8 new。

 

調整接收緩存
 

當數據從TCP接收緩存復制到用戶空間之后,會調用tcp_rcv_space_adjust()來調整TCP接收緩存和接收窗口上限的大小。

/*
* This function should be called every time data is copied to user space.
* It calculates the appropriate TCP receive buffer space.
*/
void tcp_rcv_space_adjust(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
int time;
int space;

/* 第一次調整*/
if (tp->rcvq_space.time == 0)
goto new_measure;

time = tcp_time_stamp - tp->rcvq_space.time; /*計算上次調整到現在的時間*/

/* 調整至少每隔一個RTT才進行一次,RTT的作用在這里!*/
if (time < (tp->rcv_rtt_est.rtt >> 3) || tp->rcv_rtt_est.rtt == 0)
return;

/* 一個RTT內接收方應用程序接收並復制到用戶空間的數據量的2倍*/
space = 2 * (tp->copied_seq - tp->rcvq_space.seq);
space = max(tp->rcvq_space.space, space);

/* 如果這次的space比上次的大*/
if (tp->rcvq_space.space != space) {
int rcvmem;
tp->rcvq_space.space = space; /*更新rcvq_space.space*/

/* 啟用自動調節接收緩沖區大小,並且接收緩沖區沒有上鎖*/
if (sysctl_tcp_moderate_rcvbuf && ! (sk->sk_userlocks & SOCK_RCVBUF_LOCK)) {
int new_clamp = space;
/* Receive space grows, normalize in order to take into account packet headers and
* sk_buff structure overhead.
*/
space /= tp->advmss; /* 接收緩沖區可以緩存數據包的個數*/

if (!space)
space = 1;

/* 一個數據包耗費的總內存包括:
* 應用層數據:tp->advmss,
* 協議頭:MAX_TCP_HEADER,
* sk_buff結構,
* skb_shared_info結構。
*/
rcvmem = SKB_TRUESIZE(tp->advmss + MAX_TCP_HEADER);

/* 對rcvmem進行微調*/
while(tcp_win_from_space(rcvmem) < tp->advmss)
rcvmem += 128;

space *= rcvmem;
space = min(space, sysctl_tcp_rmem[2]); /*不能超過允許的最大接收緩沖區大小*/

if (space > sk->sk_rcvbuf) {
sk->sk_rcvbuf = space; /* 調整接收緩沖區的大小*/
/* Make the window clamp follow along. */
tp->window_clamp = new_clamp; /*調整接收窗口的上限*/
}
}
}

new_measure:
/*此序號之前的數據已復制到用戶空間,下次復制將從這里開始*/
tp->rcvq_space.seq = tp->copied_seq;
tp->rcvq_space.time = tcp_time_stamp; /*記錄這次調整的時間*/
}


/* return minimum truesize of the skb containing X bytes of data */
#define SKB_TRUESIZE(X) ((X) + \
SKB_DATA_ALIGN(sizeof(struct sk_buff)) + \
SKB_DATA_ALIGN(sizeof(struct skb_shared_info)))


static inline int tcp_win_from_space(int space)
{
return sysctl_tcp_adv_win_scale <= 0 ?
(space >> (-sysctl_tcp_adv_win_scale)) :
space - (space >> sysctl_tcp_adv_win_scale);
}
tp->rcvq_space.space表示當前接收緩存的大小(只包括應用層數據,單位為字節)。

sk->sk_rcvbuf表示當前接收緩存的大小(包括應用層數據、TCP協議頭、sk_buff和skb_shared_info結構,

tcp_adv_win_scale微調,單位為字節)。

 

系統參數

(1) tcp_moderate_rcvbuf

是否自動調節TCP接收緩沖區的大小,默認值為1。

(2) tcp_adv_win_scale

在tcp_moderate_rcvbuf啟用的情況下,用來對計算接收緩沖區和接收窗口的參數進行微調,默認值為2。

This means that the application buffer is 1/4th of the total buffer space specified in the tcp_rmem variable.

(3) tcp_rmem

包括三個參數:min default max。

tcp_rmem[1] — default :接收緩沖區長度的初始值,用來初始化sock的sk_rcvbuf,默認為87380字節。

tcp_rmem[2] — max:接收緩沖區長度的最大值,用來調整sock的sk_rcvbuf,默認為4194304,一般是2000多個數據包。 

 

小結:接收端的接收窗口上限和接收緩沖區大小,是接收方應用程序在上個RTT內接收並復制到用戶空間的數據量的2倍,

並且接收窗口上限和接收緩沖區大小是遞增的。

(1)為什么是2倍呢?

In order to keep pace with the growth of the sender's congestion window during slow-start, the receiver should

use the same doubling factor. Thus the receiver should advertise a window that is twice the size of the last

measured window size.

這樣就能保證接收窗口上限的增長速度不小於擁塞窗口的增長速度,避免接收窗口成為傳輸瓶頸。

(2)收到亂序包時有什么影響?

Packets that are received out of order may have lowered the goodput during this measurement, but will increase

the goodput of the following measurement which, if larger, will supercede this measurement. 

亂序包會使本次的吞吐量測量值偏小,使下次的吞吐量測量值偏大。

 

Author
 

zhangskd @ csdn

 

Reference
 

[1] Mike Fisk, Wu-chun Feng, "Dynamic Right-Sizing in TCP".
————————————————
版權聲明:本文為CSDN博主「zhangskd」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/zhangskd/java/article/details/8200048


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM