Ollama 生产环境部署——从笔记本到服务器,踩了哪些坑
Ollama 生产环境部署——从笔记本到服务器,踩了哪些坑
适读人群:后端工程师/AI应用开发者 | 阅读时长:约15分钟 | 核心价值:Ollama生产部署的真实踩坑记录,包含GPU管理、并发优化、监控方案
去年年底,我们公司内网有个需求:法务部门要用大模型审合同,但合同涉及商业机密,绝对不能走外网API。老板拍板,让我搭一套本地模型服务。
我当时的第一反应是:这不简单吗,Ollama装上去跑个模型就完了。
然后我花了整整三周在服务器上调试。
有多惨?服务器是公司配的Dell PowerEdge R750,双路Intel,64GB内存,一张A10 24GB显卡。硬件不差。但Ollama在生产环境跑和在笔记本上跑,完全是两回事。这篇就把我踩过的坑原原本本写出来,你要是也面临类似场景,可以少走弯路。
笔记本上能跑,不代表生产没问题
先说结论:Ollama是一个非常适合开发测试的工具,界面友好、安装简单、模型管理方便。但如果你要把它用于生产,得做不少额外工作。
我在自己MacBook上玩Ollama,顺滑得很。ollama run qwen2.5:7b,几分钟下载完,聊天没问题。但服务器上一部署,马上就出问题了。
第一个坑:GPU不被识别
公司服务器跑的是Ubuntu 22.04 LTS,NVIDIA驱动版本535。装好Ollama之后,用ollama run跑模型,速度慢得离谱——仔细一看,在用CPU推理。
# 检查Ollama日志
journalctl -u ollama -f
# 看到的日志是这样的
# msg="inference compute" id=... library=cpu compute=... driver=0.0 name="" total="0 B" available="0 B"driver=0.0,这代表没检测到CUDA。排查了半天,发现是CUDA版本的问题。Ollama对CUDA版本有要求,驱动535对应CUDA 12.2,但我装的是CUDA 11.8的toolkit。
# 检查CUDA版本
nvidia-smi
nvcc --version
# 两个版本要对得上
# 卸载旧版本toolkit,装对应版本
sudo apt-get remove --purge cuda-11-8
wget https://developer.download.nvidia.com/compute/cuda/12.2.0/local_installers/cuda_12.2.0_535.54.03_linux.run
sudo sh cuda_12.2.0_535.54.03_linux.run --toolkit --silent装完重启,GPU终于被识别了。这个问题听起来简单,但当时折腾了将近半天。
GPU内存管理:Ollama的默认行为会害了你
Ollama有个默认行为:模型加载进来之后,如果5分钟没有请求,会自动卸载模型,把GPU内存释放出来。
在笔记本上,这是个合理的设计——省电省资源。但在生产环境,这是灾难。
用户发请求 → 模型已被卸载 → 重新加载模型(耗时30-60秒)→ 用户等了一分钟才看到第一个字。
我们法务的同事第一周体验下来,给的反馈是:"这玩意儿怎么有时候秒回,有时候转圈圈一分钟?"
解决方法:
# 方法1:设置环境变量,修改模型卸载超时(0表示永不卸载)
# 编辑systemd service文件
sudo systemctl edit ollama
# 在[Service]下添加
[Service]
Environment="OLLAMA_KEEP_ALIVE=-1"
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_MAX_LOADED_MODELS=2"
sudo systemctl daemon-reload
sudo systemctl restart ollamaOLLAMA_KEEP_ALIVE=-1 让模型永远不被卸载。OLLAMA_NUM_PARALLEL=4 允许同时处理4个并发请求(根据GPU显存调整)。OLLAMA_MAX_LOADED_MODELS=2 限制同时加载的模型数量,防止OOM。
设完这三个环境变量,"转圈一分钟"的问题消失了。
并发请求:默认配置撑不住真实负载
法务部门大概20个人会用这个系统,不是同时用,但高峰期可能5-6人并发。我做了个简单的压测:
import asyncio
import aiohttp
import time
async def send_request(session, prompt):
start = time.time()
async with session.post(
"http://localhost:11434/api/generate",
json={
"model": "qwen2.5:14b",
"prompt": prompt,
"stream": False
}
) as resp:
result = await resp.json()
elapsed = time.time() - start
return elapsed, len(result.get("response", ""))
async def load_test(concurrency=5, total=20):
prompt = "请分析以下合同条款是否存在法律风险,并给出具体建议:甲方有权在任何情况下单方面终止合同,乙方不得提出任何异议。"
async with aiohttp.ClientSession() as session:
tasks = [send_request(session, prompt) for _ in range(total)]
# 按并发度分批
results = []
for i in range(0, total, concurrency):
batch = tasks[i:i+concurrency]
batch_results = await asyncio.gather(*batch, return_exceptions=True)
results.extend(batch_results)
valid = [r for r in results if not isinstance(r, Exception)]
times = [r[0] for r in valid]
print(f"成功率: {len(valid)}/{total}")
print(f"平均响应时间: {sum(times)/len(times):.2f}s")
print(f"最大响应时间: {max(times):.2f}s")
print(f"最小响应时间: {min(times):.2f}s")
asyncio.run(load_test(concurrency=5, total=20))压测结果让我很难受:5并发的时候,平均响应时间从单请求的8秒飙升到45秒,而且有请求直接超时失败。
问题在哪?Ollama默认只开1个并发处理队列。后来请求排队等待,超时了。
真正解决并发的方案:加一层请求队列
光靠Ollama自己的OLLAMA_NUM_PARALLEL不够,因为它只是让推理引擎能并行处理token,但模型本身的吞吐是固定的——一张A10跑14B模型,并发再多也是这么快。
实际上需要做的是:控制进入Ollama的请求速率,让超出并发上限的请求等待而不是失败。
我用了一个简单的FastAPI中间层:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import asyncio
import aiohttp
import time
from typing import Optional
app = FastAPI()
# 信号量控制并发
MAX_CONCURRENT = 3 # A10 24GB跑14B,实测3并发是最优
semaphore = asyncio.Semaphore(MAX_CONCURRENT)
QUEUE_TIMEOUT = 120 # 最多等待120秒
class GenerateRequest(BaseModel):
model: str = "qwen2.5:14b"
prompt: str
stream: bool = False
options: Optional[dict] = None
@app.post("/api/generate")
async def generate(request: GenerateRequest):
# 尝试获取信号量,最多等待QUEUE_TIMEOUT秒
try:
await asyncio.wait_for(semaphore.acquire(), timeout=QUEUE_TIMEOUT)
except asyncio.TimeoutError:
raise HTTPException(status_code=503, detail="服务繁忙,请稍后重试")
try:
async with aiohttp.ClientSession() as session:
payload = {
"model": request.model,
"prompt": request.prompt,
"stream": request.stream
}
if request.options:
payload["options"] = request.options
async with session.post(
"http://localhost:11434/api/generate",
json=payload,
timeout=aiohttp.ClientTimeout(total=180)
) as resp:
if resp.status != 200:
raise HTTPException(status_code=resp.status, detail="上游服务错误")
result = await resp.json()
return result
finally:
semaphore.release()
@app.get("/health")
async def health():
# 检查Ollama是否正常
try:
async with aiohttp.ClientSession() as session:
async with session.get("http://localhost:11434/api/tags", timeout=aiohttp.ClientTimeout(total=5)) as resp:
if resp.status == 200:
return {"status": "ok", "queue_available": semaphore._value}
except:
pass
return {"status": "error"}这一层中间件做了两件事:限制同时进入Ollama的并发数,以及让超出并发的请求排队等待而不是直接报错。
加了这一层之后,20用户并发测试,0失败,平均响应时间稳定在25秒以内。对于合同审查这种场景,25秒是完全可以接受的。
GPU内存实际占用:比你想的更复杂
跑14B的Qwen2.5,FP16下大概需要28GB显存。我的A10只有24GB,所以必须用量化版本。
# 拉取INT4量化版本
ollama pull qwen2.5:14b-instruct-q4_K_M
# 查看实际显存占用
nvidia-smi --query-gpu=memory.used,memory.free,memory.total --format=csvQ4_K_M量化的14B模型,实际显存占用约9.5GB,留有充足余量。但这里有个坑:Ollama会保留一部分显存作为KV Cache,请求越长,KV Cache占用越大。
如果你发一个超长合同(2万字),显存可能会突然暴增,触发OOM,整个Ollama进程崩溃。
解决方法是设置context window上限:
# 在Modelfile里限制context长度
# 创建一个自定义Modelfile
cat > /opt/ollama/Modelfile-qwen-legal << 'EOF'
FROM qwen2.5:14b-instruct-q4_K_M
# 限制上下文长度,防止KV Cache撑爆显存
PARAMETER num_ctx 8192
# 针对合同审查场景的系统提示
SYSTEM """你是一位专业的合同审查助手。请仔细分析合同条款,识别潜在法律风险,给出具体、可操作的修改建议。分析时要关注:违约责任、权利义务不对等、模糊表述、强制性条款违规等方面。"""
EOF
# 基于Modelfile创建自定义模型
ollama create qwen-legal -f /opt/ollama/Modelfile-qwen-legal设了num_ctx 8192之后,再也没有出现OOM崩溃的情况。8192 tokens对于大多数合同(3000-5000字)来说够用,对于超长合同,我们在应用层做了切片处理。
监控方案:没有监控,你不知道服务挂了多久
Ollama原生没有Prometheus metrics,这是真实的痛点。我搭了一个简单的监控方案:
# metrics_exporter.py
# 把Ollama的状态暴露为Prometheus格式
import asyncio
import aiohttp
from prometheus_client import start_http_server, Gauge, Counter, Histogram
import time
# 定义metrics
ollama_up = Gauge('ollama_up', 'Ollama service is up (1) or down (0)')
ollama_loaded_models = Gauge('ollama_loaded_models', 'Number of loaded models')
ollama_request_total = Counter('ollama_request_total', 'Total requests', ['model', 'status'])
ollama_request_duration = Histogram(
'ollama_request_duration_seconds',
'Request duration in seconds',
['model'],
buckets=[1, 5, 10, 20, 30, 60, 120]
)
async def collect_ollama_metrics():
"""定期采集Ollama状态"""
while True:
try:
async with aiohttp.ClientSession() as session:
# 检查服务是否存活
async with session.get(
"http://localhost:11434/api/tags",
timeout=aiohttp.ClientTimeout(total=5)
) as resp:
if resp.status == 200:
ollama_up.set(1)
data = await resp.json()
models = data.get("models", [])
ollama_loaded_models.set(len(models))
else:
ollama_up.set(0)
except Exception as e:
ollama_up.set(0)
print(f"采集失败: {e}")
await asyncio.sleep(15) # 15秒采集一次
if __name__ == "__main__":
# 启动Prometheus metrics HTTP服务
start_http_server(9090)
print("Metrics exporter started on :9090")
asyncio.run(collect_ollama_metrics())然后配一个简单的Prometheus + Grafana就能看到服务状态了。告警规则我设了两条:
# alerting_rules.yml
groups:
- name: ollama
rules:
- alert: OllamaDown
expr: ollama_up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Ollama服务宕机"
- alert: OllamaHighLatency
expr: histogram_quantile(0.95, ollama_request_duration_seconds_bucket) > 60
for: 5m
labels:
severity: warning
annotations:
summary: "Ollama P95延迟超过60秒"这套监控搭完,我终于能睡个安稳觉——服务挂了会立刻收到钉钉通知,不用等用户来投诉。
自动重启和健康检查
Ollama偶尔会因为OOM或其他原因挂掉,systemd的自动重启要配好:
# /etc/systemd/system/ollama.service
[Unit]
Description=Ollama Service
After=network-online.target
[Service]
ExecStart=/usr/local/bin/ollama serve
User=ollama
Group=ollama
Restart=always
RestartSec=10
# 环境变量配置
Environment="OLLAMA_KEEP_ALIVE=-1"
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_MAX_LOADED_MODELS=2"
Environment="OLLAMA_HOST=0.0.0.0:11434"
# 限制内存,防止CPU内存也爆
MemoryMax=32G
[Install]
WantedBy=default.targetRestart=always + RestartSec=10,服务挂了10秒后自动重启。加上前面的监控告警,整体可用性能达到99%以上。
最终架构
我们最终的部署架构是这样的:
用户请求
|
v
[Nginx反向代理] -- 负责SSL终止、访问控制
|
v
[FastAPI中间层 :8000] -- 负责并发控制、请求队列、日志记录
|
v
[Ollama :11434] -- 实际模型推理
|
v
[A10 GPU] -- 跑 qwen-legal 模型
监控侧:
[metrics_exporter :9090] --> [Prometheus] --> [Grafana] --> [钉钉告警]这个架构上线三个月,服务稳定性比我预期的好。中间出过一次GPU驱动更新导致的故障,监控5分钟内告警,10分钟内恢复。
给你的建议
如果你也要做类似的Ollama生产部署,我的建议是:
硬件层面:显卡驱动版本要和CUDA版本严格对应,别省这个检查的时间。
配置层面:OLLAMA_KEEP_ALIVE=-1是生产环境必须设的,模型自动卸载在生产场景只会制造麻烦。
并发层面:Ollama不是高并发服务,做好限流和队列,比无脑加并发配置更有效。
显存层面:根据你的显卡实际显存选合适的量化版本,同时在Modelfile里设context上限,防止超长输入OOM。
运维层面:监控和自动重启必须有,没有监控的生产服务就是定时炸弹。
Ollama是个好工具,但它的设计目标是开发者体验,不是生产可靠性。用在生产,你得自己补上那些缺的东西。
