├── .gitattributes ├── .gitignore ├── .idea ├── .name ├── codeStyleSettings.xml ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── misc.xml ├── modules.xml ├── scopes │ └── scope_settings.xml ├── thriftCompiler.xml ├── uiDesigner.xml └── vcs.xml ├── README.md ├── Spark-Note.iml ├── hadoop ├── datanode.md ├── hadoop-ipc.md ├── metric-learn.md ├── namenode-ha.md ├── nodemanager-container-launch.md ├── nodemanager-container-localizer.md ├── nodemanager-container-monitor.md └── nodemanager-container-withrm.md ├── hbase ├── hbase-bulk-loading.md ├── hbase-filter.md └── hbase-learn.md ├── image ├── BlockPoolManager.png ├── Catalyst-Optimizer-diagram.png ├── edge_cut_vs_vertex_cut.png ├── fsdataset.png ├── hadoop-rpc.jpg ├── job.jpg ├── network-client.jpg ├── network-message.jpg ├── network-rpcenv.jpg └── project.png ├── other ├── mvn-lib.md ├── point-estimation.md └── scala-java-class-type.md ├── spark ├── class-from-root.md ├── function-closure-cleaner.md ├── mllib-pipeline.md ├── pregel-bagel.md ├── scala-implicit.md ├── shuffle-hash-sort.md ├── shuffle-study.md ├── spark-block-manager.md ├── spark-catalyst-optimizer.md ├── spark-catalyst.md ├── spark-experience.md ├── spark-important-issue.md ├── spark-join.md ├── spark-memory-manager.md └── spark-network-netty.md └── system ├── cpu.md ├── disk-io.md ├── java-memory.md └── memory.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # ========================= 18 | # Operating System Files 19 | # ========================= 20 | 21 | # OSX 22 | # ========================= 23 | 24 | .DS_Store 25 | .AppleDouble 26 | .LSOverride 27 | 28 | # Icon must ends with two \r. 29 | Icon 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Spark-Note -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 13 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 1.6 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/thriftCompiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 填坑与埋坑 2 | ========== 3 | 4 | ### [Spark SQL Join原理分析](./spark/spark-join.md) 5 | 过了一下Spark SQL对Join的支持,相对来说原理比较简单,这里简单记录一下! 6 | 7 | + 开始埋坑日期:2016-8-14 8 | + 坑状态:done 9 | 10 | ### [Spark 使用经验](./spark/spark-experience.md) 11 | 在spark使用过程中,除了内存,网络一些大的主题引起大家注意以外还有很多细节,是可以多注意! 比如正确的使用flatmap!,reduceByKey一定比groupByKey好吗?,后面会持续总结... 12 | 13 | + 开始埋坑日期:2016-7-20 14 | + 坑状态:doing 15 | 16 | ### [Spark-Catalyst Optimizer](./spark/spark-catalyst-optimizer.md) 17 | Optimizer主要会对Logical Plan进行剪枝,合并等操作,从而从Logical Plan中删除掉一些无用计算,或对一些计算的多个步骤进行合并。由于优化的策略会随着知识的发现而逐渐引入,核心还是要理解原理!! 18 | 19 | + 开始埋坑日期:2016-7-10 20 | + 坑状态:done 21 | 22 | ### [Spark Catalyst的实现分析](./spark/spark-catalyst.md) 23 | Spark SQL是Spark内部最核心以及社区最为活跃的组件,也是未来Spark对End-User最好的接口,支持SQL语句和类RDD的Dataset/DataFrame接口。相比在传统的RDD上进行开发,Spark SQL的业务逻辑在执行前和执行过程中都有相应的优化工具对其进行自动优化(即Spark Catalyst以及Tungsten两个组件),因此未来Spark SQL肯定是主流。本文主要是针对SparkSQL核心组件Catalyst进行分析,算是SparkSQL实习分析的第一步吧 24 | 25 | + 开始埋坑日期:2016-7-1 26 | + 坑状态:done 27 | 28 | 29 | ### [Spark Memory解析](./spark/spark-memory-manager.md) 30 | 在Spark日常工作中(特别是处理大数据),内存算是最常见问题。看着日志里打着各种FullGC甚至OutOfMemory日志,但是却不能理解是在哪一块出了内存问题。其实也这是正常的,Spark内存管理某种程度上还是相当复杂了,涉及RDD-Cache,Shuffle,Off-Heap等逻辑,它贯穿在整个任务执行的每个环节中。 31 | 32 | + 开始埋坑日期:2016-5-1 33 | + 坑状态:done 34 | 35 | 36 | ### [Spark Network 模块分析(基于Netty的实现)](./spark/spark-network-netty.md) 37 | 一直以来,基于Akka实现的RPC通信框架是Spark引以为豪的主要特性,也是与Hadoop等分布式计算框架对比过程中一大亮点,但是时代和技术都在演化,从Spark1.3.1版本开始,为了解决大块数据(如Shuffle)的传输问题,Spark引入了Netty通信框架,到了1.6.0版本,Netty居然完成取代了Akka,承担Spark内部所有的RPC通信以及数据流传输。 38 | 39 | + 开始埋坑日期:2016-4-1 40 | + 坑状态:done 41 | 42 | ### [Hadoop DataNode分析](./hadoop/datanode.md) 43 | 在HDFS集群中,DataNode直接提供了磁盘文件的管理,也是性能的最大的瓶颈之一,对DataNode的分析显得尤为重要。这次对DataNode串读了一下,对DataNode基本逻辑有一定的了解 44 | 45 | + 开始埋坑日期:2015-12-25 46 | + 坑状态:done 47 | 48 | ### [Hadoop IPC分析](./hadoop/hadoop-ipc.md) 49 | 最近比较悠闲,想回头再看一下Hadoop源码,准备着重了解一下HDFS内部的细节,突然发现以前熟读的hadoop ipc又有点模糊了,为了加深记忆,还是在这里重新梳理一下,针对大体思路上做一下笔记,hadoop源码分析类的“笔记”好难写,代码量太大了,只能扣一下比较重要的细节解释一下,具体的还是要自己去阅读代码!!!! 50 | 51 | + 开始埋坑日期:2015-12-15 52 | + 坑状态:done 53 | 54 | 55 | ### [系统进程性能分析](./system/java-memory.md) 56 | 好久没有埋坑了,最近突然想把系统性能以及问题跟踪相关的原理和工具稍微整理,基本都是日常开发和运维不可缺少的东西,包括[《进程分析之内存》](./system/memory.md),[《进程分析之JAVA内存》](./system/java-memory.md),[《进程分析之CPU》](./system/cpu.md),[《进程分析之磁盘IO》](./system/disk-io.md),《进程分析之网络IO》; 57 | 58 | + 开始埋坑日期:2015-12-02 59 | + 坑状态:doing 60 | 61 | 62 | ### [Pregel原理分析与Bagel实现](./spark/pregel-bagel.md) 63 | [Pregel](http://people.apache.org/~edwardyoon/documents/pregel.pdf) 2010年就已经出来了, [Bagel](https://spark.apache.org/docs/latest/bagel-programming-guide.html)也2011年 64 | 就已经在spark项目中开源, 并且在最近的graphX项目中申明不再对Bagel进行支持, 使用GraphX的"高级API"进行取代, 种种迹象好像说明Pregel这门技术已经走向"末端", 其实个人的观点倒不是这样的; 65 | 66 | 最近因为项目的需要去调研了一下图计算框架,当看到Pregel的时候就有一种感叹原来"密密麻麻"的图计算可以被简化到这样. 虽然后面项目应该是用Graphx来做,但是还是想对Pregel做一个总结. 67 | 68 | + 开始埋坑日期:2014-12-12 69 | + 坑状态:done 70 | 71 | ### [MLLib Pipeline的实现分析](./spark/mllib-pipeline.md) 72 | Spark中的Mllib一直朝着可实践性的方法前进着, 而Pipeline是这个过程中一个很重要的功能. 在2014年11月,孟祥瑞在Spark MLLib代码中CI了一个全新的package:"org.apache.spark.ml", 73 | 和传统的"org.apache.spark.mllib"独立, 这个包即Spark MLLib的Pipeline and Parameters功能. 到目前为止,这个package只有三次ci, 代码量也较少,但是基本上可以清楚看到pipeline逻辑, 74 | 这里开第一个mllib的坑, 开始对mllib进行深入学习. 75 | 76 | + 开始埋坑日期:2014-12-10 77 | + 坑状态:done 78 | 79 | ### [Spark基础以及Shuffle实现分析](./spark/shuffle-study.md) 80 | 包括mapreduce和spark在内的所有离线计算工具,shuffle操作永远是设计最为笨重的,也是整体计算性能的瓶颈。主要原因是shuffle操作是不可避免的, 81 | 而且它涉及到大量的本地IO,网络IO,甚至会占用大量的内存,CPU来做sort-based shuffle相关的操作。 82 | 这里挖一个这个坑,由于第一个坑,所以我会在这个坑里面阐述大量的spark基础的东西,顺便对这些基础做一下整理,包括Job的执行过程中等; 83 | 84 | + 开始埋坑日期:2014-9-24 85 | + 坑状态:done 86 | 87 | ### [两种ShuffleManager的实现:Hash和Sort](./spark/shuffle-hash-sort.md) 88 | 在《[Spark基础以及Shuffle实现分析](./spark/shuffle-study.md)》中分析了Spark的Shuffle的实现,但是其中遗留了两个问题. 89 | 本文针对第二个问题:"具体shuffleManager和shuffleBlockManager的实现"进行分析, 即HashShuffle和SortShuffle当前Spark中支持了两种ShuffleManager的实现; 90 | 其中SortShuffle是spark1.1版本发布的,详情参见:[Sort-based shuffle implementation](https://issues.apache.org/jira/browse/SPARK-2045) 91 | 本文将对这两种Shuffle的实现进行分析. 92 | 93 | + 开始埋坑日期:2014-11-24 94 | + 坑状态:done 95 | 96 | ###[关于Scala的implicit(隐式转换)的思考](./spark/scala-implicit.md) 97 | Scala的隐式转换是一个很重要的语法糖,在Spark中也做了很多应用,其中最大的应用就是在RDD类型上提供了reduceByKey,groupByKey等函数接口, 如果不能对隐式 98 | 转换很好的理解, 基本上都无法理解为什么在RDD中不存在这类函数的基础上可以执行这类函数.文章内部做了解释, 同时针对隐式转换做了一些总结和思考 99 | 100 | + 开始埋坑日期:2014-12-01 101 | + 坑状态:done 102 | 103 | ### [Spark-Block管理](./spark/spark-block-manager.md) 104 | 在Spark里面,block的管理基本贯穿了整个计算模型,从cache的管理,shuffle的输出等等,都和block密切相关。这里挖一坑。 105 | 106 | + 开始埋坑日期:2014-9-25 107 | + 坑状态:done 108 | 109 | ### [Spark-Block的BlockTransferService](./spark/spark-block-manager.md) 110 | 在上面的Spark-BlockManager中,我们基本了解了整个BlockManager的结构,但是在分析Spark Shuffle时候,我发现我遗留了对BlockTransferService的分析, 111 | 毕竟Spark的Shuffle的reduce过程,需要从远程来获取Block;在上面的文章中,对于remoteGET,分析到BlockTransferService就停止了,这里补上; 112 | 113 | 其实个人在0.91版本就遇到一个remoteGet的bug, 即当时remoteGet没有进行超时控制,消息丢包导致假死, 当然目前版本没有这个问题了. 具体的我会在这篇文章中进行解释; 114 | 115 | + 开始埋坑日期:2014-11-25 116 | + 期望完成日期:2014-12-10 117 | + 坑状态:doing 118 | 119 | ### [Spark闭包清理的理解](./spark/function-closure-cleaner.md) 120 | scala是一门函数编程语言,当然函数,方法,闭包这些概念也是他们的核心,在阅读spark的代码过程,也充斥着大量关于scala函数相关的特性引用,比如: 121 | 122 | def map[U: ClassTag](f: T => U): RDD[U] = new MappedRDD(this, sc.clean(f)) 123 | map函数的应用,每次我传入一个f都会做一次sc.clean的操作,那它到底做了什么事情呢?其实这些都和scala闭包有关系。 124 | 同时java8之前版本,java不对闭包的支持,那么java是通过内部类来实现,那么内部类与闭包到底有那些关系和区别呢?同时会深入剖析java的内部类与scala的内部类的区别 125 | 126 | + 开始埋坑日期:2014-9-25 127 | + 期望完成日期:2014-10-30 128 | + 坑状态:doing 129 | 130 | ### [HBase总结笔记](./hbase/hbase-learn.md) 131 | 在Hadoop系里面玩了几年了,但是HBase一直以来都不太原因去深入学习.这次借项目,系统的对HBase进行学习,这里对Hbase里面一些核心主题进行总结. 132 | 目前还没有打算从源码层面去深入研究的计划,后面有时间再一一研究. 133 | 134 | + 开始埋坑日期:2014-10-10 135 | + 坑状态:done 136 | 137 | ### [HBase Bulk Loading实践](./hbase/hbase-bulk-loading.md) 138 | 近期需要将mysql中的30T的数据导入到HBase中,一条条put,HDFS的IO负载太大,所以采用Hbase内部提供的Bulk Loading工具批量导入. 139 | Bulk Loading直接通过把HFile文件加载到已有的Hbase表中,因此我们只需要通过一个mapreduce将原始数据写为HFile格式,就可以轻易导入大量的数据. 140 | 141 | + 开始埋坑日期:2014-10-15 142 | + 坑状态:done 143 | 144 | ###[HBase Filter学习](./hbase/hbase-filter.md) 145 | HBase的逻辑查询是严格基于Filter来实现的,在HBase中,针对Filter提供了一个包来实现,类型还是挺多的,因为业务需要,这里做一个简单的整理. 146 | 对日常比较需要的Filter拿出来做一个分析 147 | 148 | + 开始埋坑日期:2014-11-10 149 | + 坑状态:done 150 | 151 | ### [NodeManager解析系列一:内存Monitor分析](./hadoop/nodemanager-container-monitor.md) 152 | 用过MapReduce都遇到因为task使用内存过多,导致container被kill,然后通过网上来找资料来设置mapreduce.map.memory.mb/mapreduce.reduce.memory.mb 153 | /mapreduce.map.java.opts/mapreduce.reduce.java.opts来解决问题。但是对于内部实现我们还是不清楚,这篇文章就是来解析NodeManager怎么 154 | 对container的内存使用进行Monitor 155 | 156 | + 开始埋坑日期:2014-10-20 157 | + 坑状态:done 158 | 159 | ### [NodeManager解析系列二:Container的启动](./hadoop/nodemanager-container-launch.md) 160 | Hadoop里面模块很多,为什么我优先对NodeManager进行解析呢?因为NodeManager与我提交的spark/mapreduce任务密切相关, 161 | 如果对NodeManager理解不透,都不能理解Spark的Executor是怎么被调度起来的。这篇文件就是对Container的启动进行分析 162 | 163 | + 开始埋坑日期:2014-11-2 164 | + 坑状态:done 165 | 166 | ### [NodeManager解析系列三:Localization的分析](./hadoop/nodemanager-container-localizer.md) 167 | 任何一个阅读过NodeManager源码的人都被Localization弄得晕头转向的,从LocalResource,LocalizedResource,LocalResourcesTracker, 168 | LocalizerTracker这些关键字开始,命名十分接近,稍不注意注意就搞糊涂了,这篇文章对Localization进行分析,从个人感受来看,对Localization 169 | 理解透了,基本上NodeManager都理解差不多了 170 | 171 | + 开始埋坑日期:2014-11-2 172 | + 坑状态:done 173 | 174 | ### [NodeManager解析系列四:与ResourceManager的交互](./hadoop/nodemanager-container-withrm.md) 175 | 在yarn模型中,NodeManager充当着slave的角色,与ResourceManager注册并提供了Containner服务。前面几部分核心分析NodeManager所提供的 176 | Containner服务,本节我们就NodeManager与ResourceManager交互进行分析。从逻辑上说,这块比Containner服务要简单很多。 177 | 178 | + 开始埋坑日期:2014-11-4 179 | + 坑状态:done 180 | 181 | ### [Hadoop的Metric系统的分析](./hadoop/metric-learn.md) 182 | 对于Hadoop/Spark/HBase此类的分布式计算系统的日常维护,熟读系统的metric信息应该是最重要的技能.本文对Hadoop的metric/metric2的实现进行深究, 183 | 但也仅仅是从实现的角度进行分析,对metric的完全理解需要时间积累,这样才能理解整个系统中每个metric的值对系统的影响. 184 | 在JVM内部,本身也有一套metric系统JMX,通过JMX可以远程查看甚至修改的应用运行时信息,本文将会从JMX开始,一步一步对这几套系统metric的实现进行分析. 185 | 186 | + 开始埋坑日期:2014-10-15 187 | + 期望完成日期:2014-10-30 188 | + 坑状态:doing -------------------------------------------------------------------------------- /Spark-Note.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /hadoop/datanode.md: -------------------------------------------------------------------------------- 1 | # DataNode分析 2 | 3 | HDFS分为DataNode和NameNode,其中NameNode提供了Meta信息的维护,而DataNode提供了真实文件块的存储和读写;NameNode是在内存中维护整个文件树,很吃内存,但是在NameNode内存充足的时候,基本上NameNode不会成为性能的瓶颈,反而DataNode,它提供了真实Block的读写,整体HDFS性能是否满足需求,就要看DataNode了。 4 | 5 | ## DataNode向NameNode提供Block存储“管理”功能 6 | Namespaces和BlockPool是NameNode和DataNode管理文件目录树/Block的单位;每个NameNode都属于一个Namespace,它维护了该文件系统上的文件目录树,其中目录的信息只需要维护在NameNode上,而文件Block信息与读写操作需要DataNode参与,即每一个NameNode上的Namespace在DataNode上都对应一个BlockPool; 7 | 8 | 对于一个HDFS集群cluster来说,传统只有一组namenode(这里的一组指的是HA,但是任何时间都只有一个NameNode提供服务)维护一个Namespace和一组datanode,每个datanode都维护一个BlockPool;但是在现在支持Federation功能的集群上,一个cluster是可以包含多组namenode,每组namenode独立维护一个Namespace,但是datanode不是独立的,此时每个datanode需要针对每个Namespace都维护一个BlockPool; 9 | 10 | ![enter image description here](http://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/images/federation.gif) 11 | 12 | DataNode通过BlockPool给NameNode提供了Block管理的功能,但是NameNode从不主动的去请求DataNode去做何种动作,而是DataNode针对每个BlockPool都维护一个与自己归属的NameNode之间心跳线程,定期的向NameNode汇报自身的状态,在NameNode返回的心跳值中会携带相关的Command命令信息,从而完成NameNode对DataNode控制。 13 | 14 | ![Alt text](../image/BlockPoolManager.png) 15 | 16 | 在DataNode中,对一组BlockPool的管理是通过BlockPoolManager这个类来提供的,在DataNode启动时,会初始化一个该对象,并从配置中读取该datanode需要服务的BlockPool,并针对每个BlockPool初始化一个BPOfferService,并与相应的NameNode完成注册以及心跳的维护。 17 | 18 | //DataNode中的逻辑 19 | blockPoolManager = new BlockPoolManager(this); 20 | //从配置中读取datanode需要服务的BlockPool 21 | blockPoolManager.refreshNamenodes(conf); 22 | //BlockPoolManager中的逻辑 23 | for (String nsToAdd : toAdd) { 24 | ArrayList addrs = 25 | Lists.newArrayList(addrMap.get(nsToAdd).values()); 26 | BPOfferService bpos = new BPOfferService(nnAddrs, dn); 27 | bpByNameserviceId.put(nsToAdd, bpos); 28 | offerServices.add(bpos); 29 | } 30 | 31 | 对于支持HA的环境下,每一个NameSpace是有一组而不是一个NameNode,单个Active,多个Standby,那么问题来了?DataNode是只需要和Active注册,交互吗?不,对于每一个BPOfferService,它会与该组的每一个NameNode都维护一个连接以及心跳,其中每个连接表示一个BPServiceActor对象。但是在心跳协议的工作过程中,BPOfferService是不会响应来自Standby NameNode的任何命令信息。 32 | 33 | //BPOfferService中的逻辑 34 | for (InetSocketAddress addr : nnAddrs) { 35 | this.bpServices.add(new BPServiceActor(addr, this)); 36 | } 37 | private BPServiceActor bpServiceToActive = null;//当前处于Active的BPServiceActor 38 | 39 | 对于具体BPServiceActor与NameNode之间的交互包括哪些功能呢? 40 | 41 | - 两次握手:在BPServiceActor初始化过程中,需要与NameNode建立起连接,并完成两次握手;第一次握手是获取NameNode的namespace信息(NamespaceInfo),并针对NameNode的版本进行验证;第二次握手是向NameNode进行register注册,获取注册以后的信息(DatanodeRegistration); 42 | - 心跳协议:BPServiceActor会定期与NameNode维持一个心跳协议;心跳信息除了维持一个节点存在性以外,还会在心跳信息中带上当前BlockPool每一个Volumn的StorageReport,容量信息和有问题的Volume,注意这些都只是信息; 43 | - blockReport:定期心跳只会上报基本容量等信息,而是否上报DataNode存储的Block信息需要根据心跳的返回值来确定(是否设置了fullBlockReportLeaseId);此时BPServiceActor通过Dataset().getBlockReports获取当前BlockPool的Block列表,并完成汇报。 44 | - cacheReport:汇报当前的BlockPool的Dataset().getCacheReport的信息 45 | - reportReceivedDeletedBlocks:向NameNode汇报已经响应删除的Block列表 46 | - reportBadBlocks:汇报有问题的block,在进行block扫描过程中,如果发现block的文件不存在就为有问题的block;(在上传的过程中,如果client没有正常关闭,此时datanode也会有一个处理坏掉block的过程,即reportRemoteBadBlock) 47 | 48 | NameNode对BPServiceActor的操作是通过心跳协议来返回的,其中主要的操作包括: 49 | - DNA_TRANSFER: Block的复制,将本地的一个Block复制(transferBlocks)到目标DataNode 50 | - DNA_INVALIDATE:回收删除Block 51 | - DNA_CACHE:将一组Block进行Cache 52 | - DNA_UNCACHE:将一组Block从Cache中删除 53 | - DNA_SHUTDOWN:关闭DataNode 54 | - DNA_FINALIZE:将当前的BlockPool置为finalize状态 55 | - DNA_RECOVERBLOCK:恢复一个Block,@todo后面会详细的分析Block的恢复 56 | 57 | 上面就是DataNode向NameNode提供Block存储“管理”功能 58 | 59 | ## DataNode提供对Replica副本操作的封装 60 | 在上面小结中,我们分析了DataNode可以为NameNode提供哪些Block管理功能,但是这些都仅是Block信息(BlockInfo)和Pool信息的交互而已,而对具体Block对应的磁盘文件的管理是通过FsDataset这个对象来提供的。 61 | 62 | 另外,在HDFS上,虽然Block是贯穿在整个系统中,但是在DataNode上,用Replica副本这个概念来解释可能更好点。何为副本?在HDFS上,每一个Block都有单副本或者多副本,这些副本分布在所有DataNode上,由NameNode来维护它的存在,而DataNode来提供副本的存储和读写。 63 | 64 | ![Alt text](../image/fsdataset.png) 65 | 66 | FsDataset对象功能分为两个粒度,分别为对Pool和Replica的管理。 67 | - Pool层面的管理DataStorage:如上描述,针对每个BlockPool,DataNode都是独立维护一个Pool管理对象BlockPoolSliceStorage,比如DataNode第一次初始化时候,需要针对每个Pool创造pool根目录,DataNode的升级也需要BlockPoolSliceStorage的支持; 68 | - Replica层面的管理FsVolume:在HDFS的配置中,“dfs.datanode.data.dir”可以指定多个Volume目录,每一个目录就对应这里的一个FsVolume对象,在每一个FsVolume对象内部,针对每个BlockPool都创建一个目录和一个对象对Pool上的Replica文件进行物理层面上的管理,这里对象即为BlockPoolSlice;比如BlockPoolSlice.createRbwFile函数可以在磁盘上指定pool的目录下面创建相应的Block文件;DataNode对Pool磁盘空间的管理,比如一个Pool在磁盘使用多少空间,也是通过BlockPoolSlice.getDfsUsed获取每个pool当前占用的磁盘空间,然后进行求和来获取的。 69 | 70 | DataNode上每个Block都是落地在磁盘上,可以通过blockid快速定位到磁盘路径(DatanodeUtil.idToBlockDir),每一个Block在物理上由Meta和Data两个文件组成,其中Meta中存储了Block的版本,crc32,id,大小,生成时间等信息,从原则上来说,好像是可以不需要在内存中缓存DataNode上存储的Block文件列表,对于Block的操作可以逐步的根据BlockPoll和Blockid定位到磁盘上的文件,但是偏偏在DataNode上维护了一个大Map(ReplicaMap)存储了当前DataNode处理的副本(Map> map)。不懂!!而且需要消耗一个线程DirectoryScanner定期将ReplicaMap中的信息与磁盘中文件进行check(FsDatasetImpl.checkAndUpdate)。 71 | 72 | 针对整个DataNode上每个Volumn目录,都有一个VolumeScanner对象,并由BlockScanner进行集中管理,它们负责对磁盘上存储的副本进行校验(校验的方式和读文件时候逻辑一直,只是把读的文件写到/dev/null中),在写失败的过程中也会针对写失败的文件标记为markSuspectBlock,优先进行扫描,如果扫描过程中发现有问题的Block,会调用datanode.reportBadBlocks向NameNode标记坏掉的Block。 73 | 74 | 75 | fsDataset对磁盘上的Block文件的删除是采用异步线程来处理的即FsDatasetAsyncDiskService,从而不会因为大的Block删除阻塞DataNode的逻辑 76 | 77 | 78 | ## DataNode提供Client文件读写的功能 79 | 在整个HDFS文件操作过程中,DataNode提供了文件内容的最终读写的能力,在DataNode中,整个数据的流动由DataXceiverServer服务来承担,它是基于Peer的通信机制,差不多是唯一一个不基于IPC通信的通信模块。 80 | 81 | 在DataXceiverServer的启动时,会初始化一个peerServer,并循环阻塞在peerServer.accept()上,接受每一个Client的连接。在HDFS中,数据的读写是采用pipeline机制,所有Client可能是客户端,也可能是另外一个DataNode,同时在数据balancer过程中,Client也可能是DataNode,这也是peer的含义,即对等通信,DataNode之间互为Server和Client。关于Peer的细节这里就不分析了,比较简单,在日常的运维的过程中,只需要关注peer连接数是否达到上限(默认是4096,很大了!),peer之间读写是否超时就可以。 82 | 83 | DataXceiverServer是一个PeerServer容器,每一个与当前DataNode的连接都会创建Peer,并被DataXceiverServer封装为一个DataXceiver实例,该实例实现了DataTransferProtocol接口,由它来处理peer之间的交互逻辑,其中主要包括下面三个功能: 84 | 85 | public interface DataTransferProtocol { 86 | void readBlock(final ExtendedBlock blk,final long blockOffset,....) 87 | void writeBlock(final ExtendedBlock blk,DatanodeInfo[] targets,....) 88 | void transferBlock(final ExtendedBlock blk,final DatanodeInfo[] targets,...) 89 | ... 90 | } 91 | 92 | 读比较好理解,写和balancer的操作中有一个DatanodeInfo[] targets参数,它即为DataNode写文件的Pipeline特性,即当前DataNode写完以后,需要将Block写到targets目标DataNode。 93 | 94 | Peer之间连接是保持Keepalive,默认超时时间是4000ms,同时连接是是双向的,有Input/OutputStream两个流,那么对于一个Peer,是怎么确定这个Peer的功能呢?是读还是写?在DataXceiver内部是通过一个op的概念来标识的。每次一个流的第一个字节来表标识这个流的功能。目前包括: 95 | 96 | public enum Op { 97 | WRITE_BLOCK((byte)80), 98 | READ_BLOCK((byte)81), 99 | READ_METADATA((byte)82), 100 | REPLACE_BLOCK((byte)83), 101 | COPY_BLOCK((byte)84), 102 | BLOCK_CHECKSUM((byte)85), 103 | TRANSFER_BLOCK((byte)86), 104 | REQUEST_SHORT_CIRCUIT_FDS((byte)87), 105 | RELEASE_SHORT_CIRCUIT_FDS((byte)88), 106 | REQUEST_SHORT_CIRCUIT_SHM((byte)89), 107 | CUSTOM((byte)127); 108 | } 109 | 在DataXceiver.readOp()内部来获取流的功能,并在DataXceiver.processOp(op)中,针对不同的流进行switch匹配和处理。下面我们会针对两个重要的流READ_BLOCK/WRITE_BLOCK进行分析。 110 | 111 | ### READ_BLOCK 112 | 对于READ操作,InputStream在Op字节后有一个序列化的OpReadBlockProto,表示这次读操作的相关参数,其中包括以下参数: 113 | 114 | message OpReadBlockProto { 115 | message ClientOperationHeaderProto { 116 | message BaseHeaderProto { 117 | required ExtendedBlockProto block = 1;//操作的Block 118 | optional hadoop.common.TokenProto token = 2; 119 | optional DataTransferTraceInfoProto traceInfo = 3; 120 | } 121 | required string clientName = 2;//客户端唯一标识 122 | } 123 | required uint64 offset = 2;//对应Block的偏移量 124 | required uint64 len = 3;//读取的长度 125 | optional bool sendChecksums = 4 [default = true]; 126 | optional CachingStrategyProto cachingStrategy = 5; 127 | } 128 | 129 | 其中核心参数就是block+offset+len,它指定需要读取的block的id,偏移量与大小。 130 | 131 | ReadBlock主要包括以下几个步骤: 132 | - DataXceiver首先会向Client发送一个READ操作响应流,其中响应了这次操作结果Status=SUCCESS以及后面发送数据的checksum。 133 | - 随后DataXceiver调用BlockSender将该Block的数据块发送到peer的outputStream中。 134 | - 最后需要等待Client返回一个ClientReadStatusProto来表示这次读取操作的Status结果(SUCCESS/ERROR/......) 135 | 136 | BlockSender内部实现了具体Block流在网络中怎么传输,后面再开一篇具体分析,里面重点逻辑是通过FsDataset.getBlockInputStream获取Block的数据流,并转化为一个一个的Packet发送到对方。 137 | 138 | 139 | ###WRITE_BLOCK 140 | 相比文件读操作,文件写操作要复杂的多,HDFS的Block写是采用pipeline,整个Pipeline包括建立,传输和关闭几个阶段。其中建立和关闭过程是一次请求,传输是反复的过程。 141 | 142 | 1. **Pipeline建立:PIPELINE_SETUP_CREATE/PIPELINE_SETUP_APPEN** 143 | Block写请求会沿着pipeline被多个DataNode接收,每个DataNode会针对写操作创建或打开一个block,并等待后面的数据写操作。末端DataNode接收建立请求以后,会发送一个ACK,并逐级返回给Client,从而完成整个pipeline建立。 144 | 145 | 2. **Pipeline数据传输:** 146 | 与BlockRead一致,Client与DataNode之间的数据传输是以Packet为单位进行传输。具体的细节是封装在BlockReceiver内部,和上面的BlockSender一样,后面会具体的去分析。 147 | 148 | 3. **Pipeline关闭:** 149 | 处于数据传输阶段的Block为RBW,如果当前的block写结束(即最后一个packet写成功:lastPacketInBlock == true),会将当前Block的状态转化为Finalized状态,并向NameNode进行Block汇报。 150 | 151 | Block写的核心内容还是在BlockReceiver内部,后面再详细分析吧,这里对WRITE_BLOCK的了解只需要理解到pipeline层面就够了。 152 | -------------------------------------------------------------------------------- /hadoop/namenode-ha.md: -------------------------------------------------------------------------------- 1 | # NameNode HA解析 2 | 3 | 4 | HA即高可用性,Namenode维护了整个HDFS集群文件的Meta信息,早期设计它为单点结构,而且功能较重,无法做到快速重启。在后续迭代过程中,通过提供SecondNamenode和BackupNamenode的功能支持,可以实现对NamnodeMeta的备份和恢复的功能,但是它们无法做到线上故障的快速恢复,需要人工的介入,即它只是保证了整个HDFS集群Meta信息的不丢失和数据的完整性,而不能保证集群的可用性。 5 | 6 | Namespace是NameNode管理Meta信息的基本单位,通过NamespaceID进行唯一标示。在HA的架构下,对于一个Namespace的管理可以由多个Namenode实例进行管理,每一个NameNode实例被称为一个NamespaceServiceID。但是在任何时刻只有一个Namenode实例为Active,其他为Standby,只有处于Active状态的节点才可以接受Client的操作请求,而其他Standby节点则通过EditLog与Active节点保持同步。 7 | 8 | 同时我们知道,NameNode对Block的维护是不会持久化Block所在的DataNode列表,而是实时接受DataNode对Block的Report。在HA架构中,NameNode之间不会对Block的Report进行同步,而是依赖DataNode主动对每一个NameNode进行Report,即对于一个NameSpace下的BlockPool的管理,每一个DataNode需要明确知道有哪些NamespaceServiceID参与,并主动向其上报Block的信息。 9 | 10 | 因此在任何时刻,这一组NameNode都是处于热状态,随时都可以从Standby状态升级为Active从而为Client提供服务。 11 | 12 | 对于状态迁移,HA架构基于Zookeep提供一个轻量级的主竞选和Failover的功能,即通过一个锁节点,谁拿到这个节点的写权限,即为Active,而其他则降级为Standby,并监听该锁节点的状态变化,只要当前Active释放了写权限,所有Standby即立即竞选成为Active,并对外提供服务。 13 | 14 | ##1. Image与EditLog 15 | NameNode维护了一个Namespace的所有信息,如Namespace中包括的文件列表以及ACL权限信息,而Image即为内存中Namespace序列化到磁盘中的文件。和Mysql之类的存储不同,对Namespace的操作不会直接将其他操作后的结果写入到磁盘中Image文件中,而只是做内存数据结构的修改,并通过写EditLog的方式将操作写入日志文件。而只会在特定的情况下将内存中Namespace信息checkpoint到磁盘的Image文件中。 16 | 17 | 为什么不直接写入Image而转写EditLog文件呢?主要和两个文件格式设计有关系,其中Image文件设计较为紧凑,文件大小只会受NameSpace文件目录个数影响,而与操作次数无关,同时它的设计主要是为了易读取而不宜修改,比如删除一个文件,无法直接对Image文件中的一部分进行修改,需要重写整个Image文件。而EditLog为追加写类型的文件,它记录了对Namespace每一个操作,EditLog文件个数和大小与操作次数直接相关,在频繁的操作下,EditLog文件大小会很大。受此限制,NameNode提供了一定checkpoint机制,可以定时刷新内存中NameSpace信息到Image文件中,或者将老的Image文件与EditLog进行合并,生成一个新的Image文件,进而可以将过期的EditLog文件进行删除,减小对NameNode磁盘的浪费。 18 | 19 | 20 | 另外,在HA架构中,EditLog提供了Active与Standby之间的同步功能,即通过将EditLog重写到Standy节点中,下面开始一一分析它的实现原理。 21 | 22 | ### 1.1 初始化 23 | `dfs.namenode.name.dir`和`dfs.namenode.edits.dir`为NameNode存储Image和EditLog文件目录。在HDFS集群安装过程中,需要进行Format操作,即初始格式化这两个目录,主要有以下几个操作: 24 | 25 | 1. 初始化一个Namespace,即随机分配一个NamespaceID,上面我们谈到Active节点和Standby节点是同时为一个NamespaceID服务,因此我们肯定的是:Format操作只会发生在Active节点上,Standby节点是不需要Format节点。 26 | 2. 将NameSpace信息写入到VERSION文件,并初始化seen_txid文件内容为0。 27 | 28 | namespaceID=972780058 29 | clusterID=yundata 30 | cTime=0 31 | storageType=NAME_NODE 32 | blockpoolID=BP-102718850-10.54.38.52-1459226879449 33 | layoutVersion=-63 34 | 35 | > 关于ClusterID的补充:hdfs namenode -format [-clusterid cid ]。 format操作有一个可选的clusterid参数,默认不提供的情况下,即随机生成一个clusterid。 36 | > 那么为什么会有Cluster概念呢?它和Namespace什么关系?正常情况,我们说一个hdfs集群(cluster),它由一组NameNode维护一个Namespace和一组DataNode而组成。即Cluster和NameSpace是一一对应的。 37 | > 但是在支持Federation功能的集群下,一个Cluster可以由多个NameSpace,即多组Namenode,它主要是解决集群过大,NameNode内存压力过大的情况下,可以对NameNode进行物理性质的划分。 38 | > 你可能会问?那为什么不直接搞多套HDFS集群呢?在没有Federation功能情况下,一般都是这样做的,毕竟NameNode的内存是有限的。但是搞多套HDFS集群对外就由多个入口,而Federation可以保证对外只有一个入口。 39 | 40 | 3. 将当前初始化以后Image文件(只包含一个根目录/)写入到磁盘文件中,并初始化一个Edit文件接受后面的操作日志写。 41 | 4. 格式化其他非FileEditLog(即提供分享功能的EditLog目录。) 42 | 43 | ### 1.2 基于QJM的EditLog分享存储 44 | `dfs.namenode.name.dir`和`dfs.namenode.edits.dir`只是提供本地NameNode对Image和EditLog文件写的路径,而在HA架构中,NameNode之间是需要进行EditLog的同步,此时需要一个"shared storage "来存储EditLog,HDFS提供了NFS和"Quorum Journal Manager"两套解决方案,这里只会对"Quorum Journal Manager"进行分析。 45 | 46 | 如格式化的第四步的描述,需要对非FileEditLog进行格式化,而这里QJM即为非FileEditLog之一。 47 | 48 | 对于HDFS来说,QJM与Zookeeper性质接近,都是是一个独立的共享式服务,也由2N+1个Journal节点组成,为NameNode提供了EditLogOutputStream和EditLogInputStream读写接口。关于读写它由以下几个特点: 49 | 50 | 1. 对于2N+1个节点组成的QJM集群,在写的情况下,不需要保证所有节点都写成功,只需要其中N+1节点(超过1/2)写成功就表示写成功。这也是它为什么叫着Quorum的原因了,即它是基于Paxos算法。 51 | 2. 在读的情况下,它提供了组合接口,即可以将所有Journal节点上的EditLog按照txid进行排序,组合出一个有效的列表,提供NameNode进行读。 52 | 3. 写Journal失败对于NameNode是致命的,NameNode会直接退出。 53 | 54 | QJM是一个较为轻量的集群,运维也比较简单,挂了直接起起来就可以,只要不超过N个节节点挂了,集群都可以正常功能。 55 | 56 | ### 1.3 EditLog Roll的实现 57 | 58 | ### 1.4 Image Checkpoint的实现 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /hadoop/nodemanager-container-monitor.md: -------------------------------------------------------------------------------- 1 | NodeManager解析系列一:内存Monitor解析 2 | ==== 3 | 4 | Yarn调度起来的任务以Container的方式运行在NodeManager节点中,NodeManager负责对Container的运行状态进行监控。在NodeManager中针对每个Container都有一个线程来阻塞并等待Container进程结束. 同时由于启动的进程会递归地生成新的进程,因此Yarn需要对整个进程树进行监控才能正确获取Container所占用的内存等信息。 5 | 6 | 为了控制Container在运行过程中所占用的Memory和Cpu使用情况,NodeManager有两种实现方式: 7 | 8 | + 使用Linux内部的Cgroup来进行监控和限制 9 | + NodeManager中的Monitor线程对运行在该NodeManager上所有的Container进行监控,在发现超过内存限制时,会请求NodeManager杀死相应Container。 10 | 11 | 基于Cgroup的工作方式除了控制内存还可以在Cpu等多个方面进行控制,但除非Yarn集群是完全公用化,需要进行很强度的控制,否则第二种方式基本满足业务的需求。本文也主要针对第二种方式中Container Monitor进行讨论。 12 | 13 | ### 进程基本信息和内存占用 14 | Container Monitor要对Container进行监控,首先第一个需要解决的问题即如何获取每个Container当前资源占用情况. 15 | 16 | NodeManager提供了ResourceCalculatorProcessTree接口来获取进程的基本信息,用户可以通过NodeManager的配置项"yarn.nodemanager.container-monitor.process-tree.class"进行自定义配置。目前,分别针对Window和Linux环境提供来默认的实现,分别为WindowsBasedProcessTree和ProcfsBasedProcessTree的实现。 17 | 18 | 这里简单分析一下Linux环境下的ProcfsBasedProcessTree的实现. 19 | 20 | Linux中,每个运行的进程都在/proc/目录下存在一个子目录,该目录下的所有的文件记录了该进程运行过程中所有信息,通过对这些子文件进行解析,就可以获取进程详细的信息。 其中,ProcfsBasedProcessTree利用到的文件有:`cmdline, stat, smaps` 21 | 22 | cmdline文件中记录该进程启动的命令行信息,如果是java程序,就相当于通过命令"jps -v"获取的进程信息,不过cmdline记录文件中用\0来代替空格,需要做一次反替代。 23 | 24 | work@node:~$ cat /proc/6929/cmdline 25 | /home/work/opt/jdk1.7/bin/java-Xms128m-Xmx750m-XX:MaxPermSize=350m-XX:ReservedCodeCacheSize=96m-ea 26 | -Dsun.io.useCanonCaches=false-Djava.net.preferIPv4Stack=true-Djsse.enableSNIExtension=false-XX:+UseCodeCacheFlushing 27 | -XX:+UseConcMarkSweepGC-XX:SoftRefLRUPolicyMSPerMB=50-Dawt.useSystemAAFontSettings=lcd 28 | -Xbootclasspath/a:/home/work/opt/idea/bin/../lib/boot.jar-Didea.paths.selector=IdeaIC13-Djb.restart.code=88com.intellij.idea.Main 29 | 30 | stat文件是一堆数字堆砌而成,其中包含的信息比较多,没有必要可以不全了解。如下 31 | 32 | work@node:~$ cat /proc/6929/stat 33 | 6929 (java) S 6892 1835 1835 0 -1 1077960704 254628 201687 317 391 120399 23093 3098 329 20 0 65 0 99371 3920023552 34 | 206380 18446744073709551615 4194304 4196452 140735679649776 140735679632336 140462360397419 0 0 4096 16796879 35 | 18446744073709551615 0 0 17 1 0 0 0 0 0 6293608 6294244 28815360 140735679656977 140735679657483 36 | 140735679657483 140735679659993 0 37 | 38 | ProcfsBasedProcessTree针对stat文件提供了ProcessInfo类的实现,它通过读取stat文件来动态更新每个进程的基本信息 39 | 40 | private static class ProcessInfo { 41 | private String pid; // process-id=6929 进程号 42 | private String name; // command name=(java) 进程名称 43 | //stat=S 进程状态,R:runnign,S:sleeping,D:disk sleep , T: stopped,T:tracing stop,Z:zombie,X:dead 44 | private String ppid; // parent process-id =6892 父进程ID 45 | private Integer pgrpId; // process group-id=1835 进程组号 46 | private Integer sessionId; // session-id=6723 c该任务所在的会话组ID 47 | private Long utime = 0L; // utime=120399 该任务在用户态运行的时间,单位为jiffies 48 | private BigInteger stime = new BigInteger("0"); // stime=23093 该任务在核心态运行的时间,单位为jiffies 49 | private Long vmem; // 单位(page) 该任务的虚拟地址空间大小 50 | private Long rssmemPage; // (page) 该任务当前驻留物理地址空间的大小 51 | 52 | 其中utime的单位为jiffies可以通过命令`getConf CLK_TCK`获取,page的页大小单位可以通过`getConf PAGESIZE`获得。 53 | 54 | 另外可以通过一定时间间隔内连续两次获取同一个进程的ProcessInfo,利用两次的utime+stime之和的增量值来表示该时间间隔中,进程所消耗的CPU时间片。 55 | 56 | `smaps`文件是在Linux内核 2.6.16中引入了进程内存接口,它相比stat文件中统计的rssmem要更加准确。但是当前的Hadoop版本是默认关闭该功能,用户可以配置yarn.nodemanage.container-monitor.procfs-tree.smaps-based-rss.enabled=true来启用。 57 | 58 | 对于每个进程,`smapes`在逻辑上是由多段虚拟内存端组成,因此统计一个进程树的真实内存大小,需要对进程树中的每个进程的所有虚拟机内存段进行遍历迭代,求出所有的内存和。因此通过`smaps`来获取rss的复杂度比stat文件要高。 59 | 下面为一个内存段的信息。 60 | 61 | 00400000-00401000 r-xp 00000000 08:07 131577 /home/work/opt/jdk1.7/bin/java 62 | //00400000-00401000表示该虚拟内存段的开始和结束位置。 63 | //00000000 该虚拟内存段在对应的映射文件中的偏移量, 64 | //08:07 映射文件的主设备和次设备号 65 | //131577 被映射到虚拟内存的文件的索引节点号 66 | //home/work/opt/jdk1.7/bin/java为被映射到虚拟内存的文件名称 67 | // r-xp为虚拟内存段的权限信息,其中第四个字段表示该端是私有的:p,还是共享的:s 68 | 69 | //进程使用内存空间,并不一定实际分配了内存(VSS) 70 | Size: 4 kB 71 | //实际分配的内存(不需要缺页中断就可以使用的) 72 | Rss: 4 kB 73 | //是平摊共享内存而计算后的使用内存(有些内存会和其他进程共享,例如mmap进来的) 74 | Pss: 4 kB 75 | //和其他进程共享的未改写页面 76 | Shared_Clean: 0 kB 77 | //和其他进程共享的已改写页面 78 | Shared_Dirty: 0 kB 79 | //未改写的私有页面页面 80 | Private_Clean: 4 kB 81 | //已改写的私有页面页面 82 | Private_Dirty: 0 kB 83 | //标记为已经访问的内存大小 84 | Referenced: 4 kB 85 | Anonymous: 0 kB 86 | AnonHugePages: 0 kB 87 | //存在于交换分区的数据大小(如果物理内存有限,可能存在一部分在主存一部分在交换分区) 88 | Swap: 0 kB 89 | //内核页大小 90 | KernelPageSize: 4 kB 91 | //MMU页大小,基本和Kernel页大小相同 92 | MMUPageSize: 4 kB 93 | Locked: 0 kB 94 | VmFlags: rd ex mr mw me dw sd 95 | 96 | 在NodeManager中,每个进程的内存段也由这几部分组成,参考ProcessSmapMemoryInfo的实现 97 | 98 | static class ProcessSmapMemoryInfo { 99 | private int size; 100 | private int rss; 101 | private int pss; 102 | private int sharedClean; 103 | private int sharedDirty; 104 | private int privateClean; 105 | private int privateDirty; 106 | private int referenced; 107 | private String regionName; 108 | private String permission; 109 | } 110 | 111 | 计算整个进程树的RSS,并不是简单的将所有rss相加,而是有一个计算规则。 112 | 113 | + 对于没有w权限的内存段不进行考虑,即权限为r--s和r-xs 114 | + 对于有写权限的内存段,该内存段对应的rss大小为Math.min(info.sharedDirty, info.pss) + info.privateDirty + info.privateClean; 115 | 116 | 如上所说,通过`smaps`文件计算的rss更加准确,但是复杂度要高。一般情况下没有必要开启整个开关,保持默认的关闭即可。 117 | 118 | 另外上述获取的RSS内存大小的大小都为pagesize,如下所示的超过内存被container-monitor杀死的日志: 119 | 120 | Container [pid=21831,containerID=container_1403615898540_0028_01_000044] is running beyond physical memory limits. 121 | Current usage: 1.0 GB of 1 GB physical memory used; 1.9 GB of 3 GB virtual memory used. Killing container. 122 | Dump of the process-tree for container_1403615898540_0028_01_000044 : 123 | |- PID PPID PGRPID SESSID CMD_NAME USER_MODE_TIME(MILLIS) SYSTEM_TIME(MILLIS) VMEM_USAGE(BYTES) RSSMEM_USAGE(PAGES) FULL_CMD_LINE 124 | |- 21837 21831 21831 21831 (java) 2111 116 1981988864 263056 java 125 | 126 | 打印的进程rss大小为263056,而该机器的页大小为4098,那么实际内存大小为1027m。 127 | 128 | ### Container-Monitor的实现 129 | 首先从NodeManager的逻辑结构来解释container-Monitor在其中的位置: 130 | 131 | + 每个NodeManager都一个ContainerManager,负责该节点上所有Container的管理,所有的Container的启停都需要通过ContainerManager进行调度 132 | + ContainerManager管理的Container的启停,在每个Container状态机内部,和向ContainerManager传递ContainerStartMonitoringEvent等事件。 133 | + ContainersMonitor如果接受到START_MONITORING_CONTAINER事件,则向Container-Monitor中提供该Container相关信息并进行监控;如果为STOP_MONITORING_CONTAINER,则将Container从Monitor中移除。 134 | 135 | 对 Container-Monitor有些配置参数可以进行设置: 136 | 137 | + yarn.nodemanager.contain_monitor.interval_ms,设置监控频率,默认为3000ms 138 | + yarn.nodemanager.resource.memory_MB,该项设置了整个NM可以配置调度的内存大小,如果监控发现超过物理内存的80%,会抛出warn信息。 139 | + yarn.nodemanager.vmem-pmem-ratio,默认为2.1,用户app设置单container内存大小是物理内存,通过该比例计算出每个container可以使用的虚拟内存大小。 140 | + yarn.nodemanager.pmem-check-enabled/vmem-check-enabled启停对物理内存/虚拟内存的使用量的监控 141 | 142 | 后面的工作就是启动一个线程(“Container Monitor”)调用ResourceCalculatorProcessTree接口获取每个container的进程树的内存。具体就不分析了,挺简单的!!! 143 | 144 | 这么简单,我写干嘛?好吧!!就当这回忆proc相关信息吧。 145 | 146 | 慢!!!还有一个逻辑很重要,Container是基于进程了来调度,创建子进程采用了“fork()+exec()”的方案,子进程启动的瞬间,它使用的内存量和父进程一致。一个进程使用的内存量可能瞬间翻倍,因此需要对进程进行"age"区分。参考如下代码: 147 | 148 | //其中curMemUsageOfAgedProcesses为age>0的进程占用内存大小,而currentMemUsage不区分age年龄大小 149 | boolean isProcessTreeOverLimit(String containerId, 150 | long currentMemUsage, 151 | long curMemUsageOfAgedProcesses, 152 | long vmemLimit) 153 | { 154 | boolean isOverLimit = false; 155 | if (currentMemUsage > (2 * vmemLimit)) { 156 | LOG.warn("Process tree for container: " + containerId + " running over twice " + "the configured limit. Limit=" + vmemLimit + ", current usage = " + currentMemUsage); 157 | isOverLimit = true; 158 | } else if (curMemUsageOfAgedProcesses > vmemLimit) { 159 | LOG.warn("Process tree for container: " + containerId + " has processes older than 1 " + "iteration running over the configured limit. Limit=" + vmemLimit + ", current usage = " + curMemUsageOfAgedProcesses); 160 | isOverLimit = true; 161 | } 162 | return isOverLimit; 163 | } 164 | 165 | 通过该逻辑,可以避免因为进程新启动瞬间占用的内存翻倍,导致进程被kill的风险。 -------------------------------------------------------------------------------- /hadoop/nodemanager-container-withrm.md: -------------------------------------------------------------------------------- 1 | NodeManager解析系列四:与ResourceManager的交互 2 | ==== 3 | 4 | 在yarn模型中,NodeManager充当着slave的角色,与ResourceManager注册并提供了Containner服务。前面几部分核心分析NodeManager所提供的 5 | Containner服务,本节我们就NodeManager与ResourceManager交互进行分析。从逻辑上说,这块比Containner服务要简单很多。 6 | 7 | ## Containner服务什么时候开始提供服务? 8 | 在NodeManager中,核心服务模块是ContainnerManager,它可以接受来自AM的请求,并启动Containner来提供服务。那么Containner服务什么时候才可以 9 | 提供呢?答案很简单:NodeManager与ResourceManager注册成功以后就可以提供服务。 10 | 11 | 在ContainerManager模块有一个blockNewContainerRequests变量来控制是否可以提供Container服务,如下: 12 | 13 | public class ContainerManagerImpl { 14 | private AtomicBoolean blockNewContainerRequests = new AtomicBoolean(false); 15 | // 16 | startContainers(StartContainersRequest requests) { 17 | if (blockNewContainerRequests.get()) { 18 | throw new NMNotYetReadyException( 19 | "Rejecting new containers as NodeManager has not" 20 | + " yet connected with ResourceManager"); 21 | } 22 | ... 23 | 24 | 在ContainerManager启动时,blockNewContainerRequests变量会被设置true,所有的startContainers操作都会被ContainerManager所拒绝, 25 | 那么该变量什么时候被设置为true呢?回答之前先看NodeManager与ResourceManager之间的通信协议 26 | 27 | ## NodeManager与ResourceManager之间的通信协议:ResourceTracker 28 | NodeManager以Client的角色与ResourceManager之间进行RPC通信,通信协议如下: 29 | 30 | public interface ResourceTracker { 31 | public RegisterNodeManagerResponse registerNodeManager( 32 | RegisterNodeManagerRequest request) throws YarnException, 33 | IOException; 34 | public NodeHeartbeatResponse nodeHeartbeat(NodeHeartbeatRequest request) 35 | throws YarnException, IOException; 36 | } 37 | 38 | NodeManager与ResourceManager之间操作由registerNodeManager和nodeHeartbeat组成,其中前者在NodeManager启动/同步时候进行注册,后者 39 | 在NodeManager运行过程中,周期性向ResourceManager进行汇报。 40 | 41 | ###先看registerNodeManager的参数和返回值: 42 | 43 | message RegisterNodeManagerRequestProto { 44 | optional NodeIdProto node_id = 1; 45 | optional int32 http_port = 3; 46 | optional ResourceProto resource = 4; 47 | optional string nm_version = 5; 48 | repeated NMContainerStatusProto container_statuses = 6; 49 | repeated ApplicationIdProto runningApplications = 7; 50 | } 51 | message RegisterNodeManagerResponseProto { 52 | optional MasterKeyProto container_token_master_key = 1; 53 | optional MasterKeyProto nm_token_master_key = 2; 54 | optional NodeActionProto nodeAction = 3; 55 | optional int64 rm_identifier = 4; 56 | optional string diagnostics_message = 5; 57 | optional string rm_version = 6; 58 | } 59 | message NodeIdProto { 60 | optional string host = 1; 61 | optional int32 port = 2; 62 | } 63 | message ResourceProto { 64 | optional int32 memory = 1; 65 | optional int32 virtual_cores = 2; 66 | } 67 | 68 | RegisterNodeManagerRequestProto为NodeManager注册提供的参数: 69 | 70 | + NodeIdProto表示当前的NodeManager的ID,它由当前NodeManager的RPC host和port组成。 71 | + ResourceProto为当前NodeManager总共可以用来分配的资源,分为内存资源和虚拟core个数: 72 | + yarn.nodemanager.resource.memory-mb配置可以使用的物理内存大小。 73 | + yarn.nodemanager.vmem-pmem-ratio可以使用的虚拟机内存比例,默认为2.1。 74 | + yarn.nodemanager.resource.cpu-vcores当前使用的虚拟机core个数。 75 | 其中ResourceProto由resource.memory-mb和resource.cpu-vcores组成 76 | + http_port为当前NodeManager的web http页面端口 77 | + NMContainerStatusProto和ApplicationIdProto为当前NodeManager中运行的Container和App状态, 78 | 对于新启动的NodeManager都为空,但是NodeManager可以多次注册,即RESYNC过程。此时这两项都不为空 79 | 80 | RegisterNodeManagerResponseProto为NodeManager注册返回信息: 81 | 82 | + MasterKeyProto/MasterKeyProto/都为相应的token信息 83 | + rm_identifier/rm_version为ResourceManager的标示和版本信息 84 | + NodeActionProto为ResourceManager通知NodeManager所进行的动作,包括RESYNC/SHUTDOWN和默认的NORMAL。 85 | + diagnostics_message为ResourceManager为该NodeManager提供的诊断信息 86 | 87 | 在NodeManager初始化过程中,即会发起注册的动作,如下: 88 | 89 | protected void registerWithRM(){ 90 | List containerReports = getNMContainerStatuses(); 91 | RegisterNodeManagerRequest request = 92 | RegisterNodeManagerRequest.newInstance(nodeId, httpPort, totalResource, 93 | nodeManagerVersionId, containerReports, getRunningApplications()); 94 | if (containerReports != null) { 95 | LOG.info("Registering with RM using containers :" + containerReports); 96 | } 97 | RegisterNodeManagerResponse regNMResponse = 98 | resourceTracker.registerNodeManager(request);//发起注册 99 | 100 | this.rmIdentifier = regNMResponse.getRMIdentifier(); 101 | // 被ResourceManager告知需要关闭当前NodeManager 102 | if (NodeAction.SHUTDOWN.equals(regNMResponse.getNodeAction())) { 103 | throw new YarnRuntimeException(); 104 | } 105 | 106 | //NodeManager需要与ResourceManager之间必须版本兼容 107 | if (!minimumResourceManagerVersion.equals("NONE")){ 108 | if (minimumResourceManagerVersion.equals("EqualToNM")){ 109 | minimumResourceManagerVersion = nodeManagerVersionId; 110 | } 111 | String rmVersion = regNMResponse.getRMVersion(); 112 | if (rmVersion == null) { 113 | throw new YarnRuntimeException("Shutting down the Node Manager. " 114 | + message); 115 | } 116 | if (VersionUtil.compareVersions(rmVersion,minimumResourceManagerVersion) < 0) { 117 | throw new YarnRuntimeException("Shutting down the Node Manager on RM " 118 | + "version error, " + message); 119 | } 120 | } 121 | MasterKey masterKey = regNMResponse.getContainerTokenMasterKey(); 122 | if (masterKey != null) { 123 | this.context.getContainerTokenSecretManager().setMasterKey(masterKey); 124 | } 125 | masterKey = regNMResponse.getNMTokenMasterKey(); 126 | if (masterKey != null) { 127 | this.context.getNMTokenSecretManager().setMasterKey(masterKey); 128 | } 129 | //设置blockNewContainerRequests为false 130 | ((ContainerManagerImpl) this.context.getContainerManager()) 131 | .setBlockNewContainerRequests(false); 132 | } 133 | 134 | 从上面注释我们可以看到registerWithRM主要进行的操作有: 135 | + 向ResourceManager发送registerNodeManager RPC请求 136 | + 如果RPC返回的动作为ShutDown,即立即关闭NodeManager 137 | + 进行版本检查,保存ResourceManager返回的各种token。 138 | + 最后一个动作很重要,设置ContainerManager的blockNewContainerRequests为false,此时ContainerManager可以接受AM的请求。 139 | 140 | ###NodeManager与ResourceManager之间的心跳协议:nodeHeartbeat 141 | 142 | message NodeHeartbeatRequestProto { 143 | optional NodeStatusProto node_status = 1; 144 | optional MasterKeyProto last_known_container_token_master_key = 2; 145 | optional MasterKeyProto last_known_nm_token_master_key = 3; 146 | } 147 | message NodeStatusProto { 148 | optional NodeIdProto node_id = 1; 149 | optional int32 response_id = 2; 150 | repeated ContainerStatusProto containersStatuses = 3; 151 | optional NodeHealthStatusProto nodeHealthStatus = 4; 152 | repeated ApplicationIdProto keep_alive_applications = 5; 153 | } 154 | message NodeHeartbeatResponseProto { 155 | optional int32 response_id = 1; 156 | optional MasterKeyProto container_token_master_key = 2; 157 | optional MasterKeyProto nm_token_master_key = 3; 158 | optional NodeActionProto nodeAction = 4; 159 | repeated ContainerIdProto containers_to_cleanup = 5; 160 | repeated ApplicationIdProto applications_to_cleanup = 6; 161 | optional int64 nextHeartBeatInterval = 7; 162 | optional string diagnostics_message = 8; 163 | } 164 | 165 | nodeHeartbeat是一个周期性质的心跳协议,每次心跳最重要的是向ResourceManager汇报自己的NodeStatus,即NodeStatusProto,它包含当前 166 | NodeManager所有调度的Container的状态信息,Node的健康信息(后面会详细解析),以及当前处于running的App列表。 167 | 168 | NodeHeartbeatResponseProto为周期性心跳ResourceManager返回的信息,其中NodeActionProto可以实现ResourceManager关闭当前正在运行NodeManager的功能。 169 | 另外一个很重要的时候,containers_to_cleanup和applications_to_cleanup可以用来清理当前节点上相应的Container和App。 170 | 171 | 从实现上看,nodeHeartbeat应该是一个线程,周期性的调用nodeHeartbeat协议,逻辑代码如下: 172 | 173 | protected void startStatusUpdater() { 174 | statusUpdaterRunnable = new Runnable() { 175 | public void run() { 176 | int lastHeartBeatID = 0; 177 | while (!isStopped) { 178 | try { 179 | NodeHeartbeatResponse response = null; 180 | NodeStatus nodeStatus = getNodeStatus(lastHeartBeatID); 181 | 182 | NodeHeartbeatRequest request =; 183 | response = resourceTracker.nodeHeartbeat(request); 184 | 185 | //get next heartbeat interval from response 186 | nextHeartBeatInterval = response.getNextHeartBeatInterval(); 187 | updateMasterKeys(response); 188 | 189 | if (response.getNodeAction() == NodeAction.SHUTDOWN) { 190 | context.setDecommissioned(true); 191 | dispatcher.getEventHandler().handle( 192 | new NodeManagerEvent(NodeManagerEventType.SHUTDOWN)); 193 | break; 194 | } 195 | if (response.getNodeAction() == NodeAction.RESYNC) { 196 | dispatcher.getEventHandler().handle( 197 | new NodeManagerEvent(NodeManagerEventType.RESYNC)); 198 | break; 199 | } 200 | removeCompletedContainersFromContext(); 201 | 202 | lastHeartBeatID = response.getResponseId(); 203 | List containersToCleanup = response.getContainersToCleanup(); 204 | if (!containersToCleanup.isEmpty()) { 205 | dispatcher.getEventHandler().handle( 206 | new CMgrCompletedContainersEvent(containersToCleanup, 207 | CMgrCompletedContainersEvent.Reason.BY_RESOURCEMANAGER)); 208 | } 209 | List appsToCleanup =response.getApplicationsToCleanup(); 210 | trackAppsForKeepAlive(appsToCleanup); 211 | if (!appsToCleanup.isEmpty()) { 212 | dispatcher.getEventHandler().handle( 213 | new CMgrCompletedAppsEvent(appsToCleanup, 214 | CMgrCompletedAppsEvent.Reason.BY_RESOURCEMANAGER)); 215 | } 216 | } catch (ConnectException e) { 217 | dispatcher.getEventHandler().handle( 218 | new NodeManagerEvent(NodeManagerEventType.SHUTDOWN)); 219 | throw new YarnRuntimeException(e); 220 | } catch (Throwable e) { 221 | LOG.error("Caught exception in status-updater", e); 222 | } finally { 223 | synchronized (heartbeatMonitor) { 224 | nextHeartBeatInterval = nextHeartBeatInterval <= 0 ? 225 | YarnConfiguration.DEFAULT_RM_NM_HEARTBEAT_INTERVAL_MS : 226 | nextHeartBeatInterval; 227 | try { 228 | heartbeatMonitor.wait(nextHeartBeatInterval); 229 | } 230 | } 231 | } 232 | } 233 | } 234 | }; 235 | statusUpdater = 236 | new Thread(statusUpdaterRunnable, "Node Status Updater"); 237 | statusUpdater.start(); 238 | } 239 | 240 | 主要的工作有: 241 | + 构造nodeStatus,其中包括当前节点所有的信息,并调用nodeHeartbeat向ResourceManager发送心跳协议 242 | + 处理ResourceManager对当前节点的SHUTDOWN和RESYNC 243 | + 处理ResourceManager返回的containers_to_cleanup和applications_to_cleanup 244 | + 每次心跳的间隔可以根据ResourceManager的当前情况,从心跳返回值中获取,从而可以控制NodeManager向ResourceManager的频率 245 | 246 | ----------- 247 | end -------------------------------------------------------------------------------- /hbase/hbase-bulk-loading.md: -------------------------------------------------------------------------------- 1 | HBase Bulk Loading的实践 2 | === 3 | 4 | > ps:文章最后谈及BulkLoad实践过程中遇到的问题。 5 | 6 | 下面代码的功能启动一个mapreduce过程,将hdfs中的文件转化为符合指定table的分区的HFile,并调用LoadIncrementalHFiles将它导入到HBase已有的表中 7 | 8 | public static class ToHFileMapper 9 | extends Mapper{ 10 | Random random = new Random(); 11 | ImmutableBytesWritable oKey = new ImmutableBytesWritable(); 12 | public void map(Object key, Text value, Context context) 13 | throws IOException, InterruptedException { 14 | KeyValueBuilder builder = new FileMetaBuilder(); 15 | Iterator keyValues = builder.getKeyValueFromRow(value.toString()); 16 | oKey.set(builder.getKey(value.toString())); 17 | while(keyValues.hasNext()) { 18 | KeyValue tmp = keyValues.next(); 19 | context.write(oKey, tmp); 20 | } 21 | } 22 | } 23 | 24 | public static void run(String fileMetaPath, String table) throws Exception{ 25 | String tmpPath = fileMetaPath.trim() + "_" + System.currentTimeMillis(); 26 | Configuration conf = new Configuration(); 27 | conf.set("hbase.zookeeper.quorum", ToHFile.zkQuorum); 28 | Job job = new Job(conf); 29 | job.setJobName(ToHFile.class.getName()); 30 | job.setJarByClass(ToHFile.class); 31 | job.setMapperClass(ToHFileMapper.class); 32 | //关键步骤 33 | HFileOutputFormat.configureIncrementalLoad(job, new HTable(conf, table)); 34 | FileInputFormat.addInputPath(job, new Path(fileMetaPath)); 35 | FileOutputFormat.setOutputPath(job, new Path(tmpPath)); 36 | job.waitForCompletion(true) ; 37 | 38 | conf.set("fs.default.name",ToHFile.hdfsV1Name); 39 | LoadIncrementalHFiles load = new LoadIncrementalHFiles(conf); 40 | load.run(new String[]{tmpPath,table}); 41 | 42 | FileSystem hdfs = FileSystem.get(conf); 43 | hdfs.delete(new Path(tmpPath),true); 44 | } 45 | 46 | 细节描述: 47 | 48 | + ToHFileMapper是一个Map过程,它读取一个HDFS文件,并输出key=ImmutableBytesWritable,Value=KeyValue类型的kv数据. 49 | KeyValue类型为HBase中最小数据单位,即为一个cell,它由rowKey,family,qualifiers,timestamp,value大小,value值组成,参考下列的可视化输出: 50 | K: 59129_3471712620_1374007953/f:status/1413288274401/Put/vlen=1/ts=0 V: 0 51 | 我们都知道HBase中数据是按照KV的格式进行组织和存储,在HBase层面它的key是rowKey,但是HFile层面,这里的key不仅仅是rowKey,参考上面的输出中K, 52 | 它由rowKey/family:qualifier/timestamp/类型/vlen=Value的大小/ts组成. 而Value就为对应的值. 53 | 我们可以通过KeyValue的API进行设置其中的每个字段的值,从而输出一条cell.注意mysql中一条记录中的每个字段对应HBase中一个cell,所以一条记录会输出多个cell. 54 | 55 | + ToHFileMapper输出的Key的类型ImmutableBytesWritable,我们必须设置它的值为该cell的rowKey, 具体原因呢: 56 | 我们知道HBase中数据按照rowKey进行划分为多个region,每个region维护一组HFile文件,因此region之间的数据是严格有序,单个region中单个HFile的内部cell也是严格有序, 57 | 但单个region中多个HFile之间不要求有序. 58 | 这种有序性的要求也是为什么我们可以把一个HFile直接加载到HBase中的原因.对于原始数据,在map阶段将key设置为rowKey,采用特殊的分区的函数, 59 | 从而可以实现将属于同一个region的数据发送到同一个reduce,在reduce里面我们按照cell的有序,写入单个HFile中,这样我们就保证了region之间的有序,单个HFile有序性. 60 | 61 | + 上面我们谈到了根据key=rowKey进行分区,将属于同一个region的数据发送到同一个reduce中进行处理.但是在我们job的配置过程中,我们没有配置reduce,没有配置分区函数 62 | 而是通过调用HFileOutputFormat的configureIncrementalLoad函数进行操作,该函数接受一个HBase的Table对象,利于该Table的性质设置job相应的属性;参考下面的源码 63 | 64 | public static void configureIncrementalLoad(Job job, HTable table) throws IOException { 65 | Configuration conf = job.getConfiguration(); 66 | //return org.apache.hadoop.mapreduce.lib.partition.TotalOrderPartitioner 67 | Class topClass = getTotalOrderPartitionerClass(); 68 | job.setPartitionerClass(topClass);//设置分区函数 69 | job.setOutputKeyClass(ImmutableBytesWritable.class); 70 | job.setOutputValueClass(KeyValue.class); 71 | job.setOutputFormatClass(HFileOutputFormat.class);//设置OutPut 72 | //设置reduce函数 73 | if (KeyValue.class.equals(job.getMapOutputValueClass())) { 74 | job.setReducerClass(KeyValueSortReducer.class); 75 | } else if (Put.class.equals(job.getMapOutputValueClass())) { 76 | job.setReducerClass(PutSortReducer.class); 77 | } 78 | } 79 | configureIncrementalLoad对Job的分区函数,reducer,output进行设置,因此对原始row数据转换为HFile,仅仅需要配置一个Map就可以了.其中reducer的实现也很简单,代码如下: 80 | 81 | protected void reduce(ImmutableBytesWritable row, 82 | Iterable kvs,Context context) throws IOException, InterruptedException { 83 | TreeSet map = new TreeSet(KeyValue.COMPARATOR); 84 | for (KeyValue kv: kvs) { 85 | map.add(kv.clone()); 86 | } 87 | int index = 0; 88 | for (KeyValue kv: map) { 89 | context.write(row, kv); 90 | } 91 | } 92 | 内部维护TreeSet,保证单HFile内部的cell之间有序,进而将他们输出到HFile中. 93 | 94 | + HFile结果输出.上述我们描述了Table,Region,HFile之间关系,其中我们没有对family进行考虑,在每个Region中,Family为管理的最大单位,它为每个rowKey的每个Family 95 | 维护一个单独的store(menstore+HFile组成).因此HFile的输出也是按照Family+region进行分开组织的.具体的结构这里就不描述了. 96 | 97 | + 输出HFile的目录可以直接作为LoadIncrementalHFiles的参数,再加上一个table参数,就可以直接将目录下的HFile"move"到HBase特定目录下面.代码如下: 98 | 99 | LoadIncrementalHFiles load = new LoadIncrementalHFiles(conf); 100 | load.run(new String[]{tmpPath,table}); 101 | 本来打算详细写一下LoadIncrementalHFiles的实现,但是通读了一下这块的实现,其实很简单的,首先确认每个HFile该放到哪个region里 102 | (代码实现允许单个大HFile跨多个region,内部会自动对文件进行切割到两个region里), 然后连接每个HFile所对应的regionServer,做server端的文件Move操作. 103 | 具体就不写了. 104 | 105 | 一切就这么简单,就可以大吞吐的将数据导入到HBase中,大幅度的减少HDFS的IO压力. 106 | 107 | ###运行过程中遇到的关于reduce提前启动的问题 108 | 109 | >在Hadoop中,mapred.reduce.slowstart.completed.maps默认配置为5%,即在Mapper运行到5%就提前启动reducer过程,之所以这样的设计的主要优点是可以提前启动 110 | >reducer的shuffle过程,从而并行提高reduce执行效率。 111 | >但是在bulk load过程因为这个而导致性能很差,主要的原因我们hbase启动了预分区为1000,reduce的数目很多,如果预启动reducer,就会出现reducer与mapper进行资源 112 | >竞争的情况,从而拖累了整个job的执行。 113 | >当然主要的原因是我们hadoop很穷。。。 114 | > 115 | 116 | ###基于KeyValue的错误实践 117 | 118 | >上面的实现方式中,map的输出是基于,因此每条记录都会有多条map io输出操作,这是一个很严重的性能问题。会导致 119 | >shuffle操作的负载很高,所以上面的实践是一个错误。目前我使用的HBase版本为0.94,在configureIncrementalLoad中,我们看到有这样一行代码: 120 | 121 | if (KeyValue.class.equals(job.getMapOutputValueClass())) { 122 | job.setReducerClass(KeyValueSortReducer.class); 123 | } else if (Put.class.equals(job.getMapOutputValueClass())) { 124 | job.setReducerClass(PutSortReducer.class); 125 | } 126 | 127 | > 即HBase本身会针对map的value输出类型不同,而使用不同reduce,而我就傻啦吧唧了使用了第一种KeyValueSortReducer.class。 128 | >KeyValueSortReducer.class和PutSortReducer.class的区别是一个put可以由多个rowkey相同的keyvalue组成,可以很大程度上减少map输出。 129 | >此时的逻辑如下 130 | 131 | public void map(Object key, Text value, Context context) 132 | throws IOException, InterruptedException { 133 | KeyValueBuilder builder = new FileMetaBuilder(); 134 | Put put = builder.getPutFromRow(value.toString()); 135 | if(put == null){ 136 | return; 137 | } 138 | oKey.set(put.getRow()); 139 | context.write(oKey,put); 140 | } 141 | //并配置job的map的key和value类型 142 | job.setMapOutputKeyClass(ImmutableBytesWritable.class); 143 | job.setMapOutputValueClass(Put.class); 144 | -------------------------------------------------------------------------------- /hbase/hbase-filter.md: -------------------------------------------------------------------------------- 1 | HBase Filter学习 2 | ============== 3 | Hbase中针对GET和SCAN操作提供了filter(过滤器)的功能,从而可以实现row过滤和column过滤,在最近项目中正好要大量使用Filter来进行查询, 4 | 下面我们从api的层面对Hbase的filter进行整理。 5 | 6 | 重要:Filter是一个名词.站在应用的角度来看,Filter是过滤出我们想要的数据.但是站在HBase源码的角度,filter是指一条记录是否被过滤.参考下面的例子: 7 | 8 | public boolean filterRowKey(byte[] buffer, int offset, int length) throws IOException { 9 | return false; 10 | } 11 | 12 | filterRowKey是对RowKey进行过滤,如果没有被过滤,是返回false,即INCLUDE在返回列表中,而true则理解为被过滤掉,即EXCLUDE. 13 | 14 | 这点不同在理解Filter的实现很重要. 下面我们来开始看Filter的实现. 15 | 16 | ## Filter的实现 17 | 18 | public abstract class Filter { 19 | abstract public boolean filterRowKey(byte[] buffer, int offset, int length) throws IOException; 20 | abstract public boolean filterAllRemaining() throws IOException; 21 | abstract public ReturnCode filterKeyValue(final Cell v) throws IOException; 22 | abstract public Cell transformCell(final Cell v) throws IOException; 23 | abstract public void filterRowCells(List kvs) throws IOException; 24 | abstract public boolean filterRow() throws IOException; 25 | } 26 | 27 | Filter是所有Filter的基类,针对RowKey,Cell都提供了过滤的功能,现在一个问题来了,对于上面那么多过滤接口,在针对一个Row过滤,这些接口调用次序是 28 | 什么样呢?下面我就按照调用顺序来解释每个接口的功能. 29 | 30 | + filterRowKey:对rowKey进行过滤,如果rowKey被过滤了,那么后面的那些操作都不需要进行了 31 | + 针对Row中cell进行过滤,由于一个row含有多个cell,因此这是一个循环过程 32 | + filterAllRemaining:是否需要结束对这条记录的filter操作 33 | + filterKeyValue:对每个cell进行过滤 34 | + transformCell:如果一个cell通过过滤,我们可以对过滤后的cell进行改写/转变 35 | + filterRowCells:对通过cell过滤后的所有cell列表进行修改 36 | + filterRow:站在row整体角度来进行过滤 37 | 38 | Filter在HBase里面有两大类,一种是集合Filter,一种是单个Filter;下面我们一一进行分析. 39 | 40 | ##FilterList 41 | FilterList是一个Filter的集合,所谓集合Filter其实就是提供Filter或/Filter集合之间的And和Or组合.每个FilterList由两部分组成: 42 | 43 | + 一组Filter子类组成的Filter集合:这个Filter可以是FilterList也可以是基础Filter,如果是FilterList那么就相当形成了一个树形的过滤器 44 | 45 | private List filters = new ArrayList(); 46 | 47 | + 一组Filter之间的关联关系, 48 | 49 | public static enum Operator { 50 | /** !AND */ 51 | MUST_PASS_ALL, 52 | /** !OR */ 53 | MUST_PASS_ONE 54 | } 55 | 56 | 其他的操作这里就不描述了,本质上就是维护一组Filter之间的逻辑关系而已; 57 | 58 | ## 基础Filter 59 | 60 | 在HBase中,所有基础Filter都继承自FilterBase类,该类提供了所有Filter默认实现,即"不被过滤".比如filterRowKey默认返回false.下面是 61 | 所有实现FilterBase的子Filter,针对几个特定的Filter下面进行分析. 62 | 63 | + org.apache.hadoop.hbase.filter.FilterBase 64 | + org.apache.hadoop.hbase.filter.CompareFilter 65 | + org.apache.hadoop.hbase.filter.DependentColumnFilter 66 | + org.apache.hadoop.hbase.filter.FamilyFilter 67 | + org.apache.hadoop.hbase.filter.QualifierFilter 68 | + org.apache.hadoop.hbase.filter.RowFilter 69 | + org.apache.hadoop.hbase.filter.ValueFilter 70 | + org.apache.hadoop.hbase.filter.ColumnCountGetFilter 71 | + org.apache.hadoop.hbase.filter.ColumnPaginationFilter 72 | + org.apache.hadoop.hbase.filter.ColumnPrefixFilter 73 | + org.apache.hadoop.hbase.filter.ColumnRangeFilter 74 | + org.apache.hadoop.hbase.filter.FirstKeyOnlyFilter 75 | + org.apache.hadoop.hbase.filter.FirstKeyValueMatchingQualifiersFilter 76 | + org.apache.hadoop.hbase.filter.FuzzyRowFilter 77 | + org.apache.hadoop.hbase.filter.InclusiveStopFilter 78 | + org.apache.hadoop.hbase.filter.KeyOnlyFilter 79 | + org.apache.hadoop.hbase.filter.MultipleColumnPrefixFilter 80 | + org.apache.hadoop.hbase.filter.PageFilter 81 | + org.apache.hadoop.hbase.filter.PrefixFilter 82 | + org.apache.hadoop.hbase.filter.RandomRowFilter 83 | + org.apache.hadoop.hbase.filter.SingleColumnValueFilter 84 | + org.apache.hadoop.hbase.filter.SingleColumnValueExcludeFilter 85 | + org.apache.hadoop.hbase.filter.SkipFilter 86 | + org.apache.hadoop.hbase.filter.TimestampsFilter 87 | + org.apache.hadoop.hbase.filter.WhileMatchFilter 88 | 89 | ###CompareFilter:比较器过滤器 90 | 91 | CompareFilter是最常用的一组Filter,它提供了针对RowKey,Family,Qualifier,Column,Value的过滤,首先它是"比较过滤器",HBase针对比较提供了 92 | CompareOp和一个可被比较的对象ByteArrayComparable. 93 | 94 | 其中CompareOP抽象了关系比较符, 95 | 96 | public enum CompareOp { 97 | /** less than */ 98 | LESS, 99 | /** less than or equal to */ 100 | LESS_OR_EQUAL, 101 | /** equals */ 102 | EQUAL, 103 | /** not equal */ 104 | NOT_EQUAL, 105 | /** greater than or equal to */ 106 | GREATER_OR_EQUAL, 107 | /** greater than */ 108 | GREATER, 109 | /** no operation */ 110 | NO_OP, 111 | } 112 | 113 | 而ByteArrayComparable提供了可以参考的比较对象,比如过滤掉Value为Null 114 | 115 | new ValueFilter(CompareOp.NOT_EQUAL, new NullComparator()); 116 | 117 | 目前支持的ByteArrayComparable有: 118 | 119 | + org.apache.hadoop.hbase.filter.ByteArrayComparable 120 | + org.apache.hadoop.hbase.filter.BinaryComparator:对两个字节数组做Bytes.compareTo比较. 121 | + org.apache.hadoop.hbase.filter.BinaryPrefixComparator:和BinaryComparator基本一直,但是考虑到参考值和被比较值之间的长度 122 | + org.apache.hadoop.hbase.filter.BitComparator:位计算比较器,通过与一个byte数组做AND/OR/XOR操作,判读结果是否为0 123 | + org.apache.hadoop.hbase.filter.NullComparator: 是否为空比较 124 | + org.apache.hadoop.hbase.filter.RegexStringComparator: 正则比较,即是否符合指定的正则 125 | + org.apache.hadoop.hbase.filter.SubstringComparator: 子字符串比较 126 | 127 | 从上面的描述我们可以看到BitComparator/NullComparator/RegexStringComparator/SubstringComparator只返回0或者1,即一般只会在上面进行 128 | EQUAL和NOT_EQUAL的CompareOp操作.而BinaryComparator/BinaryPrefixComparator存在-1,0,1,可以进行LESS和GREATER等比较. 129 | 130 | CompareFilter下面的五个比较器是提供了对Row多个层面进行filter,分别描述如下: 131 | 132 | + org.apache.hadoop.hbase.filter.FamilyFilter:对Family进行过滤 133 | + org.apache.hadoop.hbase.filter.QualifierFilter:对Qualifier进行过滤 134 | + org.apache.hadoop.hbase.filter.RowFilter:对RowKey进行过滤 135 | + org.apache.hadoop.hbase.filter.ValueFilter:对所有Cell的value进行过滤,没有区分是哪个Column的值,是针对所有Column 136 | + org.apache.hadoop.hbase.filter.DependentColumnFilter 137 | 138 | 其中RowFilter是提供filterRowKey实现,而QualifierFilter/RowFilter/ValueFilter/DependentColumnFilter都只针对filterKeyValue进行实现. 139 | 140 | 实现和使用都很简单,就不摊开的说了. 141 | 142 | ###org.apache.hadoop.hbase.filter.KeyOnlyFilter 143 | 为什么要挑KeyOnlyFilter出来讲呢?在上面我们谈到Filter的实现中有一个Cell transformCell(final Cell v)的操作,该操作不是过滤而是在过滤以后, 144 | 对Cell进行改写,KeyOnlyFilter就是对transformCell的一个实现. 145 | 146 | public class KeyOnlyFilter extends FilterBase { 147 | boolean lenAsVal; 148 | public Cell transformCell(Cell kv) { 149 | KeyValue v = KeyValueUtil.ensureKeyValue(kv); 150 | return v.createKeyOnly(this.lenAsVal); 151 | } 152 | KeyOnlyFilter有一个参数lenAsVal,值为true和false. KeyOnlyFilter的作用就是将scan过滤后的cell value设置为null,或者设置成原先value的大小(lenAsVal设置为true). 153 | 154 | 这个Filter很好的诠释了transformCell的功能,还有一个用处获取数据的meta信息用于展现. 155 | 156 | ### org.apache.hadoop.hbase.filter.WhileMatchFilter 157 | 上面讨论到KeyOnlyFilter是对transformCell功能的诠释,而这里我们要讲的WhileMatchFilter是对filterAllRemaining进行诠释. 158 | 159 | filterAllRemaining在Scan过程中,是一个前置判读,它确定了是否结束对当前记录的scan操作,即如果filterAllRemaining返回true 160 | 那么当前row的scan操作就结束了 161 | 162 | public MatchCode match(Cell cell) throws IOException { 163 | if (filter != null && filter.filterAllRemaining()) { 164 | return MatchCode.DONE_SCAN; 165 | } 166 | int ret = this.rowComparator.compareRows(row, this.rowOffset, this.rowLength, 167 | cell.getRowArray(), cell.getRowOffset(), cell.getRowLength()); 168 | ... 169 | 170 | 在进行match操作时候,先判读filterAllRemaining是否为true,如果为true,那么就不需要进行后面到compareRows操作,直接返回DONE_SCAN. 171 | 172 | 这里我们谈到的WhileMatchFilter它是一个wrapped filter,含义直译为一旦匹配到就直接过滤掉后面的记录,即结束scan操作. 173 | 它内部通过包装了一个filter,对于外面的WhileMatchFilter,它的假设只要被包装的filter的filterRowKey, filterKeyValue,filterRow,filterAllRemaining 174 | 任何一个filter返回为true,那么filterAllRemaining就会true. 175 | 176 | 它还有其他功能吗?没有! 177 | 178 | ###org.apache.hadoop.hbase.filter.SkipFilter 179 | 上面讨论的WhileMatchFilter仅仅影响filterAllRemaining功能,这里讨论的SkipFilter它是影响filter过程中最后的环节,即:filterRow. 180 | 181 | filterRow是对Row是否进行过滤最后一个环节.本质上,它是一个逻辑判断,并不一定是对rowkey,value指定指定对象进行filter. 182 | 183 | SkipFilter和WhileMatchFilter一样,也是一个wrapped filter,它的功能是如果内部filter任何一个filterKeyValue返回true,那么filterRow就直接返回true, 184 | 即任何一个cell被过滤,那么整条row就被过滤了.所以含义为跳过记录,只有任何一个cell被过滤. 185 | 186 | 还有其他的功能吗?没有!! 187 | 188 | ###org.apache.hadoop.hbase.filter.SingleColumnValueFilter 189 | 到目前为止,我们讨论的filter包括CompareFilter在内,都无法针对如下场景进行过滤:对于一条row,如果指定的colume的值满足指定逻辑,那么该 190 | row就会被提取出来. 上面讨论到ValueFilter是针对所有的所有的column的value进行过滤,而不是特定的column. 191 | 192 | 而这里讨论的SingleColumnValueFilter就是实现这个功能: 193 | 194 | public class SingleColumnValueFilter extends FilterBase { 195 | protected byte [] columnFamily; 196 | protected byte [] columnQualifier; 197 | protected CompareOp compareOp; 198 | protected ByteArrayComparable comparator; 199 | 200 | 和CompareFilter不同,SingleColumnValueFilter需要提供Family和Qualifier.而且在进行filterKeyValue过程中,如果已经找到满足指定filter的cell, 201 | 那么后面cell的filterKeyValue都会返回Include 202 | 203 | ### org.apache.hadoop.hbase.filter.ColumnPrefixFilter 204 | 上面讨论SingleColumnValueFilter解决ValueFilter不能指定column的问题,这里讨论的ColumnPrefixFilter是解决QualifierFilter只能做大小比较, 205 | 或者使用SubstringComparator进行子字符串匹配.这里讨论的ColumnPrefixFilter是要求Column必须满足指定的prefix前缀 206 | 207 | prefix和substring是有区别的,这个可以理解,但是ColumnPrefixFilter是可以从使用RegexStringComparator的QualifierFilter来实现, 208 | 209 | 所以在本质上来说,ColumnPrefixFilter没有什么特殊的. 210 | 和ColumnPrefixFilter相似的两个filter: 211 | 212 | + org.apache.hadoop.hbase.filter.MultipleColumnPrefixFilter,它可以指定多个prefix.本质上也可以用regex来实现 213 | + org.apache.hadoop.hbase.filter.ColumnRangeFilter,支持对Column进行前缀区间匹配,比如匹配column的列名处于[a,e]之间 214 | 215 | 216 | ###org.apache.hadoop.hbase.filter.PageFilter 217 | 这个也是一个很重要的PageFilter,它用于分页,即限制scan返回的row行数.它怎么实现的呢?其实很简单 218 | 219 | 上面我们讨论到filterRow是对当前row进行filter的最后一个环节,如果一个row通过了filterRowKey, filterKeyValue等过滤,此时FilterRow将可以最终确定 220 | 该row是否可以被接受或者被过滤. 221 | 222 | public class PageFilter extends FilterBase { 223 | private long pageSize = Long.MAX_VALUE; 224 | private int rowsAccepted = 0; 225 | 226 | public PageFilter(final long pageSize) { 227 | this.pageSize = pageSize; 228 | } 229 | public ReturnCode filterKeyValue(Cell ignored) throws IOException { 230 | return ReturnCode.INCLUDE; 231 | } 232 | public boolean filterAllRemaining() { 233 | return this.rowsAccepted >= this.pageSize; 234 | } 235 | public boolean filterRow() { 236 | this.rowsAccepted++; 237 | return this.rowsAccepted > this.pageSize; 238 | } 239 | 240 | 每个PageFilter都有一个pageSize,即表示页大小.在进行filterRow时,对已经返回的行数进行+1,即this.rowsAccepted++. filterAllRemaining操作用于 241 | 结束scan操作. 242 | 243 | 和传统的sql不一样,PageFilter没有start和limit,start的功能需要使用Scan.setStartRow(byte[] startRow)来进行设置 244 | 245 | ### org.apache.hadoop.hbase.filter.ColumnCountGetFilter 246 | 该filter不适合Scan使用,仅仅适用于GET 247 | 248 | 上述讨论的PageFilter是针对row进行分页,但是在get一条列很多的row时候,需要ColumnCountGetFilter来limit返回的列的数目. 249 | 250 | public class ColumnCountGetFilter extends FilterBase { 251 | private int limit = 0; 252 | private int count = 0; 253 | public ColumnCountGetFilter(final int n) { 254 | this.limit = n; 255 | } 256 | public boolean filterAllRemaining() { 257 | return this.count > this.limit; 258 | } 259 | public ReturnCode filterKeyValue(Cell v) { 260 | this.count++; 261 | return filterAllRemaining() ? ReturnCode.NEXT_COL : ReturnCode.INCLUDE_AND_NEXT_COL; 262 | } 263 | 264 | 和PageFilter一样,该Filter没有start,可以通过Get.setRowOffsetPerColumnFamily(int offset)进行设置 265 | 266 | ### org.apache.hadoop.hbase.filter.ColumnPaginationFilter 267 | 上面谈到的ColumnCountGetFilter只适合GET,而且不能指定start 268 | 269 | 这里我们谈到的ColumnPaginationFilter就是解决这个问题; 270 | 271 | public class ColumnPaginationFilter extends FilterBase 272 | { 273 | private int limit = 0; 274 | private int offset = -1; 275 | private byte[] columnOffset = null; 276 | public ColumnPaginationFilter(final int limit, final int offset) 277 | { 278 | this.limit = limit; 279 | this.offset = offset; 280 | } 281 | 282 | 它包含limit和offset两个变量用来表示每个row的返回offset-limit之间的列;其中offset可以通过指定的column来指定,即columnOffset 283 | 284 | 从实现角度来看,它就是基于filterKeyValue的NEXT_ROW,NEXT_COL,INCLUDE_AND_NEXT_COL三个返回值来影响column的filter来实现 285 | 286 | public ReturnCode filterKeyValue(Cell v) 287 | { 288 | if (count >= offset + limit) { 289 | return ReturnCode.NEXT_ROW; 290 | } 291 | ReturnCode code = count < offset ? ReturnCode.NEXT_COL : 292 | ReturnCode.INCLUDE_AND_NEXT_COL; 293 | count++; 294 | return code; 295 | } 296 | 297 | org.apache.hadoop.hbase.filter.FirstKeyOnlyFilter是一个极端的limit和offset,即offset=0,limit=1,即每个row只返回第一个column记录 298 | 299 | ###还有最后几个比较简单的filter 300 | 301 | + org.apache.hadoop.hbase.filter.TimestampsFilter:通过指定一个timestamp列表,所有不在列表中的column都会被过滤 302 | + org.apache.hadoop.hbase.filter.RandomRowFilter:每行是否被过滤是按照一定随机概率的,概率通过一个float进行指定 303 | -------------------------------------------------------------------------------- /hbase/hbase-learn.md: -------------------------------------------------------------------------------- 1 | HBase 相关主题总结 2 | ===== 3 | 4 | ## [MSLAB:A memstore-local allocation buffer](http://www.taobaotest.com/blogs/2310) 5 | Memstore功能是保存并索引所有临时的cell,每个cell的在物理内存层面上占用的内存是不连续的,此时如果对menstore进行flush操作,势必就会在内存中 6 | 清除这部分内存,后果就是造成内存碎片,Lab的功能就是预分配一块内存,将所有需要被menstore索引的 cell复制到这块内存中进行管理,从而可以实现对flush以后, 7 | 减小内存碎片. 8 | 9 | 上述文章对MSLAB进行测试,从测试结果来看,优化效果不是特别明显. 10 | 但是重要的是,LAB的内存是预分配的,默认2m,如果单RS上的region太多,会造成内存占用过大的问题.而且默认的"hbase.hregion.memstore.mslab.enabled"是启用的 11 | 12 | ## [单RS上的region个数对性能的影响](http://hbase.apache.org/book/regions.arch.html) 13 | RS以region为管理对象,每个region有自身store管理,在资源上,每个region都有一定的资源占用,因此RS对region管理也是有一定数量的限制, 14 | 15 | + 上述MSLAB是一个预分配的内存,在region中不含任何的数据时候,都会占用2M的内存,如果region过多,哪怕region不被read/write.对内存都是一个占用 16 | + memstore的flush受多个条件控制;RS上所有的region的memstore所占用的堆内存大小由"hbase.regionserver.global.memstore.size"进行设置. 17 | 对于单个memstore,如果大小超过"hbase.hregion.memstore.flush.size",那么会被flush. 18 | 对于整个RS,如果所有的region的memstore大小之和超过"hbase.regionserver.global.memstore.size.lower.limit",也会提前触发flush, 19 | 提前粗放flush也会为后面的compact增加压力 20 | + RS的重启过程需要进行region的迁移,region数目也确定了迁移的效率. 21 | 22 | 总之,RS的能力还是有限,根据官网的建议,100是最好的配置. 23 | 24 | ## [Hbase scheme的设计总结](http://hbase.apache.org/book/schema.html),[资料二](https://communities.intel.com/community/itpeernetwork/datastack/blog/2013/11/10/discussion-on-designing-hbase-tables) 25 | 26 | + CF的设计,尽量保证只有一个CF. 27 | >由于HBASE的FLUSHING和压缩是基于REGION的,当一个列族所存储的数据达到FLUSHING阀值时该表的所有列族将同时进行FLASHING操作 28 | >这将带来不必要的I/O开销。同时还要考虑到同意和一个表中不同列族所存储的记录数量的差别,即列族的势。 29 | >当列族数量差别过大将会使包含记录数量较少的列族的数据分散在多个Region之上,而Region可能是分布是不同的RegionServer上。 30 | >这样当进行查询等操作系统的效率会受到一定影响。 31 | >同时针对CF,rowKey是冗余存储的,在rowkey信息量很大的时候,多个CF对磁盘的浪费会很大. 32 | 33 | + 最小化RowKey,CF,qualifiers的名称. 34 | >在HBase这三个东西都是按照字符进行对待的,在设计的时候尽量将int/float类型转换为byte进行存储,比如rowkey为userid=340827182,为一个4字节int, 35 | >如果直接按照字符串进行编码,每个数字为一个字符,需要占用一个字节而进行byte编码可以控制它的大小. 36 | >特别是CF/qualifiers完成可以采用单字节进行设置,务必不要和传统数据库一样设置很长的名字. 37 | >缺陷是进行byte编码会导致数据看起来不直观, 38 | 39 | + hbase返回的结果按照rowkey,CF,qualifiers,timestamp的次序进行排序,合理设置它们自己值来实现排序的功能. 40 | + 在rowKey中加入散列的hex值来避免region的热点问题.针对时序的数据参考openTSDB的设计. 41 | + 版本的个数:HBase在进行数据存储时,新数据不会直接覆盖旧的数据,而是进行追加操作,不同的数据通过时间戳进行区分.默认每行数据存储三个版本. 42 | + 合理使用最小版本和TTL控制cell的有效期,对于过期自动删除的数据尤其有用 43 | >Time to live (TTL): This specifies the time after which a record is deleted and is by default set to forever. 44 | >This should be changed if you want HBase to delete rows after certain period of the time. 45 | >This is useful in the case when you store data in HBase which should be deleted after aggregation. 46 | > 47 | >Min Version: As we know HBase maintains multiple version of every record as and when they are inserted. 48 | >Min Version should always be used along with TTL. For example if the TTL is set to one year and there is no update on the row 49 | >for that period the row will be deleted. If you don’t want the row to be deleted you would have to set a min version for it such that only 50 | >the files below that version will be deleted. 51 | 52 | + 合理使用blockcache,对于blockcache的问题参考后面详细的学习. 53 | 54 | ## [region 预先Split/自动Split/手动Split的学习](http://zh.hortonworks.com/blog/apache-hbase-region-splitting-and-merging/) 55 | HBase的table的split可以通过pre-splitting,auto-splitting,forced-splitting三个过程来实现. 56 | pre-splitting为预先对region进行切割,可以在create table时指定splits或通过org.apache.hadoop.hbase.util.RegionSplitter工具进行分区 57 | 58 | //自创建cf=f1的test_table表,并使用 SplitAlgorithm. HexStringSplit算法进行pre-splitting, 59 | //或UniformSplit算法 60 | // -c 指定预先分区分区个数 61 | hbase org.apache.hadoop.hbase.util.RegionSplitter test_table HexStringSplit -c 10 -f f1 62 | 63 | //使用create 的SPLITS/SPLITSFILE属性进行设置 64 | create 'test_table', 'f1', SPLITS=['a', 'b', 'c'] 65 | echo -e "a\nb\nc" /tmp/splits 66 | create 'test_table', 'f1', SPLITSFILE=/tmp/splits' 67 | 68 | auto-splitting是一个不受master参与的自动切割的过程."什么时候自动分区"以及"分区选择的中间点"由参数"hbase.regionserver.region.split.policy所配置算法来确定, 69 | 有ConstantSizeRegionSplitPolicy,IncreasingToUpperBoundRegionSplitPolicy,KeyPrefixRegionSplitPolicy等算法 70 | 具体算法参照原文描述: 71 | 72 | >ConstantSizeRegionSplitPolicy is the default and only split policy for HBase versions before 0.94. 73 | It splits the regions when the total data size for one of the stores (corresponding to a column-family) in the region gets bigger than 74 | >configured “hbase.hregion.max.filesize”, which has a default value of 10GB. 75 | > 76 | >IncreasingToUpperBoundRegionSplitPolicy is the default split policy for HBase 0.94, 77 | >which does more aggressive splitting based on the number of regions hosted in the same region server. 78 | >The split policy uses the max store file size based on Min (R^2 * “hbase.hregion.memstore.flush.size”, “hbase.hregion.max.filesize”), 79 | >where R is the number of regions of the same table hosted on the same regionserver. 80 | >So for example, with the default memstore flush size of 128MB and the default max store size of 10GB, the first region on the region server will be split just after the first flush at 128MB. As number of regions hosted in the region server increases, it will use increasing split sizes: 512MB, 1152MB, 2GB, 3.2GB, 4.6GB, 6.2GB, etc. After reaching 9 regions, the split size will go beyond the configured “hbase.hregion.max.filesize”, at which point, 10GB split size will be used from then on. For both of these algorithms, 81 | >regardless of when splitting occurs, the split point used is the rowkey that corresponds to the mid point in the “block index” for the largest store file in the largest store. 82 | > 83 | >KeyPrefixRegionSplitPolicy is a curious addition to the HBase arsenal. You can configure the length of the prefix for your row keys for grouping them, 84 | >and this split policy ensures that the regions are not split in the middle of a group of rows having the same prefix. If you have set prefixes for your keys, 85 | >then you can use this split policy to ensure that rows having the same rowkey prefix always end up in the same region. 86 | >This grouping of records is sometimes referred to as “Entity Groups” or “Row Groups”. 87 | >This is a key feature when considering use of the “local transactions” (alternative link) feature in your application design. 88 | 89 | auto-splitting过程中不会直接进行文件的分割,而是创造两个ref来指向parent_region,在两个子region在后面需要进行compactions,才对原始数据进行rewrite. 90 | 一个region在将ref进行rewrite之前,是不允许再次进行split. 91 | 所以auto-splitting的耗时是可以接受的. 92 | 93 | 如果实在不接受auto-splitting所带来的性能问题,可以设置ConstantSizeRegionSplitPolicy和hbase.hregion.max.filesize足够大来关闭auto-splitting 94 | 使用建议:一般情况下不需要预分配太多splits,让auto-splitting根据每个分区的大小来自动分配可能达到更好的平衡 95 | 96 | forced-splitting:在shell里面可以使用split命令对table,region进行线上强制split. 97 | 98 | ## [OpenTSDB的Scheme设计](http://opentsdb.net/docs/build/html/user_guide/backends/hbase.html) 99 | OpenTSDB基于HBase实现对metric数据进行存储和查询.在详细了解实现原理之前先来看看什么是metric数据. 100 | 101 | 对metric的认识可以有两个视角: 102 | 103 | + 视角1:站在metric统计项的角度来看,metric数据即为一个特定统计项在时间流中每个时间点对应的统计值组成的序列集合. 104 | 首先每个统计项由metricValue表示一个时间点的统计值.其次由于统计项需要针对不同的实体/属性进行区分,每个统计项含有一个metricTag属性,它是一个KV集, 105 | 表示当前统计项的属性集, 106 | 比如针对metric=free-memory的统计项,那么metrixTag=[{'node':'bj'},{'rack':'01'},{'host':'bj-1'}].代表统计的bj节点下01机架上的bj-1机器的free-memory 107 | 如果修改host=bj-2,那么这两个统计项目就是针对不同的实体进行统计. 108 | + 视角2:站在统计实体的角度来看.比如上述的"bj节点下01机架上的bj-1机器"就是一个统计实体,那么在一个时间点下,该实体上所有的统计项目和统计值就组成一个 109 | metric集合,比如上述的实体对应的统计值就为metricValue=[{"cpu idle":20},{"free-memory":40}] 110 | 111 | OpenTSDB是站在视角1来对metric进行处理.因此metricName+metricTag+timestamp和metricValue组成metric统计项目和统计值,在一个时间序列下,就组成统计值序列TS 112 | 113 | 总结OpenTSDB的HBase的scheme设计: 114 | 115 | + DataTable/RowKey的设计亮点: 116 | - 由metricName+timestamp+metricTag 次序组成的key:, 117 | 将timestamp放到metricTag前面利于针对metricTag的不同进行对比 118 | - timestamp存储为小时级别的时间戳,将小时级别以下时间的统计值作为CF的一个Column Qualifiers进行表示,这样有两个好处:一是减少记录行数,二十提高查询吞吐量, 119 | 针对metric类型的统计数据,很少会按分钟级别单条去获取,而是一次按照小时级别获取回来进行绘制统计图. 120 | - tag值kv对按照key的字符序进行排列,从而可以通过设计key值来提高特定tag的优先级,从而实现针对特定tag进行查询的优化. 121 | 122 | + DataTable/Columns的设计亮点: 123 | - 单CF的设计,受HBase的实现原理,单CF是最优化的设计.优点参考上面 124 | - rowKey按照小时级别进行存储,而小时级别以下按照时间偏移量存储为qualifiers, 125 | 针对秒级别和毫秒级别的偏移量,qualifiers分为2字节和4字节两种类型进行区别,其中2字节划分前12位来存储时间偏移(最大表示4095s), 126 | 4字节划分22位(最大表示4194303毫秒)来存储毫秒偏移 ,针对4字节,开头4位用于hex存储,22位存储毫秒偏移,2位保留. 127 | - 对于2字节和4字节两种类型都保留最后的4位用于存储columns值的类型(type)信息. 128 | 该4位中,第一位为0/1来表示值为int/float,后面三位分别取值为000/010/011/100分别表示值的大小为1/2/4/8字节. 129 | - value的存储严格按照1/2/4/8字节大小进行存储,从qualifiers中可以获取值的大小,从而可以最小化存储value. 130 | 131 | + 上述的qualifiers和value都是针对int/float类型进行描述,TSDB还支持存储object类型. 132 | - 上述的qualifiers为2/4字节,针对object类型,qualifiers使用3/5字节进行存储,第1字节为01,后面2/4字节直接存储秒和毫秒级别的偏移量. 133 | 不存储type信息,那么存储时间偏移量的2/4字节高位肯定是0,这样好处:010开头的qualifiers在进行查询时候肯定是排在行首. 134 | - object value是按照UTF-8编码的JSON对象 135 | 这种类型可以用来存储聚合信息,比如t:01012={sum:100,avg:20,min:10,max:25}存储10分钟的聚合值. 136 | 137 | + DataTable的qualifiers/value聚合的设计亮点: 138 | - 在写入TSDB时候,每个偏移量是作为一个单独的qualifiers进行存储,这样方便写入,但是不适合查询,因为查询会针对每个qualifiers作为一行返回. 139 | 因此TSDB针对数据进行一次聚合(Compactions,和hbase内部的Compactions不是同一个意思). 140 | - TSDB合并很简单,qualifiers大小是固定的,value的大小可以从qualifiers中获取,因此可以直接连接起来就可以. 141 | 合并发生的时间是当前行已经过去了一个小时,或者读取未合并的行(如果合并以后再次写入,可以再次合并) 142 | 143 | + tsdb-uid Table和UID的设计: 144 | - 做了太多用户产品,第一映像就把UID理解为用户ID,它本身是Unique ID,在TSDB里面,matrixName,tag-key,tag-value都映射为UID, 145 | 进而可以进一步编码到rowkey中.UID默认是一个3字节的无符号数目 146 | - 利于tsdb-uid表存储UID和stringName(stringName类型有matrixName,tag-key,tag-value)之间的"双向"映射; 147 | - stringName到UID的映射:RowKey=stringName,CF=id,qualifiers=上述stringName一种,value=UID. 148 | - UID到stringName的映射:RowKey=UID,CF=name,qualifiers=上述stringName一种,value=stringName 149 | - UID需要保证唯一信息,tsdb-uid表的第一行,rowKey=\x00,CF=id,qualifiers=上述stringName一种,value=8字节的递增int 150 | 每次分配一个新的UID,就递增指定qualifiers的value值. 151 | 152 | ## [Coprocessor的设计](https://blogs.apache.org/hbase/entry/coprocessor_introduction) 153 | 二级索引的设计和实现是现在HBase应用中一个很重要的环节.最近使用phoenix来支持hbase的sql操作和二级索引,那么二级索引在hbase里面是什么实现的? 154 | 先看一个phoenix创建的表的scheme 155 | 156 | > {NAME => 'WEB_STAT', 157 | >coprocessor$5 => '|org.apache.phoenix.hbase.index.Indexer|1073741823|', 158 | >coprocessor$4 => '|org.apache.phoenix.coprocessor.ServerCachingEndpointImpl|1|', 159 | >coprocessor$3 => '|org.apache.phoenix.coprocessor.GroupedAggregateRegionObserver|1|', 160 | >coprocessor$2 => '|org.apache.phoenix.coprocessor.UngroupedAggregateRegionObserver|1|', 161 | >coprocessor$1 => '|org.apache.phoenix.coprocessor.ScanRegionObserver|1|', 162 | >FAMILIES =} 163 | 164 | 结论:二级索引的实现是基于coprocessor来实现的. 165 | 166 | coprocessor是HBase中一个功能插件系统,它由observer和endpoint两部分组成,这两部分其实就相对于传统关系数据库中触发器和存储过程. 167 | 168 | Observer相当于触发器,可以监听hbase内部相应的事件,并进行处理.根据监听的对象不同,Hbase内部有Master/Region/WAL三种类型的Observer. 169 | 其中Master可以监听Table和Region的DDL操作,Region可以监听GET/PUT/SCAN/DELETE/OPEN/FLUSH,WAL可以监听write的操作. 170 | 在HBase内部,每个监听对象维持了一个Observer链,如上图的表就有多个coprocessor,根据优先级来确定执行次序. 171 | 172 | Endpoint相对于存储过程,可以被定义并加载在HBase内部,本质上,它是一个动态RPC,从而可以实现在region内部进行数据处理, 173 | 减少通过scan操作把数据拉取到客户端进行处理的代价. 174 | Endpoint的执行过程相对于一个mapreduce过程,client将一个table指定的endpoint操作map到每个region上进行处理,并获取每个region的处理结果进行reduce合并操作. 175 | 176 | 回到上面的二级索引的问题,所谓的二级索引其实就是实现一个Observer,并绑定到table上, 177 | 从而可以捕获表的get/put/scan操作,写的时候写一次索引,读的时候先去查索引,根据索引的结果来真正查数据. 178 | 179 | ## [BlockCache的设计](http://hbase.apache.org/book/regionserver.arch.html) 180 | 在HBase里面有两个对内存依赖较大的模块,其中一个memStore,它为Hbase写操作提供一个临时缓存,同时为最近写的数据提高一个读缓存. 181 | 另外一个就是本节学习的BlockCache,它为Hbase的读操作提高了server端的缓存.HBase的存储依赖HDFS,通过BlockCache将HFile缓存到内存,可以很大程度上提高读性能. 182 | 183 | HBase针对BlockCache提供了两个实现.LruBlockCache和BucketCache,它们最重要的区别一个是onHeap,一个是offHeap(使用直接内存来实现),在性能上来说,基于onheap 184 | 的LruBlockCache要优于基于offHeap的BucketCache,但是好offHeap的实现采用自定义的内存管理而减少Heap的GC所带来的消耗, 185 | 在cache命中较低的应用来说,采用offheap更加有优势. 186 | 187 | LruBlockCache的Lru规则是基于block的优先级来实现.它由三个cache级别来实现, 188 | 189 | + Single access priority:数据在第一次被扫描,就进入cache,但是它在cache的优先级也是最低的,在cache不足的时候,将是第一个被删除的数据. 190 | + Mutli access priority:由Single access priority升级而来. 191 | + In-memory access priority: HBase的表可以是配置为"in-memory", 它们是优先级最高的,-ROOT-和.META.都是基于基于内存的. 192 | 193 | 对LruBlockCache一个重要点是淘汰任务不会在cache完全满的时候才会启动,默认在99%时候就会启动淘汰任务. 194 | 195 | BucketCache是基于二级缓存来实现.对二级缓存的认识首先要了解HFile的组成.HFile由DATABLOCK , METABLOCK ,DATAINDEX, METAINDEX几部分组成, 196 | 其中DATABLOCK是数据量大头. 197 | BucketCache二级缓存中的L1是基于LruBlockCache实现的,它用于存储INDEX,META在onheap中,L2是用于存储DATABLOCK的offheap,存储中L1中的数据在L1内存不够时候, 198 | 也会被淘汰到L2中进行缓存. 199 | 200 | ## [HBase HMaster Architecture](http://blog.zahoor.in/2012/08/hbase-hmaster-architecture/) 201 | HMaster在设计上还是比较轻量级别,HBase集群可以在无Master的情况运行短时间,那么具体HMaster充当了什么功能,需要仔细研究. -------------------------------------------------------------------------------- /image/BlockPoolManager.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColZer/DigAndBuried/9a453cb6197c70281945201c68d79939499cf06a/image/BlockPoolManager.png -------------------------------------------------------------------------------- /image/Catalyst-Optimizer-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColZer/DigAndBuried/9a453cb6197c70281945201c68d79939499cf06a/image/Catalyst-Optimizer-diagram.png -------------------------------------------------------------------------------- /image/edge_cut_vs_vertex_cut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColZer/DigAndBuried/9a453cb6197c70281945201c68d79939499cf06a/image/edge_cut_vs_vertex_cut.png -------------------------------------------------------------------------------- /image/fsdataset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColZer/DigAndBuried/9a453cb6197c70281945201c68d79939499cf06a/image/fsdataset.png -------------------------------------------------------------------------------- /image/hadoop-rpc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColZer/DigAndBuried/9a453cb6197c70281945201c68d79939499cf06a/image/hadoop-rpc.jpg -------------------------------------------------------------------------------- /image/job.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColZer/DigAndBuried/9a453cb6197c70281945201c68d79939499cf06a/image/job.jpg -------------------------------------------------------------------------------- /image/network-client.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColZer/DigAndBuried/9a453cb6197c70281945201c68d79939499cf06a/image/network-client.jpg -------------------------------------------------------------------------------- /image/network-message.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColZer/DigAndBuried/9a453cb6197c70281945201c68d79939499cf06a/image/network-message.jpg -------------------------------------------------------------------------------- /image/network-rpcenv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColZer/DigAndBuried/9a453cb6197c70281945201c68d79939499cf06a/image/network-rpcenv.jpg -------------------------------------------------------------------------------- /image/project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ColZer/DigAndBuried/9a453cb6197c70281945201c68d79939499cf06a/image/project.png -------------------------------------------------------------------------------- /other/mvn-lib.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.scala-tools 5 | maven-scala-plugin 6 | 7 | 8 | 9 | compile 10 | testCompile 11 | 12 | 13 | 14 | 15 | ${scala.version} 16 | 17 | 18 | 19 | 20 | org.apache.maven.plugins 21 | maven-dependency-plugin 22 | 23 | 24 | copy-dependencies 25 | package 26 | 27 | copy-dependencies 28 | 29 | 30 | ${project.build.directory}/lib 31 | false 32 | false 33 | true 34 | 35 | 36 | 37 | 38 | 39 | 40 | org.apache.maven.plugins 41 | maven-resources-plugin 42 | 2.3 43 | 44 | 45 | copy-resources 46 | package 47 | 48 | copy-resources 49 | 50 | 51 | UTF-8 52 | ${project.build.directory} 53 | 54 | 55 | src/main/resources/ 56 | 57 | config.xml 58 | log4j.xml 59 | 60 | true 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | org.apache.maven.plugins 70 | maven-jar-plugin 71 | 72 | 73 | package 74 | 75 | jar 76 | 77 | 78 | lib 79 | 80 | config.xml 81 | log4j.xml 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /other/point-estimation.md: -------------------------------------------------------------------------------- 1 | 和点估计相对应的是区间估计,这个一般入门的统计教材里都会讲。直观说,点估计一般就是要找概率密度曲线上值最大的那个点,区间估计则要寻找该曲线上满足某种条件的一个曲线段。 2 | 3 | 最大似然和最大后验是最常用的两种点估计方法。以最简单的扔硬币游戏为例,一枚硬币扔了五次,有一次是正面。用最大似然估计,就是以这五次结果为依据,判断这枚硬币每次落地时正面朝上的概率(期望值)是多少时,最有可能得到四次反面一次正面的结果。不难计算得到期望概率0.2。 4 | 5 | 用五次试验结果来估计硬币落地时正面朝上的概率显然不够可靠。这时候先验知识可以发挥一些作用。如果你的先验知识告诉你,这枚硬币是制币局制造,而制币局流出的硬币正面朝上的概率一般是0.5,这时候就需要在先验概率0.5和最大似然估计0.2之间取个折中值,这个折中值称为后验概率。这时候剩下的问题就是先验知识和最大似然估计结果各应起多大作用了。如果你对制币局的工艺非常有信心,觉得先验知识的可靠程度最起码相当于做过一千次虚拟试验,那么后验概率是(0.2 * 5 + 0.5 * 1000)/(5 + 1000) = 0.4985,如果你对制币局技术信心不足,觉得先验知识的可靠程度也就相当于做过五次试验,那么后验概率是(0.2 * 5 + 0.5 * 5)/(5 + 5) = 0.35. 这种在先验概率和最大似然结果之间做折中的方法称为后验估计方法。这是用贝耶斯观点对最大后验方法的阐述,其实也可以用用经典统计学派的偏差方差的折中来解释。 6 | 7 | EM方法是在有缺失值时进行估计的一种方法,这是一个迭代方法,每个迭代有求期望(E)和最大化(M)两个步骤。其中M可以是MLE或者MAP。一般需要先为缺失值赋值(E步骤初始化),然后重复下面的步骤: 8 | 1)用MLE或MAP构造模型(M步骤); 9 | 2) 用所得模型估计缺失值,为缺失值重新赋值(E步骤); 10 | 仍然以扔硬币为例,假设投了五次硬币,记录到结果中有两正一反,还有两次的数据没有记录下来,不妨自己用上述步骤演算一下硬币正面朝上的概率。需要注意,为缺失值赋值可以有两种策略,一种是按某种概率赋随机值,采用这种方法得到所谓hard EM,另一种用概率的期望值来为缺失变量赋值,这是通常所谓的EM。另外,上例中,为两个缺失记录赋随机值,以期望为0.8的0-1分布为他们赋值,还是以期望为0.2的0-1分布为他们赋值,得到的结果会不同。而赋值方法的这种差别,实际上体现了不同的先验信息。所以即便在M步骤中采用MLE,EM方法也融入了非常多的先验信息。 11 | 12 | 13 | 上面的例子中只有一个随机变量,而LDA中则有多个随机变量,考虑的是某些随机变量完全没有观测值的情况(也就是Latent变量),由于模型非常复杂,LDA最初提出时采用了变分方法得到一个简单的模型,EM被应用在简化后的模型上。从学习角度说,以PLSA为例来理解EM会更容易一点。另外,kmeans聚类方法实际上是典型的hard EM,而soft kmeans则是通常的EM,这个在[1]中的讨论最直观易懂。 14 | 15 | 16 | [1] Information Theory, Inference, and Learning Algorithms, http://www.http://inference.phy.cam.ac.uk/mackay/itila/ 17 | 18 | 19 | 20 | --------------------- 21 | 22 | 23 | 故事是这样的: 24 | 从前,有一个卖算法的小女孩有一堆数据,她首先假设这些数据的生成机制可以用某一个概率分布来描述。 25 | 这时她面对了两种选择: 26 | 不做进一步假设,认为这个概率分布(如果是实数域上的数据)可以是正态分布,可以是学生t-分布,可以是拉普拉斯分布,可以是各种其他各种分布,甚至还可以是把以上分布用另一个随机变量混合起来的。在这种情况下,她要做的就是非参数统计。这个和题目中提到的四个名词关系都不大,暂且按下不表。 27 | 假设形成数据的概率分布是某一族分布中的一个。每一族概率分布都可能有无数个。这时她要问自己的问题是:我们面前的这个概率分布的参数是多少。比如说她假设数据是由某种正态分布形成的,就要对这个分布的期望和方差做一个估值(estimation),也就是推论(inference)。这里就可以提到题主问题中的 point estimation 这个词了,MLE,MAP和EM都是对模型估值的方式或者方法。实际上,在对模型的参数做出估计以后,买算法的小女孩们还可以问自己另一个问题:眼前的数据确实被这个推论得知的概率分布形成的概率是多少。这时就要用到假设检验,并以此得到参数的置信区间(confidence interval),point estimation 中的“点”就是和置信区间中的“区间”相对的。 28 | 现在小女孩决定了要使用的分布族群,她还是不知道怎么估计参数的值。其实这里有三种可能的状况: 29 | 小女孩选择的模型很简单,数据量也很大,给定一组参数以后,数据的似然函数(likelihood function)可以很明白的写出来(tractable)。这时候,一个很明显也很自然的选择就是使用最有可能生成了数据的参数值,也就是说,选择让似然函数最大化的参数值,也即 maximum likelihood estimation。如果似然函数可以直接在理论上求最大值当然好,算不出最大化的表达式,可以靠数值运算最大化也可以。因为数据量很大,模型简单(甚至足够规则),MLE 是一个不错的选择。 30 | 小女孩选择的模型很复杂。似然函数虽然貌似可以写出来,但是要给指数级的项目求和,或者似然函数根本写不出来。这样的模型就不能简单最大化似然了之了。这时候,买算法的小女孩们或者老板突然发现,虽然不能直接写出模型的似然函数,要是给模型加上几个隐变量,那么给定参数下,数据与隐变量的联合分布倒是很容易算,要是知道隐变量的值,针对参数最大化似然函数也很容易。唯一的问题是,他们既不知道隐变量的值,也不知道参数的值。这时就可以用到 EM 算法了,这个两步的算法很好地解决了这个两不知的问题,也即:第一步,给定参数,对隐变量做期望,算出包括隐变量的似然函数;第二步,对这个似然函数最大化,update 参数。因为这个模型可以让似然函数递增,如果似然函数是凹函数,那就一定会收敛到最大值,如果似然函数有多个极值,则要随机化初始参数值,算很多次,选择似然最大的参数。 31 | 小女孩的模型未必很复杂,但是数据非常少,与此同时,小女孩或者老板已经关注这个问题很久,对参数有一定的想法了。这时候就可以用到贝叶斯统计了:我们可以给参数定一个先验统计分布,然后用数据算出参数的后验分布(posterior probability,其实大概就是把先验的概率密度和数据的似然函数乘一乘,然后再标准化一下的问题),然后再最大化后验,这个最大化后验分布的参数值就是 maximum a posteriori 了。其实在贝叶斯统计中,最大化后验概率的参数值未必是最好的参数值,根据决策论的看法,一般会最小化某个 loss function,得到的结果可能是后验分布的期望或者中值。不过如果参数的空间是离散的,这两个值未必在参数空间内,说不定也很不好算,所以在实际应用中,用 MAP 的也不少。 -------------------------------------------------------------------------------- /other/scala-java-class-type.md: -------------------------------------------------------------------------------- 1 | Java/Scala类型系统 2 | ====== 3 | by chenze 4 | 5 | Scala对于每一个程序员甚至Java程序员来说,它的类型系统都过于复杂.协变,逆变,上界定,下界定,视图界定,上下文界定,Manifest,ClassType一堆一堆新奇的概念,让人都在怀疑自己是在学习一门编程语言还是在研究一本数学课本. 你也许会说,我为什么要知道这些?我可以熟练的使用flatmap,map,reduce不就可以吗?当然,从应用的角度来说,没有问题!但是前几天看到一个帖子,有人说它精通Spark-Core,但是有人问了一句:Scala的ClassType在Spark-Core的作用?它就蒙了!当然我也蒙了,研究一翻发现,仅仅Spark-Core就有800处有关ClassType的应用,连RDD的定义都是`abstract class RDD[T: ClassTag]`,想想一下,如果对scala类型系统不了解,怎么去写Spark代码? 6 | 7 | ### Java中类型系统:"类"!="类型" 8 | 9 | 在Java的世界中,类为Class,而类型为Type.在jdk1.5版本之前,没有泛型的存在,对于一个对象(Object),通过getClass就可以获取到这个对象的Class,此时我们就明确的知道这个对象的Type,即Class与Type有一对一关系.比如对于`Integer a; a.getClass == Integer.class`此时我们就明确知道`typeof(a) == class Integer`; 10 | 11 | 但是引入泛型以后,一个变量的Type和Class不再一一对应了,比如`List和List的Class都是Class,但是两者的Type不一样了`,产生这种原因是在JVM层面是不支持泛型的,对于Java,泛型是工作在javac编译层,在编译过程中,会对泛型相关信息从中擦除,即`List和List都被表示为List`,但是类型信息还是必须存储的,对于Java由`ParameterizedType, TypeVariable, GenericArrayType, WildcardType`来存储泛型变量的类型信息.对于非泛型,如前面所说,他们类型就是Class.为了程序的扩展性,最终引入了`Type接口作为Class和ParameterizedType, TypeVariable, GenericArrayType, WildcardType这几种类型的总的父接口`,这样可以用Type类型的参数来接受以上五种子类的实参或者返回值类型就是Type类型的参数,统一了与泛型有关的类型和原始类型Class. 12 | 13 | ####关于四种泛型的类型的解释(摘录自:loveshisong.cn) 14 | 15 | ParameterizedType:具体的范型类型, 如Map. 16 | 17 | 有如下方法: 18 | 1. Type getRawType(): 返回承载该泛型信息的对象, 如上面那个Map承载范型信息的对象是Map 19 | 2. Type[] getActualTypeArguments(): 返回实际泛型类型列表, 如上面那个Map实际范型列表中有两个元素, 都是String 20 | 3. Type getOwnerType(): 返回是谁的member.(上面那两个最常用) 21 | public class TestType { 22 | Map map; 23 | public static void main(String[] args) throws Exception { 24 | Field f = TestType.class.getDeclaredField("map"); 25 | System.out.println(f.getGenericType()); 26 | // java.util.Map 27 | System.out.println(f.getGenericType() instanceof ParameterizedType); 28 | // true 29 | ParameterizedType pType = (ParameterizedType) f.getGenericType(); 30 | System.out.println(pType.getRawType()); 31 | // interface java.util.Map 32 | for (Type type : pType.getActualTypeArguments()) { 33 | System.out.println(type); 34 | // 打印:class java.lang.String 35 | } 36 | System.out.println(pType.getOwnerType()); 37 | // null 38 | } 39 | } 40 | 41 | TypeVariable:类型变量, 范型信息在编译时会被转换为一个特定的类型, 而TypeVariable就是用来反映在JVM编译该泛型前的信息.它的声明是这样的: `public interface TypeVariable extends Type`,也就是说它跟GenericDeclaration有一定的联系, 我是这么理解的:`TypeVariable是指在GenericDeclaration中声明的这些东西中的那个变量T、C;` . 42 | 43 | 它有如下方法: 44 | 1. Type[] getBounds(): 获取类型变量的上边界, 若未明确声明上边界则默认为Object 45 | 2. D getGenericDeclaration(): 获取声明该类型变量实体 46 | 3. String getName(): 获取在源码中定义时的名字 47 | 48 | 注意: 49 | 1. 类型变量在定义的时候只能使用extends进行(多)边界限定, 不能用super; 50 | 2. 为什么边界是一个数组? 因为类型变量可以通过&进行多个上边界限定,因此上边界有多个 51 | public class TestType { 52 | K key; 53 | V value; 54 | public static void main(String[] args) throws Exception { 55 | // 获取字段的类型 56 | Field fk = TestType.class.getDeclaredField("key"); 57 | Field fv = TestType.class.getDeclaredField("value"); 58 | Assert.that(fk.getGenericType() instanceof TypeVariable, "必须为TypeVariable类型"); 59 | Assert.that(fv.getGenericType() instanceof TypeVariable, "必须为TypeVariable类型"); 60 | TypeVariable keyType = (TypeVariable)fk.getGenericType(); 61 | TypeVariable valueType = (TypeVariable)fv.getGenericType(); 62 | // getName 方法 63 | System.out.println(keyType.getName()); 64 | // K 65 | System.out.println(valueType.getName()); 66 | // V 67 | // getGenericDeclaration 方法 68 | System.out.println(keyType.getGenericDeclaration()); 69 | // class com.test.TestType 70 | System.out.println(valueType.getGenericDeclaration()); 71 | // class com.test.TestType 72 | // getBounds 方法 73 | System.out.println("K 的上界:"); 74 | // 有两个 75 | // interface java.lang.Comparable 76 | // interface java.io.Serializable 77 | for (Type type : keyType.getBounds()) { 78 | System.out.println(type); 79 | } 80 | System.out.println("V 的上界:"); 81 | // 没明确声明上界的, 默认上界是 Object 82 | // class java.lang.Object 83 | for (Type type : valueType.getBounds()) { 84 | System.out.println(type); 85 | } 86 | } 87 | } 88 | 89 | GenericArrayType: 范型数组,组成数组的元素中有范型则实现了该接口; 它的组成元素是ParameterizedType或TypeVariable类型. 90 | 91 | 它只有一个方法: 92 | 1. Type getGenericComponentType(): 返回数组的组成对象, 即被JVM编译后实际的对象 93 | public class TestType { 94 | public static void main(String[] args) throws Exception { 95 | Method method = Test.class.getDeclaredMethods()[0]; 96 | // public void com.test.Test.show(java.util.List[],java.lang.Object[],java.util.List,java.lang.String[],int[]) 97 | System.out.println(method); 98 | Type[] types = method.getGenericParameterTypes(); // 这是 Method 中的方法 99 | for (Type type : types) { 100 | System.out.println(type instanceof GenericArrayType); 101 | } 102 | } 103 | } 104 | class Test { 105 | public void show(List[] pTypeArray, T[] vTypeArray, List list, String[] strings, int[] ints) { 106 | } 107 | } 108 | 第一个参数List[]的组成元素List是ParameterizedType类型, 打印结果为true 109 | 第二个参数T[]的组成元素T是TypeVariable类型, 打印结果为true 110 | 第三个参数List不是数组, 打印结果为false 111 | 第四个参数String[]的组成元素String是普通对象, 没有范型, 打印结果为false 112 | 第五个参数int[] pTypeArray的组成元素int是原生类型, 也没有范型, 打印结果为false 113 | 114 | WildcardType: 该接口表示通配符泛型, 比如? extends Number 和 ? super Integer. 115 | 116 | 它有如下方法: 117 | 1. Type[] getUpperBounds(): 获取范型变量的上界 118 | 2. Type[] getLowerBounds(): 获取范型变量的下界 119 | 注意:现阶段通配符只接受一个上边界或下边界, 返回数组是为了以后的扩展, 实际上现在返回的数组的大小是1 120 | public class TestType { 121 | private List a; // // a没有下界, 取下界会抛出ArrayIndexOutOfBoundsException 122 | private List b; 123 | public static void main(String[] args) throws Exception { 124 | Field fieldA = TestType.class.getDeclaredField("a"); 125 | Field fieldB = TestType.class.getDeclaredField("b"); 126 | // 先拿到范型类型 127 | Assert.that(fieldA.getGenericType() instanceof ParameterizedType, ""); 128 | Assert.that(fieldB.getGenericType() instanceof ParameterizedType, ""); 129 | ParameterizedType pTypeA = (ParameterizedType) fieldA.getGenericType(); 130 | ParameterizedType pTypeB = (ParameterizedType) fieldB.getGenericType(); 131 | // 再从范型里拿到通配符类型 132 | Assert.that(pTypeA.getActualTypeArguments()[0] instanceof WildcardType, ""); 133 | Assert.that(pTypeB.getActualTypeArguments()[0] instanceof WildcardType, ""); 134 | WildcardType wTypeA = (WildcardType) pTypeA.getActualTypeArguments()[0]; 135 | WildcardType wTypeB = (WildcardType) pTypeB.getActualTypeArguments()[0]; 136 | // 方法测试 137 | System.out.println(wTypeA.getUpperBounds()[0]); // class java.lang.Number 138 | System.out.println(wTypeB.getLowerBounds()[0]); // class java.lang.String 139 | // 看看通配符类型到底是什么, 打印结果为: ? extends java.lang.Number 140 | System.out.println(wTypeA); 141 | } 142 | } 143 | 再写几个边界的例子: 144 | List, 上界为class java.lang.Number, 属于Class类型 145 | List>, 上界为java.util.List, 属于ParameterizedType类型 146 | List>, 上界为java.util.List, 属于ParameterizedType类型 147 | List, 上界为T, 属于TypeVariable类型 148 | List, 上界为T[], 属于GenericArrayType类型 149 | 它们最终统一成Type作为数组的元素类型 150 | 151 | -------------------------------------------------------------------------------- /spark/class-from-root.md: -------------------------------------------------------------------------------- 1 | # 根目录下面的基本功能研究 2 | 3 | ## 编译 4 | 相比以前版本,spark1.1版本对编译做了很多限制,老的命令是无法直接编译命令,详细编译命令[参考](http://spark.apache.org/docs/latest/building-with-maven.html) 5 | 我们自己的执行命令 6 | 7 | sh make-distribution.sh --tgz -Phadoop-2.2 -Dhadoop.version=2.2.0 -Pyarn -Phive 8 | ## stage和task 9 | 1.1.0版本引入attempt概念,即task一次执行的实例;那么一个task的实际运行结果由**stageid**,**partitionID**,**attempID**三个维度来标识。 10 | task的状态由是否完成isCompleted以及是否失败isInterrupted来表示;同时每个task可以添加TaskCompletionListener,当task完成时候,会执行每个Listener的注册函数。参考代码:TaskContext 11 | task失败原因由TaskEndReason和TaskFailedReason以及下面的case类来表示,TaskEndReason包括成功;TaskFailedReason包括执行器丢失,fetch失败,重新提交,异常,执行结果丢失,被kill,未知失败。参考代码:TaskEndReason 12 | 其中Task的状态,可以参考TaskState单例对象,并且针对Mesos做了适配。参考代码TaskState 13 | 14 | -------------------------------------------------------------------------------- /spark/function-closure-cleaner.md: -------------------------------------------------------------------------------- 1 | Spark 闭包中ClosureCleaner操作 2 | ============ 3 | 4 | 在Scala,函数是第一等公民,可以作为参数的值传给相应的rdd转换和动作,进而进行迭代处理。 5 | 阅读spark源码,我们发现,spark对我们所传入的所有闭包函数都做了一次sc.clean操作,如下 6 | 7 | def map[U: ClassTag](f: T => U): RDD[U] = new MappedRDD(this, sc.clean(f)) 8 | private[spark] def clean[F <: AnyRef](f: F, checkSerializable: Boolean = true): F = { 9 | ClosureCleaner.clean(f, checkSerializable) 10 | f 11 | } 12 | 函数clean对闭包做了一次清理的操作,那么什么是闭包清理呢? 13 | 14 | ## 闭包 15 | 我们首先看ClosureCleaner里面一个函数: 16 | 17 | // Check whether a class represents a Scala closure 18 | private def isClosure(cls: Class[_]): Boolean = { 19 | cls.getName.contains("$anonfun$") 20 | } 21 | 该函数用来检测一个Class是不是闭包类,我们看到,如果一个对象的Class-name包含"$anonfun$",那么它就是一个闭包。再看一个实例: 22 | 23 | //BloomFilter.scala这个文件里面有一个contains函数,函数内部使用了一个匿名函数: 24 | def contains(data: Array[Byte], len: Int): Boolean = { 25 | !hash(data,numHashes, len).exists { 26 | h => !bitSet.get(h % bitSetSize) //这里是一个匿名函数 27 | } 28 | } 29 | 对BloomFilter.scala进行编译,我们会发现,它会针对这个匿名函数生成一个"BloomFilter$$anonfun$contains$1"Class,对于该类,spark将其识别闭包。 30 | 31 | 那么闭包到底是什么? 32 | 33 | > 在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。 34 | > 这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。 35 | > 所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。 36 | > 闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。 37 | 38 | 从上面的描述来看,闭包本身就是类,它的特点是它所创建的对象实例可以引用outer函数/类里面的变量。 39 | 朴素的说法就是:闭包就是能够读取外部函数的内部变量的函数。 40 | 41 | 另外,在本质上匿名函数和闭包是不同的概念,但是匿名函数一般都会被outer函数所包含,它有读取outer函数变量的能力,因此可以简单的把匿名函数理解为闭包。 42 | 43 | 简单的总结一下:闭包就是拥有对outer函数/类的变量的引用,从而可以在外面函数栈执行结束以后,依然握有外面函数栈/堆变量的引用,并可以改变他们的值。 44 | 说到这里,相信大家也看到闭包有对外部变量的引用的能力,这个能力是有潜在风险的。首先它会影响变量的GC,另外他会影响函数对象的序列化. 45 | 再回头看一下clean函数第三个参数checkSerializable: Boolean = true,即是否检查序列化的问题,默认是true。 46 | 在scala中函数对象都是可以被序列化,从而可以传输到各个slave中进行计算, 47 | 但是如果一个函数对象引用了outer函数/对象的变量是不可以被序列化,那么就导致整个函数对象序列化失败。 48 | 49 | ## java中"闭包"仿真 50 | 51 | java8版本引入Lambda表达式和闭包的支持,但是java8之前版本都没有支持,需要通过java(匿名)内部类来模拟实现,参考spark的rdd map函数的java-api 52 | 53 | JavaRDD map(Function f) 54 | public interface Function 55 | extends java.io.Serializable 56 | 57 | //实现的时候可以 58 | rdd.map(new Function{ 59 | public String class(String strIn) { 60 | return strIn; 61 | } 62 | }); 63 | 64 | 闭包和匿名内部类肯定还不是一个层次上的概念,要不然java8也不会在已有内部类的情况引入Lambda和闭包,那么它们之间有什么区别呢? 65 | 这里我首先总结一下java内部类的概念, 66 | 67 | + java内部类可以分为成员内部类,静态内部类,局部内部类,匿名内部类这个类别. 68 | + 成员内部类可以访问外部对象所有的成员变量,无论他是否是static,final,public和private 69 | + 成员内部类对成员变量访问可以直接访问,或者通过(外部类名称.this.非stattic变量)和(外部类名称.static变量名称)来访问, 70 | 如果内部类和外部类有相同的成员变量名称,那么访问内部的成员变量可以通过(变量名称)和(this.变量名称)来访问, 71 | 但是访问外部类的变量时候必须通过(外部类名称.this.变量名称) 72 | + 成员内部类里面不能定义static类变量和static函数;但是静态内部类里面可以. 73 | + 静态内部类不能访问外部类里面的非static成员变量,内部类没有(外部类名称.this)外部类的指针. 74 | + 成员内部类的对象创建,必须通过(外部类名称.内部类名称 对象变量 = 外部类对象.new 外部类名称.内部类名称), 75 | (注意:尽管new的方式不一样,但是new出来的两个内部对象的类型是相等,后面会谈到scala内部类,这点和scala是很不同,下面的实例提前做一个比较) 76 | 77 | //JAVA 78 | OuterClass outerClass1 = new OuterClass(); 79 | OuterClass outerClass2 = new OuterClass(); 80 | OuterClass.InnerClass innerClass1 = outerClass1.new InnerClass(); 81 | OuterClass.InnerClass innerClass2 = outerClass2.new InnerClass(); 82 | //two will be success 83 | outerClass1.runWithInnerClass(innerClass1); 84 | outerClass1.runWithInnerClass(innerClass2); 85 | 86 | //SCALA 87 | val scalaOuterClass1 = new ScalaOuterClass; 88 | val scalaOuterClass2 = new ScalaOuterClass; 89 | val scalaInnerClass1 = new scalaOuterClass1.ScalaInnerClass; 90 | val scalaInnerClass2 = new scalaOuterClass2.ScalaInnerClass; 91 | 92 | scalaOuterClass1.runWithInnerClass(scalaInnerClass1); 93 | // 94 | //error: type mismatch; 95 | //[INFO] found : scalaOuterClass2.ScalaInnerClass 96 | //[INFO] required: scalaOuterClass1.ScalaInnerClass 97 | scalaOuterClass1.runWithInnerClass(scalaInnerClass2); 98 | 99 | + 静态内部类和可以直接通过(外部类名称.内部类名称 对象变量 = new 外部类名称.内部类名称),即静态内部类与外部类的对象之间不存在对应关系. 100 | + 成员内部类,静态内部类都是定义类里,与传统的成员变量/静态变量相似.还有另外一种作用域里的内部类:局部内部类,即定义在方法里的内部类, 101 | 它和成员内部类的区别是,它除了拥有外部类的变量的可见性以外,还拥有方法内的部分局部变量的可见性. 102 | + 局部内部类中所拥有的方法中局部变量可见性指的是final变量,普通变量不具备可读性. 103 | + 成员内部类和静态内部类都是编译为"外部类$内部类.class",而局部内部类很根据定义的次序编译为"外部类$次序编号+内部类.class". 104 | + 匿名内部类,匿名内部类是局部内部类的一个子集,它是定义在局部方法内部,具有与局部内部类相同的外部类变量和局部变量的可读性.局部内部类的实现需要依赖接口来实现.匿名内部类会被编译成"外部类$次序编号.class". 105 | + 总结,从上面来看,内部类拥有外部类成员变量的可见性,但是内部类(局部/匿名)不能读取定义域非final局部变量. 106 | 107 | 上面简单的对java内部类进行简单总结,发现它和闭包有几个区别 108 | 109 | + 编译出来的class不一样. 110 | + 局部/匿名内部类与也是局部定义的闭包对局部变量的可见性不同. 111 | 112 | 在对java的内部类与scala的闭包的区别进行分析之前,先来看一下scala对内部类的支持. 113 | 114 | + scala也有成员内部类,静态内部类,局部内部类以及匿名类;其中静态内部类是定义在Object的类; 115 | + scala中对内部类的支持与java大体一直,连编译出来的class名称也与java完全一样.不一样的三点是: 116 | 上述的内部类的类型机制不一样;局部内部类对局部变量的可见性不一样;引入路径依赖类型和类型投影的概念 117 | + 重要:scala局部内部类对局部变量的可见性没有final/val变量的要求,比如下面的例子: 118 | 119 | def runWithInnerClass(inner:ScalaInnerClass): Unit = { 120 | var test= 2; 121 | class functionClass { 122 | def doSome1(): Unit = { 123 | inner.doSome();//可以读取函数的参数 124 | println(test)//可以读取函数局部变量 125 | test=3;//可以修改函数的局部变量 126 | println(test)// 127 | print(ScalaOuterClass.this.test3);//可以读取外部类的成员变量 128 | } 129 | }; 130 | } 131 | 132 | + 针对"外部类名称.内部类名称"这样的格式的类型,引入"路径依赖类型";比如 A.this.B就是一个路径依赖类型, 133 | 其中A.this会因为this的实例的不同而不同,比如 a1 和 a2 就是两个不同的路径,所以a1.B 与 a2.B也是不同的类型 134 | + 路径依赖类型a1.B与a2.B是两个不同类型,但是她们都有一个超类型A.B,那么如果一个方法希望接受所有A.B,那怎么写?类型投影,用 A#B的形式表示。 135 | 那么def foo(b: A#B)就可以接受a1.B和a2.B. 136 | 137 | 从上面我看到看到仅仅Java对局部(匿名)类做了"只能读取final局部变量"的限制?为什么有这个限制? 138 | 139 | 首先对于外部类的成员变量没有访问限制的原因外部类的this引用在编译为字节码时已经作为内部类(不含静态内部类)的一个成员变量添加为内部类中, 140 | 从而不管是scala还是java,对外部类的成员变量的访问都没有限制. 141 | 其次针对局部变量final条件的限制也是一种无可奈何的选择,java函数的运行是围绕进栈和出栈操作而进行,对于处于函数中局部变量(包括基本类型和引用类型) 142 | 在运行开始会放入栈中,函数运行结束会从栈中离开,离开就代表这个局部变量不存在了.同时对于处于函数中的局部内部类的生命周期明显比函数生命周期要长,在函数运行结束以后, 143 | 内部类依然引用局部类的栈变量,而此时栈已经出栈,此时内部类就会引用一个不存在的数据,这是内部类不可接受的. 144 | 145 | 但是如果限制变量为final,那么就采用"值复制"的方式来解除内部类对外部函数局部函数栈变量的引用. 146 | 对于基本类型,final类型在函数和内部类都不会被修改,因为可以复制,从而不会因为复制以后,导致两者修改都对应不同的变量. 147 | 对于引用类型,final类型代表不能修改引用的指向,但是可以修改指向的对象内部值.这样可以保证函数内部和局部内部类所修改的都指向同一个对象, 148 | 并且在外部函数运行结束以后,内部类还依然引用相应对象,从而该对象不会被虚拟机GC. 149 | 150 | 通过上面限制java就可以无bug的实现局部内部类,虽然有限制,但是还是够用,所以一直以来,局部内部类/匿名内部类广泛应用在回调操作上. 151 | 152 | 那么为什么scala的局部内部类可以访问函数的var变量呢?如下面的实例: 153 | 154 | //scala 155 | def run(): Unit ={ 156 | var data=1; 157 | class InnerClass{ 158 | def runInnerClass(): Unit = { 159 | println(data); 160 | data = 2; 161 | } 162 | } 163 | (new InnerClass()).runInnerClass(); 164 | print(data); 165 | } 166 | 167 | //截取javap中针对内部类构造函数和runnInnerClass 168 | public void runInnerClass(); 169 | flags: ACC_PUBLIC 170 | Code: 171 | stack=2, locals=1, args_size=1 172 | 0: getstatic #21 // Field scala/Predef$.MODULE$:Lscala/Predef$; 173 | 3: aload_0 174 | 4: getfield #23 // Field data$1:Lscala/runtime/IntRef; 175 | 7: getfield #29 // Field scala/runtime/IntRef.elem:I 176 | 10: invokestatic #35 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer; 177 | 13: invokevirtual #39 // Method scala/Predef$.println:(Ljava/lang/Object;)V 178 | 16: return 179 | public com.baidu.bcs.dataplatform.ScalaOuterClass$InnerClass$1(com.baidu.bcs.dataplatform.ScalaOuterClass, scala.runtime.IntRef); 180 | 181 | //截取javap中关于外部类run函数 182 | public void run(); 183 | flags: ACC_PUBLIC 184 | Code: 185 | stack=4, locals=2, args_size=1 186 | 0: new #22 // class scala/runtime/IntRef 187 | 3: dup 188 | 4: iconst_1 189 | 5: invokespecial #26 // Method scala/runtime/IntRef."":(I)V 190 | 8: astore_1 191 | 9: new #28 // class com/baidu/bcs/dataplatform/ScalaOuterClass$InnerClass$1 192 | 12: dup 193 | 13: aload_0 194 | 14: aload_1 195 | 15: invokespecial #31 // Method com/baidu/bcs/dataplatform/ScalaOuterClass$InnerClass$1."":(Lcom/baidu/bcs/dataplatform/ScalaOuterClass;Lscala/runtime/IntRef;)V 196 | 18: invokevirtual #34 // Method com/baidu/bcs/dataplatform/ScalaOuterClass$InnerClass$1.runInnerClass:()V 197 | 21: getstatic #39 // Field scala/Predef$.MODULE$:Lscala/Predef$; 198 | 24: aload_1 199 | 25: getfield #43 // Field scala/runtime/IntRef.elem:I 200 | 28: invokestatic #49 // Method scala/runtime/BoxesRunTime.boxToInteger:(I)Ljava/lang/Integer; 201 | 31: invokevirtual #53 // Method scala/Predef$.print:(Ljava/lang/Object;)V 202 | 34: return 203 | LocalVariableTable: 204 | Start Length Slot Name Signature 205 | 0 35 0 this Lcom/baidu/bcs/dataplatform/ScalaOuterClass; 206 | 9 25 1 data Lscala/runtime/IntRef; 207 | 208 | 通过javap我们可以看到,外部函数的定义局部变量data,在run函数中被包装成成一个scala/runtime/IntRef对象,并且在内部类InnerClass的构造函数中,将其传入到构造函数中, 209 | 因为这个对象是scala自己生成的,所以可以肯定的被包装这个对象是不会改变引用,相当于java final对象,然后函数和内部类之间就可以进行操作了. 210 | 一句话,把本身属于栈的基本类型变量,转换为引用类型,从而实现scala内部类可以读取外部定义函数的局部变量,并且不受final的限制. 211 | 212 | 那如果不是基本类型而是引用类型呢?很简单,封装为scala/runtime/ObjectRef. 213 | 214 | 哈哈哈!!!!终于理清楚java内部类和scala的内部的区别了;虽然这篇问题是要讲闭包,但是我相信很多人和我一样,对闭包与内部类之间的差别很模糊!!!!清晰了 215 | 216 | ## 闭包清理的实现 217 | 218 | 219 | 220 | 221 | 222 | 临时备注: 223 | 参考http://www.cnblogs.com/chenssy/p/3388487.html和http://www.cnblogs.com/yjmyzz/p/3448330.html对内部类/匿名类的实现,更好的来解释这个含义 224 | 225 | 226 | -------------------------------------------------------------------------------- /spark/mllib-pipeline.md: -------------------------------------------------------------------------------- 1 | MLLib Pipeline的实现分析 2 | =============== 3 | 4 | 在北京的最近一次Spark MeetUp中,期间连线孟祥瑞谈到下一步MLLib发展计划时, 他说到MLLib未来的发展更加注重可实践性,每一个被MLLib的所收录的算法都是可以解决企业中真实的问题. 5 | 另外谈到要把MLLine做的更加灵活,其中核心功能就是流水线, 另外还谈到将Spark Sql中schemeRDD引入到mllib中, 这两个功能即今天要分析的MLLib Pipeline的实现 6 | 7 | 在2014年11月,他就在Spark MLLib代码中CI了一个全新的package:"org.apache.spark.ml", 和传统的"org.apache.spark.mllib"独立, 这个包即Spark MLLib的 8 | [Pipeline and Parameters](https://docs.google.com/document/d/1rVwXRjWKfIb-7PI6b86ipytwbUH7irSNLF1_6dLmh8o/edit#heading=h.kaihowy4sg6c) 9 | 10 | pipeline即机器学习流水线, 在实际的机器学习应用过程中,一个任务(Job)由很多过程(Stage)组成, 比如日志的收集,清理,到字段/属性/feature的提取, 多算法的组合而成的模型, 11 | 模型的离线训练和评估,到最后的模型在线服务和在线评估,所进行的每一个stage都可以概况为数据到数据的转换以及数据到模型的转换. 12 | 13 | 后面我们会看到,mllib pipeline中的stage也做了相应的划分. 14 | 15 | Parameters即参数化,机器学习大部分的过程是一个参数调优的过程,一个模型应该很显示的告诉使用者,模型有那些参数可以进行设置,以及这些参数的默认值是怎么样的;在现有的机器学习算法中, 16 | 模型的参数可能是以模型类字段或通过一些函数进行设置,不是很直观的展现当前模型有哪些可以调优的参数; 17 | 18 | 针对这个问题,在这个版本中,提出了Parameters功能,通过trait的方式显式地指定stage拥有那些参数. 19 | 20 | 下面我们就针对这两个部分进行分析, pipeline到我今天学习为止,还只有三个PR,很多东西应该还在实验中,这里做一个分析仅供学习和备忘. 21 | 22 | ##Parameters 23 | 24 | class LogisticRegressionModel ( 25 | override val weights: Vector, 26 | override val intercept: Double) 27 | extends GeneralizedLinearModel(weights, intercept) with ClassificationModel with Serializable { 28 | 29 | private var threshold: Option[Double] = Some(0.5) 30 | 31 | 上面是传统的org.apache.spark.mllib包中一个分类器:LogisticRegressionModel, 如果理解logistics分类器,那么我知道其中的threshold为模型一个很重要的参数. 32 | 但是从对于一般的用户来说,我们只知道这个模型类中有一个threshold字段,并不能很清楚了该字段是否是模型可调优的参数,还是只是类的一个"全局变量"而已; 33 | 34 | 针对这个问题, 就有了Parameters参数化的概念,先直接看结果: 35 | 36 | class LogisticRegressionModel private[ml] ( 37 | override val parent: LogisticRegression, 38 | override val fittingParamMap: ParamMap, 39 | weights: Vector) 40 | extends Model[LogisticRegressionModel] with LogisticRegressionParams { 41 | 42 | private[classification] trait LogisticRegressionParams extends Params 43 | with HasRegParam with HasMaxIter with HasLabelCol with HasThreshold with HasFeaturesCol 44 | with HasScoreCol with HasPredictionCol { 45 | 46 | 我们看到这里的LogisticRegressionModel实现了LogisticRegressionParams,而LogisticRegressionParams继承了Params类,并且"拥有"一组Param,即HasMaxIter, HasRegParam之类. 47 | 相比传统的LogisticRegressionModel, 这个版本我们可以清楚的看到,该模型有RegParam, MaxIter等7个可控参数,其中包括我们上面谈到的threshold参数, 即HasThreshold. 48 | 49 | 即Parameters 将mllib中的组件参数进行标准化和可视化,下面我们继续针对Parameters进行分析. 50 | 51 | class Param[T] (val parent: Params,val name: String,val doc: String, 52 | val defaultValue: Option[T] = None)extends Serializable { 53 | 54 | Param表示一个参数,从概念上来说,一个参数有下面几个属性: 55 | 56 | + param的类型:即上面的[T], 它表示param值是何种类型 57 | + param的名称:即name 58 | + param的描述信息,和name相比, 它可以更长更详细, 即doc 59 | + param的默认值, 即defaultValue 60 | 61 | 针对param的类型,ml提供了一组默认的子类, 如IntParam,FloatParam之类的.就不详细展开 62 | 63 | 另外针对Param, 提供了接口来设置Param的值 64 | 65 | def w(value: T): ParamPair[T] = this -> value 66 | def ->(value: T): ParamPair[T] = ParamPair(this, value) 67 | case class ParamPair[T](param: Param[T], value: T) 68 | 69 | 即将Param和value封装为paramPair类, paramPair是一个case类,没有其他的方法, 仅仅封装了Param和Value对. 因此我们可以通过param->value来创建一个ParamPair, 后面我们会看到怎么对ParamPair 70 | 进行管理. 71 | 72 | 上面谈到Param其中一个参数我们没有进行描述, 即"parent: Params". 如果站在模型, 特征提取器等组件角度来, 它们应该是一个param的容器,它们的逻辑代码可以使用自身的容器中的param. 73 | 换句话说,如果一个组件继承自Params类,那么该组件就是一个被参数化的组件. 74 | 75 | 参考上面的LogisticRegressionModel, 它继承了LogisticRegressionParams, 而LogisticRegressionParams实现了Params, 此时LogisticRegressionModel就是一个被参数化的模型. 即自身是一个param容器 76 | 77 | 现在问题来了, 一个组件继承自Params, 然而我们没有通过add等方式来将相应的param添加到这个容器里面, 而是通过"with HasRegParam"方式来进行设置的,到底是怎么实现的呢?看一个具体param的实现 78 | 79 | private[ml] trait HasRegParam extends Params { 80 | val regParam: DoubleParam = new DoubleParam(this, "regParam", "regularization parameter") 81 | def getRegParam: Double = get(regParam) 82 | } 83 | 84 | private[ml] trait HasMaxIter extends Params { 85 | val maxIter: IntParam = new IntParam(this, "maxIter", "max number of iterations") 86 | def getMaxIter: Int = get(maxIter) 87 | } 88 | 89 | 我们看到每个具体的RegParam都是继承自Params, 这个继承感觉意义不大,所有这里就不纠结它的继承机制, 核心是它的val regParam: DoubleParam常量的定义,这里的常量会被编译为一个函数, 90 | 其中函数为public, 返回值为DoubleParam, 参数为空. 为什么要强调这个呢?这是规范. 一个具体的Param只有这样的实现, 它才会被组件的Params容器捕获. 怎么捕获呢? 在Params中有这样一个代码: 91 | 92 | def params: Array[Param[_]] = { 93 | val methods = this.getClass.getMethods 94 | methods.filter { m => 95 | Modifier.isPublic(m.getModifiers) && 96 | classOf[Param[_]].isAssignableFrom(m.getReturnType) && 97 | m.getParameterTypes.isEmpty 98 | }.sortBy(_.getName) 99 | .map(m => m.invoke(this).asInstanceOf[Param[_]]) 100 | } 101 | 102 | 这里就清晰了,一个组件, 继承Params类, 成为一个可参数化的组件, 该组件就有一个params方法可以返回该组件所有配置信息,即 Array[Param[_]]. 因为我们组件使用With方式继承了符合上面规范的"常量定义", 103 | 这些param会被这里的def params所捕获, 从而返回该组件所有的Params; 104 | 105 | 不过还有一个问题,我们一直在说, 组件继承Params类, 成为一个可参数化的组件,但是我这里def params只是返回 Array[Param[_]], 而一个属性应该有Param和value组成, 即上面我们谈到ParamPair, 106 | 因此Params类中应该还有一个容器,维护Array[ParamPair], 或者还有一个Map,维护param->value对. 107 | 108 | 没错,这就是ParamMap的功能, 它维护了维护param->value对.并且每个实现Params的组件都有一个paramMap字段. 如下: 109 | 110 | trait Params extends Identifiable with Serializable { 111 | protected val paramMap: ParamMap = ParamMap.empty 112 | } 113 | 114 | 具体的ParamMap实现这里就不分析, 就是一个Map的封装, 这里我们总结一下Parameters的功能: 115 | 116 | > Parameters即组件的可参数化, 一个组件,可以是模型,可以是特征选择器, 如果继承自Params, 那么它就是被参数化的组件, 将具体参数按照HasMaxIter类的规范进行定义, 然后通过With的 117 | > 方式追加到组件中,从而表示该组件有相应的参数, 并通过调用Params中的getParam,set等方法来操作相应的param. 118 | 119 | 整体来说Parameters的功能就是组件参数标准化 120 | 121 | ### Pipeline 122 | 如上所言, mllib pipeline是基于Spark SQL中的schemeRDD来实现, pipeline中每一次处理操作都表现为对schemeRDD的scheme或数据进行处理, 这里的操作步骤被抽象为Stage, 123 | 即PipelineStage 124 | 125 | abstract class PipelineStage extends Serializable with Logging { 126 | private[ml] def transformSchema(schema: StructType, paramMap: ParamMap): StructType 127 | } 128 | 129 | 抽象的PipelineStage的实现很简单, 只提供了transformSchema虚函数, 由具体的stage进行实现,从而在一定参数paramMap作用下,对scheme进行修改(transform). 130 | 131 | 上面我们也谈到, 具体的stage是在PipelineStage基础上划分为两大类, 即数据到数据的转换(transform)以及数据到模型的转换(fit). 132 | 133 | + Transformer: 数据到数据的转换 134 | + Estimator:数据到模型的转换 135 | 136 | 137 | 我们首先来看Transformer, 数据预处理, 特征选择与提取都表现为Transformer, 它对提供的SchemaRDD进行转换生成新的SchemaRDD, 如下所示: 138 | 139 | abstract class Transformer extends PipelineStage with Params { 140 | def transform(dataset: SchemaRDD, paramMap: ParamMap): SchemaRDD 141 | } 142 | 143 | 在mllib中, 有一种特殊的Transformer, 即模型(Model), 下面我们会看到模型是Estimator stage的产物,但是model本身是一个Transformer, 模型是经过训练和挖掘以后的一个 144 | 对象, 它的一个主要功能就是预测/推荐服务, 即它可以对传入的dataset:SchemaRDD进行预测, 填充dataset中残缺的决策字段或评分字段, 返回更新后的SchemaRDD 145 | 146 | Estimator stage的功能是模型的估计/训练, 即它是一个SchemaRDD到Model的fit过程. 如下所示fit接口. 147 | 148 | abstract class Estimator[M <: Model[M]] extends PipelineStage with Params { 149 | def fit(dataset: SchemaRDD, paramMap: ParamMap): M 150 | } 151 | 152 | 模型训练是整个数据挖掘和机器学习的目标, 如果把整个过程看着为一个黑盒, 内部可以包含了很多很多的特征提取, 模型训练等子stage, 但是站在黑盒的外面, 153 | 整个黑盒的输出就是一个模型(Model), 我们目标就是训练出一个完美的模型, 然后再利于该模型去做服务. 154 | 155 | 这句话就是pipeline的核心, 首先pipeline是一个黑盒生成的过程, 它对外提供fit接口, 完成黑盒的训练, 生成一个黑盒模型, 即PipelineModel 156 | 157 | 如果站在白盒的角度来看, pipeline的黑盒中肯定维护了一组stage, 这些stage可以是原子的stage,也可能继续是一个黑盒模型, 在fit接口调用时候, 会按照流水线的 158 | 顺序依次调用每个stage的fit/transform函数,最后输出PipelineModel. 159 | 160 | 下面我们就来分析 pipeline和PipelineModel具体的实现. 161 | 162 | class Pipeline extends Estimator[PipelineModel] { 163 | 164 | val stages: Param[Array[PipelineStage]] = new Param(this, "stages", "stages of the pipeline") 165 | def setStages(value: Array[PipelineStage]): this.type = { set(stages, value); this } 166 | def getStages: Array[PipelineStage] = get(stages) 167 | 168 | override def fit(dataset: SchemaRDD, paramMap: ParamMap): PipelineModel = { 169 | } 170 | 171 | private[ml] override def transformSchema(schema: StructType, paramMap: ParamMap): StructType = { 172 | val map = this.paramMap ++ paramMap 173 | val theStages = map(stages) 174 | require(theStages.toSet.size == theStages.size, 175 | "Cannot have duplicate components in a pipeline.") 176 | theStages.foldLeft(schema)((cur, stage) => stage.transformSchema(cur, paramMap)) 177 | } 178 | } 179 | 180 | Pipeline首先是一个Estimator, fit输出的模型为PipelineModel, 其次Pipeline也继承Params类, 即被参数化, 其中有一个参数, 即stages, 它的值为Array[PipelineStage], 181 | 即该参数存储了Pipeline拥有的所有的stage; 182 | 183 | Pipeline提供了fit和transformSchema两个接口,其中transformSchema接口使用foldLeft函数, 对schema进行转换.但是对fit接口的理解,需要先对PipelineModel进行理解, 184 | 分析完PipelineModel,我们再回头来看fit接口的实现. 先多说一句, 虽然pipeline里面的元素都是stage, 但是两种不同类型stage在里面功能不一样, 所在位置也会有不同的结果, 185 | 不过这点还是挺好理解的. 186 | 187 | class PipelineModel private[ml] ( 188 | override val parent: Pipeline, 189 | override val fittingParamMap: ParamMap, 190 | private[ml] val stages: Array[Transformer]) 191 | extends Model[PipelineModel] with Logging { 192 | 193 | override def transform(dataset: SchemaRDD, paramMap: ParamMap): SchemaRDD = { 194 | // Precedence of ParamMaps: paramMap > this.paramMap > fittingParamMap 195 | val map = (fittingParamMap ++ this.paramMap) ++ paramMap 196 | transformSchema(dataset.schema, map, logging = true) 197 | stages.foldLeft(dataset)((cur, transformer) => transformer.transform(cur, map)) 198 | } 199 | } 200 | 201 | 我们看到PipelineModel是由一组Transformer组成, 在对dataset进行预测(transform)时, 是按照Transformer的有序性(Array)逐步的对dataset进行处理. 换句话说, Pipeline的 202 | 输出是一组Transformer, 进而构造成PipelineModel. 那么我再回头来看看Pipeline的fit接口. 203 | 204 | override def fit(dataset: SchemaRDD, paramMap: ParamMap): PipelineModel = { 205 | transformSchema(dataset.schema, paramMap, logging = true) 206 | val map = this.paramMap ++ paramMap 207 | val theStages = map(stages) 208 | // Search for the last estimator. 209 | var indexOfLastEstimator = -1 210 | theStages.view.zipWithIndex.foreach { case (stage, index) => 211 | stage match { 212 | case _: Estimator[_] => 213 | indexOfLastEstimator = index 214 | case _ => 215 | } 216 | } 217 | var curDataset = dataset 218 | val transformers = ListBuffer.empty[Transformer] 219 | theStages.view.zipWithIndex.foreach { case (stage, index) => 220 | if (index <= indexOfLastEstimator) { 221 | val transformer = stage match { 222 | case estimator: Estimator[_] => 223 | estimator.fit(curDataset, paramMap) 224 | case t: Transformer => 225 | t 226 | case _ => 227 | throw new IllegalArgumentException( 228 | s"Do not support stage $stage of type ${stage.getClass}") 229 | } 230 | curDataset = transformer.transform(curDataset, paramMap) 231 | transformers += transformer 232 | } else { 233 | transformers += stage.asInstanceOf[Transformer] 234 | } 235 | } 236 | 237 | new PipelineModel(this, map, transformers.toArray) 238 | } 239 | 240 | 拿实例来解析: 241 | 242 | Transformer1 ---> Estimator1 --> Transformer2 --> Transformer3 --> Estimator2 --> Transformer4 243 | 244 | 我们知道Estimator和Transformer的区别是, Estimator需要经过一次fit操作, 才会输出一个Transformer, 而Transformer就直接就是Transformer; 245 | 246 | 对于训练过程中的Transformer,只有一个数据经过Transformer操作后会被后面的Estimator拿来做fit操作的前提下,该Transformer操作才是有意义的,否则训练数据不应该经过该Transformer. 247 | 248 | 拿上面的Transformer4来说, 它后面没有Estimator操作, 如果对训练数据进行Transformer操作没有任何意义,但是在预测数据上还是有作用的,所以它可以直接用来构建PipelineModel. 249 | 250 | 对于训练过程中Estimator(非最后一个Estimator), 即上面的Estimator1,非Estimator2, 它训练数据上训练出的模型以后, 需要利用该模型对训练数据进行transform操作,输出的数据 251 | 继续进行后面Estimator和Transformer操作. 252 | 253 | 拿缺失值填充的例子来说, 我们可以利用当前数据,训练出一个模型, 该模型计算出每个字段的平均值, 然后我们理解利用这个模型对训练数据进行缺失值填充. 254 | 255 | > 但是对于最后一个Estimator,其实是没有必要做这个"curDataset = transformer.transform(curDataset, paramMap)"操作的, 所以这里还是可以有优化的!!嘿嘿!!! 256 | 257 | 总结:好了,到目前为止,已经将Pipeline讲解的比较清楚了, 利用Pipeline可以将数据挖掘中各个步骤进行流水线化, api方便还是很简介清晰的! 258 | 259 | 最后拿孟祥瑞CI的描述信息中一个例子做结尾,其中pipeline包含两个stage,顺序为StandardScaler和LogisticRegression 260 | 261 | val scaler = new StandardScaler() 262 | .setInputCol("features") 263 | .setOutputCol("scaledFeatures") 264 | val lr = new LogisticRegression() 265 | .setFeaturesCol(scaler.getOutputCol) 266 | 267 | val pipeline = new Pipeline() 268 | .setStages(Array(scaler, lr)) 269 | val model = pipeline.fit(dataset) 270 | val predictions = model.transform(dataset) 271 | .select('label, 'score, 'prediction) 272 | 273 | 274 | @end 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | -------------------------------------------------------------------------------- /spark/pregel-bagel.md: -------------------------------------------------------------------------------- 1 | Pregel原理分析与Bagel实现 2 | ============== 3 | 4 | [pregel 2010年](http://people.apache.org/~edwardyoon/documents/pregel.pdf)就已经出来了, [bagel](https://spark.apache.org/docs/latest/bagel-programming-guide.html)也2011年 5 | 就已经在spark项目中开源, 并且在最近的graphX项目中声明不再对bagel进行支持, 使用graphX的"高级API"进行取代, 种种迹象好像说明pregel这门技术已经走向"末端", 其实个人的观点倒不是这样的; 6 | 最近因为项目的需要去调研了一下图计算框架,当看到pregel的时候就有一种感叹原来"密密麻麻"的图计算可以被简化到这样. 虽然后面项目应该是用graphx来做,但是还是想对pregel做一个总结. 7 | 8 | ###Pregel的原理分析 9 | 说到MapReduce,我们想到的是Hadoop这种类型的计算平台, 可是我更加愿意把它理解为一个编程思想或一种算法. 虽然现在Spark中采用"高级API"来提供一种数据处理的接口,但是它的核心还是map, 还是 10 | reduce,以及shuffle. Pregel所处的位置和MapReduce也一样. 11 | 12 | Pregel来自google, 最初是为了解决PageRank计算问题,由于MapReduce并不适于这种场景,所以需要发展新的计算模型, 从而产生了Pregel. Pregel和MapReduce一样, 模型很简单, 13 | Spark中基于Pregel的开源实现Bagel也不过300行代码, 但是很强大. 14 | 15 | Pregel解决的是图计算问题, 随着数据量的增长, 不管单机算法多么牛逼, 都无法解决现实中的问题.因此需要使用分布式的算法和平台来解决. Pregel就是一个分布式图计算框架. 分析pregel之前,我们 16 | 首先来思考两个问题: 17 | 18 | + 整个图的数据是分布多个机器上, 首先需要解决的问题就是图的划分, 按照边进行划分?还是按照顶点进行划分?划分以后怎么表示整个分布式图和每个子图? 19 | + 图是分布式的存储, 那么图上算法怎么进行计算? 不同机器上的顶点之间进行进行交互? 20 | 21 | 下面我们就来看Pregel是怎么解决这两个问题的. 22 | 23 | ####图的切割方式 24 | 虽然我们这里讨论是图计算,其实图的分布式表示也是图数据库中核心问题.因为只有切割了以后,一个大图才可以被分布式图数据库进行存储. 图数据因为顶点/边之间的强耦合性, 25 | 切割的方式方法比行列式数据要复杂很多, 切割的不合理会导致机器之间存储不均衡, 计算过程中也会因此带来大量的网络通信. 这两点也是衡量一个图切割的方法好坏的标准. 26 | 27 | 图的分布式表示有两种切割方式: 点切法和边切法,下图来自Spark GraphX, 为图的两种切法的可视化表示 28 | 29 | ![截图来自Spark GraphX](../image/edge_cut_vs_vertex_cut.png) 30 | 31 | 按照边切法的结果是原始图中每个顶点所有边都存储在一台机器上, 换句话说, 不管按照哪条边进切割, 任何一个顶点的关联边都在一台机器上,但是边的另外一个顶点, 有可能分布 32 | 在另外一个机器上, 即在边上只存储了目标顶点的索引数据. 33 | 边切法好处是对于读取某个顶点的数据时, 只要到一台机器上就可以了, 缺点是如果读取边的数据和目标顶点有关系, 那么就会引起跨机器的通信开销. 34 | 35 | 按照点切发的结果是原始图中每条边的数据(属性数据, 原始顶点, 目标顶点)都存储在一台机器上, 但是一个顶点的所有关联边数据可能分布在所有的机器上. 36 | 点切法好处是处理一条边时, (原始顶点, 边数据, 目标顶点)的三份数据都在一台机器上, 对应每个顶点的所有边,也可以并行在多台机器上进行计算, 因此减少了处理过程中的网络的开销. 37 | 但是缺点如果对一个顶点进行进行修改, 需要将它同步到多个子图中. 即图节点值数据的一致性问题。 38 | 39 | 现在问题来了, Pregel是点切法还是边切法呢?答案是边切法. 在Pregel中, 将图虚化为无数的顶点,每个顶点分配一个标示符ID,并保存了该顶点所关联的所有下游节点(即边) 40 | 每一次迭代过程中, 分别对每个顶点进行出来, 并将每个顶点的处理结果通过消息发送给它的关联节点. 上面我们谈到, 边切法的缺陷是引起跨机器开销, 但是Pregel有combine等机制,对消息进行 41 | 合并,从而优化了跨机器的开销, 关于combine后面会详细描述. 42 | 43 | 注意: 我们这里谈到Pregel是边切法, Bagel的实现也是边切法, 但是spark graphX的实现是点切法; 关于graphX后面再开文具体进行描述. 这里不要因为这里解释而误导对graphX的理解 44 | 45 | ####Pregel运行模式:BSP计算模型 46 | Pregel是遵循BSP计算模型, BSP即整体同步并行计算模型(Bulk Synchronous Parallel Computing Model), 基于该模型的分布式计算项目也很多,其中包括Apache的顶级项目[Hama](https://hama.apache.org/), 47 | 48 | > Many data analysis techniques such as machine learning and graph algorithms require iterative computations, 49 | > this is where Bulk Synchronous Parallel model can be more effective than "plain" MapReduce. Therefore To run such iterative data analysis applications more efficiently, 50 | > Hama offers pure Bulk Synchronous Parallel computing engine. 51 | 52 | 从Hama的描述来看,BSP计算模型在处理迭代计算(iterative computations)有着很大的性能优势;那么BSP具体是什么呢? 53 | 54 | BSP计算模型由Master和Worker组成, 其中Master负责Worker之间的协调工作,而所有的Worker之间同步执行, Worker之间通过消息的方式进行同步; 其中Master协调工作的核心概念为超级步(superstep), 55 | 和计算机的时钟概念类似, 每一个超级步为一次迭代, 所以站在BSP 整体角度来看, BSP程序从开始到结束,由一组超级步组成. 56 | 57 | 每一个超级步的从开始必须是在上一个超级步运行完成, 那么每个超级步做了什么工作呢? 58 | 59 | + worker并行计算:BSP模型针对每个worker有一个消息队列, 在每个superstep开始时候,会从消息队列中读取属于该worker的消息; 并完成worker的特定业务的计算 60 | + 每个superstep间迭代的核心是消息, worker在每次迭代开始会读取上一次迭代中其他worker发送给该worker的数据,并在本次迭代完成以后,根据业务需求,将消息发送给特定worker 61 | + master负责superstep的同步,在superstep开始时,将消息进行合并并分发给相应的worker, 并监控所有worker运行结果, 汇总所有的worker运行结束时候发送的消息. 如果Master监听到 62 | 在某次superstep以后,所有worker都标记为结束,那么就结束整个BSP程序的运行. 63 | 64 | 从上面我们可以看到, 站在BSP程序角度来看, 多个superstep间同步执行, 而superstep内部,每个worker并行运行,并基于消息来进行worker之间的数据交互. 65 | 66 | 整体来看,BSP模型由模块(每个worker理解为一个模块), 消息(消息的传递, 合并以及分发, Matser的核心功能之一), 同步时钟(superstep间的同步)组成. 67 | 68 | 下面我们就来分析一下Pregel中的BSP计算模型的应用. 69 | 70 | 编写Pregel程序的思想是"像顶点一样思考(Think Like A Vertex)", 怎么理解呢? Pregel应用BSP模型的核心是将图中的每个顶点理解为一个模块(worker),整个BSP程序的计算都是维护和更新每个顶点值, 71 | 比如PageRank, 维护每个页面顶点的rank值, 单源最短距离就是维护每个顶点到源点的距离.上面我们讨论到Pregel是按照边切法进行切割, 即每个顶点的所有边数据都在同一个机器上, 72 | 此时如果每个顶点为一个worker, 那么在每次superstep中,顶点之间可以并行计算, 并在计算结束以后, 通过消息的方式来与其他顶点之间通信.这里的消息发送源和发送目标很容易理解, 73 | 发送时, 每个顶点将相应的消息发送到该顶点对应的出边顶点, 接受时, 消息经过master合并, 每个顶点接受它入边所对应的消息. 而消息的合并,分发,superstep的同步则由Master进行同步. 74 | 75 | 从上面一段我们可以总结以下的计算模式: 76 | 77 | + 像顶点一样思考, 即每个顶点对应一个处理函数Compute 78 | + Compute函数应该包含一个消息容器, 在每次superstep时, 由Master传递给每个顶点 79 | + Compute的核心逻辑是消息处理, 并在完成消息处理以后更新当前顶点的值, 同时根据新的顶点值,将相应的消息分发给自己出边顶点 80 | + Compute函数内部可以修改一个状态值, Master根据该状态值来确认该迭代是否还需要继续进行迭代 81 | 82 | 下面我们给出compute函数的原型, 83 | 84 | void computer(messageIterators msgs){ 85 | for(; !msgs.done; msgs,next()){ 86 | doSomeThing(); 87 | } 88 | //更新当前顶点值和状态 89 | update(value, status) 90 | //给每个出边顶点分发消息 91 | sendMsgsToNeighborhood() 92 | } 93 | 94 | 上面我们基本分析了Pregel的计算模型, 不过我也看到它的缺点: 消息传递的代价. 每一次消息传播其实就传统的shuffle过程, 在消息不是特别大, 可以做内存shuffle, 可以理解.但是消息特别大时候, 95 | 可能需要上文件shuffle, 这个代码做过mapreduce/spark都清楚, 每次迭代都是shuffle,性能和带宽肯定是瓶颈. 96 | 97 | ####Combiners 98 | 上面我们谈到,BSP模型每次superstep会因为消息的传递,带来很大的网络开销, 但是其实大部分情况下, 和mapreduce中shuffle一致, 可以优先进行一下map端的combiner操作,来减少网络 99 | 传输. 上面我们谈到Pregel是基于边切分, 每个节点一个worker,但是在物理层面, 一组worker可能会调度到一台物理机器上, 因为在将一个消息从这组worker传递到Master上进行聚合之前,可以 100 | 在每个物理机器上做combiner操作, 从而减少大量的网络传输. 101 | 102 | 比方说, 假如Compute() 收到许多的int 值消息, 而它仅仅关心的是这些值的和, 而不是每一个int的值, 这种情况下,系统可以将发往同一个顶点的多个消息合并成一个消息, 该消息中仅包含它们 103 | 的和值,这样就可以减少传输和缓存的开销。 104 | 105 | 关于combiner注意点: combiners的合并的对象是消息, 而不是每个顶点的数据, 下面我们会介绍pregel中另外一个概念:Aggregators. 106 | 107 | ####Aggregators 108 | pregel是站在顶点的角度来思考问题, 每次迭代计算都是顶点与相邻顶点之间的消息传递, 但是在某些应用中, 可能需要站在全局图的角度思考问题. 109 | 110 | 打一个简单的比如: 每次迭代之前需要计算所有节点的一个度量值的均值, 如果超过一定值, 所有顶点就结束迭代.这个时候,仅仅通过消息是不能进行判断, 111 | 需要对全局图顶点做一次aggregator.然后把aggregator的值传递给每个顶点的computer函数, 在computer内部根据aggregator的值来更新顶点的状态. 112 | 那么上面的computer函数就需要针对一个参数Aggregator: 113 | 114 | void computer(messageIterators msgs, Aggregator agg){ 115 | for(; !msgs.done; msgs,next()){ 116 | doSomeThing(); 117 | } 118 | //更新当前顶点值和状态 119 | update(value, status) 120 | //给每个出边顶点分发消息 121 | sendMsgsToNeighborhood() 122 | } 123 | 124 | 站在spark角度, pregel的核心对象是存储所有顶点的RDD, 那么aggregator操作,其实就对顶点的RDD做一次reduce操作. 后面我们看到Bagel的实现的时候,就很清晰看到它的功能. 125 | 126 | 另外需要强调一下,aggregator操作是和每次superstep相关联的, 即每个superstep就会做一次aggregator操作, 并且在这次computer执行之前, 换句话说, aggregator操作是对上一次 127 | superstep的顶点数据做聚合操作. 128 | 129 | ####图的修改 130 | 我们上面谈到, pregel是站在顶点的角度来计算和更新顶点的值,但是在实际的应用中,有一类算法,可能在运行过程中对图的结构进行修改,比如新增节点/边,删除节点/边. 131 | 在实现的角度上来, 这个逻辑需要"pregel内核"的执行,computer接口中只能将需求以特定的方式传递给master, 由master进行处理. 目前Bagel是没有实现这种部分逻辑,毕竟大部分应用 132 | 是不会在计算过程中做图的修改操作. 133 | 134 | 但是在原理上看, 图的修改存在一致性的问题, 即多个worker对图并行的对图进行修改,那么怎么保证图修改的一致性呢? Pregel中用两种机制来决定如何调用:局部有序和handlers 135 | 136 | 在每次个superstep中, 删除会首先被执行, 先删除边后删除顶点,因为顶点的删除通常也意味着删除其所有的出边. 然后执行添加操作, 先增加顶点后增加边, 并且都会在Compute()函数调用前完成. 137 | 至于是否是在Aggregator执行之前执行就不太确定了,没有查询到相应的信息, 原则上来说应用是先执行图修改,再执行aggregator. 这种局部有序的操作保证了大多数冲突的结果是确定的。 138 | 139 | 剩余的冲突就需要通过用户自定义的handlers来解决. 如果在一个superstep中有多个请求需要创建一个相同的顶点,在默认情况下系统会随便挑选一个请求,但有特殊需求的用户可以 140 | 定义一个更好的冲突解决策略,用户可以在Vertex类中通过定义一个适当的handler函数来解决冲突. 同一种handler机制将被用于解决由于多个顶点删除请求或多个边增加请求或删除 141 | 请求而造成的冲突. 我们委托handler来解决这种类型的冲突,从而使得Compute()函数变得简单,而这样同时也会限制handler和Compute()的交互. 142 | 143 | 另外有一个图的修改很容易实现, 即纯local的图改变, 例如一个顶点添加或删除其自身的出边或删除其自己. Local的图修改不会引发冲突,并且顶点或边的本地增减能够立即生效, 144 | 很大程度上简化了分布式的编程. 这个在Bagel中也比较实现, 毕竟出边是和顶点一起存储在同一个机器上. 145 | 146 | ---------------------- 147 | OK!上面基本上解析了Pregel的原理, 还有一些概念没有谈到, 比如错误容忍, 每次顶点在superstep之前 先做本地的checkout, 在失败的时候可以恢复过来. 这里就不做详细的解析. 148 | 下面我们来看具体的Bagel的实现. 149 | 150 | ###Bagel的实现 151 | Bagel是Pregel一个开源实现, 目前代码开源在Spark源码中, 不过Spark官方已经放弃对这块的支持, 优先使用GraphX来进行图计算. Bagel代码量很短, 才300行, 这里简单对代码进行过一遍, 152 | 核心是围绕上面谈到的概念进行解析. 153 | 154 | Pregel是站在顶点的角度来思考, 每个顶点是一个执行单元, 自身有一个状态 ,表示是否需要对该顶点进行计算 155 | 156 | trait Vertex { 157 | def active: Boolean 158 | } 159 | 160 | 上面关于顶点状态描述较少, 这里补充一下, 顶点状态的变化. 上面我们谈到顶点状态可以在Computer函数中进行修改, 没错,但是如果一个节点被修改active=false, 不代表这个节点就不会进行后面计算了, 161 | 一个处于active=false状态的节点,在后面接收到其他节点发送的消息时, 还是会处理;但是如果所有节点都没有发送任何消息并且所有都处于active=false状态, 这个时候整个计算就结束了,注意 162 | 这里两个条件是"并且/AND", 都必须满足. 所以在Computer函数中, 如果没有消息可以发送出去了, 则一定要将自身的状态设置为false. 163 | 164 | 上面对Vertex定义很简单, 而且在Bagel, 没有Edge这个类来定义边, 具体的边信息, 都是定义在Vertex中, 即直接定义它的出边信息. 同时也可以在Vertex定义其他元素, 来表示顶点的属性数据. 165 | 166 | 实例如下: 167 | 168 | class PRVertex() extends Vertex with Serializable { 169 | var value: Double = _ 170 | var outEdges: Array[String] = _ 171 | var active: Boolean = _ 172 | 173 | def this(value: Double, outEdges: Array[String], active: Boolean = true) { 174 | this() 175 | this.value = value 176 | this.outEdges = outEdges 177 | this.active = active 178 | } 179 | } 180 | 181 | 每个顶点都有一个rank值, 以及一组出边Array, 其中Array每个元素为出边所对应的顶点的名称. 那下面问题来了, 顶点集在Bagel是怎么表示的?答案是RDD,如下所示的顶点集: 182 | 183 | vertices: RDD[(K, V)] 184 | 185 | 其中K为顶点的标示符号, 应该来说, 它应该唯一. V就为上面的Vertex子类, 存储了每个顶点的属性值, 状态信息和出边信息. 186 | 187 | 第二个重要的类就是消息类:Message. 在Bagel/Pregel, 必须明确的指定每个消息所发送的目标顶点, 至于消息中其他的值根据业务需求可以添加, 实例如下: 188 | 189 | trait Message[K] { 190 | def targetId: K 191 | } 192 | class PRMessage() extends Message[String] with Serializable { 193 | var targetId: String = _ 194 | var value: Double = _ 195 | 196 | def this(targetId: String, value: Double) { 197 | this() 198 | this.targetId = targetId 199 | this.value = value 200 | } 201 | } 202 | 203 | 在Bagel运行过程中, 每个顶点都对应了一个消息迭代器, 因此在也是一个RDD: 204 | 205 | messages: RDD[(K, M)], 206 | 207 | 其中K为指定的顶点的标示符号, 而M为上面具体消息类型; 在每个superstep计算过程中, Bagel首先对上一步骤生成的所有的消息进行combiners操作, 在当前操作结束以后, 利用当前的每个Computer函数 208 | 计算的结果生成一个新的messages: RDD[(K, M)]对象. 209 | 210 | def run[K: Manifest, V <: Vertex, M <: Message[K], C: Manifest, A: Manifest]( 211 | sc: SparkContext, vertices: RDD[(K, V)], 212 | messages: RDD[(K, M)],combiner: Combiner[M, C], 213 | aggregator: Option[Aggregator[V, A]] )( 214 | compute: (V, Option[C], Option[A], Int) => (V, Array[M]) 215 | ): RDD[(K, V)] = { 216 | 217 | var superstep = 0 218 | var verts = vertices 219 | var msgs = messages 220 | var noActivity = false 221 | var lastRDD: RDD[(K, (V, Array[M]))] = null 222 | do { 223 | val aggregated = agg(verts, aggregator) 224 | val combinedMsgs = msgs.combineByKey( 225 | combiner.createCombiner _, combiner.mergeMsg _, combiner.mergeCombiners _, partitioner) 226 | val grouped = combinedMsgs.groupWith(verts) 227 | val superstep_ = superstep 228 | val (processed, numMsgs, numActiveVerts) = 229 | comp[K, V, M, C](sc, grouped, compute(_, _, aggregated, superstep_), storageLevel) 230 | if (lastRDD != null) { 231 | lastRDD.unpersist(false) 232 | } 233 | lastRDD = processed 234 | 235 | verts = processed.mapValues { case (vert, msgs) => vert } 236 | msgs = processed.flatMap { 237 | case (id, (vert, msgs)) => msgs.map(m => (m.targetId, m)) 238 | } 239 | superstep += 1 240 | 241 | noActivity = numMsgs == 0 && numActiveVerts == 0 242 | } while (!noActivity) 243 | 244 | verts 245 | } 246 | 247 | 上面为bagel程序的主入口, 接受节点RDD, 一个初始化空的消息RDD, 一个combiner和aggregator, 同时接受用户的针对每个节点的compute计算函数. 从逻辑上,我们可以看到, Bagel是不能对 248 | 图的结构进行修改, 但是可以在computer函数内部中做local图修改, 加边和删除边, 或者删除自身(即标示自己为dead, 不再处理新接受的消息). 249 | 250 | 结构上来说, 251 | 252 | + 利用aggregator, 对节点RDD做reduce操作 253 | + 利用combiner, 对消息做combiner操作, 并按照顶点进行分组, 从而可以把指定顶点的消息传递给每个computer函数 254 | + 调用每个顶点的compute函数, compute将会返回计算以后节点新的数据和发送的所有消息 255 | + 判断是否需要继续superstep, 具体的逻辑参考上面谈到的顶点状态描述 256 | 257 | ---------------- 258 | 分析不下去了,说实话了, Bagel的逻辑很清晰,很简单, 就不继续写了, 简单浏览一下就清楚具体的实现原理了!!! 259 | 260 | 261 | 总结: Pregel是站在顶点的角度来思考图的计算, 通过顶点之间的消息传递来诠释边的概念, 在传递最短路径, pageRank这类问题有天然优越性. 具体可以参考Spark中实例代码. 不过目前GraphX中 262 | 包含了更多的高级API, 方便以及社区的持续支持, 所以一般情况下, 优先是采用Graphx进行业务开发,后面将会对GraphX做一次总结. 263 | 264 | @End 265 | 266 | 267 | 268 | -------------------------------------------------------------------------------- /spark/scala-implicit.md: -------------------------------------------------------------------------------- 1 | 关于Scala的implicit(隐式转换)的思考 2 | ========== 3 | 4 | 隐式转换是Scala的一大特性, 如果对其不是很了解, 在阅读Spark代码时候就会很迷糊,有人这样问过我? 5 | 6 | > RDD这个类没有reduceByKey,groupByKey等函数啊,并且RDD的子类也没有这些函数,但是好像PairRDDFunctions这个类里面好像有这些函数 7 | > 为什么我可以在RDD调用这些函数呢? 8 | 9 | 答案就是Scala的隐式转换; 如果需要在RDD上调用这些函数,有两个前置条件需要满足: 10 | 11 | + 首先rdd必须是RDD[(K, V)], 即pairRDD类型 12 | + 需要在使用这些函数的前面Import org.apache.spark.SparkContext._;否则就会报函数不存在的错误; 13 | 14 | 参考SparkContext Object, 我们发现其中有上10个xxToXx类型的函数: 15 | 16 | implicit def intToIntWritable(i: Int) = new IntWritable(i) 17 | implicit def longToLongWritable(l: Long) = new LongWritable(l) 18 | implicit def floatToFloatWritable(f: Float) = new FloatWritable(f) 19 | implicit def rddToPairRDDFunctions[K, V](rdd: RDD[(K, V)]) 20 | (implicit kt: ClassTag[K], vt: ClassTag[V], ord: Ordering[K] = null) = { 21 | new PairRDDFunctions(rdd) 22 | } 23 | 24 | 这么一组函数就是隐式转换,其中rddToPairRDDFunctions,就是实现:隐式的将RDD[(K, V)]类型的rdd转换为PairRDDFunctions对象,从而可以在原始的rdd对象上 25 | 调用reduceByKey之类的函数;类型隐式转换是在需要的时候才会触发,如果我调用需要进行隐式转换的函数,隐式转换才会进行,否则还是传统的RDD类型的对象; 26 | 27 | 还说一个弱智的话,这个转换不是可逆的;除非你提供两个隐式转换函数; 这是你会说,为什么我执行reduceByKey以后,返回的还是一个rdd对象呢? 这是因为reduceByKey函数 28 | 是PairRDDFunctions类型上面的函数,但是该函数会返回一个rdd对象,从而在用户的角度无法感知到PairRDDFunctions对象的存在,从而精简了用户的认识, 29 | 不知晓原理的用户可以把reduceByKey,groupByKey等函数当着rdd本身的函数 30 | 31 | 上面是对spark中应用到隐式类型转换做了分析,下面我就隐式转换进行总结; 32 | 33 | 从一个简单例子出发,我们定义一个函数接受一个字符串参数,并进行输出 34 | 35 | def func(msg:String) = println(msg) 36 | 37 | 这个函数在func("11")调用时候正常,但是在执行func(11)或func(1.1)时候就会报error: type mismatch的错误. 这个问题很好解决 38 | 39 | + 针对特定的参数类型, 重载多个func函数,这个不难, 传统JAVA中的思路, 但是需要定义多个函数 40 | + 使用超类型, 比如使用AnyVal, Any;这样的话比较麻烦,需要在函数中针对特定的逻辑做类型转化,从而进一步处理 41 | 42 | 上面两个方法使用的是传统JAVA思路,虽然都可以解决该问题,但是缺点是不够简洁;在充满了语法糖的Scala中, 针对类型转换提供了特有的implicit隐式转化的功能; 43 | 44 | 隐式转化是一个函数, 可以针对一个变量在需要的时候,自动的进行类型转换;针对上面的例子,我们可以定义intToString函数 45 | 46 | implicit def intToString(i:Int)=i.toString 47 | 48 | 此时在调用func(11)时候, scala会自动针对11进行intToString函数的调用, 从而实现可以在func函数已有的类型上提供了新的类型支持,这里有几点要说一下: 49 | 50 | + 隐式转换的核心是from类型和to类型, 至于函数名称并不重要;上面我们取为intToString,只是为了直观, int2str的功能是一样的;隐式转换函数只关心from-to类型之间的匹配 51 | 比如我们需要to类型,但是提供了from类型,那么相应的implicit函数就会调用 52 | + 隐式转换只关心类型,所以如果同时定义两个隐式转换函数,from/to类型相同,但是函数名称不同,这个时候函数调用过程中如果需要进行类型转换,就会报ambiguous二义性的错误, 53 | 即不知道使用哪个隐式转换函数进行转换 54 | 55 | 上面我们看到的例子是将函数的参数从一个类型自动转换为一个类型的例子,在Scala中, 除了针对函数参数类型进行转换以外,还可以对函数的调用者的类型进行转换. 56 | 57 | 比如A+B,上面我们谈到是针对B进行类型自动转换, 其实可以在A上做类型转换,下面我们拿一个例子来说明 58 | 59 | class IntWritable(_value:Int){ 60 | def value = _value 61 | def +(that:IntWritable): IntWritable ={ 62 | new IntWritable(that.value + value) 63 | } 64 | } 65 | implicit def intToWritable(int:Int)= new IntWritable(int) 66 | new IntWritable(10) + 10 67 | 68 | 上面我们首先定义了一个类:IntWritable, 并为int提供了一个隐式类型转换intToWritable, 从而可以使得IntWritable的+函数在原先只接受IntWritable类型参数的基础上, 69 | 接受一个Int类型的变量进行运算,即new IntWritable(10) + 10可以正常运行 70 | 71 | 现在换一个角度将"new IntWritable(10) + 10" 换为"10 + new IntWritable(10)"会是什么结果呢?会报错误吗? 72 | 73 | 按道理是应该报错误,首先一个Int内置类型的+函数,没有IntWritable这个参数类型; 其次,我们没有针对IntWritable类型提供到Int的隐式转换, 即没有提供writableToInt的implicit函数. 74 | 75 | 但是结果是什么?10 + new IntWritable(10)的是可以正常运行的,而且整个表达的类型为IntWritable,而不是Int, 即Int的10被intToWritable函数隐式函数转换为IntWritable类型; 76 | 77 | 结论:隐式转换可以针对函数参数类型和函数对象进行类型转换; 现在问题来了,看下面的例子 78 | 79 | implicit def intToWritable(int:Int)= new IntWritable(int) 80 | implicit def writableToInt(that:IntWritable)=that.value 81 | 82 | val result1 = new IntWritable(10) + 10 83 | val result2 = 10 + new IntWritable(10) 84 | 85 | 在上面的IntWritable类的基础上,我们提供了两个隐式类型转换函数, 即Int和IntWritable之间的双向转换;这样的情况下result1和result2两个变量的类型是什么? 86 | 87 | 答案:result1的类型为IntWritable, result2的类型Int;很好理解, result1中的Int类型的10被intToWritable隐式转换为IntWritable;而result2中的IntWritable(10)被writableToInt 88 | 隐式转换为Int类型; 89 | 90 | 你肯定会问?result2中为什么不是像上面的例子一样, 把Int类型的10隐式转换为IntWritable类型呢?原因就是隐式转换的优先级; 91 | 92 | > 发生类型不匹配的函数调用时, scala会尝试进行类型隐式转换;首先优先进行函数参数的类型转换,如果可以转换, 那么就完成函数的执行; 93 | > 否则尝试去对函数调用对象的类型进行转换; 如果两个尝试都失败了,就会报方法不存在或者类型不匹配的错误; 94 | 95 | OK, Scala的隐式转换是Scala里面随处可见的语法, 在Spark中也很重要, 这里对它的讲解,算是对Shuffle做一个补充了, 即一个RDD之所以可以进行基于Key的Shuffle操作 96 | 是因为RDD被隐式转换为PairRDDFunctions类型. 97 | -------------------------------------------------------------------------------- /spark/spark-catalyst-optimizer.md: -------------------------------------------------------------------------------- 1 | # Spark-Catalyst Optimizer 2 | 3 | Logical Plan Optimizer为Spark Catalyst工作最后阶段了,后面生成Physical Plan以及执行,主要是由Spark SQL来完成。Logical Plan Optimizer主要是对Logical Plan进行剪枝,合并等操作,进而删除掉一些无用计算,或对一些计算的多个步骤进行合并。 4 | 5 | 关于Optimizer:优化包括RBO(Rule Based Optimizer)/CBO(Cost Based Optimizer),其中Spark Catalyst是属于RBO,即基于一些经验规则(Rule)对Logical Plan的语法结构进行优化;在生成Physical Plan时候,还会基于Cost代价做进一步的优化,比如多表join,优先选择小表进行join,以及根据数据大小,在HashJoin/SortMergeJoin/BroadcastJoin三者之间进行抉择。 6 | 7 | 下面我们将会对一些主要的优化Rule进行逐条分析。由于优化的策略会随着知识的发现而逐渐引入,核心还是要理解原理!! 8 | 9 | > 下面实例中的`a,b`为表`t`的两个字段:`CREATE TABLE `t`(`a` int, `b` int, `c` int)`。 10 | > 可以通过explain extended sql来了解我们sql 语句优化情况. 11 | 12 | #### 1. BooleanSimplification: 简化Boolean表达式,主要是针对Where语句中的And/Or组合逻辑进行优化。 13 | 14 | 主要包括三项工作,由于比较简单,就不贴完整的sql语句了: 15 | 16 | 1. Simplifies expressions whose answer can be determined without evaluating both sides 简化不需要对两边都进行计算的Bool表达式。实例:`true or a=b`-->`true` 17 | 18 | 2. Eliminates / extracts common factors. 对`And/OR`两边相同子表达式进行抽离,避免重复计算。实例:`(a=1 and b=2) or (a=1 and b>2);`-->`(a=1) and (b=2 || b>2)` 19 | 20 | 3. Merge same expressions如果`And/OR`左右表达式完全相等,就可以删除一个。实例:`a+b=1 and a+b=1`-->`a+b=1` 21 | 22 | 4. Removes `Not` operator.转换`Not`的逻辑。实例:`not(a>b)`-->`a<=b` 23 | 24 | #### 2. NullPropagation 对NULL常量参与表达式计算进行优化。与True/False相似,如果NULL常量参与计算,那么可以直接把结果设置为NULL,或者简化计算表达式。 25 | 26 | 1. IsNull/IsNotNull/EqualNullSafe 针对NULL进行判断,直接返回NULL。 27 | 2. GetArrayItem/GetMapValue/GetStructField/GetArrayStructFields在key为NULL或者整个Array/Map为NULL的时候,直接返回NULL。 28 | 3. Substring/StringRegexExpression/BinaryComparison/BinaryArithmetic/In 字符串数字进行操作,如果参数为NULL之类的,可以直接返回NULL。 29 | 4. Coalesce/AggregateExpression如果Child表达式有NULL,可以进行删除等操作 30 | 31 | #### 3. SimplifyCasts 删除无用的cast转换。如果cast前后数据类型没有变化,即可以删除掉cast操作 32 | 33 | - 实例:`select cast(a as int) from t` --> `select a from t` //a本身就是int类型 34 | 35 | #### 4. SimplifyCaseConversionExpressions 简化字符串的大小写转换函数。如果对字符串进行连续多次的Upper/Lower操作,只需要保留最后一次转换即可。 36 | 37 | - 实例:`select lower(upper(lower(a))) as c from t;` --> `select lower(a) as c from t;` 38 | 39 | #### 5. SimplifyBinaryComparison 针对>=,<=,==等运算,如果两边表达式`semanticEquals`相等,即可以他们进行简化。 40 | 41 | 如果进行==,>=,<=比较,那么可以简化为Ture;如果进行>,<比较,那么可以简化为Flase 42 | 43 | #### 6. OptimizeIn 使用HashSet来优化set in 操作 44 | 45 | 如果In比较操作符对应的set集合数目超过"spark.sql.optimizer.inSetConversionThreshold"设置的值(默认值为10),那么Catalyst会自动将set转换为Hashset,提供in操作的性能。 46 | 47 | - 实例:`select * from t where a in (1,2,3)`对应的In操作为`Filter a#13 IN (1,2,3)`。 48 | - 而`select * from t where a in (1,2,3,4,5,6,7,8,9,10,11)`为`Filter a#19 INSET (5,10,1,6,9,2,7,3,11,8,4)` 49 | 50 | #### 7. LikeSimplification 简化正则匹配计算。针对`前缀,后缀,包含,相等`四种正则表达式,可以将Like操作转换为普通的字符串比较。 51 | 52 | 1. 如果Like表达式为前缀匹配类型"([^_%]+)%",即转换为startWith字符串函数操作。 53 | - 实例:`select * from t where a like "2%"` --> `+- 'Filter 'a.startwith(2)` //是内部转换,不存在StartWith对应的sql函数 54 | 55 | 2. 同理,如果Like表达式是后缀匹配类型"%([^_%]+)",或包含"%([^_%]+)%",或相等"([^_%]*)"。可以转换为EndsWith,Contains,EqualTo等字符串比较。 56 | 57 | 3. 如果同时为前缀和后缀,即“([^_%]+)%([^_%]+)”,即转换为EndsWith和StartWith进行And操作。 58 | 59 | 60 | #### 8. GetCurrentDatabase和ComputeCurrentTime 在优化阶段对`current_database(), current_date(), current_timestamp()`函数直接计算出值。 61 | 62 | - 实例:`select current_database()` --> `select "default" as current_database()` 63 | - 实例:`select current_timestamp();` --> `select 1467996624588000 AS current_timestamp()` 64 | 65 | 66 | #### 9. ColumnPruning 字段剪枝,即删除Child无用的的output字段 67 | 68 | 1. p @ Project(_, p2: Project) 如果p2输出的字段有p中不需要的,即可以简化p2的输出。 69 | - 实例:`select a from (select a,b from t)` --> `select a from (select a from t)`。在下面的`CollapseProject`会对这个表达式进行二次优化。 70 | 71 | 2. p @ Project(_, a: Aggregate),原理同上,Aggregate只是一个Project的包装而已 72 | - 实例:`select c from (select max(a) as c,max(b) as d from t)` --> `select c from (select max(a) as c from t)`。在下面的`CollapseProject`会对这个表达式进行二次优化。 73 | 74 | 3. a @ Aggregate(_, _, child),a @ Aggregate(_, _, child) 原理同上 75 | 76 | 4. p @ Project(_, child),if sameOutput(child.output, p.output)即child和p有相同的输出,就可以删除Project的封装 77 | - 实例:`select b from (select b from t)` --> `select b from t`这个操作与`CollapseProject`原理一致 78 | 79 | 80 | #### 10. CollapseProject 针对Project操作进行合并。将Project与子Project或子Aggregate进行合并。是一种剪枝操作 81 | 82 | 1. p1 @ Project(_, p2: Project),连续两次Project操作,并且Project输出都是deterministic类型,那么就两个Project进行合并。 83 | - 实例:`select c + 1 from (select a+b as c from t)` -->`select a+b+1 as c+1 from t`。 84 | 85 | 你可以能会问,这种合并会不会因为p1和p2的输出不是完全一样,而优化出错呢? 86 | - 首先如果p1中有,但是p2中没有!抱歉,语法错误。`select c + 1,a from (select a+b as c from t)`-->`cannot resolve '`a`' given input columns` 87 | - 其次如果p2中有,但是p1中不需要!会被ColumnPruning剪掉,不会存在这种case。`select c + 1 from (select a+b as c,a from t)`-->`select a+b+1 as c+1 from t` 88 | 因此是可以证明p1和p2连续两次Project操作,只要他们都是deterministic类型,那么他们输出肯定是一致的。 89 | 90 | 2. p @ Project(_, agg: Aggregate) 原理同上 91 | - 实例:`select c+1 from (select max(a) as c from t)` --> `select max(a)+1 as c+1 from t` 92 | 93 | 94 | #### 11. CollapseRepartition 针对多次Repartition操作进行合并,Repartition是一种基于exchange的shuffle操作,操作很重,剪枝很有必要。 95 | 96 | 如果连续进行两次Repartition,是可以对他们操作进行合并的,而且以外层的`numPartitions`和`shuffle`参数为主。 97 | - 实例:`Repartition(numPartitions, shuffle, Repartition(_, _, child))`-->`Repartition(numPartitions, shuffle, child)` 98 | 99 | > 注意:Repartition操作只针对在DataFrame's上调用`coalesce` or `repartition`函数,是无法通过SQL来构造含有Repartition的Plan。 100 | > SQL中类似的为`RepartitionByExpression`,但是它不适合这个规则 101 | > 比如:`select * from (select * from t distribute by a) distribute by a`会产生两次RepartitionByExpression操作。 102 | > == Optimized Logical Plan == 103 | > RepartitionByExpression [a#391] 104 | > +- RepartitionByExpression [a#391] 105 | > +- MetastoreRelation default, t 106 | 107 | #### 12. CombineLimits:Limit操作合并。针对GlobalLimit,LocalLimit,Limit三个,如果连续多次,会选择最小的一次limit来进行合并。 108 | 109 | - 实例:`select * from (select * from t limit 10) limit 5` --> `select * from t limit 5` 110 | - 实例:`select * from (select * from t limit 5) limit 10` --> `select * from t limit 5` 111 | 112 | #### 13. CombineFilters:Filter操作合并。针对连续多次Filter进行语义合并,即AND合并。 113 | 114 | - 实例:`select a from (select a from t where a > 10) where a>20` --> `select a from t where a > 10 and a>20` 115 | - 实例:`select a as c from (select a from t where a > 10)` --> `select a as c from t where a > 10` 116 | 117 | #### 14. CombineTypedFilters:对TypedFilter进行合并,与CombineFilters功能一致,只是它是针对TypedFilter内部的函数进行合并,而`CombineFilters`是针对表达式进行合并。 118 | 119 | 即对两个TypedFilter的Func进行And组合:`combineFilterFunction(t2.func, t1.func)` 120 | 121 | #### 15. PruneFilters 对Filter表达式进行剪枝 ,前面的`CombineFilters`和`CombineTypedFilters`都是Filter操作进行合并,这里是针对Filter表达式进行合并剪枝操作。 122 | 123 | 1. 如果Filter逻辑判断整体结果为True,那么是可以删除这个Filter表达式 124 | - 实例:`select * from t where true or a>10` --> `select * from t` 125 | 126 | 2. 如果Filter逻辑判断整体结果为False或者NULL,可以把整个plan返回data设置为Seq.empty,Scheme保持不变。 127 | - 实例:`select a from t where false` --> `LocalRelation , [a#655]` 128 | 129 | 3. 对于f @ Filter(fc, p: LogicalPlan),如果fc中判断条件在Child Plan的约束下,肯定为Ture,那么就可以移除这个Filter判断,即Filter表达式与父表达式重叠。 130 | - 实例:`select b from (select b from t where a/b>10 and b=2) where b=2` --> `select b from (select b from t where a/b>10 and b=2) ` 131 | 132 | #### 16. SimplifyConditionals 简化IF/Case语句逻辑。原理基本上和PruneFilters,BooleanSimplification一样,即删除无用的Case/IF语句 133 | 134 | 1. 对于If(predicate, trueValue, falseValue),如果predicate为常量Ture/False/Null,是可以直接删除掉IF语句。不过SQL显式是没有IF这个函数的,但是Catalyst中有很多逻辑是会生成这个IF表达式。 135 | - case If(TrueLiteral, trueValue, _) => trueValue 136 | - case If(FalseLiteral, _, falseValue) => falseValue 137 | - case If(Literal(null, _), _, falseValue) => falseValue 138 | 139 | 2. 对于CaseWhen(branches, _),如果branches数组中第一个元素就为True,那么实际不需要进行后续case比较,直接选择第一个case的对应的结果就可以 140 | - 实例:`select a, (case when true then "1" when false then "2" else "3" end) as c from t` --> 141 | `select a, "1" as c from t` 142 | 143 | 3. 对于CaseWhen(branches, _),如果中间有when的值为False或者NULL常量,是可以直接删除掉这个表达式的。 144 | - 实例:`select a, (case when b=2 then "1" when false then "2" else "3" end) as c from t` --> 145 | `select a, (case when b=2 then "1" else "3" end) as c from t`。//`when false then "2"`会被直接简化掉。 146 | 147 | #### 17. ReplaceDistinctWithAggregate 用Aggregate来替换Distinct操作,换句话说Distinct操作不会出现在最终的Physical Plan中的 148 | 149 | - Distinct(child) => Aggregate(child.output, child.output, child) 150 | - 实例:`select distinct a,b from t` --> `select a,b from t group by a,b` 151 | 152 | #### 18. ReplaceExceptWithAntiJoin 用AntiJoin操作来替换“except distinct”操作,注意不针对"except all" 153 | 154 | distinct Except(left, right)操作的含义是从left中删除调right中存在的数据,以及自己当中存在重复的操作。因此可以立刻时left和right做了一个AntiJoin,并且join是输出不相等,同时对结果做distinct操作。 155 | - 实例:`select a,b from t where b=10 except DISTINCT select a,b from t` -> `select distinct a,b from t where b=10 anti join (select a,b from t where a=10) t1 where t1.a != t.a and t1.b != t.b` 156 | 157 | #### 19. ReplaceIntersectWithSemiJoin 用LEFT SemiJoin操作来替换“Intersect distinct”操作,注意不针对"Intersect all" 158 | 159 | - 实例: "select a,b from t Intersect distinct select a,b from t where a=10" -> `select distinct a,b from t where b=10 left semi join (select a,b from t where a=10) t1 where t1.a != t.a and t1.b != t.b` 160 | 161 | > 针对上面ReplaceExceptWithAntiJoin和ReplaceIntersectWithSemiJoin,都是只支持”distinct”,那么你可能会问,那么怎么支持"all"?答案是:**spark sql根本就不支持"Intersect all"和"except all"操作,哈哈!!** 162 | 163 | #### 20. LimitPushDown Limit操作下移,可以减小Child操作返回不必要的字段条目 164 | 165 | 1. LocalLimit(exp, Union(children)) 将limit操作下移到每个 Union上面; 166 | - 实例:`(select a from t where a>10 union all select b from t where b>20) limit 30` --> `(select a from t where a>10 limit 30 union all select b from t where b>20 limit 30) limit 30` 167 | 168 | > //注意:该规则中的Union操作为`UNION ALL`,不适用于`UNION DISTINCT` 169 | 170 | 2. LocalLimit(exp, join @ Join(left, right, joinType, _)) 根据Join操作的类型,将limit操作移下移到left或者right。 171 | 172 | #### 30. PushDownPredicate 对于Filter操作,原则上它处于越底层越好,他可以显著减小后面计算的数据量。 173 | 174 | 1. filter @ Filter(condition, project @ Project(fields, grandChild)) 175 | - 实例:`select rand(),a from (select * from t) where a>1` --> `select rand(),a from t where a>1` //如果Project包含nondeterministic 176 | - 实例:`select rand(),a,id from (select *,spark_partition_id() as id from t) where a>1;` //是无法进行这个优化。 177 | 178 | 2. filter @ Filter(condition, aggregate: Aggregate) 对于Aggregate,Filter下移作用很明显。但不是所有的filter都可以下移,有些filter需要依赖整个aggregate最终的运行结果。如下所示 179 | - 实例:`select a,d from (select count(a) as d, a from t group by a) where a>1 and d>10` 对于`a>1`和`d>10`两个Filter,显然`a>1`是可以下移一层,从而可以减小group by数据量。 180 | - 而`d>10`显然不能,因此它优化以后的结果为 `select a,d from (select count(a) as d, a from t where a>1 group by a) where d>10` 181 | 182 | 3. filter @ Filter(condition, union: Union)原理一样还有大部分的一元操作,比如Limit,都可以尝试把Filter下移,来进行优化。 183 | - 实例:`select * from (select * from t limit 10) where a>10` 184 | 185 | > 但是如果子表达式输出non-deterministic类型,是不允许进行这项操作。// SPARK-13473: We can't push the predicate down when the underlying projection output non-deterministic field(s). Non-deterministic expressions are essentially stateful. This implies that, for a given input row, the output are determined by the expression's initial state and all the input rows processed before. In another word, the order of input rows matters for non-deterministic expressions, while pushing down predicates changes the order. 186 | 187 | #### 31. PushProjectThroughSample 将Project操作下移到Sample操作,从而精简Sample的输出。是一种剪枝操作 188 | - case Project(projectList, Sample(lb, up, replace, seed, child)) => Sample(lb, up, replace, seed, Project(projectList, child))() 189 | 190 | #### 32. PushPredicateThroughJoin 针对Join操作,调整Filter过滤规则 -------------------------------------------------------------------------------- /spark/spark-experience.md: -------------------------------------------------------------------------------- 1 | # Spark 使用经验 2 | 3 | ## 1. 正确的使用flatmap 4 | 在Spark中,flatMap操作可以根据输入一条记录而生成多条记录,实际应用中看到很多代码如下: 5 | 6 | sc.parallelize(Array(1,2,3),3).flatMap(i=>{val a= makeArray(i);a}) 7 | //makeArray或为MakeSeq/MakeHashMap在执行过程中,会立即完成多条记录的构建,并堆放在执行器的内存中。 8 | 9 | 而实际上flatmap的函数定义为`flatMap(f: T => TraversableOnce)`,要求返回值为`TraversableOnce`。 上面的`Array/Seq/HashMap`都是`TraversableOnce`类型,但是他们缺点是数据都已经内存中。 10 | 11 | 另外一个TraversableOnce的子类为`Iterator`,通过实现`Iterator`的`hasNext`和`next`两个函数,即可以在执行过程中来生成每条数据,执行开销一样,但是会大大的减小执行器的内存的占用。 12 | 13 | sc.parallelize(Array(1,2,3),3).flatMap(i=>{new Iterator[Int] { 14 | var finished = false 15 | override def hasNext: Boolean = !finished 16 | override def next(): Int={ 17 | val newItem = buildOneItem(i) 18 | if() finished=true 19 | newItem 20 | } 21 | }) 22 | //buildOneItem每次只会生成一条记录并放在内存中 23 | 24 | ## 2. reduceByKey一定比groupByKey好吗 25 | 在很多Spark程序性能优化文章中都优先推荐使用aggregateByKey 和reduceByKey,原因就是它提供了map-side的aggregator的功能,可以在Map端对输出的数据进行aggregator,减小shuffle write和fetch的数据量;然后这个优化的前提条件是它真的可以减少数据量,否则使用reduceByKey反而适得其反,导致shuffle-sort-map过程性能底下。原因是因为多了一个map-side的aggregator计算吗?不是,主要原因是map-side的aggregator不支持unsafeSortedShuffle,而只能选择普通SortedShuffle,在数据量较大,频繁的spill会导致sort性能低下(测试结果相关4倍)。 26 | 27 | //测试:Key为随机生成的数字,map-side的aggregator效果一般 28 | val p = sc.parallelize(0 to 10, 10) 29 | val p1=p.flatMap(a=>{val rnd=new scala.util.Random;(0 to 5000000).map(i=>(rnd.nextLong,a))}) 30 | p1.reduceByKey(_+_,5).saveAsTextFile("/bigfile/test/p1") 31 | //ShuffleWrite=599M,耗时:57s,ShuffleFetchAndWrite=599M,耗时:43s 32 | //5000000->5000000*2 33 | //2G内存会有Executor频繁进行fullGC,只能勉强提高Executor内存大小到3G。 34 | //ShuffleWrite=1197.9M,耗时:2.2m,ShuffleFetchAndWrite=1197.9M,耗时:1.8 min 35 | 36 | val p3=p.flatMap(a=>{val rnd=new scala.util.Random;(0 to 5000000).map(i=>(rnd.nextLong,a))}) 37 | p3.groupByKey(5).map(k=>k._2.reduce(_+_)).saveAsTextFile("/bigfile/test/p1") 38 | //ShuffleWrite=599M,耗时:14s,ShuffleFetchAndWrite=599M,耗时:48s 39 | //5000000->5000000*2,2G->3G 40 | //ShuffleWrite=1177.6M,耗时:28s,ShuffleFetchAndWrite=1177.6M,耗时:1.8 min 41 | 42 | > 结论:对于map-side的aggregator效果一般,map端输出数据大小基本一致的Case下,reduceByKey的性能要比groupByKey差了4倍左右; 43 | 44 | //测试:Key可以减小Value大小10分之一,map-side的aggregator效果明显 45 | val p = sc.parallelize(0 to 10, 10) 46 | val p1=p.flatMap(a=>{val rnd=new scala.util.Random;(0 to 5000000).map(i=>(rnd.nextInt(5000000/10),i))}) 47 | p1.reduceByKey(_+_,5).saveAsTextFile("/bigfile/test/p1") 48 | //ShuffleWrite=57.3M,耗时:8s,ShuffleFetchAndWrite=57.3M,耗时:5s 49 | 50 | val p3=p.flatMap(a=>{val rnd=new scala.util.Random;(0 to 5000000).map(i=>(rnd.nextLong/10,a))}) 51 | p3.groupByKey(5).map(k=>k._2.reduce(_+_)).saveAsTextFile("/bigfile/test/p1") 52 | //ShuffleWrite=599M,耗时:14s,ShuffleFetchAndWrite=599M,耗时:44s 53 | 54 | > 结论:map-side的aggregator效果明显,reduceByKey还是很有优势的,因此在实际业务环境下需要根据数据的特点来进行选择。 55 | 56 | ## 3. 优先选择Spark SQL的Table Cache,而不使用RDD的cache功能 57 | 58 | 在迭代的计算过程中,经常需要把中间结果cache到内存中,目前Spark SQL和Core都提供了cache机制,但是Spark SQL使用了`columnar`技术,即内存列存储,可以显著的减小cache对内存占用. 59 | 60 | 测试: 61 | du -sh 62 | 284K /Users/parquet 63 | spark.read.parquet("/Users/parquet").cache ==> 单副本,占用内存大小:1997.2 KB 64 | spark.read.parquet("/Users/parquet").rdd.cache ==> 单副本,占用内存大小:14.2 MB 65 | 相差7倍!而且如果原始数据越大,这个差量比例应该会更大! 66 | 67 | 所以如果实在要使用cache数据,优先将数据转换为dataset,再进行cache. 68 | 69 | 70 | ## 4. 针对Parquet关闭_metadata和_common_meta_data 71 | 72 | 在Spark中写parquet都会针对一个parquet目录增加两个_metadata和_common_meta_data文件,存储了整个目录下所有parquet文件的scheme聚合. 73 | 它们是在parquet写commit过程中完成的,该聚合操作相当耗时,所以在2.0版本中默认进行关闭.参考[SPARK-15719] 74 | 75 | 另外在spark 2.0(parquet 1.7)之前,如果写一个空的parquet文件到空的parquet目录,此时执行scheme聚合会有bug. 76 | 77 | 测试: 78 | rm -rf /Users/parquet/* 79 | assert(dataset.count == 0) 80 | dataset.write.parquet("/User/Parquet") 81 | 报错: 82 | java.lang.NullPointerException 83 | at org.apache.parquet.hadoop.ParquetFileWriter.mergeFooters(ParquetFileWriter.java:456) 84 | at org.apache.parquet.hadoop.ParquetFileWriter.writeMetadataFile(ParquetFileWriter.java:420) 85 | at org.apache.parquet.hadoop.ParquetOutputCommitter.writeMetaDataFile(ParquetOutputCommitter.java:58) 86 | // 87 | List