<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    聶永的博客

    記錄工作/學習的點點滴滴。

    隨手記之TCP Keepalive筆記

    零。前言

    TCP是無感知的虛擬連接,中間斷開兩端不會立刻得到通知。一般在使用長連接的環境下,需要心跳?;顧C制可以勉強感知其存活。業務層面有心跳機制,TCP協議也提供了心跳?;顧C制。

    一。TCP Keepalive解讀

    長連接的環境下,人們一般使用業務層面或上層應用層協議(諸如MQTT,SOCKET.IO等)里面定義和使用。一旦有熱數據需要傳遞,若此時連接已經被中介設備斷開,應用程序沒有及時感知的話,那么就會導致在一個無效的數據鏈路層面發送業務數據,結果就是發送失敗。

    無論是因為客戶端意外斷電、死機、崩潰、重啟,還是中間路由網絡無故斷開、NAT超時等,服務器端要做到快速感知失敗,減少無效鏈接操作。

    1. 交互過程

    2. 協議解讀

    下面協議解讀,基于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部分:

    1. 不太好的TCP堆棧實現,可能會要求?;顖笪谋仨殧y帶有1個字節的數據負載
    2. TCP Keepalive應該在服務器端啟用,客戶端不做任何改動;若單獨在客戶端啟用,若客戶端異常崩潰或出現連接故障,存在服務器無限期的為已打開的但已失效的文件描述符消耗資源的嚴重問題。但在特殊的NFS文件系統環境下,需要客戶端和服務器端都要啟用Tcp Keepalive機制。
    3. TCP Keepalive不是TCP規范的一部分,有三點需要注意:
      • 在短暫的故障期間,它們可能引起一個良好連接(good connection)被釋放(dropped)
      • 它們消費了不必要的寬帶
      • 在以數據包計費的互聯網消費(額外)花費金錢

    二。Tcp keepalive 如何使用

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

    1. 系統內核參數配置

    1. tcp_keepalive_time,在TCP保活打開的情況下,最后一次數據交換到TCP發送第一個?;钐綔y包的間隔,即允許的持續空閑時長,或者說每次正常發送心跳的周期,默認值為7200s(2h)。
    2. tcp_keepalive_probes 在tcp_keepalive_time之后,沒有接收到對方確認,繼續發送?;钐綔y包次數,默認值為9(次)
    3. tcp_keepalive_intvl,在tcp_keepalive_time之后,沒有接收到對方確認,繼續發送?;钐綔y包的發送頻率,默認值為75s。

    發送頻率tcp_keepalive_intvl乘以發送次數tcp_keepalive_probes,就得到了從開始探測到放棄探測確定連接斷開的時間

    若設置,服務器在客戶端連接空閑的時候,每90秒發送一次?;钐綔y包到客戶端,若沒有及時收到客戶端的TCP Keepalive ACK確認,將繼續等待15秒*2=30秒??傊梢栽?0s+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的套接字,應用程序不用重啟,內核直接生效。

    2. 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配置,系統進行讀取。

    3. C語言如何設置

    下面代碼摘取自libkeepalive源碼,C語言可以設置更為詳細的TCP內核參數。

    int socket(int domain, int type, int protocol)
    {
      int (*libc_socket)(int, int, int);
      int s, optval;
      char *env;
    
      *(void **)(&libc_socket) = dlsym(RTLD_NEXT, "socket");
      if(dlerror()) {
        errno = EACCES;
        return -1;
      }
    
      if((s = (*libc_socket)(domain, type, protocol)) != -1) {
        if((domain == PF_INET) && (type == SOCK_STREAM)) {
          if(!(env = getenv("KEEPALIVE")) || strcasecmp(env, "off")) {
            optval = 1;
          } else {
            optval = 0;
          }
          if(!(env = getenv("KEEPALIVE")) || strcasecmp(env, "skip")) {
            setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval));
          }
    #ifdef TCP_KEEPCNT
          if((env = getenv("KEEPCNT")) && ((optval = atoi(env)) >= 0)) {
            setsockopt(s, SOL_TCP, TCP_KEEPCNT, &optval, sizeof(optval));
          }
    #endif
    #ifdef TCP_KEEPIDLE
          if((env = getenv("KEEPIDLE")) && ((optval = atoi(env)) >= 0)) {
            setsockopt(s, SOL_TCP, TCP_KEEPIDLE, &optval, sizeof(optval));
          }
    #endif
    #ifdef TCP_KEEPINTVL
          if((env = getenv("KEEPINTVL")) && ((optval = atoi(env)) >= 0)) {
            setsockopt(s, SOL_TCP, TCP_KEEPINTVL, &optval, sizeof(optval));
          }
    #endif
        }
      }
    
       return s;
    }
    

    4. 針對已有程序沒有硬編碼KTTCP EEPALIVE實現

    完全可以借助于第三方工具libkeepalive,通過LD_PRELOAD方式實現。比如

    LD_PRELOAD=/the/path/libkeepalive.so java -jar /your/path/yourapp.jar &
    

    這個工具還有一個比較方便的地方,可以直接在程序運行前指定TCP?;钤敿殔担梢允∪ヅ渲胹ysctl.conf的麻煩:

    LD_PRELOAD=/the/path/libkeepalive.so \
      > KEEPCNT=20 \
      > KEEPIDLE=180 \
      > KEEPINTVL=60 \
      > java -jar /your/path/yourapp.jar &
    

    針對較老很久不更新的程序,可以嘗試一下嘛。

    三。Linux內核層面對keepalive處理

    參數和定義

    #define MAX_TCP_KEEPIDLE     32767
    #define MAX_TCP_KEEPINTVL     32767
    #define MAX_TCP_KEEPCNT          127
    #define MAX_TCP_SYNCNT          127
    
    #define TCP_KEEPIDLE          4     /* Start keeplives after this period */
    #define TCP_KEEPINTVL          5     /* Interval between keepalives */
    #define TCP_KEEPCNT          6     /* Number of keepalives before death */
    

    net/ipv4/Tcp.c,可以找到對應關系:

         case TCP_KEEPIDLE:
              val = (tp->keepalive_time ? : sysctl_tcp_keepalive_time) / HZ;
              break;
         case TCP_KEEPINTVL:
              val = (tp->keepalive_intvl ? : sysctl_tcp_keepalive_intvl) / HZ;
              break;
         case TCP_KEEPCNT:
              val = tp->keepalive_probes ? : sysctl_tcp_keepalive_probes;
              break;
    

    初始化:

     case TCP_KEEPIDLE:
          if (val < 1 || val > MAX_TCP_KEEPIDLE)
               err = -EINVAL;
          else {
               tp->keepalive_time = val * HZ;
               if (sock_flag(sk, SOCK_KEEPOPEN) &&
                   !((1 << sk->sk_state) &
                     (TCPF_CLOSE | TCPF_LISTEN))) {
                    __u32 elapsed = tcp_time_stamp - tp->rcv_tstamp;
                    if (tp->keepalive_time > elapsed)
                         elapsed = tp->keepalive_time - elapsed;
                    else
                         elapsed = 0;
                    inet_csk_reset_keepalive_timer(sk, elapsed);
               }
          }
          break;
     case TCP_KEEPINTVL:
          if (val < 1 || val > MAX_TCP_KEEPINTVL)
               err = -EINVAL;
          else
               tp->keepalive_intvl = val * HZ;
          break;
     case TCP_KEEPCNT:
          if (val < 1 || val > MAX_TCP_KEEPCNT)
               err = -EINVAL;
          else
               tp->keepalive_probes = val;
          break;
    

    這里可以找到大部分處理邏輯,net/ipv4/Tcp_timer.c:

    static void tcp_keepalive_timer (unsigned long data)
    {
         struct sock *sk = (struct sock *) data;
         struct inet_connection_sock *icsk = inet_csk(sk);
         struct tcp_sock *tp = tcp_sk(sk);
         __u32 elapsed;
    
         /* Only process if socket is not in use. */
         bh_lock_sock(sk);
         if (sock_owned_by_user(sk)) {
              /* Try again later. */
              inet_csk_reset_keepalive_timer (sk, HZ/20);
              goto out;
         }
    
         if (sk->sk_state == TCP_LISTEN) {
              tcp_synack_timer(sk);
              goto out;
         }
        // 關閉狀態的處理
         if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) {
              if (tp->linger2 >= 0) {
                   const int tmo = tcp_fin_time(sk) - TCP_TIMEWAIT_LEN;
    
                   if (tmo > 0) {
                        tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
                        goto out;
                   }
              }
              tcp_send_active_reset(sk, GFP_ATOMIC);
              goto death;
         }
    
         if (!sock_flag(sk, SOCK_KEEPOPEN) || sk->sk_state == TCP_CLOSE)
              goto out;
    
         elapsed = keepalive_time_when(tp);
    
         /* It is alive without keepalive 8) */
         if (tp->packets_out || sk->sk_send_head)
              goto resched;
    
         elapsed = tcp_time_stamp - tp->rcv_tstamp;
    
         if (elapsed >= keepalive_time_when(tp)) {
              if ((!tp->keepalive_probes && icsk->icsk_probes_out >= sysctl_tcp_keepalive_probes) ||
                   (tp->keepalive_probes && icsk->icsk_probes_out >= tp->keepalive_probes)) {
                   tcp_send_active_reset(sk, GFP_ATOMIC);
                   tcp_write_err(sk); // 向上層應用匯報連接異常
                   goto out;
              }
              if (tcp_write_wakeup(sk) <= 0) {
                   icsk->icsk_probes_out++; // 這里僅僅是計數,并沒有再次發送?;钐綔y包
                   elapsed = keepalive_intvl_when(tp);
              } else {
                   /* If keepalive was lost due to local congestion,
                   * try harder.
                   */
                   elapsed = TCP_RESOURCE_PROBE_INTERVAL;
              }
         } else {
              /* It is tp->rcv_tstamp + keepalive_time_when(tp) */
              elapsed = keepalive_time_when(tp) - elapsed;
         }
    
         TCP_CHECK_TIMER(sk);
         sk_stream_mem_reclaim(sk);
    
    resched:
         inet_csk_reset_keepalive_timer (sk, elapsed);
         goto out;
    
    death:    
         tcp_done(sk);
    
    out:
         bh_unlock_sock(sk);
         sock_put(sk);
    }
    

    keepalive_intvl_when 函數定義:

    static inline int keepalive_intvl_when(const struct tcp_sock *tp)
    {
        return tp->keepalive_intvl ? : sysctl_tcp_keepalive_intvl;
    }
    

    四。TCP Keepalive 引發的錯誤

    啟用TCP Keepalive的應用程序,一般可以捕獲到下面幾種類型錯誤

    1. ETIMEOUT 超時錯誤,在發送一個探測保護包經過(tcp_keepalive_time + tcp_keepalive_intvl * tcp_keepalive_probes)時間后仍然沒有接收到ACK確認情況下觸發的異常,套接字被關閉
      java.io.IOException: Connection timed out
      
    2. EHOSTUNREACH host unreachable(主機不可達)錯誤,這個應該是ICMP匯報給上層應用的。
      java.io.IOException: No route to host
      
    3. 鏈接被重置,終端可能崩潰死機重啟之后,接收到來自服務器的報文,然物是人非,前朝往事,只能報以無奈重置宣告之。
      java.io.IOException: Connection reset by peer
      

    五。常見的使用模式

    1. 默認情況下使用keepalive周期為2個小時,如不選擇更改,屬于誤用范疇,造成資源浪費:內核會為每一個連接都打開一個保活計時器,N個連接會打開N個?;钣嫊r器。 優勢很明顯:
    • TCP協議層面保活探測機制,系統內核完全替上層應用自動給做好了
    • 內核層面計時器相比上層應用,更為高效
    • 上層應用只需要處理數據收發、連接異常通知即可
    • 數據包將更為緊湊
    1. 關閉TCP的keepalive,完全使用業務層面心跳?;顧C制 完全應用掌管心跳,靈活和可控,比如每一個連接心跳周期的可根據需要減少或延長
    2. 業務心跳 + TCP keepalive一起使用,互相作為補充,但TCP保活探測周期和應用的心跳周期要協調,以互補方可,不能夠差距過大,否則將達不到設想的效果。朋友的公司所做IM平臺業務心跳2-5分鐘智能調整 + tcp keepalive 300秒,組合協作,據說效果也不錯。

    雖然說沒有固定的模式可遵循,那么有以下原則可以參考:

    • 不想折騰,那就棄用TCP Keepalive吧,完全依賴應用層心跳機制,靈活可控性強
    • 除非可以很好把控TCP Keepalive機制,那就可以根據需要自由使用吧

    六。注意和 HTTP的Keep-Alive區別

    • HTTP協議的Keep-Alive意圖在于連接復用,同一個連接上串行方式傳遞請求-響應數據
    • TCP的keepalive機制意圖在于保活、心跳,檢測連接錯誤。

    七。引用

    1. 我來說說TCP?;?/a>
    2. TCP Keepalive HOWTO

    posted on 2015-04-14 17:08 nieyong 閱讀(49401) 評論(1)  編輯  收藏

    評論

    # re: 隨手記之TCP Keepalive筆記 2016-07-14 18:29 hy

    請問一下筆者,tcp的服務端和客戶端,二者之間既然可以進行通信,那么肯定可以做到一方定時向另一方發送心跳數據包,這已經實現了心跳機制,為何還需要操作系統層面的支持?為何需要高層應用打開這個keepalive的機制??  回復  更多評論   


    只有注冊用戶登錄后才能發表評論。


    網站導航:
     

    公告

    所有文章皆為原創,若轉載請標明出處,謝謝~

    新浪微博,歡迎關注:

    導航

    <2016年7月>
    262728293012
    3456789
    10111213141516
    17181920212223
    24252627282930
    31123456

    統計

    常用鏈接

    留言簿(58)

    隨筆分類(130)

    隨筆檔案(151)

    個人收藏

    最新隨筆

    搜索

    最新評論

    閱讀排行榜

    評論排行榜

    主站蜘蛛池模板: 国产偷伦视频免费观看| 一本到卡二卡三卡免费高| 99视频有精品视频免费观看| 久久亚洲精品无码观看不卡| 人妻仑刮八A级毛片免费看| 亚洲精品国产高清不卡在线| 理论片在线观看免费| 亚洲女同成人AⅤ人片在线观看| 麻豆va在线精品免费播放| 亚洲精品成人a在线观看| 久久99精品免费一区二区| 亚洲午夜福利在线观看| 久久成人免费电影| 亚洲高清中文字幕| 最近中文字幕免费mv视频7| 亚洲一日韩欧美中文字幕在线| 日本无吗免费一二区| 一级毛片免费在线播放| 亚洲成AV人片在线观看无码| 亚洲精品视频免费看| 亚洲小说图区综合在线| 亚洲精品无码日韩国产不卡?V| 中国一级全黄的免费观看| 精品无码一区二区三区亚洲桃色| 免费国产作爱视频网站| 立即播放免费毛片一级| 亚洲情XO亚洲色XO无码| 日日麻批免费40分钟日本的| 色欲aⅴ亚洲情无码AV蜜桃| 亚洲伊人久久精品影院| h视频在线观看免费完整版| 亚洲狠狠婷婷综合久久蜜芽| 国产精品亚洲美女久久久 | 日本高清免费中文在线看| 亚洲一区二区三区在线观看精品中文| 特级精品毛片免费观看| 亚洲av第一网站久章草| 亚洲成AV人在线观看天堂无码| 好男人看视频免费2019中文| 中国国产高清免费av片| 亚洲狠狠婷婷综合久久蜜芽|