发布于: Nov 30, 2022

【概要】本文以 DynamoDB 存储实例相关信息,通过 PartiQL 类结构化查询语言,搭建了一套无服务器的通用实例升级规划架构。该规划系统考虑了实例升级规划中的普遍性问题,例如将实例区分为生产与非生产,考虑实例预留期限,照顾升级实际需求的约束条件等。

算法总览

要节约成本,对预留实例来说,在预留到期日当天升级可以把升级成本降至最低。但这是理想情况,实际生产中并不能保证。例如某天到期的实例过多,超过了每日处理实例上限。此时需要将溢出的实例推后到符合条件的日期。又如主从数据库到期日相同,此时需要将其拆分到不同的批次升级,以减少对系统的影响。诸如此类。所以总体思想是先按预留到期日和约束条件规划,然后根据其他关联关系调整。

算法基本上可以分成四大步,即生产和非生产实例的首轮规划,依据实例关联关系二次调整,包括主从数据库对和负载均衡器组关系。两个首轮规划可以并行,如若数据量不大则串行处理亦可。下图是算法总览的工作流示意图。后节中会结合代码实现具体展开。如果有其他关联关系需要在首轮规划的基础上调整,可后置于最后一个调整模块。

系统架构

实例和相关结构化数据,通过文件存入 S3 桶,而后通过 Lambda 导入 DynamoDB 表。规划通过 Lambda 运算,结果可以选择存入 DynamoDB 表,或者以文件存入 S3 桶。如果实例存在且有相应权限,实例相关属性信息可以直接读取。整体系统架构图如下所示:

 

 

数据导入

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 类结构化查询语言,搭建了一套无服务器的通用实例升级规划架构。该规划系统考虑了实例升级规划中的普遍性问题,例如将实例区分为生产与非生产,考虑实例预留期限,照顾升级实际需求的约束条件等。用户可以根据不同的实际情况,改变约束条件,快速得到多种条件下的规划情况。选择较优的规划,展开进一步微调和优化,从而提高工作效率。

工作展望

有几个方面可以拓展上述工作。其一可以扩大实例类别。除了生产非生产二元划分,支持多元划分,使得实例升级规划更细腻、更贴近现实。其二是支持超过两个组的负载均衡器,当区域有三个或以上可用区时,就有可能有多个组。其三是借助亚马逊云科技其他服务的支持,例如实例成本与账单信息,对规划结果进行成本预估,对实际使用情况进行费用核算等。

 

相关文章