【问题定位】Celery Worker产生僵尸进程

组内有一个基于Flask + rabbitMQ + Celery搭建的web平台,最近在上面开发需求时碰到了一个比较有趣的问题,在这里记录下来。

问题背景

web平台整体架构图如下所示:

Flask向rabbitMQ发送任务消息,后者再将任务分发给不同的Celery worker进行处理。由于每一个任务的处理时间较长,为了不阻塞worker处理下一个任务,在worker中,通过两次fork的方式,生成孤儿进程在后台进行任务处理。

问题现象

  1. worker 生成的孤儿进程在抛出异常后,没有自动退出,仍然处于运行状态。

  2. kill worker的父进程(SIGTERM),父进程不退出,很多worker变成僵尸进程。(所有的celery worker都是由同一个父进程fork出来的

排查过程

这个问题基本上是通过走读代码定位出来的,下面给出简化后的worker代码便于后面分析。

 1def worker():
 2    while True:
 3        # 等待任务...
 4        wait_task()
 5
 6        try:
 7            # 处理任务
 8            execute()
 9        except:
10            on_failure()
11        
12        ...
13
14
15def execute():
16    try:
17        pid = os.fork()
18        if pid < 0:
19            raise Exception
20        elif pid == 0:
21            pid = os.fork()
22            if pid < 0:
23                raise Exception
24            elif pid == 0:
25                # 实际执行任务处理,遇到异常直接raise
26                do_execute()
27                os._exit(0)
28            else:
29                os._exit(0)
30        else:
31            return
32    except:
33        raise

问题原因

在分析问题原因前,先来运行这样一段代码:

1if __name__ == '__main__':
2    pid = os.fork()
3    if pid < 0:
4        print "fork failed"
5    elif pid == 0:
6        print "child pid ", os.getpid()
7    else:
8        print "parent pid ", os.getpid()
9    print "pid ", os.getpid()

运行结果是:

1parent pid  78216
2pid  78216
3child pid  78217
4pid  78217

从这个结果我们可以看出,fork出来的子进程虽然和父进程不共享堆栈(子进程获得父进程堆栈的副本),但是他们共享正文段,所以他们都执行了程序的最后一行,各自输出了自己的pid。

接着来分析上述worker的代码,在execute()中,通过两次fork,最终使得do_execute()运行在一个孤儿进程中,如果正常运行,最终会执行os._exit(0)正常退出。然而,如果运行过程中抛出异常又会发生什么呢?根据父子进程共享正文段这一结论,我们可以知道这个孤儿进程抛出的异常会被第32行的except捕获到,并继续向上抛出异常,然后会被第9行worker()中的except捕获,并执行on_failure()也就是说,这个孤儿进程最终执行到了worker的代码里去了,而worker本身是一个死循环,因此这个孤儿进程就不会退出了。理论上来说,最终它会运行到第4行,成为一个“worker副本”,等待接收任务

至于为什么kill worker的父进程会导致worker变僵尸进程,需要深入研究一下celery源码中的信号处理方法。猜测是父进程在退出前,会先保证所有worker子进程已经退出,而它误以为这个“worker副本”也是自己的子进程,但是却没办法通过向子进程发送信号的方式使其退出,于是就阻塞住了自己的退出流程。而其他已经正常退出的worker就会一直处于僵尸状态。