如何在Django请求处理线程中收到SIGTERM信号时执行自定义代码
问题原因分析
你的猜测完全正确,Python的信号处理逻辑是所有信号只会被主线程接收和处理,你在信号处理器里调用sys.exit()只会在主线程抛出SystemExit异常,根本不会传递到处理请求的子线程,子线程的异常捕获逻辑自然触发不了。而且Django开发服务器默认采用多线程模型处理请求,主线程退出后子线程如果是非守护模式还会继续运行,守护模式会被直接强制终止,都走不到你写的异常捕获分支。
可行解决方案
方案1:全局终止标志轮询(最稳定,推荐)
这个方案适配你现有循环逻辑的场景,不需要改动太多代码,稳定性最高。
代码修改步骤:
- 新增全局终止标志,信号处理器收到SIGTERM后只修改标志,不直接退出
import signal import sys from django.apps import AppConfig import logging logger = logging.getLogger(__name__) # 全局终止标志 g_terminate = False def signal_handler(signal_num, frame): global g_terminate g_terminate = True # 不需要直接调用sys.exit(),等待请求处理完后进程自然退出即可 # 如果有超时要求,可以在这里启动定时器,超时后再强制调用sys.exit() class TesterConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'tester' def ready(self): logger.info('starting ready') signal.signal(signal.SIGTERM, signal_handler)
- 请求处理逻辑中每次循环轮询标志位,检测到终止信号后直接执行状态保存逻辑
import logging import time from django.http import HttpResponse from tester.apps import g_terminate # 导入全局终止标志 logger = logging.getLogger(__name__) def handler(request): try: logger.info('start') while True: # 每次循环都检查终止标志 if g_terminate: logger.info('收到终止信号,开始保存当前请求状态') # 此处编写你的状态保存逻辑 break time.sleep(1) except Exception as e: logger.info(f'exception: {e}') return HttpResponse('hallo')
方案2:主动向子线程注入异常(适用于无法轮询的场景)
如果你的长耗时逻辑不是可轮询的循环结构,可以用Python C API主动给所有活跃的请求子线程抛出异常,子线程捕获后处理状态保存。
注意:如果子线程正在执行C语言实现的扩展代码,异常不会立即触发,存在一定延迟。
示例代码参考:
# 线程ID存储,请求开始时存入,请求结束时删除 active_thread_ids = set() # 信号处理器中给所有活跃子线程抛异常 def signal_handler(signal_num, frame): import ctypes for tid in active_thread_ids: ctypes.pythonapi.PyThreadState_SetAsyncExc( ctypes.c_long(tid), ctypes.py_object(SystemExit) ) # 等待一段时间后再退出 time.sleep(2) sys.exit()
生产环境注意事项
- 不要用Django自带的runserver做生产部署,建议用Gunicorn/uWSGI这类生产级WSGI服务器
- 部署时需要调整服务器的超时配置,确保你的状态保存逻辑能在SIGKILL触发前执行完成,比如Gunicorn可以设置
--graceful-timeout参数调整优雅退出的等待时长
内容的提问来源于stack exchange,提问作者FlorianK
相关产品推荐
相关产品推荐

