新增remnanode定时任务状态上报

This commit is contained in:
lys 2026-03-19 16:16:28 +08:00
parent 8e18aa54bd
commit d8ff88ab66
3 changed files with 320 additions and 0 deletions

View File

@ -0,0 +1,138 @@
package org.dromara.job.snailjob;
import com.aizuda.snailjob.client.job.core.annotation.JobExecutor;
import com.aizuda.snailjob.client.job.core.dto.JobArgs;
import com.aizuda.snailjob.common.core.util.JsonUtil;
import com.aizuda.snailjob.common.log.SnailJobLog;
import com.aizuda.snailjob.model.dto.ExecuteResult;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.enums.NetNodeSoftTypeEnum;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.net.domain.NetNode;
import org.dromara.net.domain.NetNodeConfig;
import org.dromara.net.dto.NodeSystemStatsResponse;
import org.dromara.net.mapper.NetNodeConfigMapper;
import org.dromara.net.mapper.NetNodeMapper;
import org.dromara.net.util.RemnaNodeHttpClient;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
/**
* Remna节点系统状态采集定时任务
* 定时采集所有RemnaWave类型节点的系统状态信息
* 更新节点的内存信息和运行时间
*
* @author lys
* @date 2025-03-19
*/
@Slf4j
@Component
@RequiredArgsConstructor
@JobExecutor(name = "remnaNodeStatsJob")
public class RemnaNodeStatsJob {
private final NetNodeMapper netNodeMapper;
private final NetNodeConfigMapper netNodeConfigMapper;
/**
* 请求超时时间毫秒
*/
private static final int REQUEST_TIMEOUT = 3000;
/**
* 系统状态接口路径
*/
private static final String SYSTEM_STATS_PATH = "/node/stats/get-system-stats";
public ExecuteResult jobExecute(JobArgs jobArgs) {
SnailJobLog.LOCAL.info("remnaNodeStatsJob 开始执行. jobArgs:{}", JsonUtil.toJsonString(jobArgs));
SnailJobLog.REMOTE.info("remnaNodeStatsJob 开始执行. jobArgs:{}", JsonUtil.toJsonString(jobArgs));
// 查询所有RemnaWave类型的启用节点
LambdaQueryWrapper<NetNode> nodeQueryWrapper = new LambdaQueryWrapper<>();
nodeQueryWrapper.eq(NetNode::getNodeSoftType, NetNodeSoftTypeEnum.REMNAWAVE.getNodeValue());
List<NetNode> nodes = TenantHelper.ignore(() -> netNodeMapper.selectList(nodeQueryWrapper));
SnailJobLog.LOCAL.info("remnaNodeStatsJob 找到 {} 个RemnaWave节点", nodes.size());
SnailJobLog.REMOTE.info("remnaNodeStatsJob 找到 {} 个RemnaWave节点", nodes.size());
int successCount = 0;
int failCount = 0;
for (NetNode node : nodes) {
try {
// 获取节点配置
NetNodeConfig config = TenantHelper.ignore(() -> {
LambdaQueryWrapper<NetNodeConfig> configQueryWrapper = new LambdaQueryWrapper<>();
configQueryWrapper.eq(NetNodeConfig::getNodeId, node.getId());
return netNodeConfigMapper.selectOne(configQueryWrapper);
});
if (config == null) {
SnailJobLog.LOCAL.warn("remnaNodeStatsJob 节点 {} 配置不存在,跳过", node.getId());
SnailJobLog.REMOTE.warn("remnaNodeStatsJob 节点 {} 配置不存在,跳过", node.getId());
failCount++;
continue;
}
// 构建完整的系统状态URL
String nodeUrl = RemnaNodeHttpClient.buildNodeUrl(node.getNodeIp(), node.getConnectionPort());
String statsUrl = nodeUrl + SYSTEM_STATS_PATH;
// 使用封装的HTTP客户端调用系统状态接口
NodeSystemStatsResponse statsResponse = RemnaNodeHttpClient.getAndParse(
config, statsUrl, NodeSystemStatsResponse.class, REQUEST_TIMEOUT);
if (statsResponse != null && statsResponse.getResponse() != null) {
// 更新节点系统状态信息
updateNodeStats(node.getId(), statsResponse.getResponse());
successCount++;
SnailJobLog.LOCAL.info("remnaNodeStatsJob 节点 {} ({}) 系统状态采集成功, alloc={}MB, sys={}MB, uptime={}s",
node.getId(), node.getNodeName(),
statsResponse.getResponse().getAlloc() / 1024 / 1024,
statsResponse.getResponse().getSys() / 1024 / 1024,
statsResponse.getResponse().getUptime());
} else {
failCount++;
SnailJobLog.LOCAL.warn("remnaNodeStatsJob 节点 {} ({}) 系统状态采集失败:响应为空",
node.getId(), node.getNodeName());
SnailJobLog.REMOTE.warn("remnaNodeStatsJob 节点 {} ({}) 系统状态采集失败:响应为空",
node.getId(), node.getNodeName());
}
} catch (Exception e) {
failCount++;
SnailJobLog.LOCAL.error("remnaNodeStatsJob 节点 {} ({}) 系统状态采集异常: {}",
node.getId(), node.getNodeName(), e.getMessage());
SnailJobLog.REMOTE.error("remnaNodeStatsJob 节点 {} ({}) 系统状态采集异常: {}",
node.getId(), node.getNodeName(), e.getMessage());
}
}
String result = String.format("执行完成, 成功: %d, 失败: %d", successCount, failCount);
SnailJobLog.LOCAL.info("remnaNodeStatsJob {}", result);
SnailJobLog.REMOTE.info("remnaNodeStatsJob {}", result);
return ExecuteResult.success(result);
}
/**
* 更新节点系统状态信息
* 更新内存使用量和运行时间
*
* @param nodeId 节点ID
* @param responseData 响应数据
*/
private void updateNodeStats(Long nodeId, NodeSystemStatsResponse.ResponseData responseData) {
LambdaUpdateWrapper<NetNode> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(NetNode::getId, nodeId)
.set(NetNode::getMemUsed, responseData.getAlloc())
.set(NetNode::getUptime, responseData.getUptime().intValue());
TenantHelper.ignore(() -> netNodeMapper.update(null, updateWrapper));
}
}

View File

@ -0,0 +1,80 @@
package org.dromara.net.dto;
import lombok.Data;
/**
* 节点系统状态响应DTO
* 对应 GET /node/stats/get-system-stats 接口的响应
*
* @author lys
* @date 2025-03-19
*/
@Data
public class NodeSystemStatsResponse {
/**
* 响应数据
*/
private ResponseData response;
/**
* 响应数据
*/
@Data
public static class ResponseData {
/**
* goroutine数量
*/
private Long numGoroutine;
/**
* GC次数
*/
private Long numGC;
/**
* 已分配内存字节
*/
private Long alloc;
/**
* 总分配内存字节
*/
private Long totalAlloc;
/**
* 系统内存字节
*/
private Long sys;
/**
* malloc次数
*/
private Long mallocs;
/**
* free次数
*/
private Long frees;
/**
* 存活对象数
*/
private Long liveObjects;
/**
* GC暂停总时间纳秒
*/
private Long pauseTotalNs;
/**
* 运行时间
*/
private Long uptime;
/**
* 报告数量可选
*/
private Long reportsCount;
}
}

View File

@ -6,6 +6,8 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.dromara.net.domain.NetNode;
import org.dromara.net.domain.NetNodeConfig;
import org.dromara.net.domain.vo.NetNodeConfigVo;
import org.dromara.net.domain.vo.NetNodeVo;
@ -13,6 +15,7 @@ import org.dromara.net.dto.XrayConfig;
import org.dromara.net.dto.XrayStartRequest;
import org.dromara.net.dto.XrayStartResponse;
import org.dromara.net.mapper.NetNodeConfigMapper;
import org.dromara.net.mapper.NetNodeMapper;
import org.dromara.net.service.INetNodeConfigService;
import org.dromara.net.service.INetNodeService;
import org.dromara.net.util.RemnaNodeHttpClient;
@ -33,6 +36,7 @@ import java.util.*;
public class NetNodeConfigServiceImpl implements INetNodeConfigService {
private final NetNodeConfigMapper baseMapper;
private final NetNodeMapper netNodeMapper;
private final INetNodeService netNodeService;
/**
@ -354,6 +358,9 @@ public class NetNodeConfigServiceImpl implements INetNodeConfigService {
throw new ServiceException("启动节点Xray失败");
}
// 更新节点的内存信息和版本信息
updateNodeSystemInfo(nodeId, response);
return response;
}
@ -443,4 +450,99 @@ public class NetNodeConfigServiceImpl implements INetNodeConfigService {
String content = pem.replace("\r\n", "\n");
return Arrays.asList(content.split("\n"));
}
/**
* 更新节点的系统信息内存版本等
*
* @param nodeId 节点ID
* @param response Xray启动响应
*/
private void updateNodeSystemInfo(Long nodeId, XrayStartResponse response) {
if (response == null || response.getResponse() == null) {
return;
}
XrayStartResponse.ResponseData data = response.getResponse();
LambdaUpdateWrapper<NetNode> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(NetNode::getId, nodeId);
// 更新内存总量
if (data.getSystemInformation() != null
&& StringUtils.isNotBlank(data.getSystemInformation().getMemoryTotal())) {
Long memTotal = parseMemoryToBytes(data.getSystemInformation().getMemoryTotal());
if (memTotal != null) {
updateWrapper.set(NetNode::getMemTotal, memTotal);
log.info("更新节点 {} 内存总量: {} bytes ({} MB)", nodeId, memTotal, memTotal / 1024 / 1024);
}
}
// 更新Xray版本
if (StringUtils.isNotBlank(data.getVersion())) {
updateWrapper.set(NetNode::getXrayVersion, data.getVersion());
}
// 更新Node版本
if (data.getNodeInformation() != null && StringUtils.isNotBlank(data.getNodeInformation().getVersion())) {
updateWrapper.set(NetNode::getNodeVersion, data.getNodeInformation().getVersion());
}
// 执行更新
netNodeMapper.update(null, updateWrapper);
}
/**
* 解析内存字符串为字节数
* 支持格式: "1GB", "512MB", "1024KB", "1024B"
*
* @param memoryStr 内存字符串
* @return 字节数解析失败返回null
*/
private Long parseMemoryToBytes(String memoryStr) {
if (StringUtils.isBlank(memoryStr)) {
return null;
}
// 去除空格并转大写
String str = memoryStr.trim().toUpperCase();
try {
// 提取数字部分和单位
long multiplier = 1;
String numStr;
if (str.endsWith("GB")) {
multiplier = 1024L * 1024L * 1024L;
numStr = str.substring(0, str.length() - 2);
} else if (str.endsWith("MB")) {
multiplier = 1024L * 1024L;
numStr = str.substring(0, str.length() - 2);
} else if (str.endsWith("KB")) {
multiplier = 1024L;
numStr = str.substring(0, str.length() - 2);
} else if (str.endsWith("B")) {
multiplier = 1L;
numStr = str.substring(0, str.length() - 1);
} else if (str.endsWith("G")) {
multiplier = 1024L * 1024L * 1024L;
numStr = str.substring(0, str.length() - 1);
} else if (str.endsWith("M")) {
multiplier = 1024L * 1024L;
numStr = str.substring(0, str.length() - 1);
} else if (str.endsWith("K")) {
multiplier = 1024L;
numStr = str.substring(0, str.length() - 1);
} else {
// 没有单位假设是字节
numStr = str;
}
// 解析数字支持小数
double num = Double.parseDouble(numStr.trim());
return (long) (num * multiplier);
} catch (NumberFormatException e) {
log.warn("解析内存字符串失败: {}", memoryStr);
return null;
}
}
}