内存参数 计算_Spark统一内存管理的实现

本文从源码角度分析spark统一内存管理的实现原理。

统一内存管理对象的创建

统一内存管理对象在SparkEnv中进行创建和管理,这样内存管理就在Driver和Executor端中都可以使用。在SparkEnv的create函数中,创建内存管理对象的实现代码如下:

   val useLegacyMemoryManager = conf.getBoolean("spark.memory.useLegacyMode", false)  val memoryManager: MemoryManager =      if (useLegacyMemoryManager) {        new StaticMemoryManager(conf, numUsableCores)      } else { // spark2默认使用统一内存管理模式,所以执行这里        UnifiedMemoryManager(conf, numUsableCores)      }

从以上代码片段可知,使用静态内存管理还是统一内存管理,是由参数:spark.memory.useLegacyMode决定的。从spark-2.0开始默认都是使用统一内存管理,一般不会修改该参数。

所以,一般情况下,默认会创建统一内存管理:UnifiedMemoryManager对象。这几个对象之间的关系,如图1所示:

60abb09f271712dc1d7369bfbc1d4c2f.png

                         图1 内存管理对象和SparkContext

统一内存管理初始化

在创建统一内存管理对象时,会进行初始化操作。为了便于管理和分配内存,在初始化初始化时会把内存分成几个部分:预留内存,用户内存,执行和存储内存。

统一内存管理对象初始化时的主要步骤如下:

(1)计算JVM可用的最大内存,保存在变量:systemMemory中,默认从参数spark.testing.memory获取值但一般不设置,所以会获取:Runtime.getRuntime.maxMemory的值。

(2)计算需要预留的内存数:reservedMemory,先取参数:spark.testing.reservedMemory的值,但一般不设置,此时使用默认值:300M。

(3)计算系统使用内存的最小值,它是预留内存的1.5倍,也就是:minSystemMemory=reservedMemory * 1.5,若系统使用内存比这个值小:systemMemory < minSystemMemory,则报错:请增加spark.driver.memory的值。

(4)获取executor的内存值:val executorMemory = conf.getSizeAsBytes("spark.executor.memory"),若executorMemory < minSystemMemory,则报错:请增加spark.executor.memory的值。

(5)计算系统可用内存的总量,系统内存-预留内存,得到spark可以使用的总内存:usableMemory = systemMemory - reservedMemory

(6)计算任务执行和存储可用内存总量。计算公式是:usableMemory * memoryFraction。其中memoryFraction是一个小数,是配置项spark.memory.fraction的值,默认值是0.6。

(7)最大可用内存已经计算出来了,此时可以创建UnifiedMemoryManager对象了,代码如下:

    new UnifiedMemoryManager(      conf,      maxHeapMemory = maxMemory,      onHeapStorageRegionSize =        (maxMemory * conf.getDouble("spark.memory.storageFraction", 0.5)).toLong,      numCores = numCores)

从创建统一内存管理对象的代码中可以看出,默认情况下任务的执行内存和存储内存是个占50%。可以通过参数spark.memory.storageFraction来调整执行内存和存储内存的占比。

完成统一内存初始化后,内存的划分情况如图2所示:

b5ef090541f58bd89f5e22d48a6501e8.png

                            图2 统一内存初始化内存分布

统一内存管理的实现

前面已经说明,统一内存管理是在UnifiedMemoryManager类中实现的。下面我们来分析统一内存管理的实现逻辑。

该类的声明如下:

 private[spark] class UnifiedMemoryManager private[memory] (    conf: SparkConf,    val maxHeapMemory: Long,    onHeapStorageRegionSize: Long,    numCores: Int)

统一内存管理为spark提供了灵活使用内存的机制。它把一块大的可使用的内存分成执行内存和存储内存。执行内存主要被Executor在执行任务时使用,而存储内存主要用来存储数据块。

该类的成员变量说明如下

  • onHeapStorageRegionSize堆内内存区的大小,以字节为单位。该内存区不是静态保留的; 执行器可以在必要时进行借用。仅当实际存储内存使用量超过此区域时,才能清除缓存块。

  • maxHeapMemory最大可用堆内存。该成员变量是通过函数getMaxMemory计算而来的,具体的计算方法见下面的分析。

  • numCores核数。

获取执行内存

在执行当前任务内存不足时会需要申请执行内存。申请内存的过程可能会向存储内存池(StorageMemoryPool)借用一部分内存,并把这部分内存添加到执行内存池(ExecutionMemoryPool)中。能够向存储内存池借用内存必须满足以下条件之一:

(1)存储池的空闲内存大于0;

(2)存储是否已经借用了执行池的内存。通过:存储内存池目前的大小减去初始化设置的存储内存池的大小是否大于0来进行判断,也就是计算storagePool.poolSize - storageRegionSize是否大于0。若大于0(已借用)表示可以分配。

在借用存储内存时,可能会把存储池中的内存释放一部分,若这部分内存的rdd设置了useDisk级别,还会把这些内存的数据写入磁盘,否则,这些内存中的存储数据就丢失了。

内存块的释放是在MemoryStore对象中完成(后面的文章会详细分析这实现),官方文档中提到过,释放老的内存块的算法是LRU(最近最少使用),这是由于在MemoryStore中内存块是以LinkedHashMap的结构组织的,在链表的头部就是“最近最少使用”的内存块。这部分内容在分析MemoryStore的实现时再继续讲解。

下面分析获取执行内存操作的实现逻辑。

acquireExecutionMemory函数

在统一内存管理中实现获取执行内存的函数是:acquireExecutionMemory。该函数的原型如下:

   override private[memory] def acquireExecutionMemory(      numBytes: Long,      taskAttemptId: Long,      memoryMode: MemoryMode): Long = synchronized {...}

该函数尝试为目前的执行任务获取numBytes执行内存。对于该函数需要注意以下几点:

(1)它尝试获取numBytes字节大小的内存,返回能够获取的字节数,若返回0,则表示无法分配内存;

(2)它是同步函数,所以当有多个任务调用该函数时可能会阻塞,直到有足够的内存,这样做是为了在把数据进行持久化之前,让每个任务都有机会获取到1/2N的内存(其中N是运行的任务数)。

(3)当老的任务占用很多内存,而新任务数又不断增加时,阻塞就可能会发生。

实现逻辑

获取执行内存操作的实现逻辑如下:

(1)根据参数memoryMode的值来选择操作:若是堆内模式(ON_HEAP),获取堆内的执行和存储池总量和堆内可用存储内存总量,以及总的堆内内存大小。若是堆外模式(OFF_HEAP),获取堆外的执行和存储池总量和堆外可用存储内存总量,以及总的堆外内存大小。这一步的代码实现如下:

     val (executionPool, storagePool, storageRegionSize, maxMemory) = memoryMode match {      // 堆内模式      case MemoryMode.ON_HEAP => (        onHeapExecutionMemoryPool,        onHeapStorageMemoryPool,        onHeapStorageRegionSize,        maxHeapMemory)      // 堆外模式      case MemoryMode.OFF_HEAP => (        offHeapExecutionMemoryPool,        offHeapStorageMemoryPool,        offHeapStorageMemory,        maxOffHeapMemory)    }

(2)判断是否需要增加执行内存池(ExecutionPool)。当执行内存池中空闲内存量小于需要申请的内存量时,则会尝试增加执行池。尝试增加执行池的过程,本质上就是向存储池StorageMemoryPool借用内存的过程。能够成功借用存储池的内存,需要满足以下两个条件之一:1)存储池有空闲内存;2)存储池的量大于初始化的量。(也就是说,已经向执行内存池借用了一些内存,存储池大小增加了)

另外,这个过程可能执行多次,每次尝试都必须能够获取到一些内存,可能会清除掉一些内存中的数据块,以防其他任务在缓存大的数据块和清除数据之间进行反复。那么,为什么每次只能清除一些内存呢?这是因为在MemoryStore中,内存是以MemoryEntry对象来组织和管理的,清理时也是以这个为单位进行的,而每个这样的对象的大小是不同的。

尝试增加执行内存池大小的实现代码如下:

 def maybeGrowExecutionPool(extraMemoryNeeded: Long): Unit = {      if (extraMemoryNeeded > 0) {        // 可以分配内存的条件:1.存储池有空闲内存 或 2.存储池已经借用了执行池的内存        val memoryReclaimableFromStorage = math.max(          storagePool.memoryFree,          storagePool.poolSize - storageRegionSize)                if (memoryReclaimableFromStorage > 0) {          // 通过下面的函数来释放存储内存池的内存,减少存储内存池的大小。          val spaceToReclaim = storagePool.freeSpaceToShrinkPool(            math.min(extraMemoryNeeded, memoryReclaimableFromStorage))          // 到这里,说明存储内存池的空间已经释放,这一步只需要减少存储内存池的大小即可          storagePool.decrementPoolSize(spaceToReclaim)          // 增加执行内存池大小的量          executionPool.incrementPoolSize(spaceToReclaim)        }      }    }

要注意的是,执行内存池将借用的内存均匀地分配给活动任务,以限制每个任务的执行内存分配。保持这个大于执行池大小是很重要的,这不考虑可以通过清除存储而释放的潜在内存。另外,这个数量应该保持在“maxMemory”以下,以便在任务中执行内存分配的公平性,否则,任务可能占用超过其平均份额的执行内存。

(3)然后调用executionPool.acquireMemory来获取内存,该函数的声明如下:

 private[memory] def acquireMemory(      numBytes: Long, // 想要获取的内存数      taskAttemptId: Long, // 想要获取内存的任务数      maybeGrowPool: Long => Unit = (additionalSpaceNeeded: Long) => Unit, // 一个回调函数,用来增长内存池的大小      computeMaxPoolSize: () => Long = () => poolSize // 回调函数,用来获取某个时刻允许获取内存的最大值      ): Long = lock.synchronized 

该函数尝试为给定任务获取numBytes大小的内存,并返回获取到的内存大小。该函数可能会阻塞,直到有足够的内存再返回。该函数的执行逻辑大致如下:

  • 添加任务到taskMemory这个map中,该map保存了任务id和申请的内存大小的对应关系。

  • 调用maybeGrowExecutionPool回调函数来向storeage申请内存,若内存不够该函数会释放掉一些存储内存。一次释放的内存可能不够,所以该函数可能会尝试多次。

  • maybeGrowExecutionPool会调用memoryStore.evictBlocksToFreeSpace函数,在该函数中会根据rdd和内存模式等参数来清除一些内存块,释放对应大小的内存,具体的实现过程在后面分析。

获取存储内存

获取存储内存的过程比获取执行内存的过程要相对简单。因为,获取存储内存时不会强制释放正在使用的执行内存,而只能从执行池的空闲内存中申请。

所以,申请存储内存的步骤主要是以下几步:

(1)判断需要申请的内存数量,是否大于存储池的空闲内存量。若大于(存储池的内存量不够),则向执行池的空闲内存申请一部分内存。要注意:可能执行池的空闲内存也不够,或根本就没有空闲内存。

(2)调用存储池的内存获取函数获取内存,若空闲内存不够,则需要从存储池中按LRU算法释放一部分内存。

获取存储内存是在函数acquireStorageMemory中实现,下面我们来分析一下该函数的具体实现。

acquireStorageMemory函数

该函数的原型如下:

  override def acquireStorageMemory(      blockId: BlockId,      numBytes: Long,      memoryMode: MemoryMode): Boolean = synchronized {...}

该函数的参数:

  • memoryMode: MemoryMode:该参数是内存的模式,主要有两种:ON_HEAP或OFF_HEAP。

  • numBytes:需要申请的内存大小,单位是bytes

  • blockId:数据块的ID,也是可能会被释放的数据块。若该id为空,则会通过LRU算法寻找需要释放块对应的内存。

该函数是一个同步函数,若是多个线程同时调用该函数,可能会阻塞。

实现分析

该函数的主要实现逻辑如下:

(1)根据参数memoryMode来获取此种模式下的最大可以用存储内存,保存在变量maxMemory中。

(2)判断内存申请量(即参数numBytes)是否大于maxMemory,若申请内存大于最大可用内存,会失败。报错:该blockid的数据块需要的内存超过最大使用内存。

(3)若申请的内存大小:numBytes大于存储池的空闲内存大小,则需要从执行池中“借用”一些空闲内存。借用的意思是,从执行池的空闲内存中获取一部分内存,但要注意:最多从执行池中借用空闲内存量,不会释放任务正在使用的执行内存。实现代码如下:

 if (numBytes > storagePool.memoryFree) {       // 所需内存量大于可用存储空闲内存量,需要从执行池中申请一部分内存      val memoryBorrowedFromExecution = Math.min(executionPool.memoryFree,        numBytes - storagePool.memoryFree) //最多获取执行池中空闲的内存量大小      executionPool.decrementPoolSize(memoryBorrowedFromExecution)  // 执行内存池减少内存数      storagePool.incrementPoolSize(memoryBorrowedFromExecution) // 存储内存池增加对应内存    }

注意:这一步是体现统一内存思想的重要的一步。

(4)若能够从执行内存池中借用成功,这一步就直接在存储内存池中申请内存了。代码很简单,就是调用存储内存池的内存申请函数:

     storagePool.acquireMemory(blockId, numBytes)
storagePool#acquireMemory函数

该函数来完成存储池的内存申请工作。要注意,此时的存储池可能有空闲的内存,也可能没有空闲内存。当存储池没有空闲内存时,需要把已有的某些数据块从存储池中清除,以满足当前数据块的存储需要。

该函数的实现逻辑如下:

(1) 计算需要释放的内存量

需要申请的内存量减去空闲的内存量,就是需要释放的内存量。也就是说,需要从已经使用的存储内存块中释放一部分内存。

     val numBytesToFree = math.max(0, numBytes - memoryFree)

(2) 第(1)步已经计算出来需要释放的内存量了。下面调用StorageMemoryPool.acquireMemory函数来申请内存,释放一定的数据块。该函数会调用MemoryStore.evictBlocksToFreeSpace来清除数据块。会被清除的数据块的判断如下:

 def blockIsEvictable(blockId: BlockId, entry: MemoryEntry[_]): Boolean = {     //存储模式相同,且blockId没有被RDD占用 或则不是要替换相同RDD的不同数据块    entry.memoryMode == memoryMode &&     (rddToAdd.isEmpty || rddToAdd != getRddId(blockId))}

若有可以释放的数据块,还需要获取一把写锁,加锁的目的是防止目前还有其他的线程在读该数据块。当锁获取成功后,就可以开始删除数据块了,具体的删除过程是通过blockInfoManager.removeBlock来进行的。该函数会把需要清除的元数据和数据块从blockManager中删除。

释放内存块:MemoryStore#evictBlocksToFreeSpace函数

这是MemoryStore类的成员函数,该函数完成内存块的释放,若存储级别包含useDisk还会把内存中的数据保存到磁盘中。该函数的原型如下:

   private[spark] def evictBlocksToFreeSpace(      blockId: Option[BlockId],      space: Long,      memoryMode: MemoryMode): Long = {

其中的blockId是数据块的id,每个id都对应一个内存块。释放内存块的逻辑如下:

(1)遍历内存块的队列。这是一个LinkedHashMap,最后一次被访问的内存块节点会放到链表的后面,这样最近没有被访问的内存块就在队列的头部。

(2)检查内存块是否可以被释放。释放内存块需要满足以下条件:

1)内存块的模式必须和参数中memoryMode的值相等;

2)该blockId对应的内存块没有被其他RDD占用,或则不是要替换相同RDD的不同数据块。

(3)若满足以上两个条件,就会释放该内存块。释放内存块的过程如下:

1)确认内存块的写锁已经锁上了;

2)通过blockId的信息检查存储级别是否包含useDisk,若包含则把内存的数据写入到磁盘上。写入磁盘 的过程是通过DiskStore对象来完成的。

(4)由于实际的内存是通过MemoryStore来管理的,所以,最后一步就是从memoryStore中删除并释放blockId对应的内存块,并减少MemoryStore的内存数量。到此,就完成了内存释放的整个过程。至于MemoryStore是如何释放内存的,会在分析MemoryStore时进行分析。

计算可用堆内存储内存:maxOnHeapStorageMemory函数

该函数用来计算堆内可用内存,逻辑很简单:就是使用总的堆内存储内存-为执行器可分配的堆内内存:

   override def maxOnHeapStorageMemory: Long = synchronized { // 计算可用堆内内存    maxHeapMemory - onHeapExecutionMemoryPool.memoryUsed  }
计算对外存储内存:maxOffHeapStorageMemory函数

该函数用来计算可用堆外内存:使用总堆外内存-为执行器分配的堆外内存:

  override def maxOffHeapStorageMemory: Long = synchronized { // 计算可用堆外内存    maxOffHeapMemory - offHeapExecutionMemoryPool.memoryUsed  }

小结

本文讲述了spark统一内存管理的实现原理。从实现层面来看,Spark的统一内存管理都是在UnifiedMemoryManager类中实现。不管是执行还是存储内存不足时,都可以向对方借用内存。但内存不足时,可以根据LRU来释放存储正在使用的内存,但不能释放执行时正在使用的内存。

另外,最终的内存块释放和数据块的持久化是通过MemoryStore,DiskStore以及BlockManager这几个系统来完成的,这些组件的原理会在后面的文章中继续分析。

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

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

相关文章

Python:以鸢尾花数据为例,介绍决策树算法

文章参考来源&#xff1a; https://www.cnblogs.com/yanqiang/p/11600569.html https://www.cnblogs.com/baby-lily/p/10646226.html https://blog.csdn.net/liuziyuan333183/article/details/107399633 决策树算法 决策树算法主要有ID3, C4.5, CART这三种。 ID3算法从树的…

【转】CT球管小知识--热容量

Heat Unit 简称HU&#xff0c;为DR、CT等医疗设备中球管的热容量单位。如&#xff0c;Varian球管RAD14的热容量为300kHU。设备工作时&#xff0c;X线管两极之间要承受极高的电压&#xff0c;并通过一定量电流&#xff0c;高速电子束撞击阳极靶面&#xff0c;将产生大量热能。X线…

一键锁屏_ios快捷指令一键登录校园网(桂航为例,哆点认证)

&#xff08;鄙人水平很有限&#xff0c;所学的专业也和此无关&#xff0c;文中有的东西可能会说错&#xff0c;但我尽量用简单的方式说。请多指教&#xff09;现在很多高校现在晚上断电断网&#xff0c;最烦恼的事莫过于第二天早上起床眯着眼摸出手机输入账号密码登录校园网的…

【转】一篇文章完整了解CT成像技术(完整版)

1&#xff0e;CT的发明与发展 1.1 CT的发明 CT是计算机断层摄影术&#xff08;Computed Tomography&#xff0c;CT&#xff09;的简称&#xff0c;是继1895年伦琴发现X线以来&#xff0c;医学影像学发展史上的一次革命。 CT的发明可以追溯到1917年。当时&#xff0c;奥地利数…

Pandas数据可视化工具:图表工具-Seaborn

内容来源&#xff1a;https://www.jiqizhixin.com/articles/2019-01-30-15 简介 在本文中&#xff0c;我们将研究Seaborn&#xff0c;它是Python中另一个非常有用的数据可视化库。Seaborn库构建在Matplotlib之上&#xff0c;并提供许多高级数据可视化功能。 尽管Seaborn库可以…

图解WinCE6.0下的内核驱动和用户驱动

图解WinCE6.0下的内核驱动和用户驱动 在《WinCE驱动程序的分类》中曾提到&#xff0c;WinCE6.0的流驱动既可以加载到内核态也可以加载到用户态。下面通过一组图片简单说明一下这两种驱动的关系。 首先编写一个流驱动WCEDrv&#xff0c;代码如下。 代码 #include <windows.h&…

人体轮廓_女性人体油画轮廓柔和生动,优美动人,你喜欢吗?

人体油画是艺术和时代的产物&#xff0c;也是艺术结晶的重要体现&#xff0c;在文艺复兴以前&#xff0c;人体艺术大都以雕塑形式来表现&#xff0c;在此之后&#xff0c;人们都以意大利威尼斯绘画为代表&#xff0c;艺术家们开始以色彩塑造人体绘画艺术。随着时代进步和人们对…

机器学习分类模型中的评价指标介绍:准确率、精确率、召回率、ROC曲线

文章来源&#xff1a;https://blog.csdn.net/wf592523813/article/details/95202448 1 二分类评价指标 准确率&#xff0c;精确率&#xff0c;召回率&#xff0c;F1-Score&#xff0c; AUC, ROC, P-R曲线 1.1 准确率&#xff08;Accuracy&#xff09; 评价分类问题的性能指标…

【转】AI-900认证考试攻略

架构师的信仰系列文章&#xff0c;主要介绍我对系统架构的理解&#xff0c;从我的视角描述各种软件应用系统的架构设计思想和实现思路。 从程序员开始&#xff0c;到架构师一路走来&#xff0c;经历过太多的系统和应用。做过手机游戏&#xff0c;写过编程工具&#xff1b;做过…

300plc与组态王mpi通讯_S7-300与S7-200之间的MPI通信

通信说明S7-200PLC与S7-300PLC之间采用MPI通讯方式时&#xff0c;S7-200PLC中不需要编写任何与通讯有关的程序&#xff0c;只需要将要交换的数据整理到一个连续的V 存储区当中即可&#xff0c;而S7-300PLC中需要在组织块OB1(或是定时中断组织块OB35)当中调用系统功能X_GET(SFC6…

ORA-01114: 将块写入文件 35 时出现 IO 错误

参考文档&#xff1a; https://blog.csdn.net/z_x_1000/article/details/17263077 https://www.cnblogs.com/login2012/p/5775602.html https://www.iteye.com/blog/yangyangcom-2200174 一、问题背景 最开始发现应用服务打不开&#xff0c;于是登录服务器发现Oracle数据关…

【转】CT影像文件格式DICOM详解

CT影像文件格式DICOM详解 DICOM简介 DICOM&#xff08;Digital Imaging and Communications in Medicine&#xff09;即医学数字成像和通信&#xff0c;是医学图像和相关信息的国际标准&#xff08;ISO 12052&#xff09;。DICOM被广泛应用于放射医疗&#xff0c;心血管成像以…

fatal error lnk1120: 1 个无法解析的外部命令_3月1日七牛云存储割韭菜的应对方法...

前言早上起来看邮件&#xff0c;看到一封被七牛云割韭菜的公告&#xff1a;内心冰冰凉&#xff0c;不过大家都要吃饭的嘛总不能一直免费下去。所以来研究一下对于我们这种穷人应该如何应对。一、七牛CDN加速流程主要流程分析1、用户通过浏览器访问我的网站(腾讯云服务器)&#…

【转】DCM(DICOM)医学影像文件格式详解

1、 什么是DICOM&#xff1f; DICOM(DigitalImaging andCommunications inMedicine)是指医疗数字影像传输协定&#xff0c;是用于医学影像处理、储存、打印、传输的一组通用的标准协定。它包含了文件格式的定义以及网络通信协议。DICOM是以TCP/IP为基础的应用协定&#xff0c;并…

SM4对称加密算法及Java实现

文章来源&#xff1a;https://www.jianshu.com/p/5ec8464b0a1b 一、简介 与DES和AES算法类似&#xff0c;SM4算法是一种分组密码算法。 其分组长度为128bit&#xff0c;密钥长度也为128bit。 加密算法与密钥扩展算法均采用32轮非线性迭代结构&#xff0c;以字&#xff08;32位…

【转】DICOM网络协议(一)概述

转自&#xff1a;https://www.jianshu.com/p/8a0f0fe6a738 作者&#xff1a;我住的城市没有福合埕 DICOM (Digital Imaging and Communications in Medicine)即医学数字成像和通信&#xff0c;DICOM网络是基于TCP/IP的网络协议。通过DICOM将影像设备和存储管理设备连接起来。…

Windows进程系列(2) -- Svchost进程

在基于NT内核的Windows操作系统家族中&#xff0c;Svchost.exe是一个非常重要的进程。很多病毒、木马驻留系统与这个进程密切相关&#xff0c;因此深入了解该进程是非常有必要的。本文主要介绍Svchost进程的功能&#xff0c;以及与该进程相关的知识。      Svchost进程概述…

【转】DICOM入门(一)——语法

转自&#xff1a;https://www.jianshu.com/p/5db8933a25a4 作者&#xff1a;我住的城市没有福合埕 1.什么是DICOM DICOM(Digital Imaging and Communications in Medicine)即医学数字成像和传输协议&#xff0c;是用医疗影像&#xff08;CT 核磁共振 DR CR 超声等&#xff0…

1000并发 MySQL数据库_再送一波干货,测试2000线程并发下同时查询1000万条数据库表及索引优化...

继上篇文章《绝对干货&#xff0c;教你4分钟插入1000万条数据到mysql数据库表&#xff0c;快快进来》发布后在博客园首页展示得到了挺多的阅读量&#xff0c;我这篇文章就是对上篇文章的千万级数据库表在高并发访问下如何进行测试访问这篇文章的知识点如下:1.如何自写几十行代码…

【转】VTK修炼之道1_初识VTK

1.VTK是什么&#xff1f; Visualization ToolKit 3D计算机图形学、图象处理及可视化工具包 VTK使用C、面向对象技术开发&#xff1b;基于OpenGL&#xff0c;封装了OpenGL中的功能&#xff0c;屏蔽细节、便于交互、易于使用提供多种语言接口C&#xff0b;&#xff0b; 、Java 、…