2. Cloud Run服务部署与容器化实践
Cloud Run 的核心约定非常明确:它运行的是标准 OCI 容器镜像,而不是源码或某个特定框架的构建产物。因此,把应用部署到 Cloud Run 的完整链路可以概括为四段:写 Dockerfile 把应用打成镜像、构建并推送到 Artifact Registry、创建或更新 Cloud Run 服务、再通过修订版本与流量拆分完成灰度发布和回滚。理解这条链路,比记住某几条 gcloud 命令更重要。
一、Dockerfile:决定镜像能否在 Cloud Run 上跑起来
Cloud Run 对容器有三条硬性约束:容器必须监听由环境变量 PORT 指定的端口(默认注入 8080);进程必须在前台运行,不能自我 daemon 化;容器启动后要尽快开始接受请求。Dockerfile 的写法直接决定这三点是否满足。
以一个 Node.js Express 应用为例,推荐使用多阶段构建,把依赖安装与运行镜像分离:
# ---- 构建阶段 ----
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
# ---- 运行阶段 ----
FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app ./
# Cloud Run 会注入 PORT,默认 8080
ENV PORT=8080
EXPOSE 8080
CMD ["node", "server.js"]
应用侧必须读取 process.env.PORT,而不是把端口写死:
const port = process.env.PORT || 8080;
app.listen(port, '0.0.0.0', () => console.log(`listening on ${port}`));
这里有两个常见坑。第一,监听地址必须是 0.0.0.0 而非 127.0.0.1,否则 Cloud Run 的健康检查无法从容器外部访问。第二,镜像要尽量小:使用 slim 基础镜像、npm ci --omit=dev 去掉开发依赖、用 .dockerignore 排除 node_modules 和 .git,都能显著缩短冷启动时间。Cloud Run 按请求计费,冷启动越短,尾延迟越低。
二、构建与推送:Artifact Registry 是镜像的落脚点
Cloud Run 从 Artifact Registry 拉取镜像。先创建仓库,再构建推送。使用 Cloud Build 可以在云端完成构建,无需本地 Docker:
gcloud artifacts repositories create run-repo \
--repository-format=docker \
--location=asia-east1
gcloud builds submit \
--tag asia-east1-docker.pkg.dev/PROJECT_ID/run-repo/hello-api:v1
镜像标签策略值得提前规划。用 v1、v2 这类语义化标签便于人工识别,但 Cloud Run 真正依赖的是镜像的 digest(sha256:...)。当同一个标签被覆盖推送时,Cloud Run 不会自动重新拉取,必须显式更新服务。因此生产环境建议同时保留语义标签与 digest 记录,回滚时才能精确指向某个不可变镜像。
三、部署服务:把镜像变成可访问的 HTTPS 端点
部署命令的核心参数包括区域、内存、CPU、并发、超时和最小实例数:
gcloud run deploy hello-api \
--image asia-east1-docker.pkg.dev/PROJECT_ID/run-repo/hello-api:v1 \
--region asia-east1 \
--platform managed \
--allow-unauthenticated \
--memory 512Mi --cpu 1 \
--concurrency 80 \
--timeout 30s \
--min-instances 0 --max-instances 10 \
--set-env-vars NODE_ENV=production
几个参数需要结合业务判断。--concurrency 表示单个实例可同时处理的请求数,Node.js 这类事件循环模型可以设到 80 甚至更高;如果是 CPU 密集型的图像处理,则应降到 1~4,避免请求互相抢占。--min-instances 0 允许缩容到零,成本最低但会有冷启动;对延迟敏感的接口可设为 1 保持一个常驻实例。--timeout 上限为 3600 秒,但长连接场景要配合客户端超时一起考虑。
部署成功后,命令会输出服务 URL。此时可用 gcloud run services describe hello-api --region asia-east1 查看当前流量分配、最新修订版本和镜像 digest,作为后续版本管理的基线。
四、修订版本与流量拆分:灰度与回滚的抓手
Cloud Run 每次部署都会生成一个不可变的修订版本(Revision),命名形如 hello-api-00002-abc。流量拆分就是在这几个修订版本之间按百分比分配请求。典型做法是先发 10% 流量到新版本观察,再逐步放大:
# 部署新版本但不接收流量
gcloud run deploy hello-api \
--image .../hello-api:v2 --region asia-east1 --no-traffic
# 将 10% 流量切到新修订版本
gcloud run services update-traffic hello-api \
--region asia-east1 \
--to-revisions hello-api-00002-abc=10
# 验证通过后全量切换
gcloud run services update-traffic hello-api \
--region asia-east1 --to-latest
回滚同样简单:把流量指回上一个修订版本即可,无需重新构建镜像。这正是“镜像不可变 + 修订版本不可变”带来的好处。需要注意的是,流量拆分按请求比例生效,同一用户的连续请求可能落到不同版本,因此新版本必须保持 API 兼容;若做不到,应改用标签路由(tag)让特定 URL 指向特定修订版本,先做内部验证。
五、一次完整的灰度发布示例
假设 hello-api 已稳定运行 v1,现在要发布 v2。推荐的操作序列与观察点如下:
| 步骤 | 命令要点 | 观察与判断 |
|---|---|---|
| 构建推送 v2 | gcloud builds submit --tag .../hello-api:v2 | 构建日志无报错,镜像 digest 生成 |
| 无流量部署 | deploy --no-traffic | 新修订版本创建,流量仍 100% 在 v1 |
| 标签验证 | update-traffic --set-tags canary=hello-api-00002 | 通过 canary---hello-api-xxx.run.app 直连新版本自测 |
| 切 10% 流量 | update-traffic --to-revisions ...=10 | 观察错误率、P95 延迟、日志中版本标识 |
| 全量或回滚 | --to-latest 或指回 00001 | 指标正常则全量;异常则回滚,秒级生效 |
把这条链路走通之后,Cloud Run 的日常操作就归结为三件事:改代码、推镜像、切流量。Dockerfile 保证镜像可运行,Artifact Registry 保证镜像可追溯,修订版本与流量拆分保证发布可控可回滚。三者配合,才构成一个完整的容器化部署实践。