Gunicorn、Django、Gevent:生成的线程被阻塞

9 投票
3 回答
6306 浏览
提问于 2025-04-18 11:49

我们最近切换到了Gunicorn,并使用了gevent工作线程。

在我们的网站上,有一些任务需要花费比较长的时间来完成,超过30秒。

前言

我们之前已经尝试过使用celery,但这些任务运行得非常少,所以一直保持celery和redis在运行是不现实的。我们不想这样做,也不想根据需要启动celery和redis。我们希望彻底摆脱它们。(对此我很抱歉,但我想避免那种回答:“你为什么不使用celery,它很好!”)

我们想要异步运行的任务

我说的是那些需要执行3000个SQL查询(插入)的任务,这些查询必须一个接一个地进行。这种情况并不常见。我们还限制同时只能运行2个这样的任务。每个任务大约需要2-3分钟

我们的做法

现在,我们的做法是利用gevent工作线程,使用gevent.spawn来启动任务并返回响应。

问题

我发现启动的线程实际上是阻塞的。一旦响应返回,任务就开始运行,而在任务运行期间,其他请求都无法处理。任务会在30秒后被终止,这是gunicorn的timeout设置。为了防止这种情况,我在每个SQL查询后使用time.sleep(),这样服务器就有机会响应请求,但我觉得这并不是解决问题的根本办法。

设置情况

我们运行gunicorn、django,并使用gevent。上述行为在我的开发环境中出现,并且只使用了1个gevent工作线程。在生产环境中,我们也只会运行1个工作线程(目前如此)。而且,运行2个工作线程似乎并没有帮助处理更多请求,尤其是在一个任务阻塞的情况下。

总结

  • 我们认为使用gevent线程来处理我们的2分钟任务是可行的(比celery更好)
  • 我们使用gunicorn和gevent,想知道为什么用gevent.spawn启动的线程会阻塞
  • 这种阻塞是故意的,还是我们的设置有问题?

谢谢!

3 个回答

0

看起来这里没有人真正回答你的问题。

这个阻塞是故意的,还是我们的设置有问题?

你的设置有问题。SQL查询几乎完全是依赖输入输出的,不应该阻塞任何绿色线程(greenlets)。你可能在使用一个不支持gevent的SQL/ORM库,或者你代码中的其他部分导致了阻塞。对于这种任务,你不应该需要使用多进程。

除非你明确在绿色线程上做了join,否则服务器的响应不应该被阻塞。

0

一种在后台运行任务的方法是使用fork来分裂父进程。和Gevent不同,这种方式不会造成阻塞——你实际上是在运行两个完全独立的进程。这种方法比启动另一个(非常便宜的)绿色线程要慢,但在这种情况下,这是一个不错的权衡。

你的进程会分成两个部分,一个是父进程,一个是子进程。在父进程中,return一个响应给Gunicorn,就像在正常代码中那样。

在子进程中,进行你需要的长时间处理。最后,使用一种特殊的exit方式来清理。这里有一段代码可以处理任务并发送邮件:

    if os.fork():
        return JsonResponse({}) # async parent: return HTTP 200
    # child: do stuff, exit quietly
    ret = do_tag_notify(
        event, emails=emails, photo_names=photo_names,
        )
    logging.info('do_tag_notify/async result={0}'.format(ret))
    os._exit(0)             # pylint: disable=W0212
    logging.error("async child didn't _exit correctly") # never happens

要小心使用这个。如果子进程中出现了异常,比如语法错误或者未使用的变量,你将永远不知道!父进程的日志已经消失了。要多记录日志,但不要做得太过分。

使用fork是一个很有用的工具——好好享受吧!

-2

我决定使用一个synchronous(同步的)标准工作者,并利用multiprocessing库。这看起来是目前最简单的解决方案。

我还实现了一个全局的,利用memcached缓存来提供锁,这样只有两个任务可以同时运行。

撰写回答