UNPKG

multi-lane-manager

Version:

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

453 lines (391 loc) 16.1 kB
import axios from 'axios'; import type { ServiceInstanceInfo } from '../types'; import { getConfig, getGlobalState, updateConfigPort } from './config'; import { logger } from './logger'; /** * 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 获取指定泳道的健康服务实例 * 查询 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(); // 使用 console.log 确保日志一定会输出 console.log(`[multi-lane-manager] 🔍 获取 Nacos 实例,服务名: ${serviceName}, 目标泳道: ${targetLaneId}`); 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 []; } }