1. 项目概述一个开源工具的价值与定位如果你是一名开发者、运维工程师或者任何需要管理云服务器、VPS、独立主机的人那么“服务器资源监控”这件事你肯定没少操心。CPU、内存、磁盘、网络流量这些指标就像汽车的仪表盘不看不行但每次登录服务器敲命令又太麻烦。市面上的监控方案很多从Zabbix、Prometheus这样的重型武器到各种商业SaaS服务功能强大但往往伴随着复杂的部署、高昂的成本或者对个人用户而言过度的功能冗余。今天要聊的这个项目aezizhu/a2zusage就是在这个背景下诞生的一个非常有意思的解决方案。它的名字直译过来就是“A到Z的使用情况”听起来就带着一种“全面”、“简洁”的意味。简单来说它是一个轻量级的、自托管的服务器资源监控与展示工具。它的核心价值在于用极简的方式将你关心的核心系统指标通过一个清晰、美观的Web界面实时展示出来并且支持历史数据的回溯。这就像给你的每台服务器都装了一个专属的、本地化的“健康仪表盘”。这个项目特别适合哪些场景呢首先是个人开发者或小团队拥有几台到十几台云服务器需要一个低成本、无依赖的监控方案。其次是对数据隐私有要求的场景所有数据都在自己的服务器上无需担心上传到第三方。再者它非常适合作为学习项目你可以清晰地看到如何通过Shell脚本采集数据、如何用简单的Web后端如Python Flask提供API、如何用前端图表库如Chart.js进行可视化整个技术栈清晰明了是学习全栈开发的绝佳案例。接下来我们就深入拆解这个项目看看它是如何从零开始构建一个既实用又优雅的服务器监控工具的。我会结合常见的实践补充其可能的技术实现细节、部署中的坑点以及如何根据自身需求进行定制化扩展。2. 核心架构与设计思路拆解一个监控工具无论大小其核心逻辑无非是“采集 - 存储 - 展示”。a2zusage的设计哲学显然是“轻量”和“自包含”这意味着它要尽可能减少外部依赖用最通用的技术栈实现功能。2.1 数据采集层Shell脚本的智慧数据采集是整个系统的基石。在Linux服务器上采集系统指标最直接、最通用的工具就是Shell命令。a2zusage极有可能采用一系列Shell脚本来完成这项工作。这样做的好处是零依赖任何标准的Linux发行版都原生支持。CPU使用率通常通过解析/proc/stat文件来计算。一个经典的命令是top -bn1 | grep Cpu(s) | awk {print $2}或者使用mpstat命令。这里的关键是理解“用户态”、“系统态”、“空闲”等时间片的含义并计算出一个总体的使用百分比。内存使用率通过/proc/meminfo文件获取。命令如free -m | awk NR2{printf %.2f, $3*100/$2}可以计算出已用内存的百分比。这里需要注意缓存cache和缓冲区buffer是否计入“已使用”不同的计算方式会导致结果差异工具需要明确其计算逻辑。磁盘使用率使用df命令。例如df -h / | awk NR2{print $5}可以获取根分区使用率的百分比带%号。对于多磁盘监控可能需要遍历所有挂载点。网络流量这是相对复杂的一环。需要定时读取/proc/net/dev文件记录特定网卡如eth0或ens3的rx_bytes接收字节和tx_bytes发送字节然后通过两次读取的时间差和字节差来计算实时速率。这里要处理网卡名因发行版和云厂商而异的问题。系统负载直接读取/proc/loadavg文件即可获得1分钟、5分钟、15分钟的平均负载。注意Shell脚本采集的精度和性能需要权衡。过于频繁的采集如每秒会给系统带来额外开销也可能产生大量存储数据。通常每分钟采集一次对于大多数监控场景已经足够。a2zusage很可能通过一个cron定时任务每分钟执行一次采集脚本。2.2 数据存储层简单文件的持久化对于轻量级工具引入一个完整的数据库如MySQL、PostgreSQL或时序数据库如InfluxDB显得过于笨重。a2zusage更可能采用文件存储例如CSV逗号分隔值或JSON格式。存储格式每采集一次数据就向一个日志文件中追加一行记录。例如timestamp,cpu_usage,mem_usage,disk_usage,net_rx_rate,net_tx_rate,load1。JSON格式则更具可读性但文件体积稍大。存储策略为了控制文件大小需要实现日志轮转log rotation。例如可以按天分割文件或者当文件大小超过一定阈值如10MB时自动归档旧文件并创建新文件。一个简单的实现是使用logrotate工具配合自定义配置。优缺点文件存储的优势是极度简单无需任何额外服务。缺点是查询效率不高当需要查询长时间范围的历史数据时需要顺序读取和解析多个文件。但对于个人或小规模使用这个缺点完全可以接受。2.3 数据展示层轻量级Web服务这是用户直接交互的部分。a2zusage需要提供一个Web界面来展示实时数据和历史图表。后端API需要一个简单的Web框架来提供数据接口。Python的Flask或Bottle框架是绝佳选择它们极其轻量几行代码就能启动一个HTTP服务。这个后端主要做两件事提供实时数据当接收到前端请求时直接调用采集脚本或读取一个由采集脚本实时更新的状态文件将最新的系统指标以JSON格式返回。提供历史数据根据前端传递的时间范围参数如“最近1小时”、“最近24小时”从存储的文件中读取、聚合相应的数据点再返回给前端。这里可能涉及数据采样比如原始数据是每分钟一个点当查询“最近7天”时可以每小时取一个平均值返回以减少传输数据量。前端界面使用HTML、CSS和JavaScript构建。核心是图表库Chart.js 因其简单、美观、功能足够而成为热门选择。前端通过定时如每10秒调用后端的实时数据接口来更新仪表盘上的数字和实时图表如最近几分钟的曲线。同时提供时间选择器让用户可以查看不同时间段的历史趋势图。部署方式整个应用可以打包成一个服务。后端Web服务通常以后台进程如使用systemd运行。采集脚本由cron驱动。前端静态文件由后端服务一并托管或通过Nginx等Web服务器托管。这个架构清晰地将数据流分开每一层都可以独立替换或升级。例如如果你觉得文件存储查询太慢可以将其替换为SQLite数据库而无需改动采集和展示层的大部分代码。3. 从零开始实现一个类似的监控工具理解了设计思路后我们可以动手实现一个简化版的a2zusage。这里我将以PythonFlask作为后端Shell脚本采集Chart.js作为前端的方案为例展示关键步骤。3.1 环境准备与项目结构首先确保你的服务器上安装了Python3和pip。然后创建项目目录。mkdir ~/server-monitor cd ~/server-monitor mkdir scripts static templatesscripts/: 存放数据采集Shell脚本。static/: 存放前端CSS、JavaScript文件如Chart.js。templates/: 存放HTML模板文件。安装必要的Python包pip install flask3.2 编写数据采集脚本创建scripts/collect.sh#!/bin/bash # 定义数据存储路径 DATA_FILE/tmp/system_metrics.csv TIMESTAMP$(date %s) # 1. 采集CPU使用率取1秒内的平均值更准确 CPU_USAGE$(top -bn2 | grep Cpu(s) | tail -1 | awk -F[, ] {printf %.1f, 100 - $8}) # 另一种方式使用 /proc/stat 计算更轻量但需要记录上次状态此处用top简单示例 # 2. 采集内存使用率 MEM_USAGE$(free | awk NR2{printf %.1f, $3*100/$2}) # 3. 采集根分区磁盘使用率 DISK_USAGE$(df / | awk NR2{print $5} | sed s/%//) # 4. 采集网络流量示例网卡eth0请根据实际情况修改 # 首次运行需要创建记录文件这里简化处理仅输出当前总字节数 NET_INFO$(cat /proc/net/dev | grep -w eth0) RX_BYTES$(echo $NET_INFO | awk {print $2}) TX_BYTES$(echo $NET_INFO | awk {print $10}) # 5. 采集系统负载1分钟 LOAD_1$(cat /proc/loadavg | awk {print $1}) # 将数据写入CSV文件 echo $TIMESTAMP,$CPU_USAGE,$MEM_USAGE,$DISK_USAGE,$RX_BYTES,$TX_BYTES,$LOAD_1 $DATA_FILE # 简单的日志轮转如果文件超过1000行保留最近500行 if [ $(wc -l $DATA_FILE) -gt 1000 ]; then tail -n 500 $DATA_FILE $DATA_FILE.tmp mv $DATA_FILE.tmp $DATA_FILE fi给脚本执行权限chmod x scripts/collect.sh。然后通过cron设置每分钟执行一次crontab -e添加一行* * * * * /home/yourname/server-monitor/scripts/collect.sh实操心得网络流量采集是难点。上面的脚本只记录了累计字节数要计算速率需要在脚本内部或后端API中记录上一次的字节数和时间戳然后做差计算。一个更健壮的做法是在脚本中不仅记录当前值还计算并直接输出速率。例如在脚本内维护一个状态文件记录上次的RX_BYTES、TX_BYTES和TIMESTAMP。3.3 构建Flask后端API创建app.pyfrom flask import Flask, jsonify, render_template, request import os, csv, time, json from datetime import datetime, timedelta app Flask(__name__) DATA_FILE /tmp/system_metrics.csv def parse_metrics_file(): 读取并解析CSV数据文件 metrics [] if not os.path.exists(DATA_FILE): return metrics with open(DATA_FILE, r) as f: reader csv.reader(f) for row in reader: if len(row) 7: # 确保数据格式正确 try: ts, cpu, mem, disk, rx, tx, load row metrics.append({ timestamp: int(ts), cpu: float(cpu), memory: float(mem), disk: float(disk), net_rx: int(rx), net_tx: int(tx), load: float(load) }) except ValueError: continue # 跳过格式错误行 return metrics app.route(/) def index(): 提供主页面 return render_template(index.html) app.route(/api/current) def get_current_metrics(): 获取最新一条数据作为当前状态 metrics parse_metrics_file() if metrics: # 计算网络速率简化假设每分钟采集一次速率差值/60 latest metrics[-1] if len(metrics) 1: prev metrics[-2] time_diff latest[timestamp] - prev[timestamp] if time_diff 0: latest[net_rx_rate] (latest[net_rx] - prev[net_rx]) / time_diff latest[net_tx_rate] (latest[net_tx] - prev[net_tx]) / time_diff else: latest[net_rx_rate] 0 latest[net_tx_rate] 0 else: latest[net_rx_rate] 0 latest[net_tx_rate] 0 # 格式化时间 latest[time_str] datetime.fromtimestamp(latest[timestamp]).strftime(%H:%M:%S) return jsonify(latest) return jsonify({}) app.route(/api/history) def get_history_metrics(): 获取历史数据支持时间范围过滤 metrics parse_metrics_file() if not metrics: return jsonify([]) # 获取查询参数例如 ?hours24 hours request.args.get(hours, default24, typeint) since_ts int(time.time()) - hours * 3600 # 过滤和采样简单过滤实际可做平均采样 filtered [m for m in metrics if m[timestamp] since_ts] # 为了前端展示流畅如果数据点太多可以进行采样例如每10个点取一个 sampled filtered[::max(1, len(filtered)//200)] # 限制最多返回200个点 # 为历史数据也计算网络速率这里简化只返回累计值由前端计算差值或直接展示累计值 # 更佳实践是在采集时就计算并存储速率 for i in range(1, len(sampled)): time_diff sampled[i][timestamp] - sampled[i-1][timestamp] if time_diff 0: sampled[i][net_rx_rate] (sampled[i][net_rx] - sampled[i-1][net_rx]) / time_diff sampled[i][net_tx_rate] (sampled[i][net_tx] - sampled[i-1][net_tx]) / time_diff else: sampled[i][net_rx_rate] 0 sampled[i][net_tx_rate] 0 if sampled: sampled[0][net_rx_rate] 0 sampled[0][net_tx_rate] 0 return jsonify(sampled) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)3.4 创建前端展示界面创建templates/index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title服务器资源监控/title script srchttps://cdn.jsdelivr.net/npm/chart.js/script style body { font-family: sans-serif; margin: 20px; background-color: #f5f5f5; } .dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; } .card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .metric { margin: 10px 0; } .value { font-size: 2em; font-weight: bold; } .unit { color: #666; } canvas { width: 100% !important; height: 300px !important; } .controls { margin-bottom: 20px; } button { padding: 8px 16px; margin-right: 10px; cursor: pointer; } /style /head body h1服务器资源监控仪表盘/h1 div classcontrols button onclickloadHistory(1)最近1小时/button button onclickloadHistory(24)最近24小时/button button onclickloadHistory(168)最近7天/button span idlastUpdate最后更新: --/span /div div classdashboard div classcard h3CPU 使用率/h3 div classmetricspan classvalue idcpuValue--/spanspan classunit%/span/div canvas idcpuChart/canvas /div div classcard h3内存 使用率/h3 div classmetricspan classvalue idmemValue--/spanspan classunit%/span/div canvas idmemChart/canvas /div div classcard h3磁盘 使用率/h3 div classmetricspan classvalue iddiskValue--/spanspan classunit%/span/div canvas iddiskChart/canvas /div div classcard h3网络 速率/h3 div classmetric下行: span classvalue idnetRxValue--/span span classunitB/s/span/div div classmetric上行: span classvalue idnetTxValue--/span span classunitB/s/span/div canvas idnetChart/canvas /div div classcard h3系统负载 (1分钟)/h3 div classmetricspan classvalue idloadValue--/span/div canvas idloadChart/canvas /div /div script let charts {}; const chartConfigs { cpuChart: { label: CPU使用率 (%), color: rgb(255, 99, 132) }, memChart: { label: 内存使用率 (%), color: rgb(54, 162, 235) }, diskChart: { label: 磁盘使用率 (%), color: rgb(255, 205, 86) }, netChart: { label: 网络速率 (B/s), color: [rgb(75, 192, 192), rgb(153, 102, 255)] }, loadChart: { label: 系统负载, color: rgb(201, 203, 207) } }; // 初始化所有图表 Object.keys(chartConfigs).forEach(id { const ctx document.getElementById(id).getContext(2d); const isNet id netChart; charts[id] new Chart(ctx, { type: line, data: { labels: [], datasets: isNet ? [ { label: 下行, data: [], borderColor: chartConfigs[id].color[0], fill: false }, { label: 上行, data: [], borderColor: chartConfigs[id].color[1], fill: false } ] : [ { label: chartConfigs[id].label, data: [], borderColor: chartConfigs[id].color, fill: false } ] }, options: { responsive: true, maintainAspectRatio: false } }); }); // 更新当前状态 function updateCurrent() { fetch(/api/current) .then(r r.json()) .then(data { if (data.timestamp) { document.getElementById(cpuValue).textContent data.cpu.toFixed(1); document.getElementById(memValue).textContent data.memory.toFixed(1); document.getElementById(diskValue).textContent data.disk.toFixed(1); document.getElementById(netRxValue).textContent formatBytes(data.net_rx_rate || 0); document.getElementById(netTxValue).textContent formatBytes(data.net_tx_rate || 0); document.getElementById(loadValue).textContent data.load.toFixed(2); document.getElementById(lastUpdate).textContent 最后更新: ${data.time_str}; } }); } // 加载历史数据并更新图表 function loadHistory(hours) { fetch(/api/history?hours${hours}) .then(r r.json()) .then(historyData { if (historyData.length 0) return; const labels historyData.map(d new Date(d.timestamp * 1000).toLocaleTimeString()); // 更新CPU图表 updateChart(cpuChart, labels, historyData.map(d d.cpu)); // 更新内存图表 updateChart(memChart, labels, historyData.map(d d.memory)); // 更新磁盘图表 updateChart(diskChart, labels, historyData.map(d d.disk)); // 更新网络图表双数据集 if (charts.netChart) { charts.netChart.data.labels labels; charts.netChart.data.datasets[0].data historyData.map(d d.net_rx_rate || 0); charts.netChart.data.datasets[1].data historyData.map(d d.net_tx_rate || 0); charts.netChart.update(none); } // 更新负载图表 updateChart(loadChart, labels, historyData.map(d d.load)); }); } function updateChart(chartId, labels, data) { if (charts[chartId]) { charts[chartId].data.labels labels; charts[chartId].data.datasets[0].data data; charts[chartId].update(none); } } function formatBytes(bytes) { if (bytes 0) return 0 B; const k 1024; const sizes [B, KB, MB, GB]; const i Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) sizes[i]; } // 初始加载和定时刷新 updateCurrent(); loadHistory(24); // 默认加载24小时历史 setInterval(updateCurrent, 10000); // 每10秒更新一次当前状态 /script /body /html3.5 运行与访问在项目根目录下运行Flask应用python app.py然后在浏览器中访问http://你的服务器IP:5000就能看到监控仪表盘了。记得在防火墙中开放5000端口。4. 部署优化与生产环境考量上面的实现是一个可工作的原型。但要像a2zusage那样成为一个可靠的工具还需要考虑生产环境下的诸多问题。4.1 使用Systemd管理服务使用python app.py在前台运行不适合生产。我们应该创建一个systemd服务文件。创建/etc/systemd/system/server-monitor.service[Unit] DescriptionServer Monitor Web Service Afternetwork.target [Service] Typesimple Useryourusername WorkingDirectory/home/yourusername/server-monitor ExecStart/usr/bin/python3 /home/yourusername/server-monitor/app.py Restarton-failure RestartSec5s [Install] WantedBymulti-user.target然后启用并启动服务sudo systemctl daemon-reload sudo systemctl enable server-monitor sudo systemctl start server-monitor sudo systemctl status server-monitor # 检查状态这样服务会在系统启动时自动运行并且崩溃后会自动重启。4.2 使用Nginx反向代理直接暴露Flask开发服务器端口5000到公网不安全且性能不佳。应该使用Nginx作为反向代理。安装Nginx后配置一个虚拟主机例如/etc/nginx/sites-available/server-monitorserver { listen 80; server_name monitor.yourdomain.com; # 替换为你的域名或IP location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }创建符号链接并重启Nginxsudo ln -s /etc/nginx/sites-available/server-monitor /etc/nginx/sites-enabled/ sudo nginx -t # 测试配置 sudo systemctl reload nginx现在可以通过http://monitor.yourdomain.com访问你的监控面板更加安全规范。4.3 数据存储的优化CSV文件在数据量增大后查询效率低下。一个自然的演进是使用SQLite数据库。设计数据表创建一个metrics表包含timestamp整数时间戳、cpu、memory、disk、net_rx、net_tx、load1等字段。修改采集脚本将echo ... csv改为使用sqlite3命令行工具或Python脚本执行INSERT语句。修改后端API将文件读取逻辑改为SQL查询例如SELECT * FROM metrics WHERE timestamp ? ORDER BY timestamp。SQLite的索引可以极大提升按时间范围查询的速度。数据清理可以定期如每天执行DELETE FROM metrics WHERE timestamp ?来清理旧数据或者使用SQLite的自动清理策略。这个改动将存储层从“文件系统”升级为“轻量级数据库”在保持单文件、零外部依赖优点的同时获得了更好的查询性能和灵活性。4.4 安全性增强认证基本的HTTP认证.htpasswd或为Flask添加一个简单的登录功能如使用Flask-Login防止监控数据被公开访问。HTTPS使用Let‘s Encrypt为你的域名申请免费SSL证书并在Nginx中配置HTTPS加密数据传输。输入验证后端API对传入的参数如hours进行严格验证防止SQL注入如果用了SQL或其他攻击。5. 常见问题与排查技巧实录在实际部署和使用自建监控工具的过程中你肯定会遇到各种问题。下面记录了一些典型场景和解决方法。5.1 数据采集不准确或为空现象仪表盘上所有数据都是0或“--”或者CPU使用率始终是100%/0%。排查步骤检查采集脚本权限和执行手动运行./scripts/collect.sh看是否有错误输出。确保脚本有执行权限chmod x。检查cron日志查看系统cron日志sudo grep CRON /var/log/syslog或journalctl -u cron确认任务是否按时执行是否有错误信息。cron的环境变量与交互式Shell不同可能导致命令路径找不到。在脚本中使用绝对路径如/usr/bin/top是一个好习惯。检查数据文件查看/tmp/system_metrics.csv文件是否存在内容格式是否正确。可能是写入权限问题尝试将数据文件路径改为用户家目录下的位置。验证命令输出在脚本中逐条验证采集命令。例如top -bn1在某些系统上可能需要-b和-n1分开写或者使用mpstat需要安装sysstat包。内存计算中free命令的输出格式在不同发行版上可能有细微差别。5.2 网络流量速率计算异常现象网络速率显示为0或者出现巨大的峰值/负值。原因与解决网卡名不对脚本中写死了eth0但你的服务器网卡可能是ens3、enp0s3等。使用ip addr show或ifconfig确认活跃网卡名称并修改脚本。计数器溢出网络字节计数器是64位无符号整数达到最大值后会回绕。Shell脚本中直接做减法如果发生回绕新值小于旧值结果会变成一个巨大的正数因为Bash默认处理有符号数。解决方案在计算前判断如果新值小于旧值则假设发生了回绕计算(最大值 - 旧值) 新值。但/proc/net/dev的计数器在Linux内核中通常是64位回绕周期极长对于个人服务器几乎遇不到但代码健壮性需要考虑。时间间隔为0如果两次采集时间戳相同理论上cron最小间隔1分钟不会为0会导致除零错误。代码中必须做判断if time_diff 0:。5.3 Web界面无法访问或图表不更新现象浏览器显示“无法连接”或页面打开但数据不刷新。排查步骤检查服务状态sudo systemctl status server-monitor查看Flask服务是否在运行。检查端口监听sudo netstat -tlnp | grep :5000查看5000端口是否被正确监听。检查防火墙确保服务器防火墙如ufw和云服务商的安全组规则允许5000端口或Nginx的80/443端口的入站流量。检查浏览器控制台按F12打开开发者工具切换到“网络”(Network)标签刷新页面。查看对/api/current和/api/history的请求是否成功状态码200如果失败如404、500查看响应内容。这能快速定位是前端JS错误还是后端API错误。查看后端日志Flask的日志默认输出到控制台被systemd捕获。使用sudo journalctl -u server-monitor -f实时查看日志寻找错误信息如Python异常。5.4 历史数据图表加载缓慢或卡顿现象选择“最近7天”时页面响应很慢甚至浏览器卡死。原因与优化数据点过多7天10080分钟的数据全部返回前端Chart.js渲染上万数据点性能极差。优化方案在后端API中进行数据采样Downsampling。例如当数据点超过500个时进行等间隔采样或平均值聚合。上面示例代码中的sampled filtered[::max(1, len(filtered)//200)]就是一种简单的等间隔采样。更科学的做法是按时间窗口如每小时计算平均值、最大值、最小值返回聚合后的数据既能反映趋势又大幅减少数据量。数据库索引如果使用了SQLite在timestamp字段上创建索引能极大提升WHERE timestamp ?这类查询的速度。5.5 磁盘空间被监控数据占满预防措施必须实现数据清理策略。基于文件像示例脚本那样使用tail保留最近N行。或者使用logrotate配置按天或按大小轮转并保留固定数量的旧文件。基于数据库定期执行删除旧数据的SQL语句。可以添加一个清理脚本也通过cron每日执行sqlite3 metrics.db DELETE FROM metrics WHERE timestamp strftime(%s, now, -30 days);删除30天前的数据。通过实现一个像aezizhu/a2zusage这样的工具你不仅能获得一个实用的服务器监控方案更能深入理解数据采集、处理、存储和可视化的完整链条。这个过程会强迫你去思考很多细节如何保证采集的准确性如何高效存储和查询时间序列数据如何设计一个清晰易用的界面当出现问题时如何从数据流的一端排查到另一端这些经验远比单纯使用一个现成的监控系统来得宝贵。你可以根据自己的需求轻松地添加新的监控指标如GPU使用率、特定进程状态、温度传感器数据或者将数据接入更专业的可视化工具如Grafana让它真正成为你运维工具箱中得心应手的一部分。