首页经验耗时两天,优化失败

耗时两天,优化失败

圆圆2025-06-25 10:01:10次浏览条评论

你好,我是雨乐!

在上一篇文章基于线程池的线上服务性能优化中,我们提到了使用线程池进行某个业务功能优化,在上线之后,实时性提高了大概24-30倍样子,基本能够满足实时性要求。在正常运行了几天之后,突然收到了报警,提示popen失败,于是打开了日志,发现有如下提示:

代码语言:javascript代码运行次数:0运行复制
popen file failed, id: abc url: http:xxx.txt errno: 12
登录后复制

于是,开始查看错误提示,如下:

耗时两天,优化失败

看来是内存不足,于是,通过free命令查看所在机器的内存信息,如下:

耗时两天,优化失败

可用内存还有2.7G,不至于分配失败呀。

问题定位

看到popen()提示内存分配失败,首先就开始怀疑是否是wget使用有问题,但经过仔细研究之后,发现问题跟该命令无关,这是因为wget仅仅是将文件下载到本地,并不会占用过多的内存。

既然问题与wget命令本身无关,那么问题苗头就指向popen本身了,于是在搜索引擎中搜索popen ENOMEM,其中有一条与本次遇到的问题很像,如下:

耗时两天,优化失败

通过该文内容,得到了一个很重要的信息,那就是popen的实现是fork+execve。熟悉fork()的开发人员都知道,fork()以当前进程作为父进程创建出一个新的子进程,并且将父进程的所有资源拷贝给子进程,这样子进程作为父进程的一个副本存在。既然fork()会生成父进程的一个副本,那么父进程所占用的所有资源,在子进程中也就会被拷贝一份。换句话说,fork()函数为clone父进程的所有资源,这样就能理解为什么当可用内存小于50%的时候,popen()会失败。

于是,为了验证文章的内容是否与本次遇到的问题一致,在本地写了一个简单的测试用例,测试代码中仅仅包含popen()函数,编译,然后使用starce ./test之后,输出如下:

代码语言:javascript代码运行次数:0运行复制
...futex(0x7ffdd648a69c, FUTEX_WAKE, 1)    = 0futex(0x7ffdd648a69c, FUTEX_WAKE_PRIVATE, 1) = 0pipe2([3, 4], O_CLOEXEC)                = 0clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f82fcab37d0) = 27437close(4)                                = 0fcntl(3, F_SETFD, 0)                    = 0exit_group(0) = ?...
登录后复制

在上面的strace命令输出中,我们能看到一个很重要的函数那就是clone()(fork()函数会调用clone()),看来问题就在这。。。

源码分析

为了能够确认是否是因为popen()中的fork()所引起,于是找到了popen()函数的源码实现,如下:

代码语言:javascript代码运行次数:0运行复制
FILE *popen(const char *program, const char *type){    struct pid * volatile cur;    FILE *iop;    int pdes[2];    pid_t pid;    char *argp[] = {"sh", "-c", NULL, NULL};    if ((*type != 'r' && *type != 'w') || type[1] != '\0') {        errno = EINVAL;        return (NULL);    }    if ((cur = malloc(sizeof(struct pid))) == NULL)        return (NULL);    if (pipe(pdes) < 0) {        free(cur);        return (NULL);    }    switch (pid = fork()) {    case -1:   /* Error. */        (void)close(pdes[0]);        (void)close(pdes[1]);        free(cur);        return (NULL);        /* NOTREACHED */    case 0:    /* Child. */        {        struct pid *pcur;        /*         * We fork()'d, we got our own copy of the list, no         * contention.         */        for (pcur = pidlist; pcur; pcur = pcur->next)            close(fileno(pcur->fp));        if (*type == 'r') {            (void) close(pdes[0]);            if (pdes[1] != STDOUT_FILENO) {                (void)dup2(pdes[1], STDOUT_FILENO);                (void)close(pdes[1]);            }        } else {            (void)close(pdes[1]);            if (pdes[0] != STDIN_FILENO) {                (void)dup2(pdes[0], STDIN_FILENO);                (void)close(pdes[0]);            }        }        argp[2] = (char *)program;        execve(_PATH_BSHELL, argp, environ);        _exit(127);        /* NOTREACHED */        }    }    /* Parent; assume fdopen can't fail. */    if (*type == 'r') {        iop = fdopen(pdes[0], type);        (void)close(pdes[1]);    } else {        iop = fdopen(pdes[1], type);        (void)close(pdes[0]);    }    /* Link into list of file descriptors. */    cur->fp = iop;    cur->pid =  pid;    cur->next = pidlist;    pidlist = cur;    return (iop);}
登录后复制

在上述代码中,我们可以看到popen中使用了fork()函数。当调用完fork()函数后,子进程获得父进程的数据空间、堆和栈,但是这是子进程单独拥有的,并不和父进程共享,因此修改子进程的变量不会影响父进程的变量。父进程和子进程共享正文段。进一步验证了我们之前的观点:由于fork()函数创建的子进程复制了一份父进程的资源,如果父进程内存占用过大,使得剩余内存资源不足以使得子进程进行拷贝的时候,那么popen()函数返回失败。

问题解决

既然使用popen会存在fork()函数创建的子进程拷贝父进程资源的情况,那么有没有其它实现方法,能够使得子进程不对父进程的资源进行拷贝呢?

这就是vfork()函数!vfork()的父子进程是共享数据的,也就是说使用vfork()产生的子进程不会复制父进程的资源,而是与父进程共享同一份资源,所以在子程序中修改变量,父进程的变量也会被修改。既然可以使用vfork()能够解决此次遇到的问题,那么,也就可以使用vfork()函数来实现popen()函数的功能了,用以解决此次问题。

为了验证使用vfork()是否会调用clone,写了一个简单的代码,然后使用strace ./test命令,输出如下:

代码语言:javascript代码运行次数:0运行复制
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f64dcaac000write(1, "1\n", 21)                      = 2
登录后复制

可见,用vfork就并没有调用clone(可能与内核版本有关,待再次验证)。

于是开始着手使用vfork()来优化代码,为了与libc中popen进行区分,在此,以vpopen()和vpclose()来实现之前popen()和pclose()函数的功能,代码如下:

代码语言:javascript代码运行次数:0运行复制
//#ifdef  OPEN_MAX//static long openmax = OPEN_MAX;//#elsestatic long openmax = 0;//#endif /* * If OPEN_MAX is indeterminate, we're not * guaranteed that this is adequate. */#define OPEN_MAX_GUESS 1024 long open_max(void){    if (openmax == 0) {      /* first time through */        errno = 0;        if ((openmax = sysconf(_SC_OPEN_MAX)) < 0) {           if (errno == 0)               openmax = OPEN_MAX_GUESS;    /* it's indeterminate */           else               printf("sysconf error for _SC_OPEN_MAX");        }    }     return(openmax);} static pid_t    *childpid = NULL;  /* ptr to array allocated at run-time */static int      maxfd;  /* from our open_max(), {Prog openmax} */ FILE *vpopen(const char* cmdstring, const char *type){    int pfd[2];    FILE *fp;    pid_t   pid;     if((type[0]!='r' && type[0]!='w')||type[1]!=0)    {        errno = EINVAL;        return(NULL);    }     if (childpid == NULL) {     /* first time through */                  /* allocate zeroed out array for child pids */          maxfd = open_max();          if ( (childpid = (pid_t *)calloc(maxfd, sizeof(pid_t))) == NULL)              return(NULL);      }     if(pipe(pfd)!=0)    {        return NULL;    }     if((pid = vfork())<0)    {        return(NULL);   /* errno set by fork() */      }    else if (pid == 0) {    /* child */        if (*type == 'r')        {            close(pfd[0]);              if (pfd[1] != STDOUT_FILENO) {                  dup2(pfd[1], STDOUT_FILENO);                  close(pfd[1]);              }                   }        else        {            close(pfd[1]);              if (pfd[0] != STDIN_FILENO) {                  dup2(pfd[0], STDIN_FILENO);                  close(pfd[0]);              }                   }         /* close all descriptors in childpid[] */          for (int i = 0; i < maxfd; i++)          if (childpid[ i ] > 0)              close(i);           execl("/bin/sh", "sh", "-c", cmdstring, (char *) 0);          _exit(127);         }     if (*type == 'r') {          close(pfd[1]);          if ( (fp = fdopen(pfd[0], type)) == NULL)              return(NULL);      } else {          close(pfd[0]);          if ( (fp = fdopen(pfd[1], type)) == NULL)              return(NULL);      }     childpid[fileno(fp)] = pid; /* remember child pid for this fd */      return(fp);     }  int vpclose(FILE *fp){    int     fd, stat;      pid_t   pid;       if (childpid == NULL)          return(-1);     /* popen() has never been called */       fd = fileno(fp);      if ( (pid = childpid[fd]) == 0)          return(-1);     /* fp wasn't opened by popen() */       childpid[fd] = 0;      if (fclose(fp) == EOF)          return(-1);       while (waitpid(pid, &stat, 0) < 0)          if (errno != EINTR)              return(-1); /* error other than EINTR from waitpid() */       return(stat);   /* return child's termination status */   }
登录后复制

修改现有线上代码如下:

代码语言:javascript代码运行次数:0运行复制
std::string cmd = "wget -t 3 -c -r -nd -P /data1/data/ –delete-after -np -A .txt http://url.txt";auto fp = vpopen(cmd.str().c_str(), "r");if (!fp) {  return;}
登录后复制

编译,运行,然后在线上灰度,开始焦急的等待,此时竟然希望该进程内存占用赶紧超过50%?。

耗时两天,优化失败

赶紧看了下日志,没有输出错误日志,再通过redis命令查询该订单是否已经被加载:

耗时两天,优化失败

一切正常,看来问题已经解决(至少目前来看?)

结语

在本次优化中,使用基于vfork()的vpopen()函数来提到之前的基于fork()实现的popen()函数。最重要的一个原因是使用fork()的popen(),在创建子进程的时候会进行资源复制,即使使用写时复制技术,如果没有足够的内存来复制父进程使用的内存,fork也会失败。而之所以采用vfork(),正是因为其创建的子进程与父进程共享同一份资源,省略了资源拷贝这一个过程,进而解决了此次遇到的内存不足的问题。

但是,正是因为vfork()与父进程共享一份资源,使用稍有不慎,就会导致意想不到的后果,因此在某些内核版本中已经将其标记为废弃(obsolescent),所以本次使用vfork()来实现仅仅是一个临时版本,先让线上功能能够正常使用,后续将继续优化该功能。

生命不息,优化不止!

好了,今天的文章就到这,我们下期见!

以上就是耗时两天,优化失败的详细内容,更多请关注乐哥常识网其它相关文章!

耗时两天   优化失败
ps快捷键2021 ps快捷键怎样设置好用
相关内容
发表评论

游客 回复需填写必要信息