在工作中学算法——内存资源调度与分配

135 阅读4分钟

在现代计算中有一个非常核心和普遍的场景:资源调度与分配

  • 物理机 (capacities) : 想象一个机房里有成百上千台物理服务器。这些服务器的配置各不相同,有的内存大,有的内存小。capacities 数组代表了这个物理服务器资源池,每个元素是某台服务器当前可用的内存大小。

  • 虚拟机请求 (requests) : 不断有客户通过网站或API请求创建虚拟机(云主机)。每个请求都会指定需要的内存大小(例如 "我要一台8G内存的虚拟机")。requests 数组代表了这一系列按时间顺序到达的客户请求。

  • 调度系统的任务: 云平台的调度系统(也称“大脑”)需要为每一个新的虚拟机请求,从成千上万的物理机中挑选一台最合适的来部署。

  • 为什么选择“可用内存最小”的物理机?(Best-Fit 策略) 这是一种经典的资源分配策略,称为最佳适应 (Best-Fit) 。它的主要目的是减少内存碎片化

例子: 假设你有两台物理机,A机剩余 100GB 内存,B机剩余 20GB 内存。现在来了一个需要 16GB 内存的虚拟机请求。

  • 如果把它放在 A机,A机剩下 84GB。

  • 如果把它放在 B机,B机剩下 4GB。

    • 选择 B机(可用内存最小的)显然是更好的策略。因为这样保留了 A机这一整块大的内存资源,未来如果有一个需要 90GB 内存的大型虚拟机请求,我们仍然可以满足它。如果当初用掉了 A机,那么这个 90GB 的请求就无法满足了。

    • “编号最小”的规则:这是一个 tie-breaker(决胜局规则),当有多台物理机的“匹配度”(最小可用内存)相同时,需要一个确定的规则来选择,以保证分配结果是可预测和唯一的。选择编号最小的是最简单的一种。

现在我们抽象一下问题。

给定:

  1. 一个包含 N容器 (Bins) 的集合,每个容器 i 有一个初始的、可变的容量 (capacity) C_i
  2. 一个包含 M物品 (Items) 的有序序列,每个物品 j 有一个固定的尺寸 (size) S_j

任务:

按顺序处理每一个物品 S_j,为它寻找一个可以容纳它的容器,并遵循以下分配规则

  1. 资格筛选: 从所有容器中,筛选出那些当前剩余容量 C'_i 大于或等于物品尺寸 S_j 的容器。
  2. 最优选择: 在所有符合资格的容器中,选择那个剩余容量 C'_i 最小的容器。
  3. 决胜局规则: 如果有多个容器的剩余容量都同为最小,则选择那个原始编号(或索引)i 最小的容器。

操作与输出:

  • 如果为物品 S_j 找到了最佳容器 i,则更新该容器的容量 C'_i = C'_i - S_j,并记录下所选容器的编号 i
  • 如果找不到任何符合资格的容器,则记录一个“失败”标识(例如 -1)。
  • 最终,返回一个记录了每次分配结果(所选容器的编号或 -1)的序列。
import java.util.Scanner;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // ACM模式下的输入处理
        Scanner in = new Scanner(System.in);

        // 示例输入格式:一行空格分隔的数字
        String[] capacitiesStr = in.nextLine().split(" ");
        int[] capacities = new int[capacitiesStr.length];
        for (int i = 0; i < capacitiesStr.length; i++) {
            capacities[i] = Integer.parseInt(capacitiesStr[i]);
        }

        String[] requestsStr = in.nextLine().split(" ");
        int[] requests = new int[requestsStr.length];
        for (int i = 0; i < requestsStr.length; i++) {
            requests[i] = Integer.parseInt(requestsStr[i]);
        }
        in.close();

        // 调用核心算法
        int[] result = deployVMs(capacities, requests);

        // 格式化输出
        for (int i = 0; i < result.length; i++) {
            System.out.print(result[i] + (i == result.length - 1 ? "" : " "));
        }
        System.out.println();
    }

    /**
     * 核心算法:选择最匹配内存部署虚拟机
     * @param capacities 物理机初始内存数组
     * @param requests   虚拟机请求内存数组
     * @return 每个请求的部署结果数组
     */
    public static int[] deployVMs(int[] capacities, int[] requests) {
        // Key: 可用内存, Value: 拥有该可用内存的物理机编号集合(使用TreeSet保证编号有序)
        TreeMap<Integer, TreeSet<Integer>> availableMachines = new TreeMap<>();

        // 1. 初始化,将所有物理机按内存分组放入TreeMap
        for (int i = 0; i < capacities.length; i++) {
            int memory = capacities[i];
            // computeIfAbsent: 如果key不存在,则创建一个新的TreeSet并放入map
            availableMachines.computeIfAbsent(memory, k -> new TreeSet<>()).add(i);
        }

        int[] results = new int[requests.length];

        // 2. 按顺序处理每一个部署请求
        for (int i = 0; i < requests.length; i++) {
            int requestMemory = requests[i];

            // 3. 查找最佳物理机
            // ceilingKey(k): 返回大于或等于k的最小键。
            Integer bestFitMemory = availableMachines.ceilingKey(requestMemory);

            if (bestFitMemory == null) {
                // 如果找不到,说明没有任何物理机满足条件
                results[i] = -1;
            } else {
                // 找到了内存大小最合适的物理机分组
                TreeSet<Integer> machineIds = availableMachines.get(bestFitMemory);
                // first(): 由于TreeSet有序,第一个元素就是编号最小的。
                int machineToDeploy = machineIds.first();

                results[i] = machineToDeploy;

                // 4. 更新状态
                // a. 从旧的内存分组中移除这台物理机
                machineIds.remove(machineToDeploy);
                // b. 如果这个分组空了,就从TreeMap中移除这个内存键
                if (machineIds.isEmpty()) {
                    availableMachines.remove(bestFitMemory);
                }

                // c. 计算新的可用内存,并将这台物理机加入新的分组
                int remainingMemory = bestFitMemory - requestMemory;
                availableMachines.computeIfAbsent(remainingMemory, k -> new TreeSet<>()).add(machineToDeploy);
            }
        }

        return results;
    }
}