multi-lane-manager
Version:
Nacos 泳道管理与请求路由组件
377 lines (375 loc) • 16.4 kB
JavaScript
import {
getConfig,
getGlobalState,
updateConfigPort
} from "./chunk-WTX544FP.mjs";
import {
logger
} from "./chunk-XVCPZ7FX.mjs";
// src/utils/nacos.ts
import axios from "axios";
var nacosInstanceCache = /* @__PURE__ */ new Map();
function getCacheKey(serviceName, targetLaneId) {
return `${serviceName}:${targetLaneId}`;
}
function isCacheValid(cacheItem, ttl) {
return Date.now() - cacheItem.timestamp < ttl;
}
function cleanExpiredCache(ttl) {
const now = Date.now();
for (const [key, item] of nacosInstanceCache.entries()) {
if (now - item.timestamp >= ttl) {
nacosInstanceCache.delete(key);
logger.debug(`\u{1F5D1}\uFE0F \u6E05\u7406\u8FC7\u671F\u7F13\u5B58: ${key}`);
}
}
}
async function registerServiceInstance(port) {
const config = getConfig();
if (!config.isLaneEnabled) {
logger.debug("\u{1F6AB} \u6CF3\u9053\u529F\u80FD\u672A\u542F\u7528\uFF0C\u8DF3\u8FC7\u670D\u52A1\u6CE8\u518C");
return true;
}
try {
logger.debug(`\u{1F680} \u5F00\u59CB\u670D\u52A1\u6CE8\u518C\u6D41\u7A0B\uFF0C\u65F6\u95F4\u6233: ${(/* @__PURE__ */ new Date()).toISOString()}`);
updateConfigPort(port);
const registrationUrl = `${config.nacosUrl}/nacos/v1/ns/instance`;
const params = new URLSearchParams();
params.append("serviceName", config.serviceName);
params.append("ip", config.host);
params.append("port", port.toString());
params.append("weight", "1.0");
params.append("healthy", "true");
params.append("metadata", JSON.stringify(config.metadata));
params.append("clusterName", config.currentLaneId || "DEFAULT");
params.append("ephemeral", "true");
params.append("ipDeleteTimeout", config.instanceTtl.toString());
params.append("instanceHeartBeatInterval", config.heartbeatInterval.toString());
params.append("instanceHeartBeatTimeOut", (config.heartbeatInterval * 2).toString());
logger.debug(`\u{1F4E4} \u53D1\u9001\u6CE8\u518C\u8BF7\u6C42\u5230 Nacos: ${registrationUrl}`);
const response = await axios.post(registrationUrl, params, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Nuxt3-LaneManager/1.0"
},
timeout: config.registrationTimeout
});
logger.debug(`\u{1F4E5} \u6CE8\u518C\u54CD\u5E94\u72B6\u6001\u7801: ${response.status}, \u6570\u636E: ${JSON.stringify(response.data)}`);
if (response.status === 200 && response.data === "ok") {
logger.success(`\u2705 \u670D\u52A1\u6CE8\u518C\u6210\u529F: ${config.serviceName}@${config.host}:${port} (\u6CF3\u9053: ${config.currentLaneId})`);
const globalState = getGlobalState();
globalState._laneMgrRegistered = true;
globalState._laneMgrPort = port;
startHeartbeat(port);
return true;
} else {
logger.error(`\u274C \u670D\u52A1\u6CE8\u518C\u5931\u8D25: ${response.data}`);
return false;
}
} catch (error) {
logger.error(`\u274C \u670D\u52A1\u6CE8\u518C\u9519\u8BEF: ${error instanceof Error ? error.message : String(error)}`);
if (axios.isAxiosError(error) && error.response) {
logger.error(`\u{1F534} HTTP\u72B6\u6001\u7801: ${error.response.status}, \u54CD\u5E94: ${JSON.stringify(error.response.data)}`);
}
return false;
}
}
async function deregisterServiceInstance(port) {
const config = getConfig();
if (!config.isLaneEnabled) {
logger.debug("\u{1F6AB} \u6CF3\u9053\u529F\u80FD\u672A\u542F\u7528\uFF0C\u8DF3\u8FC7\u670D\u52A1\u6CE8\u9500");
return true;
}
try {
stopHeartbeat();
const deregistrationUrl = `${config.nacosUrl}/nacos/v1/ns/instance`;
const params = new URLSearchParams();
params.append("serviceName", config.serviceName);
params.append("ip", config.host);
params.append("port", port.toString());
params.append("ephemeral", "true");
logger.debug(`\u{1F4E4} \u53D1\u9001\u6CE8\u9500\u8BF7\u6C42\u5230 Nacos: ${deregistrationUrl}`);
const response = await axios.delete(`${deregistrationUrl}?${params.toString()}`, {
timeout: config.registrationTimeout
});
logger.debug(`\u{1F4E5} \u6CE8\u9500\u54CD\u5E94\u72B6\u6001\u7801: ${response.status}, \u6570\u636E: ${JSON.stringify(response.data)}`);
if (response.status === 200 && response.data === "ok") {
logger.success(`\u2705 \u670D\u52A1\u6CE8\u9500\u6210\u529F: ${config.serviceName}@${config.host}:${port}`);
const globalState = getGlobalState();
globalState._laneMgrRegistered = false;
globalState._laneMgrPort = void 0;
return true;
} else {
logger.error(`\u274C \u670D\u52A1\u6CE8\u9500\u5931\u8D25: ${response.data}`);
return false;
}
} catch (error) {
logger.error(`\u274C \u670D\u52A1\u6CE8\u9500\u9519\u8BEF: ${error instanceof Error ? error.message : String(error)}`);
return false;
}
}
async function sendHeartbeat(port, logDetails = false) {
const config = getConfig();
if (!config.isLaneEnabled) {
if (logDetails) {
logger.debug("\u{1F6AB} \u6CF3\u9053\u529F\u80FD\u672A\u542F\u7528\uFF0C\u8DF3\u8FC7\u5FC3\u8DF3");
}
return true;
}
try {
const heartbeatUrl = `${config.nacosUrl}/nacos/v1/ns/instance/beat`;
const params = new URLSearchParams();
params.append("serviceName", config.serviceName);
params.append("ip", config.host);
params.append("port", port.toString());
const now = /* @__PURE__ */ new Date();
const beat = {
serviceName: config.serviceName,
ip: config.host,
port,
weight: 1,
metadata: {
...config.metadata,
lastHeartbeat: now.toISOString(),
// 添加心跳时间戳
heartbeatTimestamp: now.getTime().toString()
// 添加时间戳(毫秒)
},
cluster: config.currentLaneId,
scheduled: false
};
params.append("beat", JSON.stringify(beat));
if (logDetails) {
logger.debug(`\u{1F493} \u53D1\u9001\u5FC3\u8DF3\u5230 Nacos: ${heartbeatUrl}`);
}
const response = await axios.put(`${heartbeatUrl}?${params.toString()}`, null, {
headers: {
"User-Agent": "Nuxt3-LaneManager/1.0"
},
timeout: config.registrationTimeout / 2
// 心跳超时时间应该比注册短
});
if (logDetails) {
logger.debug(`\u{1F4E5} \u5FC3\u8DF3\u54CD\u5E94\u72B6\u6001\u7801: ${response.status}, \u6570\u636E: ${JSON.stringify(response.data)}`);
}
if (response.status === 200) {
if (logDetails) {
logger.debug(`\u2705 \u5FC3\u8DF3\u6210\u529F: ${config.serviceName}@${config.host}:${port}`);
}
return true;
} else {
logger.warn(`\u26A0\uFE0F \u5FC3\u8DF3\u5931\u8D25: ${response.data}`);
return false;
}
} catch (error) {
logger.error(`\u274C \u5FC3\u8DF3\u9519\u8BEF: ${error instanceof Error ? error.message : String(error)}`);
return false;
}
}
function startHeartbeat(port) {
const config = getConfig();
const globalState = getGlobalState();
stopHeartbeat();
globalState._laneMgrHeartbeatCount = 0;
logger.info(`\u23F1\uFE0F \u542F\u52A8\u5FC3\u8DF3\u5B9A\u65F6\u5668\uFF0C\u95F4\u9694: ${config.heartbeatInterval}\u6BEB\u79D2\uFF0C\u6BCF30\u6B21\u5FC3\u8DF3\u6253\u5370\u4E00\u6B21\u65E5\u5FD7`);
globalState._laneMgrHeartbeatTimer = setInterval(async () => {
try {
if (globalState._laneMgrHeartbeatCount !== void 0) {
globalState._laneMgrHeartbeatCount++;
} else {
globalState._laneMgrHeartbeatCount = 1;
}
const shouldLogDetails = globalState._laneMgrHeartbeatCount % 30 === 0;
await sendHeartbeat(port, shouldLogDetails);
if (shouldLogDetails) {
logger.info(`\u{1F493} \u5FC3\u8DF3\u8BA1\u6570: ${globalState._laneMgrHeartbeatCount}\uFF0C\u7EE7\u7EED\u4FDD\u6301\u8FDE\u63A5...`);
}
} catch (error) {
logger.error(`\u274C \u5FC3\u8DF3\u5B9A\u65F6\u5668\u9519\u8BEF: ${error instanceof Error ? error.message : String(error)}`);
}
}, config.heartbeatInterval);
}
function stopHeartbeat() {
const globalState = getGlobalState();
if (globalState._laneMgrHeartbeatTimer) {
logger.info("\u23F9\uFE0F \u505C\u6B62\u5FC3\u8DF3\u5B9A\u65F6\u5668");
clearInterval(globalState._laneMgrHeartbeatTimer);
globalState._laneMgrHeartbeatTimer = void 0;
globalState._laneMgrHeartbeatCount = 0;
}
}
async function fetchNacosLaneInstancesFromServer(serviceName, targetLaneId, sortByHeartbeat = true) {
const config = getConfig();
logger.debug(`\u{1F4E1} \u76F4\u63A5\u4ECE Nacos \u670D\u52A1\u5668\u67E5\u8BE2\u5B9E\u4F8B\uFF0C\u670D\u52A1\u540D: ${serviceName}, \u76EE\u6807\u6CF3\u9053: ${targetLaneId}`);
try {
const nacosListUrl = `${config.nacosUrl}/nacos/v1/ns/instance/list`;
const params = new URLSearchParams({
serviceName,
healthyOnly: "true",
// 只获取健康实例
groupName: config.nacosGroupName
});
if (config.nacosNamespace) {
params.append("namespaceId", config.nacosNamespace);
}
logger.debug(`\u{1F4E4} Nacos \u5B9E\u4F8B\u67E5\u8BE2 URL: ${nacosListUrl}?${params.toString()}`);
const response = await axios.get(`${nacosListUrl}?${params.toString()}`, {
timeout: config.registrationTimeout
});
logger.debug(`\u{1F4E5} Nacos \u5B9E\u4F8B\u67E5\u8BE2\u54CD\u5E94\u72B6\u6001: ${response.status}`);
if (response.data && response.data.hosts) {
const filteredInstances = response.data.hosts.filter(
(instance) => instance.healthy && instance.enabled && instance.metadata && instance.metadata.laneId === targetLaneId
).map(
(instance) => {
let lastHeartbeat = Date.now();
if (instance.metadata?.heartbeatTimestamp) {
try {
const timestamp = parseInt(instance.metadata.heartbeatTimestamp, 10);
if (!isNaN(timestamp) && timestamp > 0) {
lastHeartbeat = timestamp;
}
} catch (error) {
logger.debug(`\u26A0\uFE0F \u89E3\u6790\u5B9E\u4F8B ${instance.ip}:${instance.port} \u6570\u5B57\u65F6\u95F4\u6233\u5931\u8D25: ${error}`);
}
} else if (instance.metadata?.lastHeartbeat) {
try {
const heartbeatTime = new Date(instance.metadata.lastHeartbeat).getTime();
if (!isNaN(heartbeatTime)) {
lastHeartbeat = heartbeatTime;
}
} catch (error) {
logger.debug(`\u26A0\uFE0F \u89E3\u6790\u5B9E\u4F8B ${instance.ip}:${instance.port} ISO\u65F6\u95F4\u6233\u5931\u8D25: ${error}`);
}
}
if (instance.instanceHeartBeatInterval && !instance.metadata?.lastHeartbeat) {
lastHeartbeat = Date.now() - instance.instanceHeartBeatInterval / 2;
}
return {
ip: instance.ip,
port: instance.port,
serviceName: instance.serviceName,
clusterName: instance.clusterName,
ephemeral: instance.ephemeral,
metadata: instance.metadata,
status: "UP",
lastHeartbeat,
version: instance.metadata?.version || "unknown"
};
}
);
if (sortByHeartbeat) {
filteredInstances.sort((a, b) => {
const timeA = typeof a.lastHeartbeat === "number" ? a.lastHeartbeat : new Date(a.lastHeartbeat).getTime();
const timeB = typeof b.lastHeartbeat === "number" ? b.lastHeartbeat : new Date(b.lastHeartbeat).getTime();
return timeB - timeA;
});
logger.debug(`\u{1F504} \u5DF2\u6309\u5FC3\u8DF3\u65F6\u95F4\u6392\u5E8F ${filteredInstances.length} \u4E2A\u5B9E\u4F8B`);
if (filteredInstances.length > 0) {
logger.debug(`\u{1F4CA} \u5B9E\u4F8B\u5FC3\u8DF3\u65F6\u95F4\u6392\u5E8F\u7ED3\u679C:`);
filteredInstances.forEach((instance, index) => {
const heartbeatTime = typeof instance.lastHeartbeat === "number" ? new Date(instance.lastHeartbeat).toISOString() : instance.lastHeartbeat;
logger.debug(` ${index + 1}. ${instance.ip}:${instance.port} - \u5FC3\u8DF3\u65F6\u95F4: ${heartbeatTime}`);
});
}
}
logger.info(`\u2705 \u4ECE\u670D\u52A1\u5668\u67E5\u8BE2\u5230 ${filteredInstances.length} \u4E2A\u5C5E\u4E8E\u6CF3\u9053 ${targetLaneId} \u7684\u5065\u5EB7\u5B9E\u4F8B${sortByHeartbeat ? "\uFF08\u5DF2\u6309\u5FC3\u8DF3\u65F6\u95F4\u6392\u5E8F\uFF09" : ""}`);
return filteredInstances;
} else {
logger.warn(`\u26A0\uFE0F Nacos \u5B9E\u4F8B\u67E5\u8BE2\u54CD\u5E94\u683C\u5F0F\u4E0D\u6B63\u786E\u6216\u65E0\u5B9E\u4F8B\u5217\u8868:`, response.data);
return [];
}
} catch (error) {
logger.error(`\u274C Nacos \u5B9E\u4F8B\u67E5\u8BE2\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`);
if (axios.isAxiosError(error) && error.response) {
logger.error(`\u{1F534} HTTP\u72B6\u6001\u7801: ${error.response.status}, \u54CD\u5E94: ${JSON.stringify(error.response.data)}`);
}
return [];
}
}
async function getNacosLaneInstances(serviceName, targetLaneId, sortByHeartbeat = true) {
const config = getConfig();
const cacheKey = getCacheKey(serviceName, targetLaneId);
const cacheTtl = config.nacosCacheTtl;
console.log(`[multi-lane-manager] \u{1F50D} \u83B7\u53D6 Nacos \u5B9E\u4F8B\uFF0C\u670D\u52A1\u540D: ${serviceName}, \u76EE\u6807\u6CF3\u9053: ${targetLaneId}`);
logger.debug(`\u{1F50D} \u83B7\u53D6 Nacos \u5B9E\u4F8B\uFF0C\u670D\u52A1\u540D: ${serviceName}, \u76EE\u6807\u6CF3\u9053: ${targetLaneId}, \u7F13\u5B58TTL: ${cacheTtl}ms`);
cleanExpiredCache(cacheTtl);
const cachedItem = nacosInstanceCache.get(cacheKey);
if (cachedItem) {
if (cachedItem.promise) {
logger.debug(`\u23F3 \u53D1\u73B0\u6B63\u5728\u8FDB\u884C\u7684\u8BF7\u6C42\uFF0C\u7B49\u5F85\u5B8C\u6210: ${cacheKey}`);
try {
return await cachedItem.promise;
} catch (error) {
logger.warn(`\u26A0\uFE0F \u6B63\u5728\u8FDB\u884C\u7684\u8BF7\u6C42\u5931\u8D25\uFF0C\u79FB\u9664\u7F13\u5B58: ${cacheKey}, \u9519\u8BEF: ${error}`);
nacosInstanceCache.delete(cacheKey);
}
} else if (isCacheValid(cachedItem, cacheTtl)) {
logger.debug(`\u{1F4BE} \u4F7F\u7528\u7F13\u5B58\u6570\u636E: ${cacheKey}, \u7F13\u5B58\u65F6\u95F4: ${new Date(cachedItem.timestamp).toISOString()}`);
return cachedItem.data;
} else {
logger.debug(`\u23F0 \u7F13\u5B58\u5DF2\u8FC7\u671F\uFF0C\u79FB\u9664: ${cacheKey}`);
nacosInstanceCache.delete(cacheKey);
}
}
logger.debug(`\u{1F310} \u521B\u5EFA\u65B0\u7684 Nacos \u67E5\u8BE2\u8BF7\u6C42: ${cacheKey}`);
const requestPromise = fetchNacosLaneInstancesFromServer(serviceName, targetLaneId, sortByHeartbeat);
nacosInstanceCache.set(cacheKey, {
data: [],
// 临时空数据
timestamp: Date.now(),
promise: requestPromise
});
try {
const result = await requestPromise;
nacosInstanceCache.set(cacheKey, {
data: result,
timestamp: Date.now()
});
logger.debug(`\u2705 \u8BF7\u6C42\u5B8C\u6210\uFF0C\u5DF2\u7F13\u5B58\u7ED3\u679C: ${cacheKey}, \u5B9E\u4F8B\u6570\u91CF: ${result.length}`);
return result;
} catch (error) {
nacosInstanceCache.delete(cacheKey);
logger.error(`\u274C Nacos \u67E5\u8BE2\u8BF7\u6C42\u5931\u8D25\uFF0C\u79FB\u9664\u7F13\u5B58: ${cacheKey}, \u9519\u8BEF: ${error}`);
throw error;
}
}
function clearNacosInstanceCache() {
const cacheSize = nacosInstanceCache.size;
nacosInstanceCache.clear();
logger.info(`\u{1F5D1}\uFE0F \u5DF2\u6E05\u7406\u6240\u6709 Nacos \u5B9E\u4F8B\u7F13\u5B58\uFF0C\u6E05\u7406\u4E86 ${cacheSize} \u4E2A\u7F13\u5B58\u9879`);
}
function getNacosCacheStats() {
const config = getConfig();
const cacheTtl = config.nacosCacheTtl;
const now = Date.now();
let validItems = 0;
let expiredItems = 0;
let pendingRequests = 0;
for (const item of nacosInstanceCache.values()) {
if (item.promise) {
pendingRequests++;
} else if (now - item.timestamp < cacheTtl) {
validItems++;
} else {
expiredItems++;
}
}
return {
totalItems: nacosInstanceCache.size,
validItems,
expiredItems,
pendingRequests
};
}
export {
registerServiceInstance,
deregisterServiceInstance,
sendHeartbeat,
startHeartbeat,
stopHeartbeat,
getNacosLaneInstances,
clearNacosInstanceCache,
getNacosCacheStats
};
//# sourceMappingURL=chunk-OFHMEPQ6.mjs.map