multi-lane-manager
Version:
Nacos 泳道管理与请求路由组件
502 lines (432 loc) • 20.1 kB
text/typescript
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;
}