10 | 云上 AI 推理服务:把 scikit-learn 模型塞进 K8s,纯 CPU 也能跑

发布时间:2026/7/28 16:03:19
10 | 云上 AI 推理服务:把 scikit-learn 模型塞进 K8s,纯 CPU 也能跑 10 | 云上 AI 推理服务把 scikit-learn 模型塞进 K8s纯 CPU 也能跑系列目录《基于华为云 FlexusX 四节点集群的云计算全栈实操》PaaS 篇本篇对应课程模块云上 AI / 云原生应用引子上一篇我们把 K3s 集群跑起来了也验证了它「负载均衡、自愈、弹性、滚动更新」的四大能力。但集群本身不产生价值——跑在上面的工作负载才产生价值。这一篇我们让集群干一件「很 AI」的事部署一个在线情感分析推理服务。模型是 scikit-learn 的 TF-IDF 逻辑回归框架用 FastAPI uvicorn 封装成 HTTP API全程纯 CPU、不用 GPU。为什么选这么「朴素」的组合因为现实里 80% 的 AI 落地需求根本用不到大模型情感分析、垃圾邮件分类、风险评分、文本相似度……这些中小模型在 CPU 上几毫秒就能出结果却要吃掉一整张几千块的 GPU 卡账怎么算都不划算。本文会带你走完一条完整链路模型训练 → 容器封装 → 镜像分发 → K8s 编排 → HTTP 在线推理并重点讲清楚一个很多人会卡住的真实难题——K8s 用的是 containerd不是 Docker本地镜像怎么交付到集群。背景与理论CPU 推理的成本逻辑《深入浅出云计算》反复强调一个观点云的价值不在「算力最大」而在「算力刚好匹配需求」。AI 推理正是这一观点的试金石。维度GPU 推理如 T4/A10CPU 推理如 FlexusX 8vCPU单卡/实例成本高GPU 溢价明显低通用算力按需付费吞吐适合大模型 / 高并发批量中小模型单请求毫秒级足够适用模型LLM、CV 大模型、嵌入向量传统 ML、轻量分类/回归部署复杂度需 nvidia-docker、device-plugin普通容器零额外依赖我们的情感分析模型是一个TfidfVectorizer(ngram_range(1,2))LogisticRegression的 Pipeline参数量极小一次predict_proba在 CPU 上亚毫秒级完成。把它放在 8vCPU 的 FlexusX 上单实例即可稳定撑起中小流量成本只有 GPU 方案的零头。这正是云原生 AI 落地的「群众路线」——不必人人都上大模型。架构┌─────────────────────────────────────────────────────────────┐ │ K3s Cluster (containerd runtime, 3 nodes) │ │ │ │ NodePort 30800 ──┐ │ │ ▼ │ │ kube-proxy (iptables) │ │ / | \ │ │ ▼ ▼ ▼ │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ sentiment │ │ sentiment │ │ sentiment │ │ │ │ -ai Pod │ │ -ai Pod │ │ -ai Pod │ │ │ │ (node1) │ │ (node3) │ │ (node4) │ │ │ │ FastAPI │ │ FastAPI │ │ FastAPI │ │ │ │ :8000 │ │ :8000 │ │ :8000 │ │ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │ │ (imagePullPolicy: Never, 本地镜像) │ │ └───────────────┴────────────────┘ │ │ │ │ 镜像来源docker save → ctr import → 内网 scp 分发 │ └─────────────────────────────────────────────────────────────┘链路关键点镜像不走公网 Registry而是本地构建后通过内网分发到各节点 containerd——这是没有私有 Registry 时的务实方案生产建议改用 Harbor/SWR后文详述。环境与准备节点弹性公网IP私有IP规格系统K8s角色node1113.47.6.41192.168.0.2528vCPU/16GiBUbuntu 24.04.4control-planenode31.94.220.182192.168.0.2418vCPU/16GiBUbuntu 24.04.4workernode4124.70.102.139192.168.0.1508vCPU/16GiBUbuntu 24.04.4worker沿用上篇的 K3s v1.36.2k3s1 containerd 2.3.2 集群。由于 node2 未纳入集群镜像分发只需在 n1、n3、n4 三节点间进行。实操步骤1. 编写推理服务 app.py服务启动时用内置中英混合语料训练模型对外暴露三个端点/元信息、/healthz探针、/predict推理。关键片段完整见../ai-service/app.py# 启动时即在模块加载阶段训练模型CPU毫秒级modelPipeline([(tfidf,TfidfVectorizer(ngram_range(1,2),min_df1)),(clf,LogisticRegression(max_iter1000)),])model.fit([tfort,_inTRAIN],[yfor_,yinTRAIN])app.get(/healthz)defhealthz():return{status:ok,pod:HOSTNAME}app.post(/predict)defpredict(req:Req):probamodel.predict_proba([req.text])[0]labelpositiveifproba[1]0.5elsenegativereturn{text:req.text,label:label,score_positive:round(float(proba[1]),4),score_negative:round(float(proba[0]),4),served_by_pod:HOSTNAME,}设计要点训练放在进程启动期而非首次请求时避免冷启动抖动语料小CPU 上训练几乎无感。/healthz与/predict分离探针走轻量端点推理走重端点互不干扰。served_by_pod字段用于验证负载均衡——能看到请求究竟落到哪个 Pod。2. 编写 DockerfilePyPI 清华镜像加速FROM python:3.11-slim WORKDIR /app # 使用国内 PyPI 镜像加速依赖安装 RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app.py . EXPOSE 8000 CMD [uvicorn, app:app, --host, 0.0.0.0, --port, 8000]requirements.txt固定版本保证可复现fastapi0.115.6 uvicorn0.34.0 scikit-learn1.6.1 pydantic2.10.43. 构建镜像在 node1 上构建最终镜像sentiment-ai:1.0体积约 600MB含 python:3.11-slim 基础镜像与 scikit-learn 依赖cdai-servicedockerbuild-tsentiment-ai:1.0.4. 把镜像交付到 K8s关键难点这是全文最关键的工程点。很多人以为docker build完kubectl apply就能用——错。K3s 用的是containerd它不读 Docker 的本地镜像库。如果你在 YAML 里写imagePullPolicy: IfNotPresent却没配可访问的 RegistryPod 会一直ImagePullBackOff。我们的务实方案无私有 Registry 时# 1) node1: 把 Docker 镜像导出成 tardockersave sentiment-ai:1.0-osentiment-ai.tar# 约 133 MiB# 2) node1: 导入到本机 containerdK3s 自带 ctrsudok3s ctr imagesimportsentiment-ai.tar# 3) 内网 scp 分发 tar 到 worker 节点scpsentiment-ai.tar node3:/tmp/scpsentiment-ai.tar node4:/tmp/# 4) node3 / node4: 各自导入 containerdsshnode3sudo k3s ctr images import /tmp/sentiment-ai.tarsshnode4sudo k3s ctr images import /tmp/sentiment-ai.tar三节点 containerd 都持有镜像后Deployment 里必须显式声明imagePullPolicy: Never告诉 K3s「别去远程拉就用本地镜像」containers:-name:sentiment-aiimage:docker.io/library/sentiment-ai:1.0imagePullPolicy:Never5. 部署到 K8sai-service.yaml完整见../k8s/ai-service.yaml关键片段apiVersion:apps/v1kind:Deploymentmetadata:name:sentiment-aispec:replicas:3template:spec:containers:-name:sentiment-aiimage:docker.io/library/sentiment-ai:1.0imagePullPolicy:Neverports:-containerPort:8000resources:requests:cpu:100mmemory:128Milimits:cpu:500mmemory:512MireadinessProbe:httpGet:path:/healthzport:8000initialDelaySeconds:3periodSeconds:5livenessProbe:httpGet:path:/healthzport:8000initialDelaySeconds:5periodSeconds:10topologySpreadConstraints:-maxSkew:1topologyKey:kubernetes.io/hostnamewhenUnsatisfiable:ScheduleAnywaylabelSelector:matchLabels:app:sentiment-ai---apiVersion:v1kind:Servicemetadata:name:sentiment-ai-svcspec:type:NodePortselector:app:sentiment-aiports:-port:8000targetPort:8000nodePort:30800sudokubectl apply-fai-service.yamlsudokubectl get pods-owide真实输出实测以下来自results/15_ai_inference.txt。Pod 跨 3 节点均衡分布sentiment-ai-7f878749b7-9whd9 Running ecs-71b9-0003 sentiment-ai-7f878749b7-p8fnx Running ecs-71b9-0004 sentiment-ai-7f878749b7-rxdf5 Running ecs-71b9-0001topologySpreadConstraints再次生效3 副本精确落到 n3/n4/n1 三个节点。服务元信息 GET /{service:sentiment-inference,framework:scikit-learn FastAPI, pod:sentiment-ai-7f878749b7-p8fnx}推理测试 POST /predict请求被负载均衡到不同 PodIN : this is absolutely wonderful, i love it OUT: labelpositive score_pos0.5604 served_by...-p8fnx IN : terrible quality, total waste of money OUT: labelnegative score_pos0.4265 served_by...-rxdf5 IN : the delivery was fast and the product works great OUT: labelpositive score_pos0.5844 served_by...-9whd9 IN : it broke after one day, very disappointed OUT: labelnegative score_pos0.4104 served_by...-p8fnx4/4 样本情感分类全部正确且返回了正负类概率分数如score_pos0.5604。更关键的是served_by_pod字段——请求被负载均衡到了p8fnx、rxdf5、9whd9三个不同的 Pod证明 Service 的流量分发真实生效。深度解读探针对 AI 服务的特殊意义这篇的 YAML 里我特意给 AI 服务配了readinessProbe和livenessProbe这比普通 Web 服务更有讲究readinessProbe就绪探针AI 服务启动时要加载/训练模型可能需要几秒才能接流量。在就绪前若放流量进来要么报错要么超时。探针失败期间kube-proxy 不会把该 Pod 加入 Endpoint避免「启动期裸奔」。livenessProbe存活探针如果模型推理时死锁、或内存泄漏把进程拖死存活探针持续失败会触发 kubelet重启该 Pod配合 ReplicaSet 实现自愈对线上推理可用性至关重要。对大模型/GPU 服务探针还要加startupProbe给足冷启动时间否则大模型加载慢会被误杀——这是我们这套小模型不必、但大模型必做的细节。踩坑与排障重点坑 1containerd 看不到 Docker 镜像现象Pod 一直ImagePullBackOffdescribe显示ErrImageNeverPull或拉取 timeout。根因docker和k3s ctr是两套独立的镜像存储。Docker 构建的镜像只在 Docker 的 graph driver 里containerd 不会自动看到。解决docker save→k3s ctr images import→ 各节点分发 →imagePullPolicy: Never。且三个节点都必须有镜像否则调度到没镜像的节点就会 Pending/ErrImagePull。坑 2镜像标签要带仓库前缀ai-service.yaml里写的是docker.io/library/sentiment-ai:1.0。导入时若只标sentiment-ai:1.0containerd 内部会规范化为docker.io/library/sentiment-ai:1.0YAML 里最好写全避免命名空间不匹配导致找不到。坑 3镜像体积python:3.11-slim scikit-learn 让镜像到 600MB。生产可换python:3.11-alpine 多阶段构建压到 200MB 以内分发更快、启动更轻。生产建议与延伸当前方案的定位本地docker savectr import 内网分发是没有私有 Registry 时的最佳过渡方案适合实验、PoC、离线环境。但它有明显的生产短板每加一个节点都要手动分发镜像不可扩展无法做镜像版本管理、签名、漏洞扫描。生产应升级为私有 Registry华为云 SWR 或自建 HarborYAML 改成image: swr.cn-xxx.myhuaweicloud.com/team/sentiment-ai:1.0imagePullPolicy: IfNotPresent配合ImagePullSecret。更大模型的路线模型服务化框架KServe / Triton Inference Server——统一推理协议、动态批处理dynamic batching、模型版本灰度GPU 推理需安装 NVIDIA device-plugin节点打nvidia.com/gpu资源标签Pod 声明limits: nvidia.com/gpu: 1大模型LLMvLLM / TGIS 等推理引擎配合 K8s 的HPA GPU 共享如 volcano做弹性。小结我们打通了一条完整的「云上 CPU AI 推理」链路模型训练——scikit-learn TF-IDF 逻辑回归启动即训容器封装——FastAPI uvicorn镜像 600MB镜像交付——docker save→ctr import→ 内网分发 →imagePullPolicy: NeverK8s 编排——3 副本跨 3 节点NodePort 30800在线推理——4/4 分类正确请求负载均衡到不同 Pod。最值得记住的工程经验K8s 用 containerdDocker 构建的镜像必须显式导入 containerd否则 Pod 拉不到镜像。这条坑踩过一次就忘不掉了。下一篇我们给这个 AI 服务加上HPA 自动伸缩让它在流量洪峰时自动扩容、低谷时缩容——真正体验 Serverless 式的弹性。配置与实测引用推理实测../results/15_ai_inference.txt部署清单../k8s/ai-service.yaml服务代码../ai-service/app.py构建文件../ai-service/Dockerfile、../ai-service/requirements.txt