前文背景就是这篇文章:https://brucewayne2099.github.io/2026/03/19/%E6%9C%AC%E5%9C%B0%E6%90%AD%E5%BB%BA%E4%B8%80%E4%B8%AAollama%E5%A4%A7%E6%A8%A1%E5%9E%8B/
我现在打算加一个功能,就是加入openai-whisper,实现在web端用语音与ai交流的目的。对应的backend.py成了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 from flask import Flask, render_template, request, jsonify from prometheus_client import start_http_server, Summary import requests import whisper import os import time app = Flask(__name__) LATENCY = Summary('ai_inference_seconds', 'Time spent processing LLM prediction') STT_LATENCY = Summary('speech_to_text_seconds', 'Time spent on Whisper transcription') stt_model = whisper.load_model("base") print("Whisper 加载完成,可以实现语音交流。") @app.route('/') def index(): return render_template('index.html') # 这里多了一个index.html,在页面里有一个喇叭按钮,可以传入语音 @app.route('/speech_to_text', methods=['POST']) def stt(): if 'file' not in request.files: return jsonify({"error": "未收到音频文件"}), 400 file = request.files['file'] file_path = "temp_audio.webm" file.save(file_path) try: with STT_LATENCY.time(): result = stt_model.transcribe(file_path, language="zh") text = result.get("text", "").strip() return jsonify({"text": text}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: if os.path.exists(file_path): os.remove(file_path) @app.route('/aigpt_api') def aigpt_api(): user_prompt = request.args.get('prompt', '') if not user_prompt: return jsonify({"response": "大哥,你还没说话呢"}) ollama_url = "http://ollama-svc:11434/api/generate" try: with LATENCY.time(): r = requests.post( ollama_url, json={ "model": "llama3:70b", # 确保你 pull 的是这个名字 "prompt": user_prompt, "stream": False }, timeout=300 ) r.raise_for_status() return jsonify(r.json()) except requests.exceptions.Timeout: return jsonify({"response": "⚠️ 70B 思考太久超时了,建议给 Pod 更多 CPU 资源"}), 504 except Exception as e: return jsonify({"response": f"❌ 后端报错: {str(e)}"}), 500 if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)
Dockerfile也改成了这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 #Dockerfile FROM docker.m.daocloud.io/library/python:3.9-slim RUN apt-get update && apt-get install -y ffmpeg RUN pip install flask prometheus_client requests openai-whisper setuptools wheel # 这里加入了openai-whisper # 创建工作目录 WORKDIR /app # 把当前目录下所有东西(包含 templates 文件夹)都拷贝进去 COPY . . # 运行 app CMD ["python", "backend.py"] # 容器就是启动这个python
修改了Dockerfile之后,我们来打包一个镜像ai-backend v8版本:docker build -t ai-backend:v8 . ,然后通过docker images |grep v8里看本地的确有了docker images库里的确有了ai-backend:v8这个镜像。这个镜像挺大的,足足11G。
但是要注意这个镜像还不在k3s里,所以还要执行docker save ai-backend:v8 |sudo k3s ctr images import - , 这样k3s的镜像库才也有了这个ai-backend:v8的镜像,可以用k3s ctr images list|grep ai-backend这个命令来确认。
然后我就执行kubectl rollout restart deployment ai-backend来滚动发布。
结果检测跟上一次发布的内容没变化,检查了一下原来是deployment.yaml里忘改v8镜像了,修改了之后,又执行kubectl rollout restart deployment ai-backend,此时发现所有的pod全挂了:
多个Deployment同时存在:有3个不同的ReplicaSet(5c87ccdd65, 5ffd4f657d, 6dddcb6665)
Pod状态异常:Pending、Error、ContainerStatusUnknown、Completed
Deployment未就绪:所有Deployment都是0/1或0/3
然后赶紧kubectl log发现报错kubelet Node n37-220-048 status is now: NodeHasNoDiskPressure,整个node没有磁盘空间了,果然打开一看120G的本地盘已经达到了95%。
于是赶紧kubectl delete deployment ai-backend -i ai-demo来释放空间,然后手动删除掉这些不应该生成的rs,再通过kubectl get rs -n ai-demo来确认。
然后docker image prune -f清理docker镜像,发现k3s ctr里找不到v8镜像了。
先把ollama pod恢复回来,让其他人可以先用exec -it或者curl的形式能访问ai,暂时先对付用。
原因查明 我这个开发机原来是120G的本地盘,里面下载了8B的大模型7G,还有一个70B的大模型 42G,已经占用了50G。
首先,docker build的时候会产生一个11G的镜像,然后执行了docker save ai-backend:v4 |sudo k3s ctr images import - ,其实这一步是一个拷贝工作,所以这里又是22G,然后我dockerfile有一个地方变化了,导致v8又是一个新的镜像,这里面就用了80G左右,
此时磁盘空间不足,又因为我执行了rollout restart,deployment就不断的起新rs,rs去创建新pod,新pod就要去加载这个11G的v8镜像。kubelet此时需要干掉镜像来救磁盘,于是就把我的v8镜像干掉了,导致ErrImageNeverPull的错误,但是deployment看到pod挂了,还是再不断生成rs,于是就莫名其妙多出来很多个rs,每个rs下都有挂掉的pod。
后续操作 首先先申请了一个数据盘500G,然后挂载到开发机上:
挂载的过程就不写了,最后用lsblk确认:
首先mkdir -p /data00/docker,创建一个文件来存储docker镜像,从原来默认的/var/lib/docker/overlay2文件夹里改放到这个500G的磁盘里。/var/lib/docker/overlay2是 Docker 的存储目录,使用的是 Overlay2 存储驱动。这里存放的是 Docker 容器和镜像的所有数据。如果你把里面的文件直接删了,那么再构建docker镜像就要从头跑一遍,不过如果你不小心把文件夹删了,就重启一下docker就会自动把对应的文件夹恢复。
这里需要sudo nano /etc/docker/daemon.json:
1 2 3 4 5 6 7 8 9 10 11 { "registry-mirrors": [ "https://docker.mirrors.ustc.edu.cn", "https://hub-mirror.c.163.com", "https://mirror.ccs.tencentyun.com", "https://docker.nju.edu.cn", "https://docker.xuanyuan.me" ], "data-root": "/data00/docker", # 这里是新加的 "storage-driver": "overlay2" }
注意,nano打开的文件要用Ctrl + O保存,然后Ctrl + X退出。
然后启动docker之后,用docker info | grep -A2 "Docker Root Dir"来确认一下:
搞完了docker镜像的新路径,现在搞一下k3s的。毕竟k3s ctr images import默认就是写到/var/lib/rancher/k3s/agent/containerd里的,如果还在这个120G的系统盘上,早晚还要爆。
这里可以骚一下,用软链接方法:
1 2 3 4 1. 迁移旧目录:sudo mv /var/lib/rancher/k3s /data00/ 2. 做软链接:sudo ln -s /data00/k3s /var/lib/rancher/k3s 3. 重启 K3s:sudo systemctl restart k3s 这样对于 K3s 来说,它以为自己还在读 /var/...,但物理上每一字节都写在了新的 500G 大盘上。
如果中规中矩的话,就是这样:
1 2 3 4 5 6 7 mkdir -r /data00/k3s sudo rsync -avPr /var/lib/rancher/k3s/ /data00/k3s/ #将原来的k3s的权重文件复制到新的路径下 vim /etc/rancher/k3s/config.yaml #如果没有这个文件就新建一个 data-dir: /data00/k3s #在这个config.yaml里加上这句话就行 systemctl start k3s
如果发现k3s启动不起来了,就按照下面的方法试一下:
1 2 3 4 5 6 7 8 9 10 11 # 1. 复制 K3s 的 kubeconfig 到默认位置 mkdir -p ~/.kube sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config sudo chown $(id -u):$(id -g) ~/.kube/config # 2. 修改配置中的服务器地址(如果是本地) sed -i 's/127.0.0.1/localhost/g' ~/.kube/config # 3. 测试连接 kubectl get nodes kubectl get pods -A
虽然我们加上了500G的数据盘,但是之前ollama-pvc.yaml没法改,只能直接apply -f启动:
1 2 3 4 5 6 7 8 9 10 11 12 # ollama-pvc.yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: ollama-storage-pvc spec: storageClassName: local-path # 他现在会自动指向/data00/k3s了 accessModes: - ReadWriteOnce # 单节点读写,适合这种模型推理场景 resources: requests: storage: 50Gi #这里不能改了,只有动态供应的 PVC 才能扩容,`local-path` StorageClass 不支持扩容
如果你要是启动就会报错:
然后随着pod全部恢复之后,页面恢复正常:
如果要删除k3s的镜像的命令就是:
1 2 k3s ctr images list k3s ctr images rm 对应的REF