Repository: shijinkui/spark_study
Branch: master
Commit: c32e63c2221b
Files: 17
Total size: 60.3 KB
Directory structure:
gitextract_dulrsnnq/
├── .gitignore
├── LICENSE
├── README.md
├── overview.markdown
├── spark_core_getstart_from_pi.markdown
├── spark_eight_style.markdown
├── spark_eight_style_1_rdd.markdown
├── spark_eight_style_2_dag_lazy.markdown
├── spark_graphx_analyze.markdown
├── spark_repl.markdown
├── spark_ui.markdown
├── src/
│ ├── saprk_八法.graffle
│ ├── spark_core.graffle
│ ├── spark_graphx_analyze.graffle
│ ├── spark_repl.graffle
│ └── spark_streaming.graffle
└── 食不厌精,脍不厌细:如何一步步将kcore算法提升5倍性能.markdown
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.DS_Store
*.zip
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# Spark Study
----------------
通过spark源码阅读,把spark内部运行逻辑用`图表+文字说明`的形式展现出来。
图表能够清晰表达全局的逻辑关系和细节部分
## 目录
1. [Graphx:构建graph和聚合消息](spark_graphx_analyze.markdown)
2. [从SparkPi图解Spark core](spark_core_getstart_from_pi.markdown) (hangzhou spark meetup分享过一次)
3. [spark UI源码阅读](spark_ui.markdown) *2015.6.11*
4. [食不厌精,脍不厌细:如何一步步将kcore算法提升5倍性能](spark_core_getstart_from_pi.markdown) *2014.12.27*
5. [图解spark repl](spark_repl.markdown) *2016.1.12*
## Spark八法
@玄畅 @明风
> “井中八法”一共八式,因寇仲天赋行军打仗的超卓领导才能,以兵法入刀,故各式均含用兵之道。
> 总纲:故善战者,立于不败之地,而不失敌之败也。是故胜兵先胜而后求战,败兵先战而后求胜,因敌而制胜。
从八个方面分析spark, 观脉络看细节。
1. [概要](spark_eight_style.markdown)
2. [Spark八法之方圆: RDD](spark_eight_style_1_rdd.markdown)
3. [Spark八法之不攻: DAG、Lazy](spark_eight_style_2_dag_lazy.markdown) 进行中
------------------
微博: @时金魁
================================================
FILE: overview.markdown
================================================
# 学习spark
## 分析spark从两个纬度
1. 框架层面 框架焦点在于处理流程,系统交互,性能等
2. 数据流 数据流关注于数据的转换、合并、shuffle、结果
## 概念
1. Master 中心节点,记录worker、Driver信息
2. Worker 任务执行的节点,取Master发送的Driver,运行任务
3. Client SparkSubmit启动,把用户的application(包含main的类)打包,分装成Driver, 提交给Master
4. Driver Client向Master注册,Master随机取非满负荷的Worker,绑定Worker和此Driver,向此Worker发送任务消息
Cluster Programming Models
distributed storage abstraction
### 备忘:scala 解释器 interactive
To recap previous chapter, RDDs provide the following facilities:
* Immutable storage of arbitrary records across a cluster (in Spark, the records are Java objects).
* Control over data partitioning, using a key for each record.
* Coarse-grained operators that take into account partitioning. • Low latency due to in-memory storage.
As long as each batched record fits in the CPU cache, this leads to fast transformations and conversions.
================================================
FILE: spark_core_getstart_from_pi.markdown
================================================
# 从SparkPi图解Spark core
----------------
@玄畅
2015.1.30
spark core是spark的核心库,执行最基础的RDD操作。通过本文,从万年pi入手,逐层分解spark core整个运行过程,一窥其貌。
文字是辅助阅读的面包屑。
先不管那么多名词,一路看下去,理脉络。
## 万年Pi
这个代码做了以下几件事:
1. 初始化配置`SparkConf`
2. 初始化上下文`SparkContext`
3. 准备数据,从1~N, 分slices段,用ParallelCollectionRDD表示
4. map变换,使用大括号里的函数计算ParallelCollectionRDD中的每一个数据项,用MappedRDD表示
5. reduce,合并数据,计算函数为: `_ + _`
```
object SparkPi {
def main(args: Array[String]) {
val conf = new SparkConf().setMaster("local").setAppName("Spark Pi")
val spark = new SparkContext(conf)
val slices = if (args.length > 0) args(0).toInt else 2
val n = math.min(100000L * slices, Int.MaxValue).toInt // avoid overflow
val count = spark.parallelize(1 until n, slices).map { i =>
val x = random * 2 - 1
val y = random * 2 - 1
if (x*x + y*y < 1) 1 else 0
}.reduce(_ + _)
println("Pi is roughly " + 4.0 * count / n)
spark.stop()
}
}
```
## 启动 & 初始化配置和上下文
1. 入口spark-submit, SparkSubmit会调用`SparkPi`的入口函数`main`() Driver, client, master, worker的启动和交互关系另表, todo)
2. 初始化SparkConf
执行spark-submit时传入的vm参数,spark参数统统由这个对象表示
3. 初始化SparkContext
初始化:UI,statusTracker, progressBar, jars, files, env, heartbeatReceiver, masterUrl, applicationId。。。
关键的是:dagScheduler, taskScheduler, schedulerBackend, blockManager
taskScheduler用于提交`TaskSet`即提交RDD
schedulerBackend用于接受任务,分配任务给worker去执行
从createTaskScheduler函数下去,取得`spark-submit`的master url,根据master的scheme协议类型生成不同的`SchedulerBackend`、`TaskScheduler`, master的类型有:local、local-cluster、simr、spark、mesos、zk、yarn-standalone、yarn-cluster。
相应的种类:LocalBackend, SparkDeploySchedulerBackend, SimrSchedulerBackend, CoarseGrainedSchedulerBackend, CoarseMesosSchedulerBackend, MesosSchedulerBackend

## 提交job
上文,初始化完了sparkContext, 执行到下面的代码:
```
val count = spark.parallelize(1 until n, slices).map { i =>
val x = random * 2 - 1
val y = random * 2 - 1
if (x*x + y*y < 1) 1 else 0
}.reduce(_ + _)
```
1. 步骤分解。这行代码,分为三步:
1. `spark.parallelize`生成`ParallelCollectionRDD`
2. 然后, `ParallelCollectionRDD`执行`map()`函数,生成`MapPartitionsRDD`
3. `MapPartitionsRDD`执行`reduce`函数,生成最终结果
上面可以看出RDD之间是有依赖关系的,这个依赖关系怎么形成的?
2. 依赖关系
`new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF))`,这个`this`指向`ParallelCollectionRDD`, 这俩RDD都继承自`RDD`, `RDD`的一个构造函数`def this(@transient oneParent: RDD[_]) =
this(oneParent.context , List(new OneToOneDependency(oneParent)))`会定义当前RDD的上一个RDD是谁,并用一个`deps: Seq[Dependency[_]]`对象表示所有的依赖对象。
如此,每个RDD都会记录与上一个RDD的关系,是一对一,还是一对多.
3. 计算函数
计算函数是用户定义的`map()`中的代码块。
一个RDD包含多个分区partition,这个计算函数会应用到所有的partition中的所有数据。这就隐含着两个遍历操作:遍历RDD的partition;遍历partiton中的数据
如此,整个RDD经过算子的运算,原始数据变成了想要的数据,即map过程,就像数学中的函数概念, `f(x) = 3x + 100`。但是,这里仅仅是表示这种计算关系,并没有马上计算出来结果。真正执行计算的时机由action函数触发,如下面的`reduce`
4. 合并
上面这些准备数据,演算过程,都是虚的,真正触发计算过程的时机在此。
既然是分布式计算,就需要把数据和算子分布到多台服务器上。
这里首先把map、reduce的用户定义的函数序列化,传输到不同的机器上。而partition实际的分片数据是可以根据`Partition`的信息定位到数据的位置的。
`reduce(_ + _)`函数会把整个计算过程封装成一个`Job`

## DAGScheduler构建final stage
`DAGScheduler`对象在`SparkContext`初始化的时候会实例化,这里开用了。
这里会对job做一些整理变换,以便符合适合下一阶段要求。在job中rdd的partition并不包含实际数据,只是partition的序号。
1. 提交Job
`eventProcessActor ! JobSubmitted` 发射消息
这里开始使用akka, `eventProcessActor`使用`DAGSchedulerEventProcessActor` 来处理收到的消息。
actor`DAGSchedulerActorSupervisor`收到提交的`JobSubmitted`。dag会执行`handleJobSubmitted`来处理提交的job。
2. 处理提交的job,生成finalStage
这里可以看到提交的是finalRDD,表示这是老末了,通过这位老末可以向上遍历找到所有的父RDD及其包含的相应的父partition。
`handleJobSubmitted`就是根据RDD的依赖关系向上遍历父RDD,关系树中的RDD都封装成stage,结果就是finalRDD变成finalStage。
这里遍历依赖关系使用先进后出的栈。先把自己finalRDD送进去,pop出来,找到所有的`dependencies`, 遍历依赖, 把`ShuffleDependency`类型的依赖封装成一个`Stage`对象添加到返回的列表中,非`ShuffleDependency`继续取出`dependencies`入栈。
每个`Stage`对象中都包含当前job的jobId。
这样,父依赖RDD和当前的finalRDD都转换成了`Stage`对象,即:finalStage包含了父stages

## DAGScheduler提交stage(submitStage)
上一步对finalRDD进行了大清洗,把RDD及其父RDD洗白成`Stage`.
1. 检查block miss的stage
检查每个stage对应的存储块是否存在, 即:每个rdd的partition id封装成一个`BlockId`, 向master的`BlockManagerMasterActor`发消息(`GetLocationsMultipleBlockIds`),收到消息后,根据本地的`blockLocations`取出`BlockManagerId`, 如果没有则返回空。
这样确保所有的stage对应的block都是存在的,然后把stage存入`waitingStages`
不得不说说`BlockManagerId`,这货有`executorId, host, port`,即它已经许身给了某个worker,有身份和地址的对象。
2. 正式提交stage
遍历`waitingStages`的拷贝,`submitStage`提交每一个stage, 这样没有miss的stage,直接进入下一步:`submitMissingTasks`
提交时,把`Stage`对象中每个分区和rdd序列化,把stage.rdd封装成`Broadcast`对象,默认为`TorrentBroadcast`。stage(RDD)的partition封装成task, 有两种类型的task:ResultTask、ShuffleMapTask。而`TaskSet`是tasks的持有者,`taskScheduler.submitTasks(...)`实际执行提交任务。
`taskScheduler`是在sparkContext初始化时生成的任务调度器。
## 任务调度器提交任务
入口:`TaskSchedulerImpl#submitTasks`
书接上文,DAG把finalRDD最后封装成`Task`对象,调用sparkContext里的`taskScheduler`。
`taskScheduler`中,加一个`maxTaskFailures`最大任务失败数,把TaskSet封装成`TaskSetManager`对象。提交到`Pool`中。这里的`Pool`即是`SparkContext`初始化时构建TaskSchedulerImpl时初始化的池化对象,把任务提交到`ConcurrentLinkedQueue`。
spark任务躺在linkedqueue中,`backend.reviveOffers()`触发下一步任务的执行。backend就是在初始化时根据不同的master类型确定的不同backend类型。
*模块之间通过队列解耦,数据在不同的模块中由不同的对象来封装和表达。*
在backend(CoarseGrainedSchedulerBackend)的reviveOffers中,`Driver`发送消息`ReviveOffers`。通过发消息的方式,任务的执行就不是顺序执行了,而是乱序并行执行。driverActor收到消息,从TaskSchedulerImpl中取出随机shuffle的task,发射task,把任务发送到各个`Executor(worker)`上执行,对象在网络上传播就需要序列化对象。发送消息对象`LaunchTask`到具体的任务执行者`executorActor`。

## Executor任务执行1
`CoarseGrainedExecutorBackend`收到`LaunchTask`消息,首先要做的就是反序列化task描述对象`TaskDescription`。下面的事情就是放到线程池中执行任务。
实例`TaskRunner`对象,放到队列中,线程池执行。
`TaskRunner`运行时,反序列化task为三个对象:taskFiles,taskJars, taskBytes。`taskFiles`,`taskJars`会加载到时机的文件,jar引入到当前类加载器中。
回顾前面application,`spark-submit`提交时会添加jar,各种参数。application执行时需要用户提交的外部依赖jar, 在每个线程中执行任务时就要把这些外部以来文件和依赖jar加载进来。
`taskBytes`反序列化为`Task`对象,运行的时候,首先实例化任务的上下文`context = new TaskContextImpl(stageId, partitionId, attemptId, runningLocally = false)`, context存入`ThreadLocal`。
现在万事具备,只需要执行Task了(application的代码), Task有两种:`ShuffleMapTask`、`ResultTask`,默认为`ResultTask`

## 执行ResultTask(默认)
反序列化`taskBinary: Broadcast[Array[Byte]]`, 还原出rdd和计算函数。`Broadcast`是DAG中封装的对象。
计算需要两部分内容:计算函数,数据。
计算函数就是application中map函数体的内容。数据就需要从存储系统中获取。
执行函数和取数据的函数:
`func(context, rdd.iterator(partition, context))`
而在提交job的代码如下:
```
def runJob[T, U: ClassTag](
rdd: RDD[T],
processPartition: Iterator[T] => U,
resultHandler: (Int, U) => Unit)
{
val processFunc = (context: TaskContext, iter: Iterator[T]) => processPartition(iter)
runJob[T, U](rdd, processFunc, 0 until rdd.partitions.size, false, resultHandler)
}
```
processFunc的签名为:`func: (TaskContext, Iterator[T]) => U,`
跟ResutTask中调用时的一样, 这样前后就对上号了。
1. 取数据
取数据从rdd的`iterator`函数进去,传入当前task所对应的partition和`TaskContext`。
数据的标示:不论数据存储在哪个level上,都需要有个唯一的标示来表示实际存储的数据:`RDDBlockId`, blockId是用rdd id和partiton index组合成的字符串。
数据序列化:不论保存在何种level上,都可以指定序列化,序列化以及压缩能够大幅节省内存,但是消耗CPU,空间与时间的平衡。
数据位置氛围本地和远程,本地存储类别为:memoryStore, tachyonStore, diskStore。远程存储类型为:todo
**本地存储:**
memoryStore保存在heap中,`LinkedHashMap`持有。根据blockId取出数据,如果没有反序列化则反序列化成`Iterator[Any]`对象。
tachyonStore保存在tackyon分布式内存存储中,根据blockId取出ByteBuffer数据,反序列化成`Iterator[Any]`对象。
diskStore保存在本地磁盘中,根据blockId,从`DiskBlockManager`中拼装文件名、文件路径,返回File对象,通过`RandomAccessFile`随机访问文件对象读取文件的具体内容,返回`ByteBuffer`, 反序列化成`Iterator[Any]`对象。
通过上述三种本地存储,取出`Iterator[Any]`数据对象,强制类型转换成`BlockResult`。如果取出的数据为空,则表示当前这个partition的数据没有分布在当前的`Executor`上,需要从远程读取数据。
**远程存储:**
本地没有当前task对应的partition数据,需要先问下`Master`这个数据分布在哪里`master.getLocations(blockId)`。master收到消息`GetLocations(blockId)`, `BlockManagerMasterActor`根据blockId在`blockLocations:JHashMap[BlockId, mutable.HashSet[BlockManagerId]]`中get出数据块表示对象`Seq[BlockManagerId]`
从master咨询得到partiton的数据块表示`Seq[BlockManagerId]`, 随机混排一下,再把数据块远程下载下来`blockTransferService.fetchBlockSync(
loc.host, loc.port, loc.executorId, blockId.toString).nioByteBuffer()`
在fetchBlocks抓取数据块时有两种方式:`NettyBlockTransferService`和`NioBlockTransferService`。netty和spark写的nio。在`SparkEnv#create`中,用户传入的spark参数`spark.shuffle.blockTransferService`(默认**netty**)实例化数据块传输对象`NettyBlockTransferService`或`NioBlockTransferService`。
以`NioBlockTransferService#fetchBlocks`为例,`SparkEnv`在初始化的时候确定了使用哪个传输服务(默认`NettyBlockTransferService`), `NettyBlockTransferService`对象在初始化的时候会启动Netty Server来监听网络端口,发送和接收数据。SparkEnv封装`NettyBlockTransferService`到对象`BlockManager`在sparkContext初始化的时候调用blockManager初始化方法`env.blockManager.initialize(applicationId)`。如此:数据块传输的netty server就启动了。
数据块的传输处理handler为`TransportChannelHandler`, 处理过程为netty的pipeline。handler包含三个成员,responseHandler处理响应,requestHandler和client发送请求。
responseHandler通过pipeline的解码,得到ResponseMessage,message有四种类型:ChunkFetchSuccess、ChunkFetchFailure、RpcResponse、RpcFailure,收到数据后,会直接从channel中取到byteBuffer,从`outstandingFetches`取出`ChunkReceivedCallback`,调用onSuccess回调函数,回调函数再调用`BlockFetchingListener`监听器,最终返回partition的完整数据块。
小结:取partition数据,如果数据保存在本地,就从cache, tachyon, disk中读取;如果保存在远程,则通过netty或者NIO读取。最终返回的都是ByteBuffer, 反序列化成响应的对象。
**重新计算**
如果storageLevel不为空,但是存储系统中都没有数据,那么就需要计算出所需的数据。
这时分三种情况:1. cache正在被加载,等待加载后直接返回;2. 从checkpoint读;2. 重新计算。
不同的RDD类型有不同的compute实现。ParallelCollectionPartition是直接返回起iterator数据。这个就是最原始的数据了。
2. 应用计算函数
现在数据有了,计算函数也有了,直接调用计算函数,传入数据,遍历partition,应用函数计算每一项数据。返回计算后的数据

## 执行ShuffleMapTask
todo ...
## 附

----------------------------
================================================
FILE: spark_eight_style.markdown
================================================
# spark八法
### 井中八法
------
“井中八法”一共八式,因寇仲天赋行军打仗的超卓领导才能,以兵法入刀,故各式均含用兵之道。
总纲:故善战者,立于不败之地,而不失敌之败也。是故胜兵先胜而后求战,败兵先战而后求胜,因敌而制胜。
1. 不攻: 故用兵之法,无恃其不来,恃吾有以待也;无恃其不攻,恃吾有所不可攻也。
2. 击奇: 善出奇者,无穷如天地,不竭如江河,营而离之,并而击之。
3. 战定: 非必取不出众,非全胜不交兵,缘是万举万当,一战而定。
4. 用谋: 用兵之法,以谋为本,是以欲谋疏阵,先谋地利;欲谋胜敌,先谋固己。
5. 速战: 疾则存, 不疾则亡。
6. 棋弈: (人生,战场如棋盘)未谋其子,先谋其势; 宁失一子,勿失一先。 狮子扑兔,君临天下; 遇强即屈,败中寻胜。
7. 方圆: 方为阳,圆为阴;阴为方,阳为圆。阴阳应象,天人合一,再不可分。
8. 兵诈: 兵者,诡道也。兵以诈立,虚则实之,实则虚之。
### 目录
------
1. 方圆 RDD
2. 不攻 DAG & Lazy
3. 击奇 Shuffle & Actor
4. 战定 Cache & Checkpoint
5. 兵诈 Spark SQL
6. 速战 Streaming
7. 用谋 MLLib
8. 棋弈 Graphx
### 组织结构
------
每个章节尽量按照:
* 综述overview
* 特点feature
* 结构structure
* 细节detail
* 关系relation
* 总结summary
================================================
FILE: spark_eight_style_1_rdd.markdown
================================================
# spark八法之方圆:RDD
> 方圆: 数据为阳,算子为阴;算子为方,数据为圆。阴阳应象,天人合一,再不可分。
>
> Spark的RDD就像方圆和阴阳的概念一样贯穿于整个Spark框架,是Spark最基础和最核心的概念。
## Overview
单机上对数据的转换操作,绝大部分变成语言都能完成。随着数据量变大,单机放不下数据并且单机计算能力有限,这就需要做把单机上干的事情平行分布到对等节点上去做,这就是分布式计算的概念。现在分布式计算框架很多,常见的有:Spark, Hadoop(MR), Pregel, Storm, Dryad, Scope, h2o等等。
近两年比较突出的是Spark, 其基于RDD概念的设计让人耳目一新。下面就看下RDD的设计和实现。
RDD(Resilient Distributed Datasets)弹性分布式数据集, spark的核心理念, 对数据及其操作抽象表达。RDD定义在一个数据集上应用一个函数,整个过程分布式并行执行。
分布式计算首先是需要分布式数据, 在分布式数据上应用计算函数。弹性的数据在中间数据丢失时不唯一依赖数据冗余备份,主要根据原始数据重新计算出所在阶段的数据。原始数据可以通过HDFS来保证不丢失。
### RDD的特点
1. **分区partitions**
2. **RDD之间的依赖dependencies**
3. **计算函数(算子): 应用在所有分区上**
4. **可选的分区器Partitioner**
5. **可选的优先位置preferred locations**
这些RDD特性作为最基础的特性被spark框架实现分布式计算,RDD有不同的子类实现不同的计算过程,如: HadoopRDD, JdbcRDD, BlockRDD, EdgeRDD等。
我们要做大数据计算,主要关心3件事情:1. 数据; 2. 函数; 3. 实现2
数据怎么加载, 计算结果如何保存; 函数就是如同在单机上对数据做的操作。具体实现就是把1、2定义好, 提交给spark去干活。
### RDD的抽象概念
一个RDD定义了对数据的一个操作过程, 用户提交的计算任务可以由多个RDD构成。多个RDD可以是对单个/多个数据的多个操作过程。多个RDD之间的关系使用依赖来表达。操作过程就是用户自定义的函数。
整个数据处理过程需要用户先用RDD和函数画一个计划草图,spark框架拿到这个草图去分布式执行延迟计算过程,然后把结果呈现给用户。而在画草图时,并没有即时计算。
### Spark运行期执行入口
* 调用包含*runJob()*的函数, 开始执行任务。
* 运行期执行过程框架主要由SparkContext, DAGScheduler, TaskScheduler, CoarseGrainedSchedulerBackend, CoarseGrainedExecutorBackend, Executor提供框架支持
图1: RDD概要图

上图简要描述RDD的生命周期, RDD由partition构成, partition通过存储系统拿到实际的数据, 应用定义的一组具有上下依赖关系的算子(RDD), 算子们运算完每个partition数据后得到transform结果, 再去执行聚合函数, 得到最终结果。
RDD只是定义了整个过程, Spark运行时框架(dag, schedule, executor等)保证任务分布式、弹性、并行执行。
RDD(弹性分布式数据集)去掉形容词,主体为:数据集。如果认为RDD就是数据集,那就有点理解错了。个人认为:RDD是定义对partition数据项转变的高阶函数,应用到输入源数据,输出转变后的数据,即:**RDD是一个数据集到另外一个数据集的映射,而不是数据本身。** 这个概念类似数学里的函数`f(x) = ax^2 + bx + c`。这个映射函数可以被序列化,所要被处理的数据被分区后分布在不同的机器上,应用一下这个映射函数,得出结果,聚合结果。
下面就细说RDD具体规格。
## **partition: 分区**
RDD, 名为弹性数据集, 大数据焦点问题是一个大字, 数据太大那就切分为一个个partition分片, 这些partition分布在不同机器上。
怎么切分是`Partitioner`定义的, `Partitioner`有两个接口: `numPartitions`分区数, `getPartition(key: Any): Int`根据传入的参数确定分区号。实现了Partitioner的有:
1. HashPartitioner
2. RangePartitioner
3. GridPartitioner
4. PythonPartitioner
一个RDD有了Partitioner, 就可以对当前RDD持有的数据进行划分, 通过`def getPartitions: Array[Partition]`获取所有的partition。在具体计算的partition的时候就可以通过数组下表确定partition。
根据dependency依赖关系, 可以拿到上一级RDD的partition数据, 如果上一级的RDD数据没有缓存没持久化, 那就根据RDD定义的算子函数计算出partition。以此类推, 一直追溯到没有依赖的root RDD(HadoopRDD), 这个没有依赖的RDD就是原始输入的数据源。拿到这个数据源, 再依次展开执行刚才的追溯。
图3: 在依赖链中计算出partition实际数据

用户的写的app代码中, 算子顺序调用, 最后一个算子的最后面的RDD, 持有这个RDD就可以向上追溯到源数据, 回来再一步步执行RDD的transform partition得到各个Partition的数据, 最终得到末尾的RDD数据。
## **Dependency: 依赖**
图4: RDD的依赖关系

`Dependency`定义的抽象函数为: `def rdd: RDD[T]`, 表示这个依赖对应的父RDD对象。
依赖分为:
1. NarrowDependency 约束当前RDD的每个分区依赖父RDD的**少量**分区
* OneToOneDependency 一对一关系
* RangeDependency 一对一关系, 父子RDD一定范围内的分区一一对应。
* PruneDependency
2. ShuffleDependency
洗牌。表示父子RDD之间的partition是混洗关系。即: 子RDD的每一个partition的数据由父RDD所有partiton的一部分组成。
NarrowDependency让数据从父RDD到子RDD数据条数减少, 最多保持相等; ShuffleDependency让数据从父RDD到子RDD数据条数保持不变, 顺序shuffle.
依赖用来表示父RDD依赖和当前RDD的关系, 子依赖持有父依赖的引用, 这样就能在当前RDD中根据依赖关系拿到父RDD对应的partition的数据。
图5: RDD调用链和依赖链

依赖关系表示RDD之间的对应关系, 多个RDD之间链式调用算子, 最后的关系图为由内到外随着算子调用增多, 层级逐步向外扩展。即: 最外层的RDD能根据与父RDD的依赖关系逐层递进一直找到原点RDD(没有父RDD)。这样, 中间某个RDD的数据分区丢失数据, 就可以根据依赖关系溯源向上重新计算出数据。这就是所谓的弹性。
## 算子: 计算函数
> 狭义的算子是: 如同普通的运算符号作用于数后可以得到新的数那样,一个算子作用于一个函数后可以根据一定的规则生成一个新的函数。(来源于[百科](http://baike.baidu.com/view/53313.htm))
我个人的理解, 所谓算子就是单个输入单个输出的函数。而RDD中定义常用函数如map, filter等, 函数内部会产生不同的RDD, 就是transform的意思, 按照产生新的RDD个数和性质不同, 我分为了三类: 单算子、复合算子、action算子:
1. **单算子**
顾名思义, 只会产生一个新的RDD, 即只产生一次数据transform
名称 | RDD | 描述
------------ | ------------ | ------------
coalesce() | CoalescedRDD | 合并分区, 也可以做shuffle合并
union(other: RDD)| PartitionerAwareUnionRDD或UnionRDD | 合并两个RDD
cartesian(other: RDD)|CartesianRDD|计算两个RDD的笛卡尔积
pipe| PipedRDD|把partition中的数据作为输入, 执行管道命令
mapPartitions | MapPartitionsRDD | 遍历RDD的所有partition, 应用一个自定义函数到partition所有元素
mapPartitionsWithIndex | MapPartitionsRDD | 遍历RDD的所有partition, 应用一个带有*原partition下标*的自定义函数到partition所有元素
zip|ZippedPartitionsRDD2|把两个相同partition数目, 相同partition中元素数的RDD对应成一个新的RDD, 元素为两个RDD对应tuple。拉链
zipPartitions|zip多个RDD, 应用函数到zip的partition, 要求RDD的partition数相同, partition元素数可以不同
zipWithIndex|ZippedWithIndexRDD|生成新的元素中包含所在partition索引位的RDD
glom|MapPartitionsRDD|把所有Partition中的数据合并到一个数组中
zipWithUniqueId|MapPartitionsRDD|
2. **复合算子**
会产生多个新的RDD, 即产生多次有次序的数据transform
名称 | RDD | 描述
------------ | ------------- | ------------
groupBy|MapPartitionsRDD -> RDD | 按照key分组, 得到的是key -> Iterator[T]的映射
sortBy | MapPartitionsRDD -> ShuffledRDD | 先对key进行变换, 再按照key排序
intersection() | MapPartitionsRDD -> CoGroupedRDD -> MapPartitionsRDD -> MapPartitionsRDD | 两个RDD交集
subtract|MapPartitionsRDD|SubtractedRDD|一个RDD减去另外一个RDD
countByValue|MapPartitionsRDD -> MapPartitionsRDD -> MapPartitionsRDD/ShuffledRDD|计算
3. Action执行函数
当前RDD为计算的末端, 进入提交RDD计算任务的入口。上面的所有内容都是吹吹牛不算数的, 调用了`runJob`就表示:我要开始兑现刚才吹的牛。
1. runJob() 提交job任务
` def runJob[T, U: ClassTag](rdd: RDD[T], func: Iterator[T] => U): Array[U] = {...}`
2. runApproximateJob() 提交返回近似值的job任务
`def runApproximateJob[T, U, R](
rdd: RDD[T],
func: (TaskContext, Iterator[T]) => U,
evaluator: ApproximateEvaluator[U, R],
timeout: Long): PartialResult[R] = {`
算子定义了对数据做变换的过程, 并未立即执行。执行的入口是`runJob`或`runApproximateJob`, 这两个多态函数有一个共同特点, 就是: **都包含参数`TaskContext, Iterator[T]`**,这个迭代器代表partition的数据, 真正执行的地方是Executor的线程池里获取一个线程, 执行Task, 在计算时需要TaskContext辅助。*后面详解*
用到这两个入口函数的, 就是计算入口, 如下列表:
名称 | 应用的函数 | 描述
------------ | ------------- | ------------
foreach|f: T => Unit|遍历所有元素, 应用函数
foreachPartition| f: Iterator[T] => Unit|应用一个函数到所有的partition对象(不是partition中所有元素)
collect|-|收集RDD中所有的元素
reduce|f: (T, T) => T|对应于scala的reduceLeft函数, 应用f函数到partition, 再应用f到每个partition的结果
fold|fold(zeroValue: T)(op: (T, T) => T): T|对应于scala的fold函数. 折叠
aggregate|(zeroValue: U)(seqOp: (U, T) => U, combOp: (U, U) => U)|对应于scala的aggregate函数, 聚合partition使用一个函数, 聚合partition的结果使用另外一个函数
treeAggregate|-|-
count|-|计算所有元素总数
countApprox|timeout: Long|计算时允许超时, 得到一个可以忍受的近似结果
countByValueApprox|timeout:Long|类似, 允许超时, 所以会得到近似结果
countApproxDistinct|-|得到相同元素的近似数, 使用HyperLogLog算法
take|num:Int|对应于scala的take函数, 选择第num个值
每个变换过程有有两个计算函数: 1. 应用到每个partition的function, 得到result; 2. 应用到每个partition的result上的function。
这两个函数都是数据映射的定义,真正开始执行是右包含`runJob()`函数的action算子启动。
## lazy: 惰性计算
上面定义好了计算函数,问题就来了,一般理解我写好了应用在数据上的函数,这个函数不是应该马上执行么?
大致来说计算过程分为:即时计算和惰性计算。
即时计算就是马上计算,就像 `1 + 1`这个`+`运算,系统遇到这个`+`会马上对符号左右的数进行求值运算。
而惰性计算就是,等一下,等我把整个任务分布到各个机器,启动任务,加载完这个partition对应的数据,你再计算。
这就是spark巧妙的地方。利用scala的惰性计算实现分布式计算。
以比较简单ResultTask为例子:
```
override def runTask(context: TaskContext): U = {
// Deserialize the RDD and the func using the broadcast variables.
val ser = SparkEnv.get.closureSerializer.newInstance()
val (rdd, func) = ser.deserialize[(RDD[T], (TaskContext, Iterator[T]) => U)](
ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)
metrics = Some(context.taskMetrics)
func(context, rdd.iterator(partition, context))
}
```
`func`就是算子里定义的映射函数, 序列化后经过DAG, TaskManager, Scheduler等Spark框架层层封装到在Task线程中执行实际的任务, 反序列化函数, 应用到partition数据集上。计算出结果。
在前面算子中, 函数的参数中一般都有个`Iterator[T]`形参, 就是表示partition数据。
`=>`这种形式的高阶函数作为参数,在函数被实际调用的时候才去求值, 这就是所谓的惰性计算。
高阶函数见: [scala Higher-order Function](http://docs.scala-lang.org/tutorials/tour/higher-order-functions.html)
## Spark运行期框架支持入口
一个RDD被运算离不开框架的支持。SparkContext会初始化运行时各个组件,封装和提交RDD计算任务, 相关函数为:
函数名字 | 描述
----------- | -----------
sc.clean() | 清理算子中的闭包函数中的未引用到的变量及其他, 以便持久化
sc.persistRDD | 注册持久化事件: 持久化当前RDD到存储系统中(内存或磁盘)
sc.unpersistRDD | 注册反持久化事件: 销毁当前RDD在存储系统的持久化副本
new RDD(rc, ...) | 构建新的RDD对象所需的参数
sc.runJob() | **开始提交运算任务, 入口**
sc.runApproximateJob() | **开始提交返回近似结果的任务, 入口**
`sc.runJob`和`sc.runApproximateJob`是提交运算任务的入口。往下就是DAG, Scheduler, Executor的菜了。
如下图所示:RDD及其依赖RDD被层层包装加工、分发、调度执行, partiton对应的实体数据应用到算子里制定的计算函数; 然后再原路返回, 这样RDD中每个partition对应一个计算结果, reduce这些结果为终极结果, application中直接得到这个最终运算结果。
图2: Spark运行时框架与RDD关系图

## 总结
**RDD几个特点**:
1. 数据: partition
2. RDD之间的关系: Dependency
3. 惰性计算
4. 算子: 确定当前RDD分区和子RDD分区的映射关系, transform
5. 执行: 通过`runJob()`入口进入spark分布式框架流程, 进行任务的调度、执行、容灾
**一句话总结**:
RDD是一个抽象的概念, 定义了数据transform映射关系, 数据链上下游关系, 组合了常用的transform形式, 提供了执行运算的入口。
----------------EOF---------------
================================================
FILE: spark_eight_style_2_dag_lazy.markdown
================================================
# Spark八法之不攻: DAG、Lazy
> 不攻: 故用兵之法, 无恃其不来, 恃吾有以待也; 无恃其不攻, 恃吾有所不可攻也。
>
> 以DAG和Lazy来应对数据计算的任务
## 综述overview
> 在图论中,如果一个有向图无法从任意顶点出发经过若干条边回到该点,则这个图是一个有向无环图(DAG图)。
> 因为有向图中一个点经过两种路线到达另一个点未必形成环,因此有向无环图未必能转化成树,但任何有向树均为有向无环图。
> --[维基百科](http://zh.wikipedia.org/wiki/%E6%9C%89%E5%90%91%E6%97%A0%E7%8E%AF%E5%9B%BE)
#### 几个概念
* Job: 用户提交的spark任务, 一个任务分为多个Stage
* Stage: 阶段, 有两种Stage: ResultStage、ShuffleMapStage
* DAG: Directed Acyclic Graph, 有向无环图。DAG用来表达整个Job。
#### Stage、Patition、Task
Spark中各个子系统之间边界清晰, 子系统内对相似的概念有不同的命名。
一个大块数据, 被分割成n块, 单个分块的数据在RDD中称为`Partition`, 在DAG中表达对分块的操作过程称为`Stage`, 在Executor中实际执行对分块数据的操作过程称为`Task`
[图1: DAG的概念图]

[图1: Spark DAG的概念图]

## 特点feature
## 结构structure
## 细节detail
## 关系relation
## 总结summary
================================================
FILE: spark_graphx_analyze.markdown
================================================
# Graphx:构建graph和聚合消息
@玄畅
2014.12.29
## About
最近在优化kcore算法时,对Graphx代码看了几遍。1.2后Graphx性能有所提升,代码不太容易理解,现在用图表示出来会更直观。
对数学上的图有点印象的是x轴、y轴坐标图,坐标中每个点用横坐标x和纵坐标y表示,即: (x1, y1), (x2, y2), 一个坐标点可以确定一个点的唯一位置
Graphx与上面的概念类似。不同的是, Graphx中的点概念更泛化,不一定用x y坐标表示,有唯一标示的对象即可,如:ID
## 1. 构建Graph
graphx的`Graph`对象是用户操作图的入口, 它包含了边(edge)和顶点(vertices)两部分. 边由点组成,所以, 所有的边中包含的点的就是点的全集。但是这个全集包含重复的点, 去重复后就是VertexRDD
点和边都包含一个attr属性,可以携带额外信息
### 1.1 构建一个图的方法
1. 根据边构建图(Graph.fromEdges)
```
def fromEdges[VD: ClassTag, ED: ClassTag](
edges: RDD[Edge[ED]],
defaultValue: VD,
edgeStorageLevel: StorageLevel = StorageLevel.MEMORY_ONLY,
vertexStorageLevel: StorageLevel = StorageLevel.MEMORY_ONLY): Graph[VD, ED]
```
2. 根据边的两个点元数据构建(Graph.fromEdgeTuples)
```
def fromEdgeTuples[VD: ClassTag](
rawEdges: RDD[(VertexId, VertexId)],
defaultValue: VD,
uniqueEdges: Option[PartitionStrategy] = None,
edgeStorageLevel: StorageLevel = StorageLevel.MEMORY_ONLY,
vertexStorageLevel: StorageLevel = StorageLevel.MEMORY_ONLY): Graph[VD, Int] =
```
### 1.2 第一步:构建边EdgeRDD
(点击查看大图)

#### 1.2.1 加载边的文本信息
从持久化系统(HDFS)中加载边的文本信息,按行处理生成tuple, 即`(srcId, dstId)`
api:
```
val rawEdgesRdd: RDD[(Long, Long)] = sc.textFile(input, partitionNum).filter(s => s != "0,0").repartition(partitionNum).map {
case line =>
val ss = line.split(",")
val src = ss(0).toLong
val dst = ss(1).toLong
if (src < dst)
(src, dst)
else
(dst, src)
}.distinct()
```
数据形如:
```
107,109
108,109
110,111
110,112
111,112
113,114
115,116
117,79
117,118
79,118
```
#### 1.2.2 第二步:初步生成Graph
1. 入口:`Graph.fromEdgeTuples(rawEdgesRdd)`
元数据为`,`分割的两个点ID,把元数据映射成`Edge(srcId, dstId, attr)`对象, attr默认为null。这样元数据就构建成了`RDD[Edge[ED]]`
2. `RDD[Edge[ED]]`要进一步转化成`EdgeRDDImpl[ED, VD]`
首先遍历`RDD[Edge[ED]]`的分区`partitions`,对分区内的边重排序`new Sorter(Edge.edgeArraySortDataFormat[ED]).sort(edgeArray, 0, edgeArray.length, Edge.lexicographicOrdering)`即:按照srcId从小到大排序。
问:为何要重排序?
答:为了遍历时顺序访问。采用数组而不是Map,数组是连续的内存单元,具有原子性,避免了Map的hash问题,访问速度快
3. 填充localSrcIds,localDstIds, data, index, global2local, local2global, vertexAttrs
数组localSrcIds,localDstIds中保存的是经过`global2local.changeValue(srcId/dstId)`转变的本地索引,即:`localSrcIds`、`localDstIds`数组下标对应于分区元素,数组中保存的索引位可以定位到`local2global`中查到具体的VertexId
`global2local`是spark私有的Map数据结构`GraphXPrimitiveKeyOpenHashMap`, 保存vertextId和本地索引的映射关系。`global2local`中包含当前partition所有srcId、dstId与本地索引的映射关系。
`data`就是当前分区的attr属性数组
`index`索引最有意思,按照srcId重排序的边数据, 会看到相同的srcId对应了不同的`dstId`, 见图中`index desc`部分。`index`中记录的是**相同`srcId`中第一个出现的`srcId`与其下标**。
`local2global`记录的是所有的VertexId信息的数组。形如:`srcId,dstId,srcId,dstId,srcId,dstId,srcId,dstId`。其中会包含相同的ID。即:当前分区所有vertextId的顺序实际值
```
# 用途:
# 根据本地下标取VertexId
localSrcIds/localDstIds -> index -> local2global -> VertexId
# 根据VertexId取本地下标,取属性
VertexId -> global2local -> index -> data -> attr object
```
*spark的数据最终是在patition中表达,所以各种transform都在这里进行,这里的数据结构性能至关重要*
### 1.3 第二步:构建顶点(VertexRDD)
(点击查看大图)

入口:`GraphImpl`365行。
`val vertices = VertexRDD.fromEdges(edgesCached, edgesCached.partitions.size, defaultVertexAttr).withTargetStorageLevel(vertexStorageLevel)`
根据边`EdgeRDD[ED, VD]`构建出点`VertexRDD`, 点是孤岛,不像边一样保存点与点之间的关系。点只保存属性attr。所以需要对拿到的点做分区。
为了能通过点找到边,每个点需要保存点所在到边信息即分区Id(pid),这些新保存在路由表`RoutingTablePartition`中。
**构建的过程:**
1. 创建路由表
根据`EdgeRDD`,map其分区,对edge partition中的数据转换成`RoutingTableMessage`数据结构。
**特别激动的是:** 为节省内存,把edgePartitionId和一个标志位通过一个32位的`int`表示。
如图所示,`RoutingTableMessage`是自定义的类型类, 一个包含vid和int的tuple`(VertexId, Int)`。 int的32~31位表示一个标志位,`01: isSrcId 10: isDstId`。30~0位表示边分区ID。赞这种做法,目测作者是山西人。
`RoutingTableMessage`想表达这样的信息:一个顶点ID,不管未来你到天涯海角,请带着你女朋友`Edge`的地址: edge分区ID。并且带着一个标志你在女友心中的位置是:`01`是左边`isSrcId`,`10`是右边`isDstId`。这样就算你流浪到非洲,也能找到小女友约会...blabla...
2. 根据路由表生成分区对象`vertexPartitions`
1. 上(1)中生成的消息路由表信息,重新分区,分区数目根edge分区数保持一致。
2. 在新分区中,map分区中的每条数据,从`RoutingTableMessage`解出数据:vid, edge pid, isSrcId/isDstId。这个三个数据项重新封装到三个数据结构中:pid2vid,srcFlags,dstFlags
3. **有意思的地方来了**,想一下,shuffle以后新分区中的点来自于之前edge不同分区,那么一个点要找到边,就需要先确定边的分区号pid, 然后在确定的edge分区中确定是srcId还是dstId, 这样就找到了边。
```
val pid2vid = Array.fill(numEdgePartitions)(new PrimitiveVector[VertexId])
val srcFlags = Array.fill(numEdgePartitions)(new PrimitiveVector[Boolean])
val dstFlags = Array.fill(numEdgePartitions)(new PrimitiveVector[Boolean])
```
上面表达的是:当前vertex分区中点在edge分区中的分布。新分区中保存这样的记录`(vids.trim().array, toBitSet(srcFlags(pid)), toBitSet(dstFlags(pid)))` vid, srcFlag, dstFlag, flag通过`BitSet`存储,很省。
如此就生成了vertex的路由表`routingTables`
4. 生成ShippableVertexPartition
根据上面`routingTables`, 重新封装路由表里的数据结构为:`ShippableVertexPartition`
ShippableVertexPartition会合并相同重复点的属性attr对象,补全缺失的attr对象。
关键是:根据vertexId生成`map:GraphXPrimitiveKeyOpenHashMap`,这个map跟边中的`global2local`是不是很相似?这个map根据long vertxId生成下标索引,目测:相同的点会有相同的下标。// todo..
3. 创建`VertexRDDImpl`对象
`new VertexRDDImpl(vertexPartitions)`,这就完事了
### 1.4 第三步 生成Graph对象[finished]
(点击查看大图)

把上述edgeRDD和vertexRDD拿过来组成Graph
```
new GraphImpl(vertices, new ReplicatedVertexView(edges.asInstanceOf[EdgeRDDImpl[ED, VD]]))
```
## 2. 常用函数分析
下面分析一下常用的graph函数`aggregateMessages`
### 2.1 aggregateMessages
`aggregateMessages`是Graphx最重要的API,1.2版本添加的新函数,用于替换`mapReduceTriplets`。目前`mapReduceTriplets`最终也是使用兼容的`aggregateMessages`
据说改用`aggregateMessages`后,性能提升30%。
它主要功能是向邻边发消息,合并邻边收到的消息,返回messageRDD
aggregateMessages的接口如下:
```
def aggregateMessages[A: ClassTag](
sendMsg: EdgeContext[VD, ED, A] => Unit,
mergeMsg: (A, A) => A,
tripletFields: TripletFields = TripletFields.All)
: VertexRDD[A] = {
aggregateMessagesWithActiveSet(sendMsg, mergeMsg, tripletFields, None)
}
```
- sendMsg: 发消息函数
```
private def sendMsg(ctx: EdgeContext[KCoreVertex, Int, Map[Int, Int]]): Unit = {
ctx.sendToDst(Map(ctx.srcAttr.preKCore -> -1, ctx.srcAttr.curKCore -> 1))
ctx.sendToSrc(Map(ctx.dstAttr.preKCore -> -1, ctx.dstAttr.curKCore -> 1))
}
```
- mergeMsg:合并消息函数。用于Map阶段,每个edge分区中每个点收到的消息合并,以及reduce阶段,合并不同分区的消息。合并vertexId相同的消息
- tripletFields:定义发消息的方向
#### 2.1.1 aggregateMessages Map阶段
(点击查看大图)

从入口函数进入aggregateMessagesWithActiveSet,首先使用`VertexRDD[VD]`更新`replicatedVertexView`, 只更新其中vertexRDD中attr对象。
问:为啥更新replicatedVertexView?
答:replicatedVertexView就是个点和边的视图,点的属性有变化,要更新边中包含的点的attr
`replicatedVertexView`这里对edgeRDD做`mapPartitions`操作,所有的操作都在每个边分区的迭代中完成。
1. 进入aggregateMessagesEdgeScan
前文中提到edge partition中包含的五个重要数据结构之一:`localSrcIds`, 顶点vertixId在当前分区中的索引.
1. 遍历`localSrcIds`, 根据其下标去`localSrcIds`中拿到srcId在全局`local2global`中的索引位,然后拿到srcId; 同理,根据下标,去`localDstIds`中取到`local2global`中的索引位, 取出dstId
有了srcId和dstId,你就可以blabla....
问: 为啥用`localSrcIds`的下标
答: 用`localDstIds`的也可以。一条边必然包含两个点:srcId, dstId
2. 发消息
看上图:
* 根据接口中定义的tripletFields,拿到发消息的方向: 1) 向dstId发;2) 向srcId发;3) 向两边发;4) 向其中一边发
* 发消息的过程就是遍历到一条边,向以srcId/dstId在本分区内的本地ID`localId`为下标的数组中添加数据,如果`localId`为下标数组中已经存在数据,则执行合并函数`mergeMsg`
* 每个点之间在发消息的时候是独立的,即:点单纯根据方向,向以相邻点的`localId`为下标的数组中插数据,互相独立,在并行上互不影响。
完事,返回消息RDD`messages: RDD[(VertexId, VD2)]`
#### 2.1.2 aggregateMessages Reduce阶段
(点击查看大图)

因为对边上,对点发消息,所以在reduce阶段主要是`VertexRDD`的菜。
入口(Graphmpl 260行):
`vertices.aggregateUsingIndex(preAgg, mergeMsg)`
收到`messages: RDD[(VertexId, VD2)]`消息RDD,开始:
1. 对`messages`做shuffled分区,分区器使用VertexRDD的`partitioner`。
因为VertexRDD的partitioner根据点VertexID做分区,所以`vertexId->消息`分区后的pid根VertextRDD完全相同,这样用zipPartitions高效的合并两个分区的数据
2. 根据对等合并attr, 聚合函数使用API传入的`mergeMsg`函数
* 小技巧:遍历节点时,遍历messagePartition。并不是每个节点都会收到消息,所以`messagePartition`集合最小,所以速度会快。遍历小集合取大集合的数据。
* 前文提到根据routingTables路由表生成VertexRDD的vertexPartitions时, vertexPartitions中重新封装了ShippableVertexPartition对象,其定义为:
```
ShippableVertexPartition[VD: ClassTag](
val index: VertexIdToIndexMap,
val values: Array[VD],
val mask: BitSet,
val routingTable: RoutingTablePartition)
```
最后生成对象:
`new ShippableVertexPartition(map.keySet, map._values, map.keySet.getBitSet, routingTable)`
所以这里用到的`index`就是map.keySet, map就是映射`vertexId->attr`
index: map.keySet, hashSet, vertexId->下标
values: map._valuers, Array[Int], 根据下标保存attr。
so so,根据vetexId从index中取到其下标,再根据下标,从values中取到attr,存在attr就用API传入的函数`mergeMsg`合并属性attr; 不存在就直接赋值。
最后得到的是收到消息的`VertexRDD`
到这里,整个map/reduce过程就完成了。
----------------------
翠花,上全图
(点击查看大图)

*如果没有绕晕,从头再读一遍*
------待续--------
================================================
FILE: spark_repl.markdown
================================================
## spark repl 图解(v0.1)
> author: 玄畅 时金魁 2016.1.12
本文基于[spark master代码](https://github.com/apache/spark)分析, 当前最新的spark版本为:2.0.0-SNAPSHOT, 最新commit id: [8cfa218](https://github.com/apache/spark/commit/8cfa218f4f1b05f4d076ec15dd0a033ad3e4500d), Commits on Jan 12, 2016
### overview
----------
repl: `Read! Eval! Print! Loop..`, 顾名思义就是: 读取输入-求值-打印,无限循环上述过程。
[jline2](https://github.com/jline/jline2)是一个java实现的repl, 有个[example](https://github.com/jline/jline2/blob/master/src/test/java/jline/example/Example.java),可以感受下是repl怎么回事。
spark repl鲜有人说,大概因为repl是非必需品,在生产和调试spark时几乎用不到repl。在刚接触spark时,跑一下 [Spark Examples](http://spark.apache.org/examples.html)时, 一般会直接在`spark-shell`里跑一下样例。
下文就是从`spark-shell`入口剖析下spark repl的运行路径。
### used by
----------
用到repl的应用:
1. spark shell
2. hue livy
3. spark-notebook
适用于spark交互式场景, 操作界面一般是notebook, 在web上写spark代码, 直接在web上运行,输出结果。
为什么notebook比较受欢迎?
配置好集群后,直接通过web界面(notebook)运行spark作业,**渲染**输出结果,特别适用于作业调试,方便、快速。算法工程师焦点于job作业,而不需要关心底下的spark集群。
### full graph
----------

### entrance
----------
从`bin/spark-shell`文件作为研究spark repl的入口。
```
export SPARK_SUBMIT_OPTS
"$FWDIR"/bin/spark-submit --class org.apache.spark.repl.Main --name "Spark shell" "$@"
```
`bin/spark-shell`向`bin/spark-submit`提交main为`org.apache.spark.repl.Main`的scala object。Main是spark-repl包里的类,所以不需要添加jar,spark自带的。
`bin/spark-submit`
```
exec "${SPARK_HOME}"/bin/spark-class org.apache.spark.deploy.SparkSubmit "$@"
```
`/bin/spark-class`负责读取环境配置,所需的jar, 然后执行org.apache.spark.deploy.SparkSubmit.main(), SparkSubmit会:
1. 生成指定的类加载器
2. 把jar添加到classpath
3. 设置系统属性 System.setProperty
4. `--class`指定的类, invoke it
经过前面的spark运行环境准备工作,后面进入到`org.apache.spark.repl.Main.main()`, 这个Main对象就是repl的入口了。

### repl
----------
org.apache.spark.repl.Main.main()函数很简单, new一个`SparkILoop`对象, 调用它的`process()`函数。
SparkILoop是repl处理输入-求值-打印的主要地方.
一个repl的过程大致有以下4个步骤:
1. 读取控制台输入
2. 编译输入的代码, 生成AST
3. apply, 执行编译后的字节码
3. 输出结果
SparkILoop对象有两个关键的成员变量:
1. `intp: SparkIMain` 解释器
2. `in: InteractiveReader` 控制台输入reader
一个读取用户输入, 一个解释执行输入的代码,打印结果。
#### step 1:
第一个进入的是`process()`函数, 这个主要是把输入参数转化成`SparkCommandLine`对象, 如果输入参数不是帮助说明参数, 进入process(command.settings)函数。
```
/** process command-line arguments and do as they request */
def process(args: Array[String]): Boolean = {
val command = new SparkCommandLine(args.toList, msg => echo(msg))
def neededHelp(): String =
(if (command.settings.help.value) command.usageMsg + "\n" else "") +
(if (command.settings.Xhelp.value) command.xusageMsg + "\n" else "")
// if they asked for no help and command is valid, we call the real main
neededHelp() match {
case "" => command.ok && process(command.settings)
case help => echoNoNL(help) ; true
}
}
```
#### step 2:
1. 创建解释器
process(command.settings)函数,首先会检查master是否是`yarn-client`,如果是则设置系统变量`SPARK_YARN_MODE=true`, 标示当前是yarn模式。
接着会执行函数`createInterpreter()`创建一个解释器对象, 这个解释器会准备基本的运行环境。后续用户输入的代码,都是通过这个解释器执行。
查询是否设置了系统变量`spark.jars`,如果不存在则读取另外一个系统变量`ADD_JARS`是否存在。由此得到一个逗号`,`分割的jar列表, 解析每个jar的URL, 添加到classpath中。
构建一个解释器对象`SparkILoopInterpreter`, 赋值给`SparkILoop`的变量`intp`
解释器继承自`InteractiveReader`, 固定的生命周期为:
```
val interactive: Boolean
def init(): Unit
def reset(): Unit
def history: History
def completion: Completion
def eraseLine(): Unit
def redrawLine(): Unit
def currentLine: String
def readYesOrNo(prompt: String, alt: => Boolean): Boolean
def readAssumingNo(prompt: String)
def readAssumingYes(prompt: String)
def readLine(prompt: String): String
```
规范见[这里](http://cnswww.cns.cwru.edu/php/chet/readline/readline.html#SEC9)
*2.1 创建解释器:*

2. 创建控制台输入reader
有两类reader:
* 用户指定readerBuffer了, new一个`SimpleReader`对象
* 根据settings配置选择一个reader对象,new一个`SparkJLineReader`或`SimpleReader`对象
reader对象生成后, 赋值给`SparkILoop`的变量`in`, 后续读取输入。
*2.2 选择reader:*

3. 添加bind绑定到执行列表
3,4,5,6都是把要执行的函数放到`pendingThunks:List[() => Unit]`中, 这些函数都是需要在解释器初始化后被执行。
名为绑定,要绑定个啥?
把一个key-value设置到intp解释器中,在后面的用户输入的表达式中可以直接引用这个key-value。在后续代码片段生成的class中引入intp作为成员变量,这样就可以直接用了。
先new一个`val bindRep = new ReadEvalPrint()`对象, ReadEvalPrint表达一个repl的过程。`bindRep`首先编译object类,再调用`set`函数, 把SparkIMain对象set进去。
// todo CodeAssembler, ObjectSourceCode, ResultObjectSour ceCode
```
object ${bindRep.evalName} {
var value: ${boundType} = _
def set(x: Any) = value = x.asInstanceOf[${boundType}]
}
```
上面这个静态类就是要编译的代码, 查询系统属性`scala.repl.name.eval`, 如果不存在就以`$eval`作为类名。成员变量`boundType`就是`SparkIMain`了。
`bindRep.callEither()`调用上面生成的object的set函数,把SparkIMain对象set进去。
这样,直接调用生成object对象`${bindRep.evalName}`的value属性就可以用`SparkIMain`了。
*2.3 绑定*

4. 添加repl自动执行代码到执行列表
在`scala.tools.nsc.ReplProps`中定义的变量`replAutorunCode`, 会引用系统变量`scala.repl.autoruncode`, 如果用户设置了这个属性, 则会读取对应的value, value一般是指向一个代码文件, 如果确实存在这个源代码文件, 则调用编译执行函数。
上述整个过程添加到待执行列表。
5. 添加欢迎信息函数到执行列表
控制台输出欢迎字符串:
```
____ __
/ __/__ ___ _____/ /__
_\ \/ _ \/ _ `/ __/ '_/
/___/ .__/\_,_/_/ /_/\_\ version %s
/_/
```
6. 添加初始化spark环境函数到执行列表
初始化spark环境initializeSpark,编译执行以下代码块:
```
// 1. command
@transient val sc = {
val _sc = org.apache.spark.repl.Main.interp.createSparkContext()
println("Spark context available as sc " +
s"(master = ${_sc.master}, app id = ${_sc.applicationId}).")
_sc
}
// 2. command
@transient val sqlContext = {
val _sqlContext = org.apache.spark.repl.Main.interp.createSQLContext()
println("SQL context available as sqlContext.")
_sqlContext
}
// 3. command
import org.apache.spark.SparkContext._
// 4. command
import sqlContext.implicits._
// 5. command
import sqlContext.sql
// 6. command
import org.apache.spark.sql.functions._
```
导入必需的类, 声明sc和sqlContext变量, 直接暴露这俩变量给用户使用。
**notebook也主要以此两个变量作为调试job代码的入口。**
上述整个过程添加到待执行列表。
7. 解释器初始化
解释器`intp`根据用户的设定是否为异步执行同步初始化或异步初始化。目前固定为同步初始化。
> 异步初始化的设置位置为: `scala.tools.ScalaSettings`
> val Yreplsync = BooleanSetting ("-Yrepl-sync", "Do not use asynchronous code for repl startup")
初始化的过程就是编译执行名为`<init>`代码:`class $repl_$init { }`,一个空类。
如果这个空类编译执行报错,那么整个repl就会hang死翘翘了。更像是个探针,测试下scala编译执行环境是否可用。
8. 执行3,4,5,6添加到执行列表中的函数
初始化完毕后, 遍历之前添加到`pendingThunks`列表中的待执行函数,apply执行之。
9. loop开始干活
解释器初始化正常,执行系统定义的代码:绑定、自动执行代码、welcome、初始化spark变量,一切正常则开始无尽循环的正事: read-eval-print, readLine => processLine。
loop退出条件为:
* 读到的行为null
* 行命令执行结果`Result`的`keepRunning==false`
`case class Result(val keepRunning: Boolean, val lineToRecord: Option[String])`
读到行代码, 执行代码内容, 执行函数路径为:
1. 解析 command(line)
2. 开始调用解释器 `interpretStartingWith(code: String)`
3. 调用解释器 `intp.interpret(code) `
4. 生成语法树 `requestFromLine(line, synthetic)`
5. 加载上下文环境, 执行语法树。`loadAndRunReq(req: Request)`
6. 调用用户输入的代码 `call()`
`m.invoke(evalClass, args.map(_.asInstanceOf[AnyRef]): _*)` 调用
这里的Method是把line source code放到一个函数中, 生成一个class, 然后调用执行。
*2.9 repl*

### summary
--------------
从SparkSubmit起,初始化解释器,执行代码: 绑定、自动执行代码、输出欢迎、初始化spark环境,进入loop状态:read-eval-print。
如果想让repl的过程有更多的自定义交互操作,可以提交SparkSubmit一个自定义的类,做一个中间代理,包装一下`SparkILoop`。弊端就是要兼容spark各个版本的代码。
后续会分析hue livy和spark notebook怎么与spark交互。

*转载请注明原作者*
--------EOF---------
================================================
FILE: spark_ui.markdown
================================================
# spark UI源码阅读
-----------
@玄畅
2015.6.11
> spark master ui中worker ui的链接使用hostName:8081, 今天上午想改成自定义proxy地址的方式, 索性读一下spark UI的代码,整理如下。
spark集群启动后,用户可以通过Spark提供UI界面实时查看spark任务的执行情况,spark standalone模式下支持离线的UI。
master上使用8080端口提供UI服务,worker上使用8081提供UI服务。
Spark Master上的UI如下:
图1: Spark Master UI

## Spark Master start UI
### 执行脚本, 启动Master
spark的启动脚本在`sbin`目录中, 启动时脚本调用路径:
`start-all.sh` => `stop-master.sh` => `spark-daemon.sh org.apache.spark.deploy.master.Master` => `../bin/spark-submit` => `spark-class org.apache.spark.deploy.SparkSubmit`
启动最终的入口为`SparkSubmit`, 在这里解析args参数, 执行`org.apache.spark.deploy.master.Master`的main函数, 怎么执行的?`mainMethod.invoke(null, childArgs.toArray)`[SparkSubmit.scala 620行]
### Master启动UI: 初始化`WebUIPage`和`ServletContextHandler`
Master继承自`Actor`, 成员变量初始化`private val webUi = new MasterWebUI(this, webUiPort)`, 在Actor的`preStart`中, 真正绑定UI端口。
再看一下`MasterWebUI`, 它的父类是`WebUI`, 实现抽象类`WebUI`的子类有:`MasterWebUI`,`HistoryServer`, `MesosClusterUI`, `SparkUI`, `WorkerWebUI`。这些都是真正提供web ui服务的。
MasterWebUI在初始化的时候(initialize), 初始化Master UI的web组件: `ContextHandler`,具体的为:
Component | path | Description
------------ | ------------- | -------------
MasterPage | / | 渲染workers, 活跃的appliction, 活跃的drivers
ApplicationPage | /app | 根据request中的appId参数, 展示这个活跃的application
HistoryNotFoundPage | /history/not-found | 历史信息找不到404
static handler | 加载静态资源, 见这里:`resources/org/apache/spark/ui/static/`
ApiRootResource handler| -
redirect handler| /app/kill, /driver/kill | 把这俩kill请求转发给master page处理, Master异步kill
`WebUIPage`继承自`WebUI`, `WebUI`有两个抽象函数`render(request: HttpServletRequest): Seq[Node]`和`renderJson(request: HttpServletRequest): JValue`。需要注意的是成员参数`prefix`, 这个就是这个页面对应的http path了。
看参数可以猜出, 接收servlet request, 处理请求, 返回Node或者Json结果。
看页面`WebUIPage`中的`content`变量, 一大堆的html代码, 补上所需的变量, 加上公共部分页头`UIUtils.basicSparkPage`, 就是一个完整的渲染后的页面了。
每个页面都需要有一个对应page handler和json handler。比如:`http://localhost:8080`返回的是渲染的html,`http://localhost:8080/json/`返回的是渲染的json
### Master启动UI: startJettyServer
`Master.scala`在prestart调用`webUi.bind()`, 生成server实例, 监听web端口, jetty server绑定handler。然后就成了。
前面page对应handler以及添加的其他handler, 生成一个`ContextHandlerCollection`, 其他就是标准的jetty接口了。
图2: Jetty Server接口

```
// Bind to the given port, or throw a java.net.BindException if the port is occupied
def connect(currentPort: Int): (Server, Int) = {
val server = new Server(new InetSocketAddress(hostName, currentPort))
val pool = new QueuedThreadPool
pool.setDaemon(true)
server.setThreadPool(pool)
val errorHandler = new ErrorHandler()
errorHandler.setShowStacks(true)
server.addBean(errorHandler)
server.setHandler(collection)
try {
server.start()
(server, server.getConnectors.head.getLocalPort)
} catch {
case e: Exception =>
server.stop()
pool.stop()
throw e
}
}
```
绑定端口, 添加pool, 设置handler, start, 齐活。
## Spark Worker start UI
sbin目录中, `start-all.sh` => `start-slaves.sh` => `start-slave.sh` => `spark-daemon.sh start org.apache.spark.deploy.worker.Worker` => `../bin/spark-submit` => `spark-class org.apache.spark.deploy.SparkSubmit`
入口`SparkSubmit`调用Worker的main函数. Worker同样是一个Actor, prestart里初始化webUi.
```
webUi = new WorkerWebUI(this, workDir, webUiPort)
webUi.bind()
```
`WorkerWebUI`同样实现了抽象类`WebUI`, 注册的web组件有:
Component | path | Description
-----|-----|----
LogPage | /logPage | 根据appId/executorId/driverId定位日志
WorkerPage | / | 展示worker中的executor以及driver信息
static handler | /static | 静态资源加载
log handler | /log | 请求log,由LogPage渲染
## 读后感
### embedded jetty
内嵌的jetty非常好用, 之前改过一个[jetty-embedded-spring-mvc](https://github.com/shijinkui/jetty-embedded-spring-mvc)和[jetty-embedded-spring-mvc-noxml](https://github.com/shijinkui/jetty-embedded-spring-mvc-noxml), 当时主要做从web容器resin到非容器的过度(finagle/rest.li/dropwizard)
embedded jetty, 轻, QPS比web容器高多了。去重量级web容器是大势所趋。
### page, handler, render
收到用户request, 匹配到hander, 渲染页面, 返回。
### 耦合
UI的代码也不少, 一大堆的资源文件(css/js/images)都放在core里。而web界面的进化必然是越来越重, UI越来越炫, 功能越来越丰富, 比如: 提供notebook功能。
UI放在core里主要是方便使用Master这个actor查询worker/application/driver的状态。
一个更好的方式是:
1. Master提供标准的metric接口和rest操作
可以通过actor message沟通, 也可以开端口用RestFul方式, 也可以用netty保持tcp长链接, decode/encode Message的交互方式。
2. Spark UI独立模块
天高任鸟飞, 让UI越来越丰富。
为什么要让UI更丰富?降低使用门槛, 方便监控。
================================================
FILE: 食不厌精,脍不厌细:如何一步步将kcore算法提升5倍性能.markdown
================================================
## 食不厌精,脍不厌细:如何一步步将KCore算法提升5倍性能
@玄畅
2014.12.27
## 1. 关于
KCore是图算法中的一个经典算法,其计算结果是判断节点重要性最常用的参考值之一。本算法实现原理基于论文Distributed k-Core Decomposition,底层实现基于Spark GraphX。经过不断的优化,充分利用图计算的特点,在亿级别节点上的运行时间,从7.8小时,缩短为1.2小时,并达到稳定的生产级别。本文还原了整个优化过程,希望对做图算法的同学,有所帮助。
**概要描述:**
开始时,图中每个点的coreness初始值设置为它的度值。然后,所有点把它当前的coreness值发给它的邻居。
接着,在获得邻居的消息后,每个节点计算更新自身的coreness,并且会把变化情况通知它的所有邻居。
如此迭代计算,直到消息数目为零,达到稳定状态,最终得到图中每个点的coreness值。
**具体过程:**
1. 读`边`的文本文件生成普通的RDD。一行一条边信息,形如:
`10,11`
`10,12`
`11,12`
`13,14`
`13,15`
2. 构建图。根据1中生成`EdgeRDD`, 滤掉重复的点,构建出`VertextRDD`, 组合成`Graph` (这块内容较为复杂,后续有源码分析)
3. 顶点`VertextRDD`中的attr为默认值`1`, 初始化为对象`KCoreVertex`替换默认值`1`
4. 迭代前的初始化。每个点都向邻居节点发消息。通过`aggregateMessages`函数执行,得到`VertexRDD[Map[Int, Int]]`,即:每个顶点收到的消息
5. 迭代过程
1. innerJoin(msg)(vertexProgram)计算每个点K值变化,更新vertextRDD的attr对象`VertexRDD`,得到变化的`changedVerts = VertexRDD[KCoreVertex]`
2. outerJoinVertices(changedVerts)。 把1变化的点更新到`Graph`中,其中通过VertexRDD中`isChange=true`表示点有变化
3. aggregateMessages(sendMsg, mergeMsg) 让Graph中有变化的点向外发消息(执行sendMsg);reduce时,`mergeMsg`合并每个分区上点收到的消息。得到`VertexRDD[Map[Int, Int]]`
4. joinVertices。在2中标记了变化的节点,这里需要重置这些标记,`isChange=false`,否则下一轮迭代时直接让这个点发消息(可能这个点在下一轮并没有收到消息,k值也没有变化)
**优化的地方主要是迭代过程,这里耗时最大**
**KCore算法概念图:**


(来自网络)
## 2. 测试数据
亿级节点
点:181640208
边:890041895
## 3. 优化之前
KCore算法在spark上能跑起来
问题:
1. 不能处理到消息数为0
2. 150迭代会任务失败,内存不够用
```
运行参数
--driver-memory 60g
--num-executors 50
--executor-memory 60g
--executor-cores 8
```
**耗时:**
迭代到150轮时耗时: 500秒左右(历史纪录看不到了,大概是这个)
## 4.1 优化过程
问题:
1. 耗内存
2. 代码可读性
### 4.1.1 第1次优化:KCoreVertex对象瘦身
每次迭代发送的消息数为:当前节点到相邻节点。节点数不变,消息一直在骤减直至消息数为0退出迭代。所以优化的重点考虑节点attr属性对象,即:`KCoreVertex`
之前成员变量数14个:
```
class KCoreVertex(val vId: Long, val vDegree: Int, val max: Int, val offset: Int) {
val coreOffset = offset
val MAX_K_CORE = max
val id = vId
val degree = if (vDegree < MAX_K_CORE) vDegree else MAX_K_CORE
var preKCore = -1
var curKCore = degree
var isChanged = false
var isInit = false
val arrayLength = degree - coreOffset
val kCoreStack = new Array[Int](arrayLength + 1)
for (i <- 0 to arrayLength) {
kCoreStack(i) = 0
}
def this(other: KCoreVertex) {
this(other.id, other.degree, other.MAX_K_CORE, other.coreOffset)
preKCore = other.preKCore
curKCore = other.curKCore
isChanged = other.isChanged
isInit = other.isInit
for (i <- 0 to arrayLength) {
kCoreStack(i) = other.kCoreStack(i)
}
}
```
优化后变为7个成员变量:
```
class KCoreVertex(val vId: Long, val vDegree: Int, val max: Int) {
var preKCore = -1
var curKCore = Math.min(vDegree, max)
var flag = 0
// 发生变化在第几次迭代
var kCoreStack = Array.fill(Math.min(vDegree, max) + 1)(0)
```
**小结:**
1. 每减少一个变量,都需要调整相应的逻辑,本地测试数据的正确性,集群上测性能
2. 减少非必需的变量,节约总是好的习惯
3. 成员变量的减少意味着复杂度的降低,需要更精炼的表达
### 4.1.2 第2次优化:数组的拷贝
数组拷贝可以分为慢拷贝(for)和快拷贝(System.arraycopy)
foreach的慢拷贝多用于源数组和目标数组类型不一致,那就迭代对每个元素转换;快拷贝是native方法,用于数组类型一致的情况.
在KCoreVertex对象中,类型一致,适用快拷贝, 代码中有三处慢拷贝,改为`arraycopy`
本地测试了下,对比如下:
拷贝 | 数组大小 | 耗时(毫秒)
------------ | ------------- | ------------
for | 千万 | 11
arraycopy | 千万 | 8
foreach | 百万 | 5
arraycopy | 百万 | 1
**小结:**
1. 数组类型一致使用快拷贝`System.arraycopy`
### 4.1.3 第3次优化:scala范型和特化系统的类型约束
特化这里指的具体的实现,只用于KCore算法实现这种情形,不适用其他场景。
泛化一般适用于通用平台,像spark、HSF、metaq、kafka这些系统,他们不关心具体的数据类型,在这些系统中一般用范型或者不关心类型直接传输`byte[]`数据
之前:
```
def KCorePregel[ED: ClassTag, A: ClassTag]
(graph: Graph[KCoreVertex, ED],
```
之后:
```
private def pregel(
originGraph: Graph[KCoreVertex, Int],
```
**小结:**
具体业务代码中使用确定的类型,代码可读性好一些。
### 4.1.4 第4次优化:函数参数定义
之前:
```
/**
* Add time message
*/
def KCorePregel[ED: ClassTag, A: ClassTag]
(graph: Graph[KCoreVertex, ED],
initialMsg: A,
checkpointPath: String,
maxIterations: Int = Int.MaxValue,
activeDirection: EdgeDirection = EdgeDirection.Either)
(vprog: (VertexId, KCoreVertex, A) => KCoreVertex,
sendMsg: EdgeTriplet[KCoreVertex, ED] => Iterator[(VertexId, A)],
mergeMsg: (A, A) => A)
: Graph[KCoreVertex, ED] = {
```
之后:
```
/**
* 核心计算函数, 迭代计算kcore
* 0. 初始化: mapReduceTriplets生成消息列表
* 1. innerjoin: 得出所有消息中涉及变更的点newVertexs, vertexProgram计算节点的kcore
* 2. outerJoinVertices: 更新1中得到的vertex到当前的graph中
* 4. aggregateMessages: 变更的点向邻居发消息
*
*/
private def pregel(
originGraph: Graph[KCoreVertex, Int],
checkpointPath: String,
maxIterations: Int): Graph[KCoreVertex, Int] = {
```
*这个函数根pregel很相似,但是pregel问题在于不能满足部分发消息的需求*
**小结:**
1. 函数参数尽可能少。 尽量让代码高内聚低耦合
----------------
**效果:**
通过上述几轮scala代码层面上的修改,算法可以运行到第150轮迭代,内存问题暂时解决,整体耗时6小时,提升了将近2小时。
----------------
### 4.2 算法实现的改进
改进之前的流程图(蓝色标记表示变化的节点):

1. 原先的思路。当前实现为常规思路,对变化的节点通过标记节点属性`ischanged=true/false`,在当前迭代结束后,擦除标记。
2. 迭代过程:
1. innerJoin 上一个迭代产生的消息RDD,找到这些消息所在的点及其属性,计算corenness, 并返回coreness变化的节点
2. outerJoinVertices 用1中变化的节点更新当前graph相对应点的属性对象`KCoreVertex`
3. mapReduceTriplets 标记为变化的节点向邻边发消息
4. joinVertices 重置标记为初始状态,即`ischanged=false`。不重置的话,下一轮迭代时,当前标记为true的节点会再发消息,但这个点可能在下一轮中并没有变化。
3. 当前主要问题
* 3次join操作在每次迭代时,**整体耗时每次单调递增1秒**,一秒不大,累计60轮单次迭代增加了1分钟,600轮就是1小时。
* 在测试过程中,迭代到100轮以后,`outerJoinVertices`和`joinVertices`耗时为140秒左右,为主要耗时的地方
* 这种问题本地debug发现不了,节点少了不明显,线上无法debug对比每次增量在哪里发生的。走读了graphx的代码也没发现有明显的问题。
-----------
尝试的解决办法:
1. mini graph
* 思路:每轮迭代中,在整个graph中标记出变化的节点,向邻边发消息。考虑到单调递增的1秒和join比较耗时,打算在一次迭代结束后,把当前变化的点从graph取出,找到包含这些点的边,以及这些边向外10次延伸的边路经。这样10次迭代就可以用mini graph了。
* 放弃原因:要得到这个mini graph,需要做十次消息聚合(mapReduceTriplet), `collectNeighborIds`同样也是用到`mapreduceTriplet`
2. 去掉ischange变量
* 放弃原因:不做标记,所有节点全向邻居发消息,节省最后一次join操作,结果迭代次数过多,消息过多,无法跑完整个数据
3. 染色 见下
#### 4.2.1 最终办法:染色
当前迭代中,在`KCoreVertex`的变量`flag`赋值为最新的迭代号`i=0,1,2...n`, 简称为染色,见下图:

1. 染色。一个节点在第3次迭代时变化了,标记为:`flag=3`,下一轮没变化`flag=3`保持不变。最终结果是:全图节点的`flag`各个不同,分别等于运行时的`i=0,1,2...n`
2. 去掉第四个join`joinVertices`。不需要重置,每个点都可以保留`i=0,1,2...n`值
3. 使用偏函数,同一迭代过程中,传递第一个参数`i=n`值
* `vertexProgram(cur_iter, _: VertexId, _: KCoreVertex, _: Map[Int, Int])`
* `sendMsg(cur_iter, _: EdgeContext[KCoreVertex, Int, Map[Int, Int]])`
* cur_iter为当前迭代号
和以往不同的是: **不需要重置标记,只让对当前迭代过程中标记为最新`i=n`的节点发消息**
**测试结果**
- 边: 890041895, 点:181640208
- 迭代平均耗时:24秒
- 迭代150轮,耗时:1.2h
- 迭代420轮,耗时:3.1h
- 能跑到消息数为0,但是消息数从4到3需要迭代80次左右,建议在消息数为10就退出迭代(不影响最终结果)
**部分日志**
```
14-12-27 23:16:57 [Driver] : {"iter" : 1, "active_msg" : 31147359, "cost" : 456437}
14-12-27 23:21:35 [Driver] : {"iter" : 2, "active_msg" : 19993211, "cost" : 278075}
14-12-27 23:24:28 [Driver] : {"iter" : 3, "active_msg" : 12935166, "cost" : 172991}
14-12-27 23:26:25 [Driver] : {"iter" : 4, "active_msg" : 8568323, "cost" : 116654}
14-12-27 23:27:40 [Driver] : {"iter" : 5, "active_msg" : 5911480, "cost" : 75346}
14-12-27 23:28:31 [Driver] : {"iter" : 6, "active_msg" : 4262917, "cost" : 50365}
14-12-28 01:55:44 [Driver] : {"iter" : 412, "active_msg" : 11, "cost" : 33277}
14-12-28 01:56:15 [Driver] : {"iter" : 413, "active_msg" : 11, "cost" : 30841}
14-12-28 01:56:39 [Driver] : {"iter" : 414, "active_msg" : 11, "cost" : 23936}
14-12-28 01:57:02 [Driver] : {"iter" : 415, "active_msg" : 11, "cost" : 23546}
14-12-28 01:57:27 [Driver] : {"iter" : 416, "active_msg" : 11, "cost" : 24452}
14-12-28 01:57:50 [Driver] : {"iter" : 417, "active_msg" : 11, "cost" : 23491}
14-12-28 01:58:15 [Driver] : {"iter" : 418, "active_msg" : 11, "cost" : 24407}
14-12-28 01:58:46 [Driver] : {"iter" : 419, "active_msg" : 11, "cost" : 31229}
14-12-28 01:59:09 [Driver] : {"iter" : 420, "active_msg" : 10, "cost" : 23603}
```
## 5. 总结
1. 效果对比
| | 平均迭代耗时 | 150轮耗时 | 420轮耗时 |
| ------------ | ------------- | ------------ | ------------ |
| 优化之前 | 188秒 | 7.8h | 跑不完 |
| 优化之后 | 24秒 | 1.2h | 3.1h |
| 对比 | 提升7倍 | 提升6.5倍 | 由不能跑全程到能3.1h跑完 |
2. scala代码方面
1. 减少非必需的变量,养成节约的好习惯
2. 类型相同的数组复制用arraycopy
3. 特化系统中尽量使用确定的类型
4. 函数参数尽可能少,高内聚低耦合
5. 恰当使用mutable和immutable对象
测试过程中,`KCoreVertext`对象改用case类和immutable.Map后,耗时大大增加,gc代价高
在大量生成新对象的地方不建议使用immutable, GC是个大负担, 这也是jvm的痛点。性能要求超高的程序,很多时候选择使用c、c++、golang,大都因为GC
2. 其他方面
1. 建立标准数据集。用于验证算法的正确性,小数据集方便本地debug
2. 每次小改动都应该本地验证正确性,集群上验证效果。
有时候,很小的改动,在集群中结果大不同
3. 不断尝试
念念不忘,必有回响。
算法第一个版本并非标准版本,根据当前的主要问题如内存、gc等,不断调整。
## 6. 感谢
感谢@明风和@刀剑在本文的逻辑和语言表达上给予的指正
----EOF----
gitextract_dulrsnnq/ ├── .gitignore ├── LICENSE ├── README.md ├── overview.markdown ├── spark_core_getstart_from_pi.markdown ├── spark_eight_style.markdown ├── spark_eight_style_1_rdd.markdown ├── spark_eight_style_2_dag_lazy.markdown ├── spark_graphx_analyze.markdown ├── spark_repl.markdown ├── spark_ui.markdown ├── src/ │ ├── saprk_八法.graffle │ ├── spark_core.graffle │ ├── spark_graphx_analyze.graffle │ ├── spark_repl.graffle │ └── spark_streaming.graffle └── 食不厌精,脍不厌细:如何一步步将kcore算法提升5倍性能.markdown
Condensed preview — 17 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (93K chars).
[
{
"path": ".gitignore",
"chars": 16,
"preview": ".DS_Store\n*.zip\n"
},
{
"path": "LICENSE",
"chars": 11325,
"preview": "Apache License\n Version 2.0, January 2004\n http://www.apache.org/licens"
},
{
"path": "README.md",
"chars": 782,
"preview": "#\t\tSpark Study\n----------------\n\n通过spark源码阅读,把spark内部运行逻辑用`图表+文字说明`的形式展现出来。\n图表能够清晰表达全局的逻辑关系和细节部分\n\n##\t\t目录\n\n1.\t[Graphx:构建g"
},
{
"path": "overview.markdown",
"chars": 878,
"preview": "#\t学习spark\n\n##\t\t分析spark从两个纬度\n \n1.\t 框架层面\t\t框架焦点在于处理流程,系统交互,性能等 \n2.\t 数据流\t\t数据流关注于数据的转换、合并、shuffle、结果\n\n##\t\t概念\n1.\tMast"
},
{
"path": "spark_core_getstart_from_pi.markdown",
"chars": 9558,
"preview": "#\t\t从SparkPi图解Spark core\n----------------\n@玄畅 \n2015.1.30\n\n\nspark core是spark的核心库,执行最基础的RDD操作。通过本文,从万年pi入手,逐层分解spark cor"
},
{
"path": "spark_eight_style.markdown",
"chars": 679,
"preview": "#\t\tspark八法\n\n###\t井中八法\n------\n“井中八法”一共八式,因寇仲天赋行军打仗的超卓领导才能,以兵法入刀,故各式均含用兵之道。\n\n总纲:故善战者,立于不败之地,而不失敌之败也。是故胜兵先胜而后求战,败兵先战而后求胜,因敌而"
},
{
"path": "spark_eight_style_1_rdd.markdown",
"chars": 8486,
"preview": "#\tspark八法之方圆:RDD\n\n\n>\t方圆: 数据为阳,算子为阴;算子为方,数据为圆。阴阳应象,天人合一,再不可分。\n>\t\n>\tSpark的RDD就像方圆和阴阳的概念一样贯穿于整个Spark框架,是Spark最基础和最核心的概念。\n\n#"
},
{
"path": "spark_eight_style_2_dag_lazy.markdown",
"chars": 845,
"preview": "#\t\tSpark八法之不攻: DAG、Lazy\n\n>\t不攻: 故用兵之法, 无恃其不来, 恃吾有以待也; 无恃其不攻, 恃吾有所不可攻也。\n>\t\n>\t以DAG和Lazy来应对数据计算的任务\n\n##\t\t综述overview\n\n>\t在图论中,如"
},
{
"path": "spark_graphx_analyze.markdown",
"chars": 8915,
"preview": "#\tGraphx:构建graph和聚合消息\n\n\n@玄畅 \n2014.12.29\n\n##\t\tAbout\n最近在优化kcore算法时,对Graphx代码看了几遍。1.2后Graphx性能有所提升,代码不太容易理解,现在用图表示出来会更直观"
},
{
"path": "spark_repl.markdown",
"chars": 7454,
"preview": "##\t\tspark repl 图解(v0.1)\n> author: 玄畅 时金魁 2016.1.12\n\n本文基于[spark master代码](https://github.com/apache/spark)分析, 当前最新的spar"
},
{
"path": "spark_ui.markdown",
"chars": 4417,
"preview": "#\tspark UI源码阅读\n-----------\n@玄畅 \n2015.6.11\n\n>\tspark master ui中worker ui的链接使用hostName:8081, 今天上午想改成自定义proxy地址的方式, 索性读一下s"
},
{
"path": "食不厌精,脍不厌细:如何一步步将kcore算法提升5倍性能.markdown",
"chars": 8420,
"preview": "##\t\t食不厌精,脍不厌细:如何一步步将KCore算法提升5倍性能\n\n@玄畅 \n2014.12.27\n\n##\t\t1.\t\t关于\n\nKCore是图算法中的一个经典算法,其计算结果是判断节点重要性最常用的参考值之一。本算法实现原理基于论文D"
}
]
// ... and 5 more files (download for full content)
About this extraction
This page contains the full source code of the shijinkui/spark_study GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 17 files (60.3 KB), approximately 24.9k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.