UNPKG

@yuntools/ali-alb

Version:

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

529 lines (447 loc) 14.2 kB
import assert from 'node:assert/strict' import Alb, { ListAsynJobsRequest, UpdateServerGroupServersAttributeResponseBody, ListServerGroupsRequest, ListServerGroupsResponseBodyServerGroups, ListServerGroupServersRequest, UpdateServerGroupServersAttributeRequest, ListAsynJobsResponseBodyJobs, } from '@alicloud/alb20200616' import { Config as ApiConfig } from '@alicloud/openapi-client' import { EcsClient } from '@yuntools/ali-ecs' import { firstValueFrom, timer, takeWhile, map, mergeMap, skipWhile, take, } from 'rxjs' import { _Client } from './client.js' import { Action, ActionRet, CalcuWeightOptions, GroupServer, JobId, JobStatus, ServerGroupId, UpdateServerWeightOptions, UpdateServerWeightOptionsInner, } from './types.js' /** 阿里云 ALB 负载均衡服务接口 */ export class AlbClient { /** * 是否输出日志 * @default false */ debug = false /** * 是否输出渐进日志 * @default true */ showProgressLog = true client: Alb nextToken = '' ecsClient: EcsClient groupServersCache = new Map<ServerGroupId, GroupServer[]>() // serversCache = new Map<ServerGroupId, ServersCache>() cacheTime: number cacheTTLSec = 10 constructor( protected id: string, protected secret: string, public endpoint = 'alb.cn-hangzhou.aliyuncs.com', public ecsClientInstance?: EcsClient, ) { this.client = this.createClient(id, secret) if (ecsClientInstance) { this.ecsClient = ecsClientInstance } else { this.ecsClient = new EcsClient(id, secret) } } /** * 获取指定服务组信息 */ async getGroup(serverGroupId: string): Promise<ListServerGroupsResponseBodyServerGroups | undefined> { const opts = { action: Action.ListServerGroups, nextToken: this.nextToken, serverGroupIds: [serverGroupId], } const req = new 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: string, serverId: string, withoutCache = false, ): Promise<GroupServer | undefined> { assert(serverId, 'serverId is required') if (withoutCache === true) { this.cleanCache(true) } let ret: GroupServer | undefined 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: string, ips: string[], ): Promise<Map<string, GroupServer>> { const ret = new Map<string, GroupServer>() 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: string, ip: string, ): Promise<GroupServer | undefined> { 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: string): Promise<GroupServer[] | undefined> { this.cleanCache() const cache = this.groupServersCache.get(serverGroupId) if (cache) { return cache } const opts = { Action: Action.ListServerGroupServers, NextToken: this.nextToken, serverGroupId, maxResults: 100, } const req = new 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: string): Promise<ListAsynJobsResponseBodyJobs | undefined> { const opts = { Action: Action.GetAsyncJobResult, NextToken: this.nextToken, jobIds: [jobId], maxResults: 10, } const req = new 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: UpdateServerWeightOptions): Promise<ActionRet | undefined> { 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: UpdateServerWeightOptions): Promise<ActionRet | undefined> { 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 as UpdateServerWeightOptionsInner) 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: string, serverIds: string[], weight: number, ): Promise<UpdateServerGroupServersAttributeResponseBody> { // 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: unknown[] = [] 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: Action.UpdateServerGroupServersAttribute, NextToken: this.nextToken, ServerGroupId: serverGroupId, servers, } const req = new 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<T extends JobStatus>(jobId: string, statusArray: T[]): Promise<T | false> { 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): void { 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: ServerGroupId, servers: GroupServer[] | undefined): void { if (servers) { this.groupServersCache.set(groupId, servers) } this.cacheTime = Date.now() } private _cleanCache(): void { this.groupServersCache.clear() this.cacheTime = 0 } private async loopServerUntilWeight(options: UpdateServerWeightOptionsInner): Promise<JobId | undefined> { 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: CalcuWeightOptions = { 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, [JobStatus.Succeeded, JobStatus.Failed], 9000) } return ret } private async loopJobUntilStatuses<T extends JobStatus>( jobId: string, statusArray: T[], /** 单位毫秒 */ startDue = 5000, /** 单位毫秒 */ intervalDuration = 3000, ): Promise<JobId> { const intv$ = timer(startDue, intervalDuration) const stream$ = intv$.pipe( takeWhile(idx => idx < 100), mergeMap(() => this.isJobMatchStatusList(jobId, statusArray), 1), skipWhile((jobStatusMatched) => { this.debug && console.info({ jobStatusMatched }) return ! jobStatusMatched }), map(() => jobId), take(1), ) return firstValueFrom(stream$) } private createClient(accessKeyId: string, accessKeySecret: string): Alb { const config = new ApiConfig({ accessKeyId, accessKeySecret }) config.endpoint = this.endpoint const client = new _Client(config) this.debug && console.info({ client }) return client } } export function caculateWeights(options: CalcuWeightOptions): number[] { 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: number[] = [] 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>