fix(gunicorn): preload 後重置 SQLAlchemy 連線池
Some checks failed
CD Pipeline / deploy (push) Failing after 9m32s
Some checks failed
CD Pipeline / deploy (push) Failing after 9m32s
This commit is contained in:
@@ -64,5 +64,5 @@ ENV FLASK_APP=app.py
|
||||
# 暴露端口(容器內 app 綁 80,docker-compose / k8s 對外映射依環境而定)
|
||||
EXPOSE 80
|
||||
|
||||
# 啟動應用(production 用 gunicorn,4 workers + 300s timeout + 啟用 access/error log)
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:80", "--workers", "4", "--timeout", "300", "--access-logfile", "-", "--error-logfile", "-", "--preload", "app:app"]
|
||||
# 啟動應用(production 用 gunicorn;preload + post_fork DB pool reset 見 gunicorn.conf.py)
|
||||
CMD ["gunicorn", "--config", "gunicorn.conf.py", "app:app"]
|
||||
|
||||
@@ -47,8 +47,8 @@ services:
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
ports:
|
||||
- "127.0.0.1:5003:80" # 僅本地連線,透過 Nginx 反向代理(nginx 反代 5003)
|
||||
# 強制使用 gunicorn 綁定 port 80 (覆蓋 Dockerfile CMD)
|
||||
command: ["gunicorn", "--bind", "0.0.0.0:80", "--workers", "4", "--timeout", "300", "--access-logfile", "-", "--error-logfile", "-", "--preload", "app:app"]
|
||||
# 強制使用 gunicorn 綁定 port 80 (覆蓋 Dockerfile CMD);preload + post_fork DB pool reset 見 gunicorn.conf.py
|
||||
command: ["gunicorn", "--config", "gunicorn.conf.py", "app:app"]
|
||||
volumes:
|
||||
# 持久化數據
|
||||
- ./data:/app/data
|
||||
|
||||
69
gunicorn.conf.py
Normal file
69
gunicorn.conf.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Gunicorn runtime config.
|
||||
|
||||
`preload_app` imports Flask before worker fork. Any SQLAlchemy engines created
|
||||
during import must drop inherited connection pools in each child worker.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from sqlalchemy.engine import Engine
|
||||
|
||||
|
||||
bind = "0.0.0.0:80"
|
||||
workers = int(os.getenv("WEB_CONCURRENCY", "4"))
|
||||
timeout = int(os.getenv("GUNICORN_TIMEOUT", "300"))
|
||||
accesslog = "-"
|
||||
errorlog = "-"
|
||||
preload_app = True
|
||||
|
||||
|
||||
def _dispose_engine(engine, label, server):
|
||||
try:
|
||||
try:
|
||||
engine.dispose(close=False)
|
||||
except TypeError:
|
||||
engine.dispose()
|
||||
server.log.info("Disposed inherited SQLAlchemy engine after fork: %s", label)
|
||||
return True
|
||||
except Exception:
|
||||
server.log.exception("Failed disposing inherited SQLAlchemy engine: %s", label)
|
||||
return False
|
||||
|
||||
|
||||
def post_fork(server, worker):
|
||||
"""Reset DB pools inherited from the preloaded master process.
|
||||
|
||||
SQLAlchemy engines are safe to keep as objects after fork, but their pools
|
||||
must be replaced so workers do not share PostgreSQL TCP sockets.
|
||||
"""
|
||||
disposed_ids = set()
|
||||
disposed_count = 0
|
||||
prefixes = ("app", "database.", "routes.", "services.")
|
||||
|
||||
for module_name, module in list(sys.modules.items()):
|
||||
if module is None or not module_name.startswith(prefixes):
|
||||
continue
|
||||
|
||||
for attr_name, value in vars(module).items():
|
||||
candidates = []
|
||||
if isinstance(value, Engine):
|
||||
candidates.append((value, f"{module_name}.{attr_name}"))
|
||||
else:
|
||||
engine = getattr(value, "engine", None)
|
||||
if isinstance(engine, Engine):
|
||||
candidates.append((engine, f"{module_name}.{attr_name}.engine"))
|
||||
|
||||
for engine, label in candidates:
|
||||
engine_id = id(engine)
|
||||
if engine_id in disposed_ids:
|
||||
continue
|
||||
disposed_ids.add(engine_id)
|
||||
if _dispose_engine(engine, label, server):
|
||||
disposed_count += 1
|
||||
|
||||
server.log.info(
|
||||
"Worker %s reset %s SQLAlchemy engine pool(s) after preload fork",
|
||||
worker.pid,
|
||||
disposed_count,
|
||||
)
|
||||
Reference in New Issue
Block a user