【问题标题】:How do I handle dyno restarts when using Django?使用 Django 时如何处理测功机重启?
【发布时间】:2015-09-07 07:02:33
【问题描述】:

我想根据他们的描述在 Heroku 上处理 dyno 重启 here:

在此期间,他们应该停止接受新的请求或作业,并尝试完成当前的请求,或将作业放回队列以供其他工作进程处理。

从表面上看,当python接收到SIGTERM并调用信号处理程序时(根据signal.signal),当前正在运行的线程停止,因此请求在运行过程中停止。

如何同时满足这两个要求? (停止接受新请求 + 完成当前请求)

【问题讨论】:

  • 我从来没有听说过需要什么特别的东西。由于 Django 的前 BDFL 为 Heroku 工作,你会认为如果是这样的话,它会被记录在案。
  • @DanielRoseman 用链接更新了问题
  • 我猜为了满足您的应用程序的干净关闭,用于服务请求的底层并发模型必须支持对来自SIGTERM/SIGINT的关闭请求进行排队,完成当前请求正在进行并终止。至少对于异步 Web 框架,我会这样做。

标签: python django heroku


【解决方案1】:

编辑:添加了简化的示例代码,更好地解释了正在进行的请求/终止,并添加了来自 CrazyPython 的要点。

从表面上看,你有 4 个问题需要解决。我将依次介绍它们,然后提供一些有助于澄清的示例代码:

处理 SIGTERM

这很简单。您只需要设置一个信号处理程序来注意您需要关闭。 PMOTW 有一组很好的例子来说明如何捕捉信号。您可以使用此代码的变体来捕获 SIGTERM 并设置一个表示您正在关闭的全局标志。

拒绝新请求

Django middleware 提供了一种将任何 HTTP 请求挂钩到您的应用程序的简洁方法。您可以创建一个简单的 process_request() 钩子,如果设置了全局标志(从上面),则返回错误页面。

完成现有请求

停止任何新请求后,您现在必须完成当前请求。虽然你现在可能不相信,但这意味着你什么也不做,让程序在 SIGTERM 之后照常运行。让我扩展一下……

与 heroku 的合同是您必须在 SIGTERM 的 10 秒内完成,否则无论如何它都会发送 SIGKILL。这意味着您无能为力(作为一个表现良好的应用程序)来确保所有请求始终完成。考虑两种情况:

  1. 您的应用程序在 10 秒内处理所有现有请求。在这种情况下,只需让您的程序运行即可完成请求。不需要特殊代码来运行请求 - 所有线程/进程都已在执行您需要的操作!
  2. 您的应用程序对某些请求的处理时间超过 10 秒。在这种情况下,您无能为力 - 在长请求完成之前,heroku 将用终极力量终止它。如果您认为可以忽略 SIGKILL,请考虑其他方式...这是不允许的 - 请参阅 signals documentation。

因此,在这两种情况下,解决方案都是让您的程序继续运行,以在终止之前让尽可能多的当前请求完成。

终止您的应用程序

最简单的做法可能是等待 10 秒后从 heroku 发出 SIGKILL。这并不优雅,但应该没问题,因为您拒绝了任何新请求。

如果这还不够好,您需要跟踪未完成的请求并使用它来决定何时可以关闭您的应用程序。关闭应用程序的确切方法将取决于托管它的内容,因此我无法在那里为您提供确切的指导。不过,希望示例代码能够为您提供足够的指导。

示例代码

从 PMOTW 中的信号处理程序示例开始,我加强了代码以添加多个线程处理请求和一个终止管理器来捕获信号并允许应用程序正常关闭。您应该可以在 Python2.7 中运行它,然后尝试终止该进程。

在此示例的基础上,CrazyPython 创建了 gist 以在 django 中提供具体实现。

import signal
import os
import time
import threading
import random


class TerminationManager(object):

    def __init__(self):
        self._running = True
        self._requests = 0
        self._lock = threading.Lock()
        signal.signal(signal.SIGTERM, self._start_shutdown)

    def _start_shutdown(self, signum, stack):
        print 'Received:', signum
        self._running = False

    def start_request(self):
        with self._lock:
            self._requests += 1

    def stop_request(self):
        with self._lock:
            self._requests -= 1

    def is_running(self):
        return self._running or self._requests > 0

    def running_requests(self):
        return self._requests


class DummyWorker(threading.Thread):

    def __init__(self, app_manager):
        super(DummyWorker, self).__init__()
        self._manager = app_manager

    def run(self):
        while self._manager.is_running():
            # Emulate random work and delay between requests.
            if random.random() > 0.9:
                self._manager.start_request()
                time.sleep(random.randint(1, 3))
                self._manager.stop_request()
            else:
                time.sleep(1)
        print "Stopping worker"


manager = TerminationManager()
print 'My PID is:', os.getpid()

for _ in xrange(10):
    t = DummyWorker(manager)
    t.start()

while manager.is_running():
    print 'Waiting with {} running requests'.format(manager.running_requests())
    time.sleep(5)

print 'All done!'

【讨论】:

  • 我已经了解了 PMOTW 的大部分内容,但我无法理解如何应用它,因为它会暂停处理请求。那么剩下的一个问题:我如何完成我当前的请求?
  • 我可以在收到 SIGKILL 并使用 sys.exit() 之前等待操作系统警报 1 秒
  • 这个答案在改进之前没有资格获得赏金,尽管非常接近获得赏金。我和 OP 有同样的看法,改进它,我可能会奖励它。
  • @Peter 这非常接近被接受;只是澄清我如何才能完成我当前的请求。我认为 CrazyPython 也会给予赏金!
  • 刚刚又试了一次。如您所见,解决方案就是让程序继续运行。希望答案现在已经清楚了。如果不是,请解释为什么你认为让它继续下去是正确的答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-02-26
  • 1970-01-01
  • 2013-02-08
  • 1970-01-01
  • 2017-08-02
  • 2013-02-21
  • 2014-12-27
相关资源
最近更新 更多