-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.xml
70 lines (48 loc) · 40 KB
/
search.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
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[让关系型数据库查询再飞一会儿]]></title>
<url>%2F2018%2F11%2F30%2F%E8%AE%A9%E5%85%B3%E7%B3%BB%E5%9E%8B%E6%95%B0%E6%8D%AE%E5%BA%93%E6%9F%A5%E8%AF%A2%E5%86%8D%E9%A3%9E%E4%B8%80%E4%BC%9A%E5%84%BF%2F</url>
<content type="text"><![CDATA[背景有一个系统的业务正在膨胀中,某一些报表(报表数据在mysql中)数据量增长比较厉害,报表页面已经处于卡爆了的状态。中间经过mysql本身的优化,已经到了当前系统架构+存储模型的瓶颈。本文提供一种优化思路,抛砖引玉。 任务分析以一条sql的优化为例(这条sql里面的字段随便改了改,不保证正确性)。12345678910111213141516SELECT d.col, COUNT(DISTINCT risk.inst_id) AS `count`FROM risk INNER JOIN d ON d.inst_id = risk.inst_id AND d.id = risk.id INNER JOIN b ON b.business_key = d.id AND d.type = b.type INNER JOIN r ON risk.inst_id = r.inst_id AND risk.id = r.idWHERE (r.visit_time >= '2018-10-27 00:00:00' AND r.visit_time <= '2018-11-28 15:54:40' AND d.id = '22821111115042' AND b.business_key = concat('22821111115042', ''))GROUP BY d.col 其中,risk表大小112MB,d表大小为9.5GB,b表208KB,r表大小为4.2GB。这个报表的生成逻辑中含有较多inner join。经过一些列的索引优化之后,该条sql的查询时间是36s,前端体验仍然不是很好,且随着报表时间范围的拉长,用户数据量的增长,查询时间会持续恶化。这里就不讨论更改表结构、迁移数据来优化查询了。 优化思路本身没有太多技术难度,但中间经过一段时间的摸索,直接说结论吧,希望对有需要的同学带来便利。用SparkSQL分布式计算的能力来加速查询,SparkSQL原生支持通过jdbc连接外部存储。首先,尝试了直接在sparksql的jdbc连接中执行上述sql,结果在意料之中,36秒左右。通过spark监控页面看到,该任务task数量为1,没有并发起来,SparkSQL将查询完全下推给mysql执行。那么问题来了,如何提升并发度呢?根据官方文档,使用jdbc连接有这么几个可用参数,这些参数的含义参考附录链接。numPartitions,partitionColumn,lowerBound,upperBound值得注意的是,partitionColumn 必须为数值类型,日期或者时间戳。lowerBound和upperBound必须为数字。在上面的case中,我们可以对r表的visit_time进行分区,并根据范围设置上下界线。(时间戳转化成long型)分别在SparkSQL load这4张表,其中对r表的visit_time进行分区,并分别在SparkSQL中注册临时表,在SparkSQL内执行上述SQL,上述SQL执行时间由36s降低到12s,如果调调SparkSQL的参数,性能可能会更好。这个方法从理论上来说,适用于任何单机关系型数据库。 原理简单剖析这里是将SparkSQL作为一个分布式查询引擎,mysql作为SparkSQL的一种数据源。SparkSQL内部有高度的统一抽象(DataFrame/DataSet)。SparkSQL从mysql中抽取数据然后根据自身的逻辑来进行运算。如果对细节感兴趣可以参考链接2。 参考文档[1] http://spark.apache.org/docs/latest/sql-data-sources-jdbc.html[2] http://spark.apache.org/docs/latest/sql-programming-guide.html]]></content>
</entry>
<entry>
<title><![CDATA[让Spark MLlib的预测性能再飞一会儿]]></title>
<url>%2F2018%2F05%2F02%2F%E8%AE%A9Spark-MLlib%E7%9A%84%E9%A2%84%E6%B5%8B%E6%80%A7%E8%83%BD%E5%86%8D%E9%A3%9E%E4%B8%80%E4%BC%9A%E5%84%BF%2F</url>
<content type="text"><![CDATA[背景介绍我们的系统有一小部分机器学习模型识别需求,因为种种原因,最终选用了Spark MLlib来进行训练和预测。MLlib的Pipeline设计很好地契合了一个机器学习流水线,在模型训练和效果验证阶段,pipeline可以简化开发流程,然而在预测阶段,MLlib pipeline的表现有点差强人意。 问题描述某个模型的输入为一个字符串,假设长度为N,在我们的场景里面这个N一般不会大于10。特征也很简单,对于每一个输入,可以在O(N)的时间计算出特征向量,分类器选用的是随机森林。对于这样的预测任务,直观上感觉应该非常快,初步估计10ms以内出结果。但是通MLlib pipeline的transform预测结果预测时,性能在86ms左右(2000条query平均响应时间)。而且,query和query之间在输入相同的情况下,也存在响应时间波动的问题。 预测性能优化先说说响应时间波动的问题,每一条query的输入都是一样的,也就排除了特征加工时的计算量波动的问题,因为整个计算中消耗内存极少,且测试时内存足够,因为也排除gc导致预测性能抖动的问题。那么剩下的只有Spark了,Spark可能在做某些事情导致了预测性能抖动。通过查看log信息,可以印证这个观点。从日志中截取了一小段,里面有大量的清理broadcast变量信息。这也为后续性能优化提供了一个方向。(下面会有部分MLlib源码,源码基于Spark2.3) 在MLlib中,是调用PipelineModel的transform方法进行预测,该方法会调用pipeline的每一个stage内的Transformer的transform方法来对输入的DataFrame/DataSet进行转换。12345@Since("2.0.0") override def transform(dataset: Dataset[_]): DataFrame = { transformSchema(dataset.schema, logging = true) stages.foldLeft(dataset.toDF)((cur, transformer) => transformer.transform(cur)) } 下面,我们先看看训练好的随机森林模型(RandomForestClassificationModel)在预测时做了些什么吧1234567override protected def transformImpl(dataset: Dataset[_]): DataFrame = { val bcastModel = dataset.sparkSession.sparkContext.broadcast(this) val predictUDF = udf { (features: Any) => bcastModel.value.predict(features.asInstanceOf[Vector]) } dataset.withColumn($(predictionCol), predictUDF(col($(featuresCol)))) } 重点来了,终于找到前面说的broadcast的’罪魁祸’了,每次预测时,MLlib都会把模型广播到集群。这样做的好处是方便批处理,但对于小计算量,压根不需要集群的预测场景这样的做法就有点浪费资源了: 每次预测都广播显然太多余。 因为每次都广播,所以之前的广播变量也会逐渐回收,在回收时,又反过来影响预测的性能。解决办法从上述代码中可以看到,RandomForestClassificationModel 预测最根本的地方是在于调用predict方法,输入是一个Vector。看看predict干了什么123override protected def predict(features: FeaturesType): Double = { raw2prediction(predictRaw(features)) } predict分为两步走:1234567891011121314151617181920212223242526272829override protected def predictRaw(features: Vector): Vector = { // TODO: When we add a generic Bagging class, handle transform there: SPARK-7128 // Classifies using majority votes. // Ignore the tree weights since all are 1.0 for now. val votes = Array.fill[Double](numClasses)(0.0) _trees.view.foreach { tree => val classCounts: Array[Double] = tree.rootNode.predictImpl(features).impurityStats.stats val total = classCounts.sum if (total != 0) { var i = 0 while (i < numClasses) { votes(i) += classCounts(i) / total i += 1 } } } Vectors.dense(votes) } override protected def raw2probabilityInPlace(rawPrediction: Vector): Vector = { rawPrediction match { case dv: DenseVector => ProbabilisticClassificationModel.normalizeToProbabilitiesInPlace(dv) dv case sv: SparseVector => throw new RuntimeException("Unexpected error in RandomForestClassificationModel:" + " raw2probabilityInPlace encountered SparseVector") } } 这两个方法的输入和输出均为vector,那么我们如果把这两个方法反射出来直接用在预测的特征向量上是不是就可以了?答案是肯定的。注意其中的raw2probability在Spark2.3中的RandomForestClassificationModel中,签名变为了raw2probabilityInPlace 全面绕开pipeline前面解决了分类器预测的性能问题,另外一个问题就来了。输入的特征向量怎么来呢?在一个MLlib Pipeline流程中,分类器预测只是最后一步,前面还有多种多样的特征加工节点。我尝试了将一个pipeline拆解成两个,一个用于特征加工,一个用于分类预测。用第一个pipeline加工特征,只绕开第二个,性能显然是提升了,但还没达到预期效果。于是,我有了另外一个想法:全面绕开pipeline,对pipeline的每一步,都避免调用原生transform接口。这样的弊端就是,必须重写pipeline的每一步预测方法,然后人肉还原pipeline的预测流程。流程大致跟上面类似。例如:OneHot(说句题外话,这东西在Spark2.3之前的版本是有bug的,详情参考官方文档)。OneHotEncoderModel的transform方法如下:123456789101112131415161718192021222324@Since("2.3.0") override def transform(dataset: Dataset[_]): DataFrame = { val transformedSchema = transformSchema(dataset.schema, logging = true) val keepInvalid = $(handleInvalid) == OneHotEncoderEstimator.KEEP_INVALID val encodedColumns = $(inputCols).indices.map { idx => val inputColName = $(inputCols)(idx) val outputColName = $(outputCols)(idx) val outputAttrGroupFromSchema = AttributeGroup.fromStructField(transformedSchema(outputColName)) val metadata = if (outputAttrGroupFromSchema.size < 0) { OneHotEncoderCommon.createAttrGroupForAttrNames(outputColName, categorySizes(idx), $(dropLast), keepInvalid).toMetadata() } else { outputAttrGroupFromSchema.toMetadata() } encoder(col(inputColName).cast(DoubleType), lit(idx)) .as(outputColName, metadata) } dataset.withColumns($(outputCols), encodedColumns) } 里面对feature进行转换的关键代码行是 encoder…1234567891011121314151617181920212223242526272829303132333435private def encoder: UserDefinedFunction = { val keepInvalid = getHandleInvalid == OneHotEncoderEstimator.KEEP_INVALID val configedSizes = getConfigedCategorySizes val localCategorySizes = categorySizes // The udf performed on input data. The first parameter is the input value. The second // parameter is the index in inputCols of the column being encoded. udf { (label: Double, colIdx: Int) => val origCategorySize = localCategorySizes(colIdx) // idx: index in vector of the single 1-valued element val idx = if (label >= 0 && label < origCategorySize) { label } else { if (keepInvalid) { origCategorySize } else { if (label < 0) { throw new SparkException(s"Negative value: $label. Input can't be negative. " + s"To handle invalid values, set Param handleInvalid to " + s"${OneHotEncoderEstimator.KEEP_INVALID}") } else { throw new SparkException(s"Unseen value: $label. To handle unseen values, " + s"set Param handleInvalid to ${OneHotEncoderEstimator.KEEP_INVALID}.") } } } val size = configedSizes(colIdx) if (idx < size) { Vectors.sparse(size, Array(idx.toInt), Array(1.0)) } else { Vectors.sparse(size, Array.empty[Int], Array.empty[Double]) } } } encoder里面关键的是这个udf,将其抠出重写之后直接作用于特征向量。 效果经过测试,全面绕开pipeline之后,响应时间下降到16ms左右。(2000条query平均响应时间),且不再有抖动。]]></content>
</entry>
<entry>
<title><![CDATA[上帝的骰子游戏]]></title>
<url>%2F2017%2F09%2F25%2Fgod-s-probable-game%2F</url>
<content type="text"><![CDATA[概率是一个很有意思的东西,通过上帝投掷出来的骰子,你能猜到上帝的意图。这是一篇白话瞎文,并不是特别严谨。 概率的两大学派概率有两大学派:概率学派,贝叶斯学派。“可悲”的是,国内的高等教育应该都是前者吧。之所以说“可悲”,是因为它固化了我们的思想,认为掷骰子的每一面,就是应该概率均等。当然,这个观点也是一己之见,欢迎来辩。 一个例子月初出去outing,我拿着一个小的行李箱,密码姑且假设为000。意外发生了,我在某次拨乱密码之后,“000”却打不开箱子了。请问,这个时候应该怎么办? 概率游戏概率学派告诉我们,三个0~9的数字组合,一共有1000种,只要遍历这1000种组合,就能打开密码箱了。贝叶斯学派说:扯淡,有先验条件没考虑进去,现在这个未知的密码压根就不是均匀分布的。我们来回顾一下这个概率游戏:之前的密码是000,把密码箱重新上锁拨乱之后,000打不开了,新的密码可能是多少?我回想了一下,我拨乱密码是随手逆时针一拨,每个数字大概率没有转一圈。因此对于每一位的数字的概率分布,并不是均等的,0后面的几个数字比较大,靠近9的概率分布偏小。因此,遍历密码时,并不需要全部遍历。最终,我尝试了几十个之后,就把密码解开了。]]></content>
</entry>
<entry>
<title><![CDATA[word2vec在学历造假中的探索]]></title>
<url>%2F2017%2F09%2F15%2Fword2vec-in-background%2F</url>
<content type="text"><![CDATA[前言 如果你想了解word2vec的原理,这篇文章并不适合你,出门右转用google。 这篇文章的东西含金量不高,希望搞NLP,ML,DL的专业人士轻拍。 因为含金量不高,所以有一些诸如数据预处理的一些琐碎的东西,因此比较适合新手村的新手任务。 背景在我们的系统中,有一处是需要校验一个人提供的学历信息是否真实。系统现有的算法准确率比较高,但是召回率比较低。举一个例子来说明一下学历造假相关背景。以计算机相关专业为例:计算机科学与技术是一级学科,计算机应用技术,信息安全,计算机系统结构是二级学科。软件工程现在貌似已经是一级学科?在硕士研究生和博士研究生的授位中,是按照二级学科来区分的。但学计算机的人都懂的,其实都一样。以至于很多人都不知道自己是哪个二级学科的,然后问题就来了,让你填你的毕业专业,你填哪个呢?填错了会不会被认为是学历造假? 问题显然,这是一个短文本匹配问题,文本短到仅由两三个词构成。而且,由于专业的局限性,非专业人士基本分不清某个一级学科下面有哪些二级学科。 解决办法 编辑距离,这个算法的缺点明显:计算机科学与技术和信息安全的编辑距离,想想都觉得大,字面上看来一点关系都没有。 word2vec: 借助NLP的东西来计算两个专业之间的相似度,挖掘隐藏信息。 基于word2vec的短文本相似度语料语料我选择的是中文维基百科,下载地址是:https://dumps.wikimedia.org/zhwiki/latest/zhwiki-latest-pages-articles.xml.bz2获得语料之后,还需要对语料进行一些预处理:(此处参考了:http://licstar.net/archives/262) 抽取正文文本 繁简转换 分词中文相关的处理,分词是绕不开的一个步骤,我采用了ICT分词的java版。 word2vec我试图用一些线程的word2vec的jar包来直接训练分词后的语料,但找了好几个,内存都爆了。无奈,我只能在spark mllib上手动做了一个。代码就不贴了,很简单,mllib有现成的word2vec算法库。 得到词向量之后,怎么表达成短文的向量呢?我采用了一个简单粗暴的办法:向量叠加。直接将短文本分词后的词向量叠加起来,再用余弦相似度来计算相似度。看一下结果吧:其中,相似度A是现在系统跑的算法,相似度B是基于word2vec向量叠加的相似度。可见,word2vec有效地挖掘出来了专业之间的潜在联系。 结论 从上面的图中可以看出,在word2vec中,一级学科和二级学科的相似度显著提升。 软件工程作为一个一级学科,跟计算机科学与技术也有极高的相似度,带来了更大的误导,但其实软件工程作为计算机的相关专业确实相关性极高。 未来工作 有比向量叠加更好的点子么?应该有吧,卷积应该是一个不错的选择,但是我还没有想好怎么卷积,毕竟我的场景比较特殊,没有标注好的样本进行训练(因为专业是有限可枚举的,如果有功夫标注的话,我想不需要模型来算相似度了,因此我的场景只是需要一个办法来计算相似度)。如果各位有啥好的点子,还请不吝赐教。 参考文献 http://licstar.net/archives/262]]></content>
</entry>
<entry>
<title><![CDATA[变参调用:scala和java的一个不同点]]></title>
<url>%2F2016%2F11%2F11%2Fa-dif-between-scala-and-java%2F</url>
<content type="text"><![CDATA[scala和java几乎没有区别,可以互相调用。注意这里说的是几乎,总有那么少数,出人意料的惊喜在告诉你,scala就是scala。 一个例子1234567891011import com.alibaba.fastjson.JSONobject test { def main(args: Array[String]) = { val map = new util.HashMap[CharSequence, CharSequence]() map.put("123", "22333") map.put("test", null) val ret = JSON.toJSONString(map) println(ret) }} 如上所示,这个例子很简单,把一个jabva的map转换成json字符串。其中JSON.toJSONString的代码如下:1234567public static String toJSONString(Object object) { return toJSONString(object, emptyFilters, new SerializerFeature[0]);}public static String toJSONString(Object object, SerializerFeature... features) { return toJSONString(object, DEFAULT_GENERATE_FEATURE, features);} 问题来了,上面的测试用例报错了:123456ambiguous reference to overloaded definition,both method toJSONString in object JSON oftype (x$1: Any, x$2: com.alibaba.fastjson.serializer.SerializerFeature*)Stringand method toJSONString in object JSON oftype (x$1: Any)String match argument types (java.util.HashMap[CharSequence,CharSequence])val ret = JSON.toJSONString(map) 错误的原因很明显,编译器在编译scala调用java依赖包里面的toJSONString函数时发生了歧义。 scala含有变参的重载函数看一段代码:123456789101112object Foo { def apply (x1: Int): Int = 2 def apply (x1: Int, x2: Int*): Int = 3 def apply (x1: Int*): Int = 4}object Test11 extends App { Console println Foo(7) Console println Foo(2, Array(3).toSeq: _*) Console println Foo(Array(3, 4).toSeq: _*)} 上述代码分别输出:2、3、4对于前两个构造函数,刚好对应了文章开头的例子,说明,scala调用类似的scala依赖是没有问题的。我们来注意一下scala如何调用变参:当你使用Foo(2, 3)调用时候,会出现歧义,因为(2, 3)可以匹配到第二个和第三个构造函数。所以,就只能用看起来很奇怪的写法Foo(2, Array(3).toSeq: _*)来进行区分。Array(3).toSeq: _*就是在告诉编译器,这个参数是变参的,别匹配错了。然后,我们来看看Java怎么做的。 java含有变参的重载函数代码就不写了,总之,java调用java类似的函数也是没有问题的。那么问题来了,为什么scala调用java类似的函数就有问题呢?要回答这个问题,我们先来看看java在进行重载调用时,编译器都做了些啥? 调用方法时,能与固定参数函数以及可变参数都匹配时,优先调用固定参数方法。 调用方法时,两个变长参数都匹配时,编译无法通过。12345678910111213public class VariVargsTest2 { public static void main(String[] args) { test("hello"); //1 } public static void test(String ...args) { System.out.println("变长参数1"); } public static void test(String string,String...args) { System.out.println("变长参数2"); }} 当没有1处代码时,程序是能够编译通过的。但是当添加了1处代码后无法通过编译,给出的错误是:The method test(String[]) is ambiguous for the type VariVargsTest2。编译器不知道选取哪个方法。 总结从scala和java对含有变参的重载函数处理的方式上,我们可以知道文章开头的代码为什么编译不过。 scala首先会自己匹配,自己匹配不了的时候,使用者可以手动来标识变参参数。 java自己匹配,并有一个自己的调用优先级顺序,实在分不清就编译不过了。回到开头的问题,scala调用java出问题了,就应该是scala编译器和java编译器在处理这个问题时的差异导致的吧。 参考文献[1] Java语法糖初探(三)–变长参数]]></content>
</entry>
<entry>
<title><![CDATA[Graphx 源码剖析-图的生成]]></title>
<url>%2F2016%2F10%2F31%2Fgraphx%2F</url>
<content type="text"><![CDATA[Graphx的实现代码并不多,这得益于Spark RDD niubility的设计。众所周知,在分布式上做图计算需要考虑点、边的切割。而RDD本身是一个分布式的数据集,所以,做Graphx只需要把边和点用RDD表示出来就可以了。本文就是从这个角度来分析Graphx的运作基本原理(本文基于Spark2.0)。 分布式图的切割方式在单机上图很好表示,在分布式环境下,就涉及到一个问题:图如何切分,以及切分之后的不同子图如何保持彼此的联系构成一个完整的图。图的切分方式有两种:点切分和边切分。在Graphx中,采用点切分。 在GraphX中,Graph类除了表示点的VertexRDD和表示边的EdgeRDD外,还有一个将点的属性和边的属性都包含在内的RDD[EdgeTriplet]。方便起见,我们先从GraphLoader中来看看如何从一个用边来描述图的文件中如何构建Graph的。123456789101112131415161718192021def edgeListFile( sc: SparkContext, path: String, canonicalOrientation: Boolean = false, numEdgePartitions: Int = -1, edgeStorageLevel: StorageLevel = StorageLevel.MEMORY_ONLY, vertexStorageLevel: StorageLevel = StorageLevel.MEMORY_ONLY) : Graph[Int, Int] = { // Parse the edge data table directly into edge partitions val lines = ... ... val edges = lines.mapPartitionsWithIndex { (pid, iter) => ... ... Iterator((pid, builder.toEdgePartition)) }.persist(edgeStorageLevel).setName("GraphLoader.edgeListFile - edges (%s)".format(path)) edges.count() GraphImpl.fromEdgePartitions(edges, defaultVertexAttr = 1, edgeStorageLevel = edgeStorageLevel, vertexStorageLevel = vertexStorageLevel) } // end of edgeListFile 从上面精简的代码中可以看出来,先得到lines一个表示边的RDD(这里所谓的边依旧是文本描述的),然后再经过一系列的转换来生成Graph。 EdgeRDDGraphImpl.fromEdgePartitions中传入的第一个参数edges为EdgeRDD的EdgePartition。先来看看EdgePartition究竟为何物。1234567891011class EdgePartition[ @specialized(Char, Int, Boolean, Byte, Long, Float, Double) ED: ClassTag, VD: ClassTag]( localSrcIds: Array[Int], localDstIds: Array[Int], data: Array[ED], index: GraphXPrimitiveKeyOpenHashMap[VertexId, Int], global2local: GraphXPrimitiveKeyOpenHashMap[VertexId, Int], local2global: Array[VertexId], vertexAttrs: Array[VD], activeSet: Option[VertexSet]) extends Serializable { 其中:localSrcIds 为本地边的源点的本地编号。localDstIds 为本地边的目的点的本地编号,与localSrcIds一一对应成边的两个点。data 为边的属性值。index 为本地边的源点全局ID到localSrcIds中下标的映射。global2local 为点的全局ID到本地ID的映射。local2global 是一个Vector,依次存储了本地出现的点,包括跨节点的点。通过这样的方式做到了点切割。有了EdgePartition之后,再通过得到EdgeRDD就容易了。 VertexRDD现在看fromEdgePartitions12345678def fromEdgePartitions[VD: ClassTag, ED: ClassTag]( edgePartitions: RDD[(PartitionID, EdgePartition[ED, VD])], defaultVertexAttr: VD, edgeStorageLevel: StorageLevel, vertexStorageLevel: StorageLevel): GraphImpl[VD, ED] = { fromEdgeRDD(EdgeRDD.fromEdgePartitions(edgePartitions), defaultVertexAttr, edgeStorageLevel, vertexStorageLevel)} fromEdgePartitions 中调用了 fromEdgeRDD1234567891011private def fromEdgeRDD[VD: ClassTag, ED: ClassTag]( edges: EdgeRDDImpl[ED, VD], defaultVertexAttr: VD, edgeStorageLevel: StorageLevel, vertexStorageLevel: StorageLevel): GraphImpl[VD, ED] = { val edgesCached = edges.withTargetStorageLevel(edgeStorageLevel).cache() val vertices = VertexRDD.fromEdges(edgesCached, edgesCached.partitions.length, defaultVertexAttr) .withTargetStorageLevel(vertexStorageLevel) fromExistingRDDs(vertices, edgesCached)} 可见,VertexRDD是由EdgeRDD生成的。接下来讲解怎么从EdgeRDD生成VertexRDD。1234567891011121314151617181920212223def fromEdges[VD: ClassTag]( edges: EdgeRDD[_], numPartitions: Int, defaultVal: VD): VertexRDD[VD] = { val routingTables = createRoutingTables(edges, new HashPartitioner(numPartitions)) val vertexPartitions = routingTables.mapPartitions({ routingTableIter => val routingTable = if (routingTableIter.hasNext) routingTableIter.next() else RoutingTablePartition.empty Iterator(ShippableVertexPartition(Iterator.empty, routingTable, defaultVal)) }, preservesPartitioning = true) new VertexRDDImpl(vertexPartitions) } private[graphx] def createRoutingTables( edges: EdgeRDD[_], vertexPartitioner: Partitioner): RDD[RoutingTablePartition] = { // Determine which vertices each edge partition needs by creating a mapping from vid to pid. val vid2pid = edges.partitionsRDD.mapPartitions(_.flatMap( Function.tupled(RoutingTablePartition.edgePartitionToMsgs))) .setName("VertexRDD.createRoutingTables - vid2pid (aggregation)") val numEdgePartitions = edges.partitions.length vid2pid.partitionBy(vertexPartitioner).mapPartitions( iter => Iterator(RoutingTablePartition.fromMsgs(numEdgePartitions, iter)), preservesPartitioning = true) } 从代码中可以看到先创建了一个路由表,这个路由表的本质依旧是RDD,然后通过路由表的转得到RDD[ShippableVertexPartition],最后再构造出VertexRDD。先讲解一下路由表,每一条边都有两个点,一个源点,一个终点。在构造路由表时,源点标记位或1,目标点标记位或2,并结合边的partitionID编码成一个Int(高2位表示源点终点,低30位表示边的partitionID)。再根据这个编码的Int反解出ShippableVertexPartition。值得注意的是,在createRoutingTables中,反解生成ShippableVertexPartition过程中根据点的id hash值partition了一次,这样,相同的点都在一个分区了。有意思的地方来了:我以为这样之后就会把点和这个点的镜像合成一个,然而实际上并没有。点和边是相互关联的,通过边生成点,通过点能找到边,如果合并了点和点的镜像,那也找不到某些边了。ShippableVertexPartition依旧以边的区分为标准,并记录了点的属性值,源点、终点信息,这样边和边的点,都在一个分区上。最终,通过new VertexRDDImpl(vertexPartitions)生成VertexRDD。 Graph12345def fromExistingRDDs[VD: ClassTag, ED: ClassTag]( vertices: VertexRDD[VD], edges: EdgeRDD[ED]): GraphImpl[VD, ED] = { new GraphImpl(vertices, new ReplicatedVertexView(edges.asInstanceOf[EdgeRDDImpl[ED, VD]])) } 在fromExistingRDDs调用new GraphImpl(vertices, new ReplicatedVertexView(edges.asInstanceOf[EdgeRDDImpl[ED, VD]]))来生成图。1234class ReplicatedVertexView[VD: ClassTag, ED: ClassTag]( var edges: EdgeRDDImpl[ED, VD], var hasSrcId: Boolean = false, var hasDstId: Boolean = false) ReplicatedVertexView是边和图的视图,当点的属性发生改变时,将改变传输到对应的边。]]></content>
</entry>
<entry>
<title><![CDATA[Flume介绍]]></title>
<url>%2F2016%2F10%2F06%2Fflume-introduce%2F</url>
<content type="text"><![CDATA[声明我对Flume的研究并不深,这一篇文章来源于2016年3月的某一个下午对Flume的调研,仅有一个下午,所以可能有一些观点是不对的。另外,文章很多内容来源于一些大神的博文,当时匆匆没有记录引用来源。所以,如果有人可以发现本文的错误,以及引用的文章,还请在留言中指出。万分感谢。 Flume OGFlume OG:Flume Original Generation,初代Flume。由三种角色构成:代理点(agent)、收集节点(collector)、主节点(master) agent 从各个数据源收集日志数据,将收集到的数据集中到 collector,然后由收集节点汇总存入 hdfs。 master 负责管理 agent,collector 的活动。 agent、collector 都称为 node,node 的角色根据配置的不同分为 logical node(逻辑节点)、physical node(物理节点)。对 logical nodes 和 physical nodes 的区分、配置、使用一直以来都是使用者最头疼的地方。 agent、collector由Source、Sink组成,当前节点的数据是从Source传送到Sink的。 Flume NGFlume NG:Flume New Generation NG只有一种角色节点:代理点(agent)。 没有collector、master节点。这是核心组件最核心的变化。 去除了 physical nodes、logical nodes 的概念和相关内容。 agent 节点的组成也发生了变化。NG agent 由 source、sink、channel 组成。 NG删减了角色,脱离了对Zookeeper的依赖 Flume NG分析基本概念 Event:一个数据单元,带有一个可选的消息头。 Flow:Event从源点到达目的点的迁移的抽象。 Client:操作位于源点处的Event,将其发送到Flume Agent。 Agent:一个独立的Flume进程,包含组件Source、Channel、Sink。 Source:用来消费传递到该组件的Event ,存入channel中。 Channel:中转Event的一个临时存储,保存有Source组件传递过来的Event。 Sink:从Channel中读取并移除Event,将Event传递到Flow Pipeline中的下一个Agent(如果有的话)。数据流:Flume 的核心是把数据从数据源收集过来,再送到目的地。为了保证输送一定成功,在送到目的地之前,会先缓存数据,待数据真正到达目的地后,删除自己缓存的数据:当sink写入失败后,可以自动重启,不会造成数据丢失,因此很可靠。Flume 传输的数据的基本单位是 Event,如果是文本文件,通常是一行记录,这也是事务的基本单位。Event 从 Source,流向 Channel,再到 Sink,本身为一个 byte 数组,并可携带 headers 信息。Event 代表着一个数据流的最小完整单元,从外部数据源来,向外部的目的地去。核心组件:Source ExecSource: 以运行 Linux 命令的方式,持续的输出最新的数据,如 tail -F 文件名 指令,在这种方式下,取的文件名必须是指定的。 ExecSource 可以实现对日志的实时收集,但是存在Flume不运行或者指令执行出错时,将无法收集到日志数据,无法保证日志数据的完整性。 SpoolSource: 监测配置的目录下新增的文件,并将文件中的数据读取出来。需要注意两点:拷贝到 spool 目录下的文件不可以再打开编辑;spool 目录下不可包含相应的子目录。SpoolSource无法实现实时的收集数据,但可以设置以分钟的方式分割文件,趋于实时。###ChannelMemory Channel, JDBC Channel , File Channel,Psuedo Transaction Channel。比较常见的是前三种 channel。 MemoryChannel 可以实现高速的吞吐,但是无法保证数据的完整性。 MemoryRecoverChannel 在官方文档的建议上已经建义使用FileChannel来替换。 FileChannel保证数据的完整性与一致性。在具体配置FileChannel时,建议FileChannel设置的目录和程序日志文件保存的目录设成不同的磁盘,以便提高效率。 Sink 可靠性在Flume NG中,可靠性指的是在数据流的传输过程中,保证events的可靠传递。在Flume NG中,所有的events都保存在Agent的Channel中,然后被发送到数据流下一个Agent或者最终的存储服务中。当且仅当它们被保存到下一个Agent的Channel中,或者被保存到最终的存储服务中。这就是Flume 提供数据流中点到点的可靠性保证的最基本的单跳消息语义传递。首先,Agent间的事务交换。Flume使用事务的办法来保证events的可靠传递。Source和Sink分别被封装在事务中,这些事务由保存event的存储提供或者由Channel提供。这就保证了event在数据流的点对点传输中是可靠的。在多级数据流中,如下图,上一级的Sink和下一级的Source都被包含在事务中,保证数据可靠地从一个Channel到另一个Channel转移。 下图A:正常情况下的 events流程。下图B:Agent2 跟central event store失联,Agent2提交的事务失败,将events缓存起来。下图C:重新恢复时,再恢复失联之前的任务以及后续的events发送。 高可用如下图所示,Agent1中,只有要有一个Sink组件可用,events就被传递到下一个组件,如果一个Sink能成功处理Event(事务完成),则会加入到一个Pool中, 否则,则会从Pool中移除,并计算失败次数,设置惩罚因子。所以,如果某一个Flow中某一层的Agent只有一个,或者全部宕机,可能导致这些Events被存储在流水线上最后一个存活节点。]]></content>
</entry>
<entry>
<title><![CDATA[Spark OFF_HEP变迁]]></title>
<url>%2F2016%2F09%2F21%2Fspark-off_heap%2F</url>
<content type="text"><![CDATA[在文章的开头,安利一下我自己的github上的一个项目:AlluxioBlockManager,同时还有我的github上的博客:blog这个项目的作用是替代Spark2.0以前默认的TachyonBlockManager,稍后解释为什么要重新开发AlluxioBlockManager,以及Spark2.0的off_heap。 OFF_HEAPSpark中RDD提供了几种存储级别,不同的存储级别可以带来不同的容错性能,例如 MEMORY_ONLY,MEMORY_ONLY_SER_2…其中,有一种特别的是OFF_HEAPoff_heap的优势在于,在内存有限的条件下,减少不必要的内存消耗,以及频繁的GC问题,提升程序性能。Spark2.0以前,默认的off_heap是Tachyon,当然,你可以通过继承ExternalBlockManager 来实现你自己想要的任何off_heap。这里说Tachyon,是因为Spark默认的TachyonBlockManager开发完成之后,就再也没有更新过,以至于Tachyon升级为Alluxio之后移除不使用的API,导致Spark默认off_heap不可用,这个问题Spark社区和Alluxio社区都有反馈:ALLUXIO-1881 Spark2.0的off_heap从spark2.0开始,社区已经移除默认的TachyonBlockManager以及ExternalBlockManager相关的API:SPARK-12667。那么,问题来了,在Spark2.0中,OFF_HEAP是怎么处理的呢?数据存在哪里?上代码:首先,在StorageLevel里面,不同的存储级别解析成不同的构造函数,从OFF_HEAP的构造函数可以看出来,OFF_HEAP依旧存在。123456789101112131415Object StorageLevel {val NONE = new StorageLevel(false, false, false, false)val DISK_ONLY = new StorageLevel(true, false, false, false)val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)val MEMORY_ONLY = new StorageLevel(false, true, false, true)val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)val OFF_HEAP = new StorageLevel(false, false, true, false) ...... ........} 在org.apache.spark.memory中,有一个MemoryMode,MemoryMode标记了使用ON_HEAP还是OFF_HEAP,在org.apache.spark.storage.memory.MemoryStore中,根据MemoryMode类型来调用不同的存储123456789101112def putBytes[T: ClassTag]( blockId: BlockId, size: Long, memoryMode: MemoryMode, _bytes: () => ChunkedByteBuffer): Boolean = { ............. val entry = new SerializedMemoryEntry[T](bytes, memoryMode, implicitly[ClassTag[T]]) entries.synchronized { entries.put(blockId, entry) } ............. } 再看MemoryStore中存数据的方法:putIteratorAsBytes1234val allocator = memoryMode match { case MemoryMode.ON_HEAP => ByteBuffer.allocate _ case MemoryMode.OFF_HEAP => Platform.allocateDirectBuffer _} 终于找到Spark2.0中off_heap的底层存储了:Platform是利用java unsafe API实现的一个访问off_heap的类。 总结spark2.0 off_heap就是利用java unsafe API实现的内存管理。优点:依然可以减少内存的使用,减少频繁的GC,提高程序性能。缺点:从代码中看到,使用OFF_HEAP并没有备份数据,也不能像alluxio那样保证数据高可用,丢失数据则需要重新计算。]]></content>
</entry>
<entry>
<title><![CDATA[Spark Streaming应用一个越跑越慢的bug]]></title>
<url>%2F2016%2F08%2F24%2Fa-bug-in-streaming-app%2F</url>
<content type="text"><![CDATA[题记:这是我的第一篇技术博文,写得不好请多提意见。然后,感谢张志斌老师,毕业之前张老师帮助我解一些“神奇的bug”,现在毕业一个月,我终于自己开始解自己认为“神奇的bug”。 背景:我需要在spark streaming上做一个窗口的统计功能,但是因为一些原因,不能利用window相关算子。于是,我在driver上保持了一个resultRDD,在DStream内不断地去更新这个resultRDD,包括新信息的统计,和过期信息的剔除。 现象:batchSize设置为1分钟,程序刚开始运行的一天内,每个batch的处理时间都是2秒以下,如下图:运行长时间之后,监控页面如下:(忽略时间戳,为了截图重启了程序)可以看到,每个job都skip了大量的stage,每个stage内,都skip了大量的task。而且有一个很有意思的现象,skip的数量都是递增的。而且,从skip的数字上来看,也很有规律。再注意 job内stage的执行时间,每个job有2个stage,加起来平均2~3秒。但此时,batch的处理时延已经达到了20~30秒。总结一下遇到的问题:我的streaming程序连续运行一周之后,慢了一个数量级,但实际花费在执行上的时间近似不变。到这,我已经认为是一个“神奇的bug”了。 debug:严格的说,batch的处理时间 = 生成执行计划时间 + task调度时间 + 各个stage执行时间在我的场景中,batch的处理时间远高于stage执行时间和。就说明,执行计划生成和task调度花费了大量时间。task调度是yarn负责,开销主要在分发策略和网络开销上,这部分不会太耗时。剩下就是执行计划生成了。在spark中,执行计划是通过RDD的依赖关系来生成DAG,并以此来划分stage生成执行计划,代码就不贴了,大致就是根据RDD的依赖关系递归地深度优先搜索,终止条件就是某个RDD的依赖为空,也就是说搜索到源RDD。了解了DAG的生成原理之后,再回过头来看文章开头说的背景,我们来模拟一下DAG的生成,DStream.foreachRDD,开始计算,假设当前时间为 t,然后t时刻的resultRDD依赖t-1时刻的resultRDD,t-1时刻resultRDD依赖于t-2时刻的resultRDD。。问题的根源找出来了,随着时间的推移,依赖的层次越来越多。最终导致DAG的生成耗费了大量时间。要解决这个问题,就要清除掉resultRDD的依赖关系,如何清除?答案是 checkpoint12345private[spark] def markCheckpointed(): Unit = { clearDependencies() partitions_ = null deps = null} 在checkpoint之后,spark会清空rdd的依赖。至此,“神奇的bug”解决。 至于前面提到的大量skip:DAG生成遍历了rdd的整个历史,但是在DAG具体的执行过程中,会发现某一些stage,task已经被运算过,因此不会再次计算,这样就产生了skip。最后,愿我的未来再不会觉得有“神奇的bug”。]]></content>
</entry>
</search>