UNPKG

multi-lane-manager

Version:

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

502 lines (432 loc) 20.1 kB
import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs'; import { logger } from './logger'; import { DEFAULT_LANE_SERVER, DEFAULT_LANE_NAMESPACE, DEFAULT_LANE_GROUP_NAME, DEFAULT_LANE_HEARTBEAT_INTERVAL, DEFAULT_LANE_INSTANCE_TTL, DEFAULT_SERVICE_NAME, DEFAULT_LANE_ID, DEFAULT_HOST, DEFAULT_LANE_TARGET_HEADER, DEFAULT_LANE_ENABLED, DEFAULT_LANE_COOKIE_ENABLED, DEFAULT_LANE_PROXY_TIMEOUT, DEFAULT_LANE_REGISTRATION_TIMEOUT, DEFAULT_NACOS_CACHE_TTL, getEnvOrDefault, getBooleanEnv, getNumberEnv } from './defaults'; /** * 检查IP地址是否为私有网络地址 * @param ip IP地址 * @returns 是否为私有网络地址 */ function isPrivateIP(ip: string): boolean { // 私有网络地址范围: // 10.0.0.0/8 (10.0.0.0 - 10.255.255.255) // 172.16.0.0/12 (172.16.0.0 - 172.31.255.255) // 192.168.0.0/16 (192.168.0.0 - 192.168.255.255) const parts = ip.split('.').map(Number); if (parts.length !== 4) return false; const [a, b] = parts; return ( a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) ); } /** * 检查IP地址是否应该被跳过 * @param ip IP地址 * @param interfaceName 网络接口名称 * @returns 是否应该跳过 */ function shouldSkipIP(ip: string, interfaceName: string): boolean { // 跳过Docker桥接网络(通常是172.17.x.x) if (ip.startsWith('172.17.')) { logger.debug(`⏭️ 跳过Docker桥接网络地址: ${ip} (接口: ${interfaceName})`); return true; } // 跳过Docker默认网络(通常是172.18.x.x到172.31.x.x) const parts = ip.split('.').map(Number); if (parts[0] === 172 && parts[1] >= 17 && parts[1] <= 31) { logger.debug(`⏭️ 跳过Docker网络地址: ${ip} (接口: ${interfaceName})`); return true; } // 跳过某些虚拟接口 const skipInterfaces = ['docker0', 'br-', 'veth']; if (skipInterfaces.some(skip => interfaceName.startsWith(skip))) { logger.debug(`⏭️ 跳过虚拟接口: ${interfaceName} (IP: ${ip})`); return true; } return false; } /** * 自动检测当前实例的IP地址 * 完全自动化,无需手动配置,遵循容器/K8s最佳实践 * * @returns 检测到的IP地址 */ function detectInstanceIP(): string { logger.info(`🔍 开始自动检测实例IP地址...`); // 1. 自动检测网络接口(核心逻辑) try { const networkInterfaces = os.networkInterfaces(); logger.debug(`🔍 检测到的网络接口: ${JSON.stringify(Object.keys(networkInterfaces))}`); // 容器环境优先的网络接口(按优先级排序) const containerInterfaces = [ 'eth0', // 最常见的容器网络接口 'eth1', // 多网卡容器 'ens3', // 云环境常见接口 'ens4', // 云环境常见接口 ]; // 物理机/虚拟机环境的网络接口 const physicalInterfaces = [ 'ens160', // VMware虚拟机 'ens192', // VMware虚拟机 'enp0s3', // VirtualBox 'enp0s8', // VirtualBox 'en0', // macOS 'en1', // macOS 'wlan0', // 无线网络 ]; // 首先尝试容器环境的网络接口 for (const interfaceName of containerInterfaces) { const networkInterface = networkInterfaces[interfaceName]; if (networkInterface) { for (const addr of networkInterface) { if (addr.family === 'IPv4' && !addr.internal && !shouldSkipIP(addr.address, interfaceName)) { logger.info(`✅ 从容器网络接口 ${interfaceName} 获取IP地址: ${addr.address}`); return addr.address; } } } } // 然后尝试物理机/虚拟机的网络接口 for (const interfaceName of physicalInterfaces) { const networkInterface = networkInterfaces[interfaceName]; if (networkInterface) { for (const addr of networkInterface) { if (addr.family === 'IPv4' && !addr.internal && !shouldSkipIP(addr.address, interfaceName)) { logger.info(`✅ 从物理网络接口 ${interfaceName} 获取IP地址: ${addr.address}`); return addr.address; } } } } // 最后遍历所有接口,寻找合适的IP const candidateIPs: Array<{ip: string, interface: string, isPrivate: boolean}> = []; for (const [interfaceName, addresses] of Object.entries(networkInterfaces)) { if (!addresses) continue; for (const addr of addresses) { if (addr.family === 'IPv4' && !addr.internal && !shouldSkipIP(addr.address, interfaceName)) { candidateIPs.push({ ip: addr.address, interface: interfaceName, isPrivate: isPrivateIP(addr.address) }); } } } if (candidateIPs.length > 0) { // 优先选择私有网络IP(容器/K8s环境通常使用私有网络) const privateIPs = candidateIPs.filter(candidate => candidate.isPrivate); if (privateIPs.length > 0) { const selected = privateIPs[0]; logger.info(`✅ 选择私有网络IP: ${selected.ip} (接口: ${selected.interface})`); return selected.ip; } // 如果没有私有网络IP,选择第一个可用的IP const selected = candidateIPs[0]; logger.info(`✅ 选择公网IP: ${selected.ip} (接口: ${selected.interface})`); return selected.ip; } } catch (error) { logger.warn(`⚠️ 自动检测网络接口失败: ${error instanceof Error ? error.message : String(error)}`); } // 2. 检查Kubernetes环境变量(作为备选方案) if (process.env.POD_IP && process.env.POD_IP !== 'localhost' && process.env.POD_IP !== '127.0.0.1') { logger.info(`🎯 网络接口检测失败,使用Kubernetes Pod IP: ${process.env.POD_IP}`); return process.env.POD_IP; } // 3. 检查其他容器环境变量(最后的备选方案) if (process.env.CONTAINER_IP && process.env.CONTAINER_IP !== 'localhost' && process.env.CONTAINER_IP !== '127.0.0.1') { logger.info(`📦 网络接口检测失败,使用容器环境变量IP: ${process.env.CONTAINER_IP}`); return process.env.CONTAINER_IP; } // 4. 如果都没有找到,使用默认值并记录详细的建议 logger.error(`❌ 无法自动检测到有效的IP地址!`); logger.error(`💡 这通常发生在以下情况:`); logger.error(` 1. 容器网络配置异常`); logger.error(` 2. 网络接口名称不在预期范围内`); logger.error(` 3. 所有检测到的IP都被过滤掉了`); logger.error(`🔧 解决方案:`); logger.error(` - 在Kubernetes中使用Downward API注入POD_IP`); logger.error(` - 检查容器网络配置`); logger.error(` - 如果必要,可以设置HOST环境变量作为临时解决方案`); return DEFAULT_HOST; } /** * 泳道管理器配置接口 * 定义了泳道管理器所需的所有配置项 */ export interface LaneManagerConfig { // Nacos 服务器配置 nacosServerAddr: string; // Nacos 服务器地址(从 NACOS_SERVER 解析) nacosServerPort: string; // Nacos 服务器端口(从 NACOS_SERVER 解析) nacosUrl: string; // 完整的 Nacos 服务器 URL nacosNamespace: string; // Nacos 命名空间 nacosGroupName: string; // Nacos 分组名称 // 服务配置 serviceName: string; // 当前服务名称 currentLaneId: string; // 当前泳道 ID host: string; // 服务主机名 port: number | null; // 服务端口,初始为 null // 泳道配置 targetLaneHeaderKey: string; // 目标泳道请求头键名(小写形式) isLaneEnabled: boolean; // 是否启用泳道功能 isLaneCookieEnabled: boolean; // 是否启用从cookie中检测泳道ID // 超时配置 proxyTimeout: number; // 代理请求超时时间(毫秒) registrationTimeout: number; // 注册请求超时时间(毫秒) heartbeatInterval: number; // 心跳间隔时间(毫秒) instanceTtl: number; // 实例过期时间(毫秒) // 缓存配置 nacosCacheTtl: number; // Nacos 实例查询缓存时间(毫秒) // 元数据 metadata: Record<string, any>; // 服务实例元数据 } // 配置缓存,避免重复创建配置对象 let configCache: LaneManagerConfig | null = null; /** * 获取配置,优先使用缓存 * @returns 泳道管理器配置对象 */ export function getConfig(): LaneManagerConfig { if (configCache) { return configCache; } // 创建新配置 configCache = createConfig(); return configCache; } /** * 清除配置缓存,强制下次获取时重新创建 * 在配置需要重新加载时调用 */ export function clearConfigCache(): void { configCache = null; logger.debug('🔄 配置缓存已清除'); } /** * 创建配置对象 * 从环境变量中读取配置,并设置默认值 * @returns 新创建的配置对象 */ function createConfig(): LaneManagerConfig { // 从环境变量读取基本配置,并设置默认值 const nacosServer = process.env.LANE_SERVER || process.env.NACOS_SERVER || DEFAULT_LANE_SERVER; logger.debug(`🔍 Nacos服务器配置: LANE_SERVER=${process.env.LANE_SERVER}, NACOS_SERVER=${process.env.NACOS_SERVER}, DEFAULT_LANE_SERVER=${DEFAULT_LANE_SERVER}`); logger.debug(`🔍 最终使用的Nacos服务器: ${nacosServer}`); // 解析Nacos服务器地址和端口 const parts = nacosServer.split(':'); const nacosServerAddr = parts[0] || 'localhost'; const nacosServerPort = parts.length > 1 ? parts[1] : '8848'; logger.debug(`🔍 解析后的Nacos地址: ${nacosServerAddr}, 端口: ${nacosServerPort}`); // 服务名称获取详细过程 logger.debug(`🔍 服务名称获取过程开始...`); logger.debug(`🔍 环境变量 SERVICE_NAME=${process.env.SERVICE_NAME}`); logger.debug(`🔍 DEFAULT_SERVICE_NAME=${DEFAULT_SERVICE_NAME}`); // 检查 package.json 中的 name let packageJsonName = '未检测'; try { // 尝试从多个可能的位置读取 package.json const possiblePaths = [ // 优先检查 server/package.json (适用于 node server/index.mjs 启动方式) path.resolve(process.cwd(), 'server', 'package.json'), // 当前工作目录 path.resolve(process.cwd(), 'package.json'), // 当前工作目录的父目录(可能是项目根目录) path.resolve(process.cwd(), '..', 'package.json'), // 常见的服务器输出目录 path.resolve(process.cwd(), 'server', '..', 'package.json'), path.resolve(process.cwd(), '.output', 'server', '..', 'package.json'), path.resolve(process.cwd(), 'dist', '..', 'package.json'), path.resolve(process.cwd(), 'build', '..', 'package.json'), // Nuxt输出目录 path.resolve(process.cwd(), '.output', 'package.json'), // 应用根目录(如果设置了APP_ROOT环境变量) ...(process.env.APP_ROOT ? [path.resolve(process.env.APP_ROOT, 'package.json')] : []), // 向上查找两级目录 path.resolve(process.cwd(), '..', '..', 'package.json'), ]; // 尝试每个可能的路径 for (const packageJsonPath of possiblePaths) { if (fs.existsSync(packageJsonPath)) { logger.debug(`🔍 尝试读取 package.json: ${packageJsonPath}`); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); if (packageJson && packageJson.name) { packageJsonName = packageJson.name; logger.debug(`✅ 成功从 ${packageJsonPath} 读取到 package.json,name: ${packageJsonName}`); // 如果是从 server/package.json 获取的,特别标记一下 if (packageJsonPath.includes('server/package.json')) { logger.info(`🚀 从 server/package.json 获取服务名称,适用于 node server/index.mjs 启动方式`); } break; } else { logger.debug(`⚠️ 从 ${packageJsonPath} 读取到 package.json,但没有 name 字段`); } } } } catch (error) { packageJsonName = `检测出错: ${error instanceof Error ? error.message : String(error)}`; logger.debug(`❌ 读取 package.json 失败: ${packageJsonName}`); } logger.debug(`🔍 package.json name=${packageJsonName}`); // 优先从环境变量获取服务名称,然后从 package.json 获取 let serviceName = ''; if (process.env.SERVICE_NAME) { serviceName = process.env.SERVICE_NAME; logger.info(`✅ 从环境变量 SERVICE_NAME 获取服务名称: ${serviceName}`); } else if (process.env.NITRO_APP_NAME) { serviceName = process.env.NITRO_APP_NAME; logger.info(`✅ 从环境变量 NITRO_APP_NAME 获取服务名称: ${serviceName}`); } else if (process.env.APP_NAME) { serviceName = process.env.APP_NAME; logger.info(`✅ 从环境变量 APP_NAME 获取服务名称: ${serviceName}`); } else if (process.env.npm_package_name) { serviceName = process.env.npm_package_name; logger.info(`✅ 从环境变量 npm_package_name 获取服务名称: ${serviceName}`); } else if (packageJsonName && packageJsonName !== '未检测' && packageJsonName !== '未设置' && !packageJsonName.startsWith('检测出错')) { serviceName = packageJsonName; logger.info(`✅ 从 package.json 获取服务名称: ${serviceName}`); } else { // 没有找到服务名称 serviceName = ''; logger.error(`❌ 警告: 无法获取服务名称,泳道功能将被禁用`); logger.error(`❌ 请通过以下方式之一设置服务名称:`); logger.error(` 1. 设置环境变量 SERVICE_NAME`); logger.error(` 2. 设置环境变量 NITRO_APP_NAME, APP_NAME 或 npm_package_name`); logger.error(` 3. 确保 server/package.json 或项目根目录的 package.json 中有 name 字段`); } logger.debug(`🔍 最终使用的服务名称: ${serviceName || '未设置 (泳道功能将被禁用)'}`); const currentLaneId = getEnvOrDefault('LANE_ID', DEFAULT_LANE_ID); // 使用智能IP检测 logger.debug(`🔍 开始检测实例IP地址...`); const host = detectInstanceIP(); logger.info(`🌐 最终使用的IP地址: ${host}`); // 创建服务实例元数据 const metadata = { laneId: currentLaneId, // 泳道 ID environment: getEnvOrDefault('NODE_ENV', 'development'), // 环境 registeredAt: new Date().toISOString(), // 注册时间 framework: 'nuxt3', // 框架 version: getEnvOrDefault('npm_package_version', 'unknown'), // 版本 hostname: os.hostname(), // 主机名 nodeVersion: process.versions.node, // Node.js 版本 }; // 检查服务名称是否有效 const hasValidServiceName = !!serviceName; // 检查泳道功能是否启用 const isLaneEnabled = hasValidServiceName ? getBooleanEnv('LANE_ENABLE', DEFAULT_LANE_ENABLED) : false; // 如果泳道功能启用但没有有效的服务名称,则停止应用 if (isLaneEnabled && !hasValidServiceName) { logger.error(`❌ 错误: 泳道功能已启用,但无法获取有效的服务名称,应用将停止启动`); logger.error(`❌ 请通过以下方式之一设置服务名称:`); logger.error(` 1. 设置环境变量 SERVICE_NAME`); logger.error(` 2. 设置环境变量 NITRO_APP_NAME, APP_NAME 或 npm_package_name`); logger.error(` 3. 确保 server/package.json 或项目根目录的 package.json 中有 name 字段`); // 停止应用 process.exit(1); } // 如果没有有效的服务名称但泳道功能未启用,则只显示警告 if (!hasValidServiceName && !isLaneEnabled) { logger.error(`❌ 警告: 无法获取有效的服务名称,已自动禁用泳道功能`); logger.error(`❌ 请通过以下方式之一设置服务名称:`); logger.error(` 1. 设置环境变量 SERVICE_NAME`); logger.error(` 2. 设置环境变量 NITRO_APP_NAME, APP_NAME 或 npm_package_name`); logger.error(` 3. 确保 server/package.json 或项目根目录的 package.json 中有 name 字段`); } // 创建完整配置对象 const config: LaneManagerConfig = { // Nacos 服务器配置 nacosServerAddr, nacosServerPort, nacosUrl: `http://${nacosServerAddr}:${nacosServerPort}`, nacosNamespace: getEnvOrDefault('LANE_NAMESPACE', process.env.NACOS_NAMESPACE || DEFAULT_LANE_NAMESPACE), nacosGroupName: getEnvOrDefault('LANE_GROUP_NAME', process.env.NACOS_GROUP_NAME || DEFAULT_LANE_GROUP_NAME), // 服务配置 serviceName, currentLaneId, host, port: null, // 初始为 null,将在服务启动时设置 // 泳道配置 targetLaneHeaderKey: (() => { // 详细记录 targetLaneHeaderKey 的获取过程 logger.debug(`🔍 目标泳道请求头键名获取过程开始...`); logger.debug(`🔍 环境变量 LANE_TARGET_HEADER=${process.env.LANE_TARGET_HEADER}`); logger.debug(`🔍 环境变量 TARGET_LANE_HEADER=${process.env.TARGET_LANE_HEADER}`); logger.debug(`🔍 默认值 DEFAULT_LANE_TARGET_HEADER=${DEFAULT_LANE_TARGET_HEADER}`); const headerKey = getEnvOrDefault('LANE_TARGET_HEADER', process.env.TARGET_LANE_HEADER || DEFAULT_LANE_TARGET_HEADER); logger.debug(`🔍 最终使用的目标泳道请求头键名: ${headerKey}`); return headerKey; })(), // 使用前面计算的 isLaneEnabled 值 isLaneEnabled: (() => { logger.debug(`🔍 泳道功能启用状态: ${isLaneEnabled} (hasValidServiceName=${hasValidServiceName}, LANE_ENABLE=${process.env.LANE_ENABLE}, DEFAULT_LANE_ENABLED=${DEFAULT_LANE_ENABLED})`); return isLaneEnabled; })(), isLaneCookieEnabled: (() => { const enabled = getBooleanEnv('LANE_COOKIE_ENABLE', DEFAULT_LANE_COOKIE_ENABLED); logger.debug(`🔍 Cookie泳道检测启用状态: ${enabled} (LANE_COOKIE_ENABLE=${process.env.LANE_COOKIE_ENABLE}, DEFAULT_LANE_COOKIE_ENABLED=${DEFAULT_LANE_COOKIE_ENABLED})`); return enabled; })(), // 超时配置 proxyTimeout: getNumberEnv('LANE_PROXY_TIMEOUT', process.env.PROXY_TIMEOUT ? parseInt(process.env.PROXY_TIMEOUT) : DEFAULT_LANE_PROXY_TIMEOUT), registrationTimeout: getNumberEnv('LANE_REGISTRATION_TIMEOUT', process.env.REGISTRATION_TIMEOUT ? parseInt(process.env.REGISTRATION_TIMEOUT) : DEFAULT_LANE_REGISTRATION_TIMEOUT), heartbeatInterval: getNumberEnv('LANE_HEARTBEAT_INTERVAL', process.env.NACOS_HEARTBEAT_INTERVAL ? parseInt(process.env.NACOS_HEARTBEAT_INTERVAL) : DEFAULT_LANE_HEARTBEAT_INTERVAL), instanceTtl: getNumberEnv('LANE_INSTANCE_TTL', process.env.NACOS_INSTANCE_TTL ? parseInt(process.env.NACOS_INSTANCE_TTL) : DEFAULT_LANE_INSTANCE_TTL), // 缓存配置 nacosCacheTtl: getNumberEnv('NACOS_CACHE_TTL', process.env.LANE_CACHE_TTL ? parseInt(process.env.LANE_CACHE_TTL) : DEFAULT_NACOS_CACHE_TTL), // 元数据 metadata, }; // 记录配置信息 logger.debug('🔧 已创建配置对象:', config); return config; } /** * 更新配置中的端口信息 * 在服务启动后调用,设置实际的服务端口 * @param port 服务端口 */ export function updateConfigPort(port: number): void { if (!configCache) { configCache = createConfig(); } configCache.port = port; logger.debug(`🔌 配置端口已更新: ${port}`); } /** * 全局状态接口 * 定义了在全局对象上存储的泳道管理器状态 */ export interface GlobalWithLaneManager { _laneMgrRegistered?: boolean; // 是否已注册服务 _laneMgrPort?: number; // 服务端口 _laneMgrHeartbeatTimer?: NodeJS.Timeout; // 心跳定时器 _laneMgrHeartbeatCount?: number; // 心跳计数器,用于控制日志打印频率 } /** * 获取全局状态对象 * 用于在不同模块间共享状态 * @returns 全局状态对象 */ export function getGlobalState(): GlobalWithLaneManager { return global as unknown as GlobalWithLaneManager; }