🧪 第15章 部署物件侦测应用
📄 一句话概括

Cloud Run 只认容器:给一个镜像,还你一个 https 地址。 五条特征里最要命的是无状态——任何实例随时可能被销毁,内存里的东西不算数。 端口由平台注入,读 PORT 环境变量;host 必须是 0.0.0.0,写回环地址外面连不进来。

📝 本章和前面几章的关系

这一篇对应教材 15.2 到 15.4.3:Cloud Run 的概要、特征、Docker 的使用准备(安装与启动)、 Cloud Run 的使用准备(免费帐号、开两个 API、装 Cloud SDK)。 上一篇把「环境」这件事想清了,这一篇讲把它交给谁跑,以及那个平台要求你改哪几行代码。

一、Cloud Run:给一个容器,还你一个地址

一句话说完:你把镜像推上去,它告诉你一个 https 地址。不用管机器、不用管系统、不用管扩容。

特征 具体是什么 对你代码的要求
无状态 实例随时被销毁重建 别把状态放内存
按请求计费 没请求不收费 冷启动要快
自动伸缩 0 到 N 个实例 启动要能并发跑
端口由平台给 读 PORT 环境变量 不能写死 5000
有超时 请求最长若干分钟 长任务要异步

五条里前三条是一件事的三个面:实例是「用完就扔」的。这跟你在本机 python run.py 一直跑着, 完全是两种东西。 按请求计费这一条要顺着想一步:没有请求时实例可以被缩到 0,所以「服务一直活着」这个前提不存在。 第 8 章那个模块级单例在本地是本意,在这里成了「每次醒来都可能重建」。

二、无状态:模块级变量就是 bug

实测:同一个应用起两个实例,各自连打三次,两个都是 [1, 2, 3]。

实例 三次请求的返回 pid
实例 A [1, 2, 3] 10552
实例 B [1, 2, 3] 25340

各数各的。两个实例是两条独立的内存,N 各有一份。 这就是「无状态」的实际含义:任何实例被销毁,内存里的东西就没了,而且别指望另一个实例知道。 所以「计数」这类东西要放外面:数据库、Redis、对象存储。 代码里出现一个模块级可变变量,部署上去就是个 bug,而且是最难查的那种——本地跑一万遍都对。

那第 8 章那个 get_detector() 为什么没犯这个错? 因为它存的是只读的模型,加载两次结果一样,实例之间根本不需要同步。 判断标准不是「能不能放内存」,是「别的实例需不需要知道你这份改动」。

三、PORT 与 0.0.0.0

import os


def serve(app):
    port = int(os.environ.get("PORT", "8080"))
    app.run(host="0.0.0.0", port=port)
环境 监听端口
不设 PORT 8080(代码里的默认值)
PORT=9090 9090
PORT=3000 3000

host="0.0.0.0" 也不能忘。 写 127.0.0.1 的话,服务只监听回环地址, 容器外面连不进来——本机测试一切正常,一部署就「连接被拒绝」。 两个错放在一起看:一个管「监听哪个地址」,一个管「监听哪个端口」。两个都对了,外面才连得进来。 平台为什么要指定端口?因为它要在容器外面做转发和健康检查,得知道往哪打。 你把端口写死 5000,平台注进来的 PORT 就没人读,转发全打到空处。

四、冷启动:第一次为什么那么慢

实例被销毁之后再来的第一个请求,要重新启动进程、加载模型——那一段就是冷启动。 对这个 API 来说,最重的一步是加载模型。 第 8 章那个 get_detector() 在模块里懒加载,第一次请求多花 35 毫秒。 换 YOLO 就是几百毫秒到一两秒。 具体多少秒这里测不出来(沙箱里没有 torch)。方向是清楚的:--min-instances=0 省钱但会冷; 设成 1 就一直温着,但一直收费。这是个成本与体验的取舍,没有标准答案。

教材没展开这一段,但真实部署第一个遇到的问题就是它:「为什么第一次访问这么慢」。 答案通常不是代码慢,是实例刚起床。

五、装 Docker Desktop:装完还要启动

步骤 做什么 怎么知道成了
下载安装 Docker Desktop for Windows 安装程序走完
启动 从开始菜单打开 托盘出现小鲸鱼图标
等服务起来 图标不转圈、变稳定 docker info 有输出
验证 docker run hello-world 打印一段欢迎语

第三行是上一篇那个 npipe 报错的现场——命令行装好了,服务还没起。这一步跟装包不是一回事, 装完不启动,一切命令都报「连不上 daemon」。 Windows 上还有一件容易卡住的事:Docker Desktop 默认用 WSL2 后端,第一次启动要装一个 Linux 内核, 装完通常要重启一次。 如果这台机器的虚拟化在 BIOS 里被关了(或者公司策略禁了),会卡在这一步, 报的是「WSL 2 installation is incomplete」之类,不是 Docker 自己的错。

六、GCP 那边要准备什么

事项 具体 为什么要
免费帐号 绑卡,有免费额度 超额才收费,额度用完服务会停
建项目 拿到项目 ID 所有资源挂在项目下,ID 全局唯一
开 Cloud Run API run.googleapis.com 没开就 403
开镜像仓库 API containerregistry.googleapis.com 镜像没地方存
装 Cloud SDK gcloud 命令 后面所有命令的前提
gcloud auth login
gcloud config set project my-project
gcloud services enable run.googleapis.com
gcloud services enable containerregistry.googleapis.com

教材 15.4 这四步全是云端操作,本沙箱一条都跑不了——它们改的是你 Google Cloud 账号里的状态, 跟本地装个包不是一类事。 要学的不是命令拼写,是「为什么要开这两个 API」:Cloud Run 要跑容器,所以开 run; 镜像要存,所以开 containerregistry。没开就用不了。 403 这个错很典型:命令没错、权限也没错,是「这项服务在你项目里根本没启用」。 Google Cloud 里几乎所有服务都是这个模式,用之前先 enable。

七、动手 Demo:Cloud Run:无状态、PORT 与冷启动

这个 Demo 先把那四条 gcloud 命令摆出来(跑不了),再用本地两个进程模拟「两个实例」, 最后验 PORT 到底从哪来。 第二节那张表是这一篇的纲:五条特征,最后都会落到某几行代码上。

import os


def serve(app):
    port = int(os.environ.get("PORT", "8080"))
    app.run(host="0.0.0.0", port=port)

实跑输出:

--------------------------------------------------------------------
Cloud Run:无状态、PORT 与冷启动
--------------------------------------------------------------------

  ① gcloud 不在,也不装
  --------------------------------------------------------------------
  shutil.which("gcloud") -> None

  [平台原样 · 本沙箱不执行]
  | gcloud auth login
  | gcloud config set project my-project
  | gcloud services enable run.googleapis.com
  | gcloud services enable containerregistry.googleapis.com
  教材 15.4 的四步:登录、设项目、开两个 API。全是云端操作。

  ! 这些命令一条都跑不了,也不需要跑——它们改的是你 Google Cloud 账号里的状态。
  ! 要学的不是命令拼写,是「为什么要开这两个 API」。
  Cloud Run 要跑容器,所以开 run.googleapis.com;
  镜像要存,所以开 containerregistry.googleapis.com。没开就用不了,报 403。

  ② Cloud Run 的五个特征
  --------------------------------------------------------------------

| 特征 | 具体是什么 | 对你代码的要求 |
|---|---|---|
| 无状态 | 实例随时被销毁重建 | 别把状态放内存 |
| 按请求计费 | 没请求不收费 | 冷启动要快 |
| 自动伸缩 | 0 到 N 个实例 | 启动要能并发跑 |
| 端口由平台给 | 读 `PORT` 环境变量 | 不能写死 5000 |
| 有超时 | 请求最长若干分钟 | 长任务要异步 |

  ! 五条里前三条是一件事的三个面:实例是「用完就扔」的。
  这跟你在本机 python run.py 一直跑着完全不一样。

  ③ PORT:唯一必须改的一行
  --------------------------------------------------------------------
  不设 PORT -> 8080
  设 PORT=9090 -> 9090
  设 PORT=3000 -> 3000

  [代码原样]
  | import os
  |
  | port = int(os.environ.get("PORT", "8080"))
  | app.run(host="0.0.0.0", port=port)
  本机默认 8080,线上由平台注入。host 必须是 0.0.0.0。

  ! host="0.0.0.0" 也不能忘。
  写 127.0.0.1 的话,服务只监听回环地址,容器外面连不进来——本机测试一切正常,
  一部署就「连接被拒绝」。
  ! 这两个错放在一起看:一个管「监听哪个地址」,一个管「监听哪个端口」。
  两个都对了,外面才连得进来。

  ④ 无状态:两个实例的计数不会共享
  --------------------------------------------------------------------
  实例 A 连请求三次 -> [1, 2, 3]
  实例 B 连请求三次 -> [1, 2, 3]
  两个 pid -> 10552 / 25340
  ! 各数各的。两个实例是两条独立的内存,N 各有一份。
  ! 这就是「无状态」的实际含义:任何实例被销毁,内存里的东西就没了,而且别指望另一个实例知道。
  ! 所以要把「计数」这类东西放外面:数据库、Redis、对象存储。
  代码里出现一个模块级可变变量,部署到 Cloud Run 上就是 bug。

  ⑤ 冷启动
  --------------------------------------------------------------------
  ! 实例被销毁之后再来的第一个请求,要重新启动进程、加载模型——那一段就是冷启动。
  ! 对这个 API 来说,冷启动里最重的一步是加载模型。
  第 8 章那个 get_detector() 在模块里懒加载,第一次请求会多花 35 毫秒。
  换 YOLO 就是几百毫秒到一两秒。
  ! 具体几秒这里测不出来(没有 torch)。但方向是清楚的:--min-instances=0 省钱但会冷;
  设成 1 就一直温着,但一直收费。这是个成本与体验的取舍。
  ! 教材没展开这一段,但真实部署第一个遇到的问题就是它:「为什么第一次访问这么慢」。
  答案不是代码慢,是实例刚起床。

第一节 shutil.which 返回 None,就是「没装」的机器可读形式。 第四节两个 pid 10552 与 25340,说明这是两个真进程,不是同一段代码跑两遍。 第五节那段冷启动本沙箱测不出来(没有 torch),能写的只有方向。

30 秒实验

  1. 把 os.environ.get 的默认值改成 5000,依次设 PORT 与不设,看两次分别监听哪个端口。
  2. 把 host 改成 127.0.0.1,再从另一个进程试着连,看报什么错。
  3. 想一想:为什么「模块级计数器」在本地一万遍都对,一上 Cloud Run 就出错?

常见误区

  1. 把状态放内存:本地全对,多实例就错,而且错得很随机。
  2. 端口写死 5000:平台注进来的 PORT 没人读,健康检查一直失败。
  3. host 写 127.0.0.1:容器外面连不进来。
  4. 装了 Docker 就以为能跑:服务没起,报的是连不上 daemon。
  5. 开了账号就以为能用 Cloud Run:API 没 enable,403。
自测三问(点开对答案) 1. 无状态到底是什么意思? —— 实例随时被销毁重建,内存里的东西既不保留也不共享,状态要放外部存储。 2. 为什么 host 必须写 0.0.0.0? —— 127.0.0.1 只监听回环地址,容器外面的请求进不来。 3. 冷启动慢在哪一步? —— 进程要重起、模型要重新加载;这个 API 里最重的是加载模型。

该讲的约束讲完了,下一篇开始动手:配项目、写 Dockerfile、建置镜像。

相关笔记