Spark-ShuffleWriter-UnsafeShuffleWriter

news2024/11/14 15:02:48

一、上下文

《Spark-ShuffleWriter》中对ShuffleWriter的获取、分类和写入做了简单的分析,下面我们对其中的UnsafeShuffleWriter做更详细的学习

二、构建UnsafeShuffleWriter

  public UnsafeShuffleWriter(
      BlockManager blockManager,
      TaskMemoryManager memoryManager,
      SerializedShuffleHandle<K, V> handle,
      long mapId,
      TaskContext taskContext,
      SparkConf sparkConf,
      ShuffleWriteMetricsReporter writeMetrics,
      ShuffleExecutorComponents shuffleExecutorComponents) throws SparkException {
    final int numPartitions = handle.dependency().partitioner().numPartitions();
    //如果分区数 大于 2的24次方(16777215) 会直接报异常
    if (numPartitions > SortShuffleManager.MAX_SHUFFLE_OUTPUT_PARTITIONS_FOR_SERIALIZED_MODE()) {
      throw new IllegalArgumentException(
        "UnsafeShuffleWriter can only be used for shuffles with at most " +
        SortShuffleManager.MAX_SHUFFLE_OUTPUT_PARTITIONS_FOR_SERIALIZED_MODE() +
        " reduce partitions");
    }
    this.blockManager = blockManager;
    this.memoryManager = memoryManager;
    this.mapId = mapId;
    final ShuffleDependency<K, V, V> dep = handle.dependency();
    this.shuffleId = dep.shuffleId();
    this.serializer = dep.serializer().newInstance();
    this.partitioner = dep.partitioner();
    this.writeMetrics = writeMetrics;
    this.shuffleExecutorComponents = shuffleExecutorComponents;
    this.taskContext = taskContext;
    this.sparkConf = sparkConf;
    //默认开启零拷贝
    this.transferToEnabled = sparkConf.getBoolean("spark.file.transferTo", true);
    //spark.shuffle.sort.initialBufferSize  默认值 4096
    //排序缓冲区大小
    this.initialSortBufferSize =
      (int) (long) sparkConf.get(package$.MODULE$.SHUFFLE_SORT_INIT_BUFFER_SIZE());
    //spark.shuffle.file.buffer  默认值     32k
    //每个随机文件输出流的内存缓冲区大小,它减少了在创建中间shuffle文件时进行的磁盘查找和系统调用的数量
    this.inputBufferSizeInBytes =
      (int) (long) sparkConf.get(package$.MODULE$.SHUFFLE_FILE_BUFFER_SIZE()) * 1024;
    open();
  }

  static final int DEFAULT_INITIAL_SER_BUFFER_SIZE = 1024 * 1024;

  private void open() throws SparkException {
    assert (sorter == null);
    //初始化排序器  专门用于基于排序的Shuffle
    //将记录数据添加到数据页,当所有记录都已插入时或当达到当前线程的shuffle内存限制时
    //使用ShuffleInemorySorter将内存中的数据按照分区ID进行排序
    //然后将排序后的记录作为序列化的压缩流写入单个或多个输出文件
    //这与 SortShuffleWriter 中使用的 ExternalSorter 不同 
    //此排序器不会合并其溢出文件。具体的合并时由UnsafeShuffleWriter做的(使用一个专门的合并过程来避免额外的序列化/反序列化)
    sorter = new ShuffleExternalSorter(
      memoryManager,
      blockManager,
      taskContext,
      initialSortBufferSize,
      partitioner.numPartitions(),
      sparkConf,
      writeMetrics);
    //默认serBuffer  = 1M
    //MyByteArrayOutputStream 是 ByteArrayOutputStream 的子类
    //缓冲区在写入数据时会自动增长 
    //此类中的方法可以在流关闭后调用,而不会生成IOException。
    serBuffer = new MyByteArrayOutputStream(DEFAULT_INITIAL_SER_BUFFER_SIZE);
    //初始化序列化输出流
    serOutputStream = serializer.serializeStream(serBuffer);
  }

三、将数据插入排序器

1、将单条数据序列化并写入缓存

while (records.hasNext()) {
  //将该ShuffleMapTask中迭代器中的数据一条一条放入排序器
  insertRecordIntoSorter(records.next());
}

void insertRecordIntoSorter(Product2<K, V> record) throws IOException {
  assert(sorter != null);
  final K key = record._1();
  //计算分区
  final int partitionId = partitioner.getPartition(key);
  //将此字节数组输出流的计数字段重置为零,以便丢弃输出流中当前累积的所有输出。输出流可以再次使用,重用已分配的缓冲区空间。
  serBuffer.reset();
  //将kv数据写入serBuffer
  serOutputStream.writeKey(key, OBJECT_CLASS_TAG);
  serOutputStream.writeValue(record._2(), OBJECT_CLASS_TAG);
  serOutputStream.flush();

  //获取serBuffer中的数据长度
  final int serializedRecordSize = serBuffer.size();
  assert (serializedRecordSize > 0);

  //向shuffle sorter写入一条数据
  //Platform.BYTE_ARRAY_OFFSET = 16 为 字节数组在内存中存放时的长度  
  //最终会调用 JDK的 native 方法 即 Unsafe的arrayBaseOffset()
  sorter.insertRecord(
    serBuffer.getBuf(), Platform.BYTE_ARRAY_OFFSET, serializedRecordSize, partitionId);
}

2、构建ShuffleExternalSorter

final class ShuffleExternalSorter extends MemoryConsumer implements ShuffleChecksumSupport {

  static final int DISK_WRITE_BUFFER_SIZE = 1024 * 1024;

  private final int numPartitions;
  private final TaskMemoryManager taskMemoryManager;
  private final BlockManager blockManager;
  private final TaskContext taskContext;
  private final ShuffleWriteMetricsReporter writeMetrics;

  //当内存中有这么多元素时,强制溢写
  private final int numElementsForSpillThreshold;

  //使用DiskBlockObjectWriter溢写使用的缓冲区大小
  private final int fileBufferSizeBytes;

  //将排序后的记录写入磁盘文件时使用的缓冲区大小
  private final int diskWriteBufferSize;

  //保存正在排序的记录的内存页。
  //溢写时,此列表中的页面会被释放回收
  //如果在TaskMemoryManager本身中维护了一个可重复使用的页面池,可以不释放回收
  private final LinkedList<MemoryBlock> allocatedPages = new LinkedList<>();

  private final LinkedList<SpillInfo> spills = new LinkedList<>();

  // 溢写后重置这些变量
  @Nullable private ShuffleInMemorySorter inMemSorter;
  @Nullable private MemoryBlock currentPage = null;
  private long pageCursor = -1;

  //每个分区的校验和计算器。禁用Shuffle校验和时为空。
  private final Checksum[] partitionChecksums;

  ShuffleExternalSorter(
      TaskMemoryManager memoryManager,
      BlockManager blockManager,
      TaskContext taskContext,
      int initialSize,
      int numPartitions,
      SparkConf conf,
      ShuffleWriteMetricsReporter writeMetrics) throws SparkException {
    super(memoryManager,
      (int) Math.min(PackedRecordPointer.MAXIMUM_PAGE_SIZE_BYTES, memoryManager.pageSizeBytes()),
      memoryManager.getTungstenMemoryMode());
    this.taskMemoryManager = memoryManager;
    this.blockManager = blockManager;
    this.taskContext = taskContext;
    this.numPartitions = numPartitions;
    // spark.shuffle.file.buffer 默认 32K
    //每个随机文件输出流的内存缓冲区大小 
    //这些缓冲区减少了在创建中间洗牌文件时进行的磁盘查找和系统调用的数量。
    this.fileBufferSizeBytes =
        (int) (long) conf.get(package$.MODULE$.SHUFFLE_FILE_BUFFER_SIZE()) * 1024;
    //spark.shuffle.spill.numElementsForceSpillThreshold   默认值 Integer.MAX_VALUE 
    //这意味着我们永远不会强制排序器溢写,直到我们达到一些限制,比如排序器中指针数组的最大页面大小限制。
    this.numElementsForSpillThreshold =
        (int) conf.get(package$.MODULE$.SHUFFLE_SPILL_NUM_ELEMENTS_FORCE_SPILL_THRESHOLD());
    this.writeMetrics = writeMetrics;
    //spark.shuffle.sort.useRadixSort 默认  true
    //是否使用基数排序对内存分区ID进行排序。基数排序要快得多,但在添加指针时需要额外的内存作为保留内存
    this.inMemSorter = new ShuffleInMemorySorter(
      this, initialSize, (boolean) conf.get(package$.MODULE$.SHUFFLE_SORT_USE_RADIXSORT()));
    //已经用了多少内存
    this.peakMemoryUsedBytes = getMemoryUsage();
    //spark.shuffle.spill.diskWriteBufferSize  默认 1024 * 1024 即 1M
    //将排序后的记录写入磁盘文件时使用的缓冲区大小
    this.diskWriteBufferSize =
        (int) (long) conf.get(package$.MODULE$.SHUFFLE_DISK_WRITE_BUFFER_SIZE());
    //分区校验和
    this.partitionChecksums = createPartitionChecksums(numPartitions, conf);
  }



}

3、向排序器插入数据

  public void insertRecord(Object recordBase, long recordOffset, int length, int partitionId)
    throws IOException {

    //..........省略..............

    //检查是否有足够的空间在排序指针数组中插入额外的记录,
    //如果需要额外的空间,则扩展数组。如果无法获得所需的空间,则内存中的数据将溢写到磁盘
    growPointerArrayIfNecessary();
    //当运行JVM时,其中有sun的Unsafe包可用,并且底层系统具有未对齐的访问能力  uaoSize = 4 
    //否则 uaoSize = 8
    final int uaoSize = UnsafeAlignedOffset.getUaoSize();
    //需要4或8个字节来存储记录长度
    final int required = length + uaoSize;
    //分配更多内存以插入其他记录。这将向内存管理器请求额外的内存,如果无法获得所请求的内存,则会溢写
    acquireNewPageIfNecessary(required);

    assert(currentPage != null);
    final Object base = currentPage.getBaseObject();
    //给定一个内存页和该页内的偏移量,将此地址编码为64位长。只要相应的页面未被释放,此地址将保持有效。
    final long recordAddress = taskMemoryManager.encodePageNumberAndOffset(currentPage, pageCursor);
    //将这条已经序列化并写入缓存的数据索引放到内存页中
    UnsafeAlignedOffset.putSize(base, pageCursor, length);
    //内存页游标向右移动 4个 字节
    pageCursor += uaoSize;
    //将缓存中的这条数据复制到 堆外 
    Platform.copyMemory(recordBase, recordOffset, base, pageCursor, length);
    //内存页游标 向右移动 数据长度
    pageCursor += length;
    //将记录地址和分区id打包 并 插入数组中,将来排序只排序索引即可
    //数组中只有一部分将用于存储指针,其余部分将保留为临时缓冲区用于排序。
    inMemSorter.insertRecord(recordAddress, partitionId);
  }


  private void growPointerArrayIfNecessary() throws IOException {
    assert(inMemSorter != null);
    if (!inMemSorter.hasSpaceForAnotherRecord()) {
      long used = inMemSorter.getMemoryUsage();
      LongArray array;
      try {
        // 可能引发溢写
        //调用父类即MemoryConsumer 的 allocateArray()
        array = allocateArray(used / 8 * 2);
      } catch (TooLargePageException e) {
        //.........
      }
      // 检查是否触发溢写
      if (inMemSorter.hasSpaceForAnotherRecord()) {
        freeArray(array);
      } else {
        inMemSorter.expandPointerArray(array);
      }
    }
  }

4、MemoryConsumer分配LongArray

支持溢写的TaskMemoryManager内存消耗者(仅仅支持Tungsten memory)

public abstract class MemoryConsumer {

  protected final TaskMemoryManager taskMemoryManager;

  public LongArray allocateArray(long size) {
    long required = size * 8L;
    //最终调用TaskMemoryManager 来申请page page的类型是 MemoryBlock 
    MemoryBlock page = taskMemoryManager.allocatePage(required, this);
    if (page == null || page.size() < required) {
      throwOom(page, required);
    }
    used += required;
    return new LongArray(page);
  }
}

TaskMemoryManager 

负责管理单个任务分配的内存。这里会触发Spark 的钨丝内存分配

此类中的大部分复杂性涉及将堆外地址编码为64位长。在堆外模式下,内存可以直接用64位长进行寻址。在堆上模式下,内存由基对象引用和该对象内的64位偏移量的组合来寻址。当我们想将指向数据结构的指针存储在其他结构中时,是一个问题,例如哈希映射或排序缓冲区中的记录指针。即使我们决定使用128位来寻址内存,我们也不能只存储基对象的地址,因为当堆因GC而重新组织时,它不能保证保持稳定。

相反,我们使用以下方法将记录指针编码为64位长:对于堆外模式,只存储原始地址,对于堆上模式,使用地址的高位13位存储“页码”,低位51位存储此页内的偏移量。这些页码用于索引到MemoryManager内的“页表”数组中,以检索基本对象。

这使我们能够处理8192(2的13次方)页。在堆上模式下,最大页面大小受到 long[]  数组最大大小的限制,使我们能够寻址8192*(2^31-1)*8字节,约为140TB的内存。

public class TaskMemoryManager {
  
  //最大 page 大小原则上应该是 2的51次方,但是堆上模式中受到 long [] 中可存的数据量限制,因此
  //最大 page 大小为 2的31次方 -1 * 8 byte 大概是 17G 
  public static final long MAXIMUM_PAGE_SIZE_BYTES = ((1L << 31) - 1) * 8L;

  public MemoryBlock allocatePage(long size, MemoryConsumer consumer) {
    assert(consumer != null);
    assert(consumer.getMode() == tungstenMemoryMode);
    //申请的page大小限制 最大约为 17G 
    if (size > MAXIMUM_PAGE_SIZE_BYTES) {
      throw new TooLargePageException(size);
    }

    //为内存消费者 可分配的执行内存 的大小
    long acquired = acquireExecutionMemory(size, consumer);
    if (acquired <= 0) {
      return null;
    }

    final int pageNumber;
    synchronized (this) {
      pageNumber = allocatedPages.nextClearBit(0);
      if (pageNumber >= PAGE_TABLE_SIZE) {
        releaseExecutionMemory(acquired, consumer);
        throw new IllegalStateException(
          "Have already allocated a maximum of " + PAGE_TABLE_SIZE + " pages");
      }
      allocatedPages.set(pageNumber);
    }
    MemoryBlock page = null;
    try {
      //调用钨丝内存分配器取分配内存,有两种模式:
      //    1、ON_HEAP  ->  HeapMemoryAllocator 
      //    2、OFF_HEAP ->  UnsafeMemoryAllocator
      //这也是一个调优点,你可以选择堆上分配,也可以选择堆外分配,默认堆上分配
      //我们单独用一篇博客来分析Spark 的 钨丝内存分配
      page = memoryManager.tungstenMemoryAllocator().allocate(acquired);
    } catch (OutOfMemoryError e) {
      logger.warn("Failed to allocate a page ({} bytes), try again.", acquired);
      // 实际上没有足够的内存,这意味着实际的可用内存比MemoryManager想象的要小,我们应该保留获得的内存。
      synchronized (this) {
        acquiredButNotUsed += acquired;
        allocatedPages.clear(pageNumber);
      }
      // this could trigger spilling to free some pages.
      return allocatePage(size, consumer);
    }
    page.pageNumber = pageNumber;
    pageTable[pageNumber] = page;
    if (logger.isTraceEnabled()) {
      logger.trace("Allocate page number {} ({} bytes)", pageNumber, acquired);
    }
    return page;
  }

}

5、排序器中的数据长相

final class ShuffleInMemorySorter {
   //存放数据地址和分区id的数组
   //该数组与原生JVM数组相比有以下特点:
   //    1、支持同时使用on-heap 和 off-heap
   //    2、没有绑定检查,因此在关闭断言时可能会使JVM进程崩溃
   //单个元素长度为 64 字节
   private LongArray array;

   public void insertRecord(long recordPointer, int partitionId) {
    if (!hasSpaceForAnotherRecord()) {
      throw new IllegalStateException("There is no space for new record");
    }
    array.set(pos, PackedRecordPointer.packPointer(recordPointer, partitionId));
    pos++;
   }

}

final class PackedRecordPointer {

  public static long packPointer(long recordPointer, int partitionId) {
    assert (partitionId <= MAXIMUM_PARTITION_ID);
    // 注意,如果没有字节对其,一个内存页 可以寻址 128M字节数据 也就是 2的27次方
    final long pageNumber = (recordPointer & MASK_LONG_UPPER_13_BITS) >>> 24;
    final long compressedAddress = pageNumber | (recordPointer & MASK_LONG_LOWER_27_BITS);
    // |  前 24字节   |  13 字节  -  27字节 |
    // |   分区id     |  内存页编号 - 页内偏移 |
    //这也是为什么该UnsafeShuffleWriter只能支持 2 的 24次方 个分区的原因了
    return (((long) partitionId) << 40) | compressedAddress;
  }

}

我们用图来描述下,排序器中的数据是如何存储的

四、溢写磁盘并释放内存

  void closeAndWriteOutput() throws IOException {
    assert(sorter != null);
    updatePeakMemoryUsed();
    serBuffer = null;
    serOutputStream = null;
    //SpillInfo 是由 ShuffleExternalSorter 写入的数据块的元数据
    //关闭排序器,并缓存中的数据进行排序并进行溢写
    final SpillInfo[] spills = sorter.closeAndGetSpills();
    try {
      //将零个或多个溢出文件合并在一起,根据溢出数量和IO压缩编解码器选择最快的合并策略。
      partitionLengths = mergeSpills(spills);
    } finally {
      //..............
    }
    mapStatus = MapStatus$.MODULE$.apply(
      blockManager.shuffleServerId(), partitionLengths, mapId);
  }


//-----------------------------
final class ShuffleExternalSorter extends MemoryConsumer implements ShuffleChecksumSupport {

  public SpillInfo[] closeAndGetSpills() throws IOException {
    if (inMemSorter != null) {
      // 对内存中的记录进行排序,并将排序后的记录写入磁盘文件。
      writeSortedFile(true);
      //释放内存页和数据块占用的内存
      freeMemory();
      //释放内存页指针数组结构所用的内存
      inMemSorter.free();
      inMemSorter = null;
    }
    return spills.toArray(new SpillInfo[spills.size()]);
  }

}

五、向调度器报告摘要信息

ShuffleMapTask向调度器返回的结果。包括任务存储shuffle文件的块管理器地址,以及每个reducer的输出大小,以便传递给reduce任务。

当调度器发现这个ShuffleMapTask执行完成,就会执行下一个ShuffleMapTask或者ResultTask

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

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

相关文章

利用Packet tracer搭建简单的网络并进行通信验证

利用Packet tracer搭建一个基本的网络通信&#xff1a; 1&#xff1a;创建网络组件 先去创建几个终端设备&#xff0c;然后再去创建一个交换机 布局完成的界面是&#xff1a; 2&#xff1a;连接组件 用指定的线进行连接交换机和PC机 全部连接的结果是: 3&#xff1a;配置PC机…

桥接网络设置多用户lxd容器

文章目录 前言配置宿主机网络固定内核版本安装 lxd、zfs 及 bridge-utils安装宿主机显卡驱动lxd 初始化创建容器模板安装容器显卡驱动复制容器 前言 使用桥接网络配置 lxd 有个好处&#xff0c;就是每个用户都可以在该局域网下有一个自己独立的 IP&#xff0c;该 IP 的端口可以…

崩坏星穹铁道PC端2.5版本剧情、奖励攻略 用GameViewer远程帮手机减负 随时畅玩星铁PC端

《崩坏&#xff1a;星穹铁道》2.5版本「碧羽飞黄射天狼」在9月10开启&#xff01;上半卡池有五星角色飞霄、知更鸟、卡芙卡、黑天鹅四位角色&#xff0c;还有2.5版本的新剧情&#xff0c;这一次崩铁上线送10连和 1000星琼等其他材料。由于游戏包体过大&#xff0c;不少玩家都选…

web群集--nginx实现重定向与重写操作的详细配置过程详与案例展示

文章目录 前言什么是重定向&#xff1f;重定向能做什么&#xff1f;何时需要重定向功能&#xff1f;nginx通过什么来实现重定向和重写操作的&#xff1f;nginx的重定向和重写有什么区别&#xff1f;案例展示重定向1.将所有对将所有对http://test. com 的访问重定向到http://www…

【iOS】push和present的区别

【iOS】push和present的区别 文章目录 【iOS】push和present的区别前言pushpop presentdismiss简单小demo来展示dismiss和presentdismiss多级 push和present的区别区别相同点 前言 在iOS开发中&#xff0c;我们经常性的会用到界面的一个切换的问题&#xff0c;这里我们需要理清…

眼镜超声波清洗机真的有用吗?畅销款热门清洗机测评,买前必看!

随着科技的飞速发展&#xff0c;人们的生活水平也日益提升。眼镜&#xff0c;作为我们日常生活中不可或缺的物品&#xff0c;其重要性不言而喻。然而&#xff0c;许多人往往忽视了对它的定期清洁保养。尽管市面上有专门用于清洁眼镜的布&#xff0c;但这种方法并不能完全去除镜…

CloudFlare问题与CDN问题

昨天将腾讯云的解析转移到Cloudflare中了&#xff0c;结果今天发现网站崩了&#xff0c;显示重定向次数过多&#xff0c;昨天估计是因为浏览器缓存&#xff0c;所以没有发现问题 问题一&#xff1a;强制HTTPS 当时看到CloudFlare的强制https时就想到了我的宝塔面板也开着强制h…

【机器学习】结构学习的基本概念以及基于约束的结构学习和基于评分的结构学习

引言 结构学习在机器学习中是指自动发现数据中潜在的结构或模式的过程&#xff0c;这通常涉及到确定数据的依赖关系、变量间的相互作用或者数据的组织形式。 文章目录 引言一、结构学习1.1 目标1.2 方法1.3 应用1.4 挑战1.5 工具和技术1.6 步骤1.7 总结 二、基于约束的结构学习…

2024年法国7大最佳影响力营销平台

影响力平台是一种工具&#xff0c;可以帮助企业找到有影响力的人&#xff0c;全面管理从头到尾的营销活动&#xff0c;并评估其效果。这类工具能够为广告商或影响力代理机构提供以下服务&#xff1a; 发布营销活动&#xff0c;让 有影响力的人可以申请。这就是所谓的 影响力市…

QT程序的安装包制作教程

在Windows平台上开发完qt c桌面应用程序以后&#xff0c;需要制作一个安装包&#xff0c;方便生产和刻盘交货&#xff0c;本文记录相关流程。 目录 一、安装Qt Installer Framework 二、准备可执行程序 2.1 生成Release程序 2.2 完成依赖库拷贝 三、创建安装包程序 一、…

2024icpc江西:H.Convolution(二维前缀和,卷积核)

题目 做法 我们发现&#xff0c;K中的每个元素都乘了对应原矩阵中子矩阵&#xff08;n-k1&#xff09;* (m-l1)中的每个数。我们就直接前缀和求就好了。 #include<bits/stdc.h> #define int long long using namespace std;const int N1e310; int n,m,k,l; int a[N][N…

语言中的浮点数

浮点数相比定点数或者整数&#xff0c;为了处理小数点引入了指数&#xff0c;导致小数点的位置根据不同浮点数而不同&#xff0c;故名为Floating Point Number. 一般而言&#xff0c;IEEE754标准被大部分编程语言的浮点数使用&#xff0c;它节省了浮点数的保存空间。如不然&…

思维导图模板,看完这些步骤学会制作

思维导图模板&#xff0c;思维导图是一种非常有效的信息组织工具&#xff0c;它将复杂的信息以图形化的形式展现出来&#xff0c;便于记忆和理解。无论是用于项目管理、学习笔记整理还是头脑风暴会议记录&#xff0c;思维导图都能够帮助人们更好地梳理思路&#xff0c;提高工作…

在 ClickHouse 中进行机器学习数据建模

本文字数&#xff1a;17443&#xff1b;估计阅读时间&#xff1a;44 分钟 作者&#xff1a;Dale McDiarmid 本文在公众号【ClickHouseInc】首发 本文将探索 MLOps 的世界&#xff0c;探讨如何在 ClickHouse 中对数据进行建模和转换&#xff0c;使其成为高效的特征存储&#xff…

百收网AI发帖子怎么发?

百收网AI发帖子怎么发&#xff1f; 百家号APP怎么发内容&#xff1f;没有电脑怎么在百度发内容&#xff1f;#百家号 查看视频讲解&#xff1a;&#xff08;点击链接https://v.youku.com/v_show/id_XNjQyMjc4MjU3Ng.html&#xff1a;https://v.youku.com/v_show/id_XNjQyMjc4M…

Linux云计算 |【第三阶段】PROJECT1-DAY1

主要内容&#xff1a; 服务器硬件&#xff08;RAID磁盘阵列、IDRAC远程控制卡&#xff09;、部署LNMP动态网站&#xff08;部署LNMP平台、上线Wordpress代码、创建数据库、初始化Wordpress&#xff09; 一、服务器介绍 1、什么是服务器 服务器&#xff08;Server&#xff09…

C++入门(part 3)

前言 在前文我们讲解了C的诞生与历史&#xff0c;顺便讲解一些C的小语法&#xff0c;本文会继续讲解C的基础语法知识。 1.inline(内联函数) inline是C新加入的关键字,用inline修饰的函数叫做内联函数&#xff0c;编译时C编译器会在调用的地方将函数展开&#xff0c;这样每次…

数据中台 | 数据资源管理平台介绍

01 产品概述 数据资源的盘查、集成、存储、组织、共享等全方位管理能力&#xff0c;无论对于企业的数字化转型&#xff0c;还是对企业数据资产的开发、运营、交易及入表&#xff0c;都具有极为关键的作用。今天&#xff0c;小兵就来为大家介绍我们自研数据智能平台中的核心产品…

废品回收小程序搭建,回收市场的机遇

随着经济的快速发展&#xff0c;居民生活水平普遍提高&#xff0c;产生的各类废品也在不断增加&#xff0c;为废品回收市场提供了发展基础。当下&#xff0c;在大众环保意识增加下&#xff0c;废品回收行业也将成为一个具有广阔发展前景的朝阳行业&#xff01; 目前&#xff0…

C++vector类 (带你一篇文章搞定C++中的vector类)

感谢大佬的光临各位&#xff0c;希望和大家一起进步&#xff0c;望得到你的三连&#xff0c;互三支持&#xff0c;一起进步 数据结构习题_LaNzikinh篮子的博客-CSDN博客 初阶数据结构_LaNzikinh篮子的博客-CSDN博客 收入专栏&#xff1a;C_LaNzikinh篮子的博客-CSDN博客 其他专…