机器扩缩容决策

46 阅读8分钟

机器扩缩容决策

引言:一个关于“等待”的故事

想象一下,你正在运营一个非常繁忙的在线服务系统,它就像一个快餐店。顾客(生产者)不断地提交订单(消息),这些订单进入一个排队系统(先进先出队列),然后由一群厨师(消费者/机器)来处理。

你的服务承诺是:任何一份订单,从顾客下单的那一刻起,到制作完成,总耗时不能超过 5 秒

我们知道,每一份订单的制作(消费)时间是固定的 1 秒。这意味着,任何一份订单在队列里等待的时间绝对不能超过 4 秒。这就是我们系统的服务等级协议 (SLA) ,是必须遵守的铁律。

你的任务就是扮演这个系统的“智能调度官”,在特定的时间点,决定是该增加厨师(扩容)还是减少厨师(缩容),以最低的成本(最少的厨师数量)来满足这个服务承诺。

第一部分:系统的运作规则

  1. 初始状态:

    • 系统开始时,只有 1 台机器(1 个厨师)。
    • 每台机器的处理能力(capability)是固定的,表示它 1 秒钟可以并行处理多少条消息。
  2. 决策时刻 (judgeWithMsgs):

    • 你不能随时调整机器数量。只有在特定的时间点 time,你才有机会进行决策。
    • 在这个时刻,会有一批数量为 msgNum 的新消息(新订单)到达并进入队列的末尾。
  3. 扩缩容规则:

    • 扩容 (增加机器): 你可以随时增加任意数量的机器,新机器会立刻投入工作。
    • 缩容 (减少机器): 你可以减少机器,但必须保证集群中至少有 1 台机器在运行。被缩容的机器会立刻停止工作。

第二部分:智能决策的核心

当你在 time 时刻进行决策时,你需要用“水晶球”预见未来,做出最经济的选择。你的思考过程如下:

  • 核心问题: “假设从现在 time 时刻起,我将机器数量调整为 X 台并且不再改变,那么这个 X 至少应该是多少,才能保证当前队列里所有的消息(包括刚刚新来的)都能在它们各自的5秒大限之内被处理完?”

  • 如何计算所需的 X ?

    1. 梳理待办任务: 首先,你需要知道队列里现在所有待处理的消息,以及它们各自的“最后期限”。一条在 t_arrival 时刻到达的消息,它的处理截止时间是 t_arrival + 5 秒。

    2. 找到最紧急的时刻: 系统面临的最大压力来自于那些即将到期的消息。你需要检查未来每一个关键的时间点(即每一批消息的截止时间点),确保在那一刻之前,所有该处理完的消息都已经被处理掉了。

    3. 计算处理能力: X 台机器的处理能力是 X * capability 条消息/秒。

    4. 进行推演: 对每一个未来的截止时间点 deadline_j,你需要计算:

      • 任务积压量:deadline_j 为止,总共有多少条消息必须被处理完。
      • 可用处理时间: 从现在 timedeadline_j,总共有 deadline_j - time 秒。
      • 所需机器数: (任务积压量) / (可用处理时间 * capability),然后向上取整,就得到了为了满足这个截止时间点所需要的机器数量。
    5. 最终决策: 在所有这些推演中,找到那个要求机器数量最多的结果。这个结果就是你在当前 time 时刻为了保证服务承诺所必须拥有的最少机器数 X_min

  • 返回结果:

    • 将计算出的 X_min 与你当前拥有的机器数进行比较。
    • 如果 X_min > 当前机器数,你需要扩容 X_min - 当前机器数 台(返回正数)。
    • 如果 X_min < 当前机器数,你可以缩容 当前机器数 - X_min 台(返回负数)。
    • 如果相等,则无需变动(返回 0)。

第三部分:你的任务清单 (输入与输出)

  • ScalingSys(int capability): 初始化系统,告知你每台机器的处理能力。
  • judgeWithMsgs(int time, int msgNum):time 时刻,有 msgNum 条新消息到来,请你根据上述决策机制,计算并返回应该扩容/缩容的机器数量。

输入格式

  • capability: 每台机器每秒处理的消息量 (1 <= capability <= 1000)。
  • time: 进行决策的时刻 (0 <= time <= 1000,且严格递增)。
  • msgNum:time 时刻新到达的消息数量 (0 <= msgNum <= 1000000)。

输出格式

  • 一个整数,表示决策结果:

    • 正数: 表示需要扩容的机器数量。
    • 负数: 表示需要缩容的机器数量。
    • 0: 表示无需扩缩容。

样例

输入:

ScalingSys(10)
judgeWithMsgs(0, 8)
judgeWithMsgs(1, 140)
judgeWithMsgs(2, 41)
judgeWithMsgs(4, 0)
时刻(time)操作 / 新消息队列状态 (消息批次和数量)当前机器数决策分析与结果
0judge(0, 8){t=0, 8条}18条消息,截止t=5。1台机器每秒处理10条,1秒内处理完。返回 0
1judge(1, 140)旧: {t=0, 0条} (t=0到t=1的1秒内,1台机器处理了10条,8条已处理完)
新: {t=1, 140条}
1队列共140条,截止t=6,还有5秒。需 ceil(140 / (5 * 10)) = 3台。当前1台,需扩容 2 台返回 2
2judge(2, 41)旧: {t=1, 110条} (t=1到t=2的1秒内,3台机器处理了30条)
新: {t=2, 41条}
3最紧急的是t=1那批: 110条,截止t=6,还有4秒。需ceil(110 / (4*10)) = 2.75 -> 3台。
两批一起看: t=1的110条和t=2的41条,截止t=6时,至少要处理完t=1的110条,以及t=2的 (41条/(t=7-t=2=5秒)) * (t=6-t=2=4秒) = 32.8条,总共约142.8条。需 ceil(142.8/(4*10)) = 3.57 -> 4台。当前3台不够,需扩容 1 台返回 1
3(无调用)(t=2到t=4的2秒内,4台机器处理了80条)4系统按现有4台机器继续处理。
4judge(4, 0)旧: {t=1, 30条}, {t=2, 41条} (t=2时总共151条,已处理80条,剩71条)
新: {t=4, 0条}
4最紧急的是t=1那批: 剩30条,截止t=6,还有2秒。需ceil(30/(2*10)) = 1.5 -> 2台。
t=2这批: 剩41条,截止t=7,还有3秒。需ceil(41/(3*10)) = 1.36 -> 2台。综合看,未来任何时刻都只需要2台或更少。当前有4台,可以缩容 1 台,变为3台,仍满足要求。返回 -1
import import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.Objects;

public class ScalingSys {
    /**
     * 内部类,用于封装一批同时到达的消息。
     */
    private static class MessageBatch {
        int arrivalTime;     // 消息到达的时间
        long messageCount; // 消息数量,使用 long 以处理大数值

        MessageBatch(int arrivalTime, long messageCount) {
            this.arrivalTime = arrivalTime;
            this.messageCount = messageCount;
        }
    }

    // --- 系统状态成员变量 ---
    private final int capability;           // 每台机器每秒处理的消息量
    private int currentMachineCount;        // 当前集群中的机器数量
    private int lastDecisionTime;           // 上一次进行决策的时间点
    private final Queue<MessageBatch> messageQueue; // 消息队列

    private static final int SLA_SECONDS = 5; // 服务等级协议:消息处理总时长限制

    /**
     * 构造函数 - 初始化系统。
     * @param capability 每台机器每秒并行消费的消息量。
     */
    public ScalingSys(int capability) {
        this.capability = capability;
        this.currentMachineCount = 1; // 初始机器数为 1
        this.lastDecisionTime = 0;    // 初始时间点为 0
        // 使用 ArrayDeque 作为队列,它通常比 LinkedList 作为 FIFO 队列性能更好
        this.messageQueue = new ArrayDeque<>();
    }

    /**
     * 在指定时刻进行扩缩容决策。我们只能在某个时间点进行操作,所以在这个时间点 
     * 我们要把所有事做完,一是总结历史,而是预测未来
     * @param time   当前决策时刻
     * @param msgNum 在该时刻新到达的消息数量
     * @return 扩容(正数)/缩容(负数)的机器数量,或0表示不变。
     */
    public int judgeWithMsgs(int time, int msgNum) {
        // --- 1. 模拟从上个决策点到当前时刻之间,已完成的工作 ---
        int timeElapsed = time - this.lastDecisionTime;
        if (timeElapsed > 0) {
            // 在这段时间内可处理的消息总量
            long processingCapacity = (long) this.currentMachineCount * this.capability * timeElapsed;

            // 从队列头部开始消耗消息
            while (processingCapacity > 0 && !messageQueue.isEmpty()) {
                MessageBatch frontBatch = messageQueue.peek();
                if (frontBatch.messageCount <= processingCapacity) {
                    // 如果容量足够处理完整个批次
                    processingCapacity -= frontBatch.messageCount; // 消耗容量
                    messageQueue.poll();                          // 移除该批次
                } else {
                    // 如果容量不足以处理完整个批次
                    frontBatch.messageCount -= processingCapacity; // 减少批次中的消息数
                    processingCapacity = 0;                       // 容量耗尽
                }
            }
        }

        // --- 2. 将新到达的消息加入队列 ---
        if (msgNum > 0) {
            messageQueue.add(new MessageBatch(time, msgNum));
        }

        // --- 3. 核心决策:计算未来所需的最小机器数 ---
        int requiredMachines = 0; // 默认为0,如果没有任务则可以缩容到1
        if (!messageQueue.isEmpty()) {
            requiredMachines = 1; // 只要有任务,至少需要1台机器
            long cumulativeBacklog = 0; // 累计待处理的任务量

            // 遍历队列中的每一批任务,计算为了满足其SLA所需的机器数
            for (MessageBatch batch : messageQueue) {
                cumulativeBacklog += batch.messageCount; // 累加任务量
                
                // 计算此批次任务的处理截止时间
                int deadline = batch.arrivalTime + SLA_SECONDS;
                // 计算从现在到截止时间还有多少秒
                int timeLeft = deadline - time;

                // 如果时间窗口无效(理论上题目约束不会发生),跳过
                if (timeLeft <= 0) continue;

                // 计算处理完所有累计任务量所需的处理能力
                long totalCapacityNeeded = (long) this.capability * timeLeft;
                
                // 计算为了在 timeLeft 秒内处理完 cumulativeBacklog 所需的机器数
                // 使用向上取整的技巧: (a + b - 1) / b
                long machinesForThisDeadline = (cumulativeBacklog + totalCapacityNeeded - 1) / totalCapacityNeeded;

                // 更新我们需要的机器数,取所有截止时间要求中的最大值
                requiredMachines = Math.max(requiredMachines, (int) machinesForThisDeadline);
            }
        } else {
             // 如果队列为空,我们只需要保留最低数量的机器
             requiredMachines = 1;
        }

        // --- 4. 计算决策结果并更新状态 ---
        int machinesToChange = requiredMachines - this.currentMachineCount;

        // 更新系统的当前状态
        this.currentMachineCount = requiredMachines;
        this.lastDecisionTime = time;

        return machinesToChange;
    }
}