Ollama: 监控与日志
监控是 Ollama 的健康体检——实时心跳、指标趋势、异常告警,防患于未然。
💡 提示:Prometheus + Grafana 是最主流的监控组合——Prometheus 负责定时采集和存储指标数据,Grafana 负责可视化展示和告警。Ollama 没有内置 Prometheus 端点,需通过 node_exporter(系统指标)+ DCGM exporter(GPU 指标)+ 自定义中间件(业务指标)组合实现。
📋 前置知识:需要先掌握以下内容
- 第19课:性能调优
1. 你将学到
- Ollama 日志配置与查看
- 关键监控指标采集
- Prometheus + Grafana 监控栈搭建
- 告警规则设计
- 慢查询诊断与日志分析
2. 一个 SaaS 创业者的真实故事
💡 提示: GPU OOM 是 Ollama 最常见的生产故障。建议在 Grafana 中设置 VRAM 使用率 > 90% 的预警告警,提前发现问题。同时监控
nvidia-smi 的 GPU 温度和功耗,过热也会导致性能骤降。
ℹ️ 信息: Ollama 没有内置的 Prometheus metrics 端点(
/metrics)。需通过 node_exporter(系统指标)+ DCGM exporter(GPU 指标)+ 自定义 FastAPI 中间件(业务指标)组合实现完整监控。
(1) 痛点:Ollama 突然变慢却不知道原因
Alice 的 SupportBot 某天响应时间从 2 秒飙升到 15 秒,但不知道是模型加载慢、GPU OOM 还是并发太高。没有监控数据只能猜。
(2) 解法:Prometheus + Grafana 实时监控
部署监控栈后,Grafana 仪表盘实时显示 GPU 利用率、请求延迟、内存占用。一眼发现是另一个进程占用了 GPU 内存:
BASH
# Prometheus scrapes Ollama metrics
# Grafana visualizes real-time dashboards
# Alertmanager sends notifications when anomalies detected
3. Ollama 日志配置
⚠️ 注意:Ollama 日志默认记录完整的请求和响应内容,可能包含用户输入的敏感信息(姓名、订单号等)。生产环境务必在反向代理层脱敏后再记录日志,或限制日志级别避免记录请求体。
(1) 日志来源与查看方式
| 平台 | 日志命令 | 说明 |
|---|---|---|
| Linux systemd | journalctl -u ollama -f |
实时跟踪 |
| Docker | docker logs -f ollama |
容器日志 |
| macOS | cat ~/.ollama/logs/server.log |
日志文件 |
| Windows | Event Viewer | 事件日志 |
(2) 调试日志开启
| 环境变量 | 用途 | 输出内容 |
|---|---|---|
| OLLAMA_DEBUG | 详细调试信息 | GPU 检测、模型加载细节 |
| OLLAMA_FLASH_ATTENTION | Flash Attention 日志 | 注意力机制状态 |
▶ 示例 1: 日志查看与分析
BASH
# Linux: real-time Ollama logs
sudo journalctl -u ollama -f --no-pager
# Filter for errors
sudo journalctl -u ollama | grep -i "error\|fail\|oom\|panic"
# Docker: real-time container logs
docker logs -f ollama --tail 100
# Enable debug logging
sudo systemctl edit ollama
[Service]
Environment="OLLAMA_DEBUG=1"
sudo systemctl restart ollama
# Check for GPU issues in logs
sudo journalctl -u ollama --since "1 hour ago" | grep -i "cuda\|gpu\|memory"
输出:
TEXT
📖 仅展示
# Ollama 命令执行成功
4. 关键监控指标
(1) 四大指标类别
| 类别 | 指标 | 采集方式 | 告警阈值 |
|---|---|---|---|
| GPU | 利用率、VRAM 使用、温度 | nvidia-smi / DCGM | > 95% 利用率 |
| 推理 | 延迟、Tokens/s、请求速率 | 自定义 exporter | > 10s 延迟 |
| 系统 | CPU、RAM、磁盘 IO | node_exporter | > 90% RAM |
| 应用 | 错误率、模型加载时间 | 日志分析 | > 5% 错误率 |
(2) Ollama 自定义指标
| 指标 | 采集方法 | 含义 |
|---|---|---|
| request_latency | API 计时 | 请求延迟 |
| tokens_per_second | 响应统计 | 生成 token 数 |
| model_load_time | 加载计时 | 模型加载时间 |
| active_models | API 查询 | 当前加载模型数 |
▶ 示例 2: 自定义指标采集
PYTHON
import ollama
import time
import json
from datetime import datetime
from pathlib import Path
class OllamaMetrics:
def __init__(self, log_file: str = "ollama_metrics.jsonl"):
self.log_file = Path(log_file)
def record_request(self, model: str, prompt: str) -> dict:
start = time.time()
try:
response = ollama.chat(
model=model,
messages=[{"role": "user", "content": prompt}],
stream=False
)
duration = time.time() - start
content = response["message"]["content"]
metrics = {
"timestamp": datetime.now().isoformat(),
"model": model,
"duration_s": round(duration, 3),
"response_length": len(content),
"estimated_tokens": len(content) // 4,
"tok_per_s": round(len(content) / 4 / duration, 1),
"status": "success"
}
except Exception as e:
duration = time.time() - start
metrics = {
"timestamp": datetime.now().isoformat(),
"model": model,
"duration_s": round(duration, 3),
"status": "error",
"error": str(e)
}
with open(self.log_file, "a") as f:
f.write(json.dumps(metrics) + "\n")
return metrics
def get_stats(self, minutes: int = 30) -> dict:
"""Calculate stats from recent metrics."""
cutoff = datetime.now().timestamp() - minutes * 60
durations = []
errors = 0
total = 0
if not self.log_file.exists():
return {}
with open(self.log_file) as f:
for line in f:
data = json.loads(line)
ts = datetime.fromisoformat(data["timestamp"]).timestamp()
if ts >= cutoff:
total += 1
durations.append(data.get("duration_s", 0))
if data.get("status") == "error":
errors += 1
if total == 0:
return {}
return {
"total_requests": total,
"error_rate": round(errors / total * 100, 1),
"avg_duration_s": round(sum(durations) / len(durations), 2),
"max_duration_s": round(max(durations), 2),
"p95_duration_s": round(sorted(durations)[int(len(durations) * 0.95)], 2)
}
# Usage
metrics = OllamaMetrics()
metrics.record_request("qwen2.5", "Hello")
print(metrics.get_stats())
输出:
TEXT
📖 仅展示
# 函数定义成功
5. Prometheus + Grafana 监控栈
(1) 监控架构
flowchart LR
A[Ollama<br/>+ Custom Exporter] --> B[Prometheus<br/>Metrics Store]
C[node_exporter<br/>System Metrics] --> B
D[DCGM Exporter<br/>GPU Metrics] --> B
B --> E[Grafana<br/>Dashboards]
E --> F[Alertmanager<br/>Notifications]
F --> G[Email/Slack]
(2) Docker Compose 监控栈
▶ 示例 3: 监控栈部署
YAML
# docker-compose.monitoring.yml
version: "3.8"
services:
prometheus:
image: prom/prometheus
container_name: prometheus
ports: ["9090:9090"]
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
restart: unless-stopped
grafana:
image: grafana/grafana
container_name: grafana
ports: ["3001:3000"]
volumes:
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
depends_on: [prometheus]
restart: unless-stopped
node-exporter:
image: prom/node-exporter
container_name: node-exporter
ports: ["9100:9100"]
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
restart: unless-stopped
volumes:
prometheus_data:
grafana_data:
YAML
# prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: "node"
static_configs:
- targets: ["node-exporter:9100"]
- job_name: "ollama_custom"
static_configs:
- targets: ["host.docker.internal:8000"] # Custom exporter
输出:
TEXT
📖 仅展示
监控栈部署成功,Prometheus/Grafana 服务运行正常
6. 告警规则
(1) 关键告警规则
| 告警 | 条件 | 级别 | 通知 |
|---|---|---|---|
| GPU OOM | VRAM > 95% 持续 2 分钟 | 严重 | 立即通知 |
| 高延迟 | P95 > 10s 持续 5 分钟 | 警告 | 邮件 |
| 服务不可用 | 健康检查失败 3 次 | 严重 | 立即通知 |
| 错误率飙升 | 错误率 > 10% 持续 3 分钟 | 警告 | 邮件 |
| 磁盘空间不足 | 可用空间 < 10% | 警告 | 邮件 |
▶ 示例 4: Prometheus 告警规则
YAML
# alert_rules.yml
groups:
- name: ollama_alerts
rules:
- alert: OllamaHighLatency
expr: histogram_quantile(0.95, rate(ollama_request_duration_seconds_bucket[5m])) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "Ollama P95 latency above 10s"
description: "Current P95: {{ $value }}s"
- alert: OllamaServiceDown
expr: up{job="ollama_custom"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "Ollama service is down"
description: "No response from Ollama for 2 minutes"
- alert: HighGPUMemory
expr: DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_TOTAL > 0.95
for: 2m
labels:
severity: critical
annotations:
summary: "GPU memory usage above 95%"
输出:
TEXT
📖 仅展示
# 告警规则加载成功
kubectl exec -it prometheus-0 -- wget -qO- http://localhost:9090/api/v1/rules | head
# {"status":"success","data":{"groups":[{"name":"ollama_alerts","rules":[...]}]}}
▶ 示例 5: 慢查询诊断
BASH
#!/bin/bash
# Slow query diagnosis script
LOG="ollama_metrics.jsonl"
echo "=== Slow Query Report (last 60 minutes) ==="
echo ""
if [ -f "$LOG" ]; then
# Find requests with duration > 5 seconds
python3 -c "
import json, sys
from datetime import datetime, timedelta
cutoff = datetime.now() - timedelta(hours=1)
slow = []
with open('$LOG') as f:
for line in f:
data = json.loads(line)
ts = datetime.fromisoformat(data['timestamp'])
if ts >= cutoff and data.get('duration_s', 0) > 5:
slow.append(data)
if slow:
print(f'Found {len(slow)} slow requests (>5s):')
for s in slow[:10]:
print(f\" {s['timestamp']}: {s['duration_s']}s, model={s.get('model','?')}, status={s.get('status','?')}\")
else:
print('No slow requests found in the last hour.')
"
else
echo "No metrics log found. Enable OllamaMetrics first."
fi
echo ""
echo "=== Current GPU Status ==="
nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total --format=csv 2>/dev/null || echo "No GPU detected"
输出:
TEXT
📖 仅展示
NAME ID SIZE
llama3.2:latest a80... 2.0 GB
mistral:latest 61... 4.1 GB
7. 综合示例:监控仪表盘配置
PYTHON
# ============================================
# Comprehensive: Monitoring dashboard config
# Prometheus + Grafana + Custom exporter
# ============================================
import json
from http.server import HTTPServer, BaseHTTPRequestHandler
import ollama
import time
import threading
# Custom Prometheus exporter for Ollama
class OllamaExporter(BaseHTTPRequestHandler):
metrics = {
"ollama_request_duration_seconds": [],
"ollama_requests_total": 0,
"ollama_errors_total": 0,
"ollama_active_models": 0,
}
def do_GET(self):
if self.path == "/metrics":
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.end_headers()
output = []
output.append(f"# HELP ollama_requests_total Total requests")
output.append(f"# TYPE ollama_requests_total counter")
output.append(f"ollama_requests_total {self.metrics['ollama_requests_total']}")
output.append(f"# HELP ollama_errors_total Total errors")
output.append(f"# TYPE ollama_errors_total counter")
output.append(f"ollama_errors_total {self.metrics['ollama_errors_total']}")
output.append(f"# HELP ollama_request_duration_seconds Request duration")
output.append(f"# TYPE ollama_request_duration_seconds histogram")
if self.metrics["ollama_request_duration_seconds"]:
durations = self.metrics["ollama_request_duration_seconds"][-100:]
avg = sum(durations) / len(durations)
output.append(f'ollama_request_duration_seconds_avg {avg:.3f}')
output.append(f'ollama_request_duration_seconds_max {max(durations):.3f}')
# Active models
try:
models = ollama.list()
active = len(models.get("models", []))
output.append(f"# HELP ollama_active_models Number of loaded models")
output.append(f"# TYPE ollama_active_models gauge")
output.append(f"ollama_active_models {active}")
except Exception:
pass
self.wfile.write("\n".join(output).encode())
elif self.path == "/health":
self.send_response(200)
self.end_headers()
self.wfile.write(b"OK")
else:
self.send_response(404)
self.end_headers()
@classmethod
def record_request(cls, duration: float, is_error: bool = False):
cls.metrics["ollama_requests_total"] += 1
cls.metrics["ollama_request_duration_seconds"].append(duration)
if is_error:
cls.metrics["ollama_errors_total"] += 1
def run_exporter(port: int = 8000):
server = HTTPServer(("0.0.0.0", port), OllamaExporter)
print(f"Ollama exporter running on :{port}/metrics")
server.serve_forever()
# Start exporter in background thread
# threading.Thread(target=run_exporter, daemon=True).start()
# Then configure Prometheus to scrape http://localhost:8000/metrics
❓ 常见问题
Q Ollama 有内置 Prometheus 端点吗?
A Ollama 本身不暴露 Prometheus 指标。需自建 exporter(如本课示例)或用 Ollama API 采集指标推送到 Prometheus。
Q Grafana 仪表盘怎么快速搭建?
A Grafana 有现成的 Node Exporter 和 DCGM 仪表盘模板(Import by ID:1860 for Node,12239 for DCGM)。Ollama 自定义指标需自建仪表盘。
Q 日志量太大怎么办?
A 设置日志轮转(logrotate for systemd)或使用 Loki 替代文件日志。只记录错误和慢查询,正常请求用 metrics 代替 logs。
Q 告警太频繁怎么调?
A 增大 持续时间(从 1m 到 5m),提高阈值(延迟从 5s 到 10s),按严重程度分级(critical 立即通知,warning 只记录)。
Q 如何监控 Docker 内的 Ollama?
A Docker stats 看 CPU/内存,nvidia-smi 看 GPU。或用 cAdvisor 采集容器指标推送到 Prometheus。
Q 监控栈对性能有影响吗?
A Prometheus scraping 每 15 秒一次,对 Ollama 几乎无影响。自定义 exporter 的健康检查请求很小(< 10 tokens)。
📖 小节
- Ollama 日志通过 journalctl/docker logs 查看,OLLAMA_DEBUG 开启详细日志
- 四大监控类别:GPU(VRAM/温度)、推理(延迟/速度)、系统(CPU/RAM)、应用(错误率)
- 自定义 exporter 将 Ollama API 指标暴露为 Prometheus 格式
- Prometheus + Grafana 是标准监控栈,15 分钟部署完成
- 告警规则按严重程度分级:critical 立即通知,warning 记录后通知
- 慢查询诊断:日志过滤 + GPU 状态检查 + 资源分析
📝 作业
- 基础题(难度⭐):配置 Ollama 日志,使用 journalctl 查看 Ollama 服务日志,过滤错误信息。
- 进阶题(难度⭐⭐):部署 Prometheus + Grafana 监控栈,导入 Node Exporter 仪表盘,确认系统指标可见。
- 挑战题(难度⭐⭐⭐):实现自定义 Ollama exporter,在 Grafana 中创建 Ollama 专用仪表盘(延迟、Tokens/s、错误率),配置至少 2 条告警规则。