Gunicorn: 澄清 timeout 和 graceful_timeout 的工作方式/方式

创建于 2017-04-03  ·  30评论  ·  资料来源: benoitc/gunicorn

(对不起这里的独白:简单的事情变得复杂了,我最终挖掘了堆栈。希望我所记录的内容对读者有所帮助。)

据我了解,默认情况下:

  • 30秒(可配置为timeout )请求处理之后,gunicorn 主进程将SIGTERM发送到工作进程,以启动正常重启。
  • 如果工作进程在另一个30秒(可配置为graceful_timeout )内没有关闭,则主进程发送SIGKILL 。 似乎在graceful_timeout期间工作人员_does_ 正常关闭时也会发送此信号(https://github.com/benoitc/gunicorn/commit/d1a09732256fa8db900a1fe75a71466cf2645ef9)。

问题:

  • 信号是否正确?
  • 当 gunicorn (sync) worker 接收到这些信号时会发生什么? 它如何告诉 WSGI 应用程序该信号已被捕获并且应该发生某些事情(好吧,我假设它只是“传递它”)?
  • 例如,Flask 如何处理SIGTERM信号 - 实际上,在请求处理期间会发生什么? 它是否只是为 WSGI 应用程序(在 werkzeug 级别)设置了一个标志,它应该在请求处理完成后关闭? 或者SIGTERM是否已经以某种方式影响正在进行的请求处理 - 终止 IO 连接或加速请求处理的东西......?

SIGKILL上,我猜请求处理只是被强行中止了。

如果我了解事情的实际运作方式,我可以提交一个小小的 PR 来改进有关此的文档。

Discussion Documentation

最有用的评论

@tuukkamustonen --timeout并不意味着请求超时。 这意味着对工人进行活性检查。 对于同步工作人员,这起到了请求超时的作用,因为工作人员除了处理请求之外不能做任何事情。 异步工作人员即使在处理长时间运行的请求时也会心跳,因此除非工作人员阻塞/冻结它不会被杀死。

如果其他人对此感到困惑,也许我们更改名称是个好主意。

所有30条评论

嗯,我认为https://github.com/benoitc/gunicorn/issues/1236#issuecomment -254059927 证实了我关于SIGTERM的假设只是在请求处理完成后将工作人员设置为关闭(并且将工作人员设置为不接受任何新的连接)。

似乎我如何解释timeoutgraceful_timeout是错误的。 这两个时间段实际上都是指请求处理开始时的时间。 因此,默认情况下,因为这两个设置都设置为30秒,所以没有启用优雅重启。 如果我执行--graceful-timeout 15 --timeout 30之类的操作,这应该意味着在 15 秒时启动正常重启,如果在此之前请求未完成,则工作人员会在 30 秒时被强制终止。

但是,似乎如果在graceful_timeouttimeout之间返回响应,那么工作人员毕竟没有重新启动? 不应该吗?

我通过app.py测试:

import time
from flask import Flask

app = Flask(__name__)

@app.route('/foo')
def foo():
    time.sleep(3)
    return 'ok'

然后:

12:51 $ gunicorn app:app --timeout 5 --graceful-timeout 1
[2017-04-03 12:51:37 +0300] [356] [INFO] Starting gunicorn 19.6.0
[2017-04-03 12:51:37 +0300] [356] [INFO] Listening at: http://127.0.0.1:8000 (356)
[2017-04-03 12:51:37 +0300] [356] [INFO] Using worker: sync
[2017-04-03 12:51:37 +0300] [359] [INFO] Booting worker with pid: 359

然后我发送curl localhost:8000/foo ,它会在 3 秒后返回。 但是在 gunicorn 中什么也没有发生 - 我没有看到优雅重启的痕迹正在启动或发生?

似乎在timeout上,抛出SystemExit(1,) ,中止了 Flask 中的当前请求处理。 什么代码或信号产生它,不能说。

这个异常通过 Flask 堆栈抛出,任何teardown_request处理程序都会捕获它。 有足够的时间来记录一些东西,但是如果你在处理程序中执行time.sleep(1)或其他一些耗时的事情,它就会被静默地杀死。 就好像在进程实际被强制终止之前有 100-200 毫秒的时间,我想知道这个延迟是什么。 这不是优雅的超时,该设置对延迟没有影响。 我希望该进程只是被强制终止,而不是看到SystemExit被抛出堆栈,但无论如何都可能在半空中终止该进程。

事实上,我没有看到graceful_timeout做任何事情——也许同步工作者不支持它,或者它不能“独立”工作(或与timeout一起)——只有当您手动发送SIGTERM时?

同样奇怪的是https://github.com/benoitc/gunicorn/blob/master/gunicorn/arbiter.py#L392根本不检查graceful标志。 我猜https://github.com/benoitc/gunicorn/blob/master/gunicorn/arbiter.py#L390确保self.WORKERS为空,因此在进行非优雅停止时不会等待优雅超时。

@benoitc @tilgovi愿意帮忙吗? 希望我上面的文章有意义...

@tuco86 graceful timeout仅在您退出仲裁器、升级它(USR2)、向仲裁器发送 HUP 信号或向工作器发送 QUIT 信号时可用。 即只在动作正常时使用

超时是为了防止忙碌的工作人员阻止其他请求。 如果他们没有在少于timeout的时间内通知仲裁者,则工作人员将简单地退出并关闭与客户端的连接。

嗯,好吧。 timeout在以下情况下是否有任何影响:

退出仲裁器,升级它(USR2),向仲裁器发送 HUP 信号或向工作器发送 QUIT 信号

我的意思是,如果工人没有在graceful_timeout中关闭怎么办 - 在那之后timeout会启动并且工人被强行杀死,还是留给用户调用SIGQUIT以防他们没有优雅地死去?

给工人的 QUIT 信号

我假设您的意思是TERM (因为QUIT被记录为 master 和 worker 的 _quick shutdown_ )?

如果工作人员在正常时间内没有关闭,它将被杀死而没有任何其他延迟。

当然。 感谢您澄清事情!

@benoitc在这张旧票的上下文中询问 - timeout文档中的最后一句话实际上是什么意思?

一般设置为三十秒。 如果您确定对同步工作人员的影响,请仅将其设置得更高。 对于非同步工作人员,这仅意味着工作进程仍在通信,并且与处理单个请求所需的时间长度无关。

不是以英语为母语的人,我很难理解这一点。 这是否意味着非同步工作人员不支持timeout (因为我似乎正在目睹:我正在使用gthread工作人员并且超时没有启动并杀死太慢的请求)?

@tuukkamustonen --timeout并不意味着请求超时。 这意味着对工人进行活性检查。 对于同步工作人员,这起到了请求超时的作用,因为工作人员除了处理请求之外不能做任何事情。 异步工作人员即使在处理长时间运行的请求时也会心跳,因此除非工作人员阻塞/冻结它不会被杀死。

如果其他人对此感到困惑,也许我们更改名称是个好主意。

@tilgovi timeout很好,尽管像worker_timeout这样的东西可能更具描述性。 我刚开始感到困惑,因为timeoutgraceful_timeout在文档中被声明为彼此相邻,所以我的大脑假设它们紧密相连,而实际上并非如此。

对于同步工作人员,这起到了请求超时的作用,因为工作人员除了处理请求之外不能做任何事情。 异步工作人员即使在处理长时间运行的请求时也会心跳,因此除非工作人员阻塞/冻结它不会被杀死。

你能举个例子说明timeout何时与非同步工作者一起工作吗? 真的是不应该发生的事情吗?也许只有当存在导致工作人员阻塞/冻结的错误时?

这是正确的。 依赖于事件循环核心的异步工作者可能会执行一个 CPU 密集型过程,该过程不会在超时内产生。

换句话说,不仅仅是一个错误。 虽然,有时它可能表示一个错误,例如当异步协议更合适时调用阻塞 I/O 函数。

陷入 CPU 密集型任务就是一个很好的例子,谢谢。

在异步代码中调用阻塞 I/O 也是一种方法,但我不确定它如何应用于此上下文 - 我正在运行带有阻塞代码的传统 Flask 应用程序,但使用异步工作程序运行它( gthread ) 没有任何猴子补丁。 它工作正常。 我知道这不再真的在这张票的上下文中了,但是像这样混合和匹配异步/同步代码不会导致问题吗?

另外,心跳间隔是多少? 与非同步工作人员一起使用timeout的合理值是多少?

gthread 工作线程不是异步的,但它确实有一个用于心跳的主线程,因此它也不会超时。 对于该工作人员,您可能不会看到超时,除非该工作人员非常过载,或者更有可能的是,您调用了不释放 GIL 的 C 扩展模块。

除非您开始看到工作人员超时,否则您可能不必更改超时。

好吧。 还有一件事:

gthread 工作线程不是异步的

gthread worker 不是异步的,但在http://docs.gunicorn.org/en/stable/design.html#asyncio -workers 中被列为“AsyncIO”worker,这可能有点令人困惑。 除此之外,使用“线程”不需要 asyncio,因此也会向读者提出问题。 只是从一个天真的用户的角度这么说,我相信这在技术上都是有根据的。

简而言之, gthread worker 是用asyncio lib 实现的,但它会产生线程来处理同步代码。 如果错了,请纠正我。

很高兴你问!

线程工作者不使用 asyncio 并且不从基本异步工作者类继承。

我们应该澄清文件。 我认为它可能已被列为异步,因为工作超时是同时处理的,使其在处理长请求和并发请求的能力方面更像异步工作而不是同步工作。

澄清文档并使其更准确地描述所有工人会很棒。

是的,gthreads 工作者不应该在 asyncio 工作者中列出。 也许有一个描述每个工人设计的部分会更好?

重新打开它,以便我们可以将其作为工作进行跟踪,以澄清有关工作人员类型和超时的部分。

@tilgovi

--timeout 并不意味着请求超时。 这意味着对工人进行活性检查。 对于同步工作人员,这起到了请求超时的作用,因为工作人员除了处理请求之外不能做任何事情。 异步工作人员即使在处理长时间运行的请求时也会心跳,因此除非工作人员阻塞/冻结它不会被杀死。

异步工作人员是否有可用的请求超时选项? 换句话说,如何让仲裁者杀死在指定时间内没有处理请求的工作人员?

@aschatten不幸的是,没有。 另见#1658。

杀死在指定时间内没有处理请求的工作人员

由于一个工作人员可能同时处理多个请求,因此因为一个请求超时而杀死整个工作人员听起来非常极端。 这不会导致所有其他请求都被白白杀死吗?

我记得 uWSGI 计划在 2.1 左右引入基于线程的杀戮,尽管这可能仅适用于同步/线程工作者(我对此的回忆很模糊)。

由于一个工作人员可能同时处理多个请求,因此因为一个请求超时而杀死整个工作人员听起来非常极端。 这不会导致所有其他请求都被白白杀死吗?

该方法可以与max_request相同,其中每种工作类型都有一个单独的实现。

我们正在本周发布一个版本,届时可能_是时候为 R20 进行分支了,我们计划在其中解决一些主要问题。 这可能是将当前超时设置为每种工作类型的适当请求超时的正确时间。

在这里评论而不是提交一个单独的问题,因为我试图了解超时应该如何工作并且我不确定这是否是一个错误。

我看到的 IMO 意外行为是这样的:

每个 max-requests 的请求(将重新启动 worker 之后的请求)都会超时,而其他请求则成功完成。 在下面的示例中,执行了 4 个请求,请求 1、2 和 4 成功,而请求 3 失败。

相关配置:

  • 线程工作者
  • 服务请求比超时时间长
  • max-requests 非零
import time

def app(environ, start_response):
    start_response('200 OK', [('Content-type', 'text/plain; charset=utf-8')])
    time.sleep(5)
    return [b"Hello World\n"]

独角兽:

gunicorn --log-level debug -k gthread -t 4 --max-requests 3 "app:app"
...
[2018-02-08 10:11:59 +0200] [28592] [INFO] Starting gunicorn 19.7.1
[2018-02-08 10:11:59 +0200] [28592] [DEBUG] Arbiter booted
[2018-02-08 10:11:59 +0200] [28592] [INFO] Listening at: http://127.0.0.1:8000 (28592)
[2018-02-08 10:11:59 +0200] [28592] [INFO] Using worker: gthread
[2018-02-08 10:11:59 +0200] [28595] [INFO] Booting worker with pid: 28595
[2018-02-08 10:11:59 +0200] [28592] [DEBUG] 1 workers
[2018-02-08 10:12:06 +0200] [28595] [DEBUG] GET /
[2018-02-08 10:12:11 +0200] [28595] [DEBUG] Closing connection.
[2018-02-08 10:12:15 +0200] [28595] [DEBUG] GET /
[2018-02-08 10:12:20 +0200] [28595] [DEBUG] Closing connection.
[2018-02-08 10:12:23 +0200] [28595] [DEBUG] GET /
[2018-02-08 10:12:23 +0200] [28595] [INFO] Autorestarting worker after current request.
[2018-02-08 10:12:27 +0200] [28592] [CRITICAL] WORKER TIMEOUT (pid:28595)
[2018-02-08 10:12:27 +0200] [28595] [INFO] Worker exiting (pid: 28595)
[2018-02-08 10:12:28 +0200] [28595] [DEBUG] Closing connection.
[2018-02-08 10:12:28 +0200] [28599] [INFO] Booting worker with pid: 28599
[2018-02-08 10:12:32 +0200] [28599] [DEBUG] GET /
[2018-02-08 10:12:37 +0200] [28599] [DEBUG] Closing connection.
^C[2018-02-08 10:12:39 +0200] [28592] [INFO] Handling signal: int

客户:

[salonen<strong i="19">@mac</strong> ~]$ curl http://127.0.0.1:8000
Hello World
[salonen<strong i="20">@mac</strong> ~]$ curl http://127.0.0.1:8000
Hello World
[salonen<strong i="21">@mac</strong> ~]$ curl http://127.0.0.1:8000
curl: (52) Empty reply from server
[salonen<strong i="22">@mac</strong> ~]$ curl http://127.0.0.1:8000
Hello World

那里应该有什么计划? 我有以下几点:

  • [ ] 更新工人描述(如果仍然需要)
  • [ ] 记录检测死亡或阻塞工人的协议

它应该是 20.0 还是我们可以推迟它?

推迟。

嘿,所以这不会是 20.0 的一部分吗?

这可能是将当前超时设置为每种工作类型的适当请求超时的正确时间。

澄清。 @ lucas03目前还不清楚那里有什么请求超时。 如果您需要特定的东西,请打开一张票?

此页面是否有帮助?
0 / 5 - 0 等级