天天看點

TCP 四次揮手的性能優化

作者:Linux碼農

有關 TCP 的四次揮手過程如下圖,

TCP 四次揮手的性能優化

具體詳情不在贅述,但是要注意一點,主動關閉連接配接的,才有 TIME_WAIT 狀态。

TCP 四次揮手的性能優化

下面介紹下主動方有關的優化

對于連接配接的關閉分為2中,一種是正常的安全關閉,走四次揮手。另一種通過RST封包暴力關閉連接配接,不走四次揮手。

安全關閉連接配接是通過調用 close 函數或 shutdown 函數進行關閉連接配接。

調用 close 函數意味着采用不優雅的方式進行完全關閉鍊路,不僅無法接收資料,也無法發送資料。主動調用 close()一方的連接配接叫孤兒連接配接。若通過netstat -p 指令就會發現連接配接對應的程序名為空。

shutdown 函數是一種以優雅的方式進行關閉鍊路,它可以通過關閉連接配接方式進行關閉鍊路。

FIN_WAIT1 狀态優化

當主動方發送 FIN 封包後,其連接配接狀态處于 FIN_WAIT1 狀态,該狀态通常會在數十秒内轉成 FIN_WAIT2 狀态。但是當異常情況下,主動方遲遲收不到對方發來的 ACK 封包,導緻主動方一直處于 FIN_WAIT1 狀态。此時若通過 netstat 指令可以看到 FIN_WAIT1 狀态。

被動方接收到主動方發送的 FIN 封包後,會向主動方發送 ACK 封包,同時向應用程式傳送一個結束符 EOF, 若此時應用程式調用 read(),傳回值為 0。

我們知道若一直收不到對端的回應封包,那麼就會出現逾時定時器逾時發生重傳,是以核心會觸發重傳 FIN 封包,調用如下:

tcp_retransmit_timer

-> tcp_write_timeout

-> tcp_orphan_retries

其中重傳次數是由 tcp_orphan_retries 參數來控制的(注意,orphan 雖然是孤兒的意思,該參數卻不隻對孤兒連接配接有效,事實上,它對所有 FIN_WAIT1 狀态下的連接配接都有效)。其預設值為 0,特指 8 次。

net.ipv4.tcp_orphan_retries = 0

當 FIN 重傳次數超過 8 次時,連接配接就會直接關閉掉。

為什麼 tcp_orphan_retries 為 0,就代表 8 次呢?從源碼可知:

/* Calculate maximal number or retries on an orphaned socket. */
static int tcp_orphan_retries(struct sock *sk, int alive)
{
int retries = sysctl_tcp_orphan_retries; /* May be zero. */

/* We know from an ICMP that something is wrong. */
if (sk->sk_err_soft && !alive)
retries = 0;

/* However, if socket sent something recently, select some safe
* number of retries. 8 corresponds to >100 seconds with minimal
* RTO of 200msec. */
if (retries == 0 && alive)
retries = 8;
return retries;
}
           

是以,若FIN_WAIT1狀态很多情況下,我們可以考慮降低 tcp_orphan_retries 的值,當 FIN 重傳此時超過該值時,報告錯誤并把連接配接直接關掉。

//調整fin重傳次數為5, 該值預設為0,特指8次

# echo 5 > /proc/sys/net/ipv4/tcp_orphan_retries

對于普通情況下,調整 tcp_orphan_retries 已經夠用了,但是如果遇到攻擊,有可能重傳的 FIN 封包壓根就發送不出去。原因如下:

  • 首先,TCP 必須保證封包是有序發送的,FIN 封包也不例外,若緩沖區中還有資料發送時,FIN 封包也不能提前發送, 會導緻發送方一段時間處于 FIN_WAIT1 狀态。
  • 其次,TCP 有流量控制功能,當接收視窗為 0 時,發送方就不能再發送資料。是以,當攻擊者通過設定接收方的接收視窗為 0,導緻發送方的 FIN 封包無法發送出去。進而導緻發送方一直處于 FIN_WAIT1 狀态。

是以為解決這種情況,可以通過調整 tcp_max_orphans 參數

net.ipv4.tcp_max_orphans = 16384

該參數定義了孤兒連接配接的最大數量。

當程序調用 close 函數關閉連接配接後,無論該連接配接是在 FIN_WAIT1 狀态 還是确實關閉了,這個連接配接與程序無關了,它就變成了孤兒連接配接。在 Linux 系統中,為了防止孤兒連接配接過多,導緻系統資源被長期占用,就提供了 tcp_max_orphans 參數。它表示若孤兒連接配接超過該最大值,新增的孤兒連接配接

不在走四次揮手,而是直接發送 RST 封包強行關閉連接配接。

void tcp_close(struct sock *sk, long timeout)
{
struct sk_buff *skb;
int data_was_unread = 0;
int state;

...

//置套接為DEAD狀态,成為孤兒套接口,同時更新系統中孤兒套接口數。
adjudge_to_death:
state = sk->sk_state;
//增加對sock執行個體的引用
sock_hold(sk);
//将sock執行個體從socket結構中分離,并且将套接字标志設定為SOCK_DEAD,表示套接字即将關閉
sock_orphan(sk);
atomic_inc(sk->sk_prot->orphan_count);

...

if (sk->sk_state != TCP_CLOSE) {
sk_stream_mem_reclaim(sk);
/*
若目前待孤兒套接口大于系統配置sysctl_tcp_max_orphans變量 || 
如果目前發送隊列中所有封包資料的總長度超過發送緩沖區長度的上限的最小值 且 
目前整個tcp傳輸層緩沖區所配置設定的記憶體超過緩沖區可用大小的最高硬性限制,則需立即關閉傳輸控制塊,
将狀态設定為CLOSE,同時發送rst給對端
*/
if (atomic_read(sk->sk_prot->orphan_count) > sysctl_tcp_max_orphans ||
(sk->sk_wmem_queued > SOCK_MIN_SNDBUF &&
atomic_read(&tcp_memory_allocated) > sysctl_tcp_mem[2])) {
if (net_ratelimit())
printk(KERN_INFO "TCP: too many of orphaned "
"sockets\n");
tcp_set_state(sk, TCP_CLOSE);
//發送RST給對端
 tcp_send_active_reset(sk, GFP_ATOMIC);
NET_INC_STATS_BH(LINUX_MIB_TCPABORTONMEMORY);
}
}
//若此時tcp狀态為close,則需釋放傳輸控制塊及其占用的資源
if (sk->sk_state == TCP_CLOSE)
inet_csk_destroy_sock(sk);
/* Otherwise, socket is reprieved until protocol close. */

out:
bh_unlock_sock(sk);
local_bh_enable();
sock_put(sk);
}
           

FIN_WAIT2 狀态優化

當主動方收到 ACK 封包後,主動方狀态變成 FIN_WAIT2,标志着主動方的發送通道已經關閉,接下來就等待對方發送 FIN 封包,關閉對端的發送通道。

TCP 四次揮手的性能優化

這個時候,若是連接配接使用的是 shutdown 函數進行關閉的,那麼連接配接可以一直處于 FIN_WAIT2 狀态。但是對于 close 函數關閉的孤兒程序,這個狀态不能持續的太久。而控制持續時長是通過 tcp_fin_timeout 參數控制的。

net.ipv4.tcp_fin_timeout = 60

該參數決定了它保持在 FIN-WAIT-2 狀态的時間。其預設值為 60 秒,是以這就意味着對于孤兒連接配接來講,若 60 秒内還未收到對端發送的 FIN 封包,連接配接就會直接關閉。

TIME_WAIT 狀态優化

TIME_WAIT 是主動方四次揮手的最後一個狀态。當收到被動方發來的 FIN 封包後,主動方回複 ACK,表示确認對方的發送通道已經關閉,進而進入TIME_WAIT 狀态 ,等待 60 秒後進行關閉。

那為什麼主動方要保留 TIME_WAIT 狀态呢?

回答這個問題可以從不保留 TIME_WAIT 狀态會發生什麼。

  • 若主動方發送的 ACK 出現了丢包,而主動方此時已經進行了關閉,導緻被動方永遠收不到 ACK 包而出現一直處于 LAST_ACK 狀态。
  • 當主動發發送的最後一個 ACK 還未到達被動方,被動方重新發送了FIN封包(發送次數仍有前面介紹的 tcp_orphan_retries 控制),但這個 FIN 封包在網絡上逗留。由于此時端口恢複了自由,使用相同的四元組建立連接配接,當連接配接建立完成後,在網上逗留的 FIN 封包又到了主動方,那麼剛建立的新的連接配接由于舊的 FIN 封包而出現誤關閉。

是以保留 TIME_WAIT 狀态可以保證發送的 ACK 封包到達被動方,也可以應付重發的封包。

在協定棧中 TIME_WAIT 狀态保持的時間為 2MSL。MSL 全稱為 Maximum Segment Lifetime, 也即是一個封包在網絡中的最長生存時間。

在 2MSL 時間内,能夠保證本次連接配接中所有在網絡中逗留的封包能夠消亡,保證下次新的連接配接不受舊連接配接的舊封包的幹擾。

是以,TIME_WAIT 和 FIN_WAIT2 狀态的最大時長都是 2MSL,由于在 linux系統中,MSL 的值為 30 秒,是以它們的持續時間都是 60 秒。

有上述可以看到,TIME_WAIT 的保留很有必要,但是它畢竟在消耗系統資源,比如處于 TIME_WAIT 狀态的端口無法提供新的連接配接使用。

在 Linux 系統中提供了 tcp_max_tw_buckets 參數

# cat /proc/sys/net/ipv4/tcp_max_tw_buckets
16384           

當 TIME_WAIT 狀态的連接配接數量超過了該參數時,就不會建立 timewait 控制塊, 新關閉的連接配接就不在經曆 TIME_WAIT 而直接關閉。

static void tcp_fin(struct sk_buff *skb, struct sock *sk, struct tcphdr *th)
{
struct tcp_sock *tp = tcp_sk(sk);
...

switch (sk->sk_state) {
...

case TCP_FIN_WAIT2:
/* Received a FIN -- send ACK and enter TIME_WAIT. */
tcp_send_ack(sk);
//收到fin後,進入TIME_WAIT
tcp_time_wait(sk, TCP_TIME_WAIT, 0);
break;
...
};

...
}



void tcp_time_wait(struct sock *sk, int state, int timeo)
{
struct inet_timewait_sock *tw = NULL;
const struct inet_connection_sock *icsk = inet_csk(sk);
const struct tcp_sock *tp = tcp_sk(sk);
int recycle_ok = 0;

if (tcp_death_row.sysctl_tw_recycle && tp->rx_opt.ts_recent_stamp)
recycle_ok = icsk->icsk_af_ops->remember_stamp(sk);

// 若TIME_WAIT數量沒有超過最大限制,則生成timewait控制塊,否則不生成
if (tcp_death_row.tw_count < tcp_death_row.sysctl_max_tw_buckets)
tw = inet_twsk_alloc(sk, state);

if (tw != NULL) {

...

} else {
/* Sorry, if we're out of memory, just CLOSE this
* socket up. We've got bigger problems than
* non-graceful socket closings.
*/
LIMIT_NETDEBUG(KERN_INFO "TCP: time wait bucket table overflow\n");
}

tcp_update_metrics(sk);
//連接配接關閉
tcp_done(sk);
}
           

是以,當伺服器并發連接配接過多時,相應的 TIME_WAIT 也就會越多,為了保證連接配接正常斷開,此時應該調大 tcp_max_tw_buckets 參數。

但是也并不是 tcp_max_tw_buckets 參數越大越好,畢竟它也是暫用資源的。

還有一種方式,就是複用處于 TIME_WAIT 狀态的連接配接,那就是打開 tcp_tw_reuse 參數。

//查詢tcp_tw_reuse 參數
# cat /proc/sys/net/ipv4/tcp_tw_reuse
0

//開啟tcp_tw_reuse
# echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse           

使用 tcp_tw_reuse 參數的函數為 tcp_twsk_unique,其調用過程過程如下:

tcp_v4_connect

-> inet_hash_connect

-> inet_hash_connect

-> twsk_unique

-> tcp_twsk_unique

int tcp_twsk_unique(struct sock *sk, struct sock *sktw, void *twp)
{
    const struct tcp_timewait_sock *tcptw = tcp_twsk(sktw);
    struct tcp_sock *tp = tcp_sk(sk);


    if (tcptw->tw_ts_recent_stamp &&
        (twp == NULL || (sysctl_tcp_tw_reuse && //開啟sysctl_tcp_tw_reuse 且在處于 TIME_WAIT 狀态并且持續 1 秒之後
                 xtime.tv_sec - tcptw->tw_ts_recent_stamp > 1))) {
        /*對write_seq設定為snd_nxt+65536+2,
          這樣能夠確定在資料傳輸速率<=80Mbit/s的情況下不會被回繞 */        
        tp->write_seq = tcptw->tw_snd_nxt + 65535 + 2;
        if (tp->write_seq == 0)
            tp->write_seq = 1;
        tp->rx_opt.ts_recent       = tcptw->tw_ts_recent;
        tp->rx_opt.ts_recent_stamp = tcptw->tw_ts_recent_stamp;
        sock_hold(sktw);
        return 1;
    }

    return 0;
}


           

從調用流程可知 該參數隻能作用于用戶端,也即是連接配接的發起方,對于服務方是沒有用的,因為該參數是在調用 connect()時起作用。

同時也需要開啟 TCP 時間戳 tcp_timestamps 選項的支援(對方也要打開),不然 sysctl_tcp_tw_reuse 不起作用。

//打開時間戳功能,預設為1

# echo 1 > /proc/sys/net/ipv4/tcp_timestamps

引用時間戳的好處:

1、複用 TIME_WAIT 連接配接,由2MSL 變成 1s

2、防止序列号繞回。

我們知道 TCP 封包的序列号隻有 32 位,而沒增加 2^32 個序列号後就會重複使用原來用過的序列号。假設我們有一條高速網絡,通信的主機雙方有足夠大的帶寬湧來快速的傳輸資料。例如 1Gb/s 的速率發送封包,則不到35秒封包的序号就會重複。這樣對 TCP 傳輸帶來混亂的情況。而采用時間戳選項,可以很容易的分辨出相同序列号的資料報,哪個是最近發送,哪個是以前發送的。

總結一下:

1)tcp_tw_reuse 選項和 tcp_timestamps 選項也必須同時打開;

2)重用 TIME_WAIT 的條件是收到最後一個包後超過 1s。

除了上面的方案外,我們還可以使用套接字選項,直接跳過産生 TIME_WAIT 的過程。

struct linger so_linger;
so_linger.l_onoff = 1;
so_linger.l_linger = 0;
setsocketopt(s, SOL_SOCKET, SO_LINGER, &so_linger, sizeof(so_linger));
           

若 l_onoff 非0,而 l_linger 為0,當調用 close()後,會立即給對端發送一個 RST 封包,不在走四次揮手,直接關閉連接配接。該方案慎用。

被動方優化

TCP 四次揮手的性能優化

從上圖可知,當被動方接收到主動方發送的 FIN 封包後,會進入 CLOSE_WAIT 狀态同時向應用程式傳送一個結束符 EOF。該狀态的作用就是等到應用程式調用 close()進行關閉鍊路。

之是以核心收到FIN封包進入 CLOSE_WAIT 狀态而不是直接幫應用程式關閉鍊路,是因為主動方有可能通過調用調用 shutdown() 關閉連接配接,它可能想在半關閉狀态下進行接收或者發送資料。

協定棧并沒有限制 CLOSE_WAIT 狀态的持續時間,是以當使用 netstat 指令發現會有很多 CLOSE_WAIT 狀态時,這個時候就要小心了,可能應用程式調用 read()傳回 0 時,未調用 close()關閉連接配接。

處于 CLOSE_WAIT 狀态調用close( )時會向對端發送 FIN 封包(當被動方沒有資料要發送時,ACK 和 FIN 封包可能會在使用同一個封包發送給主動方,四次揮手這個時候也就變成了 3 次揮手),然後等待主動方發送 ACK 進行關閉鍊路。當遲遲收不到主動方發送的 ACK 時,被動方會逾時重傳 FIN , 重發次數仍然由 tcp_orphan_retries 參數控制,這與主動方重發 FIN 封包的優化政策一緻。

當主動方發送完FIN封包進入 FIN_WAIT_1 等待對端回應 ACK 期間,接收到了對端發送的FIN封包,也就是主動方和被動方同時調用 close()進行關閉連接配接的情況,這個時候連接配接會進入一種叫做 CLOSING 的新狀态,它替代了 FIN_WAIT2 狀态。

TCP 四次揮手的性能優化

當兩端的應用程式同時發送關閉指令時,兩端的 TCP 狀态均從 ESTABLISHED 變成 FIN_WAIT_1。這将導緻雙方各發送一個 FIN, 兩個 FIN 分别到達對端,收到 FIN 後,狀态由 FIN_WAIT_1變遷為 CLOSING, 并 發送最後的 ACK。收到最後的 ACK 後,狀态變遷為 TIME_WAIT。

小結

針對 TCP 四次揮手的核心參數調整如下

TCP 四次揮手的性能優化

有關主動方的優化

主動方發送 FIN 封包後,若遲遲收不到對端的回應封包,則會進行逾時重傳FIN 封包,重傳次數由 tcp_orphan_retries 決定。

若主動方收到對端回應的 ACK 後進入 FIN_WAIT_2 狀态,此時根據關閉的方式不同,也會有不同的情況:

  • 若是通過 close 方式關閉連接配接,此時連接配接為孤兒連接配接。該狀态不能持續的太久,持續時長是通過 tcp_fin_timeout 參數控制的(預設 60s),若在這段時間還沒有收到讀端的 FIN 封包,則直接關閉連接配接。同時為了防止防止孤兒連接配接過多,就提供了 tcp_max_orphans 參數。它表示若孤兒連接配接超過該最大值,新增的孤兒連接配接不在走四次揮手,而是直接發送 RST 封包強行關閉連接配接。
  • 若是連接配接使用的是 shutdown 函數進行關閉的,那麼連接配接可以一直處于 FIN_WAIT2 狀态。

當主動方收到 FIN 後,并傳回 ACK, 此時進入TIME_WAIT 狀态。該狀态會持續 1 分鐘,為了防止該狀态過多而占用過多資源,定義了 tcp_max_tw_buckets 參數,當 TIME_WAIT 狀态的連接配接數量超過了該參數時,就不會建立 timewait 控制塊, 新關閉的連接配接就不在經曆 TIME_WAIT 而直接關閉。

當 TIME_WAIT 狀态過多時,也可以通過 tcp_tw_reuse 和 tcp_timestamps調整,使得 TIME_WAIT 狀态的端口可以複用。

被動方的優化

當出現大量的 CLOSE_WAIT 狀态時,這個時候從應用程式查找原因,是不是忘記調用 close 進行關閉鍊路。

當被動方發送 FIN 進入 LAST_ACK , 若遲遲為未收到對端的 ACK 時,會重傳FIN, 重傳次數由 tcp_orphan_retries 決定。

繼續閱讀