Gunicorn、Django、Gevent:生成的线程被阻塞
我们最近切换到了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 个回答
看起来这里没有人真正回答你的问题。
这个阻塞是故意的,还是我们的设置有问题?
你的设置有问题。SQL查询几乎完全是依赖输入输出的,不应该阻塞任何绿色线程(greenlets)。你可能在使用一个不支持gevent的SQL/ORM库,或者你代码中的其他部分导致了阻塞。对于这种任务,你不应该需要使用多进程。
除非你明确在绿色线程上做了join
,否则服务器的响应不应该被阻塞。
一种在后台运行任务的方法是使用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
是一个很有用的工具——好好享受吧!
我决定使用一个synchronous
(同步的)标准工作者,并利用multiprocessing
库。这看起来是目前最简单的解决方案。
我还实现了一个全局的池,利用memcached
缓存来提供锁,这样只有两个任务可以同时运行。