1. 程式人生 > >TCP接收快取大小的動態調整

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".