TCP-Westwood擁塞算法

TCP-Westwood在TCP-Reno的基礎上增強了窗口控制和退避處理,例如,TCPW發送端監控ACK報文的接收速率,進而估算當前連接可達到的數據發送速率(可用帶寬)。當發送端檢測到丟包時(超時或者3個重複ACK),發送端根據估算的發送速率設置擁塞窗口大小(cwnd)和慢啓動閾值(ssthresh)。不同於TCP-Reno的窗口減半處理,TCP-Westwood避免太過保守的減低窗口操作,TCP-Westwood稱此爲:Faster-Recovery。與TCP-Reno相比,TCP-Westwood更適合於無線鏈路類的TCP連接。

在TCP-Westwood定義的Faster-Recovery階段,當接收到ACK報文時,意味着對端接收了相應的一定數量的數據報文,將此報文數量除以所經過的時間,即可得到當前帶寬的一個評估值。當接收到重複ACK報文(DUPACKs)時,同樣意味着對端接收到一個亂序的數據報文,也可用於帶寬評估。

儘管如此,發送端並不能確定哪一個數據報文觸發了重複ACK報文,進而不能確定其對應的數據報文長度,這時使用一個平均的報文長度值,當之後接收到正常ACK報文之後,進行數量修正。

需要注意的是,在擁塞發生時,連接路徑中的瓶頸點應是發生了飽和,其報文緩存已經填滿,在此擁塞之前,發送端使用的帶寬小於或者等於瓶頸處的可用帶寬,所以,TCP-Westwood在調整窗口的時候,重新調整到可用帶寬處,而不是簡單的將窗口減半。

ACK接收速率的過濾

如果發送端在tk時刻接收到ACK報文,意味着對應的dk大小的數據被對端接收,使用以下公式計算帶寬的一個採樣值:

bk=dktktk1 b_{k} = \frac{d_{k}}{t_{k}-t_{k-1}}

其中tk−1代表前一個ACK報文的接收時間。既然擁塞發生在低頻的速率(高頻的速率意味着無擁塞)超過鏈路容量時,我們對採樣的速率應用一個低通濾波器來獲得可用帶寬的低頻部分。這樣做的也可去除延時ACK帶來的速率採樣噪聲。

低通濾波器的選擇十分重要,根據經驗,使用Tustin approximation方法將一個連續低通濾波器離散化,得到如下的一個離散時間濾波器:

b^k=2τtktk112τtktk1+1b^k1+bk+bk12τtktk1+1(1) \tag{1}\hat{b}_{k} = \frac{\frac{2\tau }{t_{k}-t_{k-1}}-1}{\frac{2\tau }{t_{k}-t_{k-1}}+1}\hat{b}_{k-1} + \frac{b_{k}+b_{k-1}}{\frac{2\tau }{t_{k}-t_{k-1}}+1}

其中bk ˆ爲tk時刻的可用帶寬的過濾之後的測量值,1/τ爲濾波器的截止頻率。爲理解其工作,假設ACK報文保持恆定的到達間隔,即tk − tk−1 = Δk = τ /10,之後,濾波器(1)變爲一個具有恆定係數的濾波器,如下所示:

b^k=ab^k1+(1a)2[bk+bk1](2) \tag{2}\hat{b}_{k} = a\hat{b}_{k-1}+\frac{\left ( 1-a \right )}{2}\left [ b_{k}+b_{k-1} \right ]

其中係數a = 0.9 ,此公式(2)表示一個新的bk ˆ值由90%的前一個值bk−1,和最近兩個採樣值之和的10%組成。但是,由於ACK的到達時間並不確定,其依賴於tk − tk−1的值,這就要求使用隨時間可變的係數。當ACK報文間隔增加時,上一次的值bk-1所佔比重應減少;反之,其所佔比重應增加,這真是公式(1)所實現。

最後,公式(1)的截止頻率爲1/τ,這意味值所有高於1/τ的頻率部分都過濾掉了。根據Nyquist採樣原理,爲了採樣一個帶寬爲1/τ的信號,採樣間隔需要低於或者等於τ/2。但是,ACK報文是不確定的,如當發送端空閒時,完全沒有ACK報文到達,採樣頻率無法得到保證。因此,如果在最後一個ACK報文接收後,超過τ/m (m≥2)長時間,沒有接收到新的ACK報文,濾波器將假設接收到一個虛擬的採樣bk=0。如下圖所示:

bk-1       bk      bk+1                bk+n-1       bk+n
|          |       |          |        |            |
|----------|-------|----------|--------|------------|-------> t
|          |       |                   |            |
tk-1       tk      tk+1                tk+n-1       tk+n

其中tk-1爲最後接收到的ACK報文,tk+j表示虛擬的採樣到達的時刻,tk+j+1 - tk+j = τ/m,(j ∈ [0, n-1]),對於此範圍內的j值,有bk+j=0。

如果由tk時刻起,一直沒有ACK報文,濾波器公式(1)將會以指數級速率趨近於零。

帶寬測量

如前所述,DUPACKs應當包含在帶寬估算中,因爲其意味着報文的成功發送。所以,隨後正常的ACK報文應當僅算作確認了一個報文,不應包含重複ACK確認的報文。但是,對於延時ACK,情況就有所不同。TCP-Westwood對於帶寬測量提出了以下兩點要求:

a. 發送端必須跟蹤重複ACK的數量,直到接收了正常ACK。
b. 發送端應當可以檢測到延遲ACK。

如下所以,在接收到ACK報文之後,首先需要確定其確認的報文數量(cumul_ack),如果其值爲零,表明接收到重複ACK,使用變量accounted_for記錄重複ACK數量,將cumul_ack設置爲1,即重複ACK表示成功發送了一個報文。

如果cumul_ack大於1,表明此ACK爲延遲ACK,或者爲發生報文重傳之後的正常ACK,此情況下,與已經使用的報文數量(accounted_for)進行比較,如果ACK確認的報文數量小於等於已經使用的報文數量,表明在接收到重複ACK時已經使用了這些報文,進行帶寬估算,不應再次使用了,更新accounted_for。反之,表明之後部分報文被使用,其與部分可用,清空accounted_for記錄。

最後參與帶寬計算的報文數量由變量acked表示。

PROCEDURE AckedCount

cumul_ack = current_ack_seqno - last_ack_seqno;
if (cumul_ack = 0)
    accounted_for=accounted_for+1;
    cumul_ack=1;
endif

if (cumul_ack > 1)
    if (accounted_for >= cumul_ack)
        accounted_for=accounted_for-cumul_ack;
        cumul_ack=1;
    else if (accounted_for < cumul_ack)
        cumul_ack= cumul_ack - accounted_for;
        accounted_for=0;
    endif
endif

last_ack_seqno=current_ack_seqno;
acked=cumul_ack;

return(acked);

END PROCEDURE

TCP-Westwood窗口計算

對於由重複ACK引起的快速重傳,慢啓動閾值ssthresh等於估算的帶寬乘以最小RTT時長,除以報文長度。其中,BWE*RTTmin表示的爲瓶頸鍊路的報文緩存爲空時的BDP,此設置允許在擁塞發生之後瓶頸鍊路的緩存清空。

另外,如果處於擁塞避免階段,將擁塞窗口設置爲ssthresh,反之,對於慢啓動階段,不設置窗口,其仍可按照指數級增長。

if (n DUPACKs are received)
    ssthresh = (BWE*RTTmin)/seg_size;
    if (cwin>ssthresh) /* congestion avoid. */
        cwin = ssthresh;
    endif
endif

對於報文超時引發的情況,慢啓動閾值的計算與上相同,這裏將其最小值設置爲2,將擁塞窗口設置爲1。

if (coarse timeout expires)
    ssthresh = (BWE*RTTmin)/seg_size;
    if (ssthresh < 2)
        ssthresh = 2;
    endif;
    cwin = 1;
endif

內核TCP-Westwood初始化

TCP-Westwood更新採樣速率的週期不低於50ms(TCP_WESTWOOD_RTT_MIN),即最快50ms更新一次速率。初始的RTT值定義爲20秒(TCP_WESTWOOD_INIT_RTT),應當是非常保守了,實際值很少有這麼大。

/* TCP Westwood functions and constants */
#define TCP_WESTWOOD_RTT_MIN   (HZ/20)  /* 50ms */
#define TCP_WESTWOOD_INIT_RTT  (20*HZ)  /* maybe too conservative?! */

如下TCP-Westwood擁塞處理結構,其慢啓動閾值計算函數ssthresh,和擁塞窗口計算函數cong_avoid函數,以及窗口撤銷函數undo_cwnd都是使用TCP-Reno的對應函數。而TCP-Westwood實現的擁塞函數指針主要有cwnd_event、in_ack_event和pkts_acked。

static struct tcp_congestion_ops tcp_westwood __read_mostly = {
    .init       = tcp_westwood_init,
    .ssthresh   = tcp_reno_ssthresh,
    .cong_avoid = tcp_reno_cong_avoid,
    .undo_cwnd      = tcp_reno_undo_cwnd,
    .cwnd_event = tcp_westwood_event,
    .in_ack_event   = tcp_westwood_ack,
    .get_info   = tcp_westwood_info,
    .pkts_acked = tcp_westwood_pkts_acked,

帶寬估算

如下函數westwood_filter,如果當前帶寬估算值爲空,使用第一個帶寬採樣進行更新。否則,應用低通濾波器函數進行更新。即之前的帶寬估算值佔比爲7/8,新的採樣值佔比爲1/8。

static void westwood_filter(struct westwood *w, u32 delta)
{
    /* If the filter is empty fill it with the first sample of bandwidth  */
    if (w->bw_ns_est == 0 && w->bw_est == 0) {
        w->bw_ns_est = w->bk / delta;
        w->bw_est = w->bw_ns_est;
    } else {
        w->bw_ns_est = westwood_do_filter(w->bw_ns_est, w->bk / delta);
        w->bw_est = westwood_do_filter(w->bw_est, w->bw_ns_est);
    }
}
static inline u32 westwood_do_filter(u32 a, u32 b)
{
    return ((7 * a) + b) >> 3;
}

在窗口更新函數westwood_update_window中,更新帶寬估算值。對於第一次進入此函數的情況,first_ack爲真(初始化爲1),需要同步Westwood記錄的SND.UNA。之後,如果上一次帶寬估算至今的時長超過Westwood記錄的RTT與最小RTT(TCP_WESTWOOD_RTT_MIN=50毫秒)之間的最大值時,更新帶寬估算值。參見以上函數westwood_filter。

變量bk中保存了ACK已經確認的數據量,在帶寬採樣完成之後,將其清空。在此之前,bk的值在函數TCP快速路徑函數westwood_fast_bw中累加,在慢速路徑中,使用函數tcp_westwood_ack進行累加。

static void westwood_update_window(struct sock *sk)
{
    struct westwood *w = inet_csk_ca(sk);
    s32 delta = tcp_jiffies32 - w->rtt_win_sx;
    
    /* Initialize w->snd_una with the first acked sequence number in order
     * to fix mismatch between tp->snd_una and w->snd_una for the first
     * bandwidth sample
     */
    if (w->first_ack) {
        w->snd_una = tcp_sk(sk)->snd_una;
        w->first_ack = 0;
    }

    /*
     * See if a RTT-window has passed.
     * Be careful since if RTT is less than
     * 50ms we don't filter but we continue 'building the sample'.
     * This minimum limit was chosen since an estimation on small
     * time intervals is better to avoid...
     * Obviously on a LAN we reasonably will always have
     * right_bound = left_bound + WESTWOOD_RTT_MIN
     */
    if (w->rtt && delta > max_t(u32, w->rtt, TCP_WESTWOOD_RTT_MIN)) {
        westwood_filter(w, delta);

        w->bk = 0;
        w->rtt_win_sx = tcp_jiffies32;

如下函數westwood_fast_bw,其在TCP接收的快速路徑中調用。首先使用westwood_update_window函數更新帶寬估算值(時機未到,不會更新)。其次,更新確認報文統計信息(bk),更新最小RTT值(update_rtt_min)。

static inline void westwood_fast_bw(struct sock *sk)
{
    const struct tcp_sock *tp = tcp_sk(sk);
    struct westwood *w = inet_csk_ca(sk);

    westwood_update_window(sk);

    w->bk += tp->snd_una - w->snd_una;
    w->snd_una = tp->snd_una;
    update_rtt_min(w);
}

更新最小RTT值由如下函數update_rtt_min完成,在TCP初始化時,或者擁塞丟包發生時,需要重新設置最小RTT值(reset_rtt_min)。否則,如果當前RTT小於記錄的最小RTT值,更新最小RTT值。當前RTT值在函數tcp_westwood_pkts_acked中更新。

static inline void update_rtt_min(struct westwood *w)
{
    if (w->reset_rtt_min) {
        w->rtt_min = w->rtt;
        w->reset_rtt_min = 0;
    } else
        w->rtt_min = min(w->rtt, w->rtt_min);
}

ACK報文

在接收到ACK確認報文之後,函數tcp_westwood_pkts_acked使用最新的採樣,更新Westwood記錄的RTT時長。

/*
 * @westwood_pkts_acked
 * Called after processing group of packets.
 * but all westwood needs is the last sample of srtt.
 */
static void tcp_westwood_pkts_acked(struct sock *sk, const struct ack_sample *sample)
{   
    struct westwood *w = inet_csk_ca(sk);
    
    if (sample->rtt_us > 0)
        w->rtt = usecs_to_jiffies(sample->rtt_us);

ACK事件

在ACK報文處理函數tcp_ack中調用此函數,標誌CA_ACK_SLOWPATH表示當前TCP連接是否處於慢速路徑中。在快速路徑中(westwood_fast_bw),使用(tp->snd_una - w->snd_una)計算確認的數據量。但是在慢速路徑中,由函數westwood_acked_count確定確認數據量。

static void tcp_westwood_ack(struct sock *sk, u32 ack_flags)
{
    if (ack_flags & CA_ACK_SLOWPATH) {
        struct westwood *w = inet_csk_ca(sk);

        westwood_update_window(sk);
        w->bk += westwood_acked_count(sk);

        update_rtt_min(w);
        return;
    }

    westwood_fast_bw(sk);
}

不同於快速路徑,函數westwood_acked_count需要考慮延時ACK(delayed-ack)、重複ACK和部分ACK(partial-ack)的情況。對於重複ACK,確認數據量設定爲MSS大小。對於部分ACK,確認數據量也是MSS大小,這裏由accounted中需要減去之前重複ACK確認的數據量。

對於延遲ACK或者非部分ACK的情況,確認數據量爲減去accounted的結果值。

static inline u32 westwood_acked_count(struct sock *sk)
{
    const struct tcp_sock *tp = tcp_sk(sk);
    struct westwood *w = inet_csk_ca(sk);

    w->cumul_ack = tp->snd_una - w->snd_una;

    /* If cumul_ack is 0 this is a dupack since it's not moving tp->snd_una.
     */
    if (!w->cumul_ack) {
        w->accounted += tp->mss_cache;
        w->cumul_ack = tp->mss_cache;
    }

    if (w->cumul_ack > tp->mss_cache) {
        /* Partial or delayed ack */
        if (w->accounted >= w->cumul_ack) {
            w->accounted -= w->cumul_ack;
            w->cumul_ack = tp->mss_cache;
        } else {
            w->cumul_ack -= w->accounted;
            w->accounted = 0;
        }
    }

    w->snd_una = tp->snd_una;

    return w->cumul_ack;

擁塞事件

先來看一下核心函數tcp_westwood_bw_rttmin,窗口值由帶寬估算值與最小RTT值的乘積,除以MSS生成,最小不能低於2。

static u32 tcp_westwood_bw_rttmin(const struct sock *sk)
{
    const struct tcp_sock *tp = tcp_sk(sk);
    const struct westwood *w = inet_csk_ca(sk);

    return max_t(u32, (w->bw_est * w->rtt_min) / tp->mss_cache, 2);
}

如下函數tcp_westwood_event,在發送擁塞時,設置慢啓動閾值ssthresh和擁塞窗口。

static void tcp_westwood_event(struct sock *sk, enum tcp_ca_event event)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct westwood *w = inet_csk_ca(sk);

    switch (event) {
    case CA_EVENT_COMPLETE_CWR:
        tp->snd_cwnd = tp->snd_ssthresh = tcp_westwood_bw_rttmin(sk);
        break;
    case CA_EVENT_LOSS:
        tp->snd_ssthresh = tcp_westwood_bw_rttmin(sk);
        /* Update RTT_min when next ack arrives */
        w->reset_rtt_min = 1;
        break;

內核版本 5.0

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章