009、容器编排实战:Kubernetes上的Python服务

张开发
2026/4/12 21:51:34 15 分钟阅读

分享文章

009、容器编排实战:Kubernetes上的Python服务
009、容器编排实战Kubernetes上的Python服务一、从一次深夜告警说起上周三凌晨两点手机突然狂震。监控显示线上某个Python服务的P99延迟飙到了5秒但CPU和内存曲线却平静得像条直线。登录集群一看Pod状态全是Running日志里连个Error都没有。直觉告诉我这肯定不是代码问题——服务在本地和测试环境跑得飞快。用kubectl describe pod看了一眼发现所有Pod的Ready状态都在反复横跳Readiness探针时不时失败。问题就出在这里我们的探针配置用的是HTTP GET /health而那个健康检查接口里不小心调用了数据库查询。当晚数据库某个从库网络波动导致健康检查偶尔超时K8s认为Pod“不健康”就把流量切走了。剩下的Pod压力激增队列堆积延迟自然就上去了。这件事让我重新审视在K8s上跑Python服务的细节——容器编排不是把镜像扔进去就能自愈的里面全是魔鬼。二、Python镜像别从latest开始很多人喜欢偷懒Dockerfile第一行就写FROM python:latest。这玩意儿在线上就是个定时炸弹。# 坏例子别这样写 FROM python:latest RUN pip install -r requirements.txt COPY . . CMD [python, app.py]问题在于latest标签今天可能是3.12明天就变3.13版本突变可能直接搞崩你的依赖基础镜像太大默认的python镜像包含一堆你用不着的工具上传下载慢安全漏洞还多建议用具体版本号并且选slim版本# 靠谱写法锁死版本用alpine或slim FROM python:3.11-slim # 系统依赖单独装避免apt update和pip冲突 RUN apt-get update apt-get install -y \ gcc libpq-dev --no-install-recommends \ rm -rf /var/lib/apt/lists/* # 先单独拷贝依赖文件利用Docker缓存层 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 再拷贝代码 COPY . . # 非root用户运行安全第一 RUN useradd -m -u 1000 appuser chown -R appuser:appuser /app USER appuser CMD [gunicorn, app:app, -b, 0.0.0.0:8000]这里踩过坑alpine镜像虽然小但有些Python的C扩展编译需要musl库可能会遇到奇怪的兼容性问题。生产环境我更喜欢用debian-slim平衡体积和兼容性。三、Deployment配置那些容易忽略的参数写Deployment YAML的时候很多人直接抄模板这几个参数特别容易掉坑apiVersion:apps/v1kind:Deploymentspec:replicas:3strategy:type:RollingUpdaterollingUpdate:maxSurge:1maxUnavailable:0# 保证至少有一个Pod在服务避免中断template:spec:containers:-name:apiimage:your-python-app:v1.2.3ports:-containerPort:8000resources:requests:memory:256Micpu:100mlimits:memory:512Micpu:500mlivenessProbe:httpGet:path:/healthport:8000initialDelaySeconds:30# 给应用启动留足时间periodSeconds:10timeoutSeconds:3# 别设太长默认1秒就行failureThreshold:3readinessProbe:httpGet:path:/readyport:8000initialDelaySeconds:5periodSeconds:5successThreshold:1failureThreshold:2lifecycle:preStop:exec:command:[sh,-c,sleep 10]# 给正在处理的请求留出退出时间重点说下探针配置livenessProbe失败会重启Pod。检查逻辑要轻量千万别在里面调数据库或外部API否则网络抖动一下你的Pod就重启狂欢了readinessProbe失败只是把Pod从Service的Endpoint里摘掉。这里可以检查依赖状态比如数据库连接池是否就绪两个探针的path最好分开/health做存活检查只返回200/ready做就绪检查检查依赖项四、Python应用的特殊处理4.1 Gunicorn worker数量很多人直接写CMD [gunicorn, -w, 4, ...]worker数写死。但在K8s里Pod的CPU limit可能随时调整。# 在Deployment的env里动态计算env:-name:WORKERS_PER_COREvalue:2-name:MAX_WORKERSvalue:8-name:WEB_CONCURRENCYvalue:1# 默认值会被下面的启动脚本覆盖然后在Dockerfile的启动脚本里#!/bin/bash# 根据CPU limit计算worker数cores$(nproc)workers$((cores*WORKERS_PER_CORE))workers$((workersMAX_WORKERS?MAX_WORKERS:workers))workers$((workers1?1:workers))execgunicorn app:app-w$workers-b0.0.0.0:80004.2 优雅关闭Python服务收到SIGTERM后Gunicorn默认会等所有worker处理完当前请求但K8s的terminationGracePeriodSeconds默认只有30秒。如果有些长请求超时Pod会被强制杀掉。# 在Flask/FastAPI里加个优雅关闭钩子importsignalfromappimportappdefhandle_shutdown(signum,frame):# 标记服务不可用app.config[SHUTDOWN]True# 这里可以加个等待逻辑比如等10秒time.sleep(10)signal.signal(signal.SIGTERM,handle_shutdown)同时把Deployment的terminationGracePeriodSeconds调到60秒以上。五、ConfigMap和Secret管理配置别把配置写死在代码里也别打进镜像。Python应用的环境变量读取有个细节# config.pyimportosfromdotenvimportload_dotenv load_dotenv()# 本地开发用.env文件classConfig:DB_HOSTos.getenv(DB_HOST,localhost)DB_PORTos.getenv(DB_PORT,5432)# 敏感信息用SecretDB_PASSWORDos.environ[DB_PASSWORD]# 故意不设默认值让它在缺失时直接报错K8s里这样挂载env:-name:DB_HOSTvalueFrom:configMapKeyRef:name:app-configkey:db-host-name:DB_PASSWORDvalueFrom:secretKeyRef:name:db-secretkey:password如果配置项很多也可以整个文件挂载volumes:-name:config-volumeconfigMap:name:app-configcontainers:-volumeMounts:-mountPath:/app/configname:config-volume六、本地调试技巧在本地用kubectl debug其实很方便# 如果Pod起不来进容器看看kubectl run-it--rmdebug-pod\--imagebusybox\--restartNever\--sh# 或者直接附加到已有Pod需要EphemeralContainers特性kubectl debug pod/myapp-xxx-it--imagepython:3.11-slim# 端口转发到本地kubectl port-forward pod/myapp-xxx8000:8000但更推荐用telepresence能把本地进程“嫁接”到K8s集群里直接使用集群内的Service和ConfigMap。七、监控和日志Python服务在K8s里打日志要注意别写文件直接打到stdout/stderr让Docker收集日志里带上request_id方便追踪链路用json格式输出方便ELK解析importjsonimportloggingclassJsonFormatter(logging.Formatter):defformat(self,record):log_record{time:self.formatTime(record),level:record.levelname,message:record.getMessage(),module:record.module,request_id:getattr(record,request_id,none)}returnjson.dumps(log_record)# 在Flask里用app.before_requestdefset_request_id():g.request_idrequest.headers.get(X-Request-ID,str(uuid.uuid4()))在Deployment里加上sidecar收集日志也行但大多数情况下用DaemonSet模式的Fluentd/Filebeat更省资源。八、个人经验包镜像标签别用latest生产环境一定要用具体版本号并且做好版本回滚预案。我习惯用git commit短哈希做标签一目了然。资源限制一定要设不设limits的Pod就是“噪音邻居”可能吃光节点资源。requests可以设低点limits要留足余量。Python服务内存尤其要关注因为Python进程自己不太会主动释放内存给系统。探针超时设短点默认1秒够用了设太长会拖慢故障发现。但initialDelaySeconds要给足特别是Python冷启动加载模型或连接池的时候。本地要有minikube或kind环境别直接在线上集群试配置。我本地常备一个kind集群YAML改完先在这里跑一遍。Python依赖锁死版本requirements.txt里别出现flask2.0.0这种范围依赖不同时间构建的镜像可能装到不同版本导致线上行为不一致。关注文件描述符限制Python的Gunicorngevent模式可能开大量连接默认的1024不够用。在Dockerfile里加一句RUN ulimit -n 65535其实没用容器启动时会重置要在Pod的securityContext里设securityContext:sysctls:-name:fs.file-maxvalue:65535别迷信HPA自动扩缩容听起来美好但Python服务启动慢特别是要加载机器学习模型时等Pod起来流量高峰可能都过去了。有时候预先多部署几个副本反而更稳。在K8s上跑Python服务更像是一门平衡艺术——既要利用容器编排的弹性又要照顾Python生态的脾气。配置项多试几次监控多看几天慢慢就能摸清你那个服务的“性格”了。记住没有放之四海而皆准的模板只有适合你业务场景的配置。

更多文章