先说一下整个架构
本次用的云服务器是火山云的内部开发机,它的内网IP固定但是外网IP在不断变化,可见这里字节使用了“动态NAT”或“负载均衡器出口IP池”技术。
整个的架构是:“用一个ingress来作为对外的通道,让调用方可以通过浏览器域名的方式来访问后端的service,然后service绑定一个deployment。这个deployment对应的backend pod会调用ollama的pod,询问对应的问题”。
这里的k8s用的是k3s。
再补一点名次解释:
- Ollama是一个大模型的运行框架。它可以把复杂的模型参数、配置文件打包成一个简单文件、然后开一个REST API的接口(也就是下面的11434接口),可以通过requests.post方法来请求。
- Llama3 8B 是一个开源大语言模型本体,后面我们会使用70B的大模型。
搭建过程
我们先搞一个ollama,ollama-deployment.yaml如下:
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
| # ollama-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: ollama spec: replicas: 1 selector: matchLabels: app: ollama template: metadata: labels: app: ollama spec: containers: - name: ollama image: docker.m.daocloud.io/ollama/ollama:latest # 如果这里很慢的话,可以用国内镜像 swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/ollama/ollama:latest-linuxarm64 ports: - containerPort: 11434 resources: requests: cpu: "4" # 给它4个核起步 memory: "8Gi" # 给8G内存 limits: cpu: "16" # 最高允许用到16核,如果还是回话很慢,一秒钟一个字的话就适量加大这里。 memory: "32Gi" --- apiVersion: v1 kind: Service metadata: name: ollama-svc spec: selector: app: ollama ports: - port: 11434 targetPort: 11434 # 这里暴露11434接口
|
然后进入容器里下载模型,因为我这里没有GPU,就用一个轻量的Llama3 8B,它搭配CPU的效果不错:
1 2 3 4 5
| # 找到 pod 名字 export OLLAMA_POD=$(kubectl get pods -l app=ollama -o jsonpath='{.items[0].metadata.name}')
# 让他开始下载模型(字节内网拉模型通常很快) kubectl exec -it $OLLAMA_POD -- ollama run llama3 "你好,请自我介绍" #这里它应该会给你返回内容了
|
整个过程是这样的:
- Deployment启动了Ollama引擎。
- 执行了ollama run llama3,引擎就去下载Llama3 8B 的权重文件。
- 引擎将权重加载进内存,然后在11434接口等指令。
先写一个backend.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| from flask import Flask, jsonify import requests
app = Flask(__name__)
@app.route('/aigpt') def aigpt(): # 直接请求集群内的 Service 名字 response = requests.post( "http://ollama-svc:11434/api/generate", json={ "model": "llama3", "prompt": "Analyze the sentiment of: '请告诉我梅西一共有几个冠军?'", # 这里把问题写死了。 "stream": False } ) return response.json()
if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)
|
Dockerfile如下:
1 2 3 4 5
| FROM docker.m.daocloud.io/library/python:3.9-slim RUN pip install flask requests RUN pip install --upgrade pip COPY backend.py /app.py CMD ["python", "/app.py"] # 容器就是直接启动这个backend.py
|
有了Dockerfile之后,我们来打包一个镜像ai-backend:docker build -t ai-backend:v4 . ,然后通过docker images |grep v4里看本地的确有了docker images库里的确有了ai-backend:v4这个镜像,但是要注意这个镜像还不在k3s里,所以还要执行docker save ai-backend:v4 |sudo k3s ctr images import - , 这样k3s的镜像库才也有了这个ai-backend:v4的镜像,可以用k3s ctr images list|grep ai-backend这个命令来确认。
backend-deployment.yaml如下:
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
| apiVersion: apps/v1 kind: Deployment metadata: name: ai-backend spec: replicas: 3 selector: matchLabels: app: ai-backend template: metadata: labels: app: ai-backend spec: containers: - name: backend image: ai-backend:v4 imagePullPolicy: Never # 这里说明只用本地镜像, IfNotPresent是“优先用本地,没有就去docker hub拉”,Always是“不管本地有没有,每次都去网上对比版本” ports: - containerPort: 5000 #py文件里写了5000,这里也让pod暴露5000这个接口 resources: requests: cpu: "500m" # 因为它本质的工作就是接受请求,转发给oolama-svc所以不用很高的配置。 memory: "512Mi" limits: cpu: "1" memory: "1Gi" --- apiVersion: v1 kind: Service metadata: name: ai-backend-svc spec: selector: app: ai-backend ports: - protocol: TCP port: 80 targetPort: 5000 # 后端Pod的5000对应这个service的80
|
然后可以通过kubectl apply -f backend-deployment.yaml 或者 kubectl rollout restart deployment ai-backend来滚动发布。
后端搞定了之后,ingress.yaml如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| # ai-ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: ai-backend-ingress spec: rules: - http: paths: - path: / pathType: Prefix backend: service: name: ai-backend-svc # 这里看出来 port: number: 80
|
把这些都启动了之后,检查一下状态:
![所有组件都成功]()
上面说了,这个开发机是没有一个固定的外网IP,不过本身就是测试,我这里就在我的macos上手动将开发机的ip写死到/etc/hosts里,对应的域名先暂定ai.demo.com。最后在浏览器测试一下:
![所有组件都成功]()
最后再总结一下整个流程:
- 浏览器通过写死域名和ip发送请求。
- 通过
ai.demo.com域名,流量请求到开发机的80接口。
- k3s内部的ingress接客,看到路径是/aigpt,就把请求转发给ai-backend-svc这个service。
- 后端backend Pod接收请求,执行对应的python代码,发起一个requests请求ollama svc,这俩pod是互通的。
- 返回结果到浏览器。整个过程闭环在macos和开发机里,哪怕开发机没有公网,一样请求OK,而且现在调用是不花token的。
- 做pv/pvc持久化,重启后就不用再去公网上下载模型了,避免了pod先活了,但是还要下载模型,其实服务没准备好,但是流量先进来的尴尬。
排错过程
如果发现pod起不来,可以先通过describe命令粗略看一下原因,如果看不出来啥,可以再用kubectl logs pod名称 --previous ,来查看具体的一个pods的日志,能看出来为啥“镜像顺利拉到手,但是程序为啥启动不起来”。
使用pvc做持久化
虽然上面的效果达到了目标,但是有一个致命的问题,就是ai模型动辄几十GB,容器的镜像拉取和启动太慢了,怎么缓解?
所以这里要解耦一下,镜像只包括运行环境ollama,而巨大的模型权重用过pvc挂载,这样镜像只有只百MB,可快速分发,POD重启的时候直接挂在一下卷,实现秒级就绪。
首先先创建一个ollama-pvc.yaml,做存储声明
1 2 3 4 5 6 7 8 9 10 11 12 13
| # ollama-pvc.yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: ollama-storage-pvc spec: # K3s 默认提供的存储类,会自动在宿主机 /var/lib/rancher/k3s/storage 下创建目录 storageClassName: local-path accessModes: - ReadWriteOnce # 单节点读写,适合这种模型推理场景 resources: requests: storage: 50Gi # 给够 50G,以后下个 70B 模型也放得下,
|
这里要注意!storage: 50Gi 这个数字将来只能往上加,不能往下减(会报错Forbidden: field can not be less than status.capacity),这么设计的原因是往下减的话,会破坏文件系统,如果非要缩,只能删了重建,里面之前的东西都白下载了。
然后修改ollama-deployment.yaml让它挂载到这个“pvc”上:
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
| # ollama-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: ollama spec: replicas: 1 selector: matchLabels: app: ollama template: metadata: labels: app: ollama spec: containers: - name: ollama image: docker.m.daocloud.io/ollama/ollama:latest ports: - containerPort: 11434 volumeMounts: - name: ollama-models mountPath: /root/.ollama # 这里是容器里的路径,不是宿主机的路径 resources: requests: cpu: "4" memory: "8Gi" limits: cpu: "16" memory: "32Gi" volumes: - name: ollama-models persistentVolumeClaim: claimName: ollama-storage-pvc # 这个名称就是刚才pvc里写的名称 --- apiVersion: v1 kind: Service metadata: name: ollama-svc spec: selector: app: ollama ports: - port: 11434 targetPort: 11434
|
然后重新部署一下,先用-it的方式来测试一个问题:
![这次是要从头下载的,因为他在往刚设定的PVC里下载数据]()
同样,此时用浏览器发起请求也是报错:
![因为在下载中,所以提示llama3不存在]()
这里说明一下PVC → PV → 宿主机路径的关系链:
1 2 3 4 5 6 7
| 容器内路径 (/root/.ollama) ↓ PVC (ollama-storage-pvc) ↓ PV (pvc-163ae8fb-4051-44f6-a7cc-3e1b4d3b6a9c) ↓ 宿主机实际路径 (由StorageClass决定,如:/var/lib/rancher/k3s/storage)
|
如图,这里可以看到的Path就是宿主机上的路径:
![pv的name对应的就是pvc的VOLUME]()
PVC状态说明:
Pending:等待中,存储卷还未创建(有问题)
Bound:已绑定,存储卷已成功创建并绑定到PVC(正常状态)
Released:已释放,Pod已删除但PV还未清理
Failed:失败,存储卷创建失败
现在你如果删除llama的pod,重建一个就会发现很快这个pod可以使用并且正确返回内容了。
改用70B的大模型
如果觉得8B模型不咋地,想升级用70B的大模型,就用这个:
1 2 3 4 5
| # 找到 pod 名 export OLLAMA_POD=$(kubectl get pods -l app=ollama -o jsonpath='{.items[0].metadata.name}')
# 开始拉取并运行(这个命令会卡住下载一段时间) kubectl exec -it $OLLAMA_POD -- ollama run llama3:70b
|
用了70B对8B是有了质的飞跃,而且即使想搞RAG,70B也比8B的效果更好,但是大小更大,要39G。但是它回答的时候需要先把这39G都加载到内存里,所以这里注意一下backend.py调用ollama的post请求要加上timeout=300,这样不至于模型还没算完,链接就断了。
39G虽然大,但是PVC是支持断点续传的,即使中间断网了,下一次启动会去看临时文件的:
![8B的模型有时候就是胡说八道]()
![70B的模型果然靠谱多了]()