feat: 在设备检测逻辑中添加失败计数器和错误类型判断,优化设备快照获取失败时的处理逻辑;在设备分配服务中引入设备优先前缀配置,增强设备分配的灵活性和负载均衡能力
This commit is contained in:
19
db_migration_device_priority_prefixes.sql
Normal file
19
db_migration_device_priority_prefixes.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- 添加设备优先前缀配置项
|
||||
-- 该配置用于指定设备选择时的前缀优先级,逗号分隔
|
||||
-- 例如:"xx,yy,zz" 表示优先选择 xx 开头的设备,其次 yy,最后 zz
|
||||
-- 同优先级内随机选择,实现负载均衡
|
||||
|
||||
INSERT INTO system_config (config_key, config_value, config_type, description, is_system, created_at, updated_at)
|
||||
VALUES (
|
||||
'device.priority_prefixes',
|
||||
'',
|
||||
'STRING',
|
||||
'设备选择优先前缀(逗号分隔),例如:xx,yy,zz。优先选择匹配前缀的设备,同优先级内随机。为空则全随机',
|
||||
1,
|
||||
NOW(3),
|
||||
NOW(3)
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
config_type = 'STRING',
|
||||
description = '设备选择优先前缀(逗号分隔),例如:xx,yy,zz。优先选择匹配前缀的设备,同优先级内随机。为空则全随机',
|
||||
updated_at = NOW(3);
|
||||
162
docs/设备优先前缀配置说明.md
Normal file
162
docs/设备优先前缀配置说明.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# 设备优先前缀配置说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
设备优先前缀配置允许管理员指定在空闲设备选择时的优先级规则,优先选择特定前缀的设备,实现更灵活的设备分配策略。
|
||||
|
||||
## 配置项
|
||||
|
||||
**配置键**: `device.priority_prefixes`
|
||||
**配置类型**: STRING
|
||||
**默认值**: 空字符串(全随机)
|
||||
**格式**: 逗号分隔的前缀列表
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 配置优先前缀
|
||||
|
||||
通过系统配置接口设置 `device.priority_prefixes` 的值。
|
||||
|
||||
**示例配置值**:
|
||||
```
|
||||
xx,yy,zz
|
||||
```
|
||||
|
||||
这表示:
|
||||
- 第1优先级:`xx` 开头的设备
|
||||
- 第2优先级:`yy` 开头的设备
|
||||
- 第3优先级:`zz` 开头的设备
|
||||
- 最低优先级:其他所有设备
|
||||
|
||||
### 2. 选择逻辑
|
||||
|
||||
设备分配时会按照以下逻辑进行:
|
||||
|
||||
1. **过滤占用设备**:排除被其他任务占用的设备
|
||||
2. **按前缀分组**:根据配置的前缀将设备分成不同优先级组
|
||||
3. **组内随机**:每个优先级组内的设备随机打乱
|
||||
4. **顺序尝试**:按优先级从高到低依次尝试分配,直到成功
|
||||
|
||||
**特点**:
|
||||
- ✅ 优先使用指定前缀的设备
|
||||
- ✅ 同优先级内随机选择,避免总是选中同一台设备
|
||||
- ✅ 实现负载均衡和优先级控制的平衡
|
||||
- ✅ 支持冷却机制和并发安全
|
||||
|
||||
### 3. 示例场景
|
||||
|
||||
#### 场景1:无配置(默认行为)
|
||||
```
|
||||
device.priority_prefixes = ""
|
||||
```
|
||||
**效果**:所有设备完全随机选择,与原有逻辑一致
|
||||
|
||||
#### 场景2:优先使用xx设备
|
||||
```
|
||||
device.priority_prefixes = "xx"
|
||||
```
|
||||
**假设可用设备**:`xx1, xx2, yy1, yy2, zz1`
|
||||
|
||||
**选择顺序**:
|
||||
1. 优先在 `xx1, xx2` 中随机选择
|
||||
2. 如果 xx 设备都在冷却期或被占用,再从 `yy1, yy2, zz1` 中随机选择
|
||||
|
||||
#### 场景3:多级优先级
|
||||
```
|
||||
device.priority_prefixes = "xx,yy"
|
||||
```
|
||||
**假设可用设备**:`xx1, xx2, yy1, yy2, zz1, zz2`
|
||||
|
||||
**选择顺序**:
|
||||
1. 第1优先级:在 `xx1, xx2` 中随机选择
|
||||
2. 第2优先级:如果 xx 都不可用,在 `yy1, yy2` 中随机选择
|
||||
3. 第3优先级:如果 xx、yy 都不可用,在 `zz1, zz2` 中随机选择
|
||||
|
||||
## API使用示例
|
||||
|
||||
### 查询当前配置
|
||||
```bash
|
||||
GET /api/admin/config/key/device.priority_prefixes
|
||||
```
|
||||
|
||||
### 更新配置
|
||||
```bash
|
||||
PUT /api/admin/config
|
||||
|
||||
{
|
||||
"configKey": "device.priority_prefixes",
|
||||
"configValue": "xx,yy,zz",
|
||||
"configType": "STRING",
|
||||
"description": "设备选择优先前缀(逗号分隔)"
|
||||
}
|
||||
```
|
||||
|
||||
### 批量更新(推荐)
|
||||
```bash
|
||||
PUT /api/admin/config/batch
|
||||
|
||||
[
|
||||
{
|
||||
"configKey": "device.priority_prefixes",
|
||||
"configValue": "xx,yy"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 日志输出
|
||||
|
||||
启用该功能后,日志中会输出相关信息:
|
||||
|
||||
```
|
||||
[INFO] 使用设备优先前缀配置:[xx, yy]
|
||||
[DEBUG] 优先级0(前缀=xx):2 台设备
|
||||
[DEBUG] 优先级1(前缀=yy):3 台设备
|
||||
[DEBUG] 无优先级设备:5 台
|
||||
[INFO] 设备列表已按优先级排序并随机化:[xx2, xx1, yy3, yy1, yy2, zz1, aa1, bb1, cc1, dd1]
|
||||
[INFO] 设备分配成功:设备=xx2, 任务ID=123, 原因=首次选区
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **前缀匹配**:使用 `startsWith()` 进行前缀匹配,例如配置 `xx` 会匹配 `xx1`、`xx2`、`xxabc` 等
|
||||
2. **大小写敏感**:前缀匹配区分大小写
|
||||
3. **空格处理**:配置值会自动去除前后空格,`"xx, yy"` 会被处理为 `["xx", "yy"]`
|
||||
4. **空配置**:如果配置为空或不存在,系统会退化到原有的完全随机选择逻辑
|
||||
5. **优先级顺序**:前缀在配置字符串中的顺序决定优先级,越靠前优先级越高
|
||||
6. **实时生效**:配置修改后立即生效,无需重启服务
|
||||
|
||||
## 数据库迁移
|
||||
|
||||
执行以下 SQL 脚本添加配置项:
|
||||
|
||||
```sql
|
||||
-- 位置: db_migration_device_priority_prefixes.sql
|
||||
INSERT INTO system_config (config_key, config_value, config_type, description, is_system, created_at, updated_at)
|
||||
VALUES (
|
||||
'device.priority_prefixes',
|
||||
'',
|
||||
'STRING',
|
||||
'设备选择优先前缀(逗号分隔),例如:xx,yy,zz。优先选择匹配前缀的设备,同优先级内随机。为空则全随机',
|
||||
1,
|
||||
NOW(3),
|
||||
NOW(3)
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
config_type = 'STRING',
|
||||
description = '设备选择优先前缀(逗号分隔),例如:xx,yy,zz。优先选择匹配前缀的设备,同优先级内随机。为空则全随机',
|
||||
updated_at = NOW(3);
|
||||
```
|
||||
|
||||
## 相关代码
|
||||
|
||||
- **配置服务**: `SystemConfigService.getDevicePriorityPrefixes()`
|
||||
- **分配服务**: `DeviceAllocationService.allocateDevice()`
|
||||
- **排序逻辑**: `DeviceAllocationService.sortByPrefixPriority()`
|
||||
|
||||
## 测试建议
|
||||
|
||||
1. **无配置测试**:不配置前缀,验证随机选择是否正常
|
||||
2. **单前缀测试**:配置单个前缀,验证是否优先选择该前缀设备
|
||||
3. **多前缀测试**:配置多个前缀,验证优先级顺序是否正确
|
||||
4. **负载均衡测试**:多次分配,验证同优先级内是否随机分布
|
||||
5. **冷却期测试**:验证高优先级设备在冷却期时,能否正确选择低优先级设备
|
||||
BIN
logs/audit-status.2025-10-03.0.log.gz
Normal file
BIN
logs/audit-status.2025-10-03.0.log.gz
Normal file
Binary file not shown.
BIN
logs/server.2025-10-03.0.log.gz
Normal file
BIN
logs/server.2025-10-03.0.log.gz
Normal file
Binary file not shown.
@@ -119,6 +119,10 @@ public class Detection {
|
||||
return this.lastSnapshot;
|
||||
}
|
||||
|
||||
// 失败计数器(用于监控)
|
||||
private volatile int consecutiveFailures = 0;
|
||||
private static final int MAX_FAILURES_BEFORE_ALERT = 3;
|
||||
|
||||
/**
|
||||
* 定时任务:全量拉取并交由 DeviceStats 更新内存分类与审计。
|
||||
* 默认每 30 秒执行一次,可通过配置覆盖:detection.poll.cron 或 detection.poll.fixedDelayMs
|
||||
@@ -128,7 +132,14 @@ public class Detection {
|
||||
try {
|
||||
log.info("定时拉取设备快照并更新统计");
|
||||
DeviceStatusResponse snapshot = listAllDevices();
|
||||
|
||||
if (snapshot != null) {
|
||||
// 重置失败计数器
|
||||
if (consecutiveFailures > 0) {
|
||||
log.info("设备状态获取恢复正常,重置失败计数器(之前连续失败{}次)", consecutiveFailures);
|
||||
consecutiveFailures = 0;
|
||||
}
|
||||
|
||||
deviceStats.updateWithSnapshot(snapshot);
|
||||
log.info("设备快照更新统计完成");
|
||||
|
||||
@@ -155,9 +166,36 @@ public class Detection {
|
||||
} catch (Exception ex) {
|
||||
log.warn("基于设备快照推进USING→LOGGED_IN时出现异常", ex);
|
||||
}
|
||||
} else {
|
||||
// 快照获取失败,使用旧缓存继续部分功能
|
||||
DeviceStatusResponse cachedSnapshot = getLastSnapshot();
|
||||
if (cachedSnapshot != null) {
|
||||
log.warn("使用缓存的设备快照数据(可能过期),避免服务完全中断");
|
||||
// 不更新统计,但可以尝试继续状态推进
|
||||
} else {
|
||||
log.warn("无可用设备快照(包括缓存),跳过本轮更新");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("定时拉取设备快照并更新统计失败", e);
|
||||
consecutiveFailures++;
|
||||
|
||||
// 判断异常类型,给出更明确的错误信息
|
||||
String errorType = "未知错误";
|
||||
if (e instanceof org.springframework.web.reactive.function.client.WebClientRequestException) {
|
||||
if (e.getMessage().contains("connection timed out") || e.getMessage().contains("ConnectTimeoutException")) {
|
||||
errorType = "连接超时";
|
||||
} else if (e.getMessage().contains("Connection refused")) {
|
||||
errorType = "连接被拒绝";
|
||||
}
|
||||
}
|
||||
|
||||
if (consecutiveFailures >= MAX_FAILURES_BEFORE_ALERT) {
|
||||
log.error("【告警】定时拉取设备快照连续失败{}次(错误类型:{}),请检查脚本服务器连接: {}",
|
||||
consecutiveFailures, errorType, e.getMessage());
|
||||
} else {
|
||||
log.error("定时拉取设备快照并更新统计失败(连续失败{}次,错误类型:{})",
|
||||
consecutiveFailures, errorType, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,19 @@ public class SystemConfigService {
|
||||
return getConfigValue("user.custom_scan_content", "");
|
||||
}
|
||||
|
||||
// 获取设备优先前缀列表(逗号分隔),返回List
|
||||
public List<String> getDevicePriorityPrefixes() {
|
||||
String value = getConfigValue("device.priority_prefixes", "");
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
// 按逗号分隔,去除空白,过滤空字符串
|
||||
return java.util.Arrays.stream(value.split(","))
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
// 批量更新配置
|
||||
public boolean updateConfigs(List<SystemConfig> configs) {
|
||||
if (configs == null || configs.isEmpty()) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.gameplatform.server.service.link;
|
||||
import com.gameplatform.server.mapper.agent.LinkTaskMapper;
|
||||
import com.gameplatform.server.model.entity.agent.LinkTask;
|
||||
import com.gameplatform.server.service.cooldown.MemoryMachineCooldownService;
|
||||
import com.gameplatform.server.service.admin.SystemConfigService;
|
||||
import com.gameplatform.server.mapper.history.LinkTaskStatusHistoryMapper;
|
||||
import com.gameplatform.server.model.entity.history.LinkTaskStatusHistory;
|
||||
import org.slf4j.Logger;
|
||||
@@ -28,13 +29,16 @@ public class DeviceAllocationService {
|
||||
private final MemoryMachineCooldownService machineCooldownService;
|
||||
private final LinkTaskMapper linkTaskMapper;
|
||||
private final LinkTaskStatusHistoryMapper statusHistoryMapper;
|
||||
private final SystemConfigService systemConfigService;
|
||||
|
||||
public DeviceAllocationService(MemoryMachineCooldownService machineCooldownService,
|
||||
LinkTaskMapper linkTaskMapper,
|
||||
LinkTaskStatusHistoryMapper statusHistoryMapper) {
|
||||
LinkTaskStatusHistoryMapper statusHistoryMapper,
|
||||
SystemConfigService systemConfigService) {
|
||||
this.machineCooldownService = machineCooldownService;
|
||||
this.linkTaskMapper = linkTaskMapper;
|
||||
this.statusHistoryMapper = statusHistoryMapper;
|
||||
this.systemConfigService = systemConfigService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,14 +68,13 @@ public class DeviceAllocationService {
|
||||
log.info("设备占用检查完成:原候选设备数={}, 过滤后设备数={}, 可用设备={}",
|
||||
availableDevices.size(), filteredDevices.size(), filteredDevices);
|
||||
|
||||
// 2. 打乱设备列表,实现负载均衡
|
||||
List<String> shuffledDevices = new ArrayList<>(filteredDevices);
|
||||
Collections.shuffle(shuffledDevices, ThreadLocalRandom.current());
|
||||
// 2. 按前缀优先级排序,然后打乱(同优先级内随机)
|
||||
List<String> sortedDevices = sortByPrefixPriority(filteredDevices);
|
||||
|
||||
log.info("设备列表已随机化:{}", shuffledDevices);
|
||||
log.info("设备列表已按优先级排序并随机化:{}", sortedDevices);
|
||||
|
||||
// 3. 尝试原子分配设备(按随机顺序)
|
||||
for (String deviceId : shuffledDevices) {
|
||||
// 3. 尝试原子分配设备(按优先级顺序)
|
||||
for (String deviceId : sortedDevices) {
|
||||
if (machineCooldownService.tryAllocateDevice(deviceId, reason, linkTaskId)) {
|
||||
log.info("设备分配成功:设备={}, 任务ID={}, 原因={}", deviceId, linkTaskId, reason);
|
||||
return deviceId;
|
||||
@@ -186,6 +189,73 @@ public class DeviceAllocationService {
|
||||
return availableDevices;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置的前缀优先级对设备列表进行排序
|
||||
* 优先级高的前缀排在前面,同优先级内随机打乱
|
||||
* @param devices 原始设备列表
|
||||
* @return 排序后的设备列表
|
||||
*/
|
||||
private List<String> sortByPrefixPriority(List<String> devices) {
|
||||
if (devices == null || devices.isEmpty()) {
|
||||
return devices;
|
||||
}
|
||||
|
||||
// 获取配置的优先前缀列表
|
||||
List<String> priorityPrefixes = systemConfigService.getDevicePriorityPrefixes();
|
||||
|
||||
if (priorityPrefixes.isEmpty()) {
|
||||
// 没有配置优先前缀,直接随机打乱(保持原有行为)
|
||||
List<String> shuffled = new ArrayList<>(devices);
|
||||
Collections.shuffle(shuffled, ThreadLocalRandom.current());
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
log.info("使用设备优先前缀配置:{}", priorityPrefixes);
|
||||
|
||||
// 按优先级分组:优先级索引 -> 设备列表
|
||||
// -1 表示没有匹配任何优先前缀
|
||||
List<List<String>> groups = new ArrayList<>();
|
||||
for (int i = 0; i <= priorityPrefixes.size(); i++) {
|
||||
groups.add(new ArrayList<>());
|
||||
}
|
||||
|
||||
// 将设备分组
|
||||
for (String deviceId : devices) {
|
||||
int priorityIndex = -1;
|
||||
// 查找设备匹配的最高优先级前缀
|
||||
for (int i = 0; i < priorityPrefixes.size(); i++) {
|
||||
if (deviceId.startsWith(priorityPrefixes.get(i))) {
|
||||
priorityIndex = i;
|
||||
break; // 找到第一个匹配的前缀就停止
|
||||
}
|
||||
}
|
||||
|
||||
if (priorityIndex >= 0) {
|
||||
groups.get(priorityIndex).add(deviceId);
|
||||
} else {
|
||||
// 没有匹配的放到最后一组
|
||||
groups.get(groups.size() - 1).add(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
// 对每组内部进行随机打乱,然后按优先级顺序合并
|
||||
List<String> result = new ArrayList<>();
|
||||
for (int i = 0; i < groups.size(); i++) {
|
||||
List<String> group = groups.get(i);
|
||||
if (!group.isEmpty()) {
|
||||
Collections.shuffle(group, ThreadLocalRandom.current());
|
||||
result.addAll(group);
|
||||
if (i < priorityPrefixes.size()) {
|
||||
log.debug("优先级{}(前缀={}):{} 台设备", i, priorityPrefixes.get(i), group.size());
|
||||
} else {
|
||||
log.debug("无优先级设备:{} 台", group.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证设备分配结果(分配后的双重检查)
|
||||
* @param deviceId 设备ID
|
||||
|
||||
Reference in New Issue
Block a user