UNPKG

@yuntools/ali-alb

Version:

阿里云 ALB 负载均衡模块封装,支持 ESM,CJS 导入,提供 TypeScript 类型定义

489 lines (482 loc) 17.5 kB
/** * @yuntools/ali-alb * 阿里云 ALB 负载均衡模块封装,支持 ESM,CJS 导入,提供 TypeScript 类型定义 * * @version 12.0.0 * @author waiting * @license MIT * @link https://github.com/waitingsong/yuntools#readme */ 'use strict'; var assert = require('node:assert/strict'); var Alb = require('@alicloud/alb20200616'); var openapiClient = require('@alicloud/openapi-client'); var aliEcs = require('@yuntools/ali-ecs'); var rxjs = require('rxjs'); /* eslint-disable @typescript-eslint/no-unsafe-assignment */ const _Client = typeof module === 'object' ? Alb : Alb.default; /** * 异步任务的执行状态 */ exports.JobStatus = void 0; (function (JobStatus) { /** 异步消息已入队,等待处理 */ JobStatus["Enqueued"] = "Enqueued"; /** 调用执行成功 */ JobStatus["Succeeded"] = "Succeeded"; /** 调用执行失败 */ JobStatus["Failed"] = "Failed"; /** 调用执行中 */ JobStatus["Running"] = "Running"; JobStatus["Processing"] = "Processing"; /** 调用执行终止 */ JobStatus["Stopped"] = "Stopped"; /** 执行停止中 */ JobStatus["Stopping"] = "Stopping"; /** 执行因函数被删除等原因处于无效状态(任务未被执行) */ JobStatus["Invalid"] = "Invalid"; /** 您为任务配置了最长排队等待的期限。该任务因为超期被丢弃(任务未被执行) */ JobStatus["Expired"] = "Expired"; /** 异步调用因执行错误重试中 */ JobStatus["Retrying"] = "Retrying"; })(exports.JobStatus || (exports.JobStatus = {})); exports.Action = void 0; (function (Action) { /** 查询指定异步任务信息 */ Action["GetAsyncJobResult"] = "GetAsyncJobResult"; /** ALB 服务组列表 */ Action["ListServerGroups"] = "ListServerGroups"; /** ALB 服务组所有服务器列表 */ Action["ListServerGroupServers"] = "ListServerGroupServers"; /** 更新 ALB 服务器组指定服务器的(权重)属性 */ Action["UpdateServerGroupServersAttribute"] = "UpdateServerGroupServersAttribute"; })(exports.Action || (exports.Action = {})); exports.ActionType = void 0; (function (ActionType) { ActionType["up"] = "up"; ActionType["down"] = "down"; })(exports.ActionType || (exports.ActionType = {})); /** 阿里云 ALB 负载均衡服务接口 */ class AlbClient { id; secret; endpoint; ecsClientInstance; /** * 是否输出日志 * @default false */ debug = false; /** * 是否输出渐进日志 * @default true */ showProgressLog = true; client; nextToken = ''; ecsClient; groupServersCache = new Map(); // serversCache = new Map<ServerGroupId, ServersCache>() cacheTime; cacheTTLSec = 10; constructor(id, secret, endpoint = 'alb.cn-hangzhou.aliyuncs.com', ecsClientInstance) { this.id = id; this.secret = secret; this.endpoint = endpoint; this.ecsClientInstance = ecsClientInstance; this.client = this.createClient(id, secret); if (ecsClientInstance) { this.ecsClient = ecsClientInstance; } else { this.ecsClient = new aliEcs.EcsClient(id, secret); } } /** * 获取指定服务组信息 */ async getGroup(serverGroupId) { const opts = { action: exports.Action.ListServerGroups, nextToken: this.nextToken, serverGroupIds: [serverGroupId], }; const req = new Alb.ListServerGroupsRequest(opts); this.debug && console.info({ req }); const resp = await this.client.listServerGroups(req); /* c8 ignore next 3 */ if (resp.body.nextToken) { this.nextToken = resp.body.nextToken; } const ret = resp.body.serverGroups?.[0]; this.debug && console.info({ resp, ret }); return ret; } /** * 获取指定服务组指定服务器信息 */ async getGroupServer(serverGroupId, serverId, withoutCache = false) { assert(serverId, 'serverId is required'); if (withoutCache === true) { this.cleanCache(true); } let ret; const servers = await this.listGroupServers(serverGroupId); if (servers) { ret = servers.find(server => server.serverId === serverId.trim()); } if (!ret && !withoutCache) { ret = await this.getGroupServer(serverGroupId, serverId, true); } return ret; } /** * 根据公网 IPs 获取指定服务组指定服务器信息 */ async getGroupServerByPublicIps(serverGroupId, ips) { const ret = new Map(); for await (const ip of ips) { const server = await this.getGroupServerByPublicIp(serverGroupId, ip); if (server) { ret.set(ip, server); } } return ret; } /** * 根据公网 IP 获取指定服务组指定服务器信息 */ async getGroupServerByPublicIp(serverGroupId, ip) { const ecsId = await this.ecsClient.getInstanceIdByIp(ip); if (!ecsId) { console.error(`getGroupServerByPublicIp: ECS 服务器 ${ip} 未找到`); return; } const id = ecsId.trim(); const servers = await this.listGroupServers(serverGroupId); if (servers) { return servers.find((server) => { const flag = server.serverId?.trim() === id && id.length > 0; return flag; }); } /* c8 ignore next 3 */ else { console.error(`getGroupServerByPublicIp: 服务器组 ${serverGroupId} 未找到`); } } /** * 列出指定服务器组的服务器列表信息 */ async listGroupServers(serverGroupId) { this.cleanCache(); const cache = this.groupServersCache.get(serverGroupId); if (cache) { return cache; } const opts = { Action: exports.Action.ListServerGroupServers, NextToken: this.nextToken, serverGroupId, maxResults: 100, }; const req = new Alb.ListServerGroupServersRequest(opts); this.debug && console.info({ req }); const resp = await this.client.listServerGroupServers(req); /* c8 ignore next 3 */ if (resp.body.nextToken) { this.nextToken = resp.body.nextToken; } const ret = resp.body.servers; this.debug && console.info({ resp, ret }); this.updateCache(serverGroupId, ret); return ret; } /** * 获取异步任务信息 */ async getJobInfo(jobId) { const opts = { Action: exports.Action.GetAsyncJobResult, NextToken: this.nextToken, jobIds: [jobId], maxResults: 10, }; const req = new Alb.ListAsynJobsRequest(opts); this.debug && console.info({ req }); const resp = await this.client.listAsynJobs(req); /* c8 ignore next 3 */ if (resp.body.nextToken) { this.nextToken = resp.body.nextToken; } const ret = resp.body.jobs ? resp.body.jobs[0] : void 0; this.debug && console.info({ resp, ret }); return ret; } /** * 渐进更新指定服务器的权重到指定值, * 直到异步任务状态为 Succeeded 或 Failed, * 并返回异步任务信息和服务器信息 */ async updateServerWeightByPublicIp(options) { let { ecsId } = options; if (!ecsId) { const { ip: publicIp } = options; assert(publicIp, 'publicIp is required'); ecsId = await this.ecsClient.getInstanceIdByIp(publicIp); } assert(ecsId, 'ecsId is required or publicIp is invalid'); const opts = { ...options, ecsId, }; this.debug && console.info({ ecsId }); const ret = await this.updateServerWeight(opts); return ret; } /** * 渐进更新指定服务器的权重到指定值, * 直到异步任务状态为 Succeeded 或 Failed, * 并返回异步任务信息和服务器信息 */ async updateServerWeight(options) { const { ecsId, serverGroupId } = options; assert(ecsId, 'ecsId is required'); if (typeof options.currentWeight === 'undefined') { const server = await this.getGroupServer(serverGroupId, ecsId); assert(server, `No server found by ecsId ${ecsId} and serverGroupId ${serverGroupId}`); assert(typeof server.weight === 'number', `No weight found by ecsId ${ecsId} and serverGroupId ${serverGroupId}`); options.currentWeight = server.weight; } const jobId = await this.loopServerUntilWeight(options); const jobInfo = jobId ? await this.getJobInfo(jobId) : void 0; this.cleanCache(true); const groupServer = await this.getGroupServer(serverGroupId, ecsId); const ret = { jobInfo, groupServer, }; return ret; } /** * 设置服务器组的权重属性 */ async setServersWeight(serverGroupId, serverIds, weight) { // if (weight < 0) { // throw new TypeError(`weight must be >= 0, but got ${weight}`) // } const value = +weight; assert(!Number.isNaN(value), `weight must be a number, but got ${weight}`); // if (value > 100) { // value = 100 // } // else { // value = Math.round(value) // } const servers = []; const rows = await this.listGroupServers(serverGroupId); rows?.forEach((row) => { if (row.serverId && serverIds.includes(row.serverId)) { row.weight = value; servers.push(row); } }); this.debug && console.info({ servers }); assert(servers.length !== 0, `no server found in group ${serverGroupId}`); const opts = { Action: exports.Action.UpdateServerGroupServersAttribute, NextToken: this.nextToken, ServerGroupId: serverGroupId, servers, }; const req = new Alb.UpdateServerGroupServersAttributeRequest(opts); req.serverGroupId = serverGroupId; this.debug && console.info({ req }); const resp = await this.client.updateServerGroupServersAttribute(req); const ret = resp.body; this.debug && console.info({ resp, ret }); return ret; } /** * 查询指定任务的状态是否匹配输入的状态值 */ async isJobMatchStatusList(jobId, statusArray) { const jobInfo = await this.getJobInfo(jobId); this.debug && console.info({ job: jobInfo }); if (jobInfo?.status) { for (const status of statusArray) { if (jobInfo.status === status) { return status; } } /* c8 ignore next */ } /* c8 ignore next 2 */ return false; } cleanCache(force = false) { const now = Date.now(); if (force) { this._cleanCache(); return; } const { cacheTime, cacheTTLSec } = this; assert(typeof cacheTime === 'number' || typeof cacheTime === 'undefined'); assert(typeof cacheTTLSec === 'number', 'cacheTTLSec must be a number'); if (cacheTime && ((now - cacheTime) > cacheTTLSec * 1000)) { console.log('cache expired'); this._cleanCache(); } } updateCache(groupId, servers) { if (servers) { this.groupServersCache.set(groupId, servers); } this.cacheTime = Date.now(); } _cleanCache() { this.groupServersCache.clear(); this.cacheTime = 0; } async loopServerUntilWeight(options) { const { serverGroupId, ecsId } = options; assert(ecsId, 'ecsId is required'); // this.cleanCache(true) const groupServer = await this.getGroupServer(serverGroupId, ecsId); assert(groupServer, `No server found by ecsId ${ecsId} and serverGroupId ${serverGroupId}`); const startStep = typeof options.startStep === 'undefined' ? 10 : options.startStep; const step = typeof options.step === 'undefined' ? 30 : options.step; const opts = { currentWeight: options.currentWeight, dstWeight: options.weight, startStep, step, }; const range = caculateWeights(opts); if (!range.length) { return; } if (this.showProgressLog || this.debug) { console.info({ range }); } let ret; for await (const val of range) { if (this.showProgressLog || this.debug) { console.info(`Set weight of "${ecsId}": ${val} at ${new Date().toLocaleString()}`); } const { jobId } = await this.setServersWeight(serverGroupId, [ecsId], val); this.cleanCache(true); assert(jobId, 'no jobId'); ret = jobId; await this.loopJobUntilStatuses(jobId, [exports.JobStatus.Succeeded, exports.JobStatus.Failed], 9000); } return ret; } async loopJobUntilStatuses(jobId, statusArray, /** 单位毫秒 */ startDue = 5000, /** 单位毫秒 */ intervalDuration = 3000) { const intv$ = rxjs.timer(startDue, intervalDuration); const stream$ = intv$.pipe(rxjs.takeWhile(idx => idx < 100), rxjs.mergeMap(() => this.isJobMatchStatusList(jobId, statusArray), 1), rxjs.skipWhile((jobStatusMatched) => { this.debug && console.info({ jobStatusMatched }); return !jobStatusMatched; }), rxjs.map(() => jobId), rxjs.take(1)); return rxjs.firstValueFrom(stream$); } createClient(accessKeyId, accessKeySecret) { const config = new openapiClient.Config({ accessKeyId, accessKeySecret }); config.endpoint = this.endpoint; const client = new _Client(config); this.debug && console.info({ client }); return client; } } function caculateWeights(options) { const { currentWeight, dstWeight, startStep, step } = options; assert(typeof currentWeight === 'number', 'currentWeight must be a number'); const w2 = Math.min(100, Math.max(0, dstWeight)); const startStep2 = typeof startStep === 'number' ? startStep : 10; const step2 = typeof step === 'number' ? step : 20; const range = []; if (currentWeight === w2) { return range; } else if (currentWeight > w2) { // 当前权重大于目标权重, down for (let i = currentWeight - startStep2; i > w2; i -= step2) { range.push(i); } range.push(w2); } else if (currentWeight < w2) { // 当前权重小于目标权重, up for (let i = currentWeight + startStep2; i < w2; i += step2) { range.push(i); } range.push(w2); } return range; } // const ps: CalcuWeightOptions = { // currentWeight: 100, // dstWeight: 0, // step: 50, // } // const arr = caculateWeights(ps) // ListAsynJobsResponseBodyJobs { // apiName: 'UpdateServerGroupServersAttribute', // createTime: 1650517200000, // errorCode: '', // errorMessage: '', // id: '41067037-a139-4e27-b68e-4b864f8c11cd', // modifyTime: 1650517200000, // operateType: 'Update', // resourceId: 'sgp-7zyucxmdnfdomor6hp', // resourceType: 'servergroup', // status: 'Processing' // }, // ListAsynJobsResponseBodyJobs { // apiName: 'UpdateServerGroupServersAttribute', // createTime: 1650517200000, // id: '41067037-a139-4e27-b68e-4b864f8c11cd', // modifyTime: 1650517200000, // operateType: 'Update', // resourceId: 'sgp-7zyucxmdnfdomor6hp', // resourceType: 'servergroup', // status: 'Succeeded' // }, // type GroupServersCache = Map<ServerGroupId, string> /** serverId -> GroupServer */ // type ServersCache = Map<string, GroupServer> Object.defineProperty(exports, 'ListAsynJobsRequest', { enumerable: true, get: function () { return Alb.ListAsynJobsRequest; } }); Object.defineProperty(exports, 'ListAsynJobsResponseBodyJobs', { enumerable: true, get: function () { return Alb.ListAsynJobsResponseBodyJobs; } }); Object.defineProperty(exports, 'ListServerGroupServersRequest', { enumerable: true, get: function () { return Alb.ListServerGroupServersRequest; } }); Object.defineProperty(exports, 'ListServerGroupsRequest', { enumerable: true, get: function () { return Alb.ListServerGroupsRequest; } }); Object.defineProperty(exports, 'ListServerGroupsResponseBodyServerGroups', { enumerable: true, get: function () { return Alb.ListServerGroupsResponseBodyServerGroups; } }); Object.defineProperty(exports, 'UpdateServerGroupServersAttributeRequest', { enumerable: true, get: function () { return Alb.UpdateServerGroupServersAttributeRequest; } }); Object.defineProperty(exports, 'UpdateServerGroupServersAttributeResponseBody', { enumerable: true, get: function () { return Alb.UpdateServerGroupServersAttributeResponseBody; } }); exports.AlbClient = AlbClient; exports.caculateWeights = caculateWeights; //# sourceMappingURL=index.cjs.map