CUDA学习笔记(十五)Stream and Event

news2024/11/18 6:44:05

Stream

一般来说,cuda c并行性表现在下面两个层面上:

  • Kernel level
  • Grid level

到目前为止,我们讨论的一直是kernel level的,也就是一个kernel或者一个task由许多thread并行的执行在GPU上。Stream的概念是相对于后者来说的,Grid level是指多个kernel在一个device上同时执行。

Stream和event简介

Cuda stream是指一堆异步的cuda操作,他们按照host代码调用的顺序执行在device上。Stream维护了这些操作的顺序,并在所有预处理完成后允许这些操作进入工作队列,同时也可以对这些操作进行一些查询操作。这些操作包括host到device的数据传输,launch kernel以及其他的host发起由device执行的动作。这些操作的执行总是异步的,cuda runtime会决定这些操作合适的执行时机。我们则可以使用相应的cuda api来保证所取得结果是在所有操作完成后获得的。同一个stream里的操作有严格的执行顺序,不同的stream则没有此限制。

由于不同stream的操作是异步执行的,就可以利用相互之间的协调来充分发挥资源的利用率。典型的cuda编程模式我们已经熟知了:

  • 将输入数据从host转移到device
  • 在device上执行kernel
  • 将结果从device上转移回host

在许多情况下,花费在执行kernel上的时间要比传输数据多得多,所以很容易想到将cpu和gpu之间传输数据时间隐藏在其他kernel执行过程中,我们可以将数据传输和kernel执行放在不同的stream中来实现此功能。Stream可以用来实现pipeline和双buffer(front-back)渲染。

Cuda API可分为同步和异步两类,同步函数会阻塞host端的线程执行,异步函数会立刻将控制权返还给host从而继续执行之后的动作。异步函数和stream是grid level并行的两个基石。

从软件角度来看,不同stream中的不同操作可以并行执行,但是硬件角度却不一定如此。这依赖于PCIe链接或者每个SM可获得的资源,不同的stream仍然需要等待别的stream来完成执行。下面会简单介绍在不同CC版本下,stream在device上的行为。

Cuda Streams

所有的cuda操作(包括kernel执行和数据传输)都显式或隐式的运行在stream中,stream也就两种类型,分别是:

  • 隐式声明stream(NULL stream)
  • 显示声明stream(non-NULL stream)

默认情况下是NULL stream,在之前未涉及到stream的博文中,都是该类型。如果显式的声明一个stream就是non-NULL stream了。

异步且基于stream的kernel执行和数据传输能够实现以下几种类型的并行:

  • Host运算操作和device运算操作并行
  • Host运算操作和host到device的数据传输并行
  • Host到device的数据传输和device运算操作并行
  • Device内的运算并行

下面代码是之前常见的使用形式,默认使用NULL stream:

cudaMemcpy(..., cudaMemcpyHostToDevice);
kernel<<<grid, block>>>(...);
cudaMemcpy(..., cudaMemcpyDeviceToHost);

从device角度看,所有者三个操作都是使用的默认stream,并且按照代码从上到下的顺序依次执行,device本身是不知道其他的host操作怎样执行的。从host角度来看,数据传输都是同步的并且会一直等待,直到操作完成。不过不同于数据传输,Kernel的launch是异步的,host差不多立刻就能重新得到控制权,不用管kernel是否执行完毕,从而进行下一步动作。很明显,这种异步行为有助于重叠device和host之间的运算时间。

上文内容在之前博文都有涉及,这里特别说明的是数据传输,它也是可以异步执行的,这就用到了本次讲的stream,我们必须显示的声明一个stream来分派它的执行。下面版本是异步版本的cudaMemcpy:

cudaError_t cudaMemcpyAsync(void* dst, const void* src, size_t count,cudaMemcpyKind kind, cudaStream_t stream = 0);

注意新增加的最后一个参数。这样,在host issue了这个函数给device执行后,控制权可以立刻返还给host。上面代码使用了默认stream,如果要声明一个新的stream则使用下面的API定义一个:

cudaError_t cudaStreamCreate(cudaStream_t* pStream);

这样就定义了一个可以使用在cuda异步API函数中stream。使用该函数的一个比较常见的错误,或者说容易引起混乱的地方是,这个函数返回的error code可能是上一次调用异步函数产生的。也就是说,函数返回error并不是调用该函数产生error的必要条件。

当执行一次异步数据传输时,我们必须使用pinned(或者non-pageable)memory。Pinned memory的分配如下,具体请参见前面博文:

cudaError_t cudaMallocHost(void **ptr, size_t size);
cudaError_t cudaHostAlloc(void **pHost, size_t size, unsigned int flags);

通过在将该内存pin到host的虚拟内存上,就可以将该memory的物理位置强制分配到CPU内存中以便使之在整个程序生命周期中保持不变。否则的话,操作系统可能会在任意时刻改变该host端的虚拟内存对应的物理地址。假设异步数据传输函数没有使用pinned host memory的话,操作系统就可能将数据从一块物理空间移动到另一块物理空间(因为是异步的,CPU在执行其他的动作就可能影响这块数据),而此时cuda runtime正在执行数据的传输,这会导致不确定的行为。

在执行kernel时要想设置stream的话,也是很简单的,同样只要加一个stream参数就好:

kernel_name<<<grid, block, sharedMemSize, stream>>>(argument list);
// 非默认的stream声明
cudaStream_t stream;
// 初始化
cudaStreamCreate(&stream);
// 资源释放
cudaError_t cudaStreamDestroy(cudaStream_t stream);

当执行资源释放的时候,如果仍然有stream的工作没干完,那么虽然该函数仍然会立刻返回,但是相关的工作做完后,这些资源才会自动的释放掉。

由于所有stram的执行都是异步的,就需要一些API在必要的时候做同步操作:

cudaError_t cudaStreamSynchronize(cudaStream_t stream);
cudaError_t cudaStreamQuery(cudaStream_t stream);

第一个会强制host阻塞等待,直至stream中所有操作完成为止;第二个会检查stream中的操作是否全部完成,即使有操作没完成也不会阻塞host。如果所有操作都完成了,则返回cudaSuccess,否则返回cudaErrorNotReady。

下面看一下一个代码片段来帮助理解:

复制代码

for (int i = 0; i < nStreams; i++) {
    int offset = i * bytesPerStream;
    cudaMemcpyAsync(&d_a[offset], &a[offset], bytePerStream, streams[i]);
    kernel<<grid, block, 0, streams[i]>>(&d_a[offset]);
    cudaMemcpyAsync(&a[offset], &d_a[offset], bytesPerStream, streams[i]);
}

for (int i = 0; i < nStreams; i++) {
    cudaStreamSynchronize(streams[i]);
}

复制代码

该段代码使用了三个stream,数据传输和kernel运算都被分配在了这几个并发的stream中。

 

上图就跟流水线一样差不多的道理,不多说。需要注意的是,上图中数据传输的操作并不是并行执行的,即使他们是在不同的stream中。按惯例,这种情况肯定就是硬件资源的锅了,硬件资源就那么些,软件层面做的优化无非就是尽量让所有硬件资源一刻不停的被利用起来(万恶的资本主义,嗯……),而这里就是PCIe卡了瓶颈。当然从编程角度来看,这些操作依然是相互独立的,只是他们要共享硬件资源,就不得不是串行的。有两个PCIe就可以重叠这两次数据传输操作,不过也是要保证不同的stream和不同的传输方向。

最大并发kernel数目是依赖于device本身的,Fermi支持16路并行,Kepler是32。并行数是受限于shared memory,寄存器等device资源。

Stream Scheduling

概念上来说,所有stream是同时运行的。但是,事实上通常并非如此。

False Dependencies

尽管Fermi最高支持16路并行,但是在物理上,所有stream是被塞进硬件上唯一一个工作队列来调度的,当选中一个grid来执行时,runtime会查看task的依赖关系,如果当前task依赖前面的task,该task就会阻塞,由于只有一个队列,后面的都会跟着等待,即使后面的task是别的stream上的任务。就如下图所示:

 

C和P以及R和X是可以并行的,因为他们在不同的stream中,但是ABC,PQR以及XYZ却不行,比如,在B没完成之前,C和P都在等待。

Hyper-Q

伪依赖的情况在Kepler系列里得到了解决,采用的一种叫Hyper-Q的技术,简单粗暴的理解就是,既然工作队列不够用,那就增加好了,于是Kepler上出现了32个工作队列。该技术也实现了TPC上可以同时运行compute和graphic的应用。当然,如果超过32个stream被创建了,依然会出现伪依赖的情况。

 

Stream Priorities

对于CC3.5及以上版本,stream可以有优先级的属性:

cudaError_t cudaStreamCreateWithPriority(cudaStream_t* pStream, unsigned int flags, int priority);

该函数创建一个stream,赋予priority的优先级,高优先级的grid可以抢占低优先级执行。不过优先级属性只对kernel有效,对数据传输无效。此外,如果设置的优先级超出了可设置范围,则会自动设置成最高或者最低。有效可设置范围可用下列函数查询:

cudaError_t cudaDeviceGetStreamPriorityRange(int *leastPriority, int *greatestPriority);

顾名思义,leastPriority是下限,gretestPriority是上限。老规矩,数值较小则拥有较高优先级。如果device不支持优先级设置,则这两个值都返回0。

Cuda Events

Event是stream相关的一个重要概念,其用来标记strean执行过程的某个特定的点。其主要用途是:

  • 同步stream执行
  • 操控device运行步调

Cuda api提供了相关函数来插入event到stream中和查询该event是否完成(或者叫满足条件?)。只有当该event标记的stream位置的所有操作都被执行完毕,该event才算完成。关联到默认stream上的event则对所有的stream有效。

Creation and Destruction

// 声明
cudaEvent_t event;
// 创建
cudaError_t cudaEventCreate(cudaEvent_t* event);
// 销毁
cudaError_t cudaEventDestroy(cudaEvent_t event);

同理streeam的释放,在调用该函数的时候,如果相关操作没完成,则会在操作完成后自动释放资源。

Recording Events and Mesuring Elapsed Time

Events标记了stream执行过程中的一个点,我们就可以检查正在执行的stream中的操作是否到达该点,我们可以把event当成一个操作插入到stream中的众多操作中,当执行到该操作时,所做工作就是设置CPU的一个flag来标记表示完成。下面函数将event关联到指定stream。

cudaError_t cudaEventRecord(cudaEvent_t event, cudaStream_t stream = 0);

等待event会阻塞调用host线程,同步操作调用下面的函数:

cudaError_t cudaEventSynchronize(cudaEvent_t event);

该函数类似于cudaStreamSynchronize,只不过是等待一个event而不是整个stream执行完毕。我们同时可以使用下面的API来测试event是否完成,该函数不会阻塞host:

cudaError_t cudaEventQuery(cudaEvent_t event);

该函数类似cudaStreamQuery。此外,还有专门的API可以度量两个event之间的时间间隔:

cudaError_t cudaEventElapsedTime(float* ms, cudaEvent_t start, cudaEvent_t stop);

返回start和stop之间的时间间隔,单位是毫秒。Start和stop不必关联到同一个stream上,但是要注意,如果二者任意一个关联到了non-NULL stream上,时间间隔可能要比期望的大。这是因为cudaEventRecord是异步发生的,我们没办法保证度量出来的时间恰好就是两个event之间,所以只是想要gpu工作的时间间隔,则stop和strat都关联到默认stream就好了。

下面代码简单展示了如何使用event来度量时间:

复制代码

// create two events
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
// record start event on the default stream
cudaEventRecord(start);
// execute kernel
kernel<<<grid, block>>>(arguments);
// record stop event on the default stream
cudaEventRecord(stop);
// wait until the stop event completes
cudaEventSynchronize(stop);
// calculate the elapsed time between two events
float time;
cudaEventElapsedTime(&time, start, stop);
// clean up the two events
cudaEventDestroy(start);
cudaEventDestroy(stop);

复制代码

Stream Synchronization

由于所有non-default stream的操作对于host来说都是非阻塞的,就需要相应的同步操作。

从host的角度来看,cuda操作可以被分为两类:

  • Memory相关的操作
  • Kernel launch

Kernel launch对于host来说都是异步的,许多memory操作则是同步的,比如cudaMemcpy,但是,cuda runtime也会提供异步函数来执行memory操作。

我们已经知道Stream可以被分为同步(NULL stream)和异步(non-NULL stream)两种,同步异步是针对host来讲的,异步stream不会阻塞host的执行,而大多数同步stream则会阻塞host,不过kernel launch例外,不会阻塞host。

此外,异步stream又可以被分为阻塞和非阻塞两种,阻塞非阻塞是异步stream针对同步stream来讲的。异步stream如果是阻塞stream,那么同步stream会阻塞该异步stream中的操作。如果异步stream是非阻塞stream,那么该stream不会阻塞同步stream中的操作(有点绕……)。

阻塞和非阻塞stream

使用cudaStreamCreate创建的是阻塞stream,也就是说,该stream中执行的操作会被早先执行的同步stream阻塞。通常来说,当issue一个NULL stream时,cuda context会等待之前所有阻塞stream完成后才执行该NULL stream,当然所有阻塞stream也会等待之前的NULL stream完成才开始执行。

例如:

kernel_1<<<1, 1, 0, stream_1>>>();
kernel_2<<<1, 1>>>();
kernel_3<<<1, 1, 0, stream_2>>>();

从device角度来说,这三个kernel是串行依次执行的,当然从host角度来说,却是并行非阻塞的。除了通过cudaStreamCreate生成的阻塞stream外,我们还可以通过下面的API配置生成非阻塞stream:

cudaError_t cudaStreamCreateWithFlags(cudaStream_t* pStream, unsigned int flags);
// flag为以下两种,默认为第一种,非阻塞便是第二种。
cudaStreamDefault: default stream creation flag (blocking)
cudaStreamNonBlocking: asynchronous stream creation flag (non-blocking)

如果之前的kernel_1和kernel_3的stream被定义成第二种,就不会被阻塞。

Implicit Synchronization

Cuda有两种类型的host和device之间同步:显式和隐式。我们之前已经了解到显式同步API有:

  • cudaDeviceSynchronize
  • cudaStreamSynchronize
  • cudaEventSynchronize

这三个函数由host显式的调用,在device上执行。

隐式同步我们也了解过,比如cudaMemcpy就会隐式的同步device和host,因为该函数同步作用只是数据传输的副作用,所以称为隐式。了解这些隐式同步是很中要的,因为不经意的调用这样一个函数可能会导致性能急剧降低。

隐式同步是cuda编程中比较特殊情况,因为隐式同步行为可能会导致意外的阻塞行为,通常发生在device端。许多memory相关的操作都会影响当前device的操作,比如:

  • A page-locked host memory allocation
  • A device memory allocation
  • A device memset
  • A memory copy between two addresses on the same device
  • A modification to the L1/shared memory confi guration

Explicit Synchronization

从grid level来看显式同步方式,有如下几种:

  • Synchronizing the device
  • Synchronizing a stream
  • Synchronizing an event in a stream
  • Synchronizing across streams using an event

我们可以使用之前提到过的cudaDeviceSynchronize来同步该device上的所有操作。该函数会导致host等待所有device上的运算或者数据传输操作完成。显而易见,该函数是个heavyweight的函数,我们应该尽量减少这类函数的使用。

通过使用cudaStreamSynchronize可以使host等待特定stream中的操作全部完成或者使用非阻塞版本的cudaStreamQuery来测试是否完成。

Cuda event可以用来实现更细粒度的阻塞和同步,相关函数为cudaEventSynchronize和cudaEventSynchronize,用法类似stream相关的函数。此外,cudaStreamWaitEvent提供了一种灵活的方式来引入stream之间的依赖关系:

cudaError_t cudaStreamWaitEvent(cudaStream_t stream, cudaEvent_t event);

该函数会指定该stream等待特定的event,该event可以关联到相同或者不同的stream,对于不同stream的情况,如下图所示:

 

Stream2会等待stream1中的event完成后继续执行。

Configurable Events

Event的配置可用下面函数:

cudaError_t cudaEventCreateWithFlags(cudaEvent_t* event, unsigned int flags);
cudaEventDefault
cudaEventBlockingSync
cudaEventDisableTiming
cudaEventInterprocess

cudaEventBlockingSync说明该event会阻塞host。cudaEventSynchronize默认行为是使用CPU时钟来固定的查询event状态。使用cudaEventBlockingSync,调用线程会进入休眠,将控制权交给其他线程或者进程,直到event完成为止。但是这样会导致少量的CPU时钟浪费,也会增加event完成和唤醒线程的之间的时间消耗。

cudaEventDisableTiming指定event只能用来同步,并且不需要记录计时数据。这样扔掉记录时间戳的消耗可以提高cuudaStreamWaitEvent和cudaEventQuery的调用性能。

cudaEventInterprocess指定event可以被用来作为inter-process event。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

c语言进制的转换8进制转换2进制与2转8

c语言进制的转换之8进制转换2进制与2转8 c语言的进制的转换 c语言进制的转换之8进制转换2进制与2转8一、八四二一法则二、二进制转换八进制方法三、八进制转换二进制方法四、八进制程序打印 一、八四二一法则 二、二进制转换八进制方法 如&#xff1a;111000110101001转换成八…

爬虫使用什么库更事半功倍?

目录 一、requests库 二、BeautifulSoup库 三、Scrapy框架 四、selenium库 五、Pyppeteer库 六、Scrapy-Splash库 总结 在当今的大数据时代&#xff0c;爬虫技术已经成为了收集和处理大量数据的重要手段。而选择合适的库可以大大提高爬虫的效率和准确性。本文将介绍一些…

桶装水水厂送水小程序开发

桶装水小程序是水店水厂进行线上营销的关键工具&#xff0c;帮助打通线上线下&#xff0c;方便用户在线下单&#xff0c;也方便水店水厂做好门店管理。 桶装水小程序&#xff0c;即订水小程序&#xff0c;专为水店水厂打造&#xff0c;助力实现信息化门店管理&#xff0c;同时…

如何从零设计开发一个软件程序

前言 当参与了许多项目&#xff0c;或者见识了许多优秀的软件&#xff0c;难免心里会有一些波澜和冲动。谁又不想拥有一个自己的软件呢? 但是当自己独立开发一个软件时&#xff0c;发现挺难的,不知道该如何下手。 结合自己开发软件的心得进行描述&#xff0c;可能或多或少与你…

2003-2020年全国各地级市金融发展水平测算数据(含原始数据和具体测算过程)

2003-2020年全国各地级市金融发展水平测算数据 1、时间&#xff1a;2003-2020年 2、来源&#xff1a;城市统计年鉴 3、指标&#xff1a;年末金融机构存款余额、金融机构贷款余额、GDP、金融发展水平 4、范围&#xff1a;300个地级市 5、计算公式&#xff1a;金融发展水平&…

小团队管理的艺术:实现1+1>2的协同效能

&#x1f482; 个人网站:【工具大全】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 在现代工作环境中&#…

【Solidity】智能合约案例——③版权保护合约

目录 一、合约源码分析&#xff1a; 二、合约整体流程&#xff1a; 1.部署合约&#xff1a; 2.添加实体&#xff1a; 3.查询实体 4.审核版权&#xff1a; 5.版权转让 一、合约源码分析&#xff1a; Copyright.sol&#xff1a;主合约&#xff0c;定义了版权局的实体&#xff…

SpringBoot轻松实现ip解析(含源码)

文章目录 前言应用场景(1)网站访问分析(2)欺诈风险控制(3)限制服务区域(4)显示访问者来源 示例前期准备Ip2region 特性1、IP 数据管理框架2、数据去重和压缩3、极速查询响应 版本依赖导入库具体代码ConstantAddressUtils(在线解析)IpUtil(离线解析)IpController执行结果 总结源…

Redis的持久化策略:RDB与AOF(面试题详解)

文章来源&#xff1a;Redis持久化的两种方式&#xff1a;RDB与AOF&#xff08;详解&#xff09;&#xff0c;订正了一些错误 一、概述&#xff1a; RDB和AOF持久化的由来&#xff1f; 因为Redis中的数据是基于内存的&#xff0c;所以如果出现服务器断电或者服务器宕机&#xf…

Spring Boot集成SpringFox 3.0与Pageable参数处理

Springfox 3.0有多个模块&#xff0c;提供了spring boot starter&#xff0c;与Spring Boot集成时仅需引入springfox-boot-starter&#xff0c;如下&#xff1a; <dependency><groupId>io.springfox</groupId><artifactId>springfox-boot-starter<…

FL Studio 21 for Mac中文破解版百度网盘免费下载安装激活

FL Studio 21 for Mac中文破解版是Mac系统中的一款水果音乐编辑软件&#xff0c;提供多种插件&#xff0c;包括采样器、合成器和效果器&#xff0c;可编辑不同风格的音乐作品&#xff0c;Pattern/Song双模式&#xff0c;可兼容第三方插件和音效包&#xff0c;为您的创意插上翅膀…

ElementType枚举类

作用 可用于注解Target指定作用位置&#xff0c;例如&#xff1a;在注释类上声明作用于注解 种类 TYPE类、接口&#xff08;包括注释类型&#xff09;或枚举声明FIELD字段声明&#xff08;包括枚举常量METHOD方法声明PARAMETER形式化参数说明CONSTRUCTOR构造函数说明LOCAL_VAR…

人工智能站队将再添一名大将,苹果明年或将推出AppleGPT

KlipC报道&#xff1a;据外媒报道&#xff0c;苹果公司计划每年投资 10 亿美元&#xff0c;将生成式人工智能&#xff08;AI&#xff09;整合到其产品线中。据知情人士透露苹果计划最早在2024年底开始在iPhone和iPad上融入生成式人工智能技术。 KlipC的合伙人Andi D表示&#x…

第四章 文件管理 二、文件的逻辑结构

目录 一、概括 二、无结构文件 1、定义&#xff1a; 三、有结构文件 1、定义&#xff1a; 2、逻辑结构 &#xff08;1&#xff09;顺序文件: 按存储结构分类&#xff1a; 按关键字分类&#xff1a; 总结&#xff1a; 3、索引结构 4、索引顺序结构 &#xff08;1&am…

CentOS7.9+Kubernetes1.28.3+Docker24.0.6高可用集群二进制部署

CentOS7.9Kubernetes1.28.3Docker24.0.6高可用集群二进制部署 查看版本关系 ## 从kubernetes-server-linux-amd64.tar.gz解压后有kubeadm ]# ./kubeadm config images list W1022 20:06:05.647976 29233 version.go:104] could not fetch a Kubernetes version from the in…

Unity3D 基础——鼠标悬停更改物体颜色,移走恢复

方法介绍 【unity学习笔记】OnMouseEnter、OnMouseOver、OnMouseExit_unity onmouseover_一白梦人的博客-CSDN博客https://blog.csdn.net/a1208498468/article/details/117856445 GetComponent()详解_getcomponet<> 动态名称-CSDN博客https://blog.csdn.net/kaixindrag…

飞管飞控系统仿真应用探究与浅析

数字孪生技术是对真实物理实体的虚拟映射与数字化信息的应用再造&#xff0c;因其在产品生产制造与技术运用过程中&#xff0c;可将物理世界和数字世界进行实时交汇与良好互动的特性越来越受到普遍关注与广泛应用。据统计&#xff0c;2021年全球数字孪生市场规模为约500亿元&am…

【C++技能树】Lambda表达式

Halo&#xff0c;这里是Ppeua。平时主要更新C&#xff0c;数据结构算法&#xff0c;Linux与ROS…感兴趣就关注我bua&#xff01; 文章目录 0. Lambda表达式简介1. Lambda表达式2. Lambda表达式语法 0. Lambda表达式简介 在C98及之前,想要对sort进行自定义排序,或者对自定义类…

手把手教你如何重装win10系统,自己动手安装系统其实很简单

笔者在这里写一个详细点的系统重装教程。手把手教大家如何从零开始重装win10系统。因为是写给新手来看的&#xff0c;会尽力介绍的详细一些。 文章较长&#xff0c;大家不用被吓到。简化一下具体步骤只有几步。顺利话一个小时内就可以安装好。我列了个目录&#xff1a; 一、重…

LVS负载均衡及LVS-NAT模式

一、集群概述 1.1 集群的背景 集群定义&#xff1a;为解决某个特定问题将多个计算机组合起来形成一个单系统 集群目的&#xff1a;为了解决系统的性能瓶颈 集群发展历史&#xff1a; 垂直扩展&#xff1a;向上扩展&#xff0c;增加单个机器的性能&#xff0c;即升级硬件 水…