本节目标:把容器服务、PaaS、FaaS 三种部署形态摆在一起比较取舍,实测「配置走环境变量」与「冷启动耗时」,讲清健康检查、密钥注入与各平台配置文件的形态。
适用版本:Python 3.12+(实测 3.14.6);pydantic-settings 2.15.0、fastapi 0.143.0
15.3 云平台部署与 Serverless
镜像有了、进程模型也调好了,最后一个问题:这套东西跑在哪台机器上? 自建服务器最灵活但运维最重,托管平台省心但受约束。这一节不站队,只把三种形态的能力边界、成本结构和配置形态摊开,让你按项目阶段选。
说明:本机无任何云环境,因此云平台的配置文件(
Procfile、app.yaml、service.yaml、Lambda 模板)均未实际部署,仅示意。但其中「配置走环境变量」和「冷启动耗时」两部分是可以在本地真跑的,下面会明确标注哪些是实测。
15.3.1 三种形态:容器服务 vs PaaS vs FaaS
| 形态 | 代表 | 你负责 | 平台负责 | 计费 |
|---|---|---|---|---|
| 容器服务 | Cloud Run、ECS、K8s | 镜像、端口、健康检查 | 调度、扩缩容、负载均衡 | 按 CPU/内存/时长 |
| PaaS | Heroku、Render、App Engine | 代码、依赖声明 | 构建、运行、路由全包 | 按实例时长 |
| FaaS | Lambda、Cloud Functions | 一个 handler 函数 | 一切(含按请求拉起) | 按调用次数 + 时长 |
一句话取舍:控制力从高到低是「容器服务 > PaaS > FaaS」,省心程度正好反过来。选型的经验法则:
- 有状态、长连接、需要自定义系统依赖 → 容器服务;
- 标准 Web 应用、想少运维 → PaaS;
- 事件驱动、流量稀疏、突发性强(webhook、定时任务、图片处理) → FaaS。
15.3.2 十二要素:配置与密钥走环境变量(实测)
无论选哪种形态,第一条铁律都一样:配置和密钥不进代码、不进镜像,一律走环境变量。这正是「十二要素应用」的第三条(Config)。用 pydantic-settings 2.15.0 把环境变量映射成带类型的配置对象,本机实测:
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="APP_", case_sensitive=False)
port: int = Field(default=8000, ge=1, le=65535)
database_url: str = "sqlite+aiosqlite:///./dev.db"
log_level: str = "info"
secret_key: str = Field(default="", repr=False) # repr=False 防止日志泄露
设置 APP_PORT、APP_DATABASE_URL、APP_LOG_LEVEL、APP_SECRET_KEY 四个环境变量后实例化,真实输出:
port = 8080 int
db_url = postgresql+asyncpg://u:p@db:5432/prod
log_level = warning
settings = port=8080 database_url='postgresql+asyncpg://u:p@db:5432/prod' log_level='warning'
三个要点:env_prefix="APP_" 给所有变量加命名空间,避免和系统变量撞名;类型自动转换(字符串 "8080" 变 int,且受 ge=1, le=65535 约束);repr=False 让 secret_key 不出现在 print(settings) 里——这是防止密钥随日志泄露的细节,实测最后一行确实没有 secret_key。生产上,database_url 与 secret_key 由平台的密钥管理服务(AWS Secrets Manager、GCP Secret Manager)注入为环境变量,代码侧零改动。
15.3.3 容器服务:把上一节的镜像直接推上去
容器服务(Cloud Run / ECS / K8s)的部署单元就是你 15.1 做好的镜像。Cloud Run 需要一个监听 $PORT 的 HTTP 服务,启动命令形态如下(未实测,仅示意):
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
ENV PYTHONUNBUFFERED=1
# Cloud Run 通过 $PORT 环境变量指定端口,默认 8080
CMD exec uvicorn main:app --host 0.0.0.0 --port ${PORT:-8080} --workers 2
注意 exec 前缀:它让 uvicorn 取代 shell 成为 1 号进程,这样平台发的 SIGTERM 才能直接到达 uvicorn,优雅关闭(15.2)才生效。若不加 exec,信号发给 shell,uvicorn 收不到。
Cloud Run 的部署命令与健康检查(未实测):
gcloud run deploy demo \
--source . \
--region asia-east1 \
--allow-unauthenticated \
--set-env-vars APP_LOG_LEVEL=info \
--set-secrets APP_SECRET_KEY=app-secret:latest \
--min-instances 0 --max-instances 10
--set-env-vars 注入普通配置,--set-secrets 从 Secret Manager 注入密钥。--min-instances 0 意味着没流量时缩到零、省钱,代价是冷启动(见 15.3.5);想消除冷启动就设 --min-instances 1,但那样一直有实例在计费。
15.3.4 PaaS:Procfile 与构建包
PaaS(Heroku、Render)不要求你写 Dockerfile,它靠约定推断怎么构建和启动。核心是一个 Procfile(未实测):
web: uvicorn main:app --host 0.0.0.0 --port $PORT --workers 2
一行声明「web 进程用什么命令启动」。PaaS 会读 requirements.txt 或 pyproject.toml 自动装依赖,读 runtime.txt 或 .python-version 决定 Python 版本。App Engine 则用 app.yaml(未实测):
runtime: python312
entrypoint: uvicorn main:app --host 0.0.0.0 --port $PORT
env_variables:
APP_LOG_LEVEL: info
automatic_scaling:
min_instances: 0
max_instances: 5
PaaS 的取舍很清晰:上手最快、运维最少,但你对运行时环境的控制力也最弱——想装一个系统级依赖(比如 ffmpeg)可能得靠 buildpack 或容器化,绕一圈又回到了容器服务。
15.3.5 FaaS:冷启动是最大的代价
FaaS(Lambda)把「服务器」抽象成一个函数。你只写一个 handler,平台负责一切。契约是「事件字典进、响应字典出」:
import json
def handler(event: dict, context: object = None) -> dict:
path = event.get("rawPath") or event.get("path", "/")
qs = event.get("queryStringParameters") or {}
body = {"status": "ok"} if path == "/health" else {"path": path, "query": qs}
return {
"statusCode": 200,
"headers": {"content-type": "application/json"},
"body": json.dumps(body, ensure_ascii=False),
}
这个 handler 是纯函数,本机直接调用即可验证形状(真实输出):
statusCode: 200
content-type: application/json
body: {"path": "/items", "method": "GET", "query": {"page": "2"}}
健康检查: {"status": "ok"}
真实 Lambda 里,这个 handler 前面通常套一层 ASGI 适配器(如 mangum,本机未安装),把 API Gateway 事件翻译成 ASGI 请求再交给 FastAPI。FaaS 最大的痛点是冷启动:函数一段时间没被调用就被回收,下次调用要重新「拉起运行时 + 导入应用」。用本地进程启动耗时做代理测量(真实数据):
| 阶段 | min | median |
|---|---|---|
| 纯解释器启动 | 140.2ms | 181.3ms |
import fastapi | 450.4ms | 467.8ms |
import worker_app(FastAPI 应用) | 518.4ms | 617.9ms |
导入 FastAPI 就吃掉了约 450ms,这是冷启动的大头。云平台的冷启动还要叠加容器调度、网络初始化,实测常到几百毫秒到数秒。降低冷启动的手段:
- 精简依赖:导入的包越少越快(呼应 15.1 的镜像瘦身,瘦身同时也在减冷启动);
- 惰性导入:把只在某些路径用到的重库移到函数内部
import; - 预置并发 / 最小实例:用钱换时间,
min-instances >= 1消除冷启动; - 避免大依赖:
pandas、torch这类库光导入就几百毫秒,别放进冷启动路径。
15.3.6 健康检查与就绪探针
三种形态都需要健康检查,但语义不同:
| 探针 | 问的问题 | 失败后果 | 端点建议 |
|---|---|---|---|
| liveness(存活) | 进程还活着吗? | 重启容器 | 只返回进程自身状态,不查下游 |
| readiness(就绪) | 能接流量了吗? | 从负载均衡摘除 | 检查依赖(DB、缓存)连通性 |
| startup(启动) | 启动完成了吗? | 继续等,别判死 | 给慢启动应用宽限 |
最常见的错误是把依赖检查放进 liveness:数据库抖动一下,liveness 失败,平台把好好的容器重启了,反而雪上加霜。正确做法是 liveness 只探进程自身,readiness 才查依赖。上一节的 /health 就是标准的 liveness 端点:
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
15.3.7 配置从哪来:三种来源的取舍
「配置走环境变量」是原则,但具体从哪注入有讲究:
| 来源 | 适合放什么 | 优点 | 风险 |
|---|---|---|---|
| 镜像内默认值 | 非敏感的合理默认(端口、日志级别) | 零配置可跑 | 改配置要重新构建 |
| 环境变量 | 普通配置、非敏感参数 | 12-factor、平台原生 | 明文,可能进日志/进程列表 |
| 密钥管理服务 | 密码、Token、私钥 | 加密存储、可审计、可轮转 | 需 SDK 或平台注入,略复杂 |
分界线是敏感与否:APP_LOG_LEVEL=info 放环境变量没问题;APP_SECRET_KEY 应该来自 Secret Manager,由平台以环境变量形式注入。永远不要把密钥提交进仓库或烧进镜像——镜像层可被任何人 docker history 翻出来(15.1 演示过),一旦密钥进了镜像层,删掉也没用,它还在历史层里。
15.3.8 成本模型:按什么计费决定怎么优化
三种形态的计费口径不同,优化方向也不同:
| 形态 | 计费维度 | 省钱方向 |
|---|---|---|
| 容器服务 | 实例数 × 时长 × 资源规格 | 缩容到零(min-instances 0)、按需调规格 |
| PaaS | 实例时长 | 减少常驻实例、用免费档 |
| FaaS | 调用次数 + 执行时长 × 内存 | 缩短执行时间、降内存、减少冷启动浪费 |
关键差别在流量稀疏的场景:一个每天只被调用几百次的 webhook,用容器服务要付 24 小时常驻的钱,用 FaaS 只付几百次执行的钱——差几十倍。反过来,持续高流量的服务用 FaaS 往往更贵(单次调用溢价 + 冷启动浪费),这时容器服务的固定成本反而划算。选型前先算清楚流量曲线是「持续」还是「突发稀疏」。
15.3.9 部署检查清单
上线前过一遍:
- 配置与密钥全部走环境变量,代码/镜像里零硬编码
- 镜像非 root 运行(15.1),基础镜像固定版本
-
CMD用exec形式,保证 SIGTERM 能到达应用进程(15.2) - liveness / readiness 分开,liveness 不查下游依赖
-
min-instances/--min-instances按「能否接受冷启动」决定 - 优雅关闭宽限期 > 应用的
timeout-graceful-shutdown - 日志走 stdout/stderr,由平台采集(配
PYTHONUNBUFFERED=1) - 资源限额(CPU/内存)配置,防单实例拖垮节点
- 密钥来自 Secret Manager 而非环境变量明文或镜像层
- 按流量曲线(持续 / 突发稀疏)算过成本,选了对的计费形态
延伸阅读
- Python 部署与分发:Docker、PyPI 发布与可复现环境 —— Docker、打包与发布全流程
- 应用服务器进程模型与调优 —— 云上 worker 数怎么定
- 指标、健康检查与告警接入 —— 健康检查与监控的工程化
小结
- 三种形态取舍:控制力 容器服务 > PaaS > FaaS,省心程度反过来;有状态长连接选容器,事件驱动突发流量选 FaaS。
- 无论哪种形态,配置与密钥一律走环境变量;
pydantic-settings实测能把APP_*环境变量映射成带类型、带约束的配置对象,repr=False防止密钥进日志。 - 容器服务直接复用 15.1 的镜像,
CMD必须用exec形式,否则 SIGTERM 到不了 uvicorn。 - FaaS 的最大代价是冷启动:实测仅
import fastapi就约 450ms,import应用约 520–620ms;靠精简依赖、惰性导入、最小实例数缓解。 - liveness 只探进程自身,readiness 才查依赖——把依赖检查放进 liveness 会让抖动变成无谓重启。
- 本节所有云平台配置文件(
Procfile、app.yaml、Cloud Run/Lambda)本机无云环境,均未实测,仅示意;环境变量注入与冷启动耗时两部分为实测。
到这里,15 章把应用从「一份代码」送到了「云上稳定运行的服务」。服务跑起来之后,下一步就是让它跑得更快——第 16 章从剖析方法论讲起,教你先测量、再优化,而不是凭感觉猜瓶颈。
阅读导航:上一节:应用服务器进程模型与调优 · 下一节:剖析方法论 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。