UNPKG

multi-lane-manager

Version:

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

569 lines (471 loc) 24.9 kB
import type { H3Event } from "h3"; import type { ServiceInstanceInfo } from "../types"; import { getConfig, getGlobalState, updateConfigPort } from "../utils/config"; import { DEFAULT_PORT, DEFAULT_LANE_TARGET_HEADER, HEADER_LANE_DEBUG, HEADER_LANE_DETAIL, getSafeHeaderValue } from "../utils/defaults"; import { logger } from "../utils/logger"; import { deregisterServiceInstance, getNacosLaneInstances, registerServiceInstance, startHeartbeat, stopHeartbeat } from "../utils/nacos"; import { LoadBalanceStrategy, proxyRequestWithFailover, selectInstance } from "../utils/proxy"; /** * 从cookie字符串中解析指定key的值 * * @param cookieHeader cookie字符串,格式如 "key1=value1; key2=value2" * @param key 要查找的cookie键名 * @returns 找到的cookie值,如果未找到则返回undefined */ function parseCookieValue(cookieHeader: string, key: string): string | undefined { console.log(`[multi-lane-manager] 解析Cookie: cookieHeader=${cookieHeader}, key=${key}`); if (!cookieHeader || !key) { console.log(`[multi-lane-manager] Cookie解析失败: cookieHeader或key为空`); return undefined; } // 将cookie字符串分割成键值对数组 const cookies = cookieHeader.split(';'); console.log(`[multi-lane-manager] Cookie分割结果: ${JSON.stringify(cookies)}`); // 查找匹配的cookie for (const cookie of cookies) { console.log(`[multi-lane-manager] 处理Cookie片段: ${cookie}`); const parts = cookie.split('='); if (parts.length < 2) { console.log(`[multi-lane-manager] 无效的Cookie格式: ${cookie}`); continue; } const cookieKey = parts[0].trim(); const cookieValue = parts.slice(1).join('=').trim(); // 处理值中可能包含等号的情况 console.log(`[multi-lane-manager] Cookie键值对: key=${cookieKey}, value=${cookieValue}`); // 不区分大小写比较cookie键名 if (cookieKey.toLowerCase() === key.toLowerCase()) { console.log(`[multi-lane-manager] 找到匹配的Cookie: ${cookieKey}=${cookieValue}`); return cookieValue; } } console.log(`[multi-lane-manager] 未找到匹配的Cookie: ${key}`); return undefined; } /** * 从环境变量或 Nitro 配置中获取服务器端口 * 优先使用环境变量中的端口配置 * * @returns 服务器端口 */ export function getServerPort(): number { // 优先使用环境变量中的端口 const port = parseInt(process.env.NITRO_PORT || process.env.PORT || String(DEFAULT_PORT), 10); logger.debug(`🔍 从环境变量获取服务器端口: ${port}`); return port; } /** * 自动检测和注册服务实例 * 这个函数主要负责服务注册和设置进程退出处理程序 * * @returns 是否尝试了注册 */ export function detectPortAndRegisterOnFirstRequest(): boolean { logger.info(`====== 🚀 开始端口检测和服务注册流程 ======`); // 获取配置 logger.debug(`📋 正在加载配置...`); const config = getConfig(); try { // 检查泳道功能是否启用 logger.debug(`🔍 检查泳道功能是否启用: ${config.isLaneEnabled}`); if (!config.isLaneEnabled) { logger.debug(`🚫 泳道功能未启用,跳过初始注册`); return false; } // 获取服务器端口 const serverPort = getServerPort(); logger.info(`🔌 检测到服务器端口: ${serverPort}`); // 检查全局注册状态 const globalState = getGlobalState(); if (!globalState._laneMgrRegistered) { logger.info(`🆕 首次检测到端口 ${serverPort},准备注册服务...`); // 设置全局注册状态 globalState._laneMgrRegistered = true; globalState._laneMgrPort = serverPort; // 更新配置中的端口 updateConfigPort(serverPort); // 注册服务实例 registerServiceInstance(serverPort) .then((success) => { if (success) { logger.info(`✅ 服务注册成功,设置进程退出处理程序`); // 创建优雅退出处理函数 const gracefulShutdown = async () => { logger.info(`🛑 应用退出/信号,正在注销服务,端口=${serverPort}...`); await deregisterServiceInstance(serverPort); }; // 注册进程退出事件处理程序 process.on("beforeExit", gracefulShutdown); process.on("SIGINT", async () => { logger.info("🔴 收到 SIGINT 信号,执行注销..."); await gracefulShutdown(); process.exit(0); }); process.on("SIGTERM", async () => { logger.info("🔴 收到 SIGTERM 信号,执行注销..."); await gracefulShutdown(); process.exit(0); }); } else { logger.warn(`⚠️ 服务注册失败,不设置退出处理程序`); } }) .catch((err) => { logger.error(`❌ 注册过程中出错: ${err.message}`, err.stack); }); logger.info(`====== ✅ 端口检测和服务注册流程完成,返回 true ======`); return true; } else { logger.info(`ℹ️ 服务已注册 (端口: ${globalState._laneMgrPort}),跳过初始注册`); } logger.info(`====== ✅ 端口检测和服务注册流程完成,返回 false ======`); return false; } catch (error) { logger.error( `❌ 端口检测或初始注册错误: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error.stack : "", ); return false; } } /** * 创建服务器中间件以处理跨泳道代理 * 这个中间件负责检测请求的目标泳道,并在必要时将请求转发到目标泳道的实例 * 注意:服务注册逻辑已移至 Nitro 插件 * * @returns 中间件处理函数 */ export function createServerMiddleware() { const config = getConfig(); // 使用 console.log 确保日志一定会输出 console.log(`[multi-lane-manager] 🔧 服务器中间件初始化: 启用状态=${config.isLaneEnabled}, 当前泳道ID=${config.currentLaneId}, 服务名=${config.serviceName}, 目标泳道Header=${config.targetLaneHeaderKey || DEFAULT_LANE_TARGET_HEADER.toLowerCase()}, Cookie检测=${config.isLaneCookieEnabled ? '启用' : '禁用'}`); logger.info( `🔧 服务器中间件初始化: 启用状态=${config.isLaneEnabled}, 当前泳道ID=${config.currentLaneId}, 服务名=${config.serviceName}, 目标泳道Header=${config.targetLaneHeaderKey || DEFAULT_LANE_TARGET_HEADER.toLowerCase()}, Cookie检测=${config.isLaneCookieEnabled ? '启用' : '禁用'}`, ); return async (event: H3Event) => { // 检查是否启用了调试模式 const debugHeaderKey = HEADER_LANE_DEBUG.toLowerCase(); const debugHeaderValue = event.node.req.headers[debugHeaderKey]; const isDebugMode = !!debugHeaderValue; // 记录调试模式检测过程 console.log(`[multi-lane-manager] 调试模式检测: headerKey=${HEADER_LANE_DEBUG}, headerKeyLower=${debugHeaderKey}, value=${debugHeaderValue}, isDebugMode=${isDebugMode}`); const debugInfo: string[] = []; // 如果启用了调试模式,收集调试信息 if (isDebugMode) { debugInfo.push(`请求时间: ${new Date().toISOString()}`); debugInfo.push(`请求路径: ${event.node.req.method} ${event.node.req.url || ""}`); debugInfo.push(`当前泳道: ${config.currentLaneId}`); debugInfo.push(`服务名称: ${config.serviceName}`); } // 检查请求是否已经被处理过 if (event.context._laneManagerHandled) { if (isDebugMode) debugInfo.push(`状态: 请求已被泳道管理器处理,跳过`); logger.debug(`⏭️ 请求已被泳道管理器处理,跳过`); return; } const requestPath = event.node.req.url || ""; // 特别记录 /_nuxt/ 路径的请求 if (event.path.startsWith('/_nuxt/')) { console.log(`[multi-lane-manager] Intercepting _nuxt request2: ${event.path}`); logger.info(`🔍 拦截到静态资源请求: ${event.path}`); } // 特别记录 /api/ 路径的请求 if (event.path.startsWith('/api/')) { console.log(`[multi-lane-manager] Intercepting API request: ${event.path}`); logger.info(`🔍 拦截到API请求: ${event.path}, 方法: ${event.node.req.method}, 头部: ${JSON.stringify(event.node.req.headers)}`); } // 强制使用 console.log 确保日志一定会输出 console.log(`[multi-lane-manager] 📥 中间件接收请求: ${event.node.req.method} ${requestPath}`); logger.debug(`📥 中间件接收请求: ${event.node.req.method} ${requestPath}`); if (!config.isLaneEnabled) { if (isDebugMode) debugInfo.push(`状态: 泳道功能未启用,跳过代理逻辑`); logger.debug(`🚫 泳道功能未启用,跳过代理逻辑`); // 如果启用了调试模式,添加调试响应头 if (isDebugMode && !event.node.res.headersSent) { try { event.node.res.setHeader(HEADER_LANE_DETAIL, getSafeHeaderValue(debugInfo)); } catch (error) { logger.error(`❌ 设置调试响应头时出错: ${error instanceof Error ? error.message : String(error)}`); } } return; } // 标记请求正在被处理 event.context._laneManagerProcessing = true; // 获取目标泳道ID const headerKey = config.targetLaneHeaderKey || DEFAULT_LANE_TARGET_HEADER; // 记录请求头信息,帮助调试 console.log(`[multi-lane-manager] 请求头信息:`, event.node.req.headers); // HTTP 请求头是不区分大小写的,在 Node.js 中会被自动转换为小写 const headerKeyLower = headerKey.toLowerCase(); // 1. 首先尝试从header中获取泳道ID const requestTargetLaneIdHeaderValue = event.node.req.headers[headerKeyLower]; let requestTargetLaneId = Array.isArray(requestTargetLaneIdHeaderValue) ? requestTargetLaneIdHeaderValue[0]?.trim() : requestTargetLaneIdHeaderValue?.trim(); // 记录泳道ID获取过程 console.log(`[multi-lane-manager] 尝试从请求头获取泳道ID: headerKey=${headerKey}, headerKeyLower=${headerKeyLower}, value=${requestTargetLaneIdHeaderValue}`); if (isDebugMode) { debugInfo.push(`目标泳道Header键: ${headerKey} (实际查找: ${headerKeyLower})`); } // 2. 如果header中没有泳道ID,且启用了cookie功能,则尝试从cookie中获取 if (!requestTargetLaneId && config.isLaneCookieEnabled) { const cookieHeader = event.node.req.headers.cookie; if (cookieHeader) { // 记录 Cookie 信息,帮助调试 console.log(`[multi-lane-manager] Cookie信息: ${cookieHeader}`); // 从cookie字符串中解析出目标泳道ID // 使用与header完全相同的键名,不再移除'x-'前缀 const cookieKey = headerKey; const cookieValue = parseCookieValue(cookieHeader, cookieKey); // 记录 Cookie 解析过程 console.log(`[multi-lane-manager] 尝试从Cookie获取泳道ID: cookieKey=${cookieKey}, value=${cookieValue}`); if (cookieValue) { requestTargetLaneId = cookieValue.trim(); console.log(`[multi-lane-manager] 🍪 从Cookie中获取到泳道ID: ${requestTargetLaneId}, Cookie键名: ${cookieKey}`); logger.debug(`🍪 从Cookie中获取到泳道ID: ${requestTargetLaneId}, Cookie键名: ${cookieKey}`); if (isDebugMode) debugInfo.push(`泳道ID来源: Cookie, 值: ${requestTargetLaneId}`); } else if (isDebugMode) { debugInfo.push(`Cookie中未找到泳道ID, Cookie键: ${cookieKey}`); } } else if (isDebugMode) { debugInfo.push(`请求中没有Cookie`); } } else if (requestTargetLaneId) { console.log(`[multi-lane-manager] 🔤 从Header中获取到泳道ID: ${requestTargetLaneId}`); logger.debug(`🔤 从Header中获取到泳道ID: ${requestTargetLaneId}`); if (isDebugMode) debugInfo.push(`泳道ID来源: Header, 值: ${requestTargetLaneId}`); } else if (isDebugMode) { debugInfo.push(`未找到泳道ID`); } // 处理请求的泳道路由逻辑 if (requestTargetLaneId && requestTargetLaneId !== config.currentLaneId) { // 1. 如果请求指定了目标泳道,且与当前泳道不同,则进行代理 // 确定泳道ID的来源 const sourceType = event.node.req.headers[headerKey] ? 'Header' : 'Cookie'; // 使用 console.log 确保日志一定会输出 console.log(`[multi-lane-manager] 🔀 检测到跨泳道请求: 当前泳道=${config.currentLaneId}, 目标泳道=${requestTargetLaneId} (来源: ${sourceType}), 路径: ${event.path}`); logger.info(`🔀 检测到跨泳道请求: 当前泳道=${config.currentLaneId}, 目标泳道=${requestTargetLaneId} (来源: ${sourceType}), 路径: ${event.path}`); if (isDebugMode) { debugInfo.push(`处理: 跨泳道请求`); debugInfo.push(`目标泳道: ${requestTargetLaneId}`); } } else if (!requestTargetLaneId) { // 2. 如果请求没有指定泳道ID,尝试路由到 baseline 泳道 logger.debug(`🔍 请求未指定泳道ID,尝试查找 baseline 泳道实例`); if (isDebugMode) { debugInfo.push(`处理: 未指定泳道ID,尝试路由到 baseline 泳道`); } // 如果当前已经是 baseline 泳道,直接在本地处理 if (config.currentLaneId === 'baseline') { logger.debug(`✅ 当前已是 baseline 泳道,在本地处理请求`); if (isDebugMode) { debugInfo.push(`状态: 当前已是 baseline 泳道,在本地处理`); // 如果启用了调试模式,添加调试响应头 if (!event.node.res.headersSent) { try { event.node.res.setHeader(HEADER_LANE_DETAIL, getSafeHeaderValue(debugInfo)); } catch (error) { logger.error(`❌ 设置调试响应头时出错: ${error instanceof Error ? error.message : String(error)}`); } } } // 清除处理中标记,但不标记为已处理,因为请求将由后续中间件处理 event.context._laneManagerProcessing = false; return; } // 设置目标泳道ID为 baseline requestTargetLaneId = 'baseline'; if (isDebugMode) { debugInfo.push(`目标泳道: ${requestTargetLaneId} (默认)`); } } // 如果有目标泳道ID(可能是请求中指定的,也可能是默认的 baseline),且与当前泳道不同,则尝试进行代理 if (requestTargetLaneId && requestTargetLaneId !== config.currentLaneId) { try { // 获取目标泳道的服务实例(按心跳时间排序) const targetInstances = await getNacosLaneInstances(config.serviceName, requestTargetLaneId, true); if (isDebugMode) { debugInfo.push(`找到实例数量: ${targetInstances.length}`); } if (targetInstances.length > 0) { // 使用负载均衡策略选择一个实例 const targetInstance = selectInstance( targetInstances, config.serviceName, LoadBalanceStrategy.ROUND_ROBIN ); if (isDebugMode) { debugInfo.push(`选择实例: ${targetInstance.ip}:${targetInstance.port}`); debugInfo.push(`负载均衡策略: ROUND_ROBIN`); // 添加调试响应头 if (!event.node.res.headersSent) { try { event.node.res.setHeader(HEADER_LANE_DETAIL, getSafeHeaderValue(debugInfo)); } catch (error) { logger.error(`❌ 设置调试响应头时出错: ${error instanceof Error ? error.message : String(error)}`); } } } // 转发请求到目标实例(使用故障转移) await proxyRequestWithFailover(event, targetInstance, isDebugMode, targetInstances); return; } else { // 如果是尝试路由到 baseline 但没找到实例,则在当前实例处理 if (requestTargetLaneId === 'baseline' && !event.node.req.headers[headerKey]) { logger.info(`🔄 未找到 baseline 泳道实例,在当前泳道 ${config.currentLaneId} 处理请求`); if (isDebugMode) { debugInfo.push(`状态: 未找到 baseline 泳道实例,在当前泳道处理`); // 如果启用了调试模式,添加调试响应头 if (!event.node.res.headersSent) { try { event.node.res.setHeader(HEADER_LANE_DETAIL, getSafeHeaderValue(debugInfo)); } catch (error) { logger.error(`❌ 设置调试响应头时出错: ${error instanceof Error ? error.message : String(error)}`); } } } // 清除处理中标记,但不标记为已处理,因为请求将由后续中间件处理 event.context._laneManagerProcessing = false; return; } // 对于明确指定了泳道ID的请求,如果找不到实例,尝试查找 baseline 泳道 logger.warn(`⚠️ 未找到泳道 ${requestTargetLaneId} 的健康实例,尝试查找 baseline 泳道`); if (isDebugMode) { debugInfo.push(`警告: 未找到泳道 ${requestTargetLaneId} 的健康实例,尝试查找 baseline 泳道`); } // 如果当前实例已经是 baseline 泳道,直接在本地处理,避免无限循环 if (config.currentLaneId === 'baseline') { console.log(`[multi-lane-manager] 当前实例已是 baseline 泳道,直接本地处理请求,避免无限转发`); logger.info(`🔄 当前实例已是 baseline 泳道,直接本地处理请求,避免无限转发`); if (isDebugMode) { debugInfo.push(`状态: 当前实例已是 baseline 泳道,直接本地处理请求,避免无限转发`); // 如果启用了调试模式,添加调试响应头 if (!event.node.res.headersSent) { try { event.node.res.setHeader(HEADER_LANE_DETAIL, getSafeHeaderValue(debugInfo)); } catch (error) { logger.error(`❌ 设置调试响应头时出错: ${error instanceof Error ? error.message : String(error)}`); } } } // 清除处理中标记,但不标记为已处理,因为请求将由后续中间件处理 event.context._laneManagerProcessing = false; return; } // 如果当前请求的泳道不是 baseline,则尝试查找 baseline 泳道 if (requestTargetLaneId !== 'baseline') { console.log(`[multi-lane-manager] 尝试查找 baseline 泳道实例`); // 获取 baseline 泳道的服务实例(按心跳时间排序) const baselineInstances = await getNacosLaneInstances(config.serviceName, 'baseline', true); if (baselineInstances.length > 0) { console.log(`[multi-lane-manager] 找到 baseline 泳道实例,将请求转发到 baseline 泳道`); logger.info(`✅ 找到 baseline 泳道实例,将请求转发到 baseline 泳道`); if (isDebugMode) { debugInfo.push(`状态: 找到 baseline 泳道实例,将请求转发到 baseline 泳道`); } // 使用负载均衡策略选择一个实例 const baselineInstance = selectInstance( baselineInstances, config.serviceName, LoadBalanceStrategy.ROUND_ROBIN ); if (isDebugMode) { debugInfo.push(`选择实例: ${baselineInstance.ip}:${baselineInstance.port}`); debugInfo.push(`负载均衡策略: ROUND_ROBIN`); // 添加调试响应头 if (!event.node.res.headersSent) { try { event.node.res.setHeader(HEADER_LANE_DETAIL, getSafeHeaderValue(debugInfo)); } catch (error) { logger.error(`❌ 设置调试响应头时出错: ${error instanceof Error ? error.message : String(error)}`); } } } // 转发请求到 baseline 实例(使用故障转移) await proxyRequestWithFailover(event, baselineInstance, isDebugMode, baselineInstances); return; } else { console.log(`[multi-lane-manager] 未找到 baseline 泳道实例,在当前泳道处理请求`); logger.info(`🔄 未找到 baseline 泳道实例,在当前泳道 ${config.currentLaneId} 处理请求`); if (isDebugMode) { debugInfo.push(`状态: 未找到 baseline 泳道实例,在当前泳道处理请求`); } // 清除处理中标记,但不标记为已处理,因为请求将由后续中间件处理 event.context._laneManagerProcessing = false; // 如果启用了调试模式,添加调试响应头 if (isDebugMode && !event.node.res.headersSent) { try { event.node.res.setHeader(HEADER_LANE_DETAIL, getSafeHeaderValue(debugInfo)); } catch (error) { logger.error(`❌ 设置调试响应头时出错: ${error instanceof Error ? error.message : String(error)}`); } } return; } } else { // 如果当前请求的泳道已经是 baseline,但仍然没有找到实例,则在当前泳道处理 console.log(`[multi-lane-manager] 已经是 baseline 泳道但未找到实例,在当前泳道处理请求`); logger.info(`🔄 已经是 baseline 泳道但未找到实例,在当前泳道 ${config.currentLaneId} 处理请求`); if (isDebugMode) { debugInfo.push(`状态: 已经是 baseline 泳道但未找到实例,在当前泳道处理请求`); } // 清除处理中标记,但不标记为已处理,因为请求将由后续中间件处理 event.context._laneManagerProcessing = false; // 如果启用了调试模式,添加调试响应头 if (isDebugMode && !event.node.res.headersSent) { try { event.node.res.setHeader(HEADER_LANE_DETAIL, getSafeHeaderValue(debugInfo)); } catch (error) { logger.error(`❌ 设置调试响应头时出错: ${error instanceof Error ? error.message : String(error)}`); } } return; } } } catch (e) { logger.error(`❌ 处理跨泳道请求时发生错误: ${e instanceof Error ? e.message : String(e)}`); if (isDebugMode) { debugInfo.push(`错误: ${e instanceof Error ? e.message : String(e)}`); } if (!event.node.res.headersSent) { // 如果启用了调试模式,添加调试响应头 if (isDebugMode) { try { event.node.res.setHeader(HEADER_LANE_DETAIL, getSafeHeaderValue(debugInfo)); } catch (error) { logger.error(`❌ 设置调试响应头时出错: ${error instanceof Error ? error.message : String(error)}`); } } event.node.res.statusCode = 500; // Internal Server Error event.node.res.setHeader("Content-Type", "text/plain"); event.node.res.end("处理泳道请求时发生内部服务器错误。"); } // 标记请求已处理 event.context._laneManagerHandled = true; return; } } logger.debug(`✅ 请求在当前泳道 (${config.currentLaneId}) 处理或无目标泳道指定,将由后续处理程序处理。`); if (isDebugMode) { debugInfo.push(`处理: 在当前泳道处理请求`); // 如果启用了调试模式,添加调试响应头 if (!event.node.res.headersSent) { try { event.node.res.setHeader(HEADER_LANE_DETAIL, getSafeHeaderValue(debugInfo)); } catch (error) { logger.error(`❌ 设置调试响应头时出错: ${error instanceof Error ? error.message : String(error)}`); } } } // 清除处理中标记,但不标记为已处理,因为请求将由后续中间件处理 event.context._laneManagerProcessing = false; }; } // 导出 Nacos 相关函数,方便其他模块使用 export { getNacosLaneInstances };