-
Notifications
You must be signed in to change notification settings - Fork 0
/
atom.xml
387 lines (287 loc) · 76.4 KB
/
atom.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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Albert Cheng's blog</title>
<subtitle>Code the world</subtitle>
<link href="/atom.xml" rel="self"/>
<link href="http://chengqiangboy.github.io/"/>
<updated>2018-12-12T11:42:09.734Z</updated>
<id>http://chengqiangboy.github.io/</id>
<author>
<name>Albert Cheng</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>让关系型数据库查询再飞一会儿</title>
<link href="http://chengqiangboy.github.io/2018/11/30/%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/"/>
<id>http://chengqiangboy.github.io/2018/11/30/让关系型数据库查询再飞一会儿/</id>
<published>2018-11-30T11:39:21.000Z</published>
<updated>2018-12-12T11:42:09.734Z</updated>
<content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>有一个系统的业务正在膨胀中,某一些报表(报表数据在mysql中)数据量增长比较厉害,报表页面已经处于<code>卡爆了</code>的状态。中间经过mysql本身的优化,已经到了当前系统架构+存储模型的瓶颈。本文提供一种优化思路,抛砖引玉。<br><a id="more"></a></p>
<h1 id="任务分析"><a href="#任务分析" class="headerlink" title="任务分析"></a>任务分析</h1><p>以一条sql的优化为例(这条sql里面的字段随便改了改,不保证正确性)。<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div></pre></td><td class="code"><pre><div class="line">SELECT d.col, COUNT(DISTINCT risk.inst_id) AS `count`</div><div class="line">FROM risk</div><div class="line"> INNER JOIN d</div><div class="line"> ON d.inst_id = risk.inst_id</div><div class="line"> AND d.id = risk.id</div><div class="line"> INNER JOIN b</div><div class="line"> ON b.business_key = d.id</div><div class="line"> AND d.type = b.type</div><div class="line"> INNER JOIN r</div><div class="line"> ON risk.inst_id = r.inst_id</div><div class="line"> AND risk.id = r.id</div><div class="line">WHERE (r.visit_time >= '2018-10-27 00:00:00'</div><div class="line"> AND r.visit_time <= '2018-11-28 15:54:40'</div><div class="line"> AND d.id = '22821111115042'</div><div class="line"> AND b.business_key = concat('22821111115042', ''))</div><div class="line">GROUP BY d.col</div></pre></td></tr></table></figure></p>
<p>其中,risk表大小112MB,d表大小为9.5GB,b表208KB,r表大小为4.2GB。这个报表的生成逻辑中含有较多inner join。经过一些列的索引优化之后,该条sql的查询时间是36s,前端体验仍然不是很好,且随着报表时间范围的拉长,用户数据量的增长,查询时间会持续恶化。<br>这里就不讨论更改表结构、迁移数据来优化查询了。</p>
<h1 id="优化思路"><a href="#优化思路" class="headerlink" title="优化思路"></a>优化思路</h1><p>本身没有太多技术难度,但中间经过一段时间的摸索,直接说结论吧,希望对有需要的同学带来便利。<br>用SparkSQL分布式计算的能力来加速查询,SparkSQL原生支持通过jdbc连接外部存储。<br>首先,尝试了直接在sparksql的jdbc连接中执行上述sql,结果在意料之中,36秒左右。通过spark监控页面看到,该任务task数量为1,没有并发起来,SparkSQL将查询完全下推给mysql执行。<br>那么问题来了,如何提升并发度呢?<br>根据官方文档,使用jdbc连接有这么几个可用参数,这些参数的含义参考附录链接。<br><code>numPartitions</code>,<code>partitionColumn</code>,<code>lowerBound</code>,<code>upperBound</code><br>值得注意的是,<code>partitionColumn</code> 必须为数值类型,日期或者时间戳。<code>lowerBound</code>和<code>upperBound</code>必须为数字。在上面的case中,我们可以对r表的visit_time进行分区,并根据范围设置上下界线。(时间戳转化成long型)<br>分别在SparkSQL load这4张表,其中对r表的visit_time进行分区,并分别在SparkSQL中注册临时表,在SparkSQL内执行上述SQL,上述SQL执行时间由36s降低到12s,如果调调SparkSQL的参数,性能可能会更好。<br>这个方法从理论上来说,适用于任何单机关系型数据库。</p>
<h1 id="原理简单剖析"><a href="#原理简单剖析" class="headerlink" title="原理简单剖析"></a>原理简单剖析</h1><p>这里是将SparkSQL作为一个分布式查询引擎,mysql作为SparkSQL的一种数据源。SparkSQL内部有高度的统一抽象(DataFrame/DataSet)。SparkSQL从mysql中抽取数据然后根据自身的逻辑来进行运算。如果对细节感兴趣可以参考链接2。</p>
<h1 id="参考文档"><a href="#参考文档" class="headerlink" title="参考文档"></a>参考文档</h1><p>[1] <a href="http://spark.apache.org/docs/latest/sql-data-sources-jdbc.html" rel="external nofollow noopener noreferrer" target="_blank">http://spark.apache.org/docs/latest/sql-data-sources-jdbc.html</a><br>[2] <a href="http://spark.apache.org/docs/latest/sql-programming-guide.html" rel="external nofollow noopener noreferrer" target="_blank">http://spark.apache.org/docs/latest/sql-programming-guide.html</a></p>
]]></content>
<summary type="html">
<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>有一个系统的业务正在膨胀中,某一些报表(报表数据在mysql中)数据量增长比较厉害,报表页面已经处于<code>卡爆了</code>的状态。中间经过mysql本身的优化,已经到了当前系统架构+存储模型的瓶颈。本文提供一种优化思路,抛砖引玉。<br>
</summary>
<category term="spark相关" scheme="http://chengqiangboy.github.io/categories/spark%E7%9B%B8%E5%85%B3/"/>
<category term="调优" scheme="http://chengqiangboy.github.io/tags/%E8%B0%83%E4%BC%98/"/>
<category term="spark" scheme="http://chengqiangboy.github.io/tags/spark/"/>
</entry>
<entry>
<title>让Spark MLlib的预测性能再飞一会儿</title>
<link href="http://chengqiangboy.github.io/2018/05/02/%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/"/>
<id>http://chengqiangboy.github.io/2018/05/02/让Spark-MLlib的预测性能再飞一会儿/</id>
<published>2018-05-02T09:14:21.000Z</published>
<updated>2018-06-23T09:30:57.483Z</updated>
<content type="html"><![CDATA[<h1 id="背景介绍"><a href="#背景介绍" class="headerlink" title="背景介绍"></a>背景介绍</h1><p>我们的系统有一小部分机器学习模型识别需求,因为种种原因,最终选用了Spark MLlib来进行训练和预测。MLlib的Pipeline设计很好地契合了一个机器学习流水线,在模型训练和效果验证阶段,pipeline可以简化开发流程,然而在预测阶段,MLlib pipeline的表现有点差强人意。</p>
<a id="more"></a>
<h1 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h1><p>某个模型的输入为一个字符串,假设长度为N,在我们的场景里面这个N一般不会大于10。特征也很简单,对于每一个输入,可以在O(N)的时间计算出特征向量,分类器选用的是随机森林。<br>对于这样的预测任务,直观上感觉应该非常快,初步估计10ms以内出结果。但是通MLlib pipeline的transform预测结果预测时,性能在86ms左右(2000条query平均响应时间)。而且,query和query之间在输入相同的情况下,也存在响应时间波动的问题。</p>
<h1 id="预测性能优化"><a href="#预测性能优化" class="headerlink" title="预测性能优化"></a>预测性能优化</h1><p>先说说响应时间波动的问题,每一条query的输入都是一样的,也就排除了特征加工时的计算量波动的问题,因为整个计算中消耗内存极少,且测试时内存足够,因为也排除gc导致预测性能抖动的问题。那么剩下的只有Spark了,Spark可能在做某些事情导致了预测性能抖动。通过查看log信息,可以印证这个观点。<br><img src="https://upload-images.jianshu.io/upload_images/2838375-267b2ad4c04beade.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="log"><br>从日志中截取了一小段,里面有大量的清理broadcast变量信息。这也为后续性能优化提供了一个方向。(下面会有部分MLlib源码,源码基于Spark2.3)</p>
<p>在MLlib中,是调用PipelineModel的transform方法进行预测,该方法会调用pipeline的每一个stage内的Transformer的transform方法来对输入的DataFrame/DataSet进行转换。<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">@Since("2.0.0")</div><div class="line"> override def transform(dataset: Dataset[_]): DataFrame = {</div><div class="line"> transformSchema(dataset.schema, logging = true)</div><div class="line"> stages.foldLeft(dataset.toDF)((cur, transformer) => transformer.transform(cur))</div><div class="line"> }</div></pre></td></tr></table></figure></p>
<p>下面,我们先看看训练好的随机森林模型(RandomForestClassificationModel)在预测时做了些什么吧<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div></pre></td><td class="code"><pre><div class="line">override protected def transformImpl(dataset: Dataset[_]): DataFrame = {</div><div class="line"> val bcastModel = dataset.sparkSession.sparkContext.broadcast(this)</div><div class="line"> val predictUDF = udf { (features: Any) =></div><div class="line"> bcastModel.value.predict(features.asInstanceOf[Vector])</div><div class="line"> }</div><div class="line"> dataset.withColumn($(predictionCol), predictUDF(col($(featuresCol))))</div><div class="line"> }</div></pre></td></tr></table></figure></p>
<p>重点来了,终于找到前面说的broadcast的’罪魁祸’了,每次预测时,MLlib都会把模型广播到集群。这样做的好处是方便批处理,但对于小计算量,压根不需要集群的预测场景这样的做法就有点浪费资源了:</p>
<ol>
<li>每次预测都广播显然太多余。</li>
<li>因为每次都广播,所以之前的广播变量也会逐渐回收,在回收时,又反过来影响预测的性能。<h2 id="解决办法"><a href="#解决办法" class="headerlink" title="解决办法"></a>解决办法</h2>从上述代码中可以看到,RandomForestClassificationModel 预测最根本的地方是在于调用predict方法,输入是一个Vector。看看predict干了什么<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div></pre></td><td class="code"><pre><div class="line">override protected def predict(features: FeaturesType): Double = {</div><div class="line"> raw2prediction(predictRaw(features))</div><div class="line"> }</div></pre></td></tr></table></figure>
</li>
</ol>
<p>predict分为两步走:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div></pre></td><td class="code"><pre><div class="line">override protected def predictRaw(features: Vector): Vector = {</div><div class="line"> // TODO: When we add a generic Bagging class, handle transform there: SPARK-7128</div><div class="line"> // Classifies using majority votes.</div><div class="line"> // Ignore the tree weights since all are 1.0 for now.</div><div class="line"> val votes = Array.fill[Double](numClasses)(0.0)</div><div class="line"> _trees.view.foreach { tree =></div><div class="line"> val classCounts: Array[Double] = tree.rootNode.predictImpl(features).impurityStats.stats</div><div class="line"> val total = classCounts.sum</div><div class="line"> if (total != 0) {</div><div class="line"> var i = 0</div><div class="line"> while (i < numClasses) {</div><div class="line"> votes(i) += classCounts(i) / total</div><div class="line"> i += 1</div><div class="line"> }</div><div class="line"> }</div><div class="line"> }</div><div class="line"> Vectors.dense(votes)</div><div class="line"> }</div><div class="line"></div><div class="line"> override protected def raw2probabilityInPlace(rawPrediction: Vector): Vector = {</div><div class="line"> rawPrediction match {</div><div class="line"> case dv: DenseVector =></div><div class="line"> ProbabilisticClassificationModel.normalizeToProbabilitiesInPlace(dv)</div><div class="line"> dv</div><div class="line"> case sv: SparseVector =></div><div class="line"> throw new RuntimeException("Unexpected error in RandomForestClassificationModel:" +</div><div class="line"> " raw2probabilityInPlace encountered SparseVector")</div><div class="line"> }</div><div class="line"> }</div></pre></td></tr></table></figure></p>
<p>这两个方法的输入和输出均为vector,那么我们如果把这两个方法反射出来直接用在预测的特征向量上是不是就可以了?答案是肯定的。<br>注意其中的<code>raw2probability</code>在Spark2.3中的RandomForestClassificationModel中,签名变为了<code>raw2probabilityInPlace</code></p>
<h2 id="全面绕开pipeline"><a href="#全面绕开pipeline" class="headerlink" title="全面绕开pipeline"></a>全面绕开pipeline</h2><p>前面解决了分类器预测的性能问题,另外一个问题就来了。输入的特征向量怎么来呢?在一个MLlib Pipeline流程中,分类器预测只是最后一步,前面还有多种多样的特征加工节点。我尝试了将一个pipeline拆解成两个,一个用于特征加工,一个用于分类预测。用第一个pipeline加工特征,只绕开第二个,性能显然是提升了,但还没达到预期效果。于是,我有了另外一个想法:全面绕开pipeline,对pipeline的每一步,都避免调用原生transform接口。这样的弊端就是,必须重写pipeline的每一步预测方法,然后人肉还原pipeline的预测流程。流程大致跟上面类似。<br>例如:OneHot(说句题外话,这东西在Spark2.3之前的版本是有bug的,详情参考官方文档)。<br>OneHotEncoderModel的transform方法如下:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div></pre></td><td class="code"><pre><div class="line">@Since("2.3.0")</div><div class="line"> override def transform(dataset: Dataset[_]): DataFrame = {</div><div class="line"> val transformedSchema = transformSchema(dataset.schema, logging = true)</div><div class="line"> val keepInvalid = $(handleInvalid) == OneHotEncoderEstimator.KEEP_INVALID</div><div class="line"></div><div class="line"> val encodedColumns = $(inputCols).indices.map { idx =></div><div class="line"> val inputColName = $(inputCols)(idx)</div><div class="line"> val outputColName = $(outputCols)(idx)</div><div class="line"></div><div class="line"> val outputAttrGroupFromSchema =</div><div class="line"> AttributeGroup.fromStructField(transformedSchema(outputColName))</div><div class="line"></div><div class="line"> val metadata = if (outputAttrGroupFromSchema.size < 0) {</div><div class="line"> OneHotEncoderCommon.createAttrGroupForAttrNames(outputColName,</div><div class="line"> categorySizes(idx), $(dropLast), keepInvalid).toMetadata()</div><div class="line"> } else {</div><div class="line"> outputAttrGroupFromSchema.toMetadata()</div><div class="line"> }</div><div class="line"></div><div class="line"> encoder(col(inputColName).cast(DoubleType), lit(idx))</div><div class="line"> .as(outputColName, metadata)</div><div class="line"> }</div><div class="line"> dataset.withColumns($(outputCols), encodedColumns)</div><div class="line"> }</div></pre></td></tr></table></figure></p>
<p>里面对feature进行转换的关键代码行是 encoder…<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div><div class="line">24</div><div class="line">25</div><div class="line">26</div><div class="line">27</div><div class="line">28</div><div class="line">29</div><div class="line">30</div><div class="line">31</div><div class="line">32</div><div class="line">33</div><div class="line">34</div><div class="line">35</div></pre></td><td class="code"><pre><div class="line">private def encoder: UserDefinedFunction = {</div><div class="line"> val keepInvalid = getHandleInvalid == OneHotEncoderEstimator.KEEP_INVALID</div><div class="line"> val configedSizes = getConfigedCategorySizes</div><div class="line"> val localCategorySizes = categorySizes</div><div class="line"></div><div class="line"> // The udf performed on input data. The first parameter is the input value. The second</div><div class="line"> // parameter is the index in inputCols of the column being encoded.</div><div class="line"> udf { (label: Double, colIdx: Int) =></div><div class="line"> val origCategorySize = localCategorySizes(colIdx)</div><div class="line"> // idx: index in vector of the single 1-valued element</div><div class="line"> val idx = if (label >= 0 && label < origCategorySize) {</div><div class="line"> label</div><div class="line"> } else {</div><div class="line"> if (keepInvalid) {</div><div class="line"> origCategorySize</div><div class="line"> } else {</div><div class="line"> if (label < 0) {</div><div class="line"> throw new SparkException(s"Negative value: $label. Input can't be negative. " +</div><div class="line"> s"To handle invalid values, set Param handleInvalid to " +</div><div class="line"> s"${OneHotEncoderEstimator.KEEP_INVALID}")</div><div class="line"> } else {</div><div class="line"> throw new SparkException(s"Unseen value: $label. To handle unseen values, " +</div><div class="line"> s"set Param handleInvalid to ${OneHotEncoderEstimator.KEEP_INVALID}.")</div><div class="line"> }</div><div class="line"> }</div><div class="line"> }</div><div class="line"></div><div class="line"> val size = configedSizes(colIdx)</div><div class="line"> if (idx < size) {</div><div class="line"> Vectors.sparse(size, Array(idx.toInt), Array(1.0))</div><div class="line"> } else {</div><div class="line"> Vectors.sparse(size, Array.empty[Int], Array.empty[Double])</div><div class="line"> }</div><div class="line"> }</div><div class="line"> }</div></pre></td></tr></table></figure></p>
<p>encoder里面关键的是这个udf,将其抠出重写之后直接作用于特征向量。</p>
<h2 id="效果"><a href="#效果" class="headerlink" title="效果"></a>效果</h2><p>经过测试,全面绕开pipeline之后,响应时间下降到16ms左右。(2000条query平均响应时间),且不再有抖动。</p>
]]></content>
<summary type="html">
<h1 id="背景介绍"><a href="#背景介绍" class="headerlink" title="背景介绍"></a>背景介绍</h1><p>我们的系统有一小部分机器学习模型识别需求,因为种种原因,最终选用了Spark MLlib来进行训练和预测。MLlib的Pipeline设计很好地契合了一个机器学习流水线,在模型训练和效果验证阶段,pipeline可以简化开发流程,然而在预测阶段,MLlib pipeline的表现有点差强人意。</p>
</summary>
<category term="spark相关" scheme="http://chengqiangboy.github.io/categories/spark%E7%9B%B8%E5%85%B3/"/>
<category term="调优" scheme="http://chengqiangboy.github.io/tags/%E8%B0%83%E4%BC%98/"/>
<category term="spark" scheme="http://chengqiangboy.github.io/tags/spark/"/>
<category term="MLlib" scheme="http://chengqiangboy.github.io/tags/MLlib/"/>
</entry>
<entry>
<title>上帝的骰子游戏</title>
<link href="http://chengqiangboy.github.io/2017/09/25/god-s-probable-game/"/>
<id>http://chengqiangboy.github.io/2017/09/25/god-s-probable-game/</id>
<published>2017-09-25T03:13:49.000Z</published>
<updated>2018-06-23T09:30:57.482Z</updated>
<content type="html"><![CDATA[<p>概率是一个很有意思的东西,通过上帝投掷出来的骰子,你能猜到上帝的意图。<br>这是一篇白话瞎文,并不是特别严谨。<br><a id="more"></a></p>
<h1 id="概率的两大学派"><a href="#概率的两大学派" class="headerlink" title="概率的两大学派"></a>概率的两大学派</h1><p>概率有两大学派:概率学派,贝叶斯学派。<br>“可悲”的是,国内的高等教育应该都是前者吧。之所以说“可悲”,是因为它固化了我们的思想,认为掷骰子的每一面,就是应该概率均等。当然,这个观点也是一己之见,欢迎来辩。</p>
<h1 id="一个例子"><a href="#一个例子" class="headerlink" title="一个例子"></a>一个例子</h1><p>月初出去outing,我拿着一个小的行李箱,密码姑且假设为000。意外发生了,我在某次拨乱密码之后,“000”却打不开箱子了。请问,这个时候应该怎么办?</p>
<h1 id="概率游戏"><a href="#概率游戏" class="headerlink" title="概率游戏"></a>概率游戏</h1><p>概率学派告诉我们,三个0~9的数字组合,一共有1000种,只要遍历这1000种组合,就能打开密码箱了。<br>贝叶斯学派说:扯淡,有先验条件没考虑进去,现在这个未知的密码压根就不是均匀分布的。<br>我们来回顾一下这个概率游戏:之前的密码是000,把密码箱重新上锁拨乱之后,000打不开了,新的密码可能是多少?<br>我回想了一下,我拨乱密码是随手逆时针一拨,每个数字大概率没有转一圈。因此对于每一位的数字的概率分布,并不是均等的,0后面的几个数字比较大,靠近9的概率分布偏小。因此,遍历密码时,并不需要全部遍历。最终,我尝试了几十个之后,就把密码解开了。</p>
]]></content>
<summary type="html">
<p>概率是一个很有意思的东西,通过上帝投掷出来的骰子,你能猜到上帝的意图。<br>这是一篇白话瞎文,并不是特别严谨。<br>
</summary>
<category term="碎碎念" scheme="http://chengqiangboy.github.io/categories/%E7%A2%8E%E7%A2%8E%E5%BF%B5/"/>
<category term="概率" scheme="http://chengqiangboy.github.io/tags/%E6%A6%82%E7%8E%87/"/>
</entry>
<entry>
<title>word2vec在学历造假中的探索</title>
<link href="http://chengqiangboy.github.io/2017/09/15/word2vec-in-background/"/>
<id>http://chengqiangboy.github.io/2017/09/15/word2vec-in-background/</id>
<published>2017-09-15T09:19:26.000Z</published>
<updated>2018-06-23T09:30:57.483Z</updated>
<content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><ol>
<li>如果你想了解word2vec的原理,这篇文章并不适合你,出门右转用google。</li>
<li>这篇文章的东西含金量不高,希望搞NLP,ML,DL的专业人士轻拍。</li>
<li>因为含金量不高,所以有一些诸如数据预处理的一些琐碎的东西,因此比较适合新手村的新手任务。<a id="more"></a>
</li>
</ol>
<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>在我们的系统中,有一处是需要校验一个人提供的学历信息是否真实。系统现有的算法准确率比较高,但是召回率比较低。<br>举一个例子来说明一下学历造假相关背景。以计算机相关专业为例:<br><code>计算机科学与技术</code>是一级学科,<code>计算机应用技术</code>,<code>信息安全</code>,<code>计算机系统结构</code>是二级学科。<code>软件工程</code>现在貌似已经是一级学科?<br>在硕士研究生和博士研究生的授位中,是按照二级学科来区分的。但学计算机的人都懂的,其实都一样。以至于很多人都不知道自己是哪个二级学科的,然后问题就来了,让你填你的毕业专业,你填哪个呢?填错了会不会被认为是学历造假?</p>
<h1 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h1><p>显然,这是一个短文本匹配问题,文本短到仅由两三个词构成。而且,由于专业的局限性,非专业人士基本分不清某个一级学科下面有哪些二级学科。</p>
<h1 id="解决办法"><a href="#解决办法" class="headerlink" title="解决办法"></a>解决办法</h1><ol>
<li>编辑距离,这个算法的缺点明显:<code>计算机科学与技术</code>和<code>信息安全</code>的编辑距离,想想都觉得大,字面上看来一点关系都没有。</li>
<li>word2vec: 借助NLP的东西来计算两个专业之间的相似度,挖掘隐藏信息。</li>
</ol>
<h1 id="基于word2vec的短文本相似度"><a href="#基于word2vec的短文本相似度" class="headerlink" title="基于word2vec的短文本相似度"></a>基于word2vec的短文本相似度</h1><h2 id="语料"><a href="#语料" class="headerlink" title="语料"></a>语料</h2><p>语料我选择的是中文维基百科,下载地址是:<a href="https://dumps.wikimedia.org/zhwiki/latest/zhwiki-latest-pages-articles.xml.bz2" rel="external nofollow noopener noreferrer" target="_blank">https://dumps.wikimedia.org/zhwiki/latest/zhwiki-latest-pages-articles.xml.bz2</a><br>获得语料之后,还需要对语料进行一些预处理:<br>(此处参考了:<a href="http://licstar.net/archives/262)" rel="external nofollow noopener noreferrer" target="_blank">http://licstar.net/archives/262)</a></p>
<ol>
<li>抽取正文文本</li>
<li>繁简转换</li>
</ol>
<h2 id="分词"><a href="#分词" class="headerlink" title="分词"></a>分词</h2><p>中文相关的处理,分词是绕不开的一个步骤,我采用了ICT分词的java版。</p>
<h2 id="word2vec"><a href="#word2vec" class="headerlink" title="word2vec"></a>word2vec</h2><p>我试图用一些线程的word2vec的jar包来直接训练分词后的语料,但找了好几个,内存都爆了。无奈,我只能在spark mllib上手动做了一个。代码就不贴了,很简单,mllib有现成的word2vec算法库。</p>
<p>得到词向量之后,怎么表达成短文的向量呢?<br>我采用了一个简单粗暴的办法:向量叠加。直接将短文本分词后的词向量叠加起来,再用余弦相似度来计算相似度。<br>看一下结果吧:<br><img src="http://upload-images.jianshu.io/upload_images/2838375-e171a3850a480171.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="实验结果对比"><br>其中,相似度A是现在系统跑的算法,相似度B是基于word2vec向量叠加的相似度。<br>可见,word2vec有效地挖掘出来了专业之间的潜在联系。</p>
<h1 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h1><ol>
<li>从上面的图中可以看出,在word2vec中,一级学科和二级学科的相似度显著提升。</li>
<li>软件工程作为一个一级学科,跟计算机科学与技术也有极高的相似度,带来了更大的误导,但其实软件工程作为计算机的相关专业确实相关性极高。</li>
</ol>
<h1 id="未来工作"><a href="#未来工作" class="headerlink" title="未来工作"></a>未来工作</h1><ol>
<li>有比向量叠加更好的点子么?应该有吧,卷积应该是一个不错的选择,但是我还没有想好怎么卷积,毕竟我的场景比较特殊,没有标注好的样本进行训练(因为专业是有限可枚举的,如果有功夫标注的话,我想不需要模型来算相似度了,因此我的场景只是需要一个办法来计算相似度)。如果各位有啥好的点子,还请不吝赐教。</li>
</ol>
<h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><ol>
<li><a href="http://licstar.net/archives/262" rel="external nofollow noopener noreferrer" target="_blank">http://licstar.net/archives/262</a></li>
</ol>
]]></content>
<summary type="html">
<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><ol>
<li>如果你想了解word2vec的原理,这篇文章并不适合你,出门右转用google。</li>
<li>这篇文章的东西含金量不高,希望搞NLP,ML,DL的专业人士轻拍。</li>
<li>因为含金量不高,所以有一些诸如数据预处理的一些琐碎的东西,因此比较适合新手村的新手任务。
</summary>
<category term="NLP" scheme="http://chengqiangboy.github.io/categories/NLP/"/>
<category term="spark" scheme="http://chengqiangboy.github.io/tags/spark/"/>
<category term="word2wec" scheme="http://chengqiangboy.github.io/tags/word2wec/"/>
<category term="nlp" scheme="http://chengqiangboy.github.io/tags/nlp/"/>
<category term="mllib" scheme="http://chengqiangboy.github.io/tags/mllib/"/>
<category term="学历造假" scheme="http://chengqiangboy.github.io/tags/%E5%AD%A6%E5%8E%86%E9%80%A0%E5%81%87/"/>
</entry>
<entry>
<title>变参调用:scala和java的一个不同点</title>
<link href="http://chengqiangboy.github.io/2016/11/11/a-dif-between-scala-and-java/"/>
<id>http://chengqiangboy.github.io/2016/11/11/a-dif-between-scala-and-java/</id>
<published>2016-11-11T07:20:02.000Z</published>
<updated>2018-06-23T09:30:57.481Z</updated>
<content type="html"><![CDATA[<p>scala和java几乎没有区别,可以互相调用。注意这里说的是几乎,总有那么少数,出人意料的惊喜在告诉你,scala就是scala。<br><a id="more"></a></p>
<h1 id="一个例子"><a href="#一个例子" class="headerlink" title="一个例子"></a>一个例子</h1><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div></pre></td><td class="code"><pre><div class="line">import com.alibaba.fastjson.JSON</div><div class="line"></div><div class="line">object test {</div><div class="line"> def main(args: Array[String]) = {</div><div class="line"> val map = new util.HashMap[CharSequence, CharSequence]()</div><div class="line"> map.put("123", "22333")</div><div class="line"> map.put("test", null)</div><div class="line"> val ret = JSON.toJSONString(map)</div><div class="line"> println(ret)</div><div class="line"> }</div><div class="line">}</div></pre></td></tr></table></figure>
<p>如上所示,这个例子很简单,把一个jabva的map转换成json字符串。<br>其中<code>JSON.toJSONString</code>的代码如下:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div></pre></td><td class="code"><pre><div class="line">public static String toJSONString(Object object) {</div><div class="line"> return toJSONString(object, emptyFilters, new SerializerFeature[0]);</div><div class="line">}</div><div class="line"></div><div class="line">public static String toJSONString(Object object, SerializerFeature... features) {</div><div class="line"> return toJSONString(object, DEFAULT_GENERATE_FEATURE, features);</div><div class="line">}</div></pre></td></tr></table></figure></p>
<p>问题来了,上面的测试用例报错了:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div></pre></td><td class="code"><pre><div class="line">ambiguous reference to overloaded definition,both method toJSONString in object JSON of</div><div class="line">type (x$1: Any, x$2: com.alibaba.fastjson.serializer.SerializerFeature*)String</div><div class="line">and method toJSONString in object JSON of</div><div class="line">type (x$1: Any)String </div><div class="line">match argument types (java.util.HashMap[CharSequence,CharSequence])</div><div class="line">val ret = JSON.toJSONString(map)</div></pre></td></tr></table></figure></p>
<p>错误的原因很明显,编译器在编译scala调用java依赖包里面的<code>toJSONString</code>函数时发生了歧义。</p>
<h1 id="scala含有变参的重载函数"><a href="#scala含有变参的重载函数" class="headerlink" title="scala含有变参的重载函数"></a>scala含有变参的重载函数</h1><p>看一段代码:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div></pre></td><td class="code"><pre><div class="line">object Foo {</div><div class="line"> def apply (x1: Int): Int = 2</div><div class="line"> def apply (x1: Int, x2: Int*): Int = 3</div><div class="line"> def apply (x1: Int*): Int = 4</div><div class="line"></div><div class="line">}</div><div class="line"></div><div class="line">object Test11 extends App {</div><div class="line"> Console println Foo(7)</div><div class="line"> Console println Foo(2, Array(3).toSeq: _*)</div><div class="line"> Console println Foo(Array(3, 4).toSeq: _*)</div><div class="line">}</div></pre></td></tr></table></figure></p>
<p>上述代码分别输出:<code>2、3、4</code><br>对于前两个构造函数,刚好对应了文章开头的例子,说明,scala调用类似的scala依赖是没有问题的。<br>我们来注意一下scala如何调用变参:当你使用<code>Foo(2, 3)</code>调用时候,会出现歧义,因为<code>(2, 3)</code>可以匹配到第二个和第三个构造函数。所以,就只能用看起来很奇怪的写法<code>Foo(2, Array(3).toSeq: _*)</code>来进行区分。<code>Array(3).toSeq: _*</code>就是在告诉编译器,这个参数是变参的,别匹配错了。<br>然后,我们来看看Java怎么做的。</p>
<h1 id="java含有变参的重载函数"><a href="#java含有变参的重载函数" class="headerlink" title="java含有变参的重载函数"></a>java含有变参的重载函数</h1><p>代码就不写了,总之,java调用java类似的函数也是没有问题的。<br>那么问题来了,为什么scala调用java类似的函数就有问题呢?要回答这个问题,我们先来看看java在进行重载调用时,编译器都做了些啥?</p>
<blockquote>
<ul>
<li>调用方法时,能与固定参数函数以及可变参数都匹配时,优先调用固定参数方法。</li>
<li>调用方法时,两个变长参数都匹配时,编译无法通过。<figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div></pre></td><td class="code"><pre><div class="line">public class VariVargsTest2 {</div><div class="line"> public static void main(String[] args) {</div><div class="line"> test("hello"); //1</div><div class="line"> }</div><div class="line"> public static void test(String ...args)</div><div class="line"> {</div><div class="line"> System.out.println("变长参数1");</div><div class="line"> }</div><div class="line"> public static void test(String string,String...args)</div><div class="line"> {</div><div class="line"> System.out.println("变长参数2");</div><div class="line"> }</div><div class="line">}</div></pre></td></tr></table></figure>
</li>
</ul>
</blockquote>
<p>当没有1处代码时,程序是能够编译通过的。但是当添加了1处代码后无法通过编译,给出的错误是:<code>The method test(String[]) is ambiguous for the type VariVargsTest2</code>。编译器不知道选取哪个方法。</p>
<h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>从scala和java对含有变参的重载函数处理的方式上,我们可以知道文章开头的代码为什么编译不过。</p>
<ul>
<li>scala首先会自己匹配,自己匹配不了的时候,使用者可以手动来标识变参参数。</li>
<li>java自己匹配,并有一个自己的调用优先级顺序,实在分不清就编译不过了。<br>回到开头的问题,scala调用java出问题了,就应该是scala编译器和java编译器在处理这个问题时的差异导致的吧。</li>
</ul>
<h1 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h1><p>[1] <a href="https://jssyjam.github.io/2016/05/02/Java%E8%AF%AD%E6%B3%95%E7%B3%96%E5%88%9D%E6%8E%A2%EF%BC%88%E4%B8%89%EF%BC%89-%E5%8F%98%E9%95%BF%E5%8F%82%E6%95%B0-md/" rel="external nofollow noopener noreferrer" target="_blank">Java语法糖初探(三)–变长参数</a></p>
]]></content>
<summary type="html">
<p>scala和java几乎没有区别,可以互相调用。注意这里说的是几乎,总有那么少数,出人意料的惊喜在告诉你,scala就是scala。<br>
</summary>
<category term="Scala日常" scheme="http://chengqiangboy.github.io/categories/Scala%E6%97%A5%E5%B8%B8/"/>
<category term="scala" scheme="http://chengqiangboy.github.io/tags/scala/"/>
<category term="变参" scheme="http://chengqiangboy.github.io/tags/%E5%8F%98%E5%8F%82/"/>
</entry>
<entry>
<title>Graphx 源码剖析-图的生成</title>
<link href="http://chengqiangboy.github.io/2016/10/31/graphx/"/>
<id>http://chengqiangboy.github.io/2016/10/31/graphx/</id>
<published>2016-10-31T15:34:52.000Z</published>
<updated>2018-06-23T09:30:57.482Z</updated>
<content type="html"><![CDATA[<p>Graphx的实现代码并不多,这得益于Spark RDD niubility的设计。众所周知,在分布式上做图计算需要考虑点、边的切割。而RDD本身是一个分布式的数据集,所以,做Graphx只需要把边和点用RDD表示出来就可以了。本文就是从这个角度来分析Graphx的运作基本原理(本文基于Spark2.0)。<br><a id="more"></a></p>
<h1 id="分布式图的切割方式"><a href="#分布式图的切割方式" class="headerlink" title="分布式图的切割方式"></a>分布式图的切割方式</h1><p>在单机上图很好表示,在分布式环境下,就涉及到一个问题:图如何切分,以及切分之后的不同子图如何保持彼此的联系构成一个完整的图。图的切分方式有两种:点切分和边切分。在Graphx中,采用点切分。</p>
<p>在GraphX中,<code>Graph</code>类除了表示点的<code>VertexRDD</code>和表示边的<code>EdgeRDD</code>外,还有一个将点的属性和边的属性都包含在内的<code>RDD[EdgeTriplet]</code>。<br>方便起见,我们先从<code>GraphLoader</code>中来看看如何从一个用边来描述图的文件中如何构建<code>Graph</code>的。<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div></pre></td><td class="code"><pre><div class="line">def edgeListFile(</div><div class="line"> sc: SparkContext,</div><div class="line"> path: String,</div><div class="line"> canonicalOrientation: Boolean = false,</div><div class="line"> numEdgePartitions: Int = -1,</div><div class="line"> edgeStorageLevel: StorageLevel = StorageLevel.MEMORY_ONLY,</div><div class="line"> vertexStorageLevel: StorageLevel = StorageLevel.MEMORY_ONLY)</div><div class="line"> : Graph[Int, Int] =</div><div class="line"> {</div><div class="line"></div><div class="line"> // Parse the edge data table directly into edge partitions</div><div class="line"> val lines = ... ...</div><div class="line"> val edges = lines.mapPartitionsWithIndex { (pid, iter) =></div><div class="line"> ... ...</div><div class="line"> Iterator((pid, builder.toEdgePartition))</div><div class="line"> }.persist(edgeStorageLevel).setName("GraphLoader.edgeListFile - edges (%s)".format(path))</div><div class="line"> edges.count()</div><div class="line"></div><div class="line"> GraphImpl.fromEdgePartitions(edges, defaultVertexAttr = 1, edgeStorageLevel = edgeStorageLevel,</div><div class="line"> vertexStorageLevel = vertexStorageLevel)</div><div class="line"> } // end of edgeListFile</div></pre></td></tr></table></figure></p>
<p>从上面精简的代码中可以看出来,先得到<code>lines</code>一个表示边的RDD(这里所谓的边依旧是文本描述的),然后再经过一系列的转换来生成Graph。</p>
<h1 id="EdgeRDD"><a href="#EdgeRDD" class="headerlink" title="EdgeRDD"></a>EdgeRDD</h1><p><code>GraphImpl.fromEdgePartitions</code>中传入的第一个参数<code>edges</code>为<code>EdgeRDD</code>的<code>EdgePartition</code>。先来看看<code>EdgePartition</code>究竟为何物。<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div></pre></td><td class="code"><pre><div class="line">class EdgePartition[</div><div class="line"> @specialized(Char, Int, Boolean, Byte, Long, Float, Double) ED: ClassTag, VD: ClassTag](</div><div class="line"> localSrcIds: Array[Int],</div><div class="line"> localDstIds: Array[Int],</div><div class="line"> data: Array[ED],</div><div class="line"> index: GraphXPrimitiveKeyOpenHashMap[VertexId, Int],</div><div class="line"> global2local: GraphXPrimitiveKeyOpenHashMap[VertexId, Int],</div><div class="line"> local2global: Array[VertexId],</div><div class="line"> vertexAttrs: Array[VD],</div><div class="line"> activeSet: Option[VertexSet])</div><div class="line"> extends Serializable {</div></pre></td></tr></table></figure></p>
<p>其中:<br><code>localSrcIds</code> 为本地边的源点的本地编号。<br><code>localDstIds</code> 为本地边的目的点的本地编号,与<code>localSrcIds</code>一一对应成边的两个点。<br><code>data</code> 为边的属性值。<br><code>index</code> 为本地边的源点全局ID到localSrcIds中下标的映射。<br><code>global2local</code> 为点的全局ID到本地ID的映射。<br><code>local2global</code> 是一个Vector,依次存储了本地出现的点,包括跨节点的点。<br>通过这样的方式做到了点切割。<br>有了<code>EdgePartition</code>之后,再通过得到<code>EdgeRDD</code>就容易了。</p>
<h1 id="VertexRDD"><a href="#VertexRDD" class="headerlink" title="VertexRDD"></a>VertexRDD</h1><p>现在看<code>fromEdgePartitions</code><br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div></pre></td><td class="code"><pre><div class="line">def fromEdgePartitions[VD: ClassTag, ED: ClassTag](</div><div class="line"> edgePartitions: RDD[(PartitionID, EdgePartition[ED, VD])],</div><div class="line"> defaultVertexAttr: VD,</div><div class="line"> edgeStorageLevel: StorageLevel,</div><div class="line"> vertexStorageLevel: StorageLevel): GraphImpl[VD, ED] = {</div><div class="line"> fromEdgeRDD(EdgeRDD.fromEdgePartitions(edgePartitions), defaultVertexAttr, edgeStorageLevel,</div><div class="line"> vertexStorageLevel)</div><div class="line">}</div></pre></td></tr></table></figure></p>
<p><code>fromEdgePartitions</code> 中调用了 <code>fromEdgeRDD</code><br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div></pre></td><td class="code"><pre><div class="line">private def fromEdgeRDD[VD: ClassTag, ED: ClassTag](</div><div class="line"> edges: EdgeRDDImpl[ED, VD],</div><div class="line"> defaultVertexAttr: VD,</div><div class="line"> edgeStorageLevel: StorageLevel,</div><div class="line"> vertexStorageLevel: StorageLevel): GraphImpl[VD, ED] = {</div><div class="line"> val edgesCached = edges.withTargetStorageLevel(edgeStorageLevel).cache()</div><div class="line"> val vertices =</div><div class="line"> VertexRDD.fromEdges(edgesCached, edgesCached.partitions.length, defaultVertexAttr)</div><div class="line"> .withTargetStorageLevel(vertexStorageLevel)</div><div class="line"> fromExistingRDDs(vertices, edgesCached)</div><div class="line">}</div></pre></td></tr></table></figure></p>
<p>可见,<code>VertexRDD</code>是由<code>EdgeRDD</code>生成的。接下来讲解怎么从<code>EdgeRDD</code>生成<code>VertexRDD</code>。<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div><div class="line">16</div><div class="line">17</div><div class="line">18</div><div class="line">19</div><div class="line">20</div><div class="line">21</div><div class="line">22</div><div class="line">23</div></pre></td><td class="code"><pre><div class="line">def fromEdges[VD: ClassTag](</div><div class="line"> edges: EdgeRDD[_], numPartitions: Int, defaultVal: VD): VertexRDD[VD] = {</div><div class="line"> val routingTables = createRoutingTables(edges, new HashPartitioner(numPartitions))</div><div class="line"> val vertexPartitions = routingTables.mapPartitions({ routingTableIter =></div><div class="line"> val routingTable =</div><div class="line"> if (routingTableIter.hasNext) routingTableIter.next() else RoutingTablePartition.empty</div><div class="line"> Iterator(ShippableVertexPartition(Iterator.empty, routingTable, defaultVal))</div><div class="line"> }, preservesPartitioning = true)</div><div class="line"> new VertexRDDImpl(vertexPartitions)</div><div class="line"> }</div><div class="line"></div><div class="line"> private[graphx] def createRoutingTables(</div><div class="line"> edges: EdgeRDD[_], vertexPartitioner: Partitioner): RDD[RoutingTablePartition] = {</div><div class="line"> // Determine which vertices each edge partition needs by creating a mapping from vid to pid.</div><div class="line"> val vid2pid = edges.partitionsRDD.mapPartitions(_.flatMap(</div><div class="line"> Function.tupled(RoutingTablePartition.edgePartitionToMsgs)))</div><div class="line"> .setName("VertexRDD.createRoutingTables - vid2pid (aggregation)")</div><div class="line"></div><div class="line"> val numEdgePartitions = edges.partitions.length</div><div class="line"> vid2pid.partitionBy(vertexPartitioner).mapPartitions(</div><div class="line"> iter => Iterator(RoutingTablePartition.fromMsgs(numEdgePartitions, iter)),</div><div class="line"> preservesPartitioning = true)</div><div class="line"> }</div></pre></td></tr></table></figure></p>
<p>从代码中可以看到先创建了一个路由表,这个路由表的本质依旧是RDD,然后通过路由表的转得到<code>RDD[ShippableVertexPartition]</code>,最后再构造出<code>VertexRDD</code>。先讲解一下路由表,每一条边都有两个点,一个源点,一个终点。在构造路由表时,源点标记位或1,目标点标记位或2,并结合边的partitionID编码成一个Int(高2位表示源点终点,低30位表示边的partitionID)。再根据这个编码的Int反解出<code>ShippableVertexPartition</code>。值得注意的是,在<code>createRoutingTables</code>中,反解生成<code>ShippableVertexPartition</code>过程中根据点的id hash值partition了一次,这样,相同的点都在一个分区了。有意思的地方来了:我以为这样之后就会把点和这个点的镜像合成一个,然而实际上并没有。点和边是相互关联的,通过边生成点,通过点能找到边,如果合并了点和点的镜像,那也找不到某些边了。<code>ShippableVertexPartition</code>依旧以边的区分为标准,并记录了点的属性值,源点、终点信息,这样边和边的点,都在一个分区上。<br>最终,通过<code>new VertexRDDImpl(vertexPartitions)</code>生成<code>VertexRDD</code>。</p>
<h1 id="Graph"><a href="#Graph" class="headerlink" title="Graph"></a>Graph</h1><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">def fromExistingRDDs[VD: ClassTag, ED: ClassTag](</div><div class="line"> vertices: VertexRDD[VD],</div><div class="line"> edges: EdgeRDD[ED]): GraphImpl[VD, ED] = {</div><div class="line"> new GraphImpl(vertices, new ReplicatedVertexView(edges.asInstanceOf[EdgeRDDImpl[ED, VD]]))</div><div class="line"> }</div></pre></td></tr></table></figure>
<p>在<code>fromExistingRDDs</code>调用<code>new GraphImpl(vertices, new ReplicatedVertexView(edges.asInstanceOf[EdgeRDDImpl[ED, VD]]))</code>来生成图。<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div></pre></td><td class="code"><pre><div class="line">class ReplicatedVertexView[VD: ClassTag, ED: ClassTag](</div><div class="line"> var edges: EdgeRDDImpl[ED, VD],</div><div class="line"> var hasSrcId: Boolean = false,</div><div class="line"> var hasDstId: Boolean = false)</div></pre></td></tr></table></figure></p>
<p><code>ReplicatedVertexView</code>是边和图的视图,当点的属性发生改变时,将改变传输到对应的边。</p>
]]></content>
<summary type="html">
<p>Graphx的实现代码并不多,这得益于Spark RDD niubility的设计。众所周知,在分布式上做图计算需要考虑点、边的切割。而RDD本身是一个分布式的数据集,所以,做Graphx只需要把边和点用RDD表示出来就可以了。本文就是从这个角度来分析Graphx的运作基本原理(本文基于Spark2.0)。<br>
</summary>
<category term="spark相关" scheme="http://chengqiangboy.github.io/categories/spark%E7%9B%B8%E5%85%B3/"/>
<category term="spark" scheme="http://chengqiangboy.github.io/tags/spark/"/>
<category term="graphx" scheme="http://chengqiangboy.github.io/tags/graphx/"/>
</entry>
<entry>
<title>Flume介绍</title>
<link href="http://chengqiangboy.github.io/2016/10/06/flume-introduce/"/>
<id>http://chengqiangboy.github.io/2016/10/06/flume-introduce/</id>
<published>2016-10-06T15:43:17.000Z</published>
<updated>2018-06-23T09:30:57.482Z</updated>
<content type="html"><![CDATA[<h1 id="声明"><a href="#声明" class="headerlink" title="声明"></a>声明</h1><p>我对Flume的研究并不深,这一篇文章来源于2016年3月的某一个下午对Flume的调研,仅有一个下午,所以可能有一些观点是不对的。另外,文章很多内容来源于一些大神的博文,当时匆匆没有记录引用来源。所以,如果有人可以发现本文的错误,以及引用的文章,还请在留言中指出。万分感谢。<br><a id="more"></a></p>
<h1 id="Flume-OG"><a href="#Flume-OG" class="headerlink" title="Flume OG"></a>Flume OG</h1><p>Flume OG:Flume Original Generation,初代Flume。<br>由三种角色构成:代理点(agent)、收集节点(collector)、主节点(master)</p>
<ul>
<li>agent 从各个数据源收集日志数据,将收集到的数据集中到 collector,然后由收集节点汇总存入 hdfs。</li>
<li>master 负责管理 agent,collector 的活动。</li>
<li>agent、collector 都称为 node,node 的角色根据配置的不同分为 logical node(逻辑节点)、physical node(物理节点)。对 logical nodes 和 physical nodes 的区分、配置、使用一直以来都是使用者最头疼的地方。</li>
<li>agent、collector由Source、Sink组成,当前节点的数据是从Source传送到Sink的。<br><img src="/images/flume-og-01.png" alt="flume-og-01.png"></li>
</ul>
<h1 id="Flume-NG"><a href="#Flume-NG" class="headerlink" title="Flume NG"></a>Flume NG</h1><p>Flume NG:Flume New Generation</p>
<ul>
<li>NG只有一种角色节点:代理点(agent)。</li>
<li>没有collector、master节点。这是核心组件最核心的变化。</li>
<li>去除了 physical nodes、logical nodes 的概念和相关内容。</li>
<li>agent 节点的组成也发生了变化。NG agent 由 source、sink、channel 组成。</li>
<li>NG删减了角色,脱离了对Zookeeper的依赖<br><img src="/images/flume-ng.png" alt="flume-ng.png"></li>
</ul>
<h1 id="Flume-NG分析"><a href="#Flume-NG分析" class="headerlink" title="Flume NG分析"></a>Flume NG分析</h1><h2 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h2><ul>
<li>Event:一个数据单元,带有一个可选的消息头。</li>
<li>Flow:Event从源点到达目的点的迁移的抽象。</li>
<li>Client:操作位于源点处的Event,将其发送到Flume Agent。</li>
<li>Agent:一个独立的Flume进程,包含组件Source、Channel、Sink。</li>
<li>Source:用来消费传递到该组件的Event ,存入channel中。</li>
<li>Channel:中转Event的一个临时存储,保存有Source组件传递过来的Event。</li>
<li>Sink:从Channel中读取并移除Event,将Event传递到Flow Pipeline中的下一个Agent(如果有的话)。<h2 id="数据流:"><a href="#数据流:" class="headerlink" title="数据流:"></a>数据流:</h2>Flume 的核心是把数据从数据源收集过来,再送到目的地。为了保证输送一定成功,在送到目的地之前,会先缓存数据,待数据真正到达目的地后,删除自己缓存的数据:当sink写入失败后,可以自动重启,不会造成数据丢失,因此很可靠。<br>Flume 传输的数据的基本单位是 Event,如果是文本文件,通常是一行记录,这也是事务的基本单位。Event 从 Source,流向 Channel,再到 Sink,本身为一个 byte 数组,并可携带 headers 信息。Event 代表着一个数据流的最小完整单元,从外部数据源来,向外部的目的地去。<h2 id="核心组件:"><a href="#核心组件:" class="headerlink" title="核心组件:"></a>核心组件:</h2><h3 id="Source"><a href="#Source" class="headerlink" title="Source"></a>Source</h3></li>
<li>ExecSource: 以运行 Linux 命令的方式,持续的输出最新的数据,如 tail -F 文件名 指令,在这种方式下,取的文件名必须是指定的。 ExecSource 可以实现对日志的实时收集,但是存在Flume不运行或者指令执行出错时,将无法收集到日志数据,无法保证日志数据的完整性。 </li>
<li>SpoolSource: 监测配置的目录下新增的文件,并将文件中的数据读取出来。需要注意两点:拷贝到 spool 目录下的文件不可以再打开编辑;spool 目录下不可包含相应的子目录。SpoolSource无法实现实时的收集数据,但可以设置以分钟的方式分割文件,趋于实时。<br>###Channel<br>Memory Channel, JDBC Channel , File Channel,Psuedo Transaction Channel。比较常见的是前三种 channel。</li>
<li>MemoryChannel 可以实现高速的吞吐,但是无法保证数据的完整性。 </li>
<li>MemoryRecoverChannel 在官方文档的建议上已经建义使用FileChannel来替换。</li>
<li>FileChannel保证数据的完整性与一致性。在具体配置FileChannel时,建议FileChannel设置的目录和程序日志文件保存的目录设成不同的磁盘,以便提高效率。<br><img src="/images/flume-channel.png" alt="flume-channel.png"></li>
</ul>
<h3 id="Sink"><a href="#Sink" class="headerlink" title="Sink"></a>Sink</h3><p><img src="/images/flume-sink.png" alt="flume-sink.png"></p>
<h2 id="可靠性"><a href="#可靠性" class="headerlink" title="可靠性"></a>可靠性</h2><p>在Flume NG中,可靠性指的是在数据流的传输过程中,保证events的可靠传递。<br>在Flume NG中,所有的events都保存在Agent的Channel中,然后被发送到数据流下一个Agent或者最终的存储服务中。当且仅当它们被保存到下一个Agent的Channel中,或者被保存到最终的存储服务中。这就是Flume 提供数据流中点到点的可靠性保证的最基本的单跳消息语义传递。<br>首先,Agent间的事务交换。Flume使用事务的办法来保证events的可靠传递。Source和Sink分别被封装在事务中,这些事务由保存event的存储提供或者由Channel提供。这就保证了event在数据流的点对点传输中是可靠的。在多级数据流中,如下图,上一级的Sink和下一级的Source都被包含在事务中,保证数据可靠地从一个Channel到另一个Channel转移。<br><img src="/images/flume-transaction.png" alt="flume-transaction.png"></p>
<p>下图A:正常情况下的 events流程。<br>下图B:Agent2 跟central event store失联,Agent2提交的事务失败,将events缓存起来。<br>下图C:重新恢复时,再恢复失联之前的任务以及后续的events发送。<br><img src="/images/flume-trans-example.png" alt="flume-trans-example.png"></p>
<h2 id="高可用"><a href="#高可用" class="headerlink" title="高可用"></a>高可用</h2><p>如下图所示,Agent1中,只有要有一个Sink组件可用,events就被传递到下一个组件,如果一个Sink能成功处理Event(事务完成),则会加入到一个Pool中, 否则,则会从Pool中移除,并计算失败次数,设置惩罚因子。所以,如果某一个Flow中某一层的Agent只有一个,或者全部宕机,可能导致这些Events被存储在流水线上最后一个存活节点。<br><img src="/images/flume-available.png" alt="flume-available.png"></p>
]]></content>
<summary type="html">
<h1 id="声明"><a href="#声明" class="headerlink" title="声明"></a>声明</h1><p>我对Flume的研究并不深,这一篇文章来源于2016年3月的某一个下午对Flume的调研,仅有一个下午,所以可能有一些观点是不对的。另外,文章很多内容来源于一些大神的博文,当时匆匆没有记录引用来源。所以,如果有人可以发现本文的错误,以及引用的文章,还请在留言中指出。万分感谢。<br>
</summary>
<category term="flume" scheme="http://chengqiangboy.github.io/categories/flume/"/>
<category term="flume" scheme="http://chengqiangboy.github.io/tags/flume/"/>
</entry>
<entry>
<title>Spark OFF_HEP变迁</title>
<link href="http://chengqiangboy.github.io/2016/09/21/spark-off_heap/"/>
<id>http://chengqiangboy.github.io/2016/09/21/spark-off_heap/</id>
<published>2016-09-21T15:20:17.000Z</published>
<updated>2018-06-23T09:30:57.482Z</updated>
<content type="html"><![CDATA[<p>在文章的开头,安利一下我自己的github上的一个项目:<a href="https://github.com/chengqiangboy/spark-alluxio-blockstore" rel="external nofollow noopener noreferrer" target="_blank">AlluxioBlockManager</a>,同时还有我的github上的博客:<a href="https://github.com/chengqiangboy/blog" rel="external nofollow noopener noreferrer" target="_blank">blog</a><br>这个项目的作用是替代Spark2.0以前默认的<code>TachyonBlockManager</code>,稍后解释为什么要重新开发AlluxioBlockManager,以及Spark2.0的off_heap。<br><a id="more"></a></p>
<h1 id="OFF-HEAP"><a href="#OFF-HEAP" class="headerlink" title="OFF_HEAP"></a>OFF_HEAP</h1><p>Spark中RDD提供了几种存储级别,不同的存储级别可以带来不同的容错性能,例如 <code>MEMORY_ONLY</code>,<code>MEMORY_ONLY_SER_2</code>…其中,有一种特别的是<code>OFF_HEAP</code><br><code>off_heap</code>的优势在于,在内存有限的条件下,减少不必要的内存消耗,以及频繁的GC问题,提升程序性能。<br>Spark2.0以前,默认的off_heap是Tachyon,当然,你可以通过继承<code>ExternalBlockManager</code> 来实现你自己想要的任何off_heap。<br>这里说Tachyon,是因为Spark默认的TachyonBlockManager开发完成之后,就再也没有更新过,以至于Tachyon升级为Alluxio之后移除不使用的API,导致Spark默认off_heap不可用,这个问题Spark社区和Alluxio社区都有反馈:<a href="https://alluxio.atlassian.net/browse/ALLUXIO-1881" rel="external nofollow noopener noreferrer" target="_blank">ALLUXIO-1881</a></p>
<h1 id="Spark2-0的off-heap"><a href="#Spark2-0的off-heap" class="headerlink" title="Spark2.0的off_heap"></a>Spark2.0的off_heap</h1><p>从spark2.0开始,社区已经移除默认的TachyonBlockManager以及ExternalBlockManager相关的API:<a href="https://issues.apache.org/jira/browse/SPARK-12667" rel="external nofollow noopener noreferrer" target="_blank">SPARK-12667</a>。<br>那么,问题来了,在Spark2.0中,OFF_HEAP是怎么处理的呢?数据存在哪里?<br>上代码:<br>首先,在StorageLevel里面,不同的存储级别解析成不同的构造函数,从OFF_HEAP的构造函数可以看出来,OFF_HEAP依旧存在。<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div><div class="line">13</div><div class="line">14</div><div class="line">15</div></pre></td><td class="code"><pre><div class="line">Object StorageLevel {</div><div class="line">val NONE = new StorageLevel(false, false, false, false)</div><div class="line">val DISK_ONLY = new StorageLevel(true, false, false, false)</div><div class="line">val DISK_ONLY_2 = new StorageLevel(true, false, false, false, 2)</div><div class="line">val MEMORY_ONLY = new StorageLevel(false, true, false, true)</div><div class="line">val MEMORY_ONLY_2 = new StorageLevel(false, true, false, true, 2)</div><div class="line">val MEMORY_ONLY_SER = new StorageLevel(false, true, false, false)</div><div class="line">val MEMORY_ONLY_SER_2 = new StorageLevel(false, true, false, false, 2)</div><div class="line">val MEMORY_AND_DISK = new StorageLevel(true, true, false, true)</div><div class="line">val MEMORY_AND_DISK_2 = new StorageLevel(true, true, false, true, 2)</div><div class="line">val MEMORY_AND_DISK_SER = new StorageLevel(true, true, false, false)</div><div class="line">val MEMORY_AND_DISK_SER_2 = new StorageLevel(true, true, false, false, 2)</div><div class="line">val OFF_HEAP = new StorageLevel(false, false, true, false)</div><div class="line"> ...... ........</div><div class="line">}</div></pre></td></tr></table></figure></p>
<p>在<code>org.apache.spark.memory</code>中,有一个<code>MemoryMode</code>,<code>MemoryMode</code>标记了使用<code>ON_HEAP</code>还是<code>OFF_HEAP</code>,在<code>org.apache.spark.storage.memory.MemoryStore</code>中,根据<code>MemoryMode</code>类型来调用不同的存储<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div><div class="line">6</div><div class="line">7</div><div class="line">8</div><div class="line">9</div><div class="line">10</div><div class="line">11</div><div class="line">12</div></pre></td><td class="code"><pre><div class="line">def putBytes[T: ClassTag]( </div><div class="line"> blockId: BlockId, </div><div class="line"> size: Long, </div><div class="line"> memoryMode: MemoryMode,</div><div class="line"> _bytes: () => ChunkedByteBuffer): Boolean = {</div><div class="line"> .............</div><div class="line"> val entry = new SerializedMemoryEntry[T](bytes, memoryMode, implicitly[ClassTag[T]])</div><div class="line"> entries.synchronized { </div><div class="line"> entries.put(blockId, entry)</div><div class="line"> }</div><div class="line"> .............</div><div class="line"> }</div></pre></td></tr></table></figure></p>
<p>再看<code>MemoryStore</code>中存数据的方法:<code>putIteratorAsBytes</code><br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div></pre></td><td class="code"><pre><div class="line">val allocator = memoryMode match { </div><div class="line"> case MemoryMode.ON_HEAP => ByteBuffer.allocate _ </div><div class="line"> case MemoryMode.OFF_HEAP => Platform.allocateDirectBuffer _</div><div class="line">}</div></pre></td></tr></table></figure></p>
<p>终于找到Spark2.0中off_heap的底层存储了:<code>Platform</code>是利用java unsafe API实现的一个访问off_heap的类。</p>
<h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>spark2.0 off_heap就是利用java unsafe API实现的内存管理。<br>优点:依然可以减少内存的使用,减少频繁的GC,提高程序性能。<br>缺点:从代码中看到,使用OFF_HEAP并没有备份数据,也不能像alluxio那样保证数据高可用,丢失数据则需要重新计算。</p>
]]></content>
<summary type="html">
<p>在文章的开头,安利一下我自己的github上的一个项目:<a href="https://github.com/chengqiangboy/spark-alluxio-blockstore" rel="external nofollow noopener noreferrer" target="_blank">AlluxioBlockManager</a>,同时还有我的github上的博客:<a href="https://github.com/chengqiangboy/blog" rel="external nofollow noopener noreferrer" target="_blank">blog</a><br>这个项目的作用是替代Spark2.0以前默认的<code>TachyonBlockManager</code>,稍后解释为什么要重新开发AlluxioBlockManager,以及Spark2.0的off_heap。<br>
</summary>
<category term="spark相关" scheme="http://chengqiangboy.github.io/categories/spark%E7%9B%B8%E5%85%B3/"/>
<category term="spark" scheme="http://chengqiangboy.github.io/tags/spark/"/>
<category term="off_heap" scheme="http://chengqiangboy.github.io/tags/off-heap/"/>
<category term="tachyon" scheme="http://chengqiangboy.github.io/tags/tachyon/"/>
<category term="alluxio" scheme="http://chengqiangboy.github.io/tags/alluxio/"/>
</entry>
<entry>
<title>Spark Streaming应用一个越跑越慢的bug</title>
<link href="http://chengqiangboy.github.io/2016/08/24/a-bug-in-streaming-app/"/>
<id>http://chengqiangboy.github.io/2016/08/24/a-bug-in-streaming-app/</id>
<published>2016-08-24T09:11:17.000Z</published>
<updated>2018-06-23T09:30:57.481Z</updated>
<content type="html"><![CDATA[<h1 id="题记:"><a href="#题记:" class="headerlink" title="题记:"></a>题记:</h1><p>这是我的第一篇技术博文,写得不好请多提意见。然后,感谢张志斌老师,毕业之前张老师帮助我解一些“神奇的bug”,现在毕业一个月,我终于自己开始解自己认为“神奇的bug”。<br><a id="more"></a></p>
<h1 id="背景:"><a href="#背景:" class="headerlink" title="背景:"></a>背景:</h1><p>我需要在spark streaming上做一个窗口的统计功能,但是因为一些原因,不能利用window相关算子。于是,我在driver上保持了一个resultRDD,在DStream内不断地去更新这个resultRDD,包括新信息的统计,和过期信息的剔除。</p>
<h1 id="现象:"><a href="#现象:" class="headerlink" title="现象:"></a>现象:</h1><p>batchSize设置为1分钟,程序刚开始运行的一天内,每个batch的处理时间都是2秒以下,如下图:<br><img src="/images/a-bug-in-streaming-app-start.png" alt="start.png"><br>运行长时间之后,监控页面如下:(忽略时间戳,为了截图重启了程序)<br><img src="/images/a-bug-in-streaming-app-end.png" alt="end.png"><br>可以看到,每个job都skip了大量的stage,每个stage内,都skip了大量的task。而且有一个很有意思的现象,skip的数量都是递增的。而且,从skip的数字上来看,也很有规律。<br>再注意 job内stage的执行时间,每个job有2个stage,加起来平均2~3秒。但此时,batch的处理时延已经达到了20~30秒。<br>总结一下遇到的问题:我的streaming程序连续运行一周之后,慢了一个数量级,但实际花费在执行上的时间近似不变。到这,我已经认为是一个“神奇的bug”了。</p>
<h1 id="debug:"><a href="#debug:" class="headerlink" title="debug:"></a>debug:</h1><p>严格的说,batch的处理时间 = 生成执行计划时间 + task调度时间 + 各个stage执行时间<br>在我的场景中,batch的处理时间远高于stage执行时间和。就说明,执行计划生成和task调度花费了大量时间。task调度是yarn负责,开销主要在分发策略和网络开销上,这部分不会太耗时。剩下就是执行计划生成了。<br>在spark中,执行计划是通过RDD的依赖关系来生成DAG,并以此来划分stage生成执行计划,代码就不贴了,大致就是根据RDD的依赖关系递归地深度优先搜索,终止条件就是某个RDD的依赖为空,也就是说搜索到源RDD。<br>了解了DAG的生成原理之后,再回过头来看文章开头说的背景,我们来模拟一下DAG的生成,DStream.foreachRDD,开始计算,假设当前时间为 t,然后t时刻的resultRDD依赖t-1时刻的resultRDD,t-1时刻resultRDD依赖于t-2时刻的resultRDD。。<br>问题的根源找出来了,随着时间的推移,依赖的层次越来越多。最终导致DAG的生成耗费了大量时间。<br>要解决这个问题,就要清除掉resultRDD的依赖关系,如何清除?<br>答案是 checkpoint<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><div class="line">1</div><div class="line">2</div><div class="line">3</div><div class="line">4</div><div class="line">5</div></pre></td><td class="code"><pre><div class="line">private[spark] def markCheckpointed(): Unit = { </div><div class="line"> clearDependencies()</div><div class="line"> partitions_ = null</div><div class="line"> deps = null</div><div class="line">}</div></pre></td></tr></table></figure></p>
<p>在checkpoint之后,spark会清空rdd的依赖。<br>至此,“神奇的bug”解决。</p>
<p>至于前面提到的大量skip:DAG生成遍历了rdd的整个历史,但是在DAG具体的执行过程中,会发现某一些stage,task已经被运算过,因此不会再次计算,这样就产生了skip。<br>最后,愿我的未来再不会觉得有“神奇的bug”。</p>
]]></content>
<summary type="html">
<h1 id="题记:"><a href="#题记:" class="headerlink" title="题记:"></a>题记:</h1><p>这是我的第一篇技术博文,写得不好请多提意见。然后,感谢张志斌老师,毕业之前张老师帮助我解一些“神奇的bug”,现在毕业一个月,我终于自己开始解自己认为“神奇的bug”。<br>
</summary>
<category term="spark相关" scheme="http://chengqiangboy.github.io/categories/spark%E7%9B%B8%E5%85%B3/"/>
<category term="spark streaming" scheme="http://chengqiangboy.github.io/tags/spark-streaming/"/>
<category term="checkpoint" scheme="http://chengqiangboy.github.io/tags/checkpoint/"/>
<category term="调优" scheme="http://chengqiangboy.github.io/tags/%E8%B0%83%E4%BC%98/"/>
</entry>
</feed>