前两篇文章我们把 Flask 应用的开发环境和容器化都捋顺了,镜像能跑、端口能通、依赖也没问题,但这只是万里长征走完了前半程。真正让项目从“能在本地跑”进化成“能稳定对外提供服务”,还差最关键的一步:把它扔进 Kubernetes 集群里,让集群帮忙扛起自动化管理这面大旗。
这篇基于我实际部署 Flask 项目的经验,按当前的 K8s 主流版本(1.2x 系列)来写。内容不是给 K8s 小白做科普,而是写给我们这种“Flask 开发为主、K8s 用来解决问题的实践型选手”,默认你了解 Pod、Deployment、Service 这些基础概念。目标是让你看完这篇文章,能自己写出一套可落地的 Flask on K8s 部署方案,并且真正理解每个配置项背后的原因。
1. 部署 Flask 到 K8s:先想清楚这一步到底要解决什么问题
1.1 从单机部署到 K8s 的典型演进路径
我见过不少 Flask 项目的部署形态演变,基本都走的是同一条路:最开始开发完,扔到一台云服务器上,配好 nginx + gunicorn + supervisord,搞定。后来用户量上来,发现这台服务器挂了服务就全挂了,于是引入多台服务器,前面放个负载均衡,后端挂几台 Flask 实例。再往后,每次发版要登录服务器、拉代码、重启进程,精神高度紧张,一个操作失误就是事故。
这时候大家开始拥抱容器化,用 docker-compose 管理多个容器。这比裸奔好很多,但 compose 只能管“单机上的容器”,它不具备跨节点的调度、自愈、自动扩缩容能力。比如某台机器 CPU 飙高,compose 不会帮你在另一台机器上多起一个实例;某个容器挂了,它可以 restart,但如果整台宿主机挂了,上面所有的容器就都跟着没了。
K8s 解决的核心问题正是这个:不管底层有多少台机器、哪些机器挂了、资源怎么分配,你只需要声明期望状态,集群会自己把实际状态往期望状态上拉。这种“声明式管理 + 控制器循环”的机制,就是自动化管理的底层逻辑。我当初读《深入理解 Kubernetes 源码》时感触特别深,Deployment 控制器本质上就是一个无限循环,不断比较现实状态与期望状态,然后调谐。理解了这一点,你配置出来的 YAML 才是活的,而不是抄来的模板。
1.2 Flask 这类 Web 框架和 K8s 配合为什么这么顺
很多人纠结“我的项目适不适合上 K8s”,我的判断标准很简单:应用是否无状态、是否能水平扩展。标准 Flask 应用在这种维度下是极度友好的。
Flask 本身是 WSGI 同步框架,处理一次请求就是“接收→执行视图函数→返回”,不持有连接级状态(只要你不把数据写到本地磁盘,或者把 Session 存在进程内)。这就意味着你可以随时杀掉任意一个副本,再拉起一个新副本,对服务无感知。而 K8s 的滚动更新、故障自愈、HPA 扩缩容,全部建立在“副本随时可替换”这个前提上。
另外,总有人拿 FastAPI 和 Flask 比较。FastAPI 有异步加持,在 IO 密集型场景下吞吐确实更好,但部署方式跟 Flask 在 K8s 里几乎没有差别。两者都是跑 WSGI/ASGI 服务,都是容器化后暴露端口,都由 Deployment 管副本。选型影响的是单实例性能上限,而 K8s 解决的是整体集群容量问题。比如一个 Flask 实例只能扛 200 QPS,我给你扩到 20 个实例,总量就能到 4000 QPS;FastAPI 一个实例能扛 800 QPS,但这并不能让部署架构变简单。所以结论是:哪怕你的 Flask 性能一般,K8s 也能帮你把整体吞吐量抬起来,这才是自动化管理的真正价值。
2. 开工前的准备工作:容器镜像和 Flask 应用侧改造
2.1 Flask 应用在进集群前必须做的几个改动
很多人在本地跑 Flask 跑得好好的,直接 build 镜像丢进 K8s,然后踩一堆坑。我总结下来,进集群前 Flask 应用至少要完成 4 个改造点。
第一,所有配置一律走环境变量。本地开发时写 config.py 没问题,但镜像一旦打进集群,你不可能为每个环境重新 build 一次。数据库地址、Redis 连接、密钥、日志级别,全从 os.environ 读取,用环境变量注入。配合 ConfigMap 和 Secret 在部署层管理这些值,镜像做到一次构建、到处运行。
第二,必须添加一个独立的健康检查端点。比如 /healthz,只在里面返回一个简短的 JSON。K8s 探针会定期请求这个端点,判断容器是否存活、是否可以对外提供服务。这个端点别跟业务接口混在一起,否则健康检查时还要查数据库、读缓存,一但依赖的下游服务抖动,探针就误判,导致容器被杀掉重启,这就是我们说的“探针误杀”。
# health.py from flask import Blueprint, jsonify health_bp = Blueprint('health', __name__) @health_bp.route('/healthz') def healthz(): # 这里不要做任何 IO 操作,保持轻量 return jsonify({'status': 'ok'}), 200第三,日志必须输出到 stdout,而不是写到文件。容器里的文件系统是临时的,pod 崩溃后日志就丢了。容器标准输出会被 K8s 收集,配合 logspout 或 Loki 等工具可以统一采集。写文件日志这种事,在容器世界里就忘掉它吧。
第四,生产环境不允许用 flask run 启动。flask run 是开发服务器,性能和并发都没有保障。生产要用 gunicorn 起多 worker,我这里写一个建议的启动命令,细节后面解释:
gunicorn --bind 0.0.0.0:8000 --workers 4 --threads 2 --timeout 30 --graceful-timeout 15 app:app2.2 Dockerfile 的编写要点
Flask 应用的 Dockerfile 其实很标准,但有三条我特别想强调。
第一,尽量用多阶段构建,但别为了炫技而过度设计。对纯 Python 项目来说,依赖就是 pip 安装的那些包,不需要像 Go 那样编译完再拷贝二进制。多阶段构建在 Python 项目里价值有限,我更倾向于直接用 slim 基础镜像,一条 Dockerfile 到底,但一定要把依赖安装层和代码拷贝层分离,利用 Docker 层缓存:
FROM python:3.11-slim ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 WORKDIR /app # 先只拷贝依赖清单,利用缓存 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 再拷贝项目代码 COPY . . RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app USER appuser EXPOSE 8000 CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "--threads", "2", "--timeout", "30", "--graceful-timeout", "15", "app:app"]第二,千万不要用 root 用户跑容器。我用的是useradd创建普通用户,然后USER appuser切换。安全方面的理由不赘述,单说一个运维上的痛点:root 启动的进程如果被入侵,整台节点的权限都危险;而 K8s 里经常配合 PodSecurity 策略限制容器必须以非 root 运行,写镜像时就加上能省掉后面一堆麻烦。
第三,时区问题。很多 Flask 应用里会用到datetime.now()写日志、记录时间字段。默认容器时区是 UTC,如果你的业务强依赖东八区时间,请在 Dockerfile 里加上时区设置:
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' > /etc/timezone我在这里踩过一次很典型的坑:定时任务在凌晨跑,我以为是程序 bug,排查半天发现是容器时区差 8 个小时。
3. 核心资源编排:从 Deployment 到 Service,每个字段都别乱填
3.1 Deployment 配置细节:期望状态是一切自动化管理的基础
Deployment 是整个 Flask 应用在 K8s 里的“主心骨”。很多人写 Deployment 就是网上抄一份模板,改个镜像名就 apply,一旦出问题就抓瞎。我建议你逐字段理解。
下面这份 YAML 是我实际在用的 Flask 部署配置,去掉敏感信息后大致长这样:
apiVersion: apps/v1 kind: Deployment metadata: name: flask-app namespace: production labels: app: flask-app spec: replicas: 3 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: flask-app template: metadata: labels: app: flask-app spec: terminationGracePeriodSeconds: 30 containers: - name: flask-app image: registry.cn-hangzhou.aliyuncs.com/your-namespace/flask-app:1.2.0 imagePullPolicy: IfNotPresent ports: - containerPort: 8000 resources: requests: cpu: 250m memory: 256Mi limits: cpu: 500m memory: 512Mi envFrom: - configMapRef: name: flask-app-config - secretRef: name: flask-app-secret livenessProbe: httpGet: path: /healthz port: 8000 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 2 failureThreshold: 3 readinessProbe: httpGet: path: /healthz port: 8000 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 2 successThreshold: 1 failureThreshold: 3几个容易被忽略但影响巨大的细节:
strategy里的maxSurge: 1表示滚动更新时允许额外多起 1 个新 Pod;maxUnavailable: 0表示更新过程中不允许有副本不可用。这两个参数组合起来的效果就是“先起新的,等新 Pod ready 了再接流量,再杀掉旧的”,实现真正意义上的平滑升级。如果你的服务只跑两个副本,我会建议maxSurge: 1, maxUnavailable: 1,这样升级速度更快,但会短暂出现一个副本下线。
terminationGracePeriodSeconds: 30是给应用优雅退出用的宽限期。Flask 应用收到 SIGTERM 信号后,gunicorn 会停止接收新请求,等正在处理的请求执行完再退出。这个时间设置太短,比如默认 30 秒以内还没退出就会被强制 SIGKILL,会导致正在处理中的请求直接断掉。
resources这里我特别提醒:requests 和 limits 不要拍脑袋填。requests 影响的是调度器选节点的依据,limits 是容器实际能用的上限。如果你的limits.memory设得太低,比如 Flask 应用本身内存占用有 300MB,你 limits 给 256Mi,那 pod 会持续处于 OOMKilled 状态,表现为“不停 CrashLoopBackOff”。建议先在本地/测试环境用docker stats观察容器实际内存占用,再按 1.5 倍余量来设 limits。
3.2 Service 和 Ingress:外部流量是怎么打到 Flask 上的
Deployment 负责管 Pod,但 Pod 的 IP 是漂移的,Service 才是稳定的访问入口。这里有一个核心概念叫selector,Service 用 label 匹配帮我们绑定后端 Pod 列表。
apiVersion: v1 kind: Service metadata: name: flask-app-svc namespace: production spec: type: ClusterIP selector: app: flask-app ports: - name: http port: 80 targetPort: 8000port: 80是 Service 对外暴露的端口,targetPort: 8000是容器内部 Flask 监听的端口。集群内部访问这个 Service 的 URL 是http://flask-app-svc.production.svc.cluster.local。
至于外部访问,要看你具体的业务场景。如果是内部系统,ClusterIP 配合集群内 DNS 就够了。如果要暴露到公网,通常用 Ingress。网上聊“kubernetes 部署 nginx”时经常会提到,实际上 Ingress 扮演的角色跟 nginx 反向代理非常像,它负责根据域名/路径把请求路由到不同 Service,而 Ingress 控制器最常见的实现就是 nginx ingress controller。
我的 Ingress 配置通常长这样:
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: flask-app-ingress namespace: production spec: ingressClassName: nginx rules: - host: api.example.com http: paths: - path: / pathType: Prefix backend: service: name: flask-app-svc port: number: 80使用 Ingress 之前记得先确认集群已经安装了对应的 ingress controller。没装 controller 的话,Ingress 资源创建了也形同虚设,流量根本不会转发。
3.3 ConfigMap 和 Secret:把配置从镜像里“抠”出来
我在 2.1 提到 Flask 应用要从环境变量读配置,那这些环境变量怎么注入到容器里?标准做法就是 ConfigMap 管普通配置,Secret 管敏感信息。
ConfigMap 适合存 SECRET_KEY 这种看似敏感但实际还是要管理的值吗?可以存,但更标准的做法是:非敏感配置如 FLASK_ENV、LOG_LEVEL、服务超时时间放 ConfigMap;数据库密码、Redis 密码、加密密钥等敏感信息放 Secret。Secret 里的值是 base64 编码的,但注意它只是编码不是加密,不要有“放进去就安全了”的错觉。生产上要配合 encryption at rest 或外部密钥管理工具(比如 Sealed Secrets、Vault)来用。
apiVersion: v1 kind: ConfigMap metadata: name: flask-app-config namespace: production data: FLASK_ENV: production LOG_LEVEL: info DB_HOST: mysql.production.svc.cluster.local CACHE_URL: redis://redis-svc:6379/0 # 不要把密码放在这里 --- apiVersion: v1 kind: Secret metadata: name: flask-app-secret namespace: production type: Opaque data: SECRET_KEY: bXlzZWNyZXRrZXk= DB_PASSWORD: bXlzcWxwYXNz然后在 Deployment 里用envFrom一次性注入,很方便。
4. 自动化管理:让集群帮你干活,而不是你追着集群跑
4.1 HPA 自动扩缩容:让副本数跟上流量节奏
这里要聊的是“自动化管理”最直观的体现——自动扩缩容。上 K8s 之前,流量高峰期你要手动去云控制台加机器加实例;上了 K8s 之后,HPA(HorizontalPodAutoscaler)能帮你按指标自动调整副本数。
先检查你的集群里有没有安装 metrics-server。没有它,kubectl top都跑不了,HPA 也拿不到 Pod 的 CPU 数据。安装方式各云厂商不太一样,通常直接kubectl apply一份 metrics-server 的 yaml 就行。
我实际用的 HPA 配置是 v2 API,可以同时基于 CPU 和内存来做:
apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: flask-app-hpa namespace: production spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: flask-app minReplicas: 3 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 behavior: scaleDown: stabilizationWindowSeconds: 300 policies: - type: Percent value: 50 periodSeconds: 60averageUtilization: 60表示当所有 Pod 的平均 CPU 使用率达到 60% 时,HPA 会开始扩容。扩容算法是targetReplicas = ceil(currentUtilization / targetUtilization * currentReplicas),比如现在副本数是 3,平均 CPU 到了 90%,那目标副本数就是ceil(90/60*3) = 5。
stabilizationWindowSeconds: 300是缩容稳定窗口,意思是连续 5 分钟内 CPU 都低于阈值才允许缩容。这里用百分比策略一次最多缩掉副本总数的 50%,避免流量抖动导致副本数震荡。这个配置在实际业务中非常值钱——防止流量瞬时压过来时“傻乎乎”地直接从 3 个缩到 3 个以下,导致服务抖动。
4.2 滚动更新和快速回滚:发版不再提心吊胆
K8s 的滚动更新配合 readinessProbe,能实现“零停机部署”。流程是:你改了镜像 tag,执行 kubectl rollout restart 或直接 apply,Deployment 控制器就会按 maxSurge/maxUnavailable 的策略逐步替换旧 Pod。
举个实际例子,当前跑的是 1.2.0 版本,我要升到 1.3.0:
kubectl set image deployment/flask-app flask-app=registry.cn-hangzhou.aliyuncs.com/your-namespace/flask-app:1.3.0 -n production或者更推荐的做法,直接修改 YAML 文件里的镜像 tag,然后:
kubectl apply -f deployment.yaml执行之后观察滚动状态:
kubectl rollout status deployment/flask-app -n production # 输出示例 # Waiting for rollout to finish: 1 out of 3 new replicas have been updated... # deployment "flask-app" successfully rolled out如果发现新版本有问题,比如视图函数报错、接口 500,可以马上回滚:
kubectl rollout undo deployment/flask-app -n production回滚到上一个版本就是这么一条命令。如果你要回滚到指定历史版本,先kubectl rollout history deployment/flask-app -n production查看版本号,再kubectl rollout undo deployment/flask-app --to-revision=2 -n production。
这里有一个很关键的经验:要合理使用 rollout history 的限制数。默认保留 10 个历史版本,如果你发布的很频繁,老版本会被清理掉,这时候回滚只能回滚到保留过的版本。想多保留几个版本,可以给 Deployment 加spec.revisionHistoryLimit: 20。
4.3 用 Namespace 和 ResourceQuota 做好“分地治理”
自动化管理的前提是“各归各的”。我不建议你把所有环境的应用全部塞到 default 命名空间。至少分三个:dev、staging、production。命名空间隔离的意义不只是命名不冲突,更重要的是可以让不同环境使用不同的资源配额和权限策略。
比如给生产环境设置资源配额:
apiVersion: v1 kind: ResourceQuota metadata: name: production-quota namespace: production spec: hard: requests.cpu: "20" requests.memory: 40Gi limits.cpu: "40" limits.memory: 80Gi persistentvolumeclaims: 20再配合 LimitRange 强制每个 Pod 必须声明 resource:
apiVersion: v1 kind: LimitRange metadata: name: default-limit-range namespace: production spec: limits: - default: memory: 512Mi defaultRequest: memory: 256Mi max: memory: 4Gi type: Container这套组合拳打下来,开发同学在 production 里创建一个没写 resources 的 Pod,会被 LimitRange 自动填充默认值;整个命名空间累计资源不会被某个团队打满,这也是“自动化管理”的一部分——规则自动化,而不是靠人盯。
5. 实操实录:从零部署一个 Flask 应用到集群的完整流程
5.1 从 YAML 编写到 kubectl apply:一条条命令过
理论讲得再多,不如带着你完整过一遍流程。假设我现在已经写好了 Flask 代码、Dockerfile、构建好镜像,而且集群已经存在,环境变量也有 kubectl 权限。
第一步,创建命名空间:
kubectl create namespace production第二步,按顺序 apply 配置文件:
kubectl apply -f configmap.yaml kubectl apply -f secret.yaml kubectl apply -f deployment.yaml kubectl apply -f service.yaml kubectl apply -f ingress.yaml kubectl apply -f hpa.yaml为什么先 apply ConfigMap 和 Secret 再 apply Deployment?因为 Deployment 里的 envFrom 在创建时就会引用这两个对象。如果 Deployment 先创建,但 ConfigMap 不存在,Pod 会创建失败,报CreateContainerConfigError。先准备好配置对象是最省心的顺序。
第三步,验证 Deployment 是否正常运行:
kubectl get pods -n production # 期望状态 # NAME READY STATUS RESTARTS AGE # flask-app-86c8d7f5d6-abc 1/1 Running 0 2m # flask-app-86c8d7f5d6-def 1/1 Running 0 2m # flask-app-86c8d7f5d6-ghi 1/1 Running 0 2mREADY 列显示 1/1,表示容器内的 readinessProbe 已经通过,Pod 处于可服务状态。如果显示 0/1,就要去看日志:
kubectl logs -n production deployment/flask-app第四步,验证 Service 能正确转发流量。从集群内任意节点或任意 Pod 里测试:
kubectl run curl-test --image=curlimages/curl --rm -it --restart=Never -- -v http://flask-app-svc.production.svc.cluster.local/healthz返回 HTTP/1.1 200,说明 Service 到后端 Pod 的路由没问题。
第五步,验证 Ingress 外部访问。本地测试时你可以把域名解析指到 ingress controller 的节点上,或者用curl -H "Host: api.example.com" http://<ingress-controller-ip>/来测试。
5.2 验证自动扩缩容和滚动更新是否真正生效
部署搞定后,自动化管理的两大核心功能也必须验证。先测 HPA:
起一个压测程序给 Flask 服务加压,比如用 wrk 或 ab:
wrk -t4 -c200 -d120s http://flask-app-svc.production.svc.cluster.local/healthz同时开另一个终端观察:
kubectl get hpa -n production -w # NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE # flask-app-hpa Deployment/flask-app cpu: 120%/60% 3 10 5 5m当 TARGETS 超过 60% 后,REPLICAS 会从 3 开始往上爬,直到 CPU 降下来或者达到 maxReplicas。压测结束后,等 stabilizationWindowSeconds 时间,副本自动回落到 3。
再测滚动更新。把 deployment.yaml 里的镜像 tag 改成 1.3.0,重新 apply:
kubectl apply -f deployment.yaml kubectl rollout status deployment/flask-app -n production你会看到新 Pod 逐个启动,每个新 Pod 的 readinessProbe 通过后,旧的 Pod 逐个被终结。整个过程老服务不掉线,这就是自动化管理里说的“服务可用性”。
6. 在真实环境里踩过的坑:问题排查与避坑速查
6.1 高频问题的现象、原因和解决方案
下面这些是我在这些年部署 Flask 到 K8s 过程中真实遇到过的高频问题,把每个问题的典型现象、根本原因、解决路径都整理出来了:
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| Pod 一直 ContainerCreating,报 ImagePullBackOff | 镜像名写错、tag 不存在、私有仓库未配置认证 | 检查kubectl describe pod事件;私有仓库在 Deployment 里配置 imagePullSecrets |
| Pod 不停 CrashLoopBackOff | 应用启动失败、依赖环境变量未注入、内存超限被杀 | kubectl logs看启动日志;检查 ConfigMap/Secret 名称是否正确;kubectl describe pod看 OOMKilled 状态 |
| Pod 能起来但 READY 0/1 | readinessProbe 持续失败,探针打的路径不对或端口写错 | 手动 curl 容器的 /healthz;检查探针的 port 和 path 是否与容器内一致 |
HPA 不扩容,TARGETS 显示<unknown> | metrics-server 未安装或版本不兼容 | 检查kubectl top nodes是否有数据;按集群版本重新安装 metrics-server |
| 新版本上线后大量请求 502 | 滚动更新期间旧 Pod 被摘除过快,或应用启动太慢 readinessProbe 未通过就接流量 | 调大 initialDelaySeconds;限制 maxUnavailable 为 0;确认 gunicorn 在 30 秒内能完成启动 |
| 所有副本正常但外部无法访问 | Ingress controller 未部署 / Service type 不匹配 / 安全组未放行 | 检查 ingressClassName,确认 controller 的 Pod 是否 Running,检查云安全组入站规则 |
| Flask 上传的文件下一次请求就找不到 | 文件写到本地容器磁盘,Pod 被调度到其它节点就丢失了 | 使用 PVC(PersistentVolumeClaim)挂载共享存储,或改用对象存储服务 |
第一个坑我想单独展开说一下。镜像拉取失败是最常见的入坑原因。私有仓库必须显式告诉 K8s 怎么认证,在 namespace 里创建一个 docker-registry 类型的 secret:
kubectl create secret docker-registry registry-pull-secret \ --namespace=production \ --docker-server=registry.cn-hangzhou.aliyuncs.com \ --docker-username=your-username \ --docker-password=your-password然后在 Deployment 的 template 里加上:
spec: imagePullSecrets: - name: registry-pull-secret第二个要重点说的是 gunicorn worker 数和 CPU limits 的关系。比如你 limits.cpu 设的是 500m(半核),但 gunicorn 起了 8 个 worker,每个 worker 都要占 CPU。借用 CPU 是有代价的:Linux CFS 配额会让 worker 频繁被 throttle,表现为请求延迟高、超时率飙升。所以一个靠谱的经验法则是:workers 数 <= limits.cpu 的核数,每个 worker 至少留 100m CPU。如果 limits 是 500m,workers 建议不超过 2 个;如果你确实需要 4 个 worker,那 limits 至少给到 2 核。
6.2 日志排错的几个核心命令
这部分是非常实用的命令速查。把下面这套练习到肌肉记忆,排查问题效率会大幅提升:
# 看 Pod 最新日志,按时间过滤 kubectl logs -n production deployment/flask-app --tail=100 # 看指定 Pod 的完整事件,定位容器创建失败原因 kubectl describe pod -n production <pod-name> # 进入容器内排查网络或文件问题 kubectl exec -it -n production <pod-name> -- /bin/sh # 实时跟踪 HPA 的扩缩容事件 kubectl get events --sort-by=.lastTimestamp | grep -i hpa # 查看 Deployment 的发布历史,用于回滚决策 kubectl rollout history deployment/flask-app -n production这里还想强调一个 watch 命令的用法。当你kubectl apply -f deployment.yaml后,用kubectl get pods -n production -w实时观察 Pod 状态变化,能第一时间看到新 Pod 是否真起来了、有没有处于 CrashLoopBackOff、Pending 等状态。
6.3 最后的几个经验之谈
- 先写 Deployment,用小规模验证再铺开。我在生产环境恢复过一次因为 YAML 手滑把 rock
app: flask-app``selector写错导致 Service 找不到后端,流量直接 502。先在 staging 跑一遍,再切生产,成本真的很低。 - 别在容器里 ssh、别在容器里装调试工具。容器要的是“简单、可替换”,你应该用日志和事件来分析问题,而不是把容器当成一台可以随意进入的虚拟机。真需要诊断网络问题,用
kubectl run起一个临时 pod 就行。 - 优雅退出一定要测。我见过有的 Flask 应用在 gunicorn 收到 SIGTERM 后不退出,而是继续等新的 HTTP 请求,导致 rolling update 时出现多个容器抢占同一端口、流量被路由到已经不健康的 pod 的情况。在 Dockerfile 里 CMD 直接跑 gunicorn 时记得加上
--graceful-timeout,它是专门处理这个问题的。
K8s 这套东西,光看不练永远学不会。把上面这些配置复制下去,在你自己的集群里先跑通一遍“部署→扩容→更新→回滚”全流程,踩过几个坑之后,你对自动化管理的理解就会跟现在完全不同。