tag 只是再挂一个名字,不复制;push 按层传,已有的层跳过。 部署不是覆盖,是新增一个修订版再把流量切过去——所以回滚不用重新构建。 验收六条,四条是错误路径,而且每一条都必须是 JSON。
这一篇对应教材 15.8 到 15.9:将 Docker 映像档加入 GCR、确认上传情况、部署至 Cloud Run。 上一篇把镜像建好了,这一篇把它弄到公网上,并且真的验一遍。第 3 篇到这儿收尾。
一、步骤 4:打个标签,推上去
docker tag detect-api:v1 gcr.io/my-project/detect-api:v1
docker push gcr.io/my-project/detect-api:v1
gcloud container images list-tags gcr.io/my-project/detect-api
教材 15.8。tag 是「再起个名字」,push 是「传上去」。
docker tag 不改文件,只给同一个镜像再挂一个名字。所以它几乎不花时间,也不是复制。
为什么必须换个名字才能推:
镜像仓库的地址就是名字的一部分。gcr.io/my-project/detect-api:v1 这个名字本身指明了往哪推——
registry 主机、项目、仓库、标签,四段。
docker push 是分层传的:本地已有、仓库也有的层会跳过。第一次推慢,改一行代码再推就快。
这也是为什么 Dockerfile 要分层写——上一篇那个顺序,在这儿兑现第二次。
教材 15.8.1 的「确认上传情况」就是第三条命令:看 tag 和 digest 在不在。
digest 那一串 sha256 是镜像的内容指纹,同一个 digest 到哪都是同一份东西。
二、步骤 5:部署与它的五个参数
gcloud run deploy detect-api \
--image gcr.io/my-project/detect-api:v1 \
--platform managed \
--region asia-east1 \
--allow-unauthenticated \
--memory 1Gi --cpu 1 --timeout 60
| 参数 | 为什么 |
|---|---|
--platform managed |
托管版,不用自己管机器 |
--region |
放哪个机房,跟着项目走 |
--allow-unauthenticated |
允许匿名访问,公开 API 才加 |
--memory 1Gi |
模型占内存,给小了会被 OOM 杀掉 |
--timeout 60 |
单请求最长 60 秒,超了直接断 |
--allow-unauthenticated 这条要想清楚。不加的话,所有请求都要带 Google 的身份令牌。
做内部服务时这是对的;做公开 API 时加了它,等于谁都能调、谁都能烧你的额度。
--memory 给小了会被 OOM 杀掉,而且报错很不直观——容器直接退出,日志里能看到 Killed,
但不会说「内存不够」。
这两个参数加起来,就是「部署」这件事真正的决策点:给谁用、给多少资源。
技术参数里最难的从来不是怎么填,是填多少。
三、部署之后你得到了什么
| 东西 | 是什么 |
|---|---|
| 一条 URL | 形如 https://detect-api-xxxx.run.app |
| 一个修订版 | 每次部署生成一个,带时间戳 |
| 流量指向 | 默认 100% 给最新那个修订版 |
修订版这个概念值得记:部署不是「覆盖」,是「新增一个修订版,然后把流量切过去」。 所以回滚就是把流量切回上一个修订版,不用重新构建、不用重新推镜像。 顺着想一步:既然流量可以切,就能灰度——把 10% 的流量给新版本,看着没问题再加到 100%。 这是 Cloud Run 这类平台给的东西,自己搭服务器得另外做。
四、部署完,六条验收
| 验证项 | 怎么打 | 期望 |
|---|---|---|
| 健康检查 | GET /api/health |
200 且含 status |
| 正常一张图 | POST /api/detect |
200 且 count 大于等于 1 |
| 缺文件 | 空请求 | 400 且是 JSON |
| 类型不对 | 传 .txt | 415 且是 JSON |
| 太大 | 传超限文件 | 413 且是 JSON |
| 不存在的路径 | GET /api/nope |
404 且是 JSON |
六项里四项是错误路径。 这不是苛刻,是因为正常路径你本地已经测过一百遍了,部署之后变的往往是错误路径。 尤其 413 和 404:本地跑着 dev server 时行为和线上不一样——线上前面通常还有一层负载均衡, 它自己也会先拦掉一些请求,拦完返回的东西未必是你的 JSON。
五、拿本地服务把这六条真走一遍
| 验证项 | 状态码 | 响应体 |
|---|---|---|
| 健康检查 | 200 | {"status":"ok"} |
| 正常一张图 | 200 | {"count":1,...} |
| 缺文件 | 400 | {"error":"no file"} |
| 类型不对 | 415 | {"error":"unsupported"} |
| 太大 | 413 | {"error":"payload too large"} |
| 不存在的路径 | 404 | {"error":"not found"} |
六项全对:200、200、400、415、413、404,而且每一项都是 JSON。
最后一项用的是 /api/nope 而不是 /nope。
差别很实在:错误处理器按路径前缀分流,/nope 走的是页面那条路,
返回 HTML——要验 JSON,请求就得落在 /api 下面。
第五项那个 413 是 @app.errorhandler 补出来的——第 14 章那个洞,在这儿成了验收清单上的一项。
这六条可以写成一个脚本,每次部署跑一遍。 第 11 章那些测试是在代码层验,这六条是在部署之后验,两者不重复。 少了这一步的后果很具体:部署完了,页面能打开,你以为成了,结果第一个用户传了张 3MB 的图, 拿到一坨 HTML,来问你「接口是不是坏了」。
六、三层防护,强弱不一样
| 层 | 在哪里 | 怎么坏 |
|---|---|---|
| 代码 | 第 1~11 章 | 测试能挡住 |
| 环境 | 第 15 章 | 镜像能挡住 |
| 网络与权限 | 第 15 章 | 只能靠部署后验 |
代码层有测试,环境层有镜像,而「线上到底通不通」只能真的打一次。 这也解释了为什么「本地能跑」这句话在工程上几乎不成立—— 它只覆盖了第一层。第 15 章从 Docker 讲到 Cloud Run,讲的就是把第二层和第三层补上。
七、第 3 篇到此结束
| 篇 | 做了什么 | 章 |
|---|---|---|
| 第 0 篇 | 环境、第一行代码 | 0 |
| 第 1 篇 | Flask 的地基:路由、模板、数据库、验证 | 1~3 |
| 第 2 篇 | 一个完整的物件侦测网站加测试 | 4~11 |
| 第 3 篇 | 把它做成 API,打包,部署 | 12~15 |
| 第 4 篇 | 开发机器学习 API | 16~17 |
回头看这 15 章,最值钱的不是「会写 Flask」,是那条链路:从「一个请求进来」到「一个响应出去」, 中间每一步都能说清在干什么。 下一篇开始第 4 篇,换方向:不再「用」别人训练好的模型, 而是自己做一个能对外提供服务的机器学习 API。
八、动手 Demo:步骤 4~5:推上去、部署、然后逐条验
这个 Demo 前三节是那几条命令(推不上去,本沙箱既没有 Docker 服务也没有 GCP 账号), 第四节是把六条验收在本地真打一遍。 第四节的六行是这一篇唯一能真验的东西,每一行都是真请求、真状态码。
gcloud run deploy detect-api \
--image gcr.io/my-project/detect-api:v1 \
--platform managed \
--region asia-east1 \
--allow-unauthenticated \
--memory 1Gi --cpu 1 --timeout 60
实跑输出:
--------------------------------------------------------------------
步骤 4~5:推上去、部署、然后逐条验
--------------------------------------------------------------------
① 步骤 4:推到 GCR
--------------------------------------------------------------------
[平台原样 · 本沙箱不执行]
| docker tag detect-api:v1 gcr.io/my-project/detect-api:v1
| docker push gcr.io/my-project/detect-api:v1
| gcloud container images list-tags gcr.io/my-project/detect-api
教材 15.8。tag 是「再起个名字」,push 是「传上去」。
! docker tag 不改文件,只是给同一个镜像再挂一个名字。所以它几乎不花时间,也不是复制。
! push 是分层传的:本地已有、仓库也有的层会跳过。
所以第一次推慢,改一行代码再推就快——这也是为什么 Dockerfile 要分层写。
② 步骤 5:部署
--------------------------------------------------------------------
[平台原样 · 本沙箱不执行]
| gcloud run deploy detect-api \
| --image gcr.io/my-project/detect-api:v1 \
| --platform managed \
| --region asia-east1 \
| --allow-unauthenticated \
| --memory 1Gi --cpu 1 --timeout 60
教材 15.9。参数不少,但每一个都能说清为什么。
| 参数 | 为什么 |
|---|---|
| `--platform managed` | 托管版,不用自己管机器 |
| `--region` | 放哪个机房,跟着项目走 |
| `--allow-unauthenticated` | 允许匿名访问,**公开 API 才加** |
| `--memory 1Gi` | 模型占内存,给小了会被 OOM 杀掉 |
| `--timeout 60` | 单请求最长 60 秒,超了直接断 |
! --allow-unauthenticated 这一条要想清楚。不加的话,所有请求都要带 Google 的身份令牌。
做内部服务时这是对的;做公开 API 时加了它,等于谁都能调、谁都能烧你的额度。
! --memory 给小了会被 OOM 杀掉,而且报错信息很不直观——容器直接退出,日志里能看到 Killed,
但不会说「内存不够」。
! 这两个参数加起来,就是「部署」这件事真正的决策点:给谁用、给多少资源。
③ 部署完,怎么验证
--------------------------------------------------------------------
| 验证项 | 怎么打 | 期望 |
|---|---|---|
| 健康检查 | GET /api/health | 200 且含 status |
| 正常一张图 | POST /api/detect | 200 且 count ≥ 1 |
| 缺文件 | POST /api/detect 空请求 | 400 且是 JSON |
| 类型不对 | 传 .txt | 415 且是 JSON |
| 太大 | 传超限文件 | 413 且是 JSON |
| 不存在的路径 | GET /nope | 404 且是 JSON |
! 六项里四项是错误路径。
这不是苛刻,是因为正常路径你本地已经测过一百遍了,
部署之后变的往往是错误路径——尤其是 413 和 404 这两项,本地跑着 dev server 时行为和线上不一样。
④ 拿本地服务把这些项真走一遍
--------------------------------------------------------------------
健康检查 -> 200 JSON {"status":"ok"}
正常一张图 -> 200 JSON {"count":1,"tags":[{"label":"spot"}]}
缺文件 -> 400 JSON {"error":"no file"}
类型不对 -> 415 JSON {"detail":"txt","error":"unsupported"}
太大 -> 413 JSON {"error":"payload too large"}
不存在的路径 -> 404 JSON {"error":"not found"}
! 六项全对:200、200、400、415、413、404,而且每一项都是 JSON。
! 最后一项用的是 /api/nope 而不是 /nope。
差别很实在:错误处理器按路径前缀分流,/nope 走的是页面那条路,返回 HTML——要验 JSON,
请求就得落在 /api 下面。
! 第五项那个 413 是 @app.errorhandler 补出来的——第 14 章那个洞,在这儿成了验收清单上的一项。
! 这六条可以写成一个脚本,每次部署跑一遍。
第 11 章那些测试是在代码层验,这六条是在「部署之后」验。
! 少了这一步的后果很具体:部署完了,页面能打开,你以为成了,结果第一个用户传了张 3MB 的图,
拿到一坨 HTML,来问你「接口是不是坏了」。
⑤ 这一章之后
--------------------------------------------------------------------
| 层 | 在哪里 | 怎么坏 |
|---|---|---|
| 代码 | 第 1~11 章 | 测试能挡住 |
| 环境 | 第 15 章 | 镜像能挡住 |
| 网络与权限 | 第 15 章 | 只能靠部署后验 |
! 三层的防护强度是不一样的。代码层有测试,环境层有镜像,而「线上到底通不通」只能真的打一次。
! 这也解释了为什么「本地能跑」这句话在工程上几乎不成立——它只覆盖了第一层。
⑥ 全书到这里
--------------------------------------------------------------------
| 篇 | 做了什么 | 章 |
|---|---|---|
| 第 0 篇 | 环境、第一行代码 | 0 |
| 第 1 篇 | Flask 的地基:路由、模板、数据库、验证 | 1~3 |
| 第 2 篇 | 一个完整的物件侦测网站 + 测试 | 4~11 |
| 第 3 篇 | 把它做成 API,打包,部署 | 12~15 |
| 第 4 篇 | 开发机器学习 API | 16~17 |
! 回头看这 15 章,最值钱的不是「会写 Flask」,是那条链路:从「一个请求进来」到「一个响应出去」,
中间每一步都能说清在干什么。
! 下一篇开始第 4 篇,换方向:不再「用」模型,而是「做」一个能对外提供服务的机器学习 API。
第一节 docker tag 不是复制,第三节 docker push 也不是全量重传。
第四节六项里有四项是错误路径,这是刻意排的。
第六节那张五行的表是全书索引,后面还剩第 4 篇。
30 秒实验
- 把这六条验收写成一个脚本,跑一次打印六个状态码。
- 给
/api/detect打一个 10MB 的请求,看 413 的响应体里说没说上限是多少。 - 想一想:为什么回滚不需要重新构建镜像?
常见误区
- 用 latest 部署:出问题不知道回哪个版本。
--allow-unauthenticated随手加:内部服务变成公开接口。- 部署完只看首页能不能打开:错误路径全返回 HTML 也没发现。
--memory给太小:被 OOM 杀掉,日志里只有 Killed。- 回滚时重新 build 一遍:没必要,切修订版流量就行。
自测三问(点开对答案)
1. docker tag 会复制镜像吗? —— 不会,只是给同一堆层再挂一个名字。 2. 为什么第二次 push 快? —— 按层传,仓库里已经有的层直接跳过。 3. 部署完最少验哪几项? —— 六项:健康检查、正常一张图、400、415、413、404,而且每一项都该是 JSON。第 4 篇从这里开始:不再调别人训练好的模型,而是自己做一个能对外服务的机器学习 API。
- ☐ (本篇完,下一篇 16.1 机器学习的概念、资料与任务)