使用fork后的文件描述符泄漏问题

首先这个问题相当小白,如果已经了解的同学看到这个标题应该就明白了,就能略过不看了。

我遇到这个问题是对改造的twemproxy做异常测试时出现的。

首先简介一下改造的多进程版本的twemproxy原理。

改造的twemproxy会连接zk去获取一些信息,建立监视使得后端redis掉线(使用一个watchdog来将redis注册到zookeeper上)能够通过zookeeper被通知到。

而后才会fork出子进程,主进程当获取到zookeeper的watch消息后,会通过管道来通知子进程去连接此时正常的redis。

我在测试时开启了twemproxy所在机器的防火墙,关闭了除22端口外全部流量。

一段时间后打开防火墙,发现存在两个zk的连接,一个是ESTABLISH一个是CLOSE_WAIT状态。

按照我对TCP状态转换的理解,CLOSE_WAIT是服务端发出fin包以后,客户端没有发出ack+fin包,可以判定客户端没有关闭连接。

我写了个demo程序

#include <zookeeper/zookeeper.h>

int main(int argc, char **argv)
{
        zhandle_t *zh = zookeeper_init("172.16.10.96:2181", NULL, 40000, NULL, NULL, 0);
        fork();
        sleep(10000);
        return 0;
}

有时候能复现CLOSE_WAIT泄漏有时候又不能。

看了下zookeeper_init的代码,会调用zookeeper_mt库的adaptor_init的do_io线程,这个后台线程会连接zookeeper服务器

也就是说zookeeper_init并不是同步的建立连接,在fork()前加入一句sleep(1)后,等待异步建立完成后在fork,果然能够次次复现了

重新编译了下zookeeper_mt库,打印了调用close()的函数,发现close是被成功调用的。

稍微搜了下相关的问题,看到有人这么说

If you do the fork() after zookeeper_init() - then it will get messier. As I understand each forked process will increment the reference count on any opened file descriptors. So you'll have to take care to close the "shared" file descriptors in every "other" process before you call zookeeper_close().

hmmm,这个时候我想到了fork前的描述符是共享的,但是我之前以为只要父进程或者子进程调用一次close,那么这个连接就会被关闭。于是我又写了个小程序

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

int main(int argc, char **argv)
{
        struct sockaddr_in server_addr;
        bzero(&server_addr,sizeof(server_addr));
        server_addr.sin_family=AF_INET;
        server_addr.sin_port=htons(2181);
        server_addr.sin_addr.s_addr=inet_addr("172.16.10.96");
        int fd=socket(AF_INET,SOCK_STREAM,0);
        connect(fd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr));
        if(fork()){
            close(fd);
        }
        sleep(10000);
        return 0;
}

父进程关闭连接以后使用netstat -ntp看,连接依然存在。当服务端关闭该连接后,该连接就会进入CLOSE_WAIT状态。

很明显,close只是对该FD的引用计数减一,如果是一个父子进程共享的描述符,并不会被真正的关闭。

原因已经找到了,解决问题有两个办法

一。fork以后再去zookeeper_init

二。zookeeper_int以后fork,然后手动的关闭zookeeper相关的描述符

由于我的代码框架已经定下来了,重构会有较大的不确定性,还是打算用第二个办法。

那么如何去关闭建立的描述符呢?zookeeper_close这个API肯定是不行的。

zookeeper_close把close_request变量设为1,使得do_io循环停止。

但是设置的是子进程的close_request变量,对主进程的do_io线程不会有影响,另外即使设置成功了,不仅会干掉子进程的连接同时会干掉主进程的连接。

然而zookeeper的API并没有暴露出zhandle_t结构体的相关接口,我看了下结构体的内容

struct _zhandle {
#ifdef WIN32
    SOCKET fd; /* the descriptor used to talk to zookeeper */
#else
    int fd; /* the descriptor used to talk to zookeeper */
#endif
    ...
}
typedef struct _zhandle zhandle_t;

描述符地址和zhandle_t地址相同,那么就很简单了。

#include <zookeeper/zookeeper.h>

int main(int argc, char **argv)
{
        zhandle_t *zh = zookeeper_init("172.16.10.96:2181", NULL, 40000, NULL, NULL, 0);
        sleep(1);
        if(!fork())
            close(*(int*)zh);
        sleep(10000);
        return 0;
}

再进行测试,OK,CLOSE_WAIT泄漏问题解决。

#后续思考:

1 这个问题主要还是自己对fork的理解不够深刻,类似的问题只要新建连接都会遇到,例如mysql,redis客户端等等。所以正确的顺序应该是先fork以后,在去创建这些连接。

没有人带自己摸索还是挺难的啊,不过比人家直接告诉你,也印象深刻有成就感就是了。

2 (2015.12.26更新) 如果在写三方库的时候使用shutdown代替close,就可以避免这个问题

close与shutdown的区别主要表现在:

close函数会关闭套接字ID,如果有其他的进程共享着这个套接字,那么它仍然是打开的,这个连接仍然可以用来读和写,并且有时候这是非常重要的 ,特别是对于多进程并发服务器来说。

而shutdown会切断进程共享的套接字的所有连接,不管这个套接字的引用计数是否为零,那些试图读得进程将会接收到EOF标识,那些试图写的进程将会检测到SIGPIPE信号,同时可利用shutdown的第二个参数选择断连的方式。