Hive和Spark分区策略

1.概述

离线数据处理生态系统包含许多关键任务,最大限度的提高数据管道基础设施的稳定性和效率是至关重要的。这边博客将分享Hive和Spark分区的各种策略,以最大限度的提高数据工程生态系统的稳定性和效率。

2.内容

大多数Spark Job可以通过三个阶段来表述,即读取输入数据、使用Spark处理、保存输出数据。这意味着虽然实际数据转换主要发生在内存中,但是Job通常以大量的I/O开始和结束。使用Spark常用堆栈是使用存储在HDFS上的Hive表作为输入和输出数据存储。Hive分区有效地表示为分布式文件系统上的文件目录。理论上,尽可能多的文件写入是有意义的,但是,这个也是有代价的。HDFS不能很好的支持大量小文件,每个文件在NameNode内存中大概有150字节的开销,而HDFS的整体IOPS数量有限。文件写入中的峰值绝对会导致HDFS基础架构的某些部分产生性能瓶颈。

比如从某个历史日期到当前日期重新计算表,通常用于修复错误或者数据质量问题。在处理包含一年数据的大型数据集(比如1TB以上)时,可能会将数据分成几千个Spark分区进行处理。虽然从表面上看,这种处理方法并不是最合适的,使用动态分区并将数据结果写入按照日期分区的Hive表中将产生多大100+万个文件。

假如有一个包含3个分区的Spark任务,并且想将数据写入到包含3个分区的Hive中。在这种情况下,希望发送的是将3个文件写入到HDFS,所有数据都存储在每个分区键的单个文件中。实际发生的是将生成9个文件,并且每个文件都有1个记录。使用动态分区写入Hive时,每个Spark分区都由执行程序并行处理。处理Spark分区数据时,每次执行程序在给定Spark分区中遇到新的分区键时,它都会打开一个新文件。默认情况下,Spark对数据会使用Hash或者Round Robin分区器。当应用于任意数据时,可以假设这2中方法在整个Spark分区中相对均匀但是随机分布数据行。如下图所示:

Hive和Spark分区策略

理想情况下,目标文件大小应该大约是HDFS Block大小的倍数,默认情况下为128MB。在Hive管道中,提供了一些配置来自动将结果收集到合理大小的文件中,从开发人员的角度来看几乎是透明的,比如hive.merge.smallfiles.avgsize和hive.merge.size.per.task。但是,Spark中不存在此类功能,因此,我们需要自己开发实现,来给定一个数据集,应该写入多少文件。

2.1 基于Size的计算

理论上,这是最直接的方法,设置目标大小,估计数据的大小,然后进行划分。但是,在很多情况下,文件被写入磁盘时会进行压缩,并且其格式与存储在Java堆中的记录格式有所不同。这意味着估算写入磁盘时内存的记录大小不是一件容易的事情。

虽然可以使用Spark SizeEstimator实用程序通过内存中数据的大小进行估计,然后应用某种估计的压缩文件格式因此,但是SizeEstimator会考虑数据帧、数据集的内部消耗,以及数据的大小。总体来说,这种方式不太容易准确实现。

2.2 基于行数的计算

这种方法是设置目标行数,计算数据集的大小,然后执行除法以估计目标。我们的目标行数可以通过多种方式确定,或者通过为所有数据集选择一个静态数字,或者通过确定磁盘上单个记录的大小并执行必要的计算。哪种方式是最好取决于你的数据集数量及其复杂性。计数相对来说成本较低,但是需要在计数前缓存以避免重新计算数据集。

2.3 静态文件计数

最简单的解决方案是只要求开发人员在每个插入的基础上告诉Spark总共应该写入多少个文件,这种方式需要给开发人员一些其他方法来获得具体的数字,可以通过这种方式来替换昂贵的计算。

3.如何让Spark以合理的方式分发处理数据?

即使我们知道希望如何将文件写入磁盘,我们仍然必须让Spark以符合实际的方式生成这些文件来构建我们的分区。Spark提供了许多工具来确定数据在整个分区中的分布方式。但是,各种功能中隐藏着很多复杂性,在某些情况下,它们的含义并不明显。下面将介绍Spark提供的一些选项来控制Spark输出文件的数量。

3.1 合并

Spark Coalesce是一个特殊版本的重新分区,它只允许减少总的分区,但是不需要完全的Shuffle,因此比重新分区要快得多。它通过有效的合并分区来实现这一点。如下图所示:

Hive和Spark分区策略

Coalesce在某些情况下看起来不错,但是也有一些问题。首先,Coalesce有一个让我们难以使用的行为。以一个非常基本的Spark应用程序为例,代码如下:

csharp;gutter:true; load().map(...).filter(...).save()</p> <pre><code> 比如设置的并行度为1000,但是最终只想写入10个文件,可以设置如下: </code></pre> <p>load().map(...).filter(...).coalesce(10).save()</p> <pre><code> 但是,Spark会尽可能早的有效的将合并操作下推,因此这将执行为: </code></pre> <p>load().coalesce(10).map(...).filter(...).save()</p> <pre><code> 有效的解决这种问题的方法是在转换和合并之间强制执行,代码如下所示: </code></pre> <p>val df = load().map(...).filter(...).cache() df.count() df.coalesce(10)</p> <pre><code> 缓存是必须的,否则,你将不得不重新计算数据,这可能会重新消耗资源。然后,缓存是需要消费一定资源的,如果你的数据集无法放入内存中,或者无法腾出内存将数据有效的存储在内存中两次,那么必须使用磁盘缓存,这有其自身的局限性和显著的性能损失。 此外,正如我们看到的,通常需要执行Shuffle来获得我们想要的更复杂的数据集结果。因此,Coalesce仅适用于特定的情况: * 保证只写入1个Hive分区; * 目标文件数少于你用于处理数据的Spark分区数; * 有充足的缓存资源。 ## 3.2 简单重新分区 一个简单的重新分区,它的唯一参数是目标Spark分区计数,即df.repartition(100)。在这种情况下,使用循环分区器,这意味着唯一的保证是输出数据具有大致相同大小的Spark分区。 这种分区仅适用于以下情况的文件计数问题: * 保证只需要写入1个Hive分区; * 正在写入的文件数大于你的Spark分区数或者由于某些其他原因你无法使用合并。 ## 3.3 按列重新分区 按列重新分区接收目标Spark分区计数,以及要重新分区的列序列,例如,df.repartition(100,$"date")。这对于强制Spark将具有相同键的记录分发到同一个分区很有用。一般来说,这对许多Spark操作(如JOIN)很有用,但是理论上,它也可以解决我们的问题。 按列重新分区使用HashPartitioner,它将具有相同值的记录分配给同一个分区,实际上,它将执行以下操作: ![Hive和Spark分区策略](https://johngo-pic.oss-cn-beijing.aliyuncs.com/articles/20220812/666745-20210627163325075-1112151719.png) 但是,这种方法只有在每个分区键都可以安全的写入到一个文件时才有效。这是因为无论有多少值具有特定的Hash值,它们最终都会在同一个分区中。按列重新分区仅在你写入一个或者多个小的Hive分区时才有效。在任何其他情况下,它都没有用,因为每个Hive分区总是会得到一个文件,这仅适用于最小的数据集。 ## 3.4 按具有随机因子的列重新分区 我们可以通过添加约束的随机因子来按列修改重新分区,代码如下: </code></pre> <p>df .withColumn("rand", rand() % filesPerPartitionKey) .repartition(100, $"key", $"rand")</p> <pre><code> 理论上,只要满足以下条件,这种方法应该会产生排序良好的记录和大小相当均匀的文件: * Hive分区的大小大致相同; * 知道每个Hive分区的目标文件数并且可以在运行时对其进行编码。 但是,即使我们满足上述这些条件,还有另外一个问题:散列冲突。假设,现在正在处理一年的数据,日期作为分区的唯一键。如果每个分区需要5个文件,可以执行如下操作: </code></pre> <p>df.withColumn("rand", rand() % 5).repartition(5*365, $"date", $"rand")</p> <pre><code> 在后台,Scala将构造一个包含日期和随机因素的键,例如( </code></pre> <p>class HashPartitioner(partitions: Int) extends Partitioner { def getPartition(key: Any): Int = key match { case null => 0 case _ => Utils.nonNegativeMod(key.hashCode, numPartitions) } }</p> <pre><code> 实际上,所做的就是获取关键元组的散列,然后使用目标数量的Spark分区获取它的mod。我们可以分析一下在这种情况下我们的记录将如何实现分布,分析代码如下: <pre><span>import</span><span> java.time.LocalDate </span><span>def</span> hashCodeTuple(one: String, two: Int, mod: Int): Int =<span> { val rawMod </span>= (one, two).hashCode %<span> mod rawMod </span>+ (<span>if</span> (rawMod &lt; 0) mod <span>else</span><span> 0) } </span><span>def</span> hashCodeSeq(one: String, two: Int, mod: Int): Int =<span> { val rawMod </span>= Seq(one, two).hashCode %<span> mod rawMod </span>+ (<span>if</span> (rawMod &lt; 0) mod <span>else</span><span> 0) } </span><span>def</span> iteration(numberDS: Int, filesPerPartition: Int): (Double, Double, Double) =<span> { val hashedRandKeys </span>= (0 to numberDS - 1).map(x =&gt; LocalDate.of(2019, 1, 1<span>).plusDays(x)).flatMap( x </span>=&gt; (0 to filesPerPartition - 1).map(y =&gt; hashCodeTuple(x.toString, y, filesPerPartition*<span>numberDS)) ) hashedRandKeys.size </span>//<span> Number of unique keys, with the random factor val groupedHashedKeys </span>=<span> hashedRandKeys.groupBy(identity).view.mapValues(_.size).toSeq groupedHashedKeys.size </span>//<span> number of actual sPartitions used val sortedKeyCollisions </span>= groupedHashedKeys.filter(_._2 != 1<span>).sortBy(_._2).reverse val sortedSevereKeyCollisions </span>= groupedHashedKeys.filter(_._2 &gt; 2<span>).sortBy(_._2).reverse sortedKeyCollisions.size </span>//<span> number of sPartitions with a hashing collision </span>//<span> (collisions, occurences) val collisionCounts </span>=<span> sortedKeyCollisions.map(_._2).groupBy(identity).view.mapValues(_.size).toSeq.sortBy(_._2).reverse ( groupedHashedKeys.size.toDouble </span>/<span> hashedRandKeys.size.toDouble, sortedKeyCollisions.size.toDouble </span>/<span> groupedHashedKeys.size.toDouble, sortedSevereKeyCollisions.size.toDouble </span>/<span> groupedHashedKeys.size.toDouble ) } val results </span>=<span> Seq( iteration(</span>365, 1<span>), iteration(</span>365, 5<span>), iteration(</span>365, 10<span>), iteration(</span>365, 100<span>), iteration(</span>365 * 2, 100<span>), iteration(</span>365 * 5, 100<span>), iteration(</span>365 * 10, 100<span>) ) val avgEfficiency </span>= results.map(_._1).sum /<span> results.length val avgCollisionRate </span>= results.map(_._2).sum /<span> results.length val avgSevereCollisionRate </span>= results.map(_._3).sum /<span> results.length (avgEfficiency, avgCollisionRate, avgSevereCollisionRate) </span>// 63.2%, 42%, 12.6%</pre> 上面的脚本计算了3个数量: * 效率:非空的Spark分区与输出文件数量的比率; * 碰撞率:(date,rand)的Hash值发送冲突的Spark分区的百分比; * 严重冲突率:同上,但是此键上的冲突次数为3或者更多。 冲突很重要,因为它们意味着我们的Spark分区包含多个唯一的分区键,而我们预计每个Spark分区只有1个。分析的结果可知,我们使用了63%的执行器,并且可能会出现严重的偏差,我们将近一半的执行者正在处理比预期多2到3倍或者在某些情况下高达8倍的数据。 现在,有一个解决方法,即分区缩放。在之前示例中,输出的Spark分区数量等于预期的总文件数。如果将N个对象随机分配给N个插槽,可以预期会有多个插槽包含多个对象,并且有几个空插槽。因此,需要解决此问题,必须要降低对象与插槽的比率。 我们通过缩放输出分区计数来实现这一点,通过将我们的输出Spar分区计数乘以一个大因子,类似于: </code></pre> <p>df .withColumn("rand", rand() % 5) .repartition(5<em>365</em>SCALING_FACTOR, $"date", $"rand")

分析代码如下:

import java.time.LocalDate

def hashCodeTuple(one: String, two: Int, mod: Int): Int = {
 val rawMod = (one, two).hashCode % mod
 rawMod + (if (rawMod < 0) mod else 0)
}

def hashCodeSeq(one: String, two: Int, mod: Int): Int = {
 val rawMod = Seq(one, two).hashCode % mod
 rawMod + (if (rawMod < 0) mod else 0)
}

def iteration(numberDS: Int, filesPerPartition: Int, partitionFactor: Int = 1): (Double, Double, Double, Double) = {
  val partitionCount = filesPerPartition*numberDS * partitionFactor
  val hashedRandKeys = (0 to numberDS - 1).map(x => LocalDate.of(2019, 1, 1).plusDays(x)).flatMap(
    x => (0 to filesPerPartition - 1).map(y => hashCodeTuple(x.toString, y, partitionCount))
  )

  hashedRandKeys.size // Number of unique keys, with the random factor

  val groupedHashedKeys = hashedRandKeys.groupBy(identity).view.mapValues(_.size).toSeq

  groupedHashedKeys.size // number of unique hashes - and thus, sPartitions with > 0 records

  val sortedKeyCollisions = groupedHashedKeys.filter(_._2 != 1).sortBy(_._2).reverse

  val sortedSevereKeyCollisions = groupedHashedKeys.filter(_._2 > 2).sortBy(_._2).reverse

  sortedKeyCollisions.size // number of sPartitions with a hashing collision

  // (collisions, occurences)
  val collisionCounts = sortedKeyCollisions.map(_._2).groupBy(identity).view.mapValues(_.size).toSeq.sortBy(_._2).reverse

  (
    groupedHashedKeys.size.toDouble / partitionCount,
    groupedHashedKeys.size.toDouble / hashedRandKeys.size.toDouble,
    sortedKeyCollisions.size.toDouble / groupedHashedKeys.size.toDouble,
    sortedSevereKeyCollisions.size.toDouble / groupedHashedKeys.size.toDouble
  )
}

// With a scale factor of 1
val results = Seq(
  iteration(365, 1),
  iteration(365, 5),
  iteration(365, 10),
  iteration(365, 100),
  iteration(365 * 2, 100),
  iteration(365 * 5, 100),
  iteration(365 * 10, 100)
)

val avgEfficiency = results.map(_._2).sum / results.length // What is the ratio of executors / output files
val avgCollisionRate = results.map(_._3).sum / results.length // What is the average collision rate
val avgSevereCollisionRate = results.map(_._4).sum / results.length // What is the average collision rate where 3 or more hashes collide

(avgEfficiency, avgCollisionRate, avgSevereCollisionRate) // 63.2% Efficiency, 42% collision rate, 12.6% severe collision rate

iteration(365, 5, 2) // 37.7% partitions in-use, 77.4% Efficiency, 24.4% collision rate, 4.2% severe collision rate
iteration(365, 5, 5)
iteration(365, 5, 10)
iteration(365, 5, 100)

随着我们的比例因子接近无穷大,碰撞很快接近于0,效率接近100%。但是,这会产生另外一个问题,即大量的输出Spark分区将为空。同时这些空的Spark分区也会带来一些资源开销,增加驱动程序的内存要求,并使我们更容易受到由于错误或者意外复杂性而导致分区键空间意外大的问题。

这里的一个常见方法是在使用这种方法时不显示设置分区技术(默认并行度和缩放),如果不提供分区计数,则依赖Spark默认的spark.default.parallelism值。虽然,通常并行度自然高于总输出文件数(因此,隐式提供大于1 的缩放因子)。如果满足以下条件,这种方式依然是一种有效的方法:

  • Hive分区的文件数大致相等;
  • 可以确定平均分区文件数应该是多少;
  • 大致知道唯一分区键的总数。

在示例中,我们假设其中的许多事情都很容易知道,主要是输出Hive分区的总数和每个Hive分区所需要的文件数。无论如何,这种方法都是可行的,并且可能适用于需要用例。

3.5 按范围重新分区

按范围重新分区是一个特列,它不使用RoundRobin和Hash Partitioner,而是使用一种特殊的方法,叫做Range Partitioner。

范围分区器根据某些给定键的顺序在Spark分区之间进行拆分行,但是,它不只是全局排序,它做出的保证是:

  • 具有相同散列的所有记录将在同一个分区中结束;
  • 所有Spark分区都将有一个最小值和最大值与之关联;
  • 最小值和最大值将通过使用采样来检测关键频率和范围来确定,分区边界将根据这些估计值进行初始设置;
  • 分区的大小不能保证完全相等,它们的相等性基于样本的准确性,因此,预测的每个Spark分区的最小值和最大值,分区将根据需要增长或缩小以保证前2个条件。

总而言之,范围分区将导致Spark创建与请求的Spark分区数量相等的Bucket数量,然后它将这些Bucket映射到指定分区键的范围。例如,如果你的分区键是日期,则范围可能是(最小值2021-01-01,最大值2022-01-01)。然后,对于每条记录,将记录的分区键与存储Bucket的最小值和最大值进行比较,并相应的进行分配。

Hive和Spark分区策略

4.结束语

这篇博客就和大家分享到这里,如果大家在研究学习的过程当中有什么问题,可以加群进行讨论或发送邮件给我,我会尽我所能为您解答,与君共勉!

另外,博主出书了《Kafka并不难学》和《Hadoop大数据挖掘从入门到进阶实战》,喜欢的朋友或同学, 可以在公告栏那里点击购买链接购买博主的书进行学习,在此感谢大家的支持。关注下面公众号,根据提示,可免费获取书籍的教学视频。

Original: https://www.cnblogs.com/smartloli/p/14941166.html
Author: 哥不是小萝莉
Title: Hive和Spark分区策略

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/8878/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • express学习23-多人管理项目11邮箱地址查询信息

    express学习23-多人管理项目11邮箱地址查询信息 原创 前端歌谣2022-08-07 00:06:26©著作权 文章标签 bootstrap css html 文章分类 H…

    2022年9月4日
    0940
  • [leetcode] 371. Sum of Two Integers

    [leetcode] 371. Sum of Two Integers 原创 是念2022-08-11 22:09:16博主文章分类:C++ ©著作权 文章标签 参考文献 运算符 …

    大数据 2022年9月4日
    0200
  • hive动态分区插入实验

    实验目的 验证对分区表进行动态分区插入功能 验证是否可以使用load进行动态分区插入 实验步骤 在本地文件/home/grid/a.txt中写入以下4行数据: aaa,US,CA …

    2022年8月15日
    0520
  • jvm理论-常量池-string

    字符串常量池-常量项(cp_info )结构 CONSTANT_String_info{ u1 tag=8; u2 string_index;//存放 CONSTANT_Utf8_…

    大数据 2022年9月19日
    0200
  • Spark GraphX图计算快速入门

    一.概述 GraphX是Spark中用于图形和图形并行计算的新组件。在较高的层次上,GraphX 通过引入新的Graph ;抽象来扩展Spark RDD:一个有向多重图,其属性附加…

    2022年8月17日
    0320
  • Shell 脚本编程-函数的定义与调用

    java 语言: public int methodName(int x,String name){ } javascript: function mN(x,name){ &#82…

    大数据 2022年9月7日
    0280
  • Ubuntu Server : 自动更新

    Ubuntu(16.04/18.04) 默认会每天自动安装系统的安全更新,但是不会自动安装包的更新。本文梳理 Ubuntu 16.04/18.04 系统的自动更新机制,并介绍如何配…

    大数据 2022年8月9日
    0540
  • Spark在shell中调试

    将需要调试的程序打包并上传到服务器目录/home/hadoop/test/test.jar[en]Package the program to be debugged and up…

    大数据 2022年8月5日
    0810
  • [大数据学习研究]2.利用VirtualBox模拟Linux集群

    1. 在主机Macbook上设置HOST 前文书已经把虚拟机的静态IP地址设置好,以后可以通过ip地址登录了。不过为了方便,还是设置一下,首先在Mac下修改hosts文件,这样在s…

    2022年8月18日
    0440
  • Kafka的安装与使用(转)

    9.1 Kafka 基础知识 9.1.1 消息系统 点对点消息系统:生产者发送一条消息到queue,一个queue可以有很多消费者,但是一个消息只能被一个消费者接受,当没有消费者可…

    大数据 2022年9月19日
    0190
  • 一个失败的触发器,思路加语法的错误

    [SQL基础【十九、触发器】(不建议使用触发器的原因) 什么是触发器?触发器是与表有关的数据库对象,在满足定义条件时触发,并括INSERT语句,UPDATE语句和DELETE语句;…

    大数据 2022年9月7日
    0220
  • docker build 报错

    docker build . 报错 修复方式 vim /etc/resolv.conf 重启docker systemctl restart docker 这样就可以了 Origi…

    大数据 2022年9月19日
    0170
  • Linux(centos)安装nginx

    Linux(centos)安装nginx 转载请注明出处https://www.cnblogs.com/funnyzpc/p/13913023.html 注意,本教程安装环境为ce…

    大数据 1天前
    050
  • Vision Transformer(ViT)解读

    Vision Transformer Transformer原本是用在NLP上的模型,直到Vision Transformer的出现,transformer开始了在视觉领域的应用。…

    大数据 2022年9月16日
    0380
  • hive中实现group_concat

    mysql中的group_concat分组连接功能相当强大,可以先分组再连接成字符串,还可以进行排序连接。但是hive中并没有这个函数,那么hive中怎么实现这个功能呢? 这里要用…

    2022年8月8日
    0520
  • Hadoop 2.x简介

    Hadoop 2.0产生背景 Hadoop1.0中HDFS和MapReduce在高可用、扩展性等方面存在问题 HDFS存在的问题 NameNode单点故障,难以应用于在线场景 Na…

    大数据 2022年9月7日
    0230
  • hive本地模式执行hive,减少sql处理结果用的时间

    大多数的Hadoop Job是需要Hadoop提供的完整的可扩展性来处理大数据集的,就是在yarn上运行。不过, 有时Hive的输入数据量是非常小的。在这种情况下,为查询触发执行任…

    大数据 2022年9月7日
    0280
  • HBase + Kerberos 配置示例(二)

    接上篇《HBase + Kerberos配置示例(一)》,我们继续剩下的配置工作。 环境准备 Hadoop配置 Zookeeper配置 HBase配置 Java测试程序 环境准备 …

    2022年8月17日
    0320
  • 为什么Kubernetes和容器与机器学习密不可分?

    原文出自infosecurity 作者:Rebecca James 京东云开发者社区编译 当前,数字化转型的热潮在IT领域发展的如火如荼,越来越多的企业投身其中,机器学习和人工智能…

    大数据 1天前
    050
  • docker资源隔离

    1.2 什么是Linux的容器(LXC–LinuX Container)? 1.2.1 主机虚拟化与容器的区别 image.png 1.2.2 容器发展之路 容器技术最…

    大数据 2022年9月19日
    0230
  • Mysql异地多活数据双向同步-CloudCanal实战

    异地多活是一项系统性工作,包含 web 层、应用服务层、数据层的流量分配和同步。 数据层的双向同步是整个方案基础,CloudCanal 在 MySQL 技术点 双向同步中, 暂时无…

    大数据 1天前
    080
  • 基于SpringBoot实现SSM框架整合

    前言 之前学习了Spring框架的JDBC、MVC,SpringBoot也是Spring框架下面的一个子项目,是Spring生态的一部分; 为什么我们要使用Spring的Boot子…

    大数据 1天前
    050
  • Hadoop之HDFS03【NameNode工作原理】

    NameNode的职责 序号 职责 1 负责客户端请求的响应 2 元数据的管理(查询,修改) 数据存储的形式 NameNode中的元数据信息以三种形式存储,如下 序号 方式 说明 …

    大数据 2022年9月7日
    0240
  • 深入理解深度学习——语境词嵌入(Contextual Word Embedding)

    前文介绍了因word2vec而流行的Word Embedding,这种表示方法比离散的独热编码要好很多,因为它不仅降低了维度,还可以反映出语义空间中的线性关系,如”国王…

    大数据 2022年9月16日
    0290
  • Flume+Kafka+Storm+Hbase+HDSF+Poi整合 需求: [En] Demand: 对于一个网站,我们需要根据用户的行为记录日志信息,并分析对我们有用的数据。 …

    2022年8月8日
    0450
  • 慕课嵌入式系统(第七章.任务调度机制)

    慕课嵌入式系统(第七章.任务调度机制) 原创 mozhimen2022-06-23 23:28:36©著作权 文章标签 嵌入式 任务调度 任务管理 嵌入式系统 文章分类 Hadoo…

    大数据 2022年9月7日
    0230

发表回复

登录后才能评论
免费咨询
免费咨询
扫码关注
扫码关注
联系站长

站长Johngo!

大数据和算法重度研究者!

持续产出大数据、算法、LeetCode干货,以及业界好资源!

2022012703491714

微信来撩,免费咨询:xiaozhu_tec

分享本页
返回顶部