UNPKG

multi-lane-manager

Version:

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

366 lines (364 loc) 17.5 kB
import { getConfig } from "./chunk-WTX544FP.mjs"; import { logger } from "./chunk-XVCPZ7FX.mjs"; import { DEFAULT_LANE_TARGET_HEADER, HEADER_LANE_DETAIL, HEADER_ORIGINAL_LANE, HEADER_PROXIED_BY, HEADER_PROXIED_BY_VALUE, getSafeHeaderValue } from "./chunk-YVTE7PPA.mjs"; // src/utils/proxy.ts import axios, { AxiosError } from "axios"; import { TLSSocket } from "tls"; var LoadBalanceStrategy = /* @__PURE__ */ ((LoadBalanceStrategy2) => { LoadBalanceStrategy2["RANDOM"] = "random"; LoadBalanceStrategy2["ROUND_ROBIN"] = "round-robin"; LoadBalanceStrategy2["LEAST_CONNECTIONS"] = "least-connections"; return LoadBalanceStrategy2; })(LoadBalanceStrategy || {}); var serviceIndices = {}; var instanceConnections = {}; var failedInstances = {}; var FAILURE_CONFIG = { MAX_RETRY_ATTEMPTS: 2, // 最大重试次数 FAILURE_THRESHOLD: 3, // 故障阈值(连续失败次数) BLACKLIST_DURATION: 3e4, // 黑名单持续时间(30秒) HEALTH_CHECK_INTERVAL: 6e4, // 健康检查间隔(60秒) CONNECTION_TIMEOUT: 5e3, // 连接超时时间(5秒) RETRY_DELAY: 1e3 // 重试延迟(1秒) }; function selectInstance(instances, serviceName, strategy = "round-robin" /* ROUND_ROBIN */) { if (instances.length === 0) { throw new Error("\u{1F6AB} \u6CA1\u6709\u53EF\u7528\u7684\u670D\u52A1\u5B9E\u4F8B"); } if (instances.length === 1) { return instances[0]; } switch (strategy) { case "random" /* RANDOM */: return instances[Math.floor(Math.random() * instances.length)]; case "round-robin" /* ROUND_ROBIN */: if (!serviceIndices[serviceName]) { serviceIndices[serviceName] = 0; } const index = serviceIndices[serviceName] % instances.length; serviceIndices[serviceName] = (serviceIndices[serviceName] + 1) % instances.length; return instances[index]; case "least-connections" /* LEAST_CONNECTIONS */: let minConnections = Number.MAX_SAFE_INTEGER; let selectedInstance = instances[0]; for (const instance of instances) { const instanceKey = `${instance.serviceName}:${instance.ip}:${instance.port}`; const connections = instanceConnections[instanceKey] || 0; if (connections < minConnections) { minConnections = connections; selectedInstance = instance; } } return selectedInstance; default: return instances[0]; } } function updateInstanceConnections(instance, increment) { const instanceKey = `${instance.serviceName}:${instance.ip}:${instance.port}`; if (!instanceConnections[instanceKey]) { instanceConnections[instanceKey] = 0; } if (increment) { instanceConnections[instanceKey]++; } else { instanceConnections[instanceKey] = Math.max(0, instanceConnections[instanceKey] - 1); } } function isInstanceBlacklisted(instance) { const instanceKey = `${instance.serviceName}:${instance.ip}:${instance.port}`; const failureInfo = failedInstances[instanceKey]; if (!failureInfo) { return false; } const now = Date.now(); return now < failureInfo.blacklistUntil; } function recordInstanceFailure(instance, error) { const instanceKey = `${instance.serviceName}:${instance.ip}:${instance.port}`; const now = Date.now(); if (!failedInstances[instanceKey]) { failedInstances[instanceKey] = { count: 0, lastFailTime: 0, blacklistUntil: 0 }; } const failureInfo = failedInstances[instanceKey]; failureInfo.count++; failureInfo.lastFailTime = now; if (failureInfo.count >= FAILURE_CONFIG.FAILURE_THRESHOLD) { failureInfo.blacklistUntil = now + FAILURE_CONFIG.BLACKLIST_DURATION; logger.warn(`\u26A0\uFE0F \u5B9E\u4F8B ${instanceKey} \u6545\u969C\u6B21\u6570\u8FBE\u5230\u9608\u503C ${FAILURE_CONFIG.FAILURE_THRESHOLD}\uFF0C\u52A0\u5165\u9ED1\u540D\u5355 ${FAILURE_CONFIG.BLACKLIST_DURATION / 1e3} \u79D2`); logger.warn(`\u26A0\uFE0F \u6545\u969C\u539F\u56E0: ${error.message}`); } else { logger.warn(`\u26A0\uFE0F \u5B9E\u4F8B ${instanceKey} \u6545\u969C (${failureInfo.count}/${FAILURE_CONFIG.FAILURE_THRESHOLD}): ${error.message}`); } } function resetInstanceFailure(instance) { const instanceKey = `${instance.serviceName}:${instance.ip}:${instance.port}`; if (failedInstances[instanceKey]) { const wasBlacklisted = isInstanceBlacklisted(instance); delete failedInstances[instanceKey]; if (wasBlacklisted) { logger.info(`\u2705 \u5B9E\u4F8B ${instanceKey} \u5DF2\u6062\u590D\uFF0C\u4ECE\u9ED1\u540D\u5355\u4E2D\u79FB\u9664`); } } } function shouldFailover(error) { if (error instanceof AxiosError) { if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND" || error.code === "ETIMEDOUT") { return true; } if (error.response) { const status = error.response.status; return status >= 502 && status <= 504; } if (error.code === "ECONNABORTED") { return true; } } return false; } function filterHealthyInstances(instances) { return instances.filter((instance) => !isInstanceBlacklisted(instance)); } async function proxyRequestWithFailover(event, targetInstance, isDebugMode = false, allInstances = []) { let lastError = null; const instancesToTry = [targetInstance]; if (allInstances.length > 1) { const healthyBackups = filterHealthyInstances( allInstances.filter( (inst) => inst.ip !== targetInstance.ip || inst.port !== targetInstance.port ) ); instancesToTry.push(...healthyBackups.slice(0, FAILURE_CONFIG.MAX_RETRY_ATTEMPTS)); } logger.info(`\u{1F504} \u5F00\u59CB\u4EE3\u7406\u8BF7\u6C42\uFF0C\u53EF\u5C1D\u8BD5\u5B9E\u4F8B\u6570: ${instancesToTry.length}`); for (let attempt = 0; attempt < instancesToTry.length; attempt++) { const currentInstance = instancesToTry[attempt]; const isRetry = attempt > 0; if (isInstanceBlacklisted(currentInstance)) { logger.warn(`\u26A0\uFE0F \u8DF3\u8FC7\u9ED1\u540D\u5355\u5B9E\u4F8B: ${currentInstance.ip}:${currentInstance.port}`); continue; } try { if (isRetry) { logger.info(`\u{1F504} \u6545\u969C\u8F6C\u79FB\u5230\u5B9E\u4F8B: ${currentInstance.ip}:${currentInstance.port} (\u5C1D\u8BD5 ${attempt + 1}/${instancesToTry.length})`); await new Promise((resolve) => setTimeout(resolve, FAILURE_CONFIG.RETRY_DELAY)); } const success = await proxyRequestToInstance(event, currentInstance, isDebugMode, isRetry); if (success) { resetInstanceFailure(currentInstance); if (isRetry) { logger.info(`\u2705 \u6545\u969C\u8F6C\u79FB\u6210\u529F\uFF0C\u4F7F\u7528\u5B9E\u4F8B: ${currentInstance.ip}:${currentInstance.port}`); } return true; } } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); recordInstanceFailure(currentInstance, lastError); if (!shouldFailover(lastError)) { logger.warn(`\u26A0\uFE0F \u9519\u8BEF\u7C7B\u578B\u4E0D\u9002\u5408\u6545\u969C\u8F6C\u79FB: ${lastError.message}`); break; } logger.warn(`\u26A0\uFE0F \u5B9E\u4F8B ${currentInstance.ip}:${currentInstance.port} \u8BF7\u6C42\u5931\u8D25: ${lastError.message}`); if (attempt === instancesToTry.length - 1) { logger.error(`\u274C \u6240\u6709\u5B9E\u4F8B\u90FD\u5DF2\u5C1D\u8BD5\uFF0C\u8BF7\u6C42\u6700\u7EC8\u5931\u8D25`); break; } } } return handleFinalFailure(event, lastError, isDebugMode); } async function handleFinalFailure(event, error, isDebugMode) { if (event.node.res.headersSent) { logger.warn(`\u26A0\uFE0F \u54CD\u5E94\u5934\u5DF2\u53D1\u9001\uFF0C\u65E0\u6CD5\u8FD4\u56DE\u9519\u8BEF\u54CD\u5E94`); event.context._laneManagerHandled = true; return false; } try { if (isDebugMode) { const debugInfo = []; debugInfo.push(`\u6545\u969C\u8F6C\u79FB\u65F6\u95F4: ${(/* @__PURE__ */ new Date()).toISOString()}`); debugInfo.push(`\u6700\u7EC8\u9519\u8BEF: ${error?.message || "\u6240\u6709\u5B9E\u4F8B\u90FD\u4E0D\u53EF\u7528"}`); debugInfo.push(`\u72B6\u6001: \u6240\u6709\u5B9E\u4F8B\u6545\u969C\u8F6C\u79FB\u5931\u8D25`); try { event.node.res.setHeader(HEADER_LANE_DETAIL, getSafeHeaderValue(debugInfo)); } catch (headerError) { logger.error(`\u274C \u8BBE\u7F6E\u8C03\u8BD5\u54CD\u5E94\u5934\u65F6\u51FA\u9519: ${headerError instanceof Error ? headerError.message : String(headerError)}`); } } event.node.res.statusCode = 503; event.node.res.setHeader("Content-Type", "text/plain"); const errorMessage = `\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\uFF0C\u6240\u6709\u5B9E\u4F8B\u90FD\u65E0\u6CD5\u8BBF\u95EE\u3002\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002`; event.node.res.end(errorMessage); logger.error(`\u274C \u53D1\u9001\u670D\u52A1\u4E0D\u53EF\u7528\u54CD\u5E94`); } catch (responseError) { logger.error(`\u274C \u53D1\u9001\u9519\u8BEF\u54CD\u5E94\u65F6\u51FA\u9519: ${responseError instanceof Error ? responseError.message : String(responseError)}`); } event.context._laneManagerHandled = true; return false; } async function proxyRequestToInstance(event, targetInstance, isDebugMode = false, isRetry = false) { const config = getConfig(); const requestPath = event.node.req.url || ""; const targetUrl = `http://${targetInstance.ip}:${targetInstance.port}${requestPath}`; logger.info(`\u{1F504} \u4EE3\u7406\u8BF7\u6C42\u5230\u76EE\u6807\u5B9E\u4F8B: ${targetInstance.serviceName}@${targetInstance.ip}:${targetInstance.port} (\u6CF3\u9053: ${targetInstance.metadata.laneId}), URL: ${targetUrl}`); updateInstanceConnections(targetInstance, true); try { const newHeaders = {}; for (const key in event.node.req.headers) { if (key !== "host" && key !== config.targetLaneHeaderKey) { newHeaders[key] = event.node.req.headers[key]; } } const remoteAddress = event.node.req.socket?.remoteAddress; newHeaders["X-Forwarded-For"] = remoteAddress || event.node.req.headers["x-forwarded-for"] || ""; newHeaders["X-Forwarded-Host"] = event.node.req.headers["host"] || ""; const proto = event.node.req.headers["x-forwarded-proto"] || (event.node.req.socket instanceof TLSSocket && event.node.req.socket.encrypted ? "https" : "http"); newHeaders["X-Forwarded-Proto"] = proto; const proxyRequestConfig = { method: event.node.req.method, url: targetUrl, headers: newHeaders, responseType: "stream", validateStatus: () => true, // 接受任何状态码 timeout: config.proxyTimeout }; const bodyMethods = ["POST", "PUT", "PATCH", "DELETE"]; if (event.node.req.method && bodyMethods.includes(event.node.req.method.toUpperCase())) { proxyRequestConfig.data = event.node.req; } logger.debug("\u{1F4CB} \u4EE3\u7406\u8BF7\u6C42\u914D\u7F6E:", { ...proxyRequestConfig, data: proxyRequestConfig.data ? "<REQUEST_STREAM>" : void 0 }); const proxyRes = await axios.request(proxyRequestConfig); logger.info(`\u{1F4E5} \u4EE3\u7406\u54CD\u5E94\u72B6\u6001: ${proxyRes.status}`); await new Promise((resolve, reject) => { try { event.node.res.statusCode = proxyRes.status; const headersToSet = /* @__PURE__ */ new Map(); Object.entries(proxyRes.headers).forEach(([key, value]) => { if (value !== void 0 && value !== null) { headersToSet.set(key, value); } }); headersToSet.set(HEADER_PROXIED_BY, HEADER_PROXIED_BY_VALUE); headersToSet.set(HEADER_ORIGINAL_LANE, config.currentLaneId); headersToSet.set(DEFAULT_LANE_TARGET_HEADER, targetInstance.metadata.laneId); if (isDebugMode) { const debugInfo = []; debugInfo.push(`\u4EE3\u7406\u65F6\u95F4: ${(/* @__PURE__ */ new Date()).toISOString()}`); debugInfo.push(`\u4EE3\u7406\u76EE\u6807: ${targetInstance.ip}:${targetInstance.port}`); debugInfo.push(`\u76EE\u6807\u6CF3\u9053: ${targetInstance.metadata.laneId}`); debugInfo.push(`\u54CD\u5E94\u72B6\u6001: ${proxyRes.status}`); debugInfo.push(`\u54CD\u5E94\u5927\u5C0F: ${proxyRes.headers["content-length"] || "\u672A\u77E5"}`); try { headersToSet.set(HEADER_LANE_DETAIL, getSafeHeaderValue(debugInfo)); } catch (error) { logger.error(`\u274C \u8BBE\u7F6E\u8C03\u8BD5\u54CD\u5E94\u5934\u65F6\u51FA\u9519: ${error instanceof Error ? error.message : String(error)}`); } } if (event.node.res.headersSent) { logger.warn(`\u26A0\uFE0F \u54CD\u5E94\u5934\u5DF2\u53D1\u9001\uFF0C\u65E0\u6CD5\u8BBE\u7F6E\u65B0\u7684\u54CD\u5E94\u5934`); console.log(`[multi-lane-manager] \u26A0\uFE0F \u54CD\u5E94\u5934\u5DF2\u53D1\u9001\uFF0C\u65E0\u6CD5\u8BBE\u7F6E\u65B0\u7684\u54CD\u5E94\u5934`); } for (const [key, value] of headersToSet.entries()) { try { if (!event.node.res.headersSent) { event.node.res.setHeader(key, value); logger.debug(`\u2705 \u6210\u529F\u8BBE\u7F6E\u54CD\u5E94\u5934: ${key}=${value}`); } else { logger.warn(`\u26A0\uFE0F \u65E0\u6CD5\u8BBE\u7F6E\u54CD\u5E94\u5934 ${key}\uFF0C\u54CD\u5E94\u5934\u5DF2\u53D1\u9001`); console.log(`[multi-lane-manager] \u26A0\uFE0F \u65E0\u6CD5\u8BBE\u7F6E\u54CD\u5E94\u5934 ${key}\uFF0C\u54CD\u5E94\u5934\u5DF2\u53D1\u9001`); } } catch (headerError) { logger.warn(`\u26A0\uFE0F \u8BBE\u7F6E\u54CD\u5E94\u5934 ${key} \u5931\u8D25: ${headerError instanceof Error ? headerError.message : String(headerError)}`); console.log(`[multi-lane-manager] \u26A0\uFE0F \u8BBE\u7F6E\u54CD\u5E94\u5934 ${key} \u5931\u8D25: ${headerError instanceof Error ? headerError.message : String(headerError)}`); } } proxyRes.data.on("error", (err) => { logger.error(`\u274C \u4EE3\u7406\u54CD\u5E94\u6570\u636E\u6D41\u9519\u8BEF: ${err.message}`); updateInstanceConnections(targetInstance, false); reject(err); }); proxyRes.data.on("end", () => { logger.debug("\u2705 \u4EE3\u7406\u54CD\u5E94\u6570\u636E\u6D41\u7ED3\u675F"); updateInstanceConnections(targetInstance, false); resolve(); }); event.context._laneManagerHandled = true; proxyRes.data.pipe(event.node.res); } catch (err) { updateInstanceConnections(targetInstance, false); reject(err); } }); return true; } catch (error) { updateInstanceConnections(targetInstance, false); logger.error(`\u274C \u4EE3\u7406\u8BF7\u6C42\u5230 ${targetUrl} \u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`); if (event.node.res.headersSent) { logger.warn(`\u26A0\uFE0F \u54CD\u5E94\u5934\u5DF2\u53D1\u9001\uFF0C\u65E0\u6CD5\u8FD4\u56DE\u9519\u8BEF\u54CD\u5E94`); console.log(`[multi-lane-manager] \u26A0\uFE0F \u54CD\u5E94\u5934\u5DF2\u53D1\u9001\uFF0C\u65E0\u6CD5\u8FD4\u56DE\u9519\u8BEF\u54CD\u5E94`); event.context._laneManagerHandled = true; return false; } try { if (isDebugMode) { const debugInfo = []; debugInfo.push(`\u4EE3\u7406\u65F6\u95F4: ${(/* @__PURE__ */ new Date()).toISOString()}`); debugInfo.push(`\u4EE3\u7406\u76EE\u6807: ${targetInstance.ip}:${targetInstance.port}`); debugInfo.push(`\u76EE\u6807\u6CF3\u9053: ${targetInstance.metadata.laneId}`); debugInfo.push(`\u9519\u8BEF: ${error instanceof Error ? error.message : "\u672A\u77E5\u4EE3\u7406\u9519\u8BEF"}`); try { event.node.res.setHeader(HEADER_LANE_DETAIL, getSafeHeaderValue(debugInfo)); logger.debug(`\u2705 \u6210\u529F\u8BBE\u7F6E\u9519\u8BEF\u54CD\u5E94\u5934: ${HEADER_LANE_DETAIL}`); } catch (headerError) { logger.error(`\u274C \u8BBE\u7F6E\u8C03\u8BD5\u54CD\u5E94\u5934\u65F6\u51FA\u9519: ${headerError instanceof Error ? headerError.message : String(headerError)}`); console.log(`[multi-lane-manager] \u274C \u8BBE\u7F6E\u8C03\u8BD5\u54CD\u5E94\u5934\u65F6\u51FA\u9519: ${headerError instanceof Error ? headerError.message : String(headerError)}`); } } event.node.res.statusCode = 502; event.node.res.setHeader("Content-Type", "text/plain"); const errorMessage = `\u4EE3\u7406\u8BF7\u6C42\u5230\u6CF3\u9053 ${targetInstance.metadata.laneId} \u65F6\u51FA\u9519\u3002\u539F\u56E0: ${error instanceof Error ? error.message : "\u672A\u77E5\u4EE3\u7406\u9519\u8BEF"}`; event.node.res.end(errorMessage); logger.debug(`\u2705 \u6210\u529F\u53D1\u9001\u9519\u8BEF\u54CD\u5E94`); } catch (responseError) { logger.error(`\u274C \u53D1\u9001\u9519\u8BEF\u54CD\u5E94\u65F6\u51FA\u9519: ${responseError instanceof Error ? responseError.message : String(responseError)}`); console.log(`[multi-lane-manager] \u274C \u53D1\u9001\u9519\u8BEF\u54CD\u5E94\u65F6\u51FA\u9519: ${responseError instanceof Error ? responseError.message : String(responseError)}`); } event.context._laneManagerHandled = true; return false; } } async function proxyRequest(event, targetInstance, isDebugMode = false) { return proxyRequestToInstance(event, targetInstance, isDebugMode, false); } export { LoadBalanceStrategy, selectInstance, proxyRequestWithFailover, proxyRequest }; //# sourceMappingURL=chunk-TPRQBNNR.mjs.map