机器扩缩容决策
引言:一个关于“等待”的故事
想象一下,你正在运营一个非常繁忙的在线服务系统,它就像一个快餐店。顾客(生产者)不断地提交订单(消息),这些订单进入一个排队系统(先进先出队列),然后由一群厨师(消费者/机器)来处理。
你的服务承诺是:任何一份订单,从顾客下单的那一刻起,到制作完成,总耗时不能超过 5 秒。
我们知道,每一份订单的制作(消费)时间是固定的 1 秒。这意味着,任何一份订单在队列里等待的时间绝对不能超过 4 秒。这就是我们系统的服务等级协议 (SLA) ,是必须遵守的铁律。
你的任务就是扮演这个系统的“智能调度官”,在特定的时间点,决定是该增加厨师(扩容)还是减少厨师(缩容),以最低的成本(最少的厨师数量)来满足这个服务承诺。
第一部分:系统的运作规则
-
初始状态:
- 系统开始时,只有 1 台机器(1 个厨师)。
- 每台机器的处理能力(
capability)是固定的,表示它 1 秒钟可以并行处理多少条消息。
-
决策时刻 (
judgeWithMsgs):- 你不能随时调整机器数量。只有在特定的时间点
time,你才有机会进行决策。 - 在这个时刻,会有一批数量为
msgNum的新消息(新订单)到达并进入队列的末尾。
- 你不能随时调整机器数量。只有在特定的时间点
-
扩缩容规则:
- 扩容 (增加机器): 你可以随时增加任意数量的机器,新机器会立刻投入工作。
- 缩容 (减少机器): 你可以减少机器,但必须保证集群中至少有 1 台机器在运行。被缩容的机器会立刻停止工作。
第二部分:智能决策的核心
当你在 time 时刻进行决策时,你需要用“水晶球”预见未来,做出最经济的选择。你的思考过程如下:
-
核心问题: “假设从现在
time时刻起,我将机器数量调整为X台并且不再改变,那么这个X至少应该是多少,才能保证当前队列里所有的消息(包括刚刚新来的)都能在它们各自的5秒大限之内被处理完?” -
如何计算所需的
X?-
梳理待办任务: 首先,你需要知道队列里现在所有待处理的消息,以及它们各自的“最后期限”。一条在
t_arrival时刻到达的消息,它的处理截止时间是t_arrival + 5秒。 -
找到最紧急的时刻: 系统面临的最大压力来自于那些即将到期的消息。你需要检查未来每一个关键的时间点(即每一批消息的截止时间点),确保在那一刻之前,所有该处理完的消息都已经被处理掉了。
-
计算处理能力:
X台机器的处理能力是X * capability条消息/秒。 -
进行推演: 对每一个未来的截止时间点
deadline_j,你需要计算:- 任务积压量: 到
deadline_j为止,总共有多少条消息必须被处理完。 - 可用处理时间: 从现在
time到deadline_j,总共有deadline_j - time秒。 - 所需机器数:
(任务积压量) / (可用处理时间 * capability),然后向上取整,就得到了为了满足这个截止时间点所需要的机器数量。
- 任务积压量: 到
-
最终决策: 在所有这些推演中,找到那个要求机器数量最多的结果。这个结果就是你在当前
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) | 操作 / 新消息 | 队列状态 (消息批次和数量) | 当前机器数 | 决策分析与结果 |
|---|---|---|---|---|
| 0 | judge(0, 8) | {t=0, 8条} | 1 | 8条消息,截止t=5。1台机器每秒处理10条,1秒内处理完。返回 0。 |
| 1 | judge(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。 |
| 2 | judge(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台机器继续处理。 |
| 4 | judge(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;
}
}