Python Web服务不能直接Ctrl+C终止,因SIGINT会中断正在处理的请求导致连接重置、事务未提交;平滑关闭需服务器支持(如uvicorn/gunicorn)且应用层配合,等待活跃请求完成后再退出。

Python Web服务为什么不能直接用 Ctrl+C 就完事
Ctrl+C 发送的是 SIGINT,它会立刻中断主线程,正在处理的 HTTP 请求可能被砍在中间——连接重置、响应不全、数据库事务没提交,用户看到 502 或白屏。这不是“关服务”,是“砸服务器”。平滑关闭的核心不是“快”,而是“等正在干活的请求干完再走”。
关键前提是:Web 服务器得支持优雅关闭(比如 uvicorn、gunicorn、原生 http.server 都行,但实现方式不同);应用层也得配合,比如别让长轮询或后台线程卡住主进程。
uvicorn 怎么加信号处理实现平滑退出
uvicorn 默认已内置对 SIGTERM 和 SIGINT 的响应,但只在它自己管理生命周期时生效。如果你用 uvicorn.run() 启动,直接按 Ctrl+C 就能触发 graceful shutdown——前提是没手动捕获并忽略信号,也没用 threading.Thread(daemon=True) 跑着没结束的后台任务。
- 别在代码里写
signal.signal(signal.SIGINT, lambda *a: sys.exit(0)),这会覆盖 uvicorn 自己的处理逻辑 - 如果用
uvicorn.Server类手动启动(比如嵌入到其他程序),必须显式调用server.shutdown(),且要确保在事件循环中执行(用asyncio.create_task(server.shutdown())) - 检查日志里有没有
Shutting down和Waiting for application shutdown—— 没这两句说明根本没进优雅流程
示例:正确嵌入式用法
立即学习“Python免费学习笔记(深入)”;
import asyncio
import uvicorn
<p>config = uvicorn.Config("app:app", host="0.0.0.0", port=8000)
server = uvicorn.Server(config)</p><p>async def main():
await server.serve()</p><p>if <strong>name</strong> == "<strong>main</strong>":
asyncio.run(main())gunicorn 如何配置才能真正平滑重启
gunicorn 的优雅关闭依赖两个参数:--graceful-timeout(等 worker 多久)和 --timeout(单个请求最长容忍时间)。很多人只设了 --timeout,结果 reload 时旧 worker 立刻被 kill,根本没机会收尾。
-
--graceful-timeout 30表示发SIGTERM后,最多等 30 秒让 worker 主动退出;超时就强制杀 -
--timeout 120是单个请求最大执行时间,和 graceful 无关,别混淆 - 用
gunicorn --reload开发时,它会用SIGHUP触发 reload,但默认不等待旧 worker 结束——必须加--preload或确保应用本身处理了SIGHUP - 生产环境推荐用
systemctl reload gunicorn.service,靠 systemd 的KillMode=mixed+TimeoutStopSec=45配合 gunicorn 的--graceful-timeout
自己写的 http.server 怎么加信号支持
Python 标准库的 http.server.HTTPServer 本身不响应信号,得手动加。核心是:收到 SIGTERM 后,停止接受新连接,等已有请求处理完,再退出。
- 用
server.shutdown()停止监听,但它不会等活跃请求——得自己维护一个请求计数器(比如用threading.Semaphore或原子变量) - 在每个请求处理前
semaphore.acquire(),结束后semaphore.release(),shutdown 时先 close socket,再semaphore.acquire(blocking=False)循环直到计数为 0 - 别用
ThreadingHTTPServer默认的 daemon thread,否则主线程一退,子线程直接消失;改用非 daemon + 显式 join
最小可行信号钩子:
import signal
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
<p>class Counter:
def <strong>init</strong>(self): self.n = 0
def inc(self): self.n += 1
def dec(self): self.n -= 1
def done(self): return self.n == 0</p><p>counter = Counter()
server = None</p><p>def handle_sigterm(signum, frame):
print("SIGTERM received, stopping server...")
server.shutdown() # 停新连接
while not counter.done():
time.sleep(0.1) # 等现有请求结束
exit(0)</p><p>signal.signal(signal.SIGTERM, handle_sigterm)平滑关闭最难的不是写几行信号代码,而是确认所有异步 I/O、数据库连接池、后台心跳线程都真的能响应 shutdown 通知——它们往往藏在第三方库深处,不看文档+不测压测,上线后第一个大流量就露馅。


















