UNPKG

multi-lane-manager

Version:

Nacos 泳道管理与请求路由组件

627 lines (546 loc) 21.3 kB
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, }; }