multi-lane-manager
Version:
Nacos 泳道管理与请求路由组件
366 lines (364 loc) • 17.5 kB
JavaScript
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