优雅停机
优雅停机(Graceful Shutdown)是一种良好的开发实践,有助于确保服务的可用性、数据完整性和用户体验。 通过适当处理关闭信号、请求处理和资源释放,可以实现平滑的服务停机过程,减少潜在的问题和不可预测的行为。
具体说,实现优雅停机可以带来如下好处:
- 数据完整性:通过优雅停机,可以确保正在处理的请求能够完成,以避免数据丢失或事务未完成。
- 用户体验:通过允许正在处理的请求顺利完成,用户不会中断正在进行的操作,提供更好的用户体验。
- 资源管理:在优雅停机期间,可以正确地释放和关闭与服务相关的资源,例如数据库连接、文件句柄等,以避免资源泄漏或浪费。
- 避免服务中断:优雅停机可以避免服务突然中断,并允许正在进行的请求在关闭之前得到响应。这对于对服务可用性要求很高的应用程序尤为重要。
- 协调其他服务:优雅停机还可以提供通知或广播给其他相关服务,以便它们能够适应服务的停机状态,并采取相应的措施,例如重新路由请求、切换到备用服务等。
然而,优雅停机并没有统一的实现方案,需要用户根据自己的应用程序和需求经行调整和优化。
一般来说,在应用收到停机信号( SIGTERM
或 SIGINT
)应分两步实现优雅停机:
- 停止接收新请求
- 在一定时间内处理未完成请求,释放连接、文件句柄等,如果超时后仍有在处理的任务,需要在保证数据完整性和安全性的前提下强制停机
以 tornado 为例演示:
需注意的是,在网上不少文档中都提到在71行处用
server.stop()
停止接受新请求,但经测试发现,该方法并不是停止接受新请求。
测试场景:用浏览器或者 Postman 调用一个接口,然后执行server.stop()
,然后再调用该接口,大概率仍有响应
原因:server.stop()
只是停止监听端口,停止接受新TCP连接**。如果复用之前已经创建的连接创建 http 请求,则仍可正常使用。
结论:server.stop()
并不适合用于Graceful shutdown,这可以从 tornado 开发者的回答 (opens new window) 中得到印证。
import time
import threading
import signal, asyncio
from functools import partial
shutting_down = False
_request_counter = 0
_request_counter_lock = threading.Lock()
def stop_accepting_new_request():
global shutting_down
shutting_down = True
class RequestCounterHandler(object):
def prepare(self) -> None:
if shutting_down:
self.send_error()
with _request_counter_lock:
global _request_counter
_request_counter += 1
def on_finish(self):
with _request_counter_lock:
global _request_counter
_request_counter -= 1
class CustomFallbackHandler(RequestCounterHandler, tornado.web.FallbackHandler):
def prepare(self) -> None:
RequestCounterHandler.prepare(self)
tornado.web.FallbackHandler.prepare(self)
GRACEFUL_SHUTDOWN_TIMEOUT = 30
if __name__ == '__main__':
import tornado.ioloop
import tornado.web
loop = tornado.ioloop.IOLoop.current()
def shutdown_handler(server, signum, frame):
io_loop = tornado.ioloop.IOLoop.current()
def unfinished_tasks():
return [1]
def stop_job_forcely(task):
print(f'task {task} has been finished forcely.')
def stop_backend_tasks(server, deadline):
now = time.time()
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task() and not t.done()]
if now < deadline and len(tasks) + len(unfinished_tasks()) + _request_counter > 0:
print(f"Awaiting pending backend tasks to finish, contains {len(tasks)} asyncio tasks, {_request_counter} pending requests and {len(unfinished_tasks())} tasks.")
io_loop.add_timeout(now + 1, stop_backend_tasks, server, deadline)
else:
# still has unfinished tasks, cancel them forcely.
_tasks = unfinished_tasks()
if _tasks:
print('Still has unfinished tasks after timeout and will terminate them forcely.')
for _task in unfinished_tasks():
stop_job_forcely(_task)
io_loop.stop()
print('Graceful shutdown complete.')
def shutdown():
print('Start graceful shutdown.')
try:
# stop accepting new requests
stop_accepting_new_request()
stop_backend_tasks(server, time.time() + GRACEFUL_SHUTDOWN_TIMEOUT)
except Exception as e:
print(e)
print(f'Caught signal: {signum}')
global shutting_down
if not shutting_down:
shutting_down = True
io_loop.add_callback_from_signal(shutdown)
tornado_app = tornado.web.Application([
(r'.*', CustomFallbackHandler)
], websocket_max_message_size=1024)
server = tornado_app.listen(port=8080)
signal.signal(signal.SIGTERM, partial(shutdown_handler, server))
signal.signal(signal.SIGINT, partial(shutdown_handler, server))
loop.start()
上次更新: 2023/12/08, 06:34:37