@yuntools/ali-alb
Version:
阿里云 ALB 负载均衡模块封装,支持 ESM,CJS 导入,提供 TypeScript 类型定义
489 lines (482 loc) • 17.5 kB
JavaScript
/**
* @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