A Graceful End - Python Flask/Gunicorn后端退出钩子

本文介绍如何使用Gunicorn的钩子机制实现服务的优雅启动与停止,包括信号处理、自定义钩子函数及配置文件的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基本想法

可以把注销监听、注销服务写在flask exit hook里

Flask简单场景

  1. Flask没有app.stop()方法

正常退出

  1. Python有内置的atexit库

“The atexit module defines functions to register and unregister cleanup functions. Functions thus registered are automatically executed upon normal interpreter termination. atexit runs these functions in the reverse order in which they were registered; if you register A, B, and C, at interpreter termination time, they will be run in the order C, B, A.”

指令退出CTRL+C/kill

当使用ctrl+C方式关闭服务器时可以用另一个库叫signal

题外话:可以通过注册signal.signal的形式处理一些事件,但是signal.STOP, signal.SIGKILL是没有办法拦截的(这种杀死是在内核级别实现的)。

# wsgi.py
import signal

def exit_hook(signalNumber, frame):
    print("receiving", signalNumber)
    
if __name__ == "__main__":
    signal.signal(signal.SIGINT, exit_hook)
    app.run()
    
# 可以在结束时输出
# receiving 2

# 但是如果注册signal.SIGKILL/signal.SIGSTOP则代码会报错

回答原文如下:

There is no way to handle SIGKILL

The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.

If you're looking to handle system shutdown gracefully, you should be handling SIGTERM or a startup/shutdown script such as an upstart job or a init.d script.


如何不通过Ctrl+C来关闭Flask

from flask import request
def shutdown_server():
    func = request.environ.get('werkzeug.server.shutdown')
    if func is None:
        raise RuntimeError('Not running with the Werkzeug Server')
    func()
    
@app.get('/shutdown')def shutdown():
    shutdown_server()
    return 'Server shutting down...'
    
    
---------
from multiprocessing import Process

server = Process(target=app.run)
server.start()
# ...
server.terminate()
server.join()

但其实第一种方法已经过时了....

参考文章:How To Create Exit Handlers for Your Python App


Gunicorn

Flask的话直接用这个可以,那Gunicorn等多线程、多进程场景该怎么办?

Gunicorn的场景下,下面都不适用

参考:how-to-configure-hooks-in-python-gunicorn

总结:gunicorn场景,可以使用configs.py文件,将configs全部写在里面,包括各种hook(Hook也算是config的一部分),但是官方文档关于hook的部份没有给出任何具体的解释,需要去查源代码。

样例:

# gunicorn_hooks_config.py
def on_starting(server):
    """
    Do something on server start
    """
    print("Server has started")


def on_reload(server):
    """
     Do something on reload
    """
    print("Server has reloaded")


def post_worker_init(worker):
    """
    Do something on worker initialization
    """
    print("Worker has been initialized. Worker Process id –>", worker.pid)

运行文件:

# wsgi.py
from app import app

if __name__ == '__main__':
    app.run()

运行

gunicorn -c gunicorn_hooks_config.py wsgi:app

但是官方关于这一块钩子的文档实在太简单了,啥都没。没办法,想知道怎么写函数,必须从源码开始查:

入口Gunicorn代码如下:

# gunicorn

#!/Users/xiao/opt/anaconda3/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from gunicorn.app.wsgiapp import run
if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
    sys.exit(run())
# wsgi.app.wsgiapp.py
def run():
    """\
    The ``gunicorn`` command line runner for launching Gunicorn with
    generic WSGI applications.
    """
    from gunicorn.app.wsgiapp import WSGIApplication
    WSGIApplication("%(prog)s [OPTIONS] [APP_MODULE]").run()
    
if __name__ == '__main__':
    run()

关于加载配置文件

# app.base.py

...

class Application(BaseApplication):
    ...
    
    def get_config_from_filename(self, filename):

        if not os.path.exists(filename):
            raise RuntimeError("%r doesn't exist" % filename)

        ext = os.path.splitext(filename)[1]

        try:
            module_name = '__config__'
            if ext in [".py", ".pyc"]:
                spec = importlib.util.spec_from_file_location(module_name, filename)
            else:
                msg = "configuration file should have a valid Python extension.\n"
                util.warn(msg)
                loader_ = importlib.machinery.SourceFileLoader(module_name, filename)
                spec = importlib.util.spec_from_file_location(module_name, filename, loader=loader_)
            mod = importlib.util.module_from_spec(spec)
            sys.modules[module_name] = mod
            spec.loader.exec_module(mod)
        except Exception:
            print("Failed to read config file: %s" % filename, file=sys.stderr)
            traceback.print_exc()
            sys.stderr.flush()
            sys.exit(1)

        return vars(mod)

在Arbiter.py中会有Arbiter类来负责生成和杀死worker,可以说Arbiter是Gunicorn内部的Worker大管家。但是仍然,worker类是直接调取的self.cfg.worker_class

Gunicorn这个框架里使用了paste.deploy.loadapp,因此这个也很重要。

具体流程:

wsgiapplication运行 -> 检测到configs.py -> importlib.util.spec_from_file_location 以__config__的module返回 -> 该module成为app.cfg -> 传递给Arbiter的cfg -> Worker类注册signal_handler -> 方法里调用self.cfg.worker_abort(self) -> 即 同名方法

# gunicorn/workers/base.py
class Worker(object):
    def init_signals(self):
        # reset signaling
        for s in self.SIGNALS:
            signal.signal(s, signal.SIG_DFL)
        # init new signaling
        signal.signal(signal.SIGQUIT, self.handle_quit)
        signal.signal(signal.SIGTERM, self.handle_exit)
        signal.signal(signal.SIGINT, self.handle_quit)
        signal.signal(signal.SIGWINCH, self.handle_winch)
        signal.signal(signal.SIGUSR1, self.handle_usr1)
        signal.signal(signal.SIGABRT, self.handle_abort)

        # Don't let SIGTERM and SIGUSR1 disturb active requests
        # by interrupting system calls
        signal.siginterrupt(signal.SIGTERM, False)
        signal.siginterrupt(signal.SIGUSR1, False)

        if hasattr(signal, 'set_wakeup_fd'):
            signal.set_wakeup_fd(self.PIPE[1])
    def handle_abort(self, sig, frame):
        self.alive = False
        self.cfg.worker_abort(self)
        sys.exit(1)

也就是说,configs.py里写的def worker_abort(worker)方法,必须是接收一个Worker类参数的方法。

那server呢?有了原名称调用这个思路,去找就容易了,直接搜on_exit,发现server即Arbiter类。调用场景如下348行:

class Arbiter(Object):
    def halt(self, reason=None, exit_status=0):
        """ halt arbiter """
        self.stop()
        self.log.info("Shutting down: %s", self.master_name)
        if reason is not None:
            self.log.info("Reason: %s", reason)
        if self.pidfile is not None:
            self.pidfile.unlink()
        self.cfg.on_exit(self)
        sys.exit(exit_status)

总结:

这些文档正确的写法应该是

# 注意此处lib的拼写错误是代码中的,不能自己改成init
def worker_int(worker: gunicorn.workers.Worker) -> None:
    ...

# 可以有返回值,但是返回值不会被调用
    
def on_exit(server: gunicorn.Arbiter) -> None:
    pass

似乎在Gunicorn中,Arbiter名称被隐藏,重命名为SERVER了(不理解,迷惑行为)

 

 

### 如何优雅地退出正在运行的 Python 后端程序 要实现 Python 后端程序的优雅退出,通常可以通过捕获信号或监听特定事件来触发关闭逻辑。以下是几种常见的方法: #### 方法一:通过捕获系统信号 在 Unix 或 Linux 系统上,可以利用 `signal` 模块捕获终止信号(如 SIGINT 和 SIGTERM)。当接收到这些信号时,执行清理操作并安全退出。 ```python import signal import sys def graceful_exit(signum, frame): print("Received termination signal. Shutting down gracefully...") # 执行清理工作,例如保存状态、释放资源等 cleanup() sys.exit(0) def cleanup(): print("Performing cleanup operations...") if __name__ == "__main__": signal.signal(signal.SIGINT, graceful_exit) # 处理 Ctrl+C signal.signal(signal.SIGTERM, graceful_exit) # 处理 kill 命令 try: while True: pass # 主循环模拟后端服务运行 except Exception as e: print(f"An error occurred: {e}") ``` 这种方法适用于大多数基于脚本的服务应用[^1]。 #### 方法二:通过外部请求停止服务器 如果使用的是 Web 框架(如 Flask 或 FastAPI),可以在框架中定义一个特殊的 API 路由用于接收停止指令。此路由会触发系统的关闭过程。 ```python from flask import Flask import os import threading app = Flask(__name__) shutdown_event = threading.Event() @app.route('/stop', methods=['POST']) def stop_server(): shutdown_event.set() # 设置事件标志位 func = request.environ.get('werkzeug.server.shutdown') if func is None: raise RuntimeError('Not running with the Werkzeug Server') func() # 关闭开发服务器 return 'Server shutting down...' if __name__ == '__main__': app.run(port=5000) # 如果是生产环境下的 WSGI 容器,则需要额外处理 if not shutdown_event.is_set(): shutdown_event.wait() # 阻塞直到收到停止信号 cleanup() ``` 上述代码展示了如何创建 `/stop` 接口以便于远程控制服务停机[^2]。 #### 方法三:定期轮询退出条件 对于某些无法直接响应信号的应用场景,可设计一种机制让主线程周期性检查某个共享变量或者文件是否存在作为退出依据。 ```python import time import os SHUTDOWN_FILE_PATH = "/tmp/shutdown_flag" def check_shutdown_condition(): return os.path.exists(SHUTDOWN_FILE_PATH) while not check_shutdown_condition(): do_work() # 替代为实际业务逻辑函数调用 time.sleep(1) # 减少 CPU 占用率 cleanup() print("Application has been terminated.") ``` 这种方式简单直观,在分布式环境中尤为有用[^3]。 无论采用哪种方式,请务必注意以下几点: - **数据一致性**:确保所有未完成的任务都已妥善处理完毕。 - **资源回收**:及时关闭数据库连接池、网络套接字以及其他占用型组件。 - **日志记录**:记录下整个退出流程中的重要信息方便后续排查问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值