TCP Keepalive機制刨根問底

TCP Keepalive機制刨根問底!

Tcp Keepalive的起源

TCP協議中有長鏈接和短鏈接之分。短鏈接環境下,數據交互完畢後,主動釋放鏈接;html

雙方創建交互的鏈接,可是並非一直存在數據交互,有些鏈接會在數據交互完畢後,主動釋放鏈接,而有些不會,那麼在長時間無數據交互的時間段內,交互雙方都有可能出現掉電、死機、異常重啓,仍是中間路由網絡無端斷開、NAT超時等各類意外。java

當這些意外發生以後,這些TCP鏈接並將來得及正常釋放,那麼,鏈接的另外一方並不知道對端的狀況,它會一直維護這個鏈接,長時間的積累會致使很是多的半打開鏈接,形成端系統資源的消耗和浪費,爲了解決這個問題,在傳輸層能夠利用TCP的保活報文來實現,這就有了TCP的Keepalive(保活探測)機制。linux

Tcp Keepalive存在的做用

探測鏈接的對端是否存活

 在應用交互的過程當中,可能存在如下幾種狀況:數據庫

  1. 客戶端或服務端意外斷電,死機,崩潰,重啓。
  2. 中間網絡已經中斷,而客戶端與服務器並不知道。

           
利用保活探測功能,能夠探知這種對端的意外狀況,從而保證在乎外發生時,能夠釋放半打開的TCP、
鏈接。ubuntu

防止中間設備因超時刪除鏈接相關的鏈接表

中間設備如防火牆等,會爲通過它的數據報文創建相關的鏈接信息表,並未其設置一個超時時間的定時器,若是超出預約時間,某鏈接無任何報文交互的話,中間設備會將該鏈接信息從表中刪除,在刪除後,再有應用報文過來時,中間設備將丟棄該報文,從而致使應用出現異常,這個交互的過程大體以下圖所示:
image.png服務器

這種狀況在有防火牆的應用環境下很是常見,這會給某些長時間無數據交互可是又要長時間維持鏈接的應用(如數據庫)帶來很大的影響,爲了解決這個問題,應用自己或TCP能夠經過保活報文來維持中間設備中該鏈接的信息,(也能夠在中間設備上開啓長鏈接屬性或調高鏈接表的釋放時間來解決。網絡

常見應用故障場景:socket

某財務應用,在客戶端須要填寫大量的表單數據,在客戶端與服務器端創建TCP鏈接後,客戶端終端使用者將花費幾分鐘甚至幾十分鐘填寫表單相關信息,終端使用者終於填好表單所需信息後,點擊「提交」按鈕,結果,這個時候因爲中間設備早已經將這個TCP鏈接從鏈接表中刪除了,其將直接丟棄這個報文或者給客戶端發送RST報文,應用故障產生,這將致使客戶端終端使用者全部的工做將須要從新來過,給使用者帶來極大的不便和損失。 tcp

TCP保活報文交互過程

TCP保活的交互過程大體以下圖所示:
                 ide

TCP保活可能帶來的問題:

  1. 中間設備因大量保活鏈接,致使其鏈接表滿,網關設備因爲保活問題,致使其鏈接表滿,沒法新建鏈接(XX局網閘故障案例)或性能降低嚴重
  2. 正常鏈接被釋放

      當鏈接一端在發送保活探測報文時,中間網絡正好因爲各類異常(如鏈路中斷、中間設備重啓等)而沒法將保活探測報文正確轉發至對端時,可能會致使探測的一方釋放原本正常的鏈接,可是這種可能狀況發生的機率較小,
另外,通常也能夠增長保活探測報文發生的次數來減小這種狀況發生的機率和影響。

TCP Keepalive協議解讀

下面協議解讀,基於 RFC1122#TCP Keep-Alives  (注意這是協議的解讀站在協議的角度)

  1. TCP Keepalive 雖不是標準規範,但操做系統一旦實現,默認狀況下須爲關閉,能夠被上層應用開啓和關閉
  2. TCP Keepalive必須在 沒有任何數據(包括ACK包)接收以後的週期內纔會被髮送,容許配置,默認值不可以小於2個小時
  3. 不包含數據的ACK段在被TCP發送時沒有可靠性保證,意即一旦發送,不確保必定發送成功。系統實現不能對任何特定探針包做死鏈接對待
  4. 規範建議keepalive保活包不該該包含數據,但也能夠包含1個無心義的字節,好比0x0。
  5. SEG.SEQ = SND.NXT-1,即TCP保活探測報文序列號將前一個TCP報文序列號減1。SND.NXT = RCV.NXT,即下一次發送正常報文序號等於ACK序列號;總之保活報文不在窗口控制範圍內 有一張圖,能夠很容易說明,但請仔細觀察Tcp Keepalive部分:
  6. image.png

TCP Keepalive 須要注意的點(協議層面)

  1. 不太好的TCP堆棧實現,可能會要求保活報文必須攜帶有1個字節的數據負載
  2. TCP Keepalive應該在服務器端啓用,客戶端不作任何改動;若單獨在客戶端啓用,若客戶端異常崩潰或出現鏈接故障,存在服務器無限期的爲已打開的但已失效的文件描述符消耗資源的嚴重問題。
  3. 但在特殊的NFS文件系統環境下,須要客戶端和服務器端都要啓用Tcp Keepalive機制。
  4. TCP Keepalive不是TCP規範的一部分,有三點須要注意:

    • 在短暫的故障期間,它們可能引發一個良好鏈接(good connection)被釋放(dropped)
    • 它們消費了沒必要要的寬帶
    • 在以數據包計費的互聯網消費(額外)花費金錢

Tcp keepalive 如何使用

如下環境是在Linux服務器上進行,應用程序若想使用須要設置SO_KEEPALIVE套接口選項纔可以生效。

系統內核參數配置

  1. tcp_keepalive_time,在TCP保活打開的狀況下,最後一次數據交換到TCP發送第一個保活探測包的間隔,即容許的持續空閒時長,或者說每次正常發送心跳的週期,默認值爲7200s(2h)。
  2. tcp_keepalive_probes 在tcp_keepalive_time以後,沒有接收到對方確認,繼續發送保活探測包次數,默認值爲9(次)
  3. tcp_keepalive_intvl,在tcp_keepalive_time以後,沒有接收到對方確認,繼續發送保活探測包的發送頻率,默認值爲75s。
發送頻率tcp_keepalive_intvl乘以發送次數tcp_keepalive_probes,就獲得了從開始探測到放棄探測肯定鏈接斷開的時間;

若設置,服務器在客戶端鏈接空閒的時候,每90秒發送一次保活探測包到客戶端,若沒有及時收到客戶端的TCP Keepalive ACK確認,將繼續等待15秒*2=30秒。總之能夠在90s+30s=120秒(兩分鐘)時間內可檢測到鏈接失效與否。

如下改動,須要寫入到/etc/sysctl.conf文件:

net.ipv4.tcp_keepalive_time=90
net.ipv4.tcp_keepalive_intvl=15
net.ipv4.tcp_keepalive_probes=2

保存退出,而後執行sysctl -p生效

可經過 sysctl -a | grep keepalive 命令檢測一下是否已經生效。

針對已經設置SO_KEEPALIVE的套接字,應用程序不用重啓,內核直接生效。

Java/netty服務器如何使用

只須要在服務器端一方設置便可,客戶端徹底不用設置,好比基於netty 4服務器程序:

ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .option(ChannelOption.SO_BACKLOG, 100)
             // 心跳監測
             .childOption(ChannelOption.SO_KEEPALIVE, true)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ch.pipeline().addLast(
                             new EchoServerHandler());
                 }
             });

            // Start the server.
            ChannelFuture f = b.bind(port).sync();

            // Wait until the server socket is closed.
            f.channel().closeFuture().sync();

Java程序只能作到設置SO_KEEPALIVE選項,至於TCP_KEEPCNT,TCP_KEEPIDLE,TCP_KEEPINTVL等參數配置,只能依賴於sysctl配置,系統進行讀取

其餘語言怎麼使用:連接

TcpKeepLive常見的使用模式

默認狀況下使用keepalive週期爲2個小時,如不選擇更改屬於誤用範疇,形成資源浪費:內核會爲每個鏈接都打開一個保活計時器,N個鏈接會打開N個保活計時器。

優點很明顯:

  1. TCP協議層面保活探測機制,系統內核徹底替上層應用自動給作好了
  2. 內核層面計時器相比上層應用,更爲高效
  3. 上層應用只須要處理數據收發、鏈接異常通知便可
  4. 數據包將更爲緊湊

關閉TCP的keepalive,徹底使用業務層面心跳保活機制 徹底應用掌管心跳,靈活和可控,好比每個鏈接心跳週期的可根據須要減小或延長

業務心跳 + TCP keepalive一塊兒使用,互相做爲補充,但TCP保活探測週期和應用的心跳週期要協調,以互補方可,不可以差距過大,不然將達不到設想的效果

朋友的公司所作IM平臺業務心跳2-5分鐘智能調整 + tcp keepalive 300秒,組合協做,聽說效果也不錯。

雖說沒有固定的模式可遵循,那麼有如下原則能夠參考

  1. 不想折騰,那就棄用TCP Keepalive吧,徹底依賴應用層心跳機制,靈活可控性強
  2. 除非能夠很好把控TCP Keepalive機制,那就能夠根據須要自由使用吧

TcpKeepLive 注意事項

咱們知道TCP鏈接關閉時,須要鏈接的兩端中的某一方發起關閉動做,若是某一方忽然斷電,另一端是沒法知道的。tcp的keep_alive就是用以檢測異常的一種機制。

有三個參數:

  • 發送心跳消息的間隔
  • 未收到回覆時,重試的時間間隔
  • 重試的次數

  若是是Linux操做系統,這三個值分別爲

huangcheng@ubuntu:~$ cat /proc/sys/net/ipv4/tcp_keepalive_time
7200
huangcheng@ubuntu:~$ cat /proc/sys/net/ipv4/tcp_keepalive_intvl
75
huangcheng@ubuntu:~$ cat /proc/sys/net/ipv4/tcp_keepalive_probes
9

       也就意味着每隔7200s(兩個小時)發起一次keepalive的報文,若是沒有迴應,75秒後進行重試,最多重試9次即認爲鏈接關閉。

       這三個選項分別對應TCP_KEEPIDLE、TCP_KEEPINTL和TCP_KEEPCNT的選項值,經過setsockopt進行設置。

可是,tcp本身的keepalive有這樣的一個bug:
**
正常狀況下,鏈接的另外一端主動調用colse關閉鏈接,tcp會通知,咱們知道了該鏈接已經關閉。

可是若是tcp鏈接的另外一端忽然掉線,或者重啓斷電,這個時候咱們並不知道網絡已經關閉。

而此時,若是有發送數據失敗,tcp會自動進行重傳

重傳包的優先級高於keepalive,那就意味着,咱們的keepalive老是不能發送出去。 而此時,咱們也並不知道該鏈接已經出錯而中斷。在較長時間的重傳失敗以後,咱們纔會知道

爲了不這種狀況發生,咱們要在tcp上層,自行控制。

對於此消息,記錄發送時間和收到迴應的時間。若是長時間沒有迴應,就多是網絡中斷。若是長時間沒有發送,就是說,長時間沒有進行通訊,能夠自行發一個包,用於keepalive,以保持該鏈接的存在。

測試結果

  按照例子的值在一端的socket上開啓keep alive,而後阻塞在一個recv或者不停的send,這個時候拔了網線,測試從拔掉網線到recv/send返回失敗的時間。

       在linux kernel裏頭的測試發現,對於阻塞型的socket,當recv的時候,若是沒有設置keep alive,即便網線拔掉或者ifdown,recv很長時間不會返回,最長達17分鐘,雖然這個時間比linux的默認超時時間短了不少。

可是若是設置了keep alive,基本都在keepalive_time +keepalive_probeskeepalive_intvl =33秒內返回錯誤。*
       
可是對於循環不停send的socket,當拔掉網線後,會持續一段時間send返回成功(0~10秒左右,取決 於發送數據的量),而後send阻塞,由於協議層的buffer滿了,在等待buffer空閒,大概90秒左右後纔會返回錯誤。
由此看來,send的時候,keep alive彷佛沒有起到做用,這個緣由是由於重傳機制。能夠經過給send以前設置timer來解決的。

相關文章
相關標籤/搜索