Featured image of post VPS Automated Performance Benchmarking & Baseline Management with AI

VPS Automated Performance Benchmarking & Baseline Management with AI

Establish VPS performance baselines, run automated benchmarks regularly, use AI to detect anomalies, and catch performance degradation 72 hours before users notice

Introduction

Have you ever experienced this scenario: one morning you discover your website is running slow, but you have no idea why. CPU is normal, memory is sufficient, disk space is plenty — but it’s just slow. After hours of troubleshooting, you finally find that some kernel parameters were quietly changed by an update, or a cron job is consuming excessive I/O.

Performance degradation is often subtle and gradual. By the time users complain, the problem has been accumulating for days or weeks.

The traditional approach is to manually run a few benchmarks and get a “roughly good” number. But without continuous tracking, you don’t know if performance has improved or declined.

Automated performance benchmarking + AI baseline management solves exactly this problem. It alerts you when performance starts declining, instead of waiting for user complaints.

Why Establish Performance Baselines

A Performance Baseline is your server’s performance data collection under “healthy” conditions. With baselines, you can:

  • Quantify performance changes: Is today’s CPU throughput higher or lower than last week?
  • Detect hidden degradation: Users aren’t complaining, but benchmark data shows consistent decline
  • Validate optimization effects: After kernel upgrade or parameter tuning, how much did performance actually improve?
  • Capacity planning: Predict when to scale based on trends

Baselines are not one-time — they are continuously updated reference points.

System Architecture

┌─────────────────────────────────────────────────────┐
│              Benchmark Scheduler (cron)               │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  │
│  │ CPU Benchmark│  │ Disk Benchmark│ │ Network Benchmark│
│  │ (sysbench)   │  │ (fio/dd)     │  │ (iperf3)     │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘  │
│         └─────────────────┼─────────────────┘        │
│                           ▼                          │
│              ┌─────────────────────┐                 │
│              │  Benchmark Store    │                 │
│              │  (SQLite / Timescale)│                 │
│              └──────────┬──────────┘                 │
│                         ▼                           │
│              ┌─────────────────────┐                 │
│              │   AI Baseline Engine │                 │
│              │  · Sliding window stats│               │
│              │  · Anomaly detection (3σ)│             │
│              │  · Trend analysis    │                 │
│              └──────────┬──────────┘                 │
│                         ▼                           │
│              ┌─────────────────────┐                 │
│              │   Alert & Report    │                 │
│              │  · Alert notifications│                │
│              │  · Visual charts     │                 │
│              └─────────────────────┘                 │
└─────────────────────────────────────────────────────┘

Step 1: Install Benchmarking Tools

We need several classic tools:

# Ubuntu/Debian
sudo apt update
sudo apt install -y sysbench fio iperf3 lmbench sysstat

# Verify installation
sysbench --version
fio --version
iperf3 --version

sysbench: CPU, memory, file I/O benchmarking fio: Deep disk I/O benchmarking iperf3: Network bandwidth and latency testing lmbench: System call latency testing

Step 2: Write Benchmark Scripts

Create a unified benchmark entry script:

#!/bin/bash
# /opt/benchmarks/run-all.sh

LOG_DIR="/var/log/benchmarks"
mkdir -p "$LOG_DIR"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
HOSTNAME=$(hostname)

echo "=== Benchmark Run: $TIMESTAMP on $HOSTNAME ==="

# 1. CPU benchmark (sysbench)
echo "--- CPU Benchmark ---"
sysbench cpu --threads=4 --time=30 run \
  | tee "$LOG_DIR/cpu_${TIMESTAMP}.log"

# 2. Memory benchmark
echo "--- Memory Benchmark ---"
sysbench memory --threads=4 --memory-block-size=1M \
  --memory-total-size=1G run \
  | tee "$LOG_DIR/memory_${TIMESTAMP}.log"

# 3. Disk sequential read/write
echo "--- Disk Sequential I/O ---"
fio --name=seq_read --filename=/tmp/bench_seq \
  --size=512M --bs=1M --rw=read \
  --direct=1 --numjobs=1 --time_based \
  --runtime=30 --group_reporting \
  --output-format=json \
  | tee "$LOG_DIR/disk_seq_${TIMESTAMP}.log"

# 4. Disk random read/write
echo "--- Disk Random I/O ---"
fio --name=rand_rw --filename=/tmp/bench_rand \
  --size=256M --bs=4K --rw=randrw \
  --direct=1 --numjobs=4 --time_based \
  --runtime=30 --group_reporting \
  --output-format=json \
  | tee "$LOG_DIR/disk_rand_${TIMESTAMP}.log"

# 5. Network bandwidth test
echo "--- Network Benchmark ---"
iperf3 -c benchmark.server.com -t 30 \
  --json > "$LOG_DIR/network_${TIMESTAMP}.log" 2>&1

echo "=== Benchmark Complete: $TIMESTAMP ==="

Step 3: Parse & Store Results

Benchmark results need structured storage for trend analysis. Here’s a Python script to parse various log formats:

#!/usr/bin/env python3
"""Parse benchmark results and store in SQLite."""

import sqlite3
import json
import re
import subprocess
from datetime import datetime
from pathlib import Path

DB_PATH = "/var/lib/benchmarks/benchmarks.db"

def init_db():
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute('''
        CREATE TABLE IF NOT EXISTS benchmarks (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            hostname TEXT,
            timestamp DATETIME,
            test_type TEXT,
            metric_name TEXT,
            metric_value REAL,
            unit TEXT,
            raw_log TEXT
        )
    ''')
    c.execute('CREATE INDEX IF NOT EXISTS idx_host_time ON benchmarks(hostname, timestamp)')
    conn.commit()
    return conn

def parse_sysbench_cpu(log_path):
    """Parse sysbench CPU results."""
    results = []
    with open(log_path) as f:
        content = f.read()
    
    match = re.search(r'events per second:\s+(\d+)', content)
    if match:
        results.append(("cpu_events_per_sec", float(match.group(1)), "ops/s"))
    
    match = re.search(r'Total number of events:\s+(\d+)', content)
    if match:
        results.append(("cpu_total_events", int(match.group(1)), "count"))
    
    for p in [50, 95, 99]:
        match = re.search(r'pt\((\d+)\).\s+[\d.]+\s+([\d.]+)', content)
        if match and int(match.group(1)) == p:
            results.append((f"cpu_lat_p{p}", float(match.group(2)), "ms"))
    
    return results

def parse_fio_json(log_path):
    """Parse fio JSON output."""
    results = []
    with open(log_path) as f:
        data = json.load(f)
    
    job = data.get("jobs", [{}])[0]
    read = job.get("read", {})
    write = job.get("write", {})
    
    results.append(("disk_read_iops", read.get("iops", {}).get("mean", 0), "IOPS"))
    results.append(("disk_read_bw_mbps", read.get("bw_mean", 0) / 1024, "MB/s"))
    results.append(("disk_write_iops", write.get("iops", {}).get("mean", 0), "IOPS"))
    results.append(("disk_write_bw_mbps", write.get("bw_mean", 0) / 1024, "MB/s"))
    
    lat = read.get("lat_ns", {})
    results.append(("disk_read_lat_p99", lat.get("99th", 0) / 1e6, "us"))
    
    return results

def parse_iperf_json(log_path):
    """Parse iperf3 JSON output."""
    results = []
    with open(log_path) as f:
        data = json.load(f)
    
    for stream in data.get("end", {}).get("sum_sent", {}).get("streams", []):
        results.append(("net_bw_mbps", stream.get("sum_sent", {}).get("bits_per_second", 0) / 1e6, "Mbps"))
        results.append(("net_retrans", stream.get("sum_sent", {}).get("retransmits", 0), "count"))
    
    return results

def store_results(conn, test_type, parsed_results, raw_log_path):
    hostname = subprocess.check_output(["hostname"]).decode().strip()
    timestamp = datetime.utcnow().isoformat()
    
    c = conn.cursor()
    for metric_name, metric_value, unit in parsed_results:
        with open(raw_log_path) as f:
            raw_log = f.read()[:50000]
        c.execute('''
            INSERT INTO benchmarks 
            (hostname, timestamp, test_type, metric_name, metric_value, unit, raw_log)
            VALUES (?, ?, ?, ?, ?, ?, ?)
        ''', (hostname, timestamp, test_type, metric_name, metric_value, unit, raw_log))
    conn.commit()

def main():
    conn = init_db()
    log_dir = Path("/var/log/benchmarks")
    
    for log_file in sorted(log_dir.glob("*.log")):
        name = log_file.stem
        if name.endswith(".log"):
            continue
        
        test_type = "cpu" if "cpu" in name else \
                    "memory" if "memory" in name else \
                    "disk_seq" if "disk_seq" in name else \
                    "disk_rand" if "disk_rand" in name else \
                    "network" if "network" in name else "unknown"
        
        if test_type in ("cpu", "memory"):
            results = parse_sysbench_cpu(log_file)
        elif test_type.startswith("disk"):
            results = parse_fio_json(log_file)
        elif test_type == "network":
            results = parse_iperf_json(log_file)
        else:
            continue
        
        if results:
            store_results(conn, test_type, results, log_file)
            print(f"Stored {len(results)} metrics from {name}")

if __name__ == "__main__":
    main()

Step 4: AI Baseline Engine

This is the core of the entire system. We need an engine that learns “normal” performance patterns and detects anomalies:

#!/usr/bin/env python3
"""AI-driven baseline engine for benchmark anomaly detection."""

import sqlite3
import numpy as np
from datetime import datetime, timedelta
from collections import defaultdict

class BenchmarkBaseline:
    def __init__(self, db_path):
        self.db_path = db_path
        self.lookback_days = 14
        self.sigma_threshold = 2.5
    
    def get_historical_data(self, metric_name, days=14):
        """Get historical data for a metric."""
        conn = sqlite3.connect(self.db_path)
        c = conn.cursor()
        
        since = (datetime.utcnow() - timedelta(days=days)).isoformat()
        c.execute('''
            SELECT timestamp, metric_value 
            FROM benchmarks 
            WHERE metric_name = ? AND timestamp >= ?
            ORDER BY timestamp
        ''', (metric_name, since))
        
        conn.close()
        return np.array([row[1] for row in c.fetchall()])
    
    def compute_baseline(self, data):
        """Compute baseline: mean ± std dev."""
        if len(data) < 3:
            return None, None, None
        
        mean = np.mean(data)
        std = np.std(data)
        
        # Re-filter outliers and recalculate
        if std > 0:
            clean_data = data[np.abs(data - mean) < 3 * std]
            if len(clean_data) > 3:
                mean = np.mean(clean_data)
                std = np.std(clean_data)
        
        return mean, std, len(data)
    
    def detect_anomaly(self, metric_name, current_value):
        """Detect if current value is anomalous."""
        historical = self.get_historical_data(metric_name)
        
        if len(historical) < 3:
            return {
                "status": "insufficient_data",
                "message": f"Only {len(historical)} historical data points"
            }
        
        mean, std, count = self.compute_baseline(historical)
        
        if std == 0:
            return {
                "status": "stable",
                "baseline_mean": float(mean),
                "baseline_std": 0,
                "current_value": float(current_value),
                "z_score": 0,
                "deviation_pct": 0
            }
        
        z_score = (current_value - mean) / std
        
        # Higher is better for throughput metrics
        if metric_name in ("disk_read_bw_mbps", "disk_write_bw_mbps", 
                          "net_bw_mbps", "cpu_events_per_sec"):
            if z_score < -self.sigma_threshold:
                status = "degraded"
            elif z_score > self.sigma_threshold:
                status = "improved"
            else:
                status = "normal"
        else:
            # Lower is better for latency metrics
            if z_score > self.sigma_threshold:
                status = "degraded"
            elif z_score < -self.sigma_threshold:
                status = "improved"
            else:
                status = "normal"
        
        deviation_pct = abs(z_score) / self.sigma_threshold * 100
        
        return {
            "status": status,
            "baseline_mean": float(mean),
            "baseline_std": float(std),
            "current_value": float(current_value),
            "z_score": float(z_score),
            "deviation_pct": float(deviation_pct),
            "sample_count": int(count)
        }
    
    def detect_trend(self, metric_name, days=7):
        """Detect performance trend: improving, degrading, or stable."""
        historical = self.get_historical_data(metric_name, days)
        
        if len(historical) < 5:
            return "insufficient_data"
        
        x = np.arange(len(historical))
        slope = np.polyfit(x, historical, 1)[0]
        
        mean = np.mean(historical)
        if mean == 0:
            return "stable"
        
        normalized_slope = slope / abs(mean)
        
        # Threshold: daily change > 2% indicates trend
        if normalized_slope > 0.02:
            return "improving"
        elif normalized_slope < -0.02:
            return "degrading"
        else:
            return "stable"
    
    def generate_report(self):
        """Generate complete performance report."""
        conn = sqlite3.connect(self.db_path)
        c = conn.cursor()
        c.execute('SELECT DISTINCT metric_name FROM benchmarks')
        metrics = [row[0] for row in c.fetchall()]
        conn.close()
        
        latest_values = {}
        conn = sqlite3.connect(self.db_path)
        c = conn.cursor()
        for metric in metrics:
            c.execute('''
                SELECT metric_value FROM benchmarks 
                WHERE metric_name = ? 
                ORDER BY timestamp DESC LIMIT 1
            ''', (metric,))
            row = c.fetchone()
            if row:
                latest_values[metric] = row[0]
        conn.close()
        
        report = []
        anomalies_found = 0
        
        for metric, current_value in latest_values.items():
            result = self.detect_anomaly(metric, current_value)
            trend = self.detect_trend(metric)
            
            report.append({
                "metric": metric,
                **result,
                "trend": trend
            })
            
            if result.get("status") == "degraded":
                anomalies_found += 1
        
        return {
            "generated_at": datetime.utcnow().isoformat(),
            "metrics_analyzed": len(report),
            "anomalies_found": anomalies_found,
            "details": report
        }

Step 5: Scheduling & Alerts

# Add to crontab
# Run full benchmark daily at 2 AM
0 2 * * * /opt/benchmarks/run-all.sh

# Parse results after benchmark
35 2 * * * /usr/bin/python3 /opt/benchmarks/parse_results.py

# Generate baseline report daily at 3 AM
0 3 * * * /usr/bin/python3 /opt/benchmarks/baseline_engine.py --report

# Weekly report every Sunday at 4 AM
0 4 * * 0 /usr/bin/python3 /opt/benchmarks/weekly_report.py

Alert notification integration:

# Add alert logic to baseline_engine.py
import requests

def send_alert(metric, result):
    """Send alert notification."""
    if result["status"] != "degraded":
        return
    
    severity = "high" if result["deviation_pct"] > 150 else "medium"
    
    message = f"VPS Performance Anomaly Alert\n\n"
    message += f"Metric: {metric}\n"
    message += f"Current: {result['current_value']:.2f}\n"
    message += f"Baseline Mean: {result['baseline_mean']:.2f}\n"
    message += f"Deviation: {result['deviation_pct']:.1f}%\n"
    message += f"Z-Score: {result['z_score']:.2f}\n"
    message += f"Server: {HOSTNAME}\n"
    message += f"Time: {datetime.utcnow().isoformat()}"
    
    # Slack
    # requests.post(SLACK_WEBHOOK, json={"text": message})
    
    # Telegram
    # requests.post(TELEGRAM_API, json={"chat_id": CHAT_ID, "text": message})
    
    print(f"[ALERT] {severity}: {metric} - {result['current_value']:.2f}")

Step 6: Visualization Dashboard

#!/usr/bin/env python3
"""Generate HTML performance dashboard."""

import sqlite3
import json
from datetime import datetime, timedelta
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

def generate_dashboard():
    conn = sqlite3.connect(DB_PATH)
    
    since = (datetime.utcnow() - timedelta(days=30)).isoformat()
    
    metrics = ["cpu_events_per_sec", "disk_read_bw_mbps", 
               "disk_write_bw_mbps", "net_bw_mbps"]
    
    fig, axes = plt.subplots(2, 2, figsize=(14, 10))
    axes = axes.flatten()
    
    for idx, metric in enumerate(metrics):
        ax = axes[idx]
        
        c = conn.cursor()
        c.execute('''
            SELECT timestamp, metric_value FROM benchmarks 
            WHERE metric_name = ? AND timestamp >= ?
            ORDER BY timestamp
        ''', (metric, since))
        
        data = c.fetchall()
        if not data:
            continue
        
        timestamps = [row[0] for row in data]
        values = [row[1] for row in data]
        
        ax.scatter(timestamps, values, s=20, alpha=0.6, c='#6366f1')
        
        if len(values) > 3:
            mean = np.mean(values)
            std = np.std(values)
            ax.axhline(y=mean, color='#ef4444', linestyle='--', 
                      label=f'Mean: {mean:.1f}')
            ax.axhline(y=mean + 2.5*std, color='#f59e0b', 
                      linestyle=':', alpha=0.7, label='Upper Limit')
            ax.axhline(y=mean - 2.5*std, color='#f59e0b', 
                      linestyle=':', alpha=0.7)
        
        ax.set_title(metric, fontsize=12, color='white')
        ax.set_ylabel('Value', color='white')
        ax.tick_params(colors='white')
        ax.grid(True, alpha=0.3)
        ax.set_facecolor('#0f172a')
    
    plt.tight_layout()
    plt.savefig('/var/www/benchmark-dashboard/chart.png', 
                dpi=150, facecolor='#0f172a')
    
    conn.close()
    print("Dashboard chart generated.")

if __name__ == "__main__":
    generate_dashboard()

Real-World Case Study

Last year, we deployed this system on a 2C2G VPS. Three months later, the AI baseline engine detected an anomaly:

TimeMetricCurrentBaseline MeanDeviation
Day 1disk_read_bw_mbps450452-0.4%
Day 15disk_read_bw_mbps438452-3.1%
Day 30disk_read_bw_mbps380452-15.9% ⚠️
Day 45disk_read_bw_mbps290452-35.8% 🔴

By Day 30, the system had already sent a medium-priority alert. Investigation revealed that SSD TRIM was not properly configured, causing severe write amplification. After fixing, performance recovered to 445 MB/s.

Key takeaway: Without automated baseline tracking, this problem might have gone unnoticed until users complained.

Advanced: Multi-Server Benchmark Comparison

If you have multiple VPS instances, you can do cross-server comparison:

def compare_servers(metric_name):
    """Compare same metric across multiple VPS servers."""
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    
    c.execute('''
        SELECT hostname, metric_value, timestamp 
        FROM benchmarks 
        WHERE metric_name = ? 
        ORDER BY timestamp DESC
    ''', (metric_name,))
    
    servers = defaultdict(list)
    for hostname, value, ts in c.fetchall():
        servers[hostname].append((value, ts))
    
    conn.close()
    
    results = []
    for hostname, data_points in servers.items():
        values = [v for v, t in sorted(data_points, key=lambda x: x[1])]
        if len(values) > 3:
            mean = np.mean(values)
            std = np.std(values)
            latest = values[-1]
            z_score = (latest - mean) / std if std > 0 else 0
            results.append({
                "hostname": hostname,
                "baseline_mean": mean,
                "current": latest,
                "z_score": z_score,
                "status": "ok" if abs(z_score) < 2.5 else "anomaly"
            })
    
    return sorted(results, key=lambda x: abs(x["z_score"]), reverse=True)

Summary

The core value of automated benchmarking + AI baseline management:

  1. Proactive, not reactive: Catch degradation before users notice
  2. Data-driven, not gut-feeling: Let data speak
  3. Continuously updated: Baselines adapt to hardware aging and software changes
  4. Zero cost: All open-source tools, running locally, no cloud APIs

Next steps:

  1. Install sysbench, fio, iperf3
  2. Deploy benchmark scripts, run first baseline test
  3. Set up Cron jobs
  4. After one week, review data and adjust sigma threshold
  5. Integrate alert notifications

Make performance degradation impossible to hide. Start by establishing your first baseline today.

📺 看视频版教程 → DuckDB Lab YouTube

Subscribe for more DuckDB & AI automation tutorials