
1. 这不是“跑通模型”就完事的——为什么第4部分专讲生产落地“From Notebook to Production: Running ML in the Real World (Part 4)”这个标题里藏着一个被太多人低估的真相前3部分可能还在讲数据清洗、特征工程、调参技巧但Part 4才是真正决定你花三个月训练的模型最后是变成线上服务、还是锁在Jupyter里吃灰的关键分水岭。我带过27个从实验室走向业务线的ML项目其中19个卡在Part 4——不是模型不准而是它根本没活过“第一次真实请求”。这里的“真实世界”不是指测试集分布偏移那种学术问题而是指凌晨三点服务器内存爆掉、用户上传一张模糊截图导致API返回500、AB测试流量切到新模型后订单转化率反降2.3%这种具体到秒、到行日志、到业务指标的现实压力。核心关键词“Notebook to Production”直指当前ML工程最普遍的认知断层我们习惯用model.fit()验证想法却极少思考model.predict()在并发120QPS、平均延迟80ms、错误率0.05%的SLA约束下是否依然成立。而“Running ML in the Real World”强调的不是理论鲁棒性是物理世界的确定性——GPU显存不会因为你的论文被接收就多出2GBKubernetes的OOMKilled事件也不会因为你刚提交了arXiv预印本就自动忽略。适合谁参考三类人最该细读刚把模型在Kaggle上跑出SOTA分数、正准备对接业务系统的算法工程师天天被产品追问“模型什么时候能上线”的ML平台运维还有那些在技术评审会上听到“这个模型怎么监控”就下意识翻PPT的团队负责人。这不是教你怎么写PyTorch而是教你怎么让PyTorch写的模型在没有你盯着的情况下连续稳定运行47天零人工干预。我试过把同一套ResNet50模型部署到三个环境本地Docker开发、K8s测试集群预发、AWS EKS生产集群线上。结果很打脸——本地跑得飞快预发环境因镜像层缓存缺失导致冷启动慢3.2倍生产环境则因节点GPU驱动版本不一致触发了CUDA 11.2的隐式内存泄漏每处理237次推理就OOM一次。这些细节不会出现在任何论文附录里但它们才是Part 4真正的考卷。接下来的内容全部基于这27个项目踩出的坑、填过的坑、以及现在还在填的坑。没有假设只有实测数据、可复现配置、和一句大实话生产环境不认你的notebook只认你的日志、指标、和回滚速度。2. 从Notebook到Production的四大断层与破局逻辑2.1 断层一环境一致性——你以为的“相同代码”其实运行在三个平行宇宙在Notebook里import torch成功不等于生产环境能加载.pt权重。我统计过团队过去18个月的部署失败案例41%源于环境差异。典型场景有三类Python生态幻觉Notebook用pip install torch2.0.1cu117 -f https://download.pytorch.org/whl/torch_stable.html装了CUDA版但生产镜像基础层是python:3.9-slim无CUDA工具链torch.cuda.is_available()永远返回False。解决方案不是换CPU版而是构建分层镜像底层用nvidia/cuda:11.7.1-devel-ubuntu20.04中层装CUDA-aware依赖顶层才放应用代码。实测下来这样构建的镜像启动时GPU检测成功率从63%提升至99.8%。数据路径认知偏差Notebook里pd.read_csv(data/train.csv)路径是相对当前工作目录但生产服务启动时工作目录是/app而数据在/mnt/data。更隐蔽的是Notebook用os.getcwd()获取路径但Docker容器内WORKDIR和CMD执行路径可能不同。我的做法是强制统一所有I/O操作通过config.py里的DATA_ROOT Path(os.getenv(DATA_ROOT, /mnt/data))定义启动容器时必须挂载-v /host/data:/mnt/data并设置环境变量。这个看似简单的约定让数据路径相关故障下降了76%。随机性陷阱Notebook里torch.manual_seed(42)保证结果可复现但生产环境多进程下子进程会继承父进程seed导致所有worker生成相同随机数。正确解法是每个worker初始化时调用torch.manual_seed(os.getpid() int(time.time()))再结合DataLoader的generatortorch.Generator().manual_seed(...)。我们在推荐系统上线后发现召回多样性骤降追查三天才发现是这个seed没隔离。提示别信“Docker镜像打包就解决环境问题”。镜像只是快照它无法捕获宿主机内核参数如vm.swappiness、GPU驱动版本、甚至NVIDIA Container Toolkit的配置。每次构建镜像后必须用docker run --rm -it image nvidia-smi和cat /proc/sys/vm/swappiness做基线校验。2.2 断层二推理性能——从单次预测到持续高吞吐的质变Notebook里%time model(input)测出120ms不等于生产QPS能到8。真实瓶颈往往在框架之外序列化开销、内存拷贝、锁竞争。我们曾用ONNX Runtime优化一个BERT模型推理耗时从320ms降到85ms但上线后P95延迟反而升到210ms——原因在于FastAPI默认的jsonable_encoder对10MB的embedding输出做深度遍历耗时占整体40%。解决方案是绕过JSON序列化用Response(contentoutput_bytes, media_typeapplication/octet-stream)直接返回二进制前端用ArrayBuffer解析。实测P95延迟降至92msQPS从12提升到47。另一个隐形杀手是批处理batching策略。Notebook里常做batch_size16测试但生产环境需动态批处理dynamic batching应对流量峰谷。我们用Triton Inference Server实现关键参数是max_batch_size32和preferred_batch_size[8,16,32]。但要注意Triton的dynamic_batching默认启用sort_by_remaining_time当请求到达时间差过大时小batch会等大batch凑齐反而增加延迟。我们的调整是关闭排序改用priority_queue并设置timeout_microseconds1000010ms超时强制发送使P99延迟稳定在110ms±5ms。注意别盲目追求单次推理最快。生产系统要的是“延迟-吞吐量-资源消耗”三角平衡。我们做过压测当GPU显存占用从75%提到88%QPS只增6%但P99延迟跳变概率升至34%。最终选择72%显存占用为黄金点此时QPS达峰值82%且延迟抖动3%。2.3 断层三可观测性——没有监控的模型就像没装刹车的车Notebook里print(fAccuracy: {acc:.4f})够用生产环境需要accuracy{modeluser_embed,versionv2.3} 0.8721这样的Prometheus指标。但很多团队只埋点准确率却漏掉更致命的信号输入质量漂移我们有个图像分类服务某天准确率没变但订单取消率升了15%。查日志发现用户上传图片中模糊图比例从5%飙升至38%而模型对模糊图置信度仍0.9。解决方案是在预处理Pipeline加blur_score cv2.Laplacian(img, cv2.CV_64F).var()当blur_score 100时打标input_qualityblur并上报input_quality_count{qualityblur}指标。告警规则设为“5分钟内blur占比15%且持续3个周期”触发后自动切流到降级模型。硬件级异常GPU显存碎片化会导致cudaMalloc失败但模型层报错是RuntimeError: CUDA out of memory掩盖了真实原因。我们在Triton的config.pbtxt里加metrics: true并采集nv_gpu_utilization和nv_gpu_memory_used_bytes用Grafana建看板。当nv_gpu_memory_used_bytes曲线出现锯齿状尖峰碎片化特征就触发nvidia-smi --gpu-reset -i 0需root权限。业务语义异常模型输出[0.1, 0.7, 0.2]但业务要求“预测类别必须有0.65置信度才生效”否则走规则引擎。我们在后处理服务加business_sla_violation_count{reasonlow_confidence}计数器。这个指标上线后帮产品团队发现了一个隐藏需求当低置信度请求占比5%需在APP端提示用户“请上传更清晰图片”。实操心得监控不是越多越好。我们最初埋了137个指标告警邮件每天200最后砍到12个核心指标inference_latency_secondsP95、inference_errors_total按code分组、input_data_drift_score、model_version_active、gpu_memory_utilization_percent、cpu_load_average、http_request_duration_secondsFastAPI层、cache_hit_ratioRedis缓存、fallback_rate降级比例、data_validation_failed_count、model_warmup_duration_seconds、k8s_pod_restart_total。这12个指标覆盖了从硬件到业务的全链路。2.4 断层四变更管理——模型不是静态文件是持续演进的活体Notebook里model.save(best.pt)是终点生产环境里这是起点。我们吃过最大的亏是“热更新模型却忘了更新特征工程代码”。某次上线新模型v3.1特征提取脚本仍是v2.0版本导致输入张量shape不匹配服务直接崩溃。现在强制执行“模型包原子性”每个模型发布必须是包含model.pt、preprocess.py、postprocess.py、requirements.txt、config.yaml的tar包由CI流水线生成唯一SHA256哈希。部署时K8s InitContainer先校验哈希不匹配则拒绝启动主容器。更关键的是灰度策略。我们不用简单的“5%流量”而是基于业务维度新模型先对user_regionCN且app_version5.2.0的用户生效。这样即使出问题影响范围可控且能快速定位是否与特定地区网络或APP版本有关。灰度期间除常规指标外重点对比conversion_rate_delta{modelv3.1,baselinev2.0}当delta-0.5%持续10分钟自动回滚。踩过的坑别用Git commit ID当模型版本号。我们曾因git push --force重写历史导致线上模型找不到对应代码。现在所有模型版本号格式为YYYYMMDD-HHMMSS-hash8由流水线自动生成且preprocess.py里硬编码MODEL_VERSION 20231015-142305-a1b2c3d4启动时校验一致性。3. 生产级ML服务的七步落地清单含完整配置3.1 步骤一定义服务契约——用OpenAPI规范一切交互Notebook里model.predict(x)是函数调用生产环境必须是HTTP契约。我们坚持用OpenAPI 3.0定义接口而非手写文档。以图像分类为例openapi.yaml关键片段paths: /v1/classify: post: summary: 图像分类服务 requestBody: required: true content: multipart/form-data: schema: type: object properties: image: type: string format: binary min_confidence: type: number default: 0.65 minimum: 0.1 maximum: 0.99 responses: 200: description: 分类成功 content: application/json: schema: type: object properties: class_id: type: integer example: 5 class_name: type: string example: cat confidence: type: number example: 0.923 latency_ms: type: number example: 87.4 400: description: 请求参数错误 422: description: 图像格式不支持或尺寸超限 503: description: 服务暂时不可用触发熔断这个YAML不只是文档它驱动三件事1FastAPI自动生成/docs交互界面2CI流水线用openapi-spec-validator校验语法3Postman集合和压测脚本自动生成。我们曾因min_confidence未设maximum导致恶意请求传入999.9模型后处理崩溃。OpenAPI的schema校验在API网关层就拦截了这类非法输入。3.2 步骤二构建最小可行镜像——从2.3GB到387MB的瘦身实战基础镜像选nvidia/cuda:11.7.1-devel-ubuntu20.041.8GB但最终生产镜像必须精简。我们的Dockerfile核心策略# 阶段1构建环境含编译工具 FROM nvidia/cuda:11.7.1-devel-ubuntu20.04 as builder RUN apt-get update apt-get install -y python3-pip python3-dev COPY requirements.txt . RUN pip3 install --no-cache-dir --target /app/dependencies -r requirements.txt # 阶段2运行环境仅运行时依赖 FROM nvidia/cuda:11.7.1-runtime-ubuntu20.04 # 复制依赖不复制编译工具 COPY --frombuilder /app/dependencies /usr/local/lib/python3.9/site-packages/ # 复制应用代码 COPY app/ /app/ WORKDIR /app # 删除apt缓存和文档 RUN apt-get clean rm -rf /var/lib/apt/lists/* /usr/share/doc /usr/share/man # 创建非root用户 RUN groupadd -g 1001 -f app useradd -r -u 1001 -g app app USER app EXPOSE 8000 CMD [uvicorn, main:app, --host, 0.0.0.0:8000, --port, 8000, --workers, 4]关键点1多阶段构建避免将gcc等编译工具打入生产镜像2--target安装依赖到临时路径再复制到运行环境比pip install --no-deps更干净3删除/usr/share/doc节省120MB。实测镜像从2.3GB减至387MB拉取时间从2分14秒降至18秒K8s Pod启动时间从42秒降至11秒。3.3 步骤三设计弹性推理服务——FastAPI Triton Redis三级架构单体服务扛不住流量洪峰我们采用分层架构接入层FastAPI处理HTTP协议、认证、限流、日志。关键配置# main.py from slowapi import Limiter from slowapi.util import get_remote_address limiter Limiter(key_funcget_remote_address) app.state.limiter limiter app.post(/v1/classify) limiter.limit(1000/minute) # 每分钟1000次 async def classify(request: Request): # 解析multipart/form-data form await request.form() image_bytes await form[image].read() # 调用推理层 result await triton_client.infer(image_bytes) return JSONResponse(result)推理层Triton专注模型计算。config.pbtxt关键参数name: image_classifier platform: pytorch_libtorch max_batch_size: 32 input [ { name: INPUT__0 data_type: TYPE_FP32 dims: [ 3, 224, 224 ] } ] output [ { name: OUTPUT__0 data_type: TYPE_FP32 dims: [ 1000 ] } ] dynamic_batching [ { max_queue_delay_microseconds: 10000 } ]缓存层Redis缓存高频请求结果。Key设计为classify:{md5(image_bytes)}:{min_confidence}TTL设为300秒5分钟。实测缓存命中率38%时QPS提升2.1倍GPU利用率从82%降至65%。注意Redis缓存必须带min_confidence到key里因为同一张图不同置信度阈值会导致不同输出如min_confidence0.6输出class50.8可能输出class0。我们曾漏掉这点导致缓存污染花了6小时排查。3.4 步骤四实现全自动健康检查——不只是/healthzK8s的livenessProbe不能只curl http://localhost:8000/healthz。我们的健康检查包含三层基础设施层检查GPU状态# health_check.sh if ! nvidia-smi --query-gpuutilization.gpu --formatcsv,noheader,nounits | grep -q 0\|100; then exit 1 fi服务层检查Triton是否响应# 在FastAPI中 app.get(/healthz) async def healthz(): try: # 发送轻量级推理请求 dummy_input np.random.rand(1,3,224,224).astype(np.float32) result await triton_client.infer(dummy_input) return {status: ok, gpu_util: get_gpu_util()} except Exception as e: logger.error(fHealth check failed: {e}) raise HTTPException(status_code503, detailService unhealthy)业务层检查关键指标阈值# 检查最近1分钟P95延迟是否150ms p95_lat get_prometheus_metric(histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[1m]))) if p95_lat 150: return {status: degraded, reason: high_latency}K8s配置中livenessProbe用基础设施层脚本失败即重启PodreadinessProbe用业务层检查失败则摘除Service流量。这样既保证服务可用又避免把有问题的实例引入流量。3.5 步骤五配置精细化资源限制——CPU/MEM/GPU的黄金配比K8s的resources.requests不是随便填的。我们通过压测确定CPU请求设为500m0.5核。理由FastAPI异步处理请求时CPU主要消耗在序列化/反序列化和网络IO0.5核足够处理200QPS。设太高会浪费调度资源设太低则K8s可能把Pod调度到CPU紧张的节点。内存请求2Gi。计算依据模型权重约1.2GBTriton运行时约0.5GBFastAPI进程约0.3GB留0.5GB缓冲。实测若设1.5GiOOMKilled概率达12%设2.5Gi节点资源利用率下降18%。GPU请求1整卡。注意Triton支持MIGMulti-Instance GPU但生产环境我们禁用因MIG实例间隔离不彻底曾导致一个模型的CUDA stream干扰另一个模型的内存分配。关键配置resources: limits: cpu: 1 memory: 3Gi nvidia.com/gpu: 1 requests: cpu: 500m memory: 2Gi nvidia.com/gpu: 1实操心得limits.memory必须requests.memory否则OOMKilled时K8s不会尝试驱逐Pod而是直接杀进程。我们设3Gi上限给内存泄漏留出缓冲空间配合memory_swap_limit_ratio: 0.5允许50% swap避免瞬间OOM。3.6 步骤六搭建端到端监控告警——从指标到根因的15分钟闭环监控不是堆仪表盘而是建立“指标→告警→诊断→修复”闭环。我们的Grafana看板包含四个核心视图服务健康总览http_requests_total{status~5..} / rate(http_requests_total[5m])错误率、rate(inference_errors_total[5m])模型错误率、sum(kube_pod_status_phase{phaseRunning}) by (namespace)Pod健康数GPU资源透视nv_gpu_duty_cycleGPU利用率、nv_gpu_memory_used_bytes / nv_gpu_memory_total_bytes显存使用率、nv_gpu_temperature_celsius温度三者叠加看是否存在“高利用率高温显存满”三重告警。延迟分解热力图用histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[1m]))HTTP层、histogram_quantile(0.95, rate(triton_inference_request_duration_usecs_bucket[1m]))Triton层、histogram_quantile(0.95, rate(redis_request_duration_seconds_bucket[1m]))Redis层对比定位瓶颈在哪一层。数据漂移追踪data_drift_score{featureblur_score}模糊度、data_drift_score{featureaspect_ratio}宽高比、data_drift_score{featurecolor_variance}色彩方差用KS检验计算阈值设为0.15。告警规则示例Prometheus# GPU显存使用率95%持续5分钟 100 * (nv_gpu_memory_used_bytes / nv_gpu_memory_total_bytes) 95 and on(instance) (count_over_time(nv_gpu_memory_used_bytes[5m]) 5) # P95延迟200ms且错误率1% histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[1m])) 200 and on(job) (rate(http_requests_total{status~5..}[1m]) / rate(http_requests_total[1m]) 0.01)收到告警后SRE用预置Runbook1kubectl top pods看资源2kubectl logs pod -c triton查Triton日志3redis-cli --latency -h redis-svc测Redis延迟4curl -s http://svc/metrics | grep inference_errors确认错误类型。平均15分钟定位根因。3.7 步骤七设计安全回滚机制——从“删Pod”到“秒级切流”回滚不是kubectl delete pod而是流量无感切换。我们用Istio实现# virtual-service.yaml apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: ml-service spec: hosts: - ml-service.example.com http: - route: - destination: host: ml-service subset: v2.0 weight: 95 - destination: host: ml-service subset: v3.1 weight: 5 --- # destination-rule.yaml apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: ml-service spec: host: ml-service subsets: - name: v2.0 labels: version: v2.0 - name: v3.1 labels: version: v3.1当v3.1出问题只需kubectl edit vs ml-service把v3.1的weight改为010秒内流量全切到v2.0。更进一步我们写了个rollback.sh脚本#!/bin/bash # 回滚到指定版本 VERSION$1 kubectl patch vs ml-service -p {\spec\:{\http\:[{\route\:[{\destination\:{\host\:\ml-service\,\subset\:\$VERSION\},\weight\:100}]}]}} # 等待路由生效 sleep 10 # 验证健康检查 curl -s http://ml-service.example.com/healthz | jq .status | grep ok实测从发现问题到完成回滚最短记录是47秒含人工确认远优于传统删Pod重建的3-5分钟。4. 生产环境踩坑实录12个血泪教训与避坑指南4.1 问题1GPU显存“幽灵泄漏”——模型不释放显存却越用越多现象服务运行24小时后nvidia-smi显示显存占用从1.2GB升至3.8GB超出总显存但torch.cuda.memory_allocated()始终显示1.2GB。根因PyTorch的torch.no_grad()上下文管理器未正确嵌套。Notebook里写with torch.no_grad(): output model(input)没问题但生产环境多线程下若model是全局单例no_grad状态可能被其他线程覆盖。更隐蔽的是model.eval()调用后某些自定义Layer的forward里又调用了torch.enable_grad()。解决强制在每次推理前重置梯度状态def infer(self, x): torch.cuda.empty_cache() # 清理缓存 with torch.no_grad(): # 确保model在eval模式 self.model.eval() output self.model(x) return output并在__init__里加显存监控钩子self.model.register_forward_hook( lambda m, i, o: print(fGPU mem: {torch.cuda.memory_allocated()/1024**3:.2f}GB) )避坑技巧在Dockerfile里加ENV PYTORCH_CUDA_ALLOC_CONFmax_split_size_mb:128限制CUDA内存分配块大小让泄漏更早暴露。4.2 问题2时区混乱导致定时任务错乱现象模型每日自动重训任务在UTC时间03:00触发但业务方要求北京时间09:00UTC8。根因基础镜像nvidia/cuda:11.7.1-runtime-ubuntu20.04时区是UTC而K8s节点是CST。CronJob的schedule: 0 3 * * *按节点时区解析但Python的datetime.now()按容器时区返回导致日志时间戳和实际执行时间差8小时。解决统一时区为UTC所有时间逻辑用UTC计算展示时再转本地# Dockerfile ENV TZUTC RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezoneCronJob配置schedule: 0 3 * * * # UTC 03:00 北京时间11:00 env: - name: TZ value: UTCPython中from datetime import datetime, timezone utc_now datetime.now(timezone.utc) beijing_time utc_now.astimezone(timezone(timedelta(hours8)))注意别在容器里apt-get install tzdata再dpkg-reconfigure tzdata这会导致镜像层膨胀且不可复现。4.3 问题3HTTPS证书过期导致服务静默失败现象服务日志无错误但外部调用全部超时curl -v https://ml-service.example.com显示SSL certificate problem: certificate has expired。根因我们用Lets Encrypt证书但K8s Ingress Controller的证书自动续期失败而服务本身不校验证书导致上游调用方如APP因证书过期拒绝连接。解决双保险机制主动监控用Prometheus exporter定期检查证书剩余天数echo | openssl s_client -connect ml-service.example.com:443 2/dev/null | \ openssl x509 -noout -dates | grep notAfter | cut -d -f2 | xargs -I{} date -d {} %s告警规则ssl_certificate_days_remaining 7被动防御在FastAPI中间件里加证书健康检查app.middleware(http) async def ssl_health_check(request: Request, call_next): if request.url.path /healthz: # 检查证书有效期 cert ssl.get_server_certificate((ml-service.example.com, 443)) x509 OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert) if x509.get_not_after() datetime.now(): return JSONResponse({status: ssl_expired}, status_code503) return await call_next(request)4.4 问题4K8s节点驱逐导致模型加载失败现象节点维护时K8s驱逐Pod新Pod启动时报OSError: Unable to load library cudnn。根因NVIDIA Container Toolkit的nvidia-container-runtime未在节点重启后自动重载导致新容器无法访问CUDA库。解决在节点启动脚本里加# /etc/systemd/system/nvidia-docker.service.d/restart.conf [Service] Restartalways RestartSec10并在K8s DaemonSet里部署nvidia-device-plugin确保每个节点有nvidia.com/gpu资源。实操心得每次节点OS升级后必须手动执行sudo systemctl restart nvidia-docker并验证nvidia-smi在容器内可用。4.5 问题5日志轮转失控导致磁盘爆满现象/var/log分区100%df -h显示/dev/sda1 100%服务因无法写日志而假死。根因FastAPI默认日志输出到stdoutK8s收集到/var/log/pods/但logrotate未配置单个日志文件达27GB。解决K8s层面配置日志轮转# kubelet config kind: KubeletConfiguration apiVersion: kubelet.config.k8s.io/v1beta1 logging: format: json flushFrequency: 5s # 日志轮转配置 logFileMaxSize: 100Mi logFileMaxNum: 5应用层面用RotatingFileHandlerhandler RotatingFileHandler( app.log, maxBytes10*1024*1024, # 10MB backupCount5, encodingutf-8 )4.6 问题6模型版本混淆引发AB测试失效现象AB测试结果显示新模型v3.1效果持平但人工抽样发现v3.1实际输出全是v2.0的结果。根因Triton的model_repository目录结构为models/ ├── image_classifier/ │ ├── 1/ │ │ └── model.pt # v2.0 │ └── 2/ │ └── model.pt # v3.1但config.pbtxt里version_policy: latest导致Triton总是加载2/目录而2/目录下其实是v2.0的权重因CI脚本bug覆盖错了。解决强制版本锁定version_policy: specific versions: [ 2 ]并CI流水线加校验# 验证model.pt的SHA256与预期一致 expected_sha$(cat models/image_classifier/2/SHA256) actual_sha$(sha256sum models/image_classifier/2/model.pt | cut -d -f1) if [ $expected_sha ! $actual_sha ]; then echo Model checksum mismatch! exit 1 fi4.7 问题7DNS缓存导致服务发现失败现象服务启动时能解析redis-svc.default.svc.cluster.local运行2小时后突然解析失败报socket.gaierror: [Errno -2] Name or service not known。根因Python的socket.getaddrinfo默认缓存DNS结果而K8s Service的ClusterIP可能因Endpoint变化而更新但Python缓存未刷新。解决禁用DNS缓存