本教程整合哪吒探针开源说明、一键部署、流量警报配置、每月流量重置核心功能,适配 Linux 主流发行版(CentOS/Debian/Ubuntu),尤其适合 AWS Lightsail 等云服务器运维,所有命令 / 配置均可直接复制使用,兼顾基础部署与高阶流量管理需求。
一、核心基础信息
1. 开源属性
哪吒探针为完全开源项目,采用Apache License 2.0协议(商业友好、可自由修改 / 分发),
相关软件推荐:
新版官方主仓库(持续维护):https://github.com/nezhahq/nezha
- 开发语言:后端 Go + 前端现代 Web 技术
- 核心组件:dashboard(管理面板 / 服务端)、agent(探针客户端)均开源
- 支持平台:Linux/FreeBSD/macOS,Windows 可通过 WSL 部署
2. 核心流量管理能力
本教程重点实现:服务器全维度监控 + 实时 / 周期流量告警 + 每月流量自动重置统计,数据持久化存储,不受服务器 / Agent 重启影响,适配云服务器月度流量配额管理需求。
二、环境准备(必做)
1. 服务器要求
- 服务端:需有公网 IP(如 AWS Lightsail 实例),最低配置 1 核 1G 即可
- 客户端:需能访问服务端公网 IP / 域名,无配置要求
2. 端口开放(防火墙 / 云安全组)
必须放行 2 个核心端口(以 AWS Lightsail 为例,需在实例网络 – 安全组中添加入站规则),自定义端口需同步修改:
| 端口类型 | 默认端口 | 作用 |
|---|---|---|
| TCP | 自定义(如 8080) | 面板 Web 访问端口 |
| TCP | 5555 | Agent 与服务端通信端口 |
Linux 本地防火墙开放示例(firewalld):
# 替换为自己的面板端口,5555为默认Agent端口
firewall-cmd --add-port=8080/tcp --add-port=5555/tcp --permanent
firewall-cmd --reload
# 查看开放结果
firewall-cmd --list-ports
三、一键部署:服务端(管理面板)
官方提供 2 种部署方式,交互式推荐新手,自定义参数适合云服务器标准化部署,均为原生脚本,无第三方修改。
方式 1:交互式一键部署(推荐)
直接执行,全程按提示设置面板端口、Agent 端口、管理员账号 / 密码,无需手动配置:
curl -fsSL https://raw.githubusercontent.com/nezhahq/nezha/master/script/install.sh | bash
方式 2:自定义参数一键部署(进阶,跳过交互)
适合指定端口 / 域名,直接拼接参数执行,示例为面板 8080 端口 + Agent5555 端口,可按需修改:
# 核心参数:--port 面板端口 --agent-port Agent端口 --domain 绑定域名(可选,用于SSL)
curl -fsSL https://raw.githubusercontent.com/nezhahq/nezha/master/script/install.sh | bash -s -- --port 8080 --agent-port 5555
服务端部署后关键操作
- 面板访问:浏览器输入
公网IP:面板端口(如http://1.1.1.1:8080),输入设置的管理员账号密码登录 - 服务管理(systemd,开机自启):
# 启停/重启/查看状态
systemctl start|stop|restart|status nezha-dashboard
# 设置开机自启(脚本已默认配置,可验证)
systemctl enable nezha-dashboard
设置面板样式
<script>
/*
window.TrafficScriptConfig = {
showTrafficStats: true, // 显示流量统计, 默认开启
insertAfter: true, // 如果开启总流量卡片, 是否放置在总流量卡片后面, 默认为true
interval: 60000, // 60秒刷新缓存, 单位毫秒, 默认60秒
toggleInterval: 5000, // 4秒切换流量进度条右上角内容, 0秒不切换, 单位毫秒, 默认5秒
duration: 500, // 缓出缓进切换时间, 单位毫秒, 默认500毫秒
enableLog: false // 开启日志, 默认关闭
};
*/
</script>
<script>
const SCRIPT_VERSION = 'v20250617';
// == 样式注入模块 ==
// 注入自定义CSS隐藏特定元素
function injectCustomCSS() {
const style = document.createElement('style');
style.textContent = `
/* 隐藏父级类名为 mt-4 w-full mx-auto 下的所有 div */
.mt-4.w-full.mx-auto > div {
display: none;
}
`;
document.head.appendChild(style);
}
injectCustomCSS();
// == 工具函数模块 ==
const utils = (() => {
/**
* 格式化文件大小,自动转换单位
* @param {number} bytes - 字节数
* @returns {{value: string, unit: string}} 格式化后的数值和单位
*/
function formatFileSize(bytes) {
if (bytes === 0) return { value: '0', unit: 'B' };
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return {
value: size.toFixed(unitIndex === 0 ? 0 : 2),
unit: units[unitIndex]
};
}
/**
* 计算百分比,输入可为大数,支持自动缩放
* @param {number} used - 已使用量
* @param {number} total - 总量
* @returns {string} 百分比字符串,保留2位小数
*/
function calculatePercentage(used, total) {
used = Number(used);
total = Number(total);
// 大数缩放,防止数值溢出
if (used > 1e15 || total > 1e15) {
used /= 1e10;
total /= 1e10;
}
return total === 0 ? '0.00' : ((used / total) * 100).toFixed(2);
}
/**
* 格式化日期字符串,返回 yyyy-MM-dd 格式
* @param {string} dateString - 日期字符串
* @returns {string} 格式化日期
*/
function formatDate(dateString) {
const date = new Date(dateString);
if (isNaN(date)) return '';
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}
/**
* 安全设置子元素文本内容,避免空引用错误
* @param {HTMLElement} parent - 父元素
* @param {string} selector - 子元素选择器
* @param {string} text - 要设置的文本
*/
function safeSetTextContent(parent, selector, text) {
const el = parent.querySelector(selector);
if (el) el.textContent = text;
}
/**
* 根据百分比返回渐变HSL颜色(绿→橙→红)
* @param {number} percentage - 0~100的百分比
* @returns {string} hsl颜色字符串
*/
function getHslGradientColor(percentage) {
const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
const lerp = (start, end, t) => start + (end - start) * t;
const p = clamp(Number(percentage), 0, 100);
let h, s, l;
if (p <= 35) {
const t = p / 35;
h = lerp(142, 32, t); // 绿色到橙色
s = lerp(69, 85, t);
l = lerp(45, 55, t);
} else if (p <= 85) {
const t = (p - 35) / 50;
h = lerp(32, 0, t); // 橙色到红色
s = lerp(85, 75, t);
l = lerp(55, 50, t);
} else {
const t = (p - 85) / 15;
h = 0; // 红色加深
s = 75;
l = lerp(50, 45, t);
}
return `hsl(${h.toFixed(0)}, ${s.toFixed(0)}%, ${l.toFixed(0)}%)`;
}
/**
* 透明度渐隐渐现切换内容
* @param {HTMLElement} element - 目标元素
* @param {string} newContent - 新HTML内容
* @param {number} duration - 动画持续时间,毫秒
*/
function fadeOutIn(element, newContent, duration = 500) {
element.style.transition = `opacity ${duration / 2}ms`;
element.style.opacity = '0';
setTimeout(() => {
element.innerHTML = newContent;
element.style.transition = `opacity ${duration / 2}ms`;
element.style.opacity = '1';
}, duration / 2);
}
return {
formatFileSize,
calculatePercentage,
formatDate,
safeSetTextContent,
getHslGradientColor,
fadeOutIn
};
})();
// == 流量统计渲染模块 ==
const trafficRenderer = (() => {
const toggleElements = []; // 存储需周期切换显示的元素及其内容
/**
* 渲染流量统计条目
* @param {Object} trafficData - 后台返回的流量数据
* @param {Object} config - 配置项
*/
function renderTrafficStats(trafficData, config) {
const serverMap = new Map();
// 解析月流量数据(接口中key=1,名称含4T月流量)
const monthCycle = trafficData['1'];
if (monthCycle && monthCycle.server_name && monthCycle.transfer) {
for (const serverId in monthCycle.server_name) {
const serverName = monthCycle.server_name[serverId];
const transfer = monthCycle.transfer[serverId] || 0;
const max = monthCycle.max || 0;
const from = monthCycle.from || '';
const to = monthCycle.to || '';
const next_update = monthCycle.next_update[serverId] || '';
serverMap.set(serverName, {
id: serverId,
transfer,
max,
from,
to,
next_update
});
}
}
serverMap.forEach((serverData, serverName) => {
// 查找对应服务器名称元素
const targetElements = document.querySelectorAll('section.grid.items-center.gap-2');
let targetElement = null;
targetElements.forEach(el => {
const pText = el.querySelector('p')?.textContent.trim();
if (pText === serverName) {
targetElement = el;
}
});
if (!targetElement) return;
// 格式化月流量数据
const usedFormatted = utils.formatFileSize(serverData.transfer);
const totalFormatted = utils.formatFileSize(serverData.max);
const percentage = utils.calculatePercentage(serverData.transfer, serverData.max);
const fromFormatted = utils.formatDate(serverData.from);
const toFormatted = utils.formatDate(serverData.to);
const nextUpdateFormatted = new Date(serverData.next_update).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
const uniqueClassName = 'traffic-stats-for-server-' + serverData.id;
const progressColor = utils.getHslGradientColor(percentage);
const containerDiv = targetElement.closest('div');
if (!containerDiv) return;
// 日志输出函数
const log = (...args) => { if (config.enableLog) console.log('[renderTrafficStats]', ...args); };
// 查找是否已有月流量元素
const existing = Array.from(containerDiv.querySelectorAll('.new-inserted-element'))
.find(el => el.classList.contains(uniqueClassName));
if (!config.showTrafficStats) {
// 不显示时移除元素
if (existing) {
existing.remove();
log(`移除月流量条目: ${serverName}`);
}
return;
}
// 月流量元素更新/创建
if (existing) {
// 更新已存在的月流量元素
utils.safeSetTextContent(existing, '.used-traffic', usedFormatted.value);
utils.safeSetTextContent(existing, '.used-unit', usedFormatted.unit);
utils.safeSetTextContent(existing, '.total-traffic', totalFormatted.value);
utils.safeSetTextContent(existing, '.total-unit', totalFormatted.unit);
utils.safeSetTextContent(existing, '.from-date', fromFormatted);
utils.safeSetTextContent(existing, '.to-date', toFormatted);
utils.safeSetTextContent(existing, '.percentage-value', percentage + '%');
utils.safeSetTextContent(existing, '.next-update', `next update: ${nextUpdateFormatted}`);
const progressBar = existing.querySelector('.progress-bar');
if (progressBar) {
progressBar.style.width = percentage + '%';
progressBar.style.backgroundColor = progressColor;
}
log(`更新月流量条目: ${serverName}`);
} else {
// 插入新的月流量元素
let insertTarget = containerDiv.querySelector('section.flex.items-center.w-full.justify-between.gap-1') || containerDiv.querySelector('section.grid.items-center.gap-3');
if (!insertTarget) insertTarget = containerDiv;
const defaultTimeInfoHTML = `<span class="from-date">${fromFormatted}</span>
<span class="text-neutral-500 dark:text-neutral-400">-</span>
<span class="to-date">${toFormatted}</span>`;
const contents = [
defaultTimeInfoHTML,
`<span class="text-[10px] font-medium text-neutral-800 dark:text-neutral-200 percentage-value">${percentage}%</span>`,
`<span class="text-[10px] font-medium text-neutral-600 dark:text-neutral-300">${nextUpdateFormatted}</span>`
];
const newElement = document.createElement('div');
newElement.classList.add('space-y-1.5', 'new-inserted-element', uniqueClassName);
newElement.style.width = '100%';
newElement.innerHTML = `
<div class="flex items-center justify-between">
<div class="flex items-baseline gap-1">
<span class="text-[10px] font-medium text-neutral-800 dark:text-neutral-200">月流量:</span>
<span class="text-[10px] font-medium text-neutral-800 dark:text-neutral-200 used-traffic">${usedFormatted.value}</span>
<span class="text-[10px] font-medium text-neutral-800 dark:text-neutral-200 used-unit">${usedFormatted.unit}</span>
<span class="text-[10px] text-neutral-500 dark:text-neutral-400">/ </span>
<span class="text-[10px] text-neutral-500 dark:text-neutral-400 total-traffic">${totalFormatted.value}</span>
<span class="text-[10px] text-neutral-500 dark:text-neutral-400 total-unit">${totalFormatted.unit}</span>
</div>
<div class="text-[10px] font-medium text-neutral-600 dark:text-neutral-300 time-info" style="opacity:1; transition: opacity 0.3s;">
${defaultTimeInfoHTML}
</div>
</div>
<div class="relative h-1.5">
<div class="absolute inset-0 bg-neutral-100 dark:bg-neutral-800 rounded-full"></div>
<div class="absolute inset-0 bg-emerald-500 rounded-full transition-all duration-300 progress-bar" style="width: ${percentage}%; max-width: 100%; background-color: ${progressColor};"></div>
</div>
`;
insertTarget.after(newElement);
log(`插入月流量条目: ${serverName}`);
// 月流量切换功能
if (config.toggleInterval > 0) {
const timeInfoElement = newElement.querySelector('.time-info');
if (timeInfoElement) {
toggleElements.push({
el: timeInfoElement,
contents
});
}
}
}
// ===================== 核心修正:日流量UI渲染(匹配接口key=2) =====================
const dayCycle = trafficData['2']; // 接口中key=2是日流量,固定匹配
if (dayCycle && dayCycle.server_name && dayCycle.server_name[serverData.id] === serverName) {
// 格式化日流量数据
const dayTransfer = dayCycle.transfer[serverData.id] || 0;
const dayMax = dayCycle.max || 0;
const dayUsedFormatted = utils.formatFileSize(dayTransfer);
const dayTotalFormatted = utils.formatFileSize(dayMax);
const dayPercentage = utils.calculatePercentage(dayTransfer, dayMax);
const dayFromFormatted = utils.formatDate(dayCycle.from || '');
const dayToFormatted = utils.formatDate(dayCycle.to || '');
const dayNextUpdateFormatted = new Date(dayCycle.next_update[serverData.id] || '').toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" });
const dayUniqueClassName = 'traffic-stats-for-server-' + serverData.id + '-day';
// 查找日流量插入目标(基于月流量元素定位,确保位置正确)
let insertTarget = containerDiv.querySelector('section.flex.items-center.w-full.justify-between.gap-1') || containerDiv.querySelector('section.grid.items-center.gap-3');
if (!insertTarget) insertTarget = containerDiv;
// 查找是否已有日流量元素
const dayExisting = Array.from(containerDiv.querySelectorAll('.new-inserted-element-day'))
.find(el => el.classList.contains(dayUniqueClassName));
if (dayExisting) {
// 更新已存在的日流量元素
utils.safeSetTextContent(dayExisting, '.used-traffic', dayUsedFormatted.value);
utils.safeSetTextContent(dayExisting, '.used-unit', dayUsedFormatted.unit);
utils.safeSetTextContent(dayExisting, '.total-traffic', dayTotalFormatted.value);
utils.safeSetTextContent(dayExisting, '.total-unit', dayTotalFormatted.unit);
utils.safeSetTextContent(dayExisting, '.from-date', dayFromFormatted);
utils.safeSetTextContent(dayExisting, '.to-date', dayToFormatted);
utils.safeSetTextContent(dayExisting, '.percentage-value', dayPercentage + '%');
const dayProgressBar = dayExisting.querySelector('.progress-bar');
if (dayProgressBar) {
dayProgressBar.style.width = dayPercentage + '%';
dayProgressBar.style.backgroundColor = utils.getHslGradientColor(dayPercentage);
}
log(`更新日流量条目: ${serverName}`);
} else {
// 插入新的日流量元素(样式与月流量一致,8px间距分隔)
const dayDefaultTimeInfoHTML = `<span class="from-date">${dayFromFormatted}</span>
<span class="text-neutral-500 dark:text-neutral-400">-</span>
<span class="to-date">${dayToFormatted}</span>`;
const dayContents = [
dayDefaultTimeInfoHTML,
`<span class="text-[10px] font-medium text-neutral-800 dark:text-neutral-200 percentage-value">${dayPercentage}%</span>`,
`<span class="text-[10px] font-medium text-neutral-600 dark:text-neutral-300">${dayNextUpdateFormatted}</span>`
];
const dayNewElement = document.createElement('div');
dayNewElement.classList.add('space-y-1.5', 'new-inserted-element-day', dayUniqueClassName);
dayNewElement.style.width = '100%';
dayNewElement.style.marginTop = '8px'; // 月/日流量间距,避免拥挤
dayNewElement.innerHTML = `
<div class="flex items-center justify-between">
<div class="flex items-baseline gap-1">
<span class="text-[10px] font-medium text-neutral-800 dark:text-neutral-200">日流量:</span>
<span class="text-[10px] font-medium text-neutral-800 dark:text-neutral-200 used-traffic">${dayUsedFormatted.value}</span>
<span class="text-[10px] font-medium text-neutral-800 dark:text-neutral-200 used-unit">${dayUsedFormatted.unit}</span>
<span class="text-[10px] text-neutral-500 dark:text-neutral-400">/ </span>
<span class="text-[10px] text-neutral-500 dark:text-neutral-400 total-traffic">${dayTotalFormatted.value}</span>
<span class="text-[10px] text-neutral-500 dark:text-neutral-400 total-unit">${dayTotalFormatted.unit}</span>
</div>
<div class="text-[10px] font-medium text-neutral-600 dark:text-neutral-300 time-info" style="opacity:1; transition: opacity 0.3s;">
${dayDefaultTimeInfoHTML}
</div>
</div>
<div class="relative h-1.5">
<div class="absolute inset-0 bg-neutral-100 dark:bg-neutral-800 rounded-full"></div>
<div class="absolute inset-0 bg-emerald-500 rounded-full transition-all duration-300 progress-bar" style="width: ${dayPercentage}%; max-width: 100%; background-color: ${utils.getHslGradientColor(dayPercentage)};"></div>
</div>
`;
// 插入到月流量元素后方(布局连贯)
insertTarget.after(dayNewElement);
log(`插入日流量条目: ${serverName}`);
// 日流量切换功能(与月流量同步)
if (config.toggleInterval > 0) {
const dayTimeInfoElement = dayNewElement.querySelector('.time-info');
if (dayTimeInfoElement) {
toggleElements.push({
el: dayTimeInfoElement,
contents: dayContents
});
}
}
}
}
// ===================== 日流量渲染逻辑结束 =====================
});
}
/**
* 启动周期切换内容显示(用于时间、百分比等轮播)
* @param {number} toggleInterval - 切换间隔,毫秒
* @param {number} duration - 动画时长,毫秒
*/
function startToggleCycle(toggleInterval, duration) {
if (toggleInterval <= 0) return;
let toggleIndex = 0;
setInterval(() => {
toggleIndex++;
toggleElements.forEach(({ el, contents }) => {
if (!document.body.contains(el)) return;
const index = toggleIndex % contents.length;
utils.fadeOutIn(el, contents[index], duration);
});
}, toggleInterval);
}
return {
renderTrafficStats,
startToggleCycle
};
})();
// == 数据请求和缓存模块 ==
const trafficDataManager = (() => {
let trafficCache = null;
/**
* 请求流量数据,支持缓存
* @param {string} apiUrl - 接口地址
* @param {Object} config - 配置项
* @param {Function} callback - 请求成功后的回调,参数为流量数据
*/
function fetchTrafficData(apiUrl, config, callback) {
const now = Date.now();
// 使用缓存数据
if (trafficCache && (now - trafficCache.timestamp < config.interval)) {
if (config.enableLog) console.log('[fetchTrafficData] 使用缓存数据');
callback(trafficCache.data);
return;
}
if (config.enableLog) console.log('[fetchTrafficData] 请求新数据...');
fetch(apiUrl)
.then(res => res.json())
.then(data => {
if (!data.success) {
if (config.enableLog) console.warn('[fetchTrafficData] 请求成功但数据异常');
return;
}
if (config.enableLog) console.log('[fetchTrafficData] 成功获取新数据');
const trafficData = data.data.cycle_transfer_stats;
trafficCache = {
timestamp: now,
data: trafficData
};
callback(trafficData);
})
.catch(err => {
if (config.enableLog) console.error('[fetchTrafficData] 请求失败:', err);
});
}
return {
fetchTrafficData
};
})();
// == DOM变化监听模块 ==
const domObserver = (() => {
const TARGET_SELECTOR = 'section.grid.items-center.gap-2';
let currentSection = null;
let childObserver = null;
/**
* DOM 子节点变更回调,调用传入的函数
* @param {Function} onChangeCallback - 变更处理函数
*/
function onDomChildListChange(onChangeCallback) {
onChangeCallback();
}
/**
* 监听指定section子节点变化
* @param {HTMLElement} section - 目标section元素
* @param {Function} onChangeCallback - 变更处理函数
*/
function observeSection(section, onChangeCallback) {
if (childObserver) {
childObserver.disconnect();
}
currentSection = section;
childObserver = new MutationObserver(mutations => {
for (const m of mutations) {
if (m.type === 'childList' && (m.addedNodes.length || m.removedNodes.length)) {
onDomChildListChange(onChangeCallback);
break;
}
}
});
childObserver.observe(currentSection, { childList: true, subtree: true });
// 初始调用一次
onChangeCallback();
}
/**
* 启动顶层section监听,检测section切换
* @param {Function} onChangeCallback - section变化时回调
* @returns {MutationObserver} sectionDetector实例
*/
function startSectionDetector(onChangeCallback) {
const sectionDetector = new MutationObserver(() => {
const sections = document.querySelectorAll(TARGET_SELECTOR);
if (sections.length > 0 && sections[0] !== currentSection) {
observeSection(sections[0], onChangeCallback);
}
});
const root = document.querySelector('main') || document.body;
sectionDetector.observe(root, { childList: true, subtree: true });
return sectionDetector;
}
/**
* 断开所有监听
* @param {MutationObserver} sectionDetector - 顶层section监听实例
*/
function disconnectAll(sectionDetector) {
if (childObserver) childObserver.disconnect();
if (sectionDetector) sectionDetector.disconnect();
}
return {
startSectionDetector,
disconnectAll
};
})();
// == 主程序入口 ==
(function main() {
// 默认配置
const defaultConfig = {
showTrafficStats: true,
insertAfter: true,
interval: 60000,
toggleInterval: 5000,
duration: 500,
apiUrl: '/api/v1/service',
enableLog: false
};
// 合并用户自定义配置
const config = Object.assign({}, defaultConfig, window.TrafficScriptConfig || {});
if (config.enableLog) {
console.log(`[TrafficScript] 版本: ${SCRIPT_VERSION}`);
console.log('[TrafficScript] 最终配置如下:', config);
}
/**
* 获取并刷新流量统计
*/
function updateTrafficStats() {
trafficDataManager.fetchTrafficData(config.apiUrl, config, trafficData => {
trafficRenderer.renderTrafficStats(trafficData, config);
});
}
/**
* DOM变更处理函数,触发刷新
*/
function onDomChange() {
if (config.enableLog) console.log('[main] DOM变化,刷新流量数据');
updateTrafficStats();
if (!trafficTimer) startPeriodicRefresh();
}
// 定时器句柄,防止重复启动
let trafficTimer = null;
/**
* 启动周期刷新任务
*/
function startPeriodicRefresh() {
if (!trafficTimer) {
if (config.enableLog) console.log('[main] 启动周期刷新任务');
trafficTimer = setInterval(() => {
updateTrafficStats();
}, config.interval);
}
}
// 启动内容切换轮播(如时间、百分比)
trafficRenderer.startToggleCycle(config.toggleInterval, config.duration);
// 监听DOM变化,自动刷新
const sectionDetector = domObserver.startSectionDetector(onDomChange);
// 初始化首次加载
onDomChange();
// 页面卸载时清理资源
window.addEventListener('beforeunload', () => {
domObserver.disconnectAll(sectionDetector);
if (trafficTimer) clearInterval(trafficTimer);
});
})();
</script>
四、一键部署:客户端(Agent 探针)
客户端用于监控目标服务器(可部署多台,如多个 AWS Lightsail 实例),推荐从面板生成专属命令(无错便捷),备用通用脚本可手动输入配置。
方式 1:面板生成专属一键命令(首选)
- 登录哪吒探针面板,左侧菜单栏点击添加服务器
- 页面自动生成专属安装命令(包含服务端 IP / 域名、Agent 端口、唯一密钥)
- 直接复制该命令,在需要监控的服务器上执行,一键完成安装 + 注册 + 启动
方式 2:通用客户端脚本(备用,手动配置)
若需手动指定服务端信息,执行后按提示输入服务端 IP、Agent 端口、密钥即可:
curl -fsSL https://raw.githubusercontent.com/nezhahq/nezha/master/script/agent_install.sh | bash
客户端服务管理
# 启停/重启/查看状态
systemctl start|stop|restart|status nezha-agent
# 开机自启(脚本默认配置)
systemctl enable nezha-agent
设置哪吒安全防护-必须
# 一键禁用客户端敏感权限
bash << 'EOF' #!/bin/bash set -euo pipefail RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' CONFIG_ITEMS=( "disable_auto_update: true" "disable_command_execute: true" ) check_root() { if [ $EUID -ne 0 ]; then echo -e "${RED}错误:请使用root/ sudo权限执行该脚本!${NC}" exit 1 fi } find_config() { echo -e "${YELLOW}正在检测哪吒Agent配置文件...${NC}" if [ -f "/opt/nezha/agent/config.yml" ]; then CONFIG_FILE="/opt/nezha/agent/config.yml" echo -e "${GREEN}检测到标准路径配置文件:${CONFIG_FILE}${NC}" elif [ -f "./config.yml" ]; then CONFIG_FILE="./config.yml" echo -e "${GREEN}检测到当前目录配置文件:${CONFIG_FILE}${NC}" else echo -e "${RED}错误:未找到哪吒Agent配置文件!${NC}" echo -e "${YELLOW}请确认配置文件在 /opt/nezha/agent/ 或当前执行目录下${NC}" exit 1 fi } set_config() { echo -e "${YELLOW}正在配置禁用自动更新/远程命令执行...${NC}" for ITEM in "${CONFIG_ITEMS[@]}"; do CONFIG_NAME=$(echo "$ITEM" | cut -d: -f1) if grep -q "^${CONFIG_NAME}:" "$CONFIG_FILE"; then sed -i "s/^${CONFIG_NAME}:.*/${ITEM}/" "$CONFIG_FILE" echo -e "${GREEN}已更新配置:${ITEM}${NC}" else echo -e "\n${ITEM}" >> "$CONFIG_FILE" echo -e "${GREEN}已添加配置:${ITEM}${NC}" fi done } restart_agent() { echo -e "${YELLOW}正在重启哪吒Agent...${NC}" if systemctl list-unit-files | grep -q "nezha-agent"; then systemctl restart nezha-agent sleep 2 if systemctl is-active --quiet nezha-agent; then echo -e "${GREEN}哪吒Agent(systemd)重启成功!${NC}" systemctl status nezha-agent --no-pager else echo -e "${RED}哪吒Agent(systemd)重启失败,请检查日志!${NC}" exit 1 fi else if pgrep -f "nezha-agent" > /dev/null; then pkill -9 nezha-agent echo -e "${YELLOW}已终止原有nezha-agent进程${NC}" fi if [ -f "/opt/nezha/agent/nezha-agent" ]; then nohup /opt/nezha/agent/nezha-agent > /dev/null 2>&1 & elif [ -f "./nezha-agent" ]; then nohup ./nezha-agent > /dev/null 2>&1 & fi sleep 2 if pgrep -f "nezha-agent" > /dev/null; then echo -e "${GREEN}哪吒Agent(手动二进制)重启成功!${NC}" pgrep -f "nezha-agent" | xargs ps -ef | grep nezha-agent | grep -v grep else echo -e "${RED}哪吒Agent(手动二进制)重启失败,请手动启动!${NC}" exit 1 fi fi } main() { check_root find_config set_config restart_agent echo -e "\n${GREEN}====================操作完成====================" echo -e "已成功配置:" echo -e "1. disable_auto_update: true (禁用自动更新)" echo -e "2. disable_command_execute: true (禁用远程命令执行)" echo -e "3. 哪吒Agent已重启,配置已生效!${NC}" } main EOF
五、核心配置 1:流量警报(实时 / 周期)
哪吒探针支持实时网速告警和周期流量告警,通过 JSON 规则配置,需先完成通知组配置(告警接收方式),再添加告警规则。
步骤 1:配置通知组(必做,告警接收)
- 面板左侧点击通知 → 添加通知渠道,按需配置(推荐 Telegram / 钉钉 / Server 酱 / 邮件)
- 配置完成后,点击通知组 → 新增通知组,将配置好的通知渠道加入组内(后续告警绑定该组)
步骤 2:添加流量告警规则
- 面板左侧点击告警 → 新增告警规则
- 填写规则名称(如「入站网速超 100Mbps 告警」),选择已创建的通知组
- 在规则文本框中输入 JSON 配置(以下为 3 个常用模板,可直接复制修改阈值)
- 勾选启用,点击保存,规则立即生效

常用流量告警 JSON 配置模板(直接复制)
模板 1:实时入站网速告警(超 100Mbps,持续 30 秒触发)
[
{
"type": "net_in_speed",
"max": 13107200, // 100Mbps=100*1024*1024/8 字节/秒,按需修改
"duration": 30 // 持续超阈值时间,防止波动误报,单位秒
}
]
模板 2:月度出站流量告警(超 1TB 触发,配合每月重置)
[
{
"type": "transfer_out_cycle",
"max": 1099511627776, // 1TB=1024^4 字节,按需修改
"cycle_start": "2026-02-01T00:00:00+08:00", // 周期起始时间(自然月1日)
"cycle_interval": 1, // 每1个周期
"cycle_unit": "month", // 周期单位:month(月)/day(天)/week(周)
"cover": 1 // 1=指定服务器,0=所有服务器
}
]
模板 3:实时双向总网速告警(超 200Mbps,持续 60 秒触发)
{
"type": "net_all_speed",
"max": 26214400, // 200Mbps=200*1024*1024/8 字节/秒,按需修改
"duration": 60
}
]
流量告警核心参数说明
| 参数 | 作用 | 可选值 / 说明 |
|---|---|---|
type |
监控类型 | net_in_speed(入站网速)/net_out_speed(出站网速)/net_all_speed(双向)/transfer_all_cycle(周期双向流量) |
max |
阈值上限 | 数值,流量 / 网速单位均为字节 |
duration |
持续触发时间 | 仅实时网速有效,单位秒 |
cycle_start |
周期起始时间 | RFC3339 格式,如2026-0X-01T00:00:00+08:00(自然月 1 日) |
cycle_unit |
周期单位 | hour/day/week/month/year |
cover |
服务器覆盖 | 0 = 监控所有服务器,1 = 仅监控指定服务器(配合ignore参数) |
六、核心配置 2:每月流量自动重置(周期统计)
哪吒探针提供2 种流量统计维度,周期流量统计支持每月自动重置,数据存储在数据库,不受服务器 / Agent 重启影响,是云服务器月度流量管理的核心功能。
两种流量统计机制对比
| 统计类型 | 重置规则 | 查看位置 | 适用场景 |
|---|---|---|---|
| 基础流量统计 | 服务器 / Agent 重启时重置 | 面板主页服务器卡片默认显示 | 临时监控,查看重启后流量使用 |
| 周期流量统计 | 按设定周期(如月)自动重置 | 面板主页→服务器卡片服务按钮内 | 长期管理,月度流量配额监控 |
每月流量重置配置步骤(与周期流量告警联动)
周期流量统计的重置规则通过「流量告警 JSON」配置,无需额外操作,配置后系统将按规则自动统计并在周期结束后重置,核心要点:
- cycle_start:设置为自然月 1 日 0 点(如
2026-02-01T00:00:00+08:00),每年仅需设置 1 次,系统会自动按月循环 - cycle_unit:固定为
month,cycle_interval固定为1,即每 1 个月重置一次 - 数据查看:配置后,在面板主页点击对应服务器卡片的服务按钮,即可查看周期入站 / 出站 / 双向流量的已用 / 阈值占比
快速配置:每月 1 日重置的双向流量统计(无告警,仅统计)
若仅需每月流量重置统计,无需达到阈值触发告警,可将
max设置为极大值(如 100TB),JSON 配置如下:[
{
"type": "transfer_all_cycle",
"max": 109951162777600, // 100TB,设为极大值避免触发告警
"cycle_start": "2026-02-01T00:00:00+08:00",
"cycle_interval": 1,
"cycle_unit": "month",
"cover": 0 // 监控所有服务器
}
]
七、哪吒探针常用管理操作
1. 面板核心操作
- 新增 / 删除监控服务器:左侧添加服务器/服务器列表
- 修改告警规则:左侧告警 → 编辑对应规则
- 查看周期流量:主页服务器卡片→服务按钮
- 修改管理员密码:右上角头像→个人设置
2. 服务端 / 客户端版本更新
# 服务端更新
curl -fsSL https://raw.githubusercontent.com/nezhahq/nezha/master/script/update.sh | bash
# 客户端更新
curl -fsSL https://raw.githubusercontent.com/nezhahq/nezha/master/script/agent_update.sh | bash
3. 卸载(按需)
# 服务端卸载
curl -fsSL https://raw.githubusercontent.com/nezhahq/nezha/master/script/uninstall.sh | bash
# 客户端卸载
curl -fsSL https://raw.githubusercontent.com/nezhahq/nezha/master/script/agent_uninstall.sh | bash
© 版权声明
文章版权归作者所有,未经允许请勿转载。
相关文章
暂无评论...