面試官:換人!他連 TCP 這幾個參數都不懂

每日一句英語學習,天天進步一點點:


前言

TCP 性能的提高不只考察 TCP 的理論知識,還考察了對於操心繫統提供的內核參數的理解與應用。html

TCP 協議是由操做系統實現,因此操做系統提供了很多調節 TCP 的參數。java

Linux TCP 參數

如何正確有效的使用這些參數,來提升 TCP 性能是一個不那麼簡單事情。咱們須要針對 TCP 每一個階段的問題來對症下藥,而不是病急亂投醫。面試

接下來,將以三個角度來闡述提高 TCP 的策略,分別是:編程

  • TCP 三次握手的性能提高;
  • TCP 四次揮手的性能提高;
  • TCP 數據傳輸的性能提高;

本節提綱


正文

01 TCP 三次握手的性能提高

TCP 是面向鏈接的、可靠的、雙向傳輸的傳輸層通訊協議,因此在傳輸數據以前須要通過三次握手才能創建鏈接。緩存

三次握手與數據傳輸

那麼,三次握手的過程在一個 HTTP 請求的平均時間佔比 10% 以上,在網絡狀態不佳、高併發或者遭遇 SYN 攻擊等場景中,若是不能有效正確的調節三次握手中的參數,就會對性能產生不少的影響。安全

如何正確有效的使用這些參數,來提升 TCP 三次握手的性能,這就須要理解「三次握手的狀態變遷」,這樣當出現問題時,先用 netstat 命令查看是哪一個握手階段出現了問題,再來對症下藥,而不是病急亂投醫。服務器

TCP 三次握手的狀態變遷

客戶端和服務端均可以針對三次握手優化性能。主動發起鏈接的客戶端優化相對簡單些,而服務端須要監聽端口,屬於被動鏈接方,其間保持許多的中間狀態,優化方法相對複雜一些。cookie

因此,客戶端(主動發起鏈接方)和服務端(被動鏈接方)優化的方式是不一樣的,接下來分別針對客戶端和服務端優化。網絡

客戶端優化

三次握手創建鏈接的首要目的是「同步序列號」。併發

只有同步了序列號纔有可靠傳輸,TCP 許多特性都依賴於序列號實現,好比流量控制、丟包重傳等,這也是三次握手中的報文稱爲 SYN 的緣由,SYN 的全稱就叫 Synchronize Sequence Numbers(同步序列號)。

TCP 頭部

SYN_SENT 狀態的優化

客戶端做爲主動發起鏈接方,首先它將發送 SYN 包,因而客戶端的鏈接就會處於 SYN_SENT 狀態。

客戶端在等待服務端回覆的 ACK 報文,正常狀況下,服務器會在幾毫秒內返回 SYN+ACK ,但若是客戶端長時間沒有收到 SYN+ACK 報文,則會重發 SYN 包,重發的次數由 tcp_syn_retries 參數控制,默認是 5 次:

一般,第一次超時重傳是在 1 秒後,第二次超時重傳是在 2 秒,第三次超時重傳是在 4 秒後,第四次超時重傳是在 8 秒後,第五次是在超時重傳 16 秒後。沒錯,每次超時的時間是上一次的 2 倍

當第五次超時重傳後,會繼續等待 32 秒,若是仍然服務端沒有迴應 ACK,客戶端就會終止三次握手。

因此,總耗時是 1+2+4+8+16+32=63 秒,大約 1 分鐘左右。

SYN 超時重傳

你能夠根據網絡的穩定性和目標服務器的繁忙程度修改 SYN 的重傳次數,調整客戶端的三次握手時間上限。好比內網中通信時,就能夠適當調低重試次數,儘快把錯誤暴露給應用程序。

服務端優化

當服務端收到 SYN 包後,服務端會立馬回覆 SYN+ACK 包,代表確認收到了客戶端的序列號,同時也把本身的序列號發給對方。

此時,服務端出現了新鏈接,狀態是 SYN_RCV。在這個狀態下,Linux 內核就會創建一個「半鏈接隊列」來維護「未完成」的握手信息,當半鏈接隊列溢出後,服務端就沒法再創建新的鏈接。

半鏈接隊列與全鏈接隊列

SYN 攻擊,攻擊的是就是這個半鏈接隊列。

如何查看因爲 SYN 半鏈接隊列已滿,而被丟棄鏈接的狀況?

咱們能夠經過該 netstat -s 命令給出的統計結果中, 能夠獲得因爲半鏈接隊列已滿,引起的失敗次數:

上面輸出的數值是累計值,表示共有多少個 TCP 鏈接由於半鏈接隊列溢出而被丟棄。隔幾秒執行幾回,若是有上升的趨勢,說明當前存在半鏈接隊列溢出的現象

如何調整 SYN 半鏈接隊列大小?

要想增大半鏈接隊列,不能只單純增大 tcp_max_syn_backlog 的值,還需一同增大 somaxconn 和 backlog,也就是增大 accept 隊列。不然,只單純增大 tcp_max_syn_backlog 是無效的。

增大 tcp_max_syn_backlog 和 somaxconn 的方法是修改 Linux 內核參數:

增大 backlog 的方式,每一個 Web 服務都不一樣,好比 Nginx 增大 backlog 的方法以下:

最後,改變了如上這些參數後,要重啓 Nginx 服務,由於 SYN 半鏈接隊列和 accept 隊列都是在 listen() 初始化的。

若是 SYN 半鏈接隊列已滿,只能丟棄鏈接嗎?

並非這樣,開啓 syncookies 功能就能夠在不使用 SYN 半鏈接隊列的狀況下成功創建鏈接

syncookies 的工做原理:服務器根據當前狀態計算出一個值,放在己方發出的 SYN+ACK 報文中發出,當客戶端返回 ACK 報文時,取出該值驗證,若是合法,就認爲鏈接創建成功,以下圖所示。

開啓 syncookies 功能

syncookies 參數主要有如下三個值:

  • 0 值,表示關閉該功能;
  • 1 值,表示僅當 SYN 半鏈接隊列放不下時,再啓用它;
  • 2 值,表示無條件開啓功能;

那麼在應對 SYN 攻擊時,只須要設置爲 1 便可:

SYN_RCV 狀態的優化

當客戶端接收到服務器發來的 SYN+ACK 報文後,就會回覆 ACK 給服務器,同時客戶端鏈接狀態從 SYN_SENT 轉換爲 ESTABLISHED,表示鏈接創建成功。

服務器端鏈接成功創建的時間還要再日後,等到服務端收到客戶端的 ACK 後,服務端的鏈接狀態才變爲 ESTABLISHED。

若是服務器沒有收到 ACK,就會重發 SYN+ACK 報文,同時一直處於 SYN_RCV 狀態。

當網絡繁忙、不穩定時,報文丟失就會變嚴重,此時應該調大重發次數。反之則能夠調小重發次數。修改重發次數的方法是,調整 tcp_synack_retries 參數

tcp_synack_retries 的默認重試次數是 5 次,與客戶端重傳 SYN 相似,它的重傳會經歷 一、二、四、八、16 秒,最後一次重傳後會繼續等待 32 秒,若是服務端仍然沒有收到 ACK,纔會關閉鏈接,故共須要等待 63 秒。

服務器收到 ACK 後鏈接創建成功,此時,內核會把鏈接從半鏈接隊列移除,而後建立新的徹底的鏈接,並將其添加到 accept 隊列,等待進程調用 accept 函數時把鏈接取出來。

若是進程不能及時地調用 accept 函數,就會形成 accept 隊列(也稱全鏈接隊列)溢出,最終致使創建好的 TCP 鏈接被丟棄。

 accept 隊列溢出

accept 隊列已滿,只能丟棄鏈接嗎?

丟棄鏈接只是 Linux 的默認行爲,咱們還能夠選擇向客戶端發送 RST 復位報文,告訴客戶端鏈接已經創建失敗。打開這一功能須要將 tcp_abort_on_overflow 參數設置爲 1。

tcp_abort_on_overflow 共有兩個值分別是 0 和 1,其分別表示:

  • 0 :若是 accept 隊列滿了,那麼 server 扔掉 client 發過來的 ack ;
  • 1 :若是 accept 隊列滿了,server 發送一個 RST 包給 client,表示廢掉這個握手過程和這個鏈接;

若是要想知道客戶端鏈接不上服務端,是否是服務端 TCP 全鏈接隊列滿的緣由,那麼能夠把 tcp_abort_on_overflow 設置爲 1,這時若是在客戶端異常中能夠看到不少 connection reset by peer 的錯誤,那麼就能夠證實是因爲服務端 TCP 全鏈接隊列溢出的問題。

一般狀況下,應當把 tcp_abort_on_overflow 設置爲 0,由於這樣更有利於應對突發流量。

舉個例子,當 accept 隊列滿致使服務器丟掉了 ACK,與此同時,客戶端的鏈接狀態倒是 ESTABLISHED,客戶端進程就在創建好的鏈接上發送請求。只要服務器沒有爲請求回覆 ACK,客戶端的請求就會被屢次「重發」。若是服務器上的進程只是短暫的繁忙形成 accept 隊列滿,那麼當 accept 隊列有空位時,再次接收到的請求報文因爲含有 ACK,仍然會觸發服務器端成功創建鏈接。

tcp_abort_on_overflow 爲 0 能夠應對突發流量

因此,tcp_abort_on_overflow 設爲 0 能夠提升鏈接創建的成功率,只有你很是確定 TCP 全鏈接隊列會長期溢出時,才能設置爲 1 以儘快通知客戶端。

如何調整 accept 隊列的長度呢?

accept 隊列的長度取決於 somaxconn 和 backlog 之間的最小值,也就是 min(somaxconn, backlog),其中:

  • somaxconn 是 Linux 內核的參數,默認值是 128,能夠經過 net.core.somaxconn 來設置其值;
  • backlog 是 listen(int sockfd, int backlog) 函數中的 backlog 大小;

Tomcat、Nginx、Apache 常見的 Web 服務的 backlog 默認值都是 511。

如何查看服務端進程 accept 隊列的長度?

能夠經過 ss -ltn 命令查看:

  • Recv-Q:當前 accept 隊列的大小,也就是當前已完成三次握手並等待服務端 accept() 的 TCP 鏈接;
  • Send-Q:accept 隊列最大長度,上面的輸出結果說明監聽 8088 端口的 TCP 服務,accept 隊列的最大長度爲 128;
如何查看因爲 accept 鏈接隊列已滿,而被丟棄的鏈接?

當超過了 accept 鏈接隊列,服務端則會丟掉後續進來的 TCP 鏈接,丟掉的 TCP 鏈接的個數會被統計起來,咱們可使用 netstat -s 命令來查看:

上面看到的 41150 times ,表示 accept 隊列溢出的次數,注意這個是累計值。能夠隔幾秒鐘執行下,若是這個數字一直在增長的話,說明 accept 鏈接隊列偶爾滿了。

若是持續不斷地有鏈接由於 accept 隊列溢出被丟棄,就應該調大 backlog 以及 somaxconn 參數。

如何繞過三次握手?

以上咱們只是在對三次握手的過程進行優化,接下來咱們看看如何繞過三次握手發送數據。

三次握手創建鏈接形成的後果就是,HTTP 請求必須在一個 RTT(從客戶端到服務器一個往返的時間)後才能發送。

常規 HTTP 請求

在 Linux 3.7 內核版本以後,提供了 TCP Fast Open 功能,這個功能能夠減小 TCP 鏈接創建的時延。

接下來講說,TCP Fast Open 功能的工做方式。

開啓 TCP Fast Open 功能

在客戶端首次創建鏈接時的過程:

  1. 客戶端發送 SYN 報文,該報文包含 Fast Open 選項,且該選項的 Cookie 爲空,這代表客戶端請求 Fast Open Cookie;
  2. 支持 TCP Fast Open 的服務器生成 Cookie,並將其置於 SYN-ACK 數據包中的 Fast Open 選項以發回客戶端;
  3. 客戶端收到 SYN-ACK 後,本地緩存 Fast Open 選項中的 Cookie。

因此,第一次發起 HTTP GET 請求的時候,仍是須要正常的三次握手流程。

以後,若是客戶端再次向服務器創建鏈接時的過程:

  1. 客戶端發送 SYN 報文,該報文包含「數據」(對於非 TFO 的普通 TCP 握手過程,SYN 報文中不包含「數據」)以及此前記錄的 Cookie;
  2. 支持 TCP Fast Open 的服務器會對收到 Cookie 進行校驗:若是 Cookie 有效,服務器將在 SYN-ACK 報文中對 SYN 和「數據」進行確認,服務器隨後將「數據」遞送至相應的應用程序;若是 Cookie 無效,服務器將丟棄 SYN 報文中包含的「數據」,且其隨後發出的 SYN-ACK 報文將只確認 SYN 的對應序列號;
  3. 若是服務器接受了 SYN 報文中的「數據」,服務器可在握手完成以前發送「數據」,這就減小了握手帶來的 1 個 RTT 的時間消耗
  4. 客戶端將發送 ACK 確認服務器發回的 SYN 以及「數據」,但若是客戶端在初始的 SYN 報文中發送的「數據」沒有被確認,則客戶端將從新發送「數據」;
  5. 此後的 TCP 鏈接的數據傳輸過程和非 TFO 的正常狀況一致。

因此,以後發起 HTTP GET 請求的時候,能夠繞過三次握手,這就減小了握手帶來的 1 個 RTT 的時間消耗。

注:客戶端在請求並存儲了 Fast Open Cookie 以後,能夠不斷重複 TCP Fast Open 直至服務器認爲 Cookie 無效(一般爲過時)。

Linux 下怎麼打開 TCP Fast Open 功能呢?

在 Linux 系統中,能夠經過設置 tcp_fastopn 內核參數,來打開 Fast Open 功能

tcp_fastopn 各個值的意義:

  • 0 關閉
  • 1 做爲客戶端使用 Fast Open 功能
  • 2 做爲服務端使用 Fast Open 功能
  • 3 不管做爲客戶端仍是服務器,均可以使用 Fast Open 功能

TCP Fast Open 功能須要客戶端和服務端同時支持,纔有效果。

小結

本小結主要介紹了關於優化 TCP 三次握手的幾個 TCP 參數。

三次握手優化策略

客戶端的優化

當客戶端發起 SYN 包時,能夠經過 tcp_syn_retries 控制其重傳的次數。

服務端的優化

當服務端 SYN 半鏈接隊列溢出後,會致使後續鏈接被丟棄,能夠經過 netstat -s 觀察半鏈接隊列溢出的狀況,若是 SYN 半鏈接隊列溢出狀況比較嚴重,能夠經過 tcp_max_syn_backlog、somaxconn、backlog 參數來調整 SYN 半鏈接隊列的大小。

服務端回覆 SYN+ACK 的重傳次數由 tcp_synack_retries 參數控制。若是遭受 SYN 攻擊,應把 tcp_syncookies 參數設置爲 1,表示僅在 SYN 隊列滿後開啓 syncookie 功能,能夠保證正常的鏈接成功創建。

服務端收到客戶端返回的 ACK,會把鏈接移入 accpet 隊列,等待進行調用 accpet() 函數取出鏈接。

能夠經過 ss -lnt 查看服務端進程的 accept 隊列長度,若是 accept 隊列溢出,系統默認丟棄 ACK,若是能夠把 tcp_abort_on_overflow 設置爲 1 ,表示用 RST 通知客戶端鏈接創建失敗。

若是 accpet 隊列溢出嚴重,能夠經過 listen 函數的 backlog 參數和 somaxconn 系統參數提升隊列大小,accept 隊列長度取決於 min(backlog, somaxconn)。

繞過三次握手

TCP Fast Open 功能能夠繞過三次握手,使得 HTTP 請求減小了 1 個 RTT 的時間,Linux 下能夠經過 tcp_fastopen 開啓該功能,同時必須保證服務端和客戶端同時支持。


02 TCP 四次揮手的性能提高

接下來,咱們一塊兒看看針對 TCP 四次揮手關不鏈接時,如何優化性能。

在開始以前,咱們得先了解四次揮手狀態變遷的過程。

客戶端和服務端雙方均可以主動斷開鏈接,一般先關閉鏈接的一方稱爲主動方,後關閉鏈接的一方稱爲被動方。

客戶端主動關閉

能夠看到,四次揮手過程只涉及了兩種報文,分別是 FIN 和 ACK

  • FIN 就是結束鏈接的意思,誰發出 FIN 報文,就表示它將不會再發送任何數據,關閉這一方向上的傳輸通道;
  • ACK 就是確認的意思,用來通知對方:你方的發送通道已經關閉;

四次揮手的過程:

  • 當主動方關閉鏈接時,會發送 FIN 報文,此時發送方的 TCP 鏈接將從 ESTABLISHED 變成 FIN_WAIT1。
  • 當被動方收到 FIN 報文後,內核會自動回覆 ACK 報文,鏈接狀態將從 ESTABLISHED 變成 CLOSE_WAIT,表示被動方在等待進程調用 close 函數關閉鏈接。
  • 當主動方收到這個 ACK 後,鏈接狀態由 FIN_WAIT1 變爲 FIN_WAIT2,也就是表示主動方的發送通道就關閉了
  • 當被動方進入 CLOSE_WAIT 時,被動方還會繼續處理數據,等到進程的 read 函數返回 0 後,應用程序就會調用 close 函數,進而觸發內核發送 FIN 報文,此時被動方的鏈接狀態變爲 LAST_ACK。
  • 當主動方收到這個 FIN 報文後,內核會回覆 ACK 報文給被動方,同時主動方的鏈接狀態由 FIN_WAIT2 變爲 TIME_WAIT,在 Linux 系統下大約等待 1 分鐘後,TIME_WAIT 狀態的鏈接纔會完全關閉
  • 當被動方收到最後的 ACK 報文後,被動方的鏈接就會關閉

你能夠看到,每一個方向都須要一個 FIN 和一個 ACK,所以一般被稱爲四次揮手

這裏一點須要注意是:主動關閉鏈接的,纔有 TIME_WAIT 狀態。

主動關閉方和被動關閉方優化的思路也不一樣,接下來分別說說如何優化他們。

主動方的優化

關閉的鏈接的方式一般有兩種,分別是 RST 報文關閉和 FIN 報文關閉。

若是進程異常退出了,內核就會發送 RST 報文來關閉,它能夠不走四次揮手流程,是一個暴力關閉鏈接的方式。

安全關閉鏈接的方式必須經過四次揮手,它由進程調用 closeshutdown 函數發起 FIN 報文(shutdown 參數須傳入 SHUT_WR 或者 SHUT_RDWR 纔會發送 FIN)。

調用 close 函數 和 shutdown 函數有什麼區別?

調用了 close 函數意味着徹底斷開鏈接,徹底斷開不只指沒法傳輸數據,並且也不能發送數據。 此時,調用了 close 函數的一方的鏈接叫作「孤兒鏈接」,若是你用 netstat -p 命令,會發現鏈接對應的進程名爲空。

使用 close 函數關閉鏈接是不優雅的。因而,就出現了一種優雅關閉鏈接的 shutdown 函數,它能夠控制只關閉一個方向的鏈接

第二個參數決定斷開鏈接的方式,主要有如下三種方式:

  • SHUT_RD(0):關閉鏈接的「讀」這個方向,若是接收緩衝區有已接收的數據,則將會被丟棄,而且後續再收到新的數據,會對數據進行 ACK,而後悄悄地丟棄。也就是說,對端仍是會接收到 ACK,在這種狀況下根本不知道數據已經被丟棄了。
  • SHUT_WR(1):關閉鏈接的「寫」這個方向,這就是常被稱爲「半關閉」的鏈接。若是發送緩衝區還有未發送的數據,將被當即發送出去,併發送一個 FIN 報文給對端。
  • SHUT_RDWR(2):至關於 SHUT_RD 和 SHUT_WR 操做各一次,關閉套接字的讀和寫兩個方向

close 和 shutdown 函數均可以關閉鏈接,但這兩種方式關閉的鏈接,不僅功能上有差別,控制它們的 Linux 參數也不相同。

FIN_WAIT1 狀態的優化

主動方發送 FIN 報文後,鏈接就處於 FIN_WAIT1 狀態,正常狀況下,若是能及時收到被動方的 ACK,則會很快變爲 FIN_WAIT2 狀態。

可是當遲遲收不到對方返回的 ACK 時,鏈接就會一直處於 FIN_WAIT1 狀態。此時,內核會定時重發 FIN 報文,其中重發次數由 tcp_orphan_retries 參數控制(注意,orphan 雖然是孤兒的意思,該參數卻不僅對孤兒鏈接有效,事實上,它對全部 FIN_WAIT1 狀態下的鏈接都有效),默認值是 0。

你可能會好奇,這 0 表示幾回?實際上當爲 0 時,特指 8 次,從下面的內核源碼可知:

若是 FIN_WAIT1 狀態鏈接不少,咱們就須要考慮下降 tcp_orphan_retries 的值,當重傳次數超過 tcp_orphan_retries 時,鏈接就會直接關閉掉。

對於廣泛正常狀況時,調低 tcp_orphan_retries 就已經能夠了。若是遇到惡意攻擊,FIN 報文根本沒法發送出去,這由 TCP 兩個特性致使的:

  • 首先,TCP 必須報文報文是有序發送的,FIN 報文也不例外,當發送緩衝區還有數據沒有發送時,FIN 報文也不能提早發送。
  • 其次,TCP 有流量控制功能,當接收方接收窗口爲 0 時,發送方就不能再發送數據。因此,當攻擊者下載大文件時,就能夠經過接收窗口設爲 0 ,這就會使得 FIN 報文都沒法發送出去,那麼鏈接會一直處於 FIN_WAIT1 狀態。

解決這種問題的方法,是調整 tcp_max_orphans 參數,它定義了「孤兒鏈接」的最大數量

當進程調用了 close 函數關閉鏈接,此時鏈接就會是「孤兒鏈接」,由於它沒法在發送和接收數據。Linux 系統爲了防止孤兒鏈接過多,致使系統資源長時間被佔用,就提供了 tcp_max_orphans 參數。若是孤兒鏈接數量大於它,新增的孤兒鏈接將再也不走四次揮手,而是直接發送 RST 復位報文強制關閉。

FIN_WAIT2 狀態的優化

當主動方收到 ACK 報文後,會處於 FIN_WAIT2 狀態,就表示主動方的發送通道已經關閉,接下來將等待對方發送 FIN 報文,關閉對方的發送通道。

這時,若是鏈接是用 shutdown 函數關閉的,鏈接能夠一直處於 FIN_WAIT2 狀態,由於它可能還能夠發送或接收數據。但對於 close 函數關閉的孤兒鏈接,因爲沒法在發送和接收數據,因此這個狀態不能夠持續過久,而 tcp_fin_timeout 控制了這個狀態下鏈接的持續時長,默認值是 60 秒:

它意味着對於孤兒鏈接(調用 close 關閉的鏈接),若是在 60 秒後尚未收到 FIN 報文,鏈接就會直接關閉。

這個 60 秒不是隨便決定的,它與 TIME_WAIT 狀態持續的時間是相同的,後面咱們在來講說爲何是 60 秒。

TIME_WAIT 狀態的優化

TIME_WAIT 是主動方四次揮手的最後一個狀態,也是最常碰見的狀態。

當收到被動方發來的 FIN 報文後,主動方會馬上回復 ACK,表示確認對方的發送通道已經關閉,接着就處於 TIME_WAIT 狀態。在 Linux 系統,TIME_WAIT 狀態會持續 60 秒後纔會進入關閉狀態。

TIME_WAIT 狀態的鏈接,在主動方看來確實快已經關閉了。而後,被動方沒有收到 ACK 報文前,仍是處於 LAST_ACK 狀態。若是這個 ACK 報文沒有到達被動方,被動方就會重發 FIN 報文。重發次數仍然由前面介紹過的 tcp_orphan_retries 參數控制。

TIME-WAIT 的狀態尤爲重要,主要是兩個緣由:

  • 防止具備相同「四元組」的「舊」數據包被收到;
  • 保證「被動關閉鏈接」的一方能被正確的關閉,即保證最後的 ACK 能讓被動關閉方接收,從而幫助其正常關閉;

緣由一:防止舊鏈接的數據包

TIME-WAIT 的一個做用是防止收到歷史數據,從而致使數據錯亂的問題。

假設 TIME-WAIT 沒有等待時間或時間太短,被延遲的數據包抵達後會發生什麼呢?

接收到歷史數據的異常

  • 如上圖黃色框框服務端在關閉鏈接以前發送的 SEQ = 301 報文,被網絡延遲了。
  • 這時有相同端口的 TCP 鏈接被複用後,被延遲的 SEQ = 301 抵達了客戶端,那麼客戶端是有可能正常接收這個過時的報文,這就會產生數據錯亂等嚴重的問題。

因此,TCP 就設計出了這麼一個機制,通過 2MSL 這個時間,足以讓兩個方向上的數據包都被丟棄,使得原來鏈接的數據包在網絡中都天然消失,再出現的數據包必定都是新創建鏈接所產生的。

緣由二:保證鏈接正確關閉

TIME-WAIT 的另一個做用是等待足夠的時間以確保最後的 ACK 能讓被動關閉方接收,從而幫助其正常關閉。

假設 TIME-WAIT 沒有等待時間或時間太短,斷開鏈接會形成什麼問題呢?

沒有確保正常斷開的異常

  • 如上圖紅色框框客戶端四次揮手的最後一個 ACK 報文若是在網絡中被丟失了,此時若是客戶端 TIME-WAIT 太短或沒有,則就直接進入了 CLOSE 狀態了,那麼服務端則會一直處在 LASE-ACK 狀態。
  • 當客戶端發起創建鏈接的 SYN 請求報文後,服務端會發送 RST 報文給客戶端,鏈接創建的過程就會被終止。

咱們再回過頭來看看,爲何 TIME_WAIT 狀態要保持 60 秒呢?這與孤兒鏈接 FIN_WAIT2 狀態默認保留 60 秒的原理是同樣的,由於這兩個狀態都須要保持 2MSL 時長。MSL 全稱是 Maximum Segment Lifetime,它定義了一個報文在網絡中的最長生存時間(報文每通過一次路由器的轉發,IP 頭部的 TTL 字段就會減 1,減到 0 時報文就被丟棄,這就限制了報文的最長存活時間)。

爲何是 2 MSL 的時長呢?這實際上是至關於至少容許報文丟失一次。好比,若 ACK 在一個 MSL 內丟失,這樣被動方重發的 FIN 會在第 2 個 MSL 內到達,TIME_WAIT 狀態的鏈接能夠應對。

爲何不是 4 或者 8 MSL 的時長呢?你能夠想象一個丟包率達到百分之一的糟糕網絡,連續兩次丟包的機率只有萬分之一,這個機率實在是過小了,忽略它比解決它更具性價比。

所以,TIME_WAIT 和 FIN_WAIT2 狀態的最大時長都是 2 MSL,因爲在 Linux 系統中,MSL 的值固定爲 30 秒,因此它們都是 60 秒。

雖然 TIME_WAIT 狀態有存在的必要,但它畢竟會消耗系統資源。若是發起鏈接一方的 TIME_WAIT 狀態過多,佔滿了全部端口資源,則會致使沒法建立新鏈接。

  • 客戶端受端口資源限制:若是客戶端 TIME_WAIT 過多,就會致使端口資源被佔用,由於端口就65536個,被佔滿就會致使沒法建立新的鏈接;
  • 服務端受系統資源限制:因爲一個 四元組表示TCP鏈接,理論上服務端能夠創建不少鏈接,服務端確實只監聽一個端口 可是會把鏈接扔給處理線程,因此理論上監聽的端口能夠繼續監聽。可是線程池處理不了那麼多一直不斷的鏈接了。因此當服務端出現大量 TIME_WAIT 時,系統資源被佔滿時,會致使處理不過來新的鏈接;

另外,Linux 提供了 tcp_max_tw_buckets 參數,當 TIME_WAIT 的鏈接數量超過該參數時,新關閉的鏈接就再也不經歷 TIME_WAIT 而直接關閉:

當服務器的併發鏈接增多時,相應地,同時處於 TIME_WAIT 狀態的鏈接數量也會變多,此時就應當調大 tcp_max_tw_buckets 參數,減小不一樣鏈接間數據錯亂的機率。

tcp_max_tw_buckets 也不是越大越好,畢竟內存和端口都是有限的。

有一種方式能夠在創建新鏈接時,複用處於 TIME_WAIT 狀態的鏈接,那就是打開 tcp_tw_reuse 參數。可是須要注意,該參數是隻用於客戶端(創建鏈接的發起方),由於是在調用 connect() 時起做用的,而對於服務端(被動鏈接方)是沒有用的。

tcp_tw_reuse 從協議角度理解是安全可控的,能夠複用處於 TIME_WAIT 的端口爲新的鏈接所用。

什麼是協議角度理解的安全可控呢?主要有兩點:

  • 只適用於鏈接發起方,也就是 C/S 模型中的客戶端;
  • 對應的 TIME_WAIT 狀態的鏈接建立時間超過 1 秒才能夠被複用。

使用這個選項,還有一個前提,須要打開對 TCP 時間戳的支持(對方也要打開 ):

因爲引入了時間戳,它能帶來了些好處:

  • 咱們在前面提到的 2MSL 問題就不復存在了,由於重複的數據包會由於時間戳過時被天然丟棄;
  • 同時,它還能夠防止序列號繞回,也是由於重複的數據包會因爲時間戳過時被天然丟棄;

老版本的 Linux 還提供了 tcp_tw_recycle 參數,可是當開啓了它,就有兩個坑:

  • Linux 會加快客戶端和服務端 TIME_WAIT 狀態的時間,也就是它會使得 TIME_WAIT 狀態會小於 60 秒,很容易致使數據錯亂;
  • 另外,Linux 會丟棄全部來自遠端時間戳小於上次記錄的時間戳(由同一個遠端發送的)的任何數據包。就是說要使用該選項,則必須保證數據包的時間戳是單調遞增的。那麼,問題在於,此處的時間戳並非咱們一般意義上面的絕對時間,而是一個相對時間。不少狀況下,咱們是無法保證時間戳單調遞增的,好比使用了 NAT,LVS 等狀況;

因此,不建議設置爲 1 ,建議關閉它:

在 Linux 4.12 版本後,Linux 內核直接取消了這一參數。

另外,咱們能夠在程序中設置 socket 選項,來設置調用 close 關閉鏈接行爲。

若是l_onoff爲非 0, 且l_linger值爲 0,那麼調用close後,會立該發送一個 RST 標誌給對端,該 TCP 鏈接將跳過四次揮手,也就跳過了 TIME_WAIT 狀態,直接關閉。

但這爲跨越 TIME_WAIT 狀態提供了一個可能,不過是一個很是危險的行爲,不值得提倡。

被動方的優化

當被動方收到 FIN 報文時,內核會自動回覆 ACK,同時鏈接處於 CLOSE_WAIT 狀態,顧名思義,它表示等待應用進程調用 close 函數關閉鏈接。

內核沒有權利替代進程去關閉鏈接,由於若是主動方是經過 shutdown 關閉鏈接,那麼它就是想在半關閉鏈接上接收數據或發送數據。所以,Linux 並無限制 CLOSE_WAIT 狀態的持續時間。

固然,大多數應用程序並不使用 shutdown 函數關閉鏈接。因此,當你用 netstat 命令發現大量 CLOSE_WAIT 狀態。就須要排查你的應用程序,由於可能由於應用程序出現了 Bug,read 函數返回 0 時,沒有調用 close 函數。

處於 CLOSE_WAIT 狀態時,調用了 close 函數,內核就會發出 FIN 報文關閉發送通道,同時鏈接進入 LAST_ACK 狀態,等待主動方返回 ACK 來確認鏈接關閉。

若是遲遲收不到這個 ACK,內核就會重發 FIN 報文,重發次數仍然由 tcp_orphan_retries 參數控制,這與主動方重發 FIN 報文的優化策略一致。

還有一點咱們須要注意的,若是被動方迅速調用 close 函數,那麼被動方的 ACK 和 FIN 有可能在一個報文中發送,這樣看起來,四次揮手會變成三次揮手,這只是一種特殊狀況,不用在乎。

若是鏈接雙方同時關閉鏈接,會怎麼樣?

因爲 TCP 是雙全工的協議,因此是會出現兩方同時關閉鏈接的現象,也就是同時發送了 FIN 報文。

此時,上面介紹的優化策略仍然適用。兩方發送 FIN 報文時,都認爲本身是主動方,因此都進入了 FIN_WAIT1 狀態,FIN 報文的重發次數仍由 tcp_orphan_retries 參數控制。

同時關閉

接下來,雙方在等待 ACK 報文的過程當中,都等來了 FIN 報文。這是一種新狀況,因此鏈接會進入一種叫作 CLOSING 的新狀態,它替代了 FIN_WAIT2 狀態。接着,雙方內核回覆 ACK 確認對方發送通道的關閉後,進入 TIME_WAIT 狀態,等待 2MSL 的時間後,鏈接自動關閉。

小結

針對 TCP 四次揮手的優化,咱們須要根據主動方和被動方四次揮手狀態變化來調整系統 TCP 內核參數。

四次揮手的優化策略

主動方的優化

主動發起 FIN 報文斷開鏈接的一方,若是遲遲沒收到對方的 ACK 回覆,則會重傳 FIN 報文,重傳的次數由 tcp_orphan_retries 參數決定。

當主動方收到 ACK 報文後,鏈接就進入 FIN_WAIT2 狀態,根據關閉的方式不一樣,優化的方式也不一樣:

  • 若是這是 close 函數關閉的鏈接,那麼它就是孤兒鏈接。若是 tcp_fin_timeout 秒內沒有收到對方的 FIN 報文,鏈接就直接關閉。同時,爲了應對孤兒鏈接佔用太多的資源,tcp_max_orphans 定義了最大孤兒鏈接的數量,超過期鏈接就會直接釋放。
  • 反之是 shutdown 函數關閉的鏈接,則不受此參數限制;

當主動方接收到 FIN 報文,並返回 ACK 後,主動方的鏈接進入 TIME_WAIT 狀態。這一狀態會持續 1 分鐘,爲了防止 TIME_WAIT 狀態佔用太多的資源,tcp_max_tw_buckets 定義了最大數量,超過期鏈接也會直接釋放。

當 TIME_WAIT 狀態過多時,還能夠經過設置 tcp_tw_reusetcp_timestamps 爲 1 ,將 TIME_WAIT 狀態的端口複用於做爲客戶端的新鏈接,注意該參數只適用於客戶端。

被動方的優化

被動關閉的鏈接方應對很是簡單,它在回覆 ACK 後就進入了 CLOSE_WAIT 狀態,等待進程調用 close 函數關閉鏈接。所以,出現大量 CLOSE_WAIT 狀態的鏈接時,應當從應用程序中找問題。

當被動方發送 FIN 報文後,鏈接就進入 LAST_ACK 狀態,在未等到 ACK 時,會在 tcp_orphan_retries 參數的控制下重發 FIN 報文。


03 TCP 傳輸數據的性能提高

在前面介紹的是三次握手和四次揮手的優化策略,接下來主要介紹的是 TCP 傳輸數據時的優化策略。

TCP 鏈接是由內核維護的,內核會爲每一個鏈接創建內存緩衝區:

  • 若是鏈接的內存配置太小,就沒法充分使用網絡帶寬,TCP 傳輸效率就會下降;
  • 若是鏈接的內存配置過大,很容易把服務器資源耗盡,這樣就會致使新鏈接沒法創建;

所以,咱們必須理解 Linux 下 TCP 內存的用途,才能正確地配置內存大小。

滑動窗口是如何影響傳輸速度的?

TCP 會保證每個報文都可以抵達對方,它的機制是這樣:報文發出去後,必須接收到對方返回的確認報文 ACK,若是遲遲未收到,就會超時重發該報文,直到收到對方的 ACK 爲止。

因此,TCP 報文發出去後,並不會立馬從內存中刪除,由於重傳時還須要用到它。

因爲 TCP 是內核維護的,因此報文存放在內核緩衝區。若是鏈接很是多,咱們能夠經過 free 命令觀察到 buff/cache 內存是會增大。

若是 TCP 是每發送一個數據,都要進行一次確認應答。當上一個數據包收到了應答了, 再發送下一個。這個模式就有點像我和你面對面聊天,你一句我一句,但這種方式的缺點是效率比較低的。

按數據包進行確認應答

因此,這樣的傳輸方式有一個缺點:數據包的往返時間越長,通訊的效率就越低

要解決這一問題不難,並行批量發送報文,再批量確認報文即刻。

並行處理

然而,這引出了另外一個問題,發送方能夠爲所欲爲的發送報文嗎?固然這不現實,咱們還得考慮接收方的處理能力。

當接收方硬件不如發送方,或者系統繁忙、資源緊張時,是沒法瞬間處理這麼多報文的。因而,這些報文只能被丟掉,使得網絡效率很是低。

爲了解決這種現象發生,TCP 提供一種機制可讓「發送方」根據「接收方」的實際接收能力控制發送的數據量,這就是滑動窗口的由來。

接收方根據它的緩衝區,能夠計算出後續可以接收多少字節的報文,這個數字叫作接收窗口。當內核接收到報文時,必須用緩衝區存放它們,這樣剩餘緩衝區空間變小,接收窗口也就變小了;當進程調用 read 函數後,數據被讀入了用戶空間,內核緩衝區就被清空,這意味着主機能夠接收更多的報文,接收窗口就會變大。

所以,接收窗口並非恆定不變的,接收方會把當前可接收的大小放在 TCP 報文頭部中的窗口字段,這樣就能夠起到窗口大小通知的做用。

發送方的窗口等價於接收方的窗口嗎?若是不考慮擁塞控制,發送方的窗口大小「約等於」接收方的窗口大小,由於窗口通知報文在網絡傳輸是存在時延的,因此是約等於的關係。

TCP 頭部

從上圖中能夠看到,窗口字段只有 2 個字節,所以它最多能表達 65535 字節大小的窗口,也就是 64KB 大小。

這個窗口大小最大值,在當今高速網絡下,很明顯是不夠用的。因此後續有了擴充窗口的方法:在 TCP 選項字段定義了窗口擴大因子,用於擴大TCP通告窗口,使 TCP 的窗口定義從 2 個字節(16 位) 增長爲 4 字節(32 位),因此此時窗口的最大值能夠達到 1GB。

Linux 中打開這一功能,須要把 tcp_window_scaling 配置設爲 1(默認打開):

要使用窗口擴大選項,通信雙方必須在各自的 SYN 報文中發送這個選項:

  • 主動創建鏈接的一方在 SYN 報文中發送這個選項;
  • 而被動創建鏈接的一方只有在收到帶窗口擴大選項的 SYN 報文以後才能發送這個選項。

這樣看來,只要進程能及時地調用 read 函數讀取數據,而且接收緩衝區配置得足夠大,那麼接收窗口就能夠無限地放大,發送方也就無限地提高發送速度。

這是不可能的,由於網絡的傳輸能力是有限的,當發送方依據發送窗口,發送超過網絡處理能力的報文時,路由器會直接丟棄這些報文。所以,緩衝區的內存並非越大越好。

若是肯定最大傳輸速度?

在前面咱們知道了 TCP 的傳輸速度,受制於發送窗口與接收窗口,以及網絡設備傳輸能力。其中,窗口大小由內核緩衝區大小決定。若是緩衝區與網絡傳輸能力匹配,那麼緩衝區的利用率就達到了最大化。

問題來了,如何計算網絡的傳輸能力呢?

相信你們都知道網絡是有「帶寬」限制的,帶寬描述的是網絡傳輸能力,它與內核緩衝區的計量單位不一樣:

  • 帶寬是單位時間內的流量,表達是「速度」,好比常見的帶寬 100 MB/s;
  • 緩衝區單位是字節,當網絡速度乘以時間才能獲得字節數;

這裏須要說一個概念,就是帶寬時延積,它決定網絡中飛行報文的大小,它的計算方式:

好比最大帶寬是 100 MB/s,網絡時延(RTT)是 10ms 時,意味着客戶端到服務端的網絡一共能夠存放 100MB/s * 0.01s = 1MB 的字節。

這個 1MB 是帶寬和時延的乘積,因此它就叫「帶寬時延積」(縮寫爲 BDP,Bandwidth Delay Product)。同時,這 1MB 也表示「飛行中」的 TCP 報文大小,它們就在網絡線路、路由器等網絡設備上。若是飛行報文超過了 1 MB,就會致使網絡過載,容易丟包。

因爲發送緩衝區大小決定了發送窗口的上限,而發送窗口又決定了「已發送未確認」的飛行報文的上限。所以,發送緩衝區不能超過「帶寬時延積」。

發送緩衝區與帶寬時延積的關係:

  • 若是發送緩衝區「超過」帶寬時延積,超出的部分就沒辦法有效的網絡傳輸,同時致使網絡過載,容易丟包;
  • 若是發送緩衝區「小於」帶寬時延積,就不能很好的發揮出網絡的傳輸效率。

因此,發送緩衝區的大小最好是往帶寬時延積靠近。

怎樣調整緩衝區大小?

在 Linux 中發送緩衝區和接收緩衝都是能夠用參數調節的。設置完後,Linux 會根據你設置的緩衝區進行動態調節

調節發送緩衝區範圍

先來看看發送緩衝區,它的範圍經過 tcp_wmem 參數配置;

上面三個數字單位都是字節,它們分別表示:

  • 第一個數值是動態範圍的最小值,4096 byte = 4K;
  • 第二個數值是初始默認值,87380 byte ≈ 86K;
  • 第三個數值是動態範圍的最大值,4194304 byte = 4096K(4M);

發送緩衝區是自行調節的,當發送方發送的數據被確認後,而且沒有新的數據要發送,就會把發送緩衝區的內存釋放掉。

調節接收緩衝區範圍

而接收緩衝區的調整就比較複雜一些,先來看看設置接收緩衝區範圍的 tcp_rmem 參數:

上面三個數字單位都是字節,它們分別表示:

  • 第一個數值是動態範圍的最小值,表示即便在內存壓力下也能夠保證的最小接收緩衝區大小,4096 byte = 4K;
  • 第二個數值是初始默認值,87380 byte ≈ 86K;
  • 第三個數值是動態範圍的最大值,6291456 byte = 6144K(6M);

接收緩衝區能夠根據系統空閒內存的大小來調節接收窗口:

  • 若是系統的空閒內存不少,就能夠自動把緩衝區增大一些,這樣傳給對方的接收窗口也會變大,於是提高發送方發送的傳輸數據數量;
  • 反正,若是系統的內存很緊張,就會減小緩衝區,這雖然會下降傳輸效率,能夠保證更多的併發鏈接正常工做;

發送緩衝區的調節功能是自動開啓的,而接收緩衝區則須要配置 tcp_moderate_rcvbuf 爲 1 來開啓調節功能

調節 TCP 內存範圍

接收緩衝區調節時,怎麼知道當前內存是否緊張或充分呢?這是經過 tcp_mem 配置完成的:

上面三個數字單位不是字節,而是「頁面大小」,1 頁表示 4KB,它們分別表示:

  • 當 TCP 內存小於第 1 個值時,不須要進行自動調節;
  • 在第 1 和第 2 個值之間時,內核開始調節接收緩衝區的大小;
  • 大於第 3 個值時,內核再也不爲 TCP 分配新內存,此時新鏈接是沒法創建的;

通常狀況下這些值是在系統啓動時根據系統內存數量計算獲得的。根據當前 tcp_mem 最大內存頁面數是 177120,當內存爲 (177120 * 4) / 1024K ≈ 692M 時,系統將沒法爲新的 TCP 鏈接分配內存,即 TCP 鏈接將被拒絕。

根據實際場景調節的策略

在高併發服務器中,爲了兼顧網速與大量的併發鏈接,咱們應當保證緩衝區的動態調整的最大值達到帶寬時延積,而最小值保持默認的 4K 不變便可。而對於內存緊張的服務而言,調低默認值是提升併發的有效手段。

同時,若是這是網絡 IO 型服務器,那麼,調大 tcp_mem 的上限可讓 TCP 鏈接使用更多的系統內存,這有利於提高併發能力。須要注意的是,tcp_wmem 和 tcp_rmem 的單位是字節,而 tcp_mem 的單位是頁面大小。並且,千萬不要在 socket 上直接設置 SO_SNDBUF 或者 SO_RCVBUF,這樣會關閉緩衝區的動態調整功能。

小結

本節針對 TCP 優化數據傳輸的方式,作了一些介紹。

數據傳輸的優化策略

TCP 可靠性是經過 ACK 確認報文實現的,又依賴滑動窗口提高了發送速度也兼顧了接收方的處理能力。

但是,默認的滑動窗口最大值只有 64 KB,不知足當今的高速網絡的要求,要想要想提高發送速度必須提高滑動窗口的上限,在 Linux 下是經過設置 tcp_window_scaling 爲 1 作到的,此時最大值可高達 1GB。

滑動窗口定義了網絡中飛行報文的最大字節數,當它超過帶寬時延積時,網絡過載,就會發生丟包。而當它小於帶寬時延積時,就沒法充分利用網絡帶寬。所以,滑動窗口的設置,必須參考帶寬時延積。

內核緩衝區決定了滑動窗口的上限,緩衝區可分爲:發送緩衝區 tcp_wmem 和接收緩衝區 tcp_rmem。

Linux 會對緩衝區動態調節,咱們應該把緩衝區的上限設置爲帶寬時延積。發送緩衝區的調節功能是自動打開的,而接收緩衝區須要把 tcp_moderate_rcvbuf 設置爲 1 來開啓。其中,調節的依據是 TCP 內存範圍 tcp_mem。

但須要注意的是,若是程序中的 socket 設置 SO_SNDBUF 和 SO_RCVBUF,則會關閉緩衝區的動態整功能,因此不建議在程序設置它倆,而是交給內核自動調整比較好。

有效配置這些參數後,既可以最大程度地保持併發性,也能讓資源充裕時鏈接傳輸速度達到最大值。


巨人的肩膀

[1] 系統性能調優必知必會.陶輝.極客時間.

[2] 網絡編程實戰專欄.盛延敏.極客時間.

[3] http://www.blogjava.net/yongb...

[4] http://blog.itpub.net/3155935...

[5] https://blog.51cto.com/profes...


好文推薦

本篇是 TCP 系列的「終章」了,往期的 TCP 圖解文章以下:

TCP 半鏈接隊列和全鏈接隊列滿了會發生什麼?又該如何應對?

實戰!我用「大白鯊」讓你看見 TCP

你還在爲 TCP 重傳、滑動窗口、流量控制、擁塞控制發愁嗎?看完圖解就不愁了

硬不硬你說了算!近 40 張圖解被問千百遍的 TCP 三次握手和四次揮手面試題

相關文章
相關標籤/搜索