multi-lane-manager
Version:
Nacos 泳道管理与请求路由组件
627 lines (546 loc) • 21.3 kB
text/typescript
import axios from 'axios';
import type { ServiceInstanceInfo } from '../types';
import { getConfig, getGlobalState, updateConfigPort } from './config';
import { logger } from './logger';
/**
* Nacos 实例查询缓存项
*/
interface NacosCacheItem {
data: ServiceInstanceInfo[]; // 缓存的实例数据
timestamp: number; // 缓存时间戳
promise?: Promise<ServiceInstanceInfo[]>; // 正在进行的请求 Promise(用于去重)
}
/**
* Nacos 实例查询缓存
* 使用 Map 存储缓存,key 为 "serviceName:targetLaneId" 格式
*/
const nacosInstanceCache = new Map<string, NacosCacheItem>();
/**
* 生成缓存键
* @param serviceName 服务名称
* @param targetLaneId 目标泳道ID
* @returns 缓存键
*/
function getCacheKey(serviceName: string, targetLaneId: string): string {
return `${serviceName}:${targetLaneId}`;
}
/**
* 检查缓存是否有效
* @param cacheItem 缓存项
* @param ttl 缓存过期时间(毫秒)
* @returns 是否有效
*/
function isCacheValid(cacheItem: NacosCacheItem, ttl: number): boolean {
return Date.now() - cacheItem.timestamp < ttl;
}
/**
* 清理过期的缓存项
* @param ttl 缓存过期时间(毫秒)
*/
function cleanExpiredCache(ttl: number): void {
const now = Date.now();
for (const [key, item] of nacosInstanceCache.entries()) {
if (now - item.timestamp >= ttl) {
nacosInstanceCache.delete(key);
logger.debug(`🗑️ 清理过期缓存: ${key}`);
}
}
}
/**
* Nacos 实例接口
* 定义了从 Nacos 服务器返回的实例数据结构
*/
export interface NacosInstance {
instanceId: string; // 实例 ID
ip: string; // 实例 IP 地址
port: number; // 实例端口
weight: number; // 实例权重
healthy: boolean; // 实例是否健康
enabled: boolean; // 实例是否启用
ephemeral: boolean; // 是否为临时实例
clusterName: string; // 集群名称
serviceName: string; // 服务名称
metadata: Record<string, string>; // 实例元数据
instanceHeartBeatInterval?: number; // 心跳间隔
}
/**
* Nacos 服务列表响应接口
* 定义了 Nacos 服务实例列表查询的响应结构
*/
export interface NacosServiceListResponse {
name?: string; // 服务名称
groupName?: string; // 分组名称
clusters?: string; // 集群名称
cacheMillis?: number; // 缓存时间
hosts: NacosInstance[]; // 实例列表
lastRefTime?: number; // 最后刷新时间
checksum?: string; // 校验和
allIPs?: boolean; // 是否包含所有 IP
env?: string; // 环境
}
/**
* 注册服务实例到 Nacos
* 将当前服务实例注册到 Nacos 服务注册中心
*
* @param port 服务端口
* @returns 注册是否成功
*/
export async function registerServiceInstance(port: number): Promise<boolean> {
const config = getConfig();
// 检查泳道功能是否启用
if (!config.isLaneEnabled) {
logger.debug('🚫 泳道功能未启用,跳过服务注册');
return true;
}
try {
logger.debug(`🚀 开始服务注册流程,时间戳: ${new Date().toISOString()}`);
// 更新配置中的端口
updateConfigPort(port);
// 构建注册请求
const registrationUrl = `${config.nacosUrl}/nacos/v1/ns/instance`;
const params = new URLSearchParams();
// 设置基本参数
params.append('serviceName', config.serviceName);
params.append('ip', config.host);
params.append('port', port.toString());
params.append('weight', '1.0');
params.append('healthy', 'true');
params.append('metadata', JSON.stringify(config.metadata));
params.append('clusterName', config.currentLaneId || 'DEFAULT');
params.append('ephemeral', 'true');
// 添加过期时间参数
params.append('ipDeleteTimeout', config.instanceTtl.toString());
params.append('instanceHeartBeatInterval', config.heartbeatInterval.toString());
params.append('instanceHeartBeatTimeOut', (config.heartbeatInterval * 2).toString());
// 发送注册请求
logger.debug(`📤 发送注册请求到 Nacos: ${registrationUrl}`);
const response = await axios.post(registrationUrl, params, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'Nuxt3-LaneManager/1.0',
},
timeout: config.registrationTimeout,
});
// 记录响应结果
logger.debug(`📥 注册响应状态码: ${response.status}, 数据: ${JSON.stringify(response.data)}`);
if (response.status === 200 && response.data === 'ok') {
logger.success(`✅ 服务注册成功: ${config.serviceName}@${config.host}:${port} (泳道: ${config.currentLaneId})`);
// 设置全局状态
const globalState = getGlobalState();
globalState._laneMgrRegistered = true;
globalState._laneMgrPort = port;
// 启动心跳
startHeartbeat(port);
return true;
} else {
logger.error(`❌ 服务注册失败: ${response.data}`);
return false;
}
} catch (error) {
logger.error(`❌ 服务注册错误: ${error instanceof Error ? error.message : String(error)}`);
if (axios.isAxiosError(error) && error.response) {
logger.error(`🔴 HTTP状态码: ${error.response.status}, 响应: ${JSON.stringify(error.response.data)}`);
}
return false;
}
}
/**
* 从 Nacos 注销服务实例
* 从 Nacos 服务注册中心注销当前服务实例
*
* @param port 服务端口
* @returns 注销是否成功
*/
export async function deregisterServiceInstance(port: number): Promise<boolean> {
const config = getConfig();
// 检查泳道功能是否启用
if (!config.isLaneEnabled) {
logger.debug('🚫 泳道功能未启用,跳过服务注销');
return true;
}
try {
// 停止心跳
stopHeartbeat();
// 构建注销请求
const deregistrationUrl = `${config.nacosUrl}/nacos/v1/ns/instance`;
const params = new URLSearchParams();
params.append('serviceName', config.serviceName);
params.append('ip', config.host);
params.append('port', port.toString());
params.append('ephemeral', 'true');
// 发送注销请求
logger.debug(`📤 发送注销请求到 Nacos: ${deregistrationUrl}`);
const response = await axios.delete(`${deregistrationUrl}?${params.toString()}`, {
timeout: config.registrationTimeout,
});
// 记录响应结果
logger.debug(`📥 注销响应状态码: ${response.status}, 数据: ${JSON.stringify(response.data)}`);
if (response.status === 200 && response.data === 'ok') {
logger.success(`✅ 服务注销成功: ${config.serviceName}@${config.host}:${port}`);
// 更新全局状态
const globalState = getGlobalState();
globalState._laneMgrRegistered = false;
globalState._laneMgrPort = undefined;
return true;
} else {
logger.error(`❌ 服务注销失败: ${response.data}`);
return false;
}
} catch (error) {
logger.error(`❌ 服务注销错误: ${error instanceof Error ? error.message : String(error)}`);
return false;
}
}
/**
* 发送心跳到 Nacos
* 定期向 Nacos 发送心跳,保持服务实例的活跃状态
*
* @param port 服务端口
* @param logDetails 是否打印详细日志,默认为 false
* @returns 心跳是否成功
*/
export async function sendHeartbeat(port: number, logDetails: boolean = false): Promise<boolean> {
const config = getConfig();
// 检查泳道功能是否启用
if (!config.isLaneEnabled) {
if (logDetails) {
logger.debug('🚫 泳道功能未启用,跳过心跳');
}
return true;
}
try {
// 构建心跳请求
const heartbeatUrl = `${config.nacosUrl}/nacos/v1/ns/instance/beat`;
const params = new URLSearchParams();
params.append('serviceName', config.serviceName);
params.append('ip', config.host);
params.append('port', port.toString());
// 构建心跳数据
const now = new Date();
const beat = {
serviceName: config.serviceName,
ip: config.host,
port: port,
weight: 1.0,
metadata: {
...config.metadata,
lastHeartbeat: now.toISOString(), // 添加心跳时间戳
heartbeatTimestamp: now.getTime().toString(), // 添加时间戳(毫秒)
},
cluster: config.currentLaneId,
scheduled: false
};
params.append('beat', JSON.stringify(beat));
// 发送心跳请求(只在需要时打印日志)
if (logDetails) {
logger.debug(`💓 发送心跳到 Nacos: ${heartbeatUrl}`);
}
const response = await axios.put(`${heartbeatUrl}?${params.toString()}`, null, {
headers: {
'User-Agent': 'Nuxt3-LaneManager/1.0',
},
timeout: config.registrationTimeout / 2, // 心跳超时时间应该比注册短
});
// 记录响应结果(只在需要时打印日志)
if (logDetails) {
logger.debug(`📥 心跳响应状态码: ${response.status}, 数据: ${JSON.stringify(response.data)}`);
}
if (response.status === 200) {
if (logDetails) {
logger.debug(`✅ 心跳成功: ${config.serviceName}@${config.host}:${port}`);
}
return true;
} else {
// 失败总是打印日志,无论 logDetails 如何设置
logger.warn(`⚠️ 心跳失败: ${response.data}`);
return false;
}
} catch (error) {
logger.error(`❌ 心跳错误: ${error instanceof Error ? error.message : String(error)}`);
return false;
}
}
/**
* 启动心跳定时器
* 创建一个定时器,定期向 Nacos 发送心跳
*
* @param port 服务端口
*/
export function startHeartbeat(port: number): void {
const config = getConfig();
const globalState = getGlobalState();
// 如果已经有心跳定时器,先停止
stopHeartbeat();
// 初始化心跳计数器
globalState._laneMgrHeartbeatCount = 0;
// 创建新的心跳定时器
logger.info(`⏱️ 启动心跳定时器,间隔: ${config.heartbeatInterval}毫秒,每30次心跳打印一次日志`);
globalState._laneMgrHeartbeatTimer = setInterval(async () => {
try {
// 增加心跳计数器
if (globalState._laneMgrHeartbeatCount !== undefined) {
globalState._laneMgrHeartbeatCount++;
} else {
globalState._laneMgrHeartbeatCount = 1;
}
// 是否打印详细日志(每30次打印一次)
const shouldLogDetails = globalState._laneMgrHeartbeatCount % 30 === 0;
// 发送心跳
await sendHeartbeat(port, shouldLogDetails);
// 如果达到30次,在日志中显示计数器状态
if (shouldLogDetails) {
logger.info(`💓 心跳计数: ${globalState._laneMgrHeartbeatCount},继续保持连接...`);
}
} catch (error) {
logger.error(`❌ 心跳定时器错误: ${error instanceof Error ? error.message : String(error)}`);
}
}, config.heartbeatInterval);
}
/**
* 停止心跳定时器
* 清除心跳定时器,通常在服务注销时调用
*/
export function stopHeartbeat(): void {
const globalState = getGlobalState();
if (globalState._laneMgrHeartbeatTimer) {
logger.info('⏹️ 停止心跳定时器');
clearInterval(globalState._laneMgrHeartbeatTimer);
globalState._laneMgrHeartbeatTimer = undefined;
globalState._laneMgrHeartbeatCount = 0; // 重置心跳计数器
}
}
/**
* 内部函数:直接从 Nacos 查询实例(不使用缓存)
* @param serviceName 服务名称
* @param targetLaneId 目标泳道ID
* @param sortByHeartbeat 是否按心跳时间排序
* @returns 健康服务实例列表
*/
async function fetchNacosLaneInstancesFromServer(
serviceName: string,
targetLaneId: string,
sortByHeartbeat: boolean = true
): Promise<ServiceInstanceInfo[]> {
const config = getConfig();
logger.debug(`📡 直接从 Nacos 服务器查询实例,服务名: ${serviceName}, 目标泳道: ${targetLaneId}`);
try {
// 构建查询 URL
const nacosListUrl = `${config.nacosUrl}/nacos/v1/ns/instance/list`;
// 设置查询参数
const params = new URLSearchParams({
serviceName,
healthyOnly: 'true', // 只获取健康实例
groupName: config.nacosGroupName,
});
// 添加命名空间参数(如果有)
if (config.nacosNamespace) {
params.append('namespaceId', config.nacosNamespace);
}
logger.debug(`📤 Nacos 实例查询 URL: ${nacosListUrl}?${params.toString()}`);
// 发送查询请求
const response = await axios.get<NacosServiceListResponse>(`${nacosListUrl}?${params.toString()}`, {
timeout: config.registrationTimeout,
});
logger.debug(`📥 Nacos 实例查询响应状态: ${response.status}`);
// 处理响应数据
if (response.data && response.data.hosts) {
// 过滤出目标泳道的健康实例
const filteredInstances = response.data.hosts
.filter(
(instance) =>
instance.healthy && instance.enabled && instance.metadata && instance.metadata.laneId === targetLaneId,
)
.map(
(instance): ServiceInstanceInfo => {
// 尝试从元数据中获取最后心跳时间
let lastHeartbeat = Date.now();
// 检查实例元数据中是否有心跳时间信息
if (instance.metadata?.heartbeatTimestamp) {
// 优先使用数字时间戳(毫秒)
try {
const timestamp = parseInt(instance.metadata.heartbeatTimestamp, 10);
if (!isNaN(timestamp) && timestamp > 0) {
lastHeartbeat = timestamp;
}
} catch (error) {
logger.debug(`⚠️ 解析实例 ${instance.ip}:${instance.port} 数字时间戳失败: ${error}`);
}
} else if (instance.metadata?.lastHeartbeat) {
// 备选:使用ISO字符串时间戳
try {
const heartbeatTime = new Date(instance.metadata.lastHeartbeat).getTime();
if (!isNaN(heartbeatTime)) {
lastHeartbeat = heartbeatTime;
}
} catch (error) {
logger.debug(`⚠️ 解析实例 ${instance.ip}:${instance.port} ISO时间戳失败: ${error}`);
}
}
// 如果Nacos返回了实例的心跳间隔信息,可以估算最后心跳时间
if (instance.instanceHeartBeatInterval && !instance.metadata?.lastHeartbeat) {
// 使用当前时间减去心跳间隔的一半作为估算值
lastHeartbeat = Date.now() - (instance.instanceHeartBeatInterval / 2);
}
return {
ip: instance.ip,
port: instance.port,
serviceName: instance.serviceName,
clusterName: instance.clusterName,
ephemeral: instance.ephemeral,
metadata: instance.metadata,
status: 'UP',
lastHeartbeat,
version: instance.metadata?.version || 'unknown',
};
},
);
// 如果需要按心跳时间排序
if (sortByHeartbeat) {
filteredInstances.sort((a, b) => {
// 按最后心跳时间降序排列(最近心跳的在前)
const timeA = typeof a.lastHeartbeat === 'number' ? a.lastHeartbeat : new Date(a.lastHeartbeat).getTime();
const timeB = typeof b.lastHeartbeat === 'number' ? b.lastHeartbeat : new Date(b.lastHeartbeat).getTime();
return timeB - timeA;
});
logger.debug(`🔄 已按心跳时间排序 ${filteredInstances.length} 个实例`);
// 在调试模式下输出排序后的实例信息
if (filteredInstances.length > 0) {
logger.debug(`📊 实例心跳时间排序结果:`);
filteredInstances.forEach((instance, index) => {
const heartbeatTime = typeof instance.lastHeartbeat === 'number'
? new Date(instance.lastHeartbeat).toISOString()
: instance.lastHeartbeat;
logger.debug(` ${index + 1}. ${instance.ip}:${instance.port} - 心跳时间: ${heartbeatTime}`);
});
}
}
logger.info(`✅ 从服务器查询到 ${filteredInstances.length} 个属于泳道 ${targetLaneId} 的健康实例${sortByHeartbeat ? '(已按心跳时间排序)' : ''}`);
return filteredInstances;
} else {
logger.warn(`⚠️ Nacos 实例查询响应格式不正确或无实例列表:`, response.data);
return [];
}
} catch (error) {
logger.error(`❌ Nacos 实例查询失败: ${error instanceof Error ? error.message : String(error)}`);
if (axios.isAxiosError(error) && error.response) {
logger.error(`🔴 HTTP状态码: ${error.response.status}, 响应: ${JSON.stringify(error.response.data)}`);
}
return [];
}
}
/**
* 从 Nacos 获取指定泳道的健康服务实例(带缓存)
* 查询 Nacos 获取指定服务名称和泳道 ID 的健康实例列表
* 支持按心跳时间排序,优先选择最近心跳的实例
* 使用缓存和请求去重机制提高性能
*
* @param serviceName 服务名称
* @param targetLaneId 目标泳道ID
* @param sortByHeartbeat 是否按心跳时间排序(最近心跳的在前)
* @returns 健康服务实例列表
*/
export async function getNacosLaneInstances(
serviceName: string,
targetLaneId: string,
sortByHeartbeat: boolean = true
): Promise<ServiceInstanceInfo[]> {
const config = getConfig();
const cacheKey = getCacheKey(serviceName, targetLaneId);
const cacheTtl = config.nacosCacheTtl;
// 使用 console.log 确保日志一定会输出
console.log(`[multi-lane-manager] 🔍 获取 Nacos 实例,服务名: ${serviceName}, 目标泳道: ${targetLaneId}`);
logger.debug(`🔍 获取 Nacos 实例,服务名: ${serviceName}, 目标泳道: ${targetLaneId}, 缓存TTL: ${cacheTtl}ms`);
// 定期清理过期缓存
cleanExpiredCache(cacheTtl);
// 检查缓存
const cachedItem = nacosInstanceCache.get(cacheKey);
if (cachedItem) {
// 如果有正在进行的请求,等待该请求完成(请求去重)
if (cachedItem.promise) {
logger.debug(`⏳ 发现正在进行的请求,等待完成: ${cacheKey}`);
try {
return await cachedItem.promise;
} catch (error) {
// 如果正在进行的请求失败,移除缓存项并继续执行新请求
logger.warn(`⚠️ 正在进行的请求失败,移除缓存: ${cacheKey}, 错误: ${error}`);
nacosInstanceCache.delete(cacheKey);
}
}
// 如果缓存有效,直接返回缓存数据
else if (isCacheValid(cachedItem, cacheTtl)) {
logger.debug(`💾 使用缓存数据: ${cacheKey}, 缓存时间: ${new Date(cachedItem.timestamp).toISOString()}`);
return cachedItem.data;
}
// 缓存过期,移除缓存项
else {
logger.debug(`⏰ 缓存已过期,移除: ${cacheKey}`);
nacosInstanceCache.delete(cacheKey);
}
}
// 创建新的请求 Promise
logger.debug(`🌐 创建新的 Nacos 查询请求: ${cacheKey}`);
const requestPromise = fetchNacosLaneInstancesFromServer(serviceName, targetLaneId, sortByHeartbeat);
// 将 Promise 存储到缓存中(用于请求去重)
nacosInstanceCache.set(cacheKey, {
data: [], // 临时空数据
timestamp: Date.now(),
promise: requestPromise,
});
try {
// 等待请求完成
const result = await requestPromise;
// 更新缓存,移除 Promise
nacosInstanceCache.set(cacheKey, {
data: result,
timestamp: Date.now(),
});
logger.debug(`✅ 请求完成,已缓存结果: ${cacheKey}, 实例数量: ${result.length}`);
return result;
} catch (error) {
// 请求失败,移除缓存项
nacosInstanceCache.delete(cacheKey);
logger.error(`❌ Nacos 查询请求失败,移除缓存: ${cacheKey}, 错误: ${error}`);
throw error;
}
}
/**
* 清理所有 Nacos 实例查询缓存
* 用于手动清理缓存或在配置变更时重置缓存
*/
export function clearNacosInstanceCache(): void {
const cacheSize = nacosInstanceCache.size;
nacosInstanceCache.clear();
logger.info(`🗑️ 已清理所有 Nacos 实例缓存,清理了 ${cacheSize} 个缓存项`);
}
/**
* 获取当前缓存状态信息
* 用于调试和监控
*/
export function getNacosCacheStats(): {
totalItems: number;
validItems: number;
expiredItems: number;
pendingRequests: number;
} {
const config = getConfig();
const cacheTtl = config.nacosCacheTtl;
const now = Date.now();
let validItems = 0;
let expiredItems = 0;
let pendingRequests = 0;
for (const item of nacosInstanceCache.values()) {
if (item.promise) {
pendingRequests++;
} else if (now - item.timestamp < cacheTtl) {
validItems++;
} else {
expiredItems++;
}
}
return {
totalItems: nacosInstanceCache.size,
validItems,
expiredItems,
pendingRequests,
};
}