《Python编程实战》15.3 云平台部署与 Serverless

容器服务、PaaS、FaaS 三种部署形态的取舍:实测 pydantic-settings 环境变量注入与冷启动导入耗时,讲清 Procfile / app.yaml / Cloud Run / Lambda 的配置形态、密钥注入、健康检查与 Serverless 冷启动的代价。

本节目标:把容器服务、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/内存/时长
PaaSHeroku、Render、App Engine代码、依赖声明构建、运行、路由全包按实例时长
FaaSLambda、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 最大的痛点是冷启动:函数一段时间没被调用就被回收,下次调用要重新「拉起运行时 + 导入应用」。用本地进程启动耗时做代理测量(真实数据):

阶段minmedian
纯解释器启动140.2ms181.3ms
import fastapi450.4ms467.8ms
import worker_app(FastAPI 应用)518.4ms617.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 而非环境变量明文或镜像层
  • 按流量曲线(持续 / 突发稀疏)算过成本,选了对的计费形态

延伸阅读

小结

  • 三种形态取舍:控制力 容器服务 > 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 章从剖析方法论讲起,教你先测量、再优化,而不是凭感觉猜瓶颈。

阅读导航:上一节:应用服务器进程模型与调优 · 下一节:剖析方法论 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「python」更多文章

  1. 《Python高级编程》目录
  2. 《Python高级编程》11.3 PEP 流程与版本迁移策略
  3. 《Python高级编程》11.2 嵌入式与自由线程运行时