使用 Django 时如何处理 dyno 重启?

noɥ*_*ɐɹƆ 5 python django heroku

我想根据这里的描述来处理 Heroku 上的 dyno 重启:

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

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

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

Pet*_*ain 2

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

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

处理 SIGTERM

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

拒绝新请求

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

完成现有请求

随着任何新请求的停止,您现在必须完成当前的请求。尽管您现在可能不相信,但这意味着您只需什么都不做,让程序在 SIGTERM 之后照常运行。让我扩展一下...

与 Heroku 的约定是,您必须在 SIGTERM 发生后 10 秒内完成,否则无论如何都会发送 SIGKILL。这意味着您无法(作为一个行为良好的应用程序)来确保所有请求始终完成。考虑两种情况:

  1. 您的应用程序会在 10 秒内处理所有现有请求。在这种情况下,只需让程序继续运行即可完成请求。不需要特殊的代码来运行请求 - 所有线程/进程都已经在执行您需要的操作!
  2. 您的应用程序对于某些请求需要花费超过 10 秒的时间。在这种情况下,您无能为力- 在长请求完成之前,heroku 将用最终力量终止它。如果您认为可以忽略 SIGKILL,请想想否则......这是不允许的 - 请参阅信号文档

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

终止您的申请

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

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

示例代码

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

在此示例的基础上,CrazyPython 创建了此要点,以在 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!'
Run Code Online (Sandbox Code Playgroud)