個人博客請訪問 http://www.x0100.top
背景
由於微服務的興起,以及公司內部各種服務間的各種調用,最近也遇過幾次線上大量TIME_WAIT的問題,雖然知道怎麼解決,但也點燃了我對tcp協議重新溫習學習的興趣。
從何學習呢?重新看下幾百多頁的《TCP/IP詳解》,那太花時間了,以前頁看過一次。不如從一些優秀的開源項目裏,看下他們是怎麼使用tcp建立連接的?那就從jedis開始吧。😃
源碼分析步驟
下載源代碼
git clone https://github.com/xetorthio/jedis.git
git checkout tags/jedis-2.9.0
分析
從最簡單的jedis.set追到底層的tcp網絡編程吧
時序圖
image.png
核心tcp參數設置
socket.setReuseAddress(true);
socket.setKeepAlive(true); // Will monitor the TCP connection is
// valid
socket.setTcpNoDelay(true); // Socket buffer Whetherclosed, to
// ensure timely delivery of data
socket.setSoLinger(true, 0); // Control calls close () method
socket.setSoTimeout(soTimeout);
1 setReuseAddress
可以看出setReuseAddress本質上就是對SO_REUSEADDR的設置
public void setReuseAddress(boolean on) throws SocketException {
if (isClosed())
throw new SocketException("Socket is closed");
getImpl().setOption(SocketOptions.SO_REUSEADDR, Boolean.valueOf(on));
}
SO_REUSEADDR主要的作用(其它作用)就是對TIME_WAIT連接的重用,對於jedis,作者應該是希望客戶端重啓後,重新創建到redis服務器的tcp連接能夠重用之前的接口。因爲客戶端重啓時,活躍的jedis連接由於時客戶端主動關閉,會持續2*MSL的TIME_WAIT狀態,在此期間會一直佔用着客戶端端口。MSL的大小可通過以下命令查看
sysctl -a | grep tcp_fin_timeout
tcp_fin_timeout = 60
2 setKeepAlive
SO_KEEPALIVE是tcp利用心跳機制保持連接的存活,假如連接已經斷開,則會響應錯誤碼Broken Pipe給上層應用,由於keepalive發起心跳包的開始實際比較遲,對於調整後的linux操作系統仍然需要5分鐘,數值過小又會導致過多無用的包發出,5分鐘的時間長度發出後,通常也會由於各種各樣的原因連接早已被銷燬,例如客戶端服務器端的連接經過了lvs,haproxy各種各樣的代理,代理服務器本身也會有個keepalive的最長時間。一般業務對於SO_KEEPALIVE的依賴比較小,假如需要保持連接的話,會自己進行一些存活性的維持和判斷,例如
-
定期小週期發送心跳包keep alive
-
在使用連接前進行判斷,例如連接池通常採用的testOnBorrow, testOnReturn之類的機制。
sudo sysctl -a | grep tcp_keepalive
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_time = 300 //通常默認值爲7200
3 setTcpNoDelay
TCP_NODELAY的作用就是禁用Nagle,減少由於小包帶來的推遲發送的延遲
If set, disable the Nagle algorithm. This means that segments are always sent as soon as possible, even if there is only a small amount of data. When not set, data is buffered until there is a suffi‐ cient amount to send out, thereby avoiding the frequent sending of small packets, which results in poor utilization of the network. This option is overridden by TCP_CORK; however, setting this option forces an explicit flush of pending output, even if TCP_CORK is currently set.
4 setSoLinger(true, 0)
這個選項的設置就是jedis關閉的時候直接發送RST,而不是按照正常的4次揮手的關閉流程,避免了客戶端TIME_WAIT的情況,但不是一個很好的pratice,因爲服務端會只會收到RST,這裏留下一個問題,redis是如何處理rst的,是否是直接忽略,不然就會堆積很多錯誤日誌?
5 socket.setSoTimeout(soTimeout)
SO_TIMEOUT的作用就是設置以下三個socket操作的超時時間,很明顯,ServerSocket.accept()是針對服務端而言,DatagramSocket.receive()是針對UDP,在這裏jedis生效的是SocketInputStream.read(),作用就是jedis發送命令後,開始讀redis請求的響應時間。
ServerSocket.accept();
SocketInputStream.read();
DatagramSocket.receive();
總結
tcp是一個非常複雜的網絡協議,但對於“客戶端”的tcp編程其實也不是特別難,從jedis在tcp的使用上看,直接用了blocking io,同時設置了5個參數,就滿足了大部分場合的使用,對於一般互聯網的服務,也只是使用多一個池化JedisPool而已。