1. 程式人生 > >Linux上TCP的幾個核心引數調優

Linux上TCP的幾個核心引數調優

Linux作為一個強大的作業系統,提供了一系列核心引數供我們進行調優。光TCP的調優引數就有50多個。在和線上問題鬥智鬥勇的過程中,筆者積累了一些在內網環境應該進行調優的引數。在此分享出來,希望對大家有所幫助。 ## 調優清單 好了,在這裡先列出調優清單。請記住,這裡只是筆者在內網進行TCP核心引數調優的經驗,僅供參考。同時,筆者還會在餘下的部落格裡面詳細解釋了為什麼要進行這些調優! |序號|核心引數|值|備註| |---| --- | :--- |---| |1.1|/proc/sys/net/ipv4/tcp\_max\_syn\_backlog|2048|| |1.2|/proc/sys/net/core/somaxconn|2048|| |1.3|/proc/sys/net/ipv4/tcp\_abort\_on\_overflow|1|| |2.1|/proc/sys/net/ipv4/tcp\_tw\_recycle|0|NAT環境必須為0| |2.2|/proc/sys/net/ipv4/tcp\_tw\_reuse|1|| |3.1|/proc/sys/net/ipv4/tcp\_syn\_retries|3|| |3.2|/proc/sys/net/ipv4/tcp\_retries2|5|| |3.3|/proc/sys/net/ipv4/tcp\_slow\_start\_after\_idle|0| ## tcp\_max\_syn\_backlog,somaxconn,tcp\_abort_on\_overflow tcp\_max\_syn\_backlog,somaxconn,tcp\_abort_on\_overflow這三個引數是關於 核心TCP連線緩衝佇列的設定。如果應用層來不及將已經三次握手建立成功的TCP連線從佇列中取出,溢位了這個緩衝佇列(全連線佇列)之後就會丟棄這個連線。如下圖所示: ![](https://oscimg.oschina.net/oscnet/up-6da3083f11a90a9feb67b624b3dbc0516f5.png) 從而產生一些詭異的現象,這個現象詭異之處就在於,是在TCP第三次握手的時候丟棄連線 ![](https://oscimg.oschina.net/oscnet/up-232e6f0b58a6fc20750e283c16a7266a16c.png) 就如圖中所示,第二次握手的SYNACK傳送給client端了。所以就會出現client端認為連線成功,而Server端確已經丟棄了這個連線的現象!由於無法感知到Server已經丟棄了連線。 所以如果沒有心跳的話,只有在發出第一個請求後,Server才會傳送一個reset端通知這個連線已經被丟棄了,建立連線後第二天再用,也會報錯!所以我們要調大Backlog佇列! ``` echo 2048 > /proc/sys/net/ipv4/tcp_max_syn_backlog echo 2048 > /proc/sys/net/core/somaxconn ``` 當然了,為了儘量避免第一筆呼叫失敗問題,我們也同時要設定 ``` echo 1 > /proc/sys/net/ipv4/tcp_abort_on_overflow ``` 設定這個值以後,Server端核心就會在這個連線被溢位之後傳送一個reset包給client端。 ![](https://oscimg.oschina.net/oscnet/up-c189ee4aff83b8b59d6fde2400409785c82.png) 如果我們的client端是NIO的話,就可以收到一個socket close的事件以感知到連線被關閉! ![](https://oscimg.oschina.net/oscnet/up-bba3483f0ab178e3787ac2b2c2eb02175ce.png) ### 注意Java預設的Backlog是50 這個TCP Backlog的佇列大小值是min(tcp\_max\_syn\_backlog,somaxconn,應用層設定的backlog),而Java如果不做額外設定,Backlog預設值僅僅只有50。C語言在使用listen呼叫的時候需要傳進Backlog引數。 ## tcp\_tw\_recycle tcp\_tw\_recycle這個引數一般是用來抑制TIME_WAIT數量的,但是它有一個副作用。即在tcp\_timestamps開啟(Linux預設開啟),tcp\_tw\_recycle會經常導致下面這種現象。 ![](https://oscimg.oschina.net/oscnet/up-ce85027edb38411a93f0601ae25f4f4bdec.png) 也即,如果你的Server開啟了tcp\_tw\_recycle,那麼別人如果通過NAT之類的呼叫你的Server的話,NAT後面的機器只有一臺機器能正常工作,其它情況大概率失敗。具體原因呢由下圖所示: ![](https://oscimg.oschina.net/oscnet/up-cfd1f05447cf1af3fc9a369b3ad64791188.png) 在tcp\_tw\_recycle=1同時tcp\_timestamps(預設開啟的情況下),對同一個IP的連線會做這樣的限制,也即之前後建立的連線的時間戳必須要大於之前建立連線的最後時間戳,但是經過NAT的一個IP後面是不同的機器,時間戳相差極大,就會導致核心直接丟棄時間戳較低的連線的現象。由於這個引數導致的問題,高版本核心已經去掉了這個引數。如果考慮TIME\_WAIT問題,可以考慮設定一下 ``` echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse ``` ## tcp\_syn\_retries 這個引數值得是client傳送SYN如果server端不回覆的話,重傳SYN的次數。對我們的直接影響呢就是connet建立連線時的超時時間。當然Java通過一些C原生系統呼叫的組合使得我們可以進行超時時間的設定。在Linux裡面預設設定是5,下面給出建議值3和預設值5之間的超時時間。 |tcp\_syn\_retries|timeout| |---| --- | |1|min(so_sndtimeo,3s)| |2|min(so_sndtimeo,7s)| |3|min(so_sndtimeo,15s)| |4|min(so_sndtimeo,31s)| |5|min(so_sndtimeo,63s)| 下圖給出了,重傳和超時情況的對應圖: ![](https://oscimg.oschina.net/oscnet/up-8b379a49d7842e738e9109216e9bee401e4.png) 當然了,不同核心版本的超時時間可能不一樣,因為初始RTO在核心小版本間都會有細微的變化。所以,有時候在抓包時候可能會出現(3,6,12......)這樣的序列。當然Java的API有超時時間: ``` java: // 函式呼叫中攜帶有超時時間 public void connect(SocketAddress endpoint, int timeout) ; ``` 所以,對於Java而言,這個核心引數的設定沒有那麼重要。但是,有些程式碼可能會有忘了設定timeout的情況,例如某個版本的Kafka就是,所以它在我們一些混沌測試的情況下,容災恢復的時間會達到一分多鐘,主要時間就是卡在connect上面-_-!,而這時我們的tcp\_syn\_retries設定的是5,也即超時時間63s。減少這個恢復時間的手段就是: ``` echo 3 > /proc/sys/net/ipv4/tcp_syn_retries ``` ## tcp\_retries2 tcp\_retries2這個引數表面意思是在傳輸過程中tcp的重傳次數。但在某個版本之後Linux核心僅僅用這個tcp\_retries2來計算超時時間,在這段時間的重傳次數純粹由RTO等環境因素決定,重傳超時時間在5/15下的表現為: |tcp\_retries2|對端無響應| | --- | :--- |:---| |5|25.6s-51.2s根據動態rto定| |15|924.6s-1044.6s根據動態rto定| 如果我們在應用層設定的Socket所有ReadTimeout都很小的話(例如3s),這個核心引數調整是沒有必要的。但是,筆者經常發現有的系統,因為一兩個慢的介面或者SQL,所以將ReadTimeout設的很大的情況。 ![](https://oscimg.oschina.net/oscnet/up-836bf2508c1ef79051dcf203193fa1bd21e.png) 平常這種情況是沒有問題的,因為慢請求頻率很低,不會對系統造成什麼風險。但是,物理機突然宕機時候的情況就不一樣了,由於ReadTimeOut設定的過大,導致所有落到這臺宕機的機器都會在min(ReadTimeOut,(924.6s-1044.6s)(Linux預設tcp\_retries2是15))後才能從read系統呼叫返回。假設ReadTimeout設定了個5min,系統匯流排程數是200,那麼只要5min內有200個請求落到宕機的server就會使A系統失去響應! ![](https://oscimg.oschina.net/oscnet/up-f49993bf00d4386cc7dcc011adc208306a5.png) 但如果將tcp\_retries2設定為5,那麼超時返回時間即為min(ReadTimeOut 5min,25.6-51.2s),也就是30s左右,極大的緩解了這一情況。 ``` echo 5 > /proc/sys/net/ipv4/tcp_retries2 ``` 但是針對這種現象,最好要做資源上的隔離,例如執行緒上的隔離或者機器級的隔離。 ![](https://oscimg.oschina.net/oscnet/up-17eb5135546ea57fb6ba0677e8c51db2aa5.png) golang的goroutine排程模型就可以很好的解決執行緒資源不夠的問題,但缺點是goroutine裡面不能有阻塞的系統呼叫,不然也會和上面一樣,但僅僅對於系統之間互相呼叫而言,都是非阻塞IO,所以golang做微服務還是非常Nice的。當然了我大Java用純IO事件觸發編寫程式碼也不會有問題,就是對心智負擔太高-_-! ### 物理機突然宕機和程序宕不一樣 值得注意的是,物理機宕機和程序宕但核心還存在表現完全不一樣。 ![](https://oscimg.oschina.net/oscnet/up-4c0648fd58d526fb941ba608cccd52b7783.png) 僅僅程序宕而核心存活,那麼核心會立馬傳送reset給對端,從而不會卡住A系統的執行緒資源。 ## tcp\_slow\_start\_after\_idle 還有一個可能需要調整的引數是tcp\_slow\_start\_after\_idle,Linux預設是1,即開啟狀態。開啟這個引數後,我們的TCP擁塞視窗會在一個RTO時間空閒之後重置為初始擁塞視窗(CWND)大小,這無疑大幅的減少了長連線的優勢。對應Linux原始碼為: ``` static void tcp_event_data_sent(struct tcp_sock *tp, struct sk_buff *skb, struct sock *sk){ // 如果開啟了start_after_idle,而且這次傳送的時間-上次傳送的時間>一個rto,就重置tcp擁塞視窗 if (sysctl_tcp_slow_start_after_idle && (!tp->packets_out && (s32)(now - tp->lsndtime) > icsk->icsk_rto)) tcp_cwnd_restart(sk, __sk_dst_get(sk)); } ``` ![](https://oscimg.oschina.net/oscnet/up-24851b36ef38c48271a8b9295d0b2dc3eca.png) 關閉這個引數後,無疑會提高某些請求的傳輸速度(在頻寬夠的情況下)。 ``` echo 0 > /proc/sys/net/ipv4/tcp_slow_start_after_idle ``` 當然了,Linux啟用這個引數也是有理由的,如果我們的網路情況是時刻在變化的,例如拿個手機到處移動,那麼將擁塞視窗重置確實是個不錯的選項。但是就我們內網系統間呼叫而言,是不太必要的了。 ## 初始CWND大小 毫無疑問,新建連線之後的初始TCP擁塞視窗大小也直接影響到我們的請求速率。在Linux2.6.32原始碼中,其初始擁塞視窗是(2-4個)mss大小,對應於內網估計也就是(2.8-5.6K)(MTU 1500),這個大小對於某些大請求可能有點捉襟見肘。 在Linux 2.6.39以上或者某些RedHat維護的小版本中已經把CWND 增大到RFC 6928所規定的的10段,也就是在內網裡面估計14K左右(MTU 1500)。 ``` Linux 新版本 /* TCP initial congestion window */ #define TCP_INIT_CWND 10 ``` ## 公眾號 關注筆者公眾號,獲取更多幹貨文章 ![](https://oscimg.oschina.net/oscnet/up-03e8bdd592b3eb9dec0a50fa5ff56192df0.JPEG) ## 總結 Linux提供了一大堆內參引數供我們進行調優,其預設設定的引數在很多情況下並不是最佳實踐,所以我們需要潛心研究,找到最適合當前環境的組合