三层分开:应用包管启动、detector 管模型、views 管路由。 create_app 加一个 config 参数,测试和线上就能跑同一份代码。 启动时该做的是读配置、建目录、注册蓝图,不该下载权重。
这一篇对应教材 14.1 到 14.3.3:目录架构与模组、准备实作、编写启动程式码、加载组态建立应用、 编写通用设定、确认运作情况。 第 13 章定了规格,这一篇开始把它写出来。先立壳,再填肉。
一、三层分开
detect_api/
__init__.py 应用包,create_app 在这儿
config.py 配置
detector/
detector.py 读图、前处理、推理
postprocess.py 坐标还原、筛选、NMS
views/
detect.py 路由与错误处理
wsgi.py 入口,给部署平台用
实测这颗树的形状就是这样,wsgi.py 在包外面。
为什么把模型和路由分成两个目录:换模型的时候只动 detector/,改接口的时候只动 views/。
混在一起的项目,改一次要读完所有文件。
detector/ 里又拆成两个文件:detector.py(读图、前处理、推理)和 postprocess.py(坐标还原、
筛选、NMS)。拆开的理由是后处理不需要模型,它是纯数学,可以单独测。
这个分法跟第 3 章那个蓝图结构是一个思路,只是粒度粗一点——因为整个 API 只有一组端点, 不值得再拆蓝图。
二、create_app 六行
def create_app(config=None):
app = Flask(__name__, instance_relative_config=True)
app.config.from_object(Config)
if config:
app.config.update(config)
app.json.ensure_ascii = False
os.makedirs(app.instance_path, exist_ok=True)
app.register_blueprint(detect_bp)
return app
每一行都能往上追到前面某一章:工厂是第 2 章,配置类是第 2 章,蓝图是第 4 章,
ensure_ascii 是第 12 章。
instance_relative_config=True 值得单说:它让 instance 目录相对于应用包算,
而不是相对于当前工作目录。换个地方启动也不会跑偏。
config 那个参数是给测试用的——第 11 章那些 fixture 就是这么传内存库的。
工厂模式的好处在这里兑现:同一个应用能造出很多个互不相干的实例。
三、配置里哪些走环境变量
| 配置 | 来源 | 为什么 |
|---|---|---|
MAX_CONTENT_LENGTH |
环境变量 MAX_MB |
运维要调,改代码要重建镜像 |
INPUT_SIZE |
写死 416x416 | 模型决定的,改了效果就变 |
CONF_THRESHOLD |
写死 0.30 | 同上 |
NMS_IOU |
写死 0.45 | 同上 |
这是「谁该调」的判断,不是技术判断。 上传上限是运维的事:本地 2MB 够用,线上可能要 10MB。 写在环境变量里,改一下重启就行,不用重新构建镜像。 另外三个是模型行为,改了结果就变,应该跟代码一起进版本库——否则「昨天结果好好的,今天不对了」, 查半天才发现是环境变量被改了。
第 2 章那个配置分层在这里又用上一次:from_object 给默认值,update 覆盖。
测试传内存库、线上读环境变量,同一份代码。
四、跑起来看两个端点
| 请求 | 结果 |
|---|---|
GET /api/health |
200 + {"input":[416,416],"status":"ok"} |
GET /nope |
404 + HTML(还是第 10 章那个错误页) |
健康检查端点几乎是每个 API 的标配。
部署平台靠它判断「这个实例活了没有」,负载均衡靠它判断「要不要把流量切走」。
它返回的东西越简单越好:一个状态、几个关键配置。
别在里面查数据库、别在里面调外部接口——它会被高频调用,而且它挂了就等于整个实例挂了。
/nope 返回的还是 HTML,这是这一篇最后要补的洞。先把功能跑通,再把错误响应统一成 JSON。
五、启动时该干什么
| 该做 | 不该做 |
|---|---|
| 读配置 | 连数据库跑一堆查询 |
| 建目录 | 下载模型权重(网络可能不通) |
| 注册蓝图 | 起后台线程做定时任务 |
| 设日志级别 | 打印一大堆调试信息 |
模型加载放哪是个具体问题:模块级懒加载(第 8 章那个写法),还是启动时主动加载一次? 推荐后者:启动时加载,加载失败就启动失败。 这样部署平台立刻知道这个实例有问题,而不是等第一个用户来才 500。 第 8 章那个懒加载省事,但它把错误推迟到了运行时。两种都有人用,知道差别在哪就行。
不该做的那些有个共同点:它们都可能失败,而且失败的方式不可控。启动流程里每多一个外部依赖, 部署就多一种失败模式。
六、动手 Demo:目录架构与启动代码:先把壳立起来
这个 Demo 照教材 14.1 造出整套目录,然后真的起服务、真的发请求。 第四个那两行响应是实测的:一个 200 带 JSON,一个 404 还是 HTML。
import os
from flask import Flask
from .config import Config
from .views.detect import bp as detect_bp
def create_app(config=None):
app = Flask(__name__, instance_relative_config=True)
app.config.from_object(Config)
if config:
app.config.update(config)
app.json.ensure_ascii = False
os.makedirs(app.instance_path, exist_ok=True)
app.register_blueprint(detect_bp)
return app
实跑输出:
--------------------------------------------------------------------
目录架构与启动代码:先把壳立起来
--------------------------------------------------------------------
① 目录长这样
--------------------------------------------------------------------
|- api1/
|- wsgi.py
|- detect_api/
|- __init__.py
|- config.py
|- detector/
|- __init__.py
|- detector.py
|- postprocess.py
|- views/
|- __init__.py
|- detect.py
! 三层:detect_api/(应用包)、detector/(模型相关)、views/(路由)。
! 为什么把模型和路由分开:换模型的时候只动 detector/,改接口的时候只动 views/。
混在一起的那些项目,改一次要读完全部文件。
! wsgi.py 在包外面。它是「入口」,被 gunicorn 之类的服务器调用,第 15 章部署的时候会用到它。
② create_app 工厂
--------------------------------------------------------------------
[代码原样]
| def create_app(config=None):
| app = Flask(__name__, instance_relative_config=True)
| app.config.from_object(Config)
| if config:
| app.config.update(config)
| app.json.ensure_ascii = False
| os.makedirs(app.instance_path, exist_ok=True)
| app.register_blueprint(detect_bp)
| return app
六行,每一行都是第 2 章那个工厂的扩展。
! instance_relative_config=True 让 instance 目录相对于应用包算,而不是相对于当前工作目录。
换个地方启动也不会跑偏。
! create_app(config) 那个参数是给测试用的——第 11 章那些 fixture 就是这么传内存库的。
工厂模式的好处在这里兑现:同一个应用能造出很多个互不相干的实例。
③ 配置从环境变量来
--------------------------------------------------------------------
[代码原样]
| class Config:
| MAX_CONTENT_LENGTH = int(os.environ.get("MAX_MB", "2")) * 1024 * 1024
| INPUT_SIZE = (416, 416)
| CONF_THRESHOLD = 0.30
| NMS_IOU = 0.45
三个模型参数直接写死,上传上限从环境变量读。
! 为什么只有上传上限走环境变量?
因为它是运维要调的:本地 2MB 够用,线上可能要 10MB,改代码就得重新构建镜像。
! 另外三个是模型行为,改了效果就变,应该跟代码一起进版本库——这属于「谁该调」的判断,
不是技术判断。
④ 真的跑起来
--------------------------------------------------------------------
GET /api/health -> 200
响应体 -> {"input":[416,416],"status":"ok"}
GET /nope -> 404(还是 HTML 错误页)
! 健康检查端点几乎是每个 API 的标配。
部署平台靠它判断「这个实例活了没有」,负载均衡靠它判断「要不要把流量切走」。
! /nope 返回的还是第 10 章那个 HTML 错误页——这一篇最后会把它改成 JSON。
⑤ 启动时该干什么、不该干什么
--------------------------------------------------------------------
| 该做 | 不该做 |
|---|---|
| 读配置 | 连数据库跑一堆查询 |
| 建目录 | 下载模型权重(网络可能不通) |
| 注册蓝图 | 起后台线程做定时任务 |
| 设日志级别 | 打印一大堆调试信息 |
! 模型加载放哪是个具体问题:放模块级第一次调用时懒加载(第 8 章那个写法),
还是启动时主动加载一次?
! 推荐后者:启动时加载,加载失败就启动失败。
这样部署平台立刻知道这个实例有问题,而不是等第一个用户来才 500。
! 第 8 章那个懒加载省事,但它把错误推迟到了运行时。两种都有人用,知道差别就行。
第一节最外面那个 api1/ 就是项目根,wsgi.py 跟 detect_api/ 平级。 第四节两个状态码:一个 200 一个 404,差别在于有没有对应的路由。 第五节那张表右边四条,共同点是「都可能失败」。
30 秒实验
- 把
instance_relative_config=True去掉,然后从另一个目录启动,看 instance 跑哪去了。 - 给
/api/health加一项model_loaded,返回模型有没有加载。 - 想一想:为什么配置里只有上传上限该走环境变量?
常见误区
- 模型和路由混在一个文件里:换模型要读完全部代码。
- 启动时下载模型权重:网络不通就启动不了,而且每次重启都下。
- 健康检查里查数据库:它挂了就等于实例挂了。
- 配置全写死:换个环境要改代码重新构建。
自测三问(点开对答案)
1. 为什么把 detector 和 views 分成两个目录? —— 换模型只动 detector,改接口只动 views,互不牵连。 2. create_app 的 config 参数是干什么的? —— 让调用方覆盖默认配置,测试传内存库、线上读环境变量。 3. 模型加载放启动时还是懒加载? —— 推荐启动时,失败就启动失败,部署平台立刻能发现。壳立起来了,下一篇填肉:前处理和后处理这两头最容易被忽略的活。
- ☐ (本篇完,下一篇 14.2 前处理与后处理)