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 秒实验
- 把
os.environ.get的默认值改成 5000,依次设 PORT 与不设,看两次分别监听哪个端口。 - 把
host改成127.0.0.1,再从另一个进程试着连,看报什么错。 - 想一想:为什么「模块级计数器」在本地一万遍都对,一上 Cloud Run 就出错?
常见误区
- 把状态放内存:本地全对,多实例就错,而且错得很随机。
- 端口写死 5000:平台注进来的 PORT 没人读,健康检查一直失败。
- host 写 127.0.0.1:容器外面连不进来。
- 装了 Docker 就以为能跑:服务没起,报的是连不上 daemon。
- 开了账号就以为能用 Cloud Run:API 没 enable,403。
自测三问(点开对答案)
1. 无状态到底是什么意思? —— 实例随时被销毁重建,内存里的东西既不保留也不共享,状态要放外部存储。 2. 为什么 host 必须写 0.0.0.0? —— 127.0.0.1 只监听回环地址,容器外面的请求进不来。 3. 冷启动慢在哪一步? —— 进程要重起、模型要重新加载;这个 API 里最重的是加载模型。该讲的约束讲完了,下一篇开始动手:配项目、写 Dockerfile、建置镜像。
- ☐ (本篇完,下一篇 15.3 步骤 1~3:配置、Dockerfile、建置映像)