sentinel系统负载自适应流控

系统负载自适应流控

规则配置

规则创建

public class SystemRule extends AbstractRule {
    private double highestSystemLoad = -1;
    private double highestCpuUsage = -1;
    private double qps = -1;
    private long avgRt = -1;
    private long maxThread = -1;
}

SystemRule类包含了以下几个指标。

  • highestSystemLoad:对应 Dashboard 上的 LOAD 菜单,代表系统最高负载值,默认为 -1,只有大于等于 0.0 才生效。
  • avgRt:对应 Dashboard 上的 RT菜单,代表系统平均响应时间,默认为 -1,只有大于0才生效。
  • maxThread:对应 Dashboard 上的线程数菜单,代表系统允许的最大线程数,默认为 -1,只有大于 0 才生效。
  • qps:对应 Dashboard 上的入口 QPS 菜单,代表限流的阈值,默认为 -1,只有大于 0 才生效。
  • highestCpuUsage:对应 Dashboard 上的 CPU 使用率菜单,代表最高CPU 使用率,取值是 [0,1] 之间,默认为 -1,只有大于等于0.0才生效
监听器实例化和管理

这部分和之前的黑白名单差不多

系统负载自适应规则的核心类是 SystemRuleManager,它负责管理系统负载自适应规则的加载、更新和监听。当系统负载自适应规则发生变化时,SystemRuleManager 通过观察者模式通知相应的 RulePropertyListener 进行更新

创建监听器的代码位置

public final class SystemRuleManager {
    // 省略其它代码...

    private static AtomicBoolean checkSystemStatus = new AtomicBoolean(false);

    private static SystemStatusListener statusListener = null;
    private final static SystemPropertyListener listener = new SystemPropertyListener();
    private static SentinelProperty<List<SystemRule>> currentProperty = new DynamicSentinelProperty<List<SystemRule>>();

    // 创建单核线程池
    private final static ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1,
                                                                                               new NamedThreadFactory("sentinel-system-status-record-task", true));

    static {

        checkSystemStatus.set(false);
        // 初始化系统状态监听器
        statusListener = new SystemStatusListener();
        // 任务调度, 一秒执行一次statusListener的任务, 即监听系统的负载状态
        scheduler.scheduleAtFixedRate(statusListener, 0, 1, TimeUnit.SECONDS);
        // 初始化SystemRule监听器
        currentProperty.addListener(listener);
    }

    // 省略其它代码...
}

规则初始化

当调用SystemRuleManagerloadRules()

public static void loadRules(List<SystemRule> rules) {
    currentProperty.updateValue(rules);
}


@Override
public boolean updateValue(T newValue) {
    if (isEqual(value, newValue)) {
        return false;
    }
    RecordLog.info("[DynamicSentinelProperty] Config will be updated to: {}", newValue);
	
    // 注意看这里, 和之前的黑白名单规则一样, 也是初始化了
    value = newValue;
    for (PropertyListener<T> listener : listeners) {
        // 遍历通知监听器
        listener.configUpdate(newValue);
    }
    return true;
}


@Override
public synchronized void configUpdate(List<SystemRule> rules) {
    // 为了恢复这些系统设置到初始状态,以便重新进行监控和设置
    restoreSetting();
    // systemRules = rules;
    // 如果配置SystemRule, 那么遍历规则, 并加载规则
    if (rules != null && rules.size() >= 1) {
        for (SystemRule rule : rules) {
            // 加载系统配置,根据传入的SystemRule对象中的参数设置系统最高负载、CPU使用率、平均响应时间、最大线程数和QPS
            loadSystemConf(rule);
        }
    } else { // 如果没有配置SystemRule, 那么关闭系统自适应检查
        checkSystemStatus.set(false);
    }
    
    // 省略其它代码...
}

核心loadSystemConf()

此方法会判断是否配置了 LOAD、RT、THREAD、QPS、CPU,如果配置这些规则中的某一个,那么就将 checkSystemStatus 置为 true,也就是打开系统自适应功能

也就是说, 系统自适应功能是否开启就看这个方法

public static void loadSystemConf(SystemRule rule) {
    boolean checkStatus = false;
    // Check if it's valid.

    // highestSystemLoad参数大于等于0且小于当前最高系统负载,则更新最高系统负载,并标记为已设置
    if (rule.getHighestSystemLoad() >= 0) {
        highestSystemLoad = Math.min(highestSystemLoad, rule.getHighestSystemLoad());
        highestSystemLoadIsSet = true;
        checkStatus = true;
    }

    // 如果highestCpuUsage参数大于0且小于等于1,则更新CPU使用率的最高限制,并标记为已设置,如果大于1则记录警告日志
    if (rule.getHighestCpuUsage() >= 0) {
        if (rule.getHighestCpuUsage() > 1) {
            RecordLog.warn(String.format("[SystemRuleManager] Ignoring invalid SystemRule: "
                                         + "highestCpuUsage %.3f > 1", rule.getHighestCpuUsage()));
        } else {
            highestCpuUsage = Math.min(highestCpuUsage, rule.getHighestCpuUsage());
            highestCpuUsageIsSet = true;
            checkStatus = true;
        }
    }

    // 如果果avgRt参数大于等于0,则更新平均响应时间的最高限制,并标记为已设置
    if (rule.getAvgRt() >= 0) {
        maxRt = Math.min(maxRt, rule.getAvgRt());
        maxRtIsSet = true;
        checkStatus = true;
    }
    
    // 如果maxThread参数大于等于0,则更新最大线程数的最高限制,并标记为已设置
    if (rule.getMaxThread() >= 0) {
        maxThread = Math.min(maxThread, rule.getMaxThread());
        maxThreadIsSet = true;
        checkStatus = true;
    }

    // 如果qps参数大于等于0,则更新QPS的最高限制,并标记为已设置
    if (rule.getQps() >= 0) {
        qps = Math.min(qps, rule.getQps());
        qpsIsSet = true;
        checkStatus = true;
    }

    // 根据上述值决定是否开启系统自适应检查
    checkSystemStatus.set(checkStatus);

}

流程图如下
在这里插入图片描述

规则验证

SystemSlot是第六个执行的slot

public class SystemSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {
        // 检查系统规则
        SystemRuleManager.checkSystem(resourceWrapper, count);

        // 如果检查通过,继续执行后续的处理链
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        fireExit(context, resourceWrapper, count, args);
    }
}

核心方法就是checkSystem()

public static void checkSystem(ResourceWrapper resourceWrapper, int count) throws BlockException {
    // 参数验证,资源为空直接放行
    if (resourceWrapper == null) {
        return;
    }
    // 判断系统自适应功能是否开启,如果没开启则直接放行。
    if (!checkSystemStatus.get()) {
        return;
    }

    // 判断资源的流量是否为入口流量,如果不是IN,则直接放行,也就是说Sentinel系统自适应限流只对入口流量生效,如果类型为OUT则直接放行
    if (resourceWrapper.getEntryType() != EntryType.IN) {
        return;
    }

    // 获取当前qps,如果当前qps大于SystemRule规则配置的阈值,则直接抛BlockException异常
    double currentQps = Constants.ENTRY_NODE.passQps();
    if (currentQps + count > qps) {
        throw new SystemBlockException(resourceWrapper.getName(), "qps");
    }

    // 获取当前线程,如果当前线程大于SystemRule规则配置的阈值,则直接抛BlockException 异常
    int currentThread = Constants.ENTRY_NODE.curThreadNum();
    if (currentThread > maxThread) {
        throw new SystemBlockException(resourceWrapper.getName(), "thread");
    }

    // 获取当前平均响应时间指标,如果当前平均响应时间大于SystemRule规则配置的阈值,则直接抛BlockException异常
    double rt = Constants.ENTRY_NODE.avgRt();
    if (rt > maxRt) {
        throw new SystemBlockException(resourceWrapper.getName(), "rt");
    }

    // 如果当前系统负载大于规则配置的系统负载,则采取bbr算法验证
    if (highestSystemLoadIsSet && getCurrentSystemAvgLoad() > highestSystemLoad) {
        // bbr算法
        if (!checkBbr(currentThread)) {
            throw new SystemBlockException(resourceWrapper.getName(), "load");
        }
    }

    // 判断当前CPU使用率是否大于SystemRule规则配置的阈值,如果大于,则抛出BlockException异常
    if (highestCpuUsageIsSet && getCurrentCpuUsage() > highestCpuUsage) {
        throw new SystemBlockException(resourceWrapper.getName(), "cpu");
    }
}

// 使用BBR对负载进行校验
private static boolean checkBbr(int currentThread) {
    if (currentThread > 1 &&
        currentThread > Constants.ENTRY_NODE.maxSuccessQps() * Constants.ENTRY_NODE.minRt() / 1000) {
        return false;
    }
    return true;
}

上述有几个点需要说明

  1. BBR是什么?负载怎么获取的?
  2. Constants.ENTRY_NODE中的指标是什么存储进去的?
  3. CPU又是怎么获取的
BBR算法

BBR (Bottleneck Bandwidth and Round-trip propagation time) 是 Google 开发的一种拥塞控制算法,主要用于解决网络拥塞问题。下面我们将上面的代码进行拆解下:

  • 首先检查当前线程数是否大于 1,如果不是,则直接返回 true,表示通过 BBR 检查。
  • 如果当前线程数大于 1,那么检查当前线程数是否大于 (Constants.ENTRY_NODE.maxSuccessQps() * Constants.ENTRY_NODE.minRt() / 1000)
    • maxSuccessQps() 是每秒最大成功请求数
    • minRt() 是最小响应时间
    • 如果当前线程数大于这个计算值,那么返回 false,表示未通过 BBR 检查。否则,返回 true

用通俗的语言解释:检查当前线程数是否大于(每秒最大成功请求数 * 最小响应时间 / 1000),如果大于这个值,说明系统可能出现拥塞,返回 false,否则返回 true

举个例子,假设 currentThread 为 5,maxSuccessQps() 为 10,minRt() 为 200。那么计算值为 (10 * 200) / 1000 = 2。因为 currentThread 大于计算值,所以返回 false,表示未通过 BBR 检查。

checkBbr 方法的目的是在系统负载较高的情况下,通过限制并行线程数来防止系统过载

Constants.ENTRY_NODE相关说明

其实Constants.ENTRY_NODE的指标其实就是在ClusterNode中统计的, 这个ClusterNode专门用户统计某资源在全部Context内的指标

public final static ClusterNode ENTRY_NODE = new ClusterNode(TOTAL_IN_RESOURCE_NAME, ResourceTypeConstants.COMMON);

ClusterNode最终也是通过StatisticSlot统计QPS、Thread、avgRt 这三个指标, 可以看到下边类图的继承关系
在这里插入图片描述

观察一下StatisticSlot是怎么收集这个几个资源的, 下边展示核心代码, 非核心代码省略

public class StatisticSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {
        try {
            // 其它代码...
            if (resourceWrapper.getEntryType() == EntryType.IN) {
                // 通过线程数
                Constants.ENTRY_NODE.increaseThreadNum();
                // QPS通过数
                Constants.ENTRY_NODE.addPassRequest(count);
            }
        } catch (PriorityWaitException ex) {
            // 其它代码...
            if (resourceWrapper.getEntryType() == EntryType.IN) {
                // 拒绝线程数
                Constants.ENTRY_NODE.increaseThreadNum();
            }

            if (resourceWrapper.getEntryType() == EntryType.IN) {
                // 拒绝QPS数
                Constants.ENTRY_NODE.increaseBlockQps(count);
            }
        }
    }


    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        // // 获取当前时间作为响应时间
        long completeStatTime = TimeUtil.currentTimeMillis();
        // rt(此次请求所耗费 的时间)= 响应时间 - 开始时间
        long rt = completeStatTime - context.getCurEntry().getCreateTimestamp();

        // 如果是请求类型是 IN
        if (resourceWrapper.getEntryType() == EntryType.IN) {
            // 则记录 rt 到 ClusterNode
            recordCompleteFor(Constants.ENTRY_NODE, count, rt, error);
        }
    }
}

可以看到上边代码判断流量类型为 EntryType.IN, 才调用 Constants.ENTRY_NODE相关的方法统计QPS、Thread、avgRt

补充说明, 记录的开始时间并不是在StatisticSlot的入口方法entry(), 而是初始化资源的时

因为StatisticSlot已经是责任链的第三个 Slot 了,前面已经经过一些Slot和其他逻辑

public Entry(ResourceWrapper resourceWrapper, int count, Object[] args) {
    this.resourceWrapper = resourceWrapper;
    // 记录开始时间
    this.createTimestamp = TimeUtil.currentTimeMillis();
    this.count = count;
    this.args = args;
}
CPU相关指标
获取

Java提供了与之对应的API供我们获取CPU指标, sentinel直接在这个基础上进行了封装, 代码位于com.alibaba.csp.sentinel.slots.system.SystemStatusListener#run, 这个工具类可以改造为我们所用

public class SystemStatusListener implements Runnable {

    volatile double currentLoad = -1;
    volatile double currentCpuUsage = -1;


    volatile long processCpuTime = 0;
    volatile long processUpTime = 0;

    /*
    通过JMX获取操作系统的系统负载、CPU使用率等指标信息,并计算当前进程的CPU使用率。如果系统负载超过预设阈值,则记录系统状态日志
    */
    @Override
    public void run() {
        try {
            // 获取操作系统的MXBean实例
            OperatingSystemMXBean osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);
            // 获取系统平均负载值
            currentLoad = osBean.getSystemLoadAverage();

          	// 获取系统CPU使用率, 0.0代表所有CPU完全空闲,1.0代表所有CPU一直在满负荷运行
            double systemCpuUsage = osBean.getSystemCpuLoad();

            
            RuntimeMXBean runtimeBean = ManagementFactory.getPlatformMXBean(RuntimeMXBean.class);
            // 获取当前进程的CPU时间(以纳秒为单位)
            long newProcessCpuTime = osBean.getProcessCpuTime();
            // 获取当前Java虚拟机的运行时间(以毫秒为单位)
            long newProcessUpTime = runtimeBean.getUptime();
            // 获取可用的CPU核心数量
            int cpuCores = osBean.getAvailableProcessors();
            
            // 计算前后两次采集之间进程CPU时间的差值,并转换成毫秒
            long processCpuTimeDiffInMs = TimeUnit.NANOSECONDS
                    .toMillis(newProcessCpuTime - processCpuTime);
            // 计算运行时间的差值
            long processUpTimeDiffInMs = newProcessUpTime - processUpTime;
            // 将CPU时间差除以运行时间差,然后除以可用CPU核心数。这样得到的结果是每个CPU核心上的平均进程CPU使用率
            double processCpuUsage = (double) processCpuTimeDiffInMs / processUpTimeDiffInMs / cpuCores;
            
            // 更新全局变量存储最新的进程CPU时间和运行时间,以便下一次循环计算时使用
            processCpuTime = newProcessCpuTime;
            processUpTime = newProcessUpTime;
			
            // 将计算得到的进程CPU使用率与系统CPU使用率进行比较,取较大者作为当前CPU使用率
            currentCpuUsage = Math.max(processCpuUsage, systemCpuUsage);
            // 如果当前系统负载(currentLoad)大于预先设定的阈值(SystemRuleManager
            if (currentLoad > SystemRuleManager.getSystemLoadThreshold()) {
                // 调用writeSystemStatusLog()方法,将系统过载信息写入日志中
                writeSystemStatusLog();
            }
        } catch (Throwable e) {
            RecordLog.warn("[SystemStatusListener] Failed to get system metrics from JMX", e);
        }
    }
}
获取频率
public final class SystemRuleManager {
    
    // 这种线程池的创建方式值的学习,因为使用了NamedThreadFactory,将线程池里的线程做到见名知意
    private final static ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, new NamedThreadFactory("sentinel-system-status-record-task", true));
    
    static {
        // 1s 执行一次
        scheduler.scheduleAtFixedRate(new SystemStatusListener(), 0, 1, TimeUnit.SECONDS);
    }
}

参考资料

通关 Sentinel 流量治理框架 - 编程界的小學生

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/467795.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

unity发布安卓获取读取权限

一、Player Settings 设置 Player Settings>Player>Other Settings> Android > Write Permission > External (SDCard). 二、代码 using System.Collections; using System.Collections.Generic; using System.IO; using UnityEngine; using UnityEngine.Andr…

配置LVS NAT模式

配置LVS NAT模式 环境准备 client1&#xff1a;eth0->192.168.88.10&#xff0c;网关192.168.88.5lvs1: eth0 -> 192.168.88.5&#xff1b;eth1->192.168.99.5web1&#xff1a;eth1->192.168.99.100&#xff1b;网关192.168.99.5web2&#xff1a;eth1->192.168…

【深度学习实践】面部表情识别,深度学习分类模型,mmpretrain用于分类的实用教程,多任务网络头

文章目录 数据集数据集的进一步处理转换training.csv转换validation.csv 剔除无法使用的图片数据选择mmpretrain框架来训练配置四个文件改写base model改写base datasetsschedulesdefault_runtime 总配置开始训练训练分析考虑在网络上增加facial_landmarks回归head考虑是否可以…

力扣-1351 统计有序矩阵中的负数

给你一个 m * n 的矩阵 grid&#xff0c;矩阵中的元素无论是按行还是按列&#xff0c;都以非严格递减顺序排列。 请你统计并返回 grid 中 负数 的数目。 示例 1&#xff1a; 输入&#xff1a;grid [[4,3,2,-1],[3,2,1,-1],[1,1,-1,-2],[-1,-1,-2,-3]] 输出&#xff1a;8 解释&…

基于stable diffusion的IP海报生成

【AIGC】只要10秒&#xff0c;AI生成IP海报&#xff0c;解放双手&#xff01;&#xff01;&#xff01;在AIGC市场发展的趋势下&#xff0c;如何帮助设计工作者解放双手。本文将从图像生成方向切入&#xff0c;帮助大家体系化的学习Stable diffusion的使用&#xff0c;完成自有…

游戏发行商的重要意义:武汉灰京文化的角色和任务

游戏发行商在游戏产业中扮演着重要的角色&#xff0c;他们不仅是游戏内容的传播者&#xff0c;还肩负着众多重要任务。武汉灰京文化作为一家游戏发行商&#xff0c;具有重要意义&#xff0c;他们在测试、本地化、版本移植、分销平台关系、品牌战略制定、法务支持等方面承担着多…

【异质集成】高k复杂氧化物栅介质在GaN HEMTs上的异质集成

COMMUNICATIONS ENGINEERING | (2024) 3:15论文阅读。 文章讨论了高k复杂氧化物栅介质在宽带隙高电子迁移率晶体管&#xff08;HEMTs&#xff09;上的异质集成。 其核心内容的总结如下&#xff1a; 研究背景&#xff1a; 异质集成不同晶体材料因其在高性能多功能电子和光子…

mybatis-plus 的saveBatch性能分析

Mybatis-Plus 的批量保存saveBatch 性能分析 目录 Mybatis-Plus 的批量保存saveBatch 性能分析背景批量保存的使用方案循环插入使用PreparedStatement 预编译优点&#xff1a;缺点&#xff1a; Mybatis-Plus 的saveBatchMybatis-Plus实现真正的批量插入自定义sql注入器定义通用…

B002-springcloud alibaba 微服务环境搭建

目录 创建父工程创建基础模块创建用户微服务创建商品微服务创建订单微服务微服务调用 创建父工程 新建项目springcloud-alibaba&#xff0c;本工程不需要写代码&#xff0c;删除src 导包 <parent><groupId>org.springframework.boot</groupId><artifact…

语音识别:whisper部署服务器(远程访问,语音实时识别文字)

Whisper是OpenAI于2022年发布的一个开源深度学习模型&#xff0c;专门用于语音识别任务。它能够将音频转换成文字&#xff0c;支持多种语言的识别&#xff0c;包括但不限于英语、中文、西班牙语等。Whisper模型的特点是它在多种不同的音频条件下&#xff08;如不同的背景噪声水…

CentOS7使用Docker部署.net Webapi

1 准备WebApi项目 对于已存在的WebApi项目&#xff0c;需要添加Docker支持&#xff1b; 编码时&#xff0c;先设置好项目需要的端口号&#xff1a;program.cs中&#xff0c;app.Run("http://*:8000");设置端口为&#xff1a;8000在VS中&#xff0c;选中项目&#xf…

VSCode + PicGo + Github 实现markdown图床管理

目录 PicGo客户端VSvode插件 PicGo客户端 PicGo 是一个图片上传管理工具 官网&#xff1a;https://molunerfinn.com/PicGo/ github图传使用说明&#xff1a;https://picgo.github.io/PicGo-Doc/zh/guide/config.html#GitHub图床 步骤&#xff1a; 1、创建一个github公开仓库…

ELK集群实战

1、 Elasticsearch集群部署 服务器 安装软件主机名IP地址系统版本配置ElasticsearchElk10.12.153.180centos7.5.18042核4GElasticsearchEs110.12.153.178centos7.5.18042核4GElasticsearchEs210.12.153.179centos7.5.18042核4G 2、创建运行的ES普通用户 3、上传es的数据包 …

基于Spring Boot的研究生志愿填报辅助系统

摘 要 二十一世纪我们的社会进入了信息时代&#xff0c;信息管理系统的建立&#xff0c;大大提高了人们信息化水平。传统的管理方式对时间、地点的限制太多&#xff0c;而在线管理系统刚好能满足这些需求&#xff0c;在线管理系统突破了传统管理方式的局限性。于是本文针对这一…

软件杯 深度学习 python opencv 火焰检测识别 火灾检测

文章目录 0 前言1 基于YOLO的火焰检测与识别2 课题背景3 卷积神经网络3.1 卷积层3.2 池化层3.3 激活函数&#xff1a;3.4 全连接层3.5 使用tensorflow中keras模块实现卷积神经网络 4 YOLOV54.1 网络架构图4.2 输入端4.3 基准网络4.4 Neck网络4.5 Head输出层 5 数据集准备5.1 数…

Elasticsearch数据存储优化方案

优化Elasticsearch数据存储有助于提升系统性能、降低成本、提高数据查询效率以及增强系统的稳定性和可靠性。通常我们再优化Elasticsearch数据存储会遇到一些问题&#xff0c;导致项目卡壳。以下是优化Elasticsearch数据存储的一些重要作用&#xff1a; 1、问题背景 在某些场景…

记录对NSIS的一些微调 实现Electron安装包美化

利洽科技-nsNiuniuSkinUI - NSIS 实现了electron 的安装包美化&#xff0c;免费&#xff0c;便捷。 下面我整理了一些关于它的微调&#xff0c;使其安装卸载更加简单快捷。 1. 默认展示安装路径部分 &#xff08;1&#xff09;将moreconfiginfo标签visible 设置为 true&#…

每周编辑精选|微软开源 Orca-Math 高质量数学数据集、清华大学研究团队发布条件去噪扩散模型 SPDiff...

Orca-Math 是微软研究院发布的数学推理模型&#xff0c;该模型展示了较小的专业模型在特定领域的价值&#xff0c;它们可以匹配甚至超越更大模型的性能。微软近期开源了用于训练 Orca-Math 的 Orca-Math-200K 数学单词问题数据集&#xff0c;现已在 hyper.ai 官网提供下载&…

webpack5零基础入门-11处理html资源

1.目的 主要是为了自动引入打包后的js与css资源&#xff0c;避免手动引入 2.安装相关包 npm install --save-dev html-webpack-plugin 3.引入插件 const HtmlWebpackPlugin require(html-webpack-plugin); 4.添加插件&#xff08;通过new方法调用&#xff09; /**插件 *…

使用 Boot Camp 助理查明您的 Mac 需不需要 Windows 安装介质

使用 Boot Camp 助理查明您的 Mac 需不需要 Windows 安装介质 当前的 Mac 机型无需介质即可安装 Windows&#xff0c;也就是说&#xff0c;您不需要用到外置驱动器。较早的 Mac 机型需要用到 USB 驱动器或光盘驱动器。使用 Boot Camp 助理可查明您需要用到什么。 Boot Camp 助…