这个标题是我这系列文章的第三篇。前两篇分别写了 Flask 应用从零开发到能跑起来,再把它塞进 Docker 容器里完成镜像构建。这第三篇要解决的问题很直接:当你的 Flask 应用不再只是本地玩具,而是需要稳定运行、自动扩缩容、故障自愈的时候,怎么把它交给 Kubernetes 来管。
先说清楚这篇文章的适用范围。如果你只是写了个个人博客,放在一台云服务器上跑,那 Docker Compose 也许就够了,K8s 对你是过度设计。但如果你面对的是多个服务、多个实例、流量会波动、发布不能中断、出了问题最好能自己恢复的生产环境,那 Kubernetes 这套东西迟早要学。这篇文章就是用 Flask 这个大家最熟的 Web 框架做例子,把整个部署链路走一遍,把里面真正关键的操作和概念捋清楚。
我默认你已经对 Flask 和 Docker 有基本了解。知道怎么用docker build打镜像,知道 Dockerfile 大概长什么样。如果你还不会,建议先翻翻这个系列的前两篇,或者随便找本 Docker 入门书翻一下,然后回来继续看,体验会顺畅很多。
1. 部署方案设计:为什么是 Kubernetes,而不是 Docker Compose
1.1 从一个具体场景看 K8s 解决什么问题
假设你手上有一个 Flask 应用,是给公司内部用的报表系统。一开始流量不大,单机跑没问题。后来接入的业务方多了,每天早晚高峰期并发能到几百,单机开始吃力,偶尔还因为内存占用过高直接把进程干掉,导致服务短时间不可用。
你第一个反应可能是:多开几个进程,用 Nginx 做负载均衡。但问题马上来了:
- 这几台机器上的应用版本不一致怎么办?
- 某台机器挂了,流量怎么自动切走?
- 高峰期过了,多余的机器能不能自动缩掉省钱?
- 发新版本的时候,怎么做到不停机更新?
Docker Compose 能帮你在一台机器上编排容器,但它解决不了跨机器的服务发现、自动调度、故障转移和弹性伸缩。这时候就需要 Kubernetes 出场。
Kubernetes 本质上是一个容器编排系统,它的核心能力可以概括为三件事:调度、服务发现和状态收敛。
调度是说,你告诉 K8s“我要跑 3 个副本”,它会自动帮你挑 3 台合适的机器把这些容器放上去。服务发现是说,这些副本之间、以及外部访问这些副本,不需要你手动维护 IP 列表,K8s 通过 Service 对象帮你搞定。状态收敛是说,你声明了期望状态(比如“运行 3 个副本”),K8s 会不停地对比当前状态,发现不一致就自动调整。这第三个能力是 K8s 最迷人也最核心的设计哲学,后面讲 Deployment 的时候你会有更直观的感受。
1.2 用 Flask 应用验证全链路的价值
选 Flask 来做这个演示,是因为它足够简单。Flask 应用的核心就是一个 Python 进程,监听某个端口,处理 HTTP 请求。不像 Java 应用那样有复杂的 JVM 调优,也不像专门的消息消费者那样依赖外部 Broker。用它来学习 K8s,你可以把注意力完全集中在 Kubernetes 本身的概念和操作上,不用分心去处理应用框架的特殊性。
但这不代表 Flask 在 K8s 里跑和别的应用有什么本质区别。K8s 不认识 Flask,它只认识容器。它看到的只是一个镜像,一个启动命令,一组资源限制,一组探针配置。这套东西对 FastAPI、Django、Node.js Express、Spring Boot 完全通用。所以学会了这一套,你换任何一个框架都只是换镜像的问题。
这个 Flask 应用后续还可以扩展一些典型功能,比如:
- 在应用里加一个
/healthz健康检查接口,返回 JSON 状态,供 K8s 探活。 - 用环境变量注入数据库连接串,而不是写在代码里。
- 把日志输出到 stdout,方便 K8s 统一收集。
这些小改动在容器化时代属于常规操作,我会在后面的步骤里逐个展开。
1.3 整体架构与关键组件预览
在动手之前,先把我们要部署的整体架构画出来。你可以照着这个逻辑去理解后面每一步在干嘛:
用户请求 -> Ingress -> Service -> Pod(Flask应用容器) | +-> ConfigMap(配置文件) +-> Secret(敏感信息) +-> HPA(自动扩缩容)在这个架构里,Ingress 是入口网关,负责把外部请求转发到集群内部。Service 是内部负载均衡器,把请求分发给各个 Pod。Pod 里面跑的就是你的 Flask 容器实例,通常由 Deployment 管理。ConfigMap 和 Secret 负责把配置和敏感数据和镜像解耦。HPA 负责根据 CPU 或者自定义指标自动调整副本数。
这篇文章会把这些组件逐个过一遍,给你一个能直接照着操作的完整流程。不需要你一次性全理解,后面的实操会帮你把概念串起来。
2. Flask 应用容器化:为 Kubernetes 部署准备镜像
2.1 基础镜像选择与依赖管理
K8s 部署的第一步永远是容器镜像。如果镜像本身有问题,后面 Kubernetes 写得再漂亮也白搭。Flask 应用的 Dockerfile 一般长这样:
FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 5000 CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]这里有几个点值得展开说一下。
基础镜像选择:我推荐用python:3.11-slim而不是python:3.11。原因很简单:slim 版本基于 Debian 的精简系统,移除了大量用不到的包,镜像体积可以从 1GB 级别降到 150MB 左右。在 K8s 里,镜像体积直接关系到拉取速度。集群规模大了以后,每次发布新版本都要从镜像仓库拉几百 MB 甚至 1GB 的镜像,非常浪费时间。用 slim 版本能显著缩短发布周期。
如果你对镜像体积有更极致的追求,可以试试用python:3.11-alpine。但这个版本用的是 musl libc,有些 Python 包没有预编译的 wheel,安装的时候会自动编译,如果包里有 C 扩展(比如psycopg2、cryptography),编译起来会比较痛苦,还可能遇到兼容性问题。我的建议是:除非你很清楚自己在做什么,否则优先用 slim,不要为了省那一百 MB 给自己挖坑。
为什么要用 Gunicorn 而不是 Flask 自带的开发服务器。Flask 自带的app.run()服务器是 Werkzeug 开发的调试服务器,单进程、性能差,而且官方文档明确说了不能用于生产环境。Gunicorn 是一个成熟的 WSGI 服务器,支持多 worker 并发处理请求。在容器里用 Gunicorn 启动 Flask 应用是生产环境的标配。
2.2 多阶段构建与镜像瘦身技巧
如果你的 Flask 应用在构建阶段需要编译一些东西(比如前端资源打包、生成 protobuf 文件),那我强烈建议你用多阶段构建。
# 第一阶段:构建 FROM python:3.11-slim AS builder WORKDIR /build COPY requirements.txt . RUN pip install --no-cache-dir --prefix=/install -r requirements.txt # 如果还需要前端构建,可以在这里加 node 环境 # 第二阶段:运行 FROM python:3.11-slim WORKDIR /app COPY --from=builder /install /usr/local COPY . . EXPOSE 5000 CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]多阶段构建的核心思想是:把构建环境和运行环境分开。构建阶段装了一堆编译工具、依赖包、临时文件,最终都不带进运行镜像。运行镜像只包含真正需要的东西,体积小黑点少,攻击面也小。
构建 Flask 镜像时还有个常见坑:不要把整个项目目录 COPY 进去,包括本地虚拟环境、__pycache__、.git这些乱七八糟的东西。最佳实践是在项目里加一个.dockerignore文件:
__pycache__/ *.pyc *.pyo *.egg-info/ .venv/ venv/ .git/ .env .pytest_cache/.dockerignore的作用是给docker build命令一个“白名单”,告诉它哪些文件不用发送给 Docker daemon。如果不加这个文件,你本地几百 MB 的虚拟环境目录也可能被打包进去,既拖慢构建速度,又污染镜像内容。
我踩过最深刻的一次坑是:有次写完代码忘了更新requirements.txt,本地装了个新库,能在本地正常跑,但docker build之后的镜像死活报 ModuleNotFoundError。查了半天才想起来新库没进requirements.txt。所以强烈建议你每次本地调试结束后,都用pip freeze > requirements.txt同步一下依赖列表,并且用docker build之后的镜像跑一遍冒烟测试,确认镜像里的应用能正常工作再推送到镜像仓库。
2.3 镜像推送到私有仓库的必要性
镜像构建完成后,K8s 要从某个地方拉取这个镜像。如果你用的是 Docker Hub,可以推送到公共仓库,但生产环境谁会把镜像放在公共仓库?私有仓库几乎是标配。
两种常见选择:
- 云厂商的镜像仓库,比如阿里云 ACR、腾讯云 TCR、华为云 SWR。
- 自建 Harbor,适合对数据主权有要求或者完全离线的环境。
推送到私有仓库的命令很简单,就是一下两下:
docker tag flask-demo:v1.0 registry.example.com/flask-demo:v1.0 docker push registry.example.com/flask-demo:v1.0需要注意的是,如果你用私有仓库,K8s 节点需要配置认证信息才能拉取镜像。这通常用kubectl create secret docker-registry解决:
kubectl create secret docker-registry regcred \ --docker-server=registry.example.com \ --docker-username=your-username \ --docker-password=your-password \ --namespace=flask-app然后在 Deployment 的 spec 里加上imagePullSecrets字段,K8s 拉取镜像的时候就能认证了。这个细节在测试环境用 Docker Hub 的时候完全感知不到,但是换了私有仓库一定会踩到,提前讲出来免得你到时一脸懵。
3. Kubernetes 部署实战:编写核心清单文件
3.1 用 Namespace 给应用划分独立空间
人多了需要分房间,K8s 集群里跑的东西多了也需要分空间。Namespace 就是 K8s 用来做逻辑隔离的资源划分机制。它本身不提供硬隔离(那是后面的网络策略、资源配额做的事),但至少能让你在管理多个应用的时候不互相干扰。
创建一个 Namespace 非常简单:
apiVersion: v1 kind: Namespace metadata: name: flask-app也可以直接用命令创建:
kubectl create namespace flask-app后续所有资源都带上namespace: flask-app,这样你清理解析环境的时候,一行kubectl delete namespace flask-app就能把这个环境里的所有资源连锅端,非常干净。
有人可能会问:只有一个小应用,有必要用 Namespace 吗?我的看法是:就算只有一个应用,也建议建一个专属 Namespace。原因很实在:以后你要在这个集群上部署别的东西(比如监控组件、日志收集器),没有 Namespace 的话,大家全堆在 default 里,谁是谁都分不清。用 Namespace 把不同应用隔离开,是运维纪律的一部分。
还有一个小技巧:可以用 JSON 文件同时创建多个资源,比如:
kubectl create namespace flask-app --dry-run=client -o yaml > namespace.yaml这种方式适合你在写自动化脚本时使用,输出一个 YAML 文件,然后统一用kubectl apply -f namespace.yaml创建。不建议一条条命令手动敲,太容易漏。
3.2 Deployment:声明期望状态的控制器
Deployment 是 K8s 里最核心的工作负载控制器,它专门负责管理无状态应用。无状态的意思是说:每个 Pod 都是可替换的,实例之间不共享状态,任何一个实例挂了,新建一个顶上来就行,不需要恢复数据。
Flask 应用本质上是无状态的(前提是你没把 Session 存在本地文件,没把上传文件存在本地磁盘),非常适合用 Deployment 管理。
一个典型的 Flask 应用 Deployment 长这样:
apiVersion: apps/v1 kind: Deployment metadata: name: flask-app namespace: flask-app spec: replicas: 3 selector: matchLabels: app: flask-app template: metadata: labels: app: flask-app spec: containers: - name: flask-app image: registry.example.com/flask-demo:v1.0 ports: - containerPort: 5000 env: - name: DB_HOST valueFrom: configMapKeyRef: name: flask-config key: DB_HOST - name: DB_PASSWORD valueFrom: secretKeyRef: name: flask-secret key: DB_PASSWORD resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi readinessProbe: httpGet: path: /healthz port: 5000 initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: httpGet: path: /healthz port: 5000 initialDelaySeconds: 15 periodSeconds: 20这个 YAML 里的信息量很大,挑几个关键点理解透彻比盲写一百遍都管用。
selector.matchLabels和template.metadata.labels必须匹配。这是 Deployment 管理 Pod 的依据。Deployment 通过 label selector 找到它管理的 Pod,所以这两个地方的 labels 必须保持一致。很多人写 YAML 时粗心,这里写成app: flask-app,那里写成name: flask-app,结果 Deployment 创建了 Pod 却管不了它,或者 Service 选不到后端 Pod。这是 K8s 新手最容易掉的坑之一,建议每次写完 YAML 都检查一遍 label 的 key 和 value 是否完全一致。
resources里的requests和limits要区分。requests是 K8s 调度器用来决定把 Pod 放在哪台节点上的依据,相当于你告诉调度器“我这个容器至少需要这么多资源”。limits是运行时限制,如果容器试图超过这个限制,可能会被杀死或者被限流。如果不设置requests,调度器就不知道这个 Pod 的资源需求,可能会把整台节点塞满,导致资源碎片化。不设limits的风险是:一个容器内存泄露就能拖垮整台节点。
探针配置是生产环境必备。readinessProbe(就绪探针)决定这个 Pod 是否可以被加入 Service 的负载均衡池。如果你的 Flask 应用还在启动阶段(加载模型、初始化连接池),但 K8s 已经把它标记为就绪,流量打进去就必然报错。livenessProbe(存活探针)决定这个 Pod 是否还活着,如果探针失败了,K8s 会杀掉这个容器重新创建。这两个探针的initialDelaySeconds和periodSeconds参数要根据实际应用调整。不要照抄,要观察你的应用启动时间。
这里推荐 Flask 应用里加一个简单的健康检查接口:
@app.route("/healthz") def healthz(): return {"status": "ok"}更完善的做法是在这个接口里顺便检查依赖的数据库连接是否正常,这样探针能反映出应用的真实健康状态。不过要注意livenessProbe不要用这种带外部依赖的接口,否则数据库一抖动,K8s 就把你的 Pod 杀了重启,反而更不稳定。一般来说:readiness可以检查外部依赖,liveness尽量只检查进程本身。
3.3 Service:集群内部的服务发现与负载均衡
Deployment 管理 Pod,但 Pod 的 IP 是动态分配的,随时会变。你不能让外部访问者记住某个 Pod 的 IP。这时候就需要 Service 来提供一个稳定的访问入口。
Service 有几个类型,最常见的三个:
- ClusterIP:只能在集群内部访问,用于服务间调用。
- NodePort:把服务暴露到每个节点的某个端口上,外部通过
节点IP:NodePort访问。 - LoadBalancer:在云平台上自动创建负载均衡器,把外部流量导入集群。
对于我们的 Flask 应用,最常用的方案是:内部用 ClusterIP 让其他服务访问,外部通过 Ingress 暴露。
apiVersion: v1 kind: Service metadata: name: flask-app namespace: flask-app spec: selector: app: flask-app ports: - port: 80 targetPort: 5000这个 Service 的意思是:集群内部访问flask-app.flask-app.svc.cluster.local:80,会被转发到后端某个 Pod 的 5000 端口。selector负责挑选后端 Pod,它和 Deployment 的 selector 用的是同一套 label。
用 Service 做负载均衡不需要你配置任何算法。K8s 默认用 iptables 或者 IPVS 做轮询转发,这个你不需要关心,也不需要去调。除非你遇到 TCP 长连接在一个 Pod 上聚集的问题,才需要考虑把负载均衡模式换成 IPVS 并开启会话保持。那是后面的进阶内容,这里不展开。
关于port和targetPort的区别,很多人一开始分不清。port是 Service 对外暴露的端口,targetPort是后端 Pod 上容器实际监听的端口。因为 Flask 容器内部监听 5000,所以我写port: 80, targetPort: 5000,这样外部服务访问 Service 的 80 端口就行,不需要知道容器内部的 5000。这种解耦在容器编排里非常常见。
3.4 ConfigMap 和 Secret:配置与敏感信息分离
在 K8s 之前,你可能会把数据库连接串直接写在 Flask 的配置文件里,或者用环境变量手动传。在 K8s 里,更规范的做法是用 ConfigMap 和 Secret 管理配置。
ConfigMap 存普通配置,Secret 存敏感信息(比如密码、密钥、证书)。两者本质都是键值对,只是在 K8s 内部处理方式略有不同(Secret 在 etcd 中是 base64 编码存储,而且可以配置加密)。
使用方式有两种:一种是作为环境变量注入,一种是作为文件挂载到 Pod 里。
环境变量注入适用于简单的键值配置:
apiVersion: v1 kind: ConfigMap metadata: name: flask-config namespace: flask-app data: DB_HOST: "mysql-service" DB_PORT: "3306" APP_ENV: "production"然后在 Deployment 里用valueFrom.configMapKeyRef引用:
env: - name: DB_HOST valueFrom: configMapKeyRef: name: flask-config key: DB_HOST这样做的好处是:修改 ConfigMap 不用重新构建镜像,只需要kubectl apply一下更新后的 ConfigMap,然后滚动重启 Deployment 就能让新配置生效。
文件挂载适用于多行配置,比如 Flask 的config.py或者 Nginx 的nginx.conf。把整个配置文件塞进 ConfigMap 的 data 里,然后挂载到容器的指定路径。
Secret 的使用方式和 ConfigMap 几乎一样,只是创建方式略有不同:
kubectl create secret generic flask-secret \ --from-literal=DB_PASSWORD=yourpassword \ --namespace=flask-app或者用 YAML:
apiVersion: v1 kind: Secret metadata: name: flask-secret namespace: flask-app type: Opaque data: DB_PASSWORD: eW91cnBhc3N3b3Jkdata字段的值必须是 base64 编码后的字符串。你可以用echo -n 'yourpassword' | base64生成。这里有个细节:不要把 Secret 文件直接提交到 Git 仓库里。哪怕是私有仓库也不行。一旦泄露,你所有环境的安全防线都崩塌了。更合理的做法是:用 Helm、Kustomize 这类工具做模板渲染,把实际密钥放在部署的时候通过命令行参数或者云端密钥管理器注入。
3.5 Ingress:统一的外部访问入口
Service 的 NodePort 虽然能暴露服务,但端口号是一万以上的大随机数,而且每个 Service 都要占用每个节点的端口,实际使用非常别扭。更优雅的方案是 Ingress。
Ingress 是 K8s 集群的七层负载均衡器,它根据域名和路径把外部 HTTP 请求路由到集群内部的 Service。
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: flask-app namespace: flask-app spec: rules: - host: flask.example.com http: paths: - path: / pathType: Prefix backend: service: name: flask-app port: number: 80有了 Ingress,外部访问flask.example.com的请求会先进 Ingress Controller(通常是一个 Nginx),然后根据 host 匹配规则转发给后端的 flask-app Service。
Ingress 只有声明是不够的,集群里必须运行一个 Ingress Controller。最常见的实现是 Ingress-Nginx Controller 或者云厂商托管的 Ingress 插件。没有 Controller,你写再漂亮的 Ingress 资源也没用。这就有个常识:Ingress 资源只是路由规则,真正执行转发策略的是 Ingress Controller 这个组件。很多新手以为建好 Ingress 就能用了,结果发现访问不通,查了半天才发现集群里根本没装 Controller。
此外,Ingress 还需要处理 HTTPS 证书。常用的做法是:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: flask-app namespace: flask-app spec: tls: - hosts: - flask.example.com secretName: flask-tls-secret rules: - host: flask.example.com http: paths: - path: / pathType: Prefix backend: service: name: flask-app port: number: 80证书存在 Secret 里,K8s 会自动把证书挂载到 Ingress Controller。证书的获取可以用 cert-manager 配合 Let's Encrypt 自动申请续期,这是另一个专题,不在这里展开。作为入门,先用自签名证书或者云平台的免费证书把 HTTPS 跑通就够了。
4. 自动化管理实战:滚动更新与弹性伸缩
4.1 滚动更新机制与发布参数
说到自动化管理,第一个能力就是发布。传统部署方式发新版本,通常要先停服再更新,或者手动执行脚本切换流量。K8s 的滚动更新机制把这个过程完全自动化了。
你只需要做一件事:更新 Deployment 里的镜像 tag,然后 apply 一下。
kubectl set image deployment/flask-app flask-app=registry.example.com/flask-demo:v1.1 --namespace=flask-app或者更规范的做法:修改 YAML 文件里的image字段,然后kubectl apply -f deployment.yaml。
K8s 会按照 Deployment 配置的更新策略分批替换旧版本 Pod。默认策略是RollingUpdate,参数有maxUnavailable和maxSurge:
maxUnavailable:更新过程中允许最多几个 Pod 不可用。默认值是 25%。如果你的副本数是 4,那最多允许 1 个 Pod 处于不可用状态。maxSurge:更新过程中允许最多可以多创建多少个 Pod(超出期望副本数)。默认值也是 25%。副本数是 4,最多多创建 1 个。
这两个参数决定发布速度和安全性的平衡。数值越大发布越快,但可用性窗口越窄。生产环境建议保持默认值,除非你的应用启动很慢(这时候需要调大maxSurge来保持容量,同时调小maxUnavailable来确保有足够实例可用)。
滚动更新的关键前提是:你的探针配置正确。如果readinessProbe没配好,新 Pod 启动后虽然进程起来了,但还没准备好就收到了流量,用户就会看到 502。如果livenessProbe配得太严格,新 Pod 启动慢了一点就被杀掉,形成不停重启的循环。发布是否平滑,很大程度上取决于探针配置是否贴合你的应用启动时间和依赖关系。
发布的同时,建议配合kubectl rollout status观察状态:
kubectl rollout status deployment/flask-app --namespace=flask-app这条命令会一直等待发布完成,直到所有 Pod 都更新到新版本。如果中途发现问题,可以用:
kubectl rollout undo deployment/flask-app --namespace=flask-app瞬间回滚到上一个版本。这一点是传统部署方式完全无法比拟的。就算你发布了错误配置,回滚也只需要一秒。
4.2 利用 HorizontalPodAutoscaler 实现弹性伸缩
Deployment 的副本数是手动指定的,但生产环境的流量不是恒定的。早晚高峰流量上去了,你需要更多的实例;深夜流量下来了,你需要少一些实例省成本。手动改replicas当然可行,但更智能的做法是使用 HPA(HorizontalPodAutoscaler)。
HPA 的核心原理是:它通过 Metrics API 读取 Pod 的资源使用指标(比如 CPU 利用率),然后根据你设定的目标值,计算出需要的副本数,自动调整 Deployment 的replicas字段。
一个针对 Flask 应用的 HPA 配置:
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: flask-app-hpa namespace: flask-app spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: flask-app minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 50这个配置的意思是:保持所有副本的 CPU 平均利用率在 50% 左右。高于这个值就扩容,低于这个值就缩容。副本数最少 2 个,最多 10 个。
HPA 的计算公式是这样的:
期望副本数 = ceil(当前副本数 × (当前指标值 / 期望指标值))举个例子:当前有 3 个副本,每个平均 CPU 利用率是 80%,期望值是 50%,那么期望副本数 = 3 × (80 / 50) = 4.8,向上取整为 5。HPA 会自动把replicas改成 5。
这里有两点需要注意:
指标延迟。HPA 依赖 Metrics Server 收集指标数据,数据采集有延迟,通常 15 到 30 秒。所以流量突然暴涨的时候,扩容是滞后于请求量的。这就意味着HPA 不能替代容量规划,它只是在你有基础容量的前提下做弹性调整。如果你的应用 3 秒就撑不住了,等 HPA 扩容回来可能已经雪崩了。这种情况需要的是预留足够的 base 容量,或者用 KEDA 这类基于事件驱动的自定义伸缩器做更快速的反应。
缩容有冷却时间。K8s 为了避免指标抖动导致频繁扩缩容,默认有一个--horizontal-pod-autoscaler-downscale-stabilization-window参数,默认值 5 分钟。意思是你缩容到目标值之后,至少在 5 分钟内不会再触发新的缩容。这是保护机制,防止用户看到 HPA 疯狂地加加减减。
如果你希望 HPA 基于更多指标(比如 QPS、请求延迟)扩容,K8s 也支持自定义指标,但这就需要你部署 Prometheus Adapter 把 Prometheus 的指标暴露给 K8s Metrics API。这个是进阶玩法,可以作为后续学习方向。
4.3 与 CI/CD 流水线衔接发布流程
说到自动化管理,不能不提 CI/CD。K8s 自动化管理里的闭环是:代码提交 -> 自动构建镜像 -> 自动推送到仓库 -> 自动更新 K8s 工作负载。
一个常见的 GitHub Actions 流水线大概长这样:
name: Build and Deploy on: push: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Login to Registry uses: docker/login-action@v3 with: registry: registry.example.com username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . push: true tags: | registry.example.com/flask-demo:${{ github.sha }} registry.example.com/flask-demo:latest - name: Deploy to Kubernetes run: | kubectl set image deployment/flask-app \ flask-app=registry.example.com/flask-demo:${{ github.sha }} \ --namespace=flask-app这个流水线的关键在于:
镜像 tag 使用 commit SHA。这样做的好处是每个提交对应一个唯一镜像版本,可追溯。生产环境不建议用latest作为发布版本,因为无法区分新旧。当然,这个流水线里需要你在 GitHub Actions 的 runner 上配置 kubeconfig 才能执行kubectl命令,生产环境建议用 GitOps 方式(比如 ArgoCD 或 Flux),但这已经超出这篇文章的范围了。这里先把流程跑通,MVP 就有了。
5. 监控与排障:没有这一环谈不上自动化管理
5.1 核心监控指标与日志收集
应用部署上去了,但如果出了问题你不知道,等于白部署。K8s 环境里,监控和日志收集是自动化管理的放大器。
资源监控层面,最基础的是看 CPU 和内存使用。HPA 依赖的 Metrics Server 本来就采集这些数据,你可以用kubectl top直接查看:
kubectl top pods --namespace=flask-app这会列出每个 Pod 的 CPU 和内存实际使用量。如果某个 Pod 内存持续接近limits上限,你要警惕它是不是有内存泄漏问题。
想看更细粒度的指标(请求延迟、错误率、QPS),就需要上 Prometheus + Grafana。Prometheus 负责采集和存储指标,Grafana 负责可视化。Flask 应用需要暴露 Prometheus 指标接口,通常用prometheus-flask-exporter这个库:
from prometheus_flask_exporter import PrometheusMetrics metrics = PrometheusMetrics(app) # 自动收集请求延迟、错误码、请求量等指标关于日志,K8s 的设计哲学是:应用只需要把日志写到 stdout(标准输出),K8s 会自动把每个容器的 stdout 收集到节点层的日志文件里。然后你通过kubectl logs查看:
kubectl logs -f deployment/flask-app --namespace=flask-app如果你需要日志的集中收集和检索(特别是在多个 Pod 副本的情况下,你不可能一个一个去翻日志),就需要在集群里部署一个 DaemonSet 来收集所有节点上的容器日志,转发到 ElasticSearch 或者 Loki 这类集中存储里。这个属于日志基础设施的建设,小规模可以先不做,等应用多了再考虑。
5.2 与 FastAPI 的比较:关于框架选择的冷思考
写这篇部署文章的时候,热词里出现了“Flask 与 FastAPI 比较”,我相信很多人选型时卡在这里过。聊几句我个人的观察,顺便带出一点部署层的思考。
Flask 的优势是简洁、生态成熟、上手门槛低。它的同步模型在大多数业务系统场景下完全够用,配合 Gunicorn 的多 worker 也能撑起不小的并发。FastAPI 的优势是异步原生、类型提示友好、自动生成 OpenAPI 文档,性能更好。
但站在 K8s 部署的角度,这两个框架部署到 K8s 的路径和复杂度几乎没有区别。都是构建镜像,写 Deployment,配置探针,套 Service,Ingress,监控日志。异步框架原生性能好,但你的应用如果大量阻塞在数据库查询上,异步带来的收益也有限。
我的观点是:选框架看你的团队背景和业务形态,不要为了一个异步特性去押宝一个没什么经验的框架。Flask 的同步模型让你更容易理解请求-响应的生命周期,出问题的时候也更好排查。等你把异步、协程这些概念吃透了,再切 FastAPI 也不迟,毕竟部署路径你已经走通了。把应用从 Flask 迁到 FastAPI 的代码改动量通常不大,两边都是 Python WSGI 生态(FastAPI 也兼容 WSGI 模式)。
另外说一句,网上经常能看到拿《深入理解 Kubernetes 源码》这种书来背书的情况。我的建议是:部署使用阶段,看源码不是你的第一优先级。先把 Deployment、Service、Ingress、HPA 这些 API 对象用熟,把 kubectl 操作练到条件反射,再去了解 kubelet 怎么干活、controller-manager 里各个 controller 的循环逻辑长什么样,会轻松得多。源码是进阶方向,不是入门起点。书可以买来放在手边,但不要指望读了它你就能直接搞定部署,那是本末倒置。
6. 常见问题排查实录与速查表
部署 K8s 和部署传统虚拟机完全是两种心智模型,我实践中遇到过很多坑,这里挑几个最高频的写下来。这些问题如果你没遇到过,觉得没用,欢迎过两个月再回来看。
6.1 镜像拉取失败
现象:Pod 一直处于ImagePullBackOff状态,kubectl describe pod能看到拉取镜像失败的错误。
排查思路:
- 确认镜像 tag 是否存在。如果是私有仓库,确认镜像是否已经 push 成功。
- 确认节点能否访问镜像仓库。如果内网环境,可能需要配置镜像仓库的 internal endpoint。
- 确认
imagePullSecrets是否正确配置。私有仓库的认证信息没配,必然拉取失败。 - 检查镜像名拼写。像
registry.example.com/flask-demo:v1.0这种路径格式,少一个斜杠都可能导致解析错误。
用kubectl describe pod查看事件是 K8s 排障的第一步,这个命令要麻木练到条件反射。不懂就看,看多了就熟。
6.2 CrashLoopBackOff:应用一直启动失败
现象:Pod 反复启动又退出,状态是CrashLoopBackOff。
排查思路:
首先要明确:CrashLoopBackOff 的意思是 K8s 尝试启动容器,但容器每次启动后很快就退出,导致 K8s 不断重启它,而且重启间隔在指数退避。造成这种情况的原因大多是应用本身的问题。
- 先用
kubectl logs看应用日志,最常见的错误是启动时连不上数据库、配置读取失败、端口被占用。 - 确认 CMD 命令是否正确。如果你用的 Gunicorn,检查 wsgi 模块路径是否写对。Flask 的入口文件如果是
app.py,里面定义了app = Flask(__name__),Gunicorn 参数应该是app:app,而不是app:flask_app。 - 如果应用在本地 Docker 能跑,但 K8s 里启动失败,重点检查环境变量是否通过 ConfigMap/Secret 正确注入。可以临时在 Deployment 里加一条
command: ["/bin/sh", "-c", "env"]的启动命令让容器启动后打印环境变量,方便比对。
一个非常隐蔽的坑:如果 Flask 应用监听的是127.0.0.1:5000,容器里外面根本访问不到。Gunicorn 的--bind参数要写成0.0.0.0:5000,不然探针检查也失败。这种问题经常出现在本地开发没问题,但容器里就各种诡异的场景里。
6.3 Service 选不到 Pod,或者服务间调用失败
现象:Service 能创建,但在 Pod 内部访问 Service 的 DNS 名字不通,或者访问时报 502。
排查思路:
- 确认 Service 的
selector和 Pod 的labels是否匹配。kubectl get endpoints能直接看到 Service 后面有没有后端 Pod。如果 endpoints 列表为空,说明 selector 写错了。 - 确认
targetPort是否写对。要转发到容器内实际的监听端口,而不是 Service 的 port。 - 查看 Service 的
kubectl describe service,看 Endpoints 部分列举了哪些 IP。如果有 Pod 处于 CrashLoopBackOff,Service 会把它从后端移除,但 endpoints 会显示为空或只有健康副本。
实用的排查命令清单一并给你:
kubectl get pods -n flask-app -o wide kubectl logs -f deployment/flask-app -n flask-app kubectl describe pod <pod-name> -n flask-app kubectl get endpoints -n flask-app kubectl get svc -n flask-app -o yaml kubectl exec -it <pod-name> -n flask-app -- curl -v http://flask-app.flask-app.svc.cluster.local:80/healthz6.4 探针配置引发的血案:健康检查接口的写法坑
现象:应用明明正常,但 Pod 一直被重启,或者流量总是报 503。
排查思路:
这个坑我在生产环境踩过很痛的一回。当时我们的 Flask 应用确实“活着”,但健康检查接口/healthz里有一步是检查数据库连接池是否可用。那段时间数据库偶发连接超时,导致健康检查接口有时候返回 500,然后 K8s 的 liveness 探针判断容器不健康,直接杀掉了 Pod。杀完之后数据库连接恢复正常了,但服务已经重启了,所有请求都断了。
教训是:livenessProbe不要检查外部依赖。存活探针只应该检查进程本身是否活着,否则外部子系统一抖动,你的应用就被误杀。readinessProbe可以检查外部依赖,因为就绪探针失败只会把 Pod 从负载均衡池里摘掉,不会杀进程。这是一个很容易忽视但影响巨大的设计原则。
如果你想让健康检查更完善,可以考虑拆成两个不同的接口:一个只检查进程存活的/healthz给 liveness 用,一个检查依赖的/readiness给 readiness 用。
6.5 常见问题速查表
| 问题现象 | 可能原因 | 排查命令 | 建议 |
|---|---|---|---|
| ImagePullBackOff | 镜像拉取失败 | kubectl describe pod | 检查镜像名/tag/仓库认证 |
| CrashLoopBackOff | 应用启动失败或启动后崩溃 | kubectl logs | 看容器日志,检查配置和环境变量 |
| Pending 状态 | 调度失败、资源不足 | kubectl describe pod | 检查节点资源和 requests 是否过大 |
| Service 不通 | selector 不匹配 | kubectl get endpoints | 比对 labels 和 selector |
| 探针导致重启 | liveness 检查了外部依赖 | kubectl describe pod | 把 liveness 改为只检查进程本身 |
| 502/504 | Service 后端 Pod 未就绪或负载过高 | kubectl get endpoints | 检查 readinessProbe 和资源 limit |
7. 写在最后:部署只是开始,自动化管理是一个系统
这篇文章从 Flask 应用的容器化开始,走完了构建镜像、编写 Deployment、配置 Service、定义 Ingress、挂载 ConfigMap/Secret、设置 HPA、接入 CI/CD 的整个链路。看起来步骤很多,但它的核心思想其实只有一句:把你的应用声明成期望状态,剩下的交给 Kubernetes 自己去达成。
我个人在实际操作中的体会是:K8s 最大的门槛不是它的概念多,而是它要求你换一种思考方式。以前跑传统服务器,你关心的是一台机器上的进程管理、文件系统、环境变量。现在跑 K8s,你关心的是如何把一切状态外部化:配置放到 ConfigMap 里,密钥放到 Secret 里,日志打到 stdout 里,数据存到外部存储里。只当一个纯粹的无状态计算单元,K8s 才能放心地帮你加加减减,不停扩容缩容,随时销毁重建。
最后再分享一个小技巧:学习 K8s 不要只盯着它本身的 YAML,一定要配合一套完整的工具链来用。我强烈建议你本地装一个 kind(Kubernetes in Docker)或者 minikube,把文章里所有 YAML 在本地集群里亲手跑一遍。虚拟机上的模拟永远比不上真实集群里的 Debug 体验。你把kubectl describe、kubectl get events这些命令练熟了,再上生产就是水到渠成。这篇文章写的部署方式不需要你背,照着抄一遍、跑一遍、坏一遍、修一遍,才能算真正学会。