TheRiver | blog

You have reached the world's edge, none but devils play past here

0%

Tcp_keepalive

墨菲定律

墨菲定律,又译为摩菲定律,具体内容是“凡是可能出错的事就一定会出错”

前言

一个学徒,在第一次接触一件事物时,不能很全面,很深入的认知.一段时间后,没了新鲜感,容易失去兴趣,流于表面.就像我,简单的tcp握手,挥手流程,一直没有真正的了解过,心存侥幸,等到真正遇到这方面的问题,才方恨少于读书.吃了墨菲定律的亏,所以说,还是要一步一步,踏踏实实的行进啊.心存侥幸的,都是赌徒.做学徒,不做赌徒.

参考

TCP Keepalive HOWTO

随手记之TCP Keepalive笔记

介绍

keepalive概念非常简单:当您设置TCP连接时,您可以关联一组计时器。其中一些计时器处理keepalive程序。当keepalive定时器到达零时,您向对等体发送一个keepalive探测数据包,其中没有数据并且ACK标志已打开。

正文

一般服务器进程间都会有一定的保活机制,确保对端已经建立的连接是有效的,如果保活失败则可以进行一定的重连操作尝试恢复.或者直接清理掉无效的连接,减少系统的资源.

除此之外,操作系统(姑且这样认为吧)提供了一些参数,可以设置后通过其实现对连接进行检测.我接触到keeplaive也是在这样的情境下.当上层程序因为种种原因,或是其他网络原因,配置原因.一些tcp连接没有成功释放,例如我最近遇到的:CLOSE_WAIT.系统中存在大量的连接处于CLOSE_WAIT状态,使得进程占用的open file很快就达到上限,严重影响其他业务.

  • CLOSE_WAIT状态


tcp四次挥手的过程中,作为被动关闭方,在收到主动关闭方发来的FIN后,回复ACK确认,然后自己便处于CLOSE_WAIT状态,通常这个状态持续的时间比较短,如果发现环境中存在大量的CLOSE_WAIT状态的连接,则一定有问题了.

解决CLOSE_WAIT

  • 找到问题的根因,对症下药

    可以抓包结合日志等分析,造成问题的具体原因,再去对应的做修改

  • 暂时规避

    通过设置tcp的keepalive值,让操作系统代理对tcp连接进行保活,及时释放有问题的连接

具体操作

可以这样查看当前配置的值:

[root@localhost ~]# sysctl -a | grep keepalive
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_time = 7200

修改系统的缺省值:

echo "net.ipv4.tcp_keepalive_time=7200" > /etc/sysctl.conf
echo "net.ipv4.tcp_keepalive_intvl=75" >> /etc/sysctl.conf
echo "net.ipv4.tcp_keepalive_probes=9" >> /etc/sysctl.conf

//使其生效
sysctl -p

值的解释:

TCP_KEEPDILE 设置连接上如果没有数据发送的话,多久后发送keepalive探测分组,单位是秒
TCP_KEEPINTVL 前后两次探测之间的时间间隔,单位是秒
TCP_KEEPCNT 关闭一个非活跃连接之前的最大重试次数

必要的一步:

手动修改,或者用下面介绍的LD_PRELOAD加载的libkeepalive.so

setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval));  //打开keepalive开关
setsockopt(sockfd, SOL_TCP, TCP_KEEPCNT, &optval, sizeof(optval));        //探测的次数
setsockopt(sockfd, SOL_TCP, TCP_KEEPIDLE, &optval, sizeof(optval));        //多少秒没有数据往来开始探测
setsockopt(sockfd, SOL_TCP, TCP_KEEPINTVL, &optval, sizeof(optval));    //探测的间隔时间
//以此来让创建的sockfd被代理进行保活

libkeepalive.so

下载地址

源码:

#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/tcp.h>

int socket(int domain, int type, int protocol);

int socket(int domain, int type, int protocol)
{
  static int (*orig_socket)(int, int, int) = NULL;
  int sockfd = -1, optval = 1;
  char *env;

  do {
    /* if the original function is NULL, try to resolve it or break */
    if(!orig_socket && !(*(void **)(&orig_socket) = dlsym(RTLD_NEXT, "socket"))) {
      errno = EACCES;
      break;
    }

    /* call original function with parameters */
    if((sockfd = (*orig_socket)(domain, type, protocol)) == -1) break;

    /* socket must be IPv4 or IPv6 */
    if((domain != AF_INET) && (domain != AF_INET6)) break;

    /* socket must be TCP */
    if(!(type & SOCK_STREAM)) break;

    /* if environment variable KEEPALIVE is set to "off", break */
    if((env = getenv("KEEPALIVE")) && !strcasecmp(env, "off")) break;

    /* if setting keepalive fails, break */
    if(setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval)) == -1) break;

    #ifdef TCP_KEEPCNT
    /* if environment variable KEEPCNT is set, override the default option value */
    if((env = getenv("KEEPCNT"))) {
      optval = atoi(env);
      setsockopt(sockfd, SOL_TCP, TCP_KEEPCNT, &optval, sizeof(optval));
    }
    #endif

    #ifdef TCP_KEEPIDLE
    /* if environment variable KEEPIDLE is set, override the default option value */
    if((env = getenv("KEEPIDLE"))) {
      optval = atoi(env);
      setsockopt(sockfd, SOL_TCP, TCP_KEEPIDLE, &optval, sizeof(optval));
    }
    #endif

    #ifdef TCP_KEEPINTVL
    /* if environment variable KEEPINTVL is set, override the default option value */
    if((env = getenv("KEEPINTVL"))) {
      optval = atoi(env);
      setsockopt(sockfd, SOL_TCP, TCP_KEEPINTVL, &optval, sizeof(optval));
    }
    #endif
  } while(0);

  return sockfd;
}

preload更改函数调用

就这么点代码,使用了PRELOAD修改了原有的socket函数,默认都加了设置保活参数的setsockopt.

下面VMWARE验证:
找到了之前的一个简单代码改了下,作为客户端和本地的21端口建立连接,通过preload预加载的方式将libkeepalive.so加载进去:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include <unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

int main()
{
    int iConnFd = 0;
    char szAddr[] = {"127.0.0.1"};
    struct sockaddr_in  stServerAddr = {0};

    for (int i = 0; i < 10000; ++i)
    {
        if ((iConnFd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
        {
            perror("Failed to create socket!");
            return -1;
        }

        stServerAddr.sin_family = AF_INET;
        stServerAddr.sin_port = htons(21);

        if (inet_pton(AF_INET, szAddr, (void*)&stServerAddr.sin_addr) <= 0)
        {
            perror("Failed to swicth ip addr!");
            close(iConnFd);
            iConnFd = -1;
            return -1;
        }

        if (connect(iConnFd, (struct sockaddr*)&stServerAddr, sizeof(stServerAddr)) < 0)
        {
            perror("Failed to connect!");
            close(iConnFd);
        }

        while(1)
            ;
    }


    return 0;
}

可以看到客户端和21端口建立三次握手成功5秒后,就开始发keepalive探测消息了.
再看看系统调用:

socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
setsockopt(3, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0
setsockopt(3, SOL_TCP, TCP_KEEPCNT, [5], 4) = 0
setsockopt(3, SOL_TCP, TCP_KEEPIDLE, [5], 4) = 0
setsockopt(3, SOL_TCP, TCP_KEEPINTVL, [5], 4) = 0
connect(3, {sa_family=AF_INET, sin_port=htons(21), sin_addr=inet_addr("127.0.0.1")}, 16) = 0

使用的命令:

env LD_PRELOAD="/home/wang/libkeepalive-0.3/libkeepalive.so" KEEPCNT=5 KEEPIDLE=5 KEEPINTVL=5 ./a.out

ending

----------- ending -----------