聶永的博客

          記錄工作/學(xué)習(xí)的點(diǎn)點(diǎn)滴滴。

          隨手記之TCP Keepalive筆記

          零。前言

          TCP是無感知的虛擬連接,中間斷開兩端不會(huì)立刻得到通知。一般在使用長連接的環(huán)境下,需要心跳保活機(jī)制可以勉強(qiáng)感知其存活。業(yè)務(wù)層面有心跳機(jī)制,TCP協(xié)議也提供了心跳保活機(jī)制。

          一。TCP Keepalive解讀

          長連接的環(huán)境下,人們一般使用業(yè)務(wù)層面或上層應(yīng)用層協(xié)議(諸如MQTT,SOCKET.IO等)里面定義和使用。一旦有熱數(shù)據(jù)需要傳遞,若此時(shí)連接已經(jīng)被中介設(shè)備斷開,應(yīng)用程序沒有及時(shí)感知的話,那么就會(huì)導(dǎo)致在一個(gè)無效的數(shù)據(jù)鏈路層面發(fā)送業(yè)務(wù)數(shù)據(jù),結(jié)果就是發(fā)送失敗。

          無論是因?yàn)榭蛻舳艘馔鈹嚯姟⑺罊C(jī)、崩潰、重啟,還是中間路由網(wǎng)絡(luò)無故斷開、NAT超時(shí)等,服務(wù)器端要做到快速感知失敗,減少無效鏈接操作。

          1. 交互過程

          2. 協(xié)議解讀

          下面協(xié)議解讀,基于RFC1122#TCP Keep-Alives

          1. TCP Keepalive雖不是標(biāo)準(zhǔn)規(guī)范,但操作系統(tǒng)一旦實(shí)現(xiàn),默認(rèn)情況下須為關(guān)閉,可以被上層應(yīng)用開啟和關(guān)閉。
          2. TCP Keepalive必須在沒有任何數(shù)據(jù)(包括ACK包)接收之后的周期內(nèi)才會(huì)被發(fā)送,允許配置,默認(rèn)值不能夠小于2個(gè)小時(shí)
          3. 不包含數(shù)據(jù)的ACK段在被TCP發(fā)送時(shí)沒有可靠性保證,意即一旦發(fā)送,不確保一定發(fā)送成功。系統(tǒng)實(shí)現(xiàn)不能對任何特定探針包作死連接對待
          4. 規(guī)范建議keepalive保活包不應(yīng)該包含數(shù)據(jù),但也可以包含1個(gè)無意義的字節(jié),比如0x0。
          5. SEG.SEQ = SND.NXT-1,即TCP保活探測報(bào)文序列號將前一個(gè)TCP報(bào)文序列號減1。SND.NXT = RCV.NXT,即下一次發(fā)送正常報(bào)文序號等于ACK序列號;總之保活報(bào)文不在窗口控制范圍內(nèi) 有一張圖,可以很容易說明,但請仔細(xì)觀察Tcp Keepalive部分:

          1. 不太好的TCP堆棧實(shí)現(xiàn),可能會(huì)要求保活報(bào)文必須攜帶有1個(gè)字節(jié)的數(shù)據(jù)負(fù)載
          2. TCP Keepalive應(yīng)該在服務(wù)器端啟用,客戶端不做任何改動(dòng);若單獨(dú)在客戶端啟用,若客戶端異常崩潰或出現(xiàn)連接故障,存在服務(wù)器無限期的為已打開的但已失效的文件描述符消耗資源的嚴(yán)重問題。但在特殊的NFS文件系統(tǒng)環(huán)境下,需要客戶端和服務(wù)器端都要啟用Tcp Keepalive機(jī)制。
          3. TCP Keepalive不是TCP規(guī)范的一部分,有三點(diǎn)需要注意:
            • 在短暫的故障期間,它們可能引起一個(gè)良好連接(good connection)被釋放(dropped)
            • 它們消費(fèi)了不必要的寬帶
            • 在以數(shù)據(jù)包計(jì)費(fèi)的互聯(lián)網(wǎng)消費(fèi)(額外)花費(fèi)金錢

          二。Tcp keepalive 如何使用

          以下環(huán)境是在Linux服務(wù)器上進(jìn)行。應(yīng)用程序若想使用,需要設(shè)置SO_KEEPALIVE套接口選項(xiàng)才能夠生效。

          1. 系統(tǒng)內(nèi)核參數(shù)配置

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

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

          若設(shè)置,服務(wù)器在客戶端連接空閑的時(shí)候,每90秒發(fā)送一次保活探測包到客戶端,若沒有及時(shí)收到客戶端的TCP Keepalive ACK確認(rèn),將繼續(xù)等待15秒*2=30秒。總之可以在90s+30s=120秒(兩分鐘)時(shí)間內(nèi)可檢測到連接失效與否。

          以下改動(dòng),需要寫入到/etc/sysctl.conf文件:

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

          保存退出,然后執(zhí)行sysctl -p生效。可通過 sysctl -a | grep keepalive 命令檢測一下是否已經(jīng)生效。

          針對已經(jīng)設(shè)置SO_KEEPALIVE的套接字,應(yīng)用程序不用重啟,內(nèi)核直接生效。

          2. Java/netty服務(wù)器如何使用

          只需要在服務(wù)器端一方設(shè)置即可,客戶端完全不用設(shè)置,比如基于netty 4服務(wù)器程序:

          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程序只能做到設(shè)置SO_KEEPALIVE選項(xiàng),至于TCP_KEEPCNT,TCP_KEEPIDLE,TCP_KEEPINTVL等參數(shù)配置,只能依賴于sysctl配置,系統(tǒng)進(jìn)行讀取。

          3. C語言如何設(shè)置

          下面代碼摘取自libkeepalive源碼,C語言可以設(shè)置更為詳細(xì)的TCP內(nèi)核參數(shù)。

          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實(shí)現(xiàn)

          完全可以借助于第三方工具libkeepalive,通過LD_PRELOAD方式實(shí)現(xiàn)。比如

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

          這個(gè)工具還有一個(gè)比較方便的地方,可以直接在程序運(yùn)行前指定TCP保活詳細(xì)參數(shù),可以省去配置sysctl.conf的麻煩:

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

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

          三。Linux內(nèi)核層面對keepalive處理

          參數(shù)和定義

          #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,可以找到對應(yīng)關(guān)系:

               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;
               }
              // 關(guān)閉狀態(tài)的處理
               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); // 向上層應(yīng)用匯報(bào)連接異常
                         goto out;
                    }
                    if (tcp_write_wakeup(sk) <= 0) {
                         icsk->icsk_probes_out++; // 這里僅僅是計(jì)數(shù),并沒有再次發(fā)送保活探測包
                         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 函數(shù)定義:

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

          四。TCP Keepalive 引發(fā)的錯(cuò)誤

          啟用TCP Keepalive的應(yīng)用程序,一般可以捕獲到下面幾種類型錯(cuò)誤

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

          五。常見的使用模式

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

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

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

          六。注意和 HTTP的Keep-Alive區(qū)別

          • HTTP協(xié)議的Keep-Alive意圖在于連接復(fù)用,同一個(gè)連接上串行方式傳遞請求-響應(yīng)數(shù)據(jù)
          • TCP的keepalive機(jī)制意圖在于保活、心跳,檢測連接錯(cuò)誤。

          七。引用

          1. 我來說說TCP保活
          2. TCP Keepalive HOWTO

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

          評論

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

          請問一下筆者,tcp的服務(wù)端和客戶端,二者之間既然可以進(jìn)行通信,那么肯定可以做到一方定時(shí)向另一方發(fā)送心跳數(shù)據(jù)包,這已經(jīng)實(shí)現(xiàn)了心跳機(jī)制,為何還需要操作系統(tǒng)層面的支持?為何需要高層應(yīng)用打開這個(gè)keepalive的機(jī)制??  回復(fù)  更多評論   


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


          網(wǎng)站導(dǎo)航:
           

          公告

          所有文章皆為原創(chuàng),若轉(zhuǎn)載請標(biāo)明出處,謝謝~

          新浪微博,歡迎關(guān)注:

          導(dǎo)航

          <2015年4月>
          2930311234
          567891011
          12131415161718
          19202122232425
          262728293012
          3456789

          統(tǒng)計(jì)

          常用鏈接

          留言簿(58)

          隨筆分類(130)

          隨筆檔案(151)

          個(gè)人收藏

          最新隨筆

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 彩票| 湖北省| 历史| 广安市| 大悟县| 买车| 越西县| 胶南市| 玉林市| 岑溪市| 浦北县| 平谷区| 冀州市| 巨鹿县| 内丘县| 华亭县| 奎屯市| 宾阳县| 衡阳县| 桐城市| 仪征市| 北川| 伊川县| 江源县| 武山县| 惠东县| 葫芦岛市| 昔阳县| 蓬莱市| 兴海县| 太湖县| 应城市| 德令哈市| 宿迁市| 福鼎市| 通江县| 牟定县| 安西县| 麻江县| 色达县| 永仁县|