modify: 集成 granian/uvicorn(gunicorn 保留),启动参数保持现状用函数转换

This commit is contained in:
2026-10-13 01:59:00 +08:00
parent 3a359fa485
commit 2df1e7266c
7 changed files with 1126 additions and 579 deletions
+21
View File
@@ -123,6 +123,27 @@ base = [
"fastapi.middleware.cors", "fastapi.middleware.cors",
"fastapi.middleware.gzip", "fastapi.middleware.gzip",
"uvicorn", "uvicorn",
"granian", # 备选服务器(APP_SERVER=granian,见 public/main.py),含 Rust 内核
"granian._compat",
"granian._futures",
"granian._granian",
"granian._imports",
"granian._internal",
"granian._loops",
"granian._signals",
"granian._types",
"granian.asgi",
"granian.constants",
"granian.errors",
"granian.http",
"granian.log",
"granian.net",
"granian.rsgi",
"granian.server",
"granian.server.common",
"granian.server.mp",
"granian.server.mt",
"granian.wsgi",
"chongming_cache", "chongming_cache",
"chongming_cache.backends", "chongming_cache.backends",
"chongming_cache.backends.sqlite_", "chongming_cache.backends.sqlite_",
+5 -2
View File
@@ -16,6 +16,8 @@
# #
# 可选:/etc/chongming.env 中写 APP_WORKERS=<N> 覆盖 worker 数(默认取 # 可选:/etc/chongming.env 中写 APP_WORKERS=<N> 覆盖 worker 数(默认取
# config.toml [production.server].workers,打包版通常为 4)。 # config.toml [production.server].workers,打包版通常为 4)。
# 可选:APP_SERVER=granian|uvicorn|gunicorn 切换 HTTP 服务器(默认 Linux 下
# gunicorn,Windows 下 uvicorn);三者的启动参数同取自 [production.server]。
[Unit] [Unit]
Description=Chongming Zenoh Gateway (FastAPI + Zenoh + MQTT) Description=Chongming Zenoh Gateway (FastAPI + Zenoh + MQTT)
@@ -29,7 +31,8 @@ StartLimitBurst=5
[Service] [Service]
Type=simple Type=simple
# 打包应用入口:Nuitka 编译产物,前台运行 gunicorn master(Linux 分支) # 打包应用入口:Nuitka 编译产物,前台运行所选服务器 master(默认 Linux 分支为
# gunicorn,可用 APP_SERVER 切到 granian/uvicorn)
ExecStart=/data/chongming/chongming ExecStart=/data/chongming/chongming
# 所有相对路径(config.toml / config/ / resources/*.mbank / static.svfs / # 所有相对路径(config.toml / config/ / resources/*.mbank / static.svfs /
# data/ / logs/ / uploads/)均相对工作目录解析,必须指向安装目录 # data/ / logs/ / uploads/)均相对工作目录解析,必须指向安装目录
@@ -48,7 +51,7 @@ Environment=PYTHONUNBUFFERED=1
# 私有动态库,再补一条 Environment=LD_LIBRARY_PATH=/opt/sophon/sophon-ffmpeg-latest/lib # 私有动态库,再补一条 Environment=LD_LIBRARY_PATH=/opt/sophon/sophon-ffmpeg-latest/lib
Environment=PATH=/opt/sophon/sophon-ffmpeg-latest/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin Environment=PATH=/opt/sophon/sophon-ffmpeg-latest/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 优雅停机:SIGTERM 由 gunicorn 排空 worker,等在途请求/告警上传完成 # 优雅停机:SIGTERM 由服务器 master 排空 worker,等在途请求/告警上传完成
KillSignal=SIGTERM KillSignal=SIGTERM
KillMode=control-group KillMode=control-group
TimeoutStopSec=30 TimeoutStopSec=30
+2
View File
@@ -33,6 +33,8 @@ debug = false
env = "production" env = "production"
[production.server] [production.server]
# 启动参数对各服务器通用(由 main.py 的转换函数映射,无需按服务器改配置);
# 服务器/副本数可用环境变量覆盖:APP_SERVER=granian|uvicorn|gunicorn、APP_WORKERS=<N>
access_log = false access_log = false
host = "0.0.0.0" host = "0.0.0.0"
port = 8000 port = 8000
+127 -44
View File
@@ -3,23 +3,126 @@
""" """
FastAPI chongming 示例 FastAPI chongming 示例
主入口文件,适配 chongming 打包系统 主入口文件,适配 chongming 打包系统
服务器选择(APP_SERVER 环境变量,默认见 DEFAULT_SERVER):
- gunicorn:现状保留(Linux 默认,uvloop worker + preload 多进程)
- uvicorn:Windows 默认(gunicorn 依赖 fork,Windows 不可用)
- granian:Rust HTTP 内核,多进程
启动参数保持 config.toml `[<env>.server]` 现状(host / port / workers /
reload / access_log / timeout_keep_alive),由 `_to_uvicorn`、`_to_granian`、
`_to_gunicorn_args` 转换为对应服务器的参数。
""" """
import asyncio # noqa: F401
import os import os
import sys
import platform import platform
from utils.launch import launch import sys
from multiprocessing import freeze_support from multiprocessing import freeze_support
from pathlib import Path
import server # noqa: F401 # 预加载:master 内完成模块银行装载,fork 出的 worker 共享
import uvloop # noqa: F401 # 显式导入保证打包时收集 uvloop(uvicorn/granian loop 依赖)
from utils.launch import launch
from uvicorn.workers import UvicornWorker from uvicorn.workers import UvicornWorker
import server
import uvloop
import asyncio
use_shell = platform.system() == "Windows" use_shell = platform.system() == "Windows"
# 未设置 APP_SERVER 时的平台默认(保持现状)
DEFAULT_SERVER = "uvicorn" if use_shell else "gunicorn"
SUPPORTED_SERVERS = ("gunicorn", "uvicorn", "granian")
# 热重载(reload=true)监听的源码目录,避免 granian 默认扫 cwd 时被 logs/ 写日志触发
RELOAD_PATHS = [Path("src/chongming"), Path("src/plugins")]
class UvloopUvicornWorker(UvicornWorker): class UvloopUvicornWorker(UvicornWorker):
CONFIG_KWARGS = {"loop": "uvloop"} CONFIG_KWARGS = {"loop": "uvloop"}
def _server_params(app_config: dict) -> dict:
"""把 `[<env>.server]` 的现状参数整理为各服务器共用的启动参数。
只做缺省兜底与运行环境覆盖(打包环境强制禁 reload、APP_WORKERS 覆盖副本数),
键名保持 config.toml 现状,改名交给下面的转换函数。
"""
server_config = app_config.get("server", {})
# 打包环境强制禁用重载
is_frozen = getattr(sys, "frozen", False)
# 支持通过环境变量 APP_WORKERS 覆盖 worker 数量(方便嵌入式设备调优)
default_workers = server_config.get("workers", 4)
workers = int(os.environ.get("APP_WORKERS", str(default_workers)))
return {
"host": server_config.get("host", "0.0.0.0"),
"port": server_config.get("port", 8000),
"workers": workers,
"reload": False if is_frozen else server_config.get("reload", False),
"access_log": server_config.get("access_log", False),
"timeout_keep_alive": server_config.get("timeout_keep_alive", 5),
"log_level": "debug" if app_config.get("debug", False) else "info",
}
def _to_uvicorn(params: dict) -> dict:
"""共用参数 → uvicorn.run 关键字参数(键名一致)。"""
return {
"host": params["host"],
"port": params["port"],
"reload": params["reload"],
"workers": params["workers"],
"log_level": params["log_level"],
"access_log": params["access_log"],
"timeout_keep_alive": params["timeout_keep_alive"],
}
def _to_granian(params: dict) -> dict:
"""共用参数 → granian.Granian 关键字参数。
仅两处改名:host → address、access_log → log_access;
timeout_keep_alive 在 granian 无对应项(HTTP/1 keep-alive 只有开关、无空闲
超时配置),忽略。
"""
return {
"address": params["host"],
"port": params["port"],
"interface": "asgi",
"workers": params["workers"],
"reload": params["reload"],
"log_level": params["log_level"],
"log_access": params["access_log"],
}
def _to_gunicorn_args(params: dict) -> list:
"""共用参数 → gunicorn 命令行参数(保持现状:uvloop worker + preload)。"""
return [
"gunicorn",
"server:app",
"-k",
"__main__.UvloopUvicornWorker",
"--preload",
"-w",
str(params["workers"]),
"-b",
f"{params['host']}:{params['port']}",
]
def _select_server(logger) -> str:
"""按 APP_SERVER 环境变量选择服务器,非法值/平台不兼容时回退默认。"""
server_kind = (os.environ.get("APP_SERVER") or DEFAULT_SERVER).strip().lower()
if server_kind not in SUPPORTED_SERVERS:
logger.warning(
f"未知 APP_SERVER={server_kind!r}(可选: {', '.join(SUPPORTED_SERVERS)}),"
f"回退 {DEFAULT_SERVER}"
)
server_kind = DEFAULT_SERVER
if server_kind == "gunicorn" and use_shell:
logger.warning("Windows 不支持 gunicorn(依赖 fork),回退 uvicorn")
server_kind = "uvicorn"
return server_kind
def app_run(app_config: dict, default_config: dict): def app_run(app_config: dict, default_config: dict):
""" """
应用运行函数 - 适配模块银行环境 应用运行函数 - 适配模块银行环境
@@ -35,18 +138,8 @@ def app_run(app_config: dict, default_config: dict):
logger = get_logger("app") logger = get_logger("app")
# 获取服务器配置 params = _server_params(app_config)
server_config = app_config.get("server", {}) server_kind = _select_server(logger)
host = server_config.get("host", "0.0.0.0")
port = server_config.get("port", 8000)
timeout_keep_alive = server_config.get("timeout_keep_alive", 5)
# 打包环境强制禁用重载
is_frozen = getattr(sys, "frozen", False)
reload = False if is_frozen else server_config.get("reload", False)
# 支持通过环境变量 APP_WORKERS 覆盖 worker 数量(方便嵌入式设备调优)
default_workers = server_config.get("workers", 4)
workers = int(os.environ.get("APP_WORKERS", str(default_workers)))
# 显示启动信息 # 显示启动信息
env = app_config.get("env", "development") env = app_config.get("env", "development")
@@ -54,41 +147,31 @@ def app_run(app_config: dict, default_config: dict):
logger.info(f"🚀 启动 {default_config.get('app.name', 'FastAPI 应用')}") logger.info(f"🚀 启动 {default_config.get('app.name', 'FastAPI 应用')}")
logger.info(f"📦 版本: {default_config.get('app.version', '1.0.0')}") logger.info(f"📦 版本: {default_config.get('app.version', '1.0.0')}")
logger.info(f"🌍 环境: {env}") logger.info(f"🌍 环境: {env}")
logger.info(f"📍 地址: http://{host}:{port}") logger.info(f"🖥️ 服务器: {server_kind}")
logger.info(f"📚 文档: http://{host}:{port}/docs") logger.info(f"📍 地址: http://{params['host']}:{params['port']}")
logger.info(f"📚 文档: http://{params['host']}:{params['port']}/docs")
logger.info(f"🔧 调试: {'启用' if app_config.get('debug', False) else '禁用'}") logger.info(f"🔧 调试: {'启用' if app_config.get('debug', False) else '禁用'}")
logger.info(f"🔄 重载: {'启用' if reload else '禁用'}") logger.info(f"🔄 重载: {'启用' if params['reload'] else '禁用'}")
logger.info(f"👥 Workers: {workers}") logger.info(f"👥 Workers: {params['workers']}")
logger.info("=" * 50) logger.info("=" * 50)
# 根据操作系统选择不同的服务器启动方式 # 根据所选服务器启动(默认平台分支保持现状)
if use_shell: if server_kind == "granian":
import granian
granian.Granian(
target="server:app",
**_to_granian(params),
reload_paths=RELOAD_PATHS,
).serve()
elif server_kind == "uvicorn":
import uvicorn import uvicorn
uvicorn.run( uvicorn.run("server:app", **_to_uvicorn(params))
"server:app",
host=host,
port=port,
reload=reload,
workers=workers,
log_level="debug" if app_config.get("debug", False) else "info",
access_log=False,
timeout_keep_alive=timeout_keep_alive,
)
else: else:
from gunicorn.app.wsgiapp import run from gunicorn.app.wsgiapp import run
sys.argv = [ sys.argv = _to_gunicorn_args(params)
"gunicorn",
"server:app",
"-k",
"__main__.UvloopUvicornWorker",
"--preload",
"-w",
str(workers),
"-b",
f"{host}:{port}",
]
run() run()
+10 -1
View File
@@ -9,6 +9,7 @@ dependencies = [
"eclipse-zenoh", "eclipse-zenoh",
"fastapi[uvicorn]", "fastapi[uvicorn]",
"gunicorn", "gunicorn",
"granian[reload,uvloop]>=2.8.4",
"module-bank", "module-bank",
"openai", "openai",
"python-multipart", "python-multipart",
@@ -36,7 +37,14 @@ requires-python = ">=3.12"
version = "0.1.0" version = "0.1.0"
[dependency-groups] [dependency-groups]
dev = ["chongming-cli", "nuitka", "pyarmor", "pyinstaller", "ruff"] dev = [
"chongming-cli",
"greenlet>=3.5.6",
"nuitka",
"pyarmor",
"pyinstaller",
"ruff",
]
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/chongming"] packages = ["src/chongming"]
@@ -49,5 +57,6 @@ url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"
[project.scripts] [project.scripts]
gunicorn = "chongming:gunicorn_serve" gunicorn = "chongming:gunicorn_serve"
granian = "chongming:granian_serve"
serve = "chongming:serve" serve = "chongming:serve"
worker_serve = "chongming_worker.process:worker_serve" worker_serve = "chongming_worker.process:worker_serve"
+89 -2
View File
@@ -1,7 +1,66 @@
"""chongming 网关启动入口(uvicorn / granian / gunicorn)。
config.toml 中 `[<env>.server]` 的启动参数保持现状(host / port / workers /
reload / access_log / timeout_keep_alive),由 `_to_uvicorn`、`_to_granian` 在
启动时转换为对应服务器的关键字参数,切换服务器不必改配置格式。
"""
from collections.abc import Mapping
from pathlib import Path
from typing import Any
import uvicorn import uvicorn
from chongming_config import load_config from chongming_config import Server, load_config
from .app.core.logger import get_logger from .app.core.logger import get_logger
# 开发期热重载(reload=true)监听的源码目录(granian 用;uvicorn 走其默认扫描)
RELOAD_PATHS = [Path("src/chongming"), Path("src/plugins")]
def _server_config_view(server_config: Server) -> Mapping[str, Any]:
"""[<env>.server] 的宽松视图:实际 TOML 可能带 Server 之外的键
(如 timeout_keep_alive,chongming_config.Server 尚未声明)。"""
return server_config
def _to_uvicorn(server_config: Server) -> dict:
"""把 [<env>.server] 的现状参数转为 uvicorn.run 的关键字参数。
uvicorn 的参数名与配置项一致,这里只做缺省兜底。
"""
cfg = _server_config_view(server_config)
converted: dict = {
"host": cfg.get("host", "0.0.0.0"),
"port": cfg.get("port", 8000),
"reload": cfg.get("reload", False),
}
for key in ("workers", "access_log", "timeout_keep_alive"):
if key in cfg:
converted[key] = cfg[key]
return converted
def _to_granian(server_config: Server) -> dict:
"""把 [<env>.server] 的现状参数转为 granian.Granian 的关键字参数。
仅两处改名:host → address、access_log → log_access;
timeout_keep_alive 在 granian 无对应项(HTTP/1 keep-alive 只有开关、无空闲
超时配置),忽略。
"""
cfg = _server_config_view(server_config)
converted: dict = {
"address": cfg.get("host", "0.0.0.0"),
"port": cfg.get("port", 8000),
"interface": "asgi",
}
for key in ("reload", "workers"):
if key in cfg:
converted[key] = cfg[key]
if "access_log" in cfg:
converted["log_access"] = cfg["access_log"]
return converted
def serve(): def serve():
""" """
@@ -20,7 +79,34 @@ def serve():
logger = get_logger("app") logger = get_logger("app")
logger.info(f"启动服务: {server_config}") logger.info(f"启动服务: {server_config}")
logger.info(f"启动日志: {log_config}") logger.info(f"启动日志: {log_config}")
uvicorn.run("chongming.app:app", **server_config) uvicorn.run("chongming.app:app", **_to_uvicorn(server_config))
def granian_serve():
"""
使用 granian 启动服务
从配置文件中加载服务器配置和日志配置,然后使用 granian 启动应用服务。
该函数适用于开发环境或单进程部署场景。
Returns:
None: 该函数不返回值,会阻塞运行直到服务停止
"""
# 延迟导入:未选用 granian 的部署环境不必安装它
import granian
config = load_config()
env = config["default"]["env"]
server_config = config[env]["server"]
log_config = config[env]["logging"]
logger = get_logger("app")
logger.info(f"启动服务: {server_config}")
logger.info(f"启动日志: {log_config}")
granian.Granian(
target="chongming.app:app",
**_to_granian(server_config),
reload_paths=RELOAD_PATHS,
).serve()
def gunicorn_serve(): def gunicorn_serve():
@@ -36,6 +122,7 @@ def gunicorn_serve():
None: 该函数不返回值,会阻塞运行直到服务停止 None: 该函数不返回值,会阻塞运行直到服务停止
""" """
import sys import sys
from gunicorn.app.wsgiapp import run from gunicorn.app.wsgiapp import run
config = load_config() config = load_config()
Generated
+872 -530
View File
File diff suppressed because it is too large Load Diff