发布于: Nov 30, 2022
【概要】本文以 DynamoDB 存储实例相关信息,通过 PartiQL 类结构化查询语言,搭建了一套无服务器的通用实例升级规划架构。该规划系统考虑了实例升级规划中的普遍性问题,例如将实例区分为生产与非生产,考虑实例预留期限,照顾升级实际需求的约束条件等。
算法总览
要节约成本,对预留实例来说,在预留到期日当天升级可以把升级成本降至最低。但这是理想情况,实际生产中并不能保证。例如某天到期的实例过多,超过了每日处理实例上限。此时需要将溢出的实例推后到符合条件的日期。又如主从数据库到期日相同,此时需要将其拆分到不同的批次升级,以减少对系统的影响。诸如此类。所以总体思想是先按预留到期日和约束条件规划,然后根据其他关联关系调整。
算法基本上可以分成四大步,即生产和非生产实例的首轮规划,依据实例关联关系二次调整,包括主从数据库对和负载均衡器组关系。两个首轮规划可以并行,如若数据量不大则串行处理亦可。下图是算法总览的工作流示意图。后节中会结合代码实现具体展开。如果有其他关联关系需要在首轮规划的基础上调整,可后置于最后一个调整模块。
数据导入
DynamoDB 去年底开始支持类结构化查询语言 PartiQL (一种与 SQL 兼容的查询语言)查询、插入、更新和删除表数据,十分便利。例如实例数据的导入,可以通过类 SQL 语言完成:
class InstanceLoader { // instances: 通过 S3 的 CSV 文件读入实例数组,在此从略 async insert(instances) { for (const instance of instances) { const select = `select * from InstanceTable where id = '${instance.id}'`; const items = (await dynamodb.executeStatement({Statement: select}).promise()).Items; switch (items.length) { case 0: const insert = `insert into InstanceTable value { 'id': '${instance.id}', 'mode': '${instance.mode}', 'zone': '${instance.zone}', 'type': '${instance.type}', 'application': '${instance.application}', 'reserveExpiryDate': '${instance.reserveExpiryDate}' }`; console.log(`Instance ${instance.id} does not exist, insert it.`); await dynamodb.executeStatement({Statement: insert}).promise(); break; case 1: const update = `update InstanceTable set mode = '${instance.mode}' set zone = '${instance.zone}' set type = '${instance.type}' set application = '${instance.application}' set reserveExpiryDate = '${instance.reserveExpiryDate}' where id = '${instance.id}'`; console.log(`Instance ${instance.id} exists, update it.`); await dynamodb.executeStatement({Statement: update}).promise(); break; } } } }
DynamoDB 和该类结构化语言都支持数组类型。例如负载均衡器不同组内有多个实例,可以按数组同时存储到一个值内:
class LoadBalancing { id; groupA = []; groupB = []; toQuotedString(arr) { return "'" + arr.join("', '") + "'"; } } class LoadBalancingLoader { // lbs: 通过 S3 的 CSV 文件读入负载均衡器数组,在此从略 async insert(lbs) { for (const lb of lbs) { const select = `select * from LoadBalancingTable where id = '${lb.id}'`; const items = (await dynamodb.executeStatement({Statement: select}).promise()).Items; switch (items.length) { case 0: const insert = `insert into LoadBalancingTable value { 'id': '${lb.id}', 'groupA': [${lb.toQuotedString(lb.groupA)}], 'groupB': [${lb.toQuotedString(lb.groupB)}] }`; console.log(`Load balancing ${lb.id} does not exist, insert it.`); await dynamodb.executeStatement({Statement: insert}).promise(); break; case 1: const update = `update LoadBalancingTable set groupA = [${lb.toQuotedString(lb.groupA)}] set groupB = [${lb.toQuotedString(lb.groupB)}] where id = '${lb.id}'`; console.log(`Load balancing ${lb.id} exists, update it.`); await dynamodb.executeStatement({Statement: update}).promise(); break; } } } }
主从数据库对比负载均衡器组略简单,因为是单一值,不是数组值。相关数据处理代码在此从略。
算法核心
规划算法的核心是以日为单位的批次及其管理。一个批次定义为某日处理某组实例。批次管理最重要的是根据欲升级日期和约束条件,新建或者查找符合约束条件的批次。即该日星期数为可排星期且非节假日,该批次实例未达到日处理上限等。如果给定的日期不满足,则往后依次轮询。
class Batch { date; instances = []; get key() { return this.date.toDateString(); } get size() { return this.instances.length; } addInstance(instance) { this.instances.push(instance); } } class BatchManager { batchMap = new Map(); createBatch(date, limit) { if (this.batchMap.has(date.toDateString())) { const batch = this.batchMap.get(date.toDateString()); return batch.size < limit ? batch : null; } const batch = new Batch(new Date(date), this); this.batchMap.set(batch.key, batch); return batch; } retrieveBatch(date, limit, allowedDays, holidays) { var batch = null; do { date = date.nextValidDate(allowedDays, holidays); batch = this.createBatch(date, limit); if (batch == null) { date = date.plusOneDay(); } } while (batch == null); return batch; } }
规划预留实例,主要是根据其预留到期日排列。利用批次管理器,从实例预留到期日开始,找到符合条件的批次,放置实例。
class Scheduler { async scheduleRdInstances(mode, limit, allowedDays, holidays) { const instances = await this.instanceManager.selectReservedInstances(mode); for (var i = 0; i < instances.length; i++) { const item = instances[i]; const instance = new Instance(item.id.S, item.mode.S, item.zone.S, item.type.S, item.application.S, item.reserveExpiryDate.S); const date = new Date(instance.reserveExpiryDate); const batch = this.batchManager.retrieveBatch(date, limit, allowedDays, holidays); batch.addInstance(instance); } } }
规划按需实例,利用批次管理器,从升级首日开始,找到符合条件的批次,放置实例。这里在读取按需实例时,会根据约束条件按应用或者机型排序。
class InstanceManager { async selectOdInstances(mode, sortBy) { const select = `select * from InstanceTable where reserveExpiryDate = 'OD' and mode = '${mode}'`; const items = (await dynamodb.executeStatement({Statement: select}).promise()).Items; switch (sortBy) { case 'app': console.log("Sort by application.") items.sort((i, j) => i.application.S.localeCompare(j.application.S)); break; case 'type': console.log("Sort by instance type.") items.sort((i, j) => Instance.compareType(j.type.S, i.type.S)); break; } return items; } } class Scheduler { async scheduleOdInstances(mode, limit, allowedDays, holidays, sortBy, startDate) { const instances = await this.instanceManager.selectOdInstances(mode, sortBy); const date = new Date(startDate); for (var i = 0; i < instances.length; i++) { const item = instances[i]; const instance = new Instance(item.id.S, item.mode.S, item.zone.S, item.type.S, item.application.S, item.reserveExpiryDate.S); const batch = this.batchManager.retrieveBatch(date, limit, allowedDays, holidays); batch.addInstance(instance); } } }
至此,规划算法就比较清楚了,结合算法总览图,大体上是以下几步:① 规划非生产预留实例,② 规划非生产按需实例,③ 规划生产预留实例,④ 规划生产按需实例,⑤ 调整主从数据库实例,⑥ 调整负载均衡器实例。最后两步是根据实例间关联关系调整。调整思路在算法总览一节有描述,在此不赘述。最后把批次按日期排序,把排期日期、实例各属性、各关联关系信息,依次填入汇总表即可完成。
class Scheduler { consolidate() { const instances = []; const batches = Array.from(this.batchManager.batchMap.values()); batches.sort((i, j) => i.key.localeCompare(j.key)); for (const batch of batches) { for (const i of batch.instances) { const db = this.instanceManager.checkDatabaseReplica(i.id); const lb = this.instanceManager.checkLoadBalancing(i.id); instances.push(new Scheduled(i.id, i.mode, i.zone, i.type, i.application, i.reserveExpiryDate, batch.date, db[0], db[1], lb[0], lb[1])); } } } async schedule(event) { await this.scheduleRdInstances("dev", event.devDailyLimit, event.devAllowedDays, event.holidays); await this.scheduleOdInstances("dev", event.devDailyLimit, event.devAllowedDays, event.holidays, event.sortBy, event.startDate); await this.scheduleRdInstances("prod", event.prodDailyLimit, event.prodAllowedDays, event.holidays); await this.scheduleOdInstances("prod", event.prodDailyLimit, event.prodAllowedDays, event.holidays, event.sortBy, event.startDate); await this.adjustDatabaseReplica(event); await this.adjustLoadBalancing(event); this.consolidate(); } }
辅助函数
为了便于编码,在函数调用入口定义了数个日期类的辅助函数。
exports.handler = async event => { Date.prototype.toDateString = function() { return this.toISOString().substring(0, 10);}; Date.prototype.plusDays = function(days) { const d = new Date(this); d.setDate(d.getDate() + days); return d; }; Date.prototype.plusOneDay = function() { return this.plusDays(1); }; Date.prototype.nextValidDate = function(allowedDays, holidays) { var date = this; while (!allowedDays.includes(date.getDay()) || holidays.includes(date.toDateString())) { date = date.plusOneDay(); }; return date; } await new Scheduler().schedule(event); };
性能测试
利用一套模拟数据集对本系统进行测试。该数据集包含 362 台实例,其中非生产预留实例 9 台,非生产按需实例 105 台,生产预留实例 167 台,生产按需实例 81 台。预留到期日分布于数个时间节点。另外有 5 对 10 台主从数据库,41 个负载均衡器组共 84 台实例。测试结果显示,针对各项约束条件下的规划耗时均在秒级,通常在 3 秒内完成。
本文以 DynamoDB 存储实例相关信息,通过 PartiQL 类结构化查询语言,搭建了一套无服务器的通用实例升级规划架构。该规划系统考虑了实例升级规划中的普遍性问题,例如将实例区分为生产与非生产,考虑实例预留期限,照顾升级实际需求的约束条件等。用户可以根据不同的实际情况,改变约束条件,快速得到多种条件下的规划情况。选择较优的规划,展开进一步微调和优化,从而提高工作效率。
工作展望
有几个方面可以拓展上述工作。其一可以扩大实例类别。除了生产非生产二元划分,支持多元划分,使得实例升级规划更细腻、更贴近现实。其二是支持超过两个组的负载均衡器,当区域有三个或以上可用区时,就有可能有多个组。其三是借助亚马逊云科技其他服务的支持,例如实例成本与账单信息,对规划结果进行成本预估,对实际使用情况进行费用核算等。