forked from kangfoo/kangfoo.github.com
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathatom.xml
More file actions
2806 lines (2802 loc) · 169 KB
/
atom.xml
File metadata and controls
2806 lines (2802 loc) · 169 KB
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
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title><![CDATA[kangfoo's blog]]></title>
<link href="http://kangfoo.u.qiniudn.com//atom.xml" rel="self"/>
<link href="http://kangfoo.u.qiniudn.com//"/>
<updated>2014-03-05T01:02:11+08:00</updated>
<id>http://kangfoo.u.qiniudn.com//</id>
<author>
<name><![CDATA[kangfoo]]></name>
<email><![CDATA[baseo4233@126.com]]></email>
</author>
<generator uri="http://press.opoo.org/">OpooPress</generator>
<entry>
<title type="html"><![CDATA[Hadoop MapReduce 学习参考博客汇总]]></title>
<link href="http://kangfoo.u.qiniudn.com//article/2014/03/hadoop--xue-xi-can-kao-bo-ke-hui-zong/"/>
<updated>2014-03-05T01:01:00+08:00</updated>
<id>http://kangfoo.u.qiniudn.com//article/2014/03/hadoop--xue-xi-can-kao-bo-ke-hui-zong/</id>
<content type="html"><![CDATA[<p>TODO.</p>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[Hadoop Pipes & Streaming]]></title>
<link href="http://kangfoo.u.qiniudn.com//article/2014/03/hadoop-pipes--streaming/"/>
<updated>2014-03-03T22:26:00+08:00</updated>
<id>http://kangfoo.u.qiniudn.com//article/2014/03/hadoop-pipes--streaming/</id>
<content type="html"><![CDATA[<p>申明:本文大部分出自于 <a href="http://new.osforce.cn/?mu=20140227220525KZol8ENMYdFQ6SjMveU26nEZ">开源力量</a> LouisT 老师的<a href="http://new.osforce.cn/course/101?mc101=20140301233857au7XG16o9ukfev1pmFCOfv2s">开源力量培训课-Hadoop Development</a>课件 和 Apache 官方文档。</p>
<h2>Streaming</h2>
<ul>
<li>Streaming 是 hadoop 里面提供的一个工具</li>
<li>Streaming 框架允许任何程序语言实现的程序在 Hadoop MapReduce 中使用,方便任何程序向 Hadoop 平台移植,具有很强的扩展性;</li>
<li>mapper 和 reducer 会从标准输入中读取用户数据,一行一行处理后发送给标准输出。Streaming 工具会创建 MapReduce 作业,发送给各个 tasktracker,同时监控整个作业的执行过程;</li>
<li>如果一个文件(可执行或者脚本)作为 mapper,mapper 初始化时,每一个 mapper 任务会把该文件作为一个单独进程启动,mapper 任务运行时,它把输入切法成行并把每一行提供给可执行文件进程的标准输入。同 时,mapper 收集可执行文件进程标准输出的内容,并把收到的每一行内容转化成 key/value,作为 mapper的输出。默认情况下,一行中第一个 tab 之前的部分作为 key,之后的(不包括)作为value。如果没有 tab,整行作为 key 值,value值为null。对于reducer,类似;</li>
</ul>
<h3>Streaming 优点</h3>
<ol>
<li><p>开发效率高,便于移植。Hadoop Streaming 使用 Unix 标准流作为 Hadoop 和应用程序之间的接口。在单机上可按照 cat input | mapper | sort | reducer > output 进行测试,若单机上测试通过,集群上一般控制好内存也可以很好的执行成功。</p>
</li>
<li><p>提高运行效率。对内存要求较高,可用C/C++控制内存。比纯java实现更好。</p>
</li>
</ol>
<h3>Streaming缺点</h3>
<ol>
<li><p>Hadoop Streaming 默认只能处理文本数据,(0.21.0之后可以处理二进制数据)。</p>
</li>
<li><p>Steaming 中的 mapper 和 reducer 默认只能想标准输出写数据,不能方便的多路输出。</p>
</li>
</ol>
<p>更详细内容请参考于: http://hadoop.apache.org/docs/r1.2.1/streaming.html</p>
<pre class='brush:shell'>$HADOOP_HOME/bin/hadoop jar $HADOOP_HOME/hadoop-streaming.jar \
-input myInputDirs \
-output myOutputDir \
-mapper /bin/cat \
-reducer /bin/wc
</pre><h3>streaming示例</h3>
<p>perl 语言的<a href="https://github.com/kangfoo/hadoop1.study/tree/master/kangfoo/study.hdfs/src/main/java/com/kangfoo/study/hadoop1/streaming">streaming示例</a> 代码</p>
<pre class='brush:perl'>-rw-rw-r--. 1 hadoop hadoop 48 2月 22 10:47 data
-rw-rw-r--. 1 hadoop hadoop 107399 2月 22 10:41 hadoop-streaming-1.2.1.jar
-rw-rw-r--. 1 hadoop hadoop 186 2月 22 10:45 mapper.pl
-rw-rw-r--. 1 hadoop hadoop 297 2月 22 10:55 reducer.pl
##
$ ../bin/hadoop jar hadoop-streaming-1.2.1.jar -mapper mapper.pl -reducer reducer.pl -input /test/streaming -output /test/streamingout1 -file mapper.pl -file reducer.pl
</pre><h2>Hadoop pipes</h2>
<ol>
<li>Hadoop pipes 是 Hadoop MapReduce 的 C++ 的接口代称。不同于使用标准输入和输出来实现 map 代码和 reduce 代码之间的 Streaming。</li>
<li>Pipes 使用套接字 socket 作为 tasktracker 与 C++ 版本函数的进程间的通讯,未使用 JNI。</li>
<li>与 Streaming 不同,Pipes 是 Socket 通讯,Streaming 是标准输入输出。</li>
</ol>
<h3>编译 Hadoop Pipes</h3>
<p>编译c++ pipes( 确保操作系统提前安装好了 openssl,zlib,glib,openssl-devel)
Hadoop更目录下执行
ant -Dcompile.c++=yes examples</p>
<p>具体请参见《Hadoop Pipes 编译》</p>
<h3>Hadoop官方示例:</h3>
<pre class='brush:shell'>hadoop/src/examples/pipes/impl
config.h.in
sort.cc
wordcount-nopipe.cc
wordcount-part.cc
wordcount-simple.cc
</pre><p>运行前需要把可执行文件和输入数据上传到 hdfs:</p>
<pre class='brush:shell'>$ ./bin/hadoop fs -mkdir /test/pipes/input
$ ./bin/hadoop fs -put a.txt /test/pipes/input
$ ./bin/hadoop fs -cat /test/pipes/input/a.txt
hello hadoop hello hive hello hbase hello zk
</pre><p>上传执行文件,重新命名为/test/pipes/exec</p>
<pre class='brush:shell'>$ ./bin/hadoop fs -put ./build/c++-examples/Linux-amd64-64/bin/wordcount-simple /test/pipes/exec
</pre><p>在编译好的文件夹目录下执行</p>
<pre>$ cd hadoop/build/c++-examples/Linux-amd64-64/bin
$ ../../../../bin/hadoop pipes -Dhadoop.pipes.java.recordreader=true -Dhadoop.pipes.java.recordwriter=true -reduces 4 -input /test/pipes/input -output /test/pipes/input/output1 -program /test/pipes/execs
</pre><p>执行结果如下:</p>
<pre>$ ./bin/hadoop fs -cat /test/pipes/input/output1/part-00000 hbase 1
$ ./bin/hadoop fs -cat /test/pipes/input/output1/part-00001 hello 4 hive 1
$ ./bin/hadoop fs -cat /test/pipes/input/output1/part-00002 hadoop 1 zk 1
$ ./bin/hadoop fs -cat /test/pipes/input/output1/part-00003
</pre><h3>参考博客:</h3>
<ul>
<li><a href="http://dongxicheng.org/mapreduce/hadoop-pipes-programming/">Hadoop pipes编程</a></li>
<li><a href="http://hongweiyi.com/2012/05/hadoop-pipes-src/">Hadoop Pipes运行机制</a></li>
</ul>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[Hadoop MapReduce Sort]]></title>
<link href="http://kangfoo.u.qiniudn.com//article/2014/03/hadoop-mapreduce-sort/"/>
<updated>2014-03-03T22:24:00+08:00</updated>
<id>http://kangfoo.u.qiniudn.com//article/2014/03/hadoop-mapreduce-sort/</id>
<content type="html"><![CDATA[<p>排序是 MapReduce 的核心。排序可分为四种排序:普通排序、部分排序、全局排序、辅助排序</p>
<h2>普通排序</h2>
<p>Mapreduce 本身自带排序功能;Text 对象是不适合排序的;IntWritable,LongWritable 等实现了WritableComparable 类型的对象都是可以排序的。</p>
<h2>部分排序</h2>
<p>map 和 reduce 处理过程中包含了默认对 key 的排序,那么如果不要求全排序,可以直接把结果输出,每个输出文件中包含的就是按照key执行排序的结果。</p>
<h3>控制排序顺序</h3>
<p>键的排序是由 RawComparator 控制的,规则如下:</p>
<ol>
<li>若属性 mapred.output.key.comparator.class 已设置,则使用该类的实例。调用 JobConf 的 setOutputKeyComparatorClass() 方法进行设置。</li>
<li>否则,键必须是 WritableComparable 的子类,并使用针对该键类的已登记的 comparator.</li>
<li>如果没有已登记的 comparator ,则使用 RawComparator 将字节流反序列化为一个对象,再由 WritableComparable 的 compareTo() 方法进行操作。</li>
</ol>
<h2>全局排序(对所有数据排序)</h2>
<p>Hadoop 没有提供全局数据排序,而全局排序是非常普遍的需求。</p>
<h3>实现方案</h3>
<ul>
<li>首先,创建一系列的排好序的文件;</li>
<li>其次,串联这些文件;</li>
<li>最后,生成一个全局排序的文件。</li>
</ul>
<p>主要思路是使用一个partitioner来描述全局排序的输出。该方法关键在于如何划分各个分区。</p>
<p>例,对整数排序,[0,10000] 的在 partition 0 中,(10000,20000] 在 partition 1 中… …即第n个reduce 所分配到的数据全部大于第 n-1 个 reduce 中的数据。每个 reduce 的结果都是有序的。</br>
然后再将所有的输出文件顺序合并成一个大的文件,那么就实现了全局排序。</p>
<p>在比较理想的数据分布均匀的情况下,每个分区内的数据量要基本相同。</p>
<p>但实际中数据往往分布不均匀,出现数据倾斜,这时按照此方法进行的分区划分数据就不适用,可对数据进行采样。</p>
<h3>采样器</h3>
<p>通过对 key 空间进行采样,可以较为均匀的划分数据集。采样的核心思想是只查看一小部分键,获取键的相似分布,并由此构建分区。采样器是在 map 阶段之前进行的, 在提交 job 的 client 端完成的。</p>
<h4>Sampler接口</h4>
<p>Sampler 接口是 Hadoop 的采样器,它的 getSample() 方法返回一组样本。此接口一般不由客户端调用,而是由 InputSampler 类的静态方法 writePartitionFile() 调用,以创建一个顺序文件来存储定义分区的键。</p>
<p>Sampler接口声明如下:</p>
<pre class='brush:java'> public interface Sampler<K,V> {
K[] getSample(InputFormat<K,V> inf, JobConf job) throws IOException;
}
</pre><p>继承 Sample 的类还有 IntervalSampler 间隔采样器,RandomSampler 随机采样器,SplitSampler 分词采样器。它们都是 InputSampler 的静态内部类。</p>
<p>getSample() 方法根据 job 的配置信息以及输入格式获得抽样结果,三个采样类各自有不同的实现。</p>
<p><strong>IntervalSampler 根据一定的间隔从 s 个分区中采样数据,非常适合对排好序的数据采样。</strong></p>
<pre class='brush:java'>public static class IntervalSampler<K,V> implements Sampler<K,V> {
private final double freq;// 哪一条记录被选中的概率
private final int maxSplitsSampled;// 采样的最大分区数
/**
* For each split sampled, emit when the ratio of the number of records
* retained to the total record count is less than the specified
* frequency.
*/
@SuppressWarnings("unchecked") // ArrayList::toArray doesn't preserve type
public K[] getSample(InputFormat<K,V> inf, JobConf job) throws IOException {
InputSplit[] splits = inf.getSplits(job, job.getNumMapTasks());// 1. 得到输入分区数组
ArrayList<K> samples = new ArrayList<K>();
int splitsToSample = Math.min(maxSplitsSampled, splits.length);
int splitStep = splits.length / splitsToSample; // 2. 分区采样时的间隔splitStep = 输入分区总数 除以 splitsToSample的 商;
long records = 0;
long kept = 0;
for (int i = 0; i < splitsToSample; ++i) {
RecordReader<K,V> reader = inf.getRecordReader(splits[i * splitStep], // 3. 采样下标为i * splitStep的数据
job, Reporter.NULL);
K key = reader.createKey();
V value = reader.createValue();
while (reader.next(key, value)) {// 6. 循环读取下一条记录
++records;
if ((double) kept / records < freq) { // 4. 如果当前样本数与已经读取的记录数的比值小于freq,则将这条记录添加到样本集合
++kept;
samples.add(key);// 5. 将记录添加到样本集合中
key = reader.createKey();
}
}
reader.close();
}
return (K[])samples.toArray();
}
}
… …
}
</pre><p><strong>RandomSampler 是常用的采样器,它随机地从输入数据中抽取 Key</strong>。</p>
<pre class='brush:java'> public static class RandomSampler<K,V> implements Sampler<K,V> {
private double freq;// 一个Key被选中的 概率
private final int numSamples;// 从所有被选中的分区中获得的总共的样本数目
private final int maxSplitsSampled;// 需要检查扫描的最大分区数目
/**
* Randomize the split order, then take the specified number of keys from
* each split sampled, where each key is selected with the specified
* probability and possibly replaced by a subsequently selected key when
* the quota of keys from that split is satisfied.
*/
@SuppressWarnings("unchecked") // ArrayList::toArray doesn't preserve type
public K[] getSample(InputFormat<K,V> inf, JobConf job) throws IOException {
InputSplit[] splits = inf.getSplits(job, job.getNumMapTasks());// 1. 获取所有的输入分区
ArrayList<K> samples = new ArrayList<K>(numSamples);// 2. 确定需要抽样扫描的分区数目
int splitsToSample = Math.min(maxSplitsSampled, splits.length);// 3. 取最小的为采样的分区数
Random r = new Random();
long seed = r.nextLong();
r.setSeed(seed);
LOG.debug("seed: " + seed);
// shuffle splits 4. 对输入分区数组shuffle排序
for (int i = 0; i < splits.length; ++i) {
InputSplit tmp = splits[i];
int j = r.nextInt(splits.length);// 5. 打乱其原始顺序
splits[i] = splits[j];
splits[j] = tmp;
}
// our target rate is in terms of the maximum number of sample splits,
// but we accept the possibility of sampling additional splits to hit
// the target sample keyset
// 5. 然后循环逐 个扫描每个分区中的记录进行采样,
for (int i = 0; i < splitsToSample ||
(i < splits.length && samples.size() < numSamples); ++i) {
RecordReader<K,V> reader = inf.getRecordReader(splits[i], job,
Reporter.NULL);
// 6. 取出一条记录
K key = reader.createKey();
V value = reader.createValue();
while (reader.next(key, value)) {
if (r.nextDouble() <= freq) {
if (samples.size() < numSamples) {// 7. 判断当前的采样数是否小于最大采样数
samples.add(key); //8. 小于则这条记录被选中,放进采样集合中,
} else {
// When exceeding the maximum number of samples, replace a
// random element with this one, then adjust the frequency
// to reflect the possibility of existing elements being
// pushed out
int ind = r.nextInt(numSamples);// 9. 从[0,numSamples]中选择一个随机数
if (ind != numSamples) {
samples.set(ind, key);// 10. 替换掉采样集合随机数对应位置的记录,
}
freq *= (numSamples - 1) / (double) numSamples;// 11. 调小频率
}
key = reader.createKey();// 12. 下一条纪录的key
}
}
reader.close();
}
return (K[])samples.toArray();// 13. 返回
}
}
… …
}
</pre><p><strong>SplitSampler 从 s 个分区中采样前 n 个记录,是采样随机数据的一种简便方式。</strong></p>
<pre class='brush:java'> public static class SplitSampler<K,V> implements Sampler<K,V> {
private final int numSamples;// 最大采样数
private final int maxSplitsSampled;// 最大分区数
… …
/**
* From each split sampled, take the first numSamples / numSplits records.
*/
@SuppressWarnings("unchecked") // ArrayList::toArray doesn't preserve type
public K[] getSample(InputFormat<K,V> inf, JobConf job) throws IOException {
InputSplit[] splits = inf.getSplits(job, job.getNumMapTasks());
ArrayList<K> samples = new ArrayList<K>(numSamples);
int splitsToSample = Math.min(maxSplitsSampled, splits.length);// 1. 采样的分区数
int splitStep = splits.length / splitsToSample; // 2. 分区采样时的间隔 = 分片的长度 与 输入分片的总数的 商
int samplesPerSplit = numSamples / splitsToSample; // 3. 每个分区的采样数
long records = 0;
for (int i = 0; i < splitsToSample; ++i) {
RecordReader<K,V> reader = inf.getRecordReader(splits[i * splitStep], // 4.采样下标为i * splitStep的数据
job, Reporter.NULL);
K key = reader.createKey();
V value = reader.createValue();
while (reader.next(key, value)) {
samples.add(key);// 5. 将记录添加到样本集合中
key = reader.createKey();
++records;
if ((i+1) * samplesPerSplit <= records) { // 6. 当前样本数大于当前的采样分区所需要的样本数,则停止对当前分区的采样。
break;
}
}
reader.close();
}
return (K[])samples.toArray();
}
}
</pre><p><strong>Hadoop为顺序文件提供了一个 TotalOrderPartitioner 类,可以用来实现全局排序</strong>;TotalOrderPartitioner 源代码理解。TotalOrderPartitioner 内部定义了多个字典树(内部类)。</p>
<pre class='brush:java'>interface Node<T>
// 特里树,利用字符串的公共前缀来节约存储空间,最大限度地减少无谓的字符串比较,查询效率比哈希表高
static abstract class TrieNode implements Node<BinaryComparable>
static class InnerTrieNode extends TrieNode
static class LeafTrieNode extends TrieNode
… …
</pre><p>由 TotalOrderPartitioner 调用 getPartition() 方法返回分区,由 buildTrieRec() 构建特里树.</p>
<pre class='brush:java'> private TrieNode buildTrieRec(BinaryComparable[] splits, int lower,
int upper, byte[] prefix, int maxDepth, CarriedTrieNodeRef ref) {
… …
}
</pre><h4>采样器使用示例</h4>
<ol>
<li>新建文件,名为 random.txt,里面每行存放一个数据。可由 RandomGenerator 类生成准备数据</li>
<li>执行 TestTotalOrderPartitioner.java</li>
</ol>
<h2>辅助排序</h2>
<p>先按 key 排序,在按 相同的 key 不同的 value 再排序。可实现对值分组的效果。</p>
<ul>
<li>可参考博客 <a href="http://heipark.iteye.com/blog/1990237">Hadoop二次排序关键点和出现时机(也叫辅助排序、Secondary Sort)</a></li>
<li>或者 hadoop example 工程下参考 SecondarySort.java</li>
</ul>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[Hadoop MapReduce Join]]></title>
<link href="http://kangfoo.u.qiniudn.com//article/2014/03/hadoop-mapreduce-join/"/>
<updated>2014-03-03T22:23:00+08:00</updated>
<id>http://kangfoo.u.qiniudn.com//article/2014/03/hadoop-mapreduce-join/</id>
<content type="html"><![CDATA[<p>在 Hadoop 中可以通过 MapReduce,Pig,hive,Cascading编程进行大型数据集间的连接操作。连接操作如果由 Mapper 执行,则称为“map端连接”;如果由 Reduce 执行,则称为“Reduce端连接”。</p>
<p>连接操作的具体实现技术取决于数据集的规模以及分区方式。</br>
若一个数据集很大而另一个数据集很小,以至于可以分发到集群中的每一个节点之中,则可以执行一个 MapReduce 作业,将各个数据集的数据放到一起,从而实现连接。</br>
若两个数据规模均很大,没有哪个数据集可以完全复制到集群的每个节点,可以使用 MapReduce 作业进行连接,使用 Map 端连接还是 Reduce 端连接取决于数据的组织方式。</br></p>
<p>Map端连接将所有的工作在 map 中操作,效率高但是不通用。而 Reduce 端连接利用了 shuff 机制,进行连接,效率不高。</p>
<p>DistributedCache 能够在任务运行过程中及时地将文件和存档复制到任务节点进行本地缓存以供使用。各个文件通常只复制到一个节点一次。可用 api 或者命令行在需要的时候将本地文件添加到 hdfs 文件系统中。</p>
<p>本文中的示例 <strong>出自于 <a href="http://new.osforce.cn/?mu=20140227220525KZol8ENMYdFQ6SjMveU26nEZ">开源力量</a> LouisT 老师的<a href="http://new.osforce.cn/course/101?mc101=20140301233857au7XG16o9ukfev1pmFCOfv2s">开源力量培训课-Hadoop Development</a>课件。</strong></p>
<h3>Map端连接</h3>
<p>Map 端联接是指数据到达 map 处理函数之前进行合并的。它要求 map 的输入数据必须先分区并以特定的方式排序。各个输入数据集被划分成相同数量的分区,并均按相同的键排序(连接键)。同一键的所有输入纪录均会放在同一个分区。以满足 MapReduce 作业的输出。</p>
<p>若作业的 Reduce 数量相同、键相同、输入文件是不可切分的,那么 map 端连接操作可以连接多个作业的输出。</p>
<p>在 Map 端连接效率比 Reduce 端连接效率高(Reduce端Shuff耗时),但是要求比较苛刻。</p>
<h4>基本思路</h4>
<ol>
<li>将需要 join 的两个文件,一个存储在 HDFS 中,一个使用 DistributedCache.addCacheFile() 将需要 join 另一个文件加入到所有 Map 的缓存里(DistributedCache.addCacheFile() 需要在作业提交前设置);</li>
<li>在 Map 函数里读取该文件,进行 Join;</li>
<li>将结果输出到 reduce 端;</li>
</ol>
<h4>使用步骤</h4>
<ol>
<li>在 HDFS 中上传文件(文本文件、压缩文件、jar包等);</li>
<li>调用相关API添加文件信息;</li>
<li>task运行前直接调用文件读写API获取文件;</li>
</ol>
<h3>Reduce端Join</h3>
<p>reduce 端联接比 map 端联接更普遍,因为输入的数据不需要特定的结构;效率低(所有数据必须经过shuffle过程)。</p>
<h4>基本思路</h4>
<ol>
<li>Map 端读取所有文件,并在输出的内容里加上标识代表数据是从哪个文件里来的;</li>
<li>在 reduce 处理函数里,对按照标识对数据进行保存;</li>
<li>然后根据 Key 的 Join 来求出结果直接输出;</li>
</ol>
<h3>示例程序</h3>
<p>使用 MapReduce map 端join 或者 reduce 端 join 实现如下两张表 emp, dep 中的 SQL 联合查询的数据效果。</p>
<pre class='brush:text'>Table EMP:(新建文件EMP,第一行属性名不要)
----------------------------------------
Name Sex Age DepNo
zhang male 20 1
li female 25 2
wang female 30 3
zhou male 35 2
----------------------------------------
Table Dep:(新建文件DEP,第一行属性名不要)
DepNo DepName
1 Sales
2 Dev
3 Mgt
------------------------------------------------------------
SQL:
select name,sex ,age, depName from emp inner join DEP on EMP.DepNo = Dep.DepNo
----------------------------------------
实现效果:
$ ./bin/hadoop fs -cat /reduceSideJoin/output11/part-r-00000
zhang male 20 sales
li female 25 dev
wang female 30 dev
zhou male 35 dev
</pre><p>Map 端 Join 的例子:<a href="https://github.com/kangfoo/hadoop1.study/blob/master/kangfoo/study.hdfs/src/main/java/com/kangfoo/study/hadoop1/mp/join/TestMapSideJoin.java">TestMapSideJoin</a> </br>
Reduce 端 Join 的例子:<a href="https://github.com/kangfoo/hadoop1.study/blob/master/kangfoo/study.hdfs/src/main/java/com/kangfoo/study/hadoop1/mp/join/TestReduceSideJoin.java">TestReduceSideJoin</a> </br></p>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[Hadoop MapReduce 计数器]]></title>
<link href="http://kangfoo.u.qiniudn.com//article/2014/03/hadoop-mapreduce--ji-shu-qi/"/>
<updated>2014-03-03T22:22:00+08:00</updated>
<id>http://kangfoo.u.qiniudn.com//article/2014/03/hadoop-mapreduce--ji-shu-qi/</id>
<content type="html"><![CDATA[<p>计数器是一种收集系统信息有效手段,用于质量控制或应用级统计。可辅助诊断系统故障。计数器可以比日志更方便的统计事件发生次数。</p>
<h3>内置计数器</h3>
<p>Hadoop 为每个作业维护若干内置计数器,主要用来记录作业的执行情况。</p>
<h4>内置计数器包括</h4>
<ul>
<li>MapReduce 框架计数器(Map-Reduce Framework)</li>
<li>文件系统计数器(FielSystemCounters)</li>
<li>作业计数器(Job Counters)</li>
<li>文件输入格式计数器(File Output Format Counters)</li>
<li>文件输出格式计数器(File Input Format Counters)</li>
</ul>
<!--表格数据太多,暂缓。 TODO-->
<!--<div style="height:0px;border-bottom:1px dashed red"></div>
<table width="100%" border="1" cellpadding="3" cellspacing="0" bordercolor="#eeeeee">
<tbody>
<tr>
<td><em>组别 </em></td>
<td><em>计数器名称 </em></td>
<td><em>说明 </em></td>
</tr>
<tr>
<th rowspan=13>Map-Reduce <br>Framework </th>
<td>Map input records </td>
<td>作业中所有的 map 已处理的输入纪录数。每次 RecordReader 读到一条纪录并将其传递给 map 的 map() 函数时,此计数器的值增加 </td>
</tr>
<tr>
<td>Map skipped records </td>
<td>作业中所有 map 跳过的输入纪录数。 </td>
</tr>
</tbody>
</table>
-->
<p>计数器由其关联的 task 进行维护,定期传递给 tasktracker,再由 tasktracker 传给 jobtracker。因此,计数器能够被全局地聚集。内置计数器实际由 jobtracker 维护,不必在整个网络发送。</p>
<p>一个任务的计数器值每次都是完整传输的,仅当一个作业执行成功之后,计数器的值才完整可靠的。</p>
<h3>自定义Java计数器</h3>
<p>MapReduce 允许用户自定义计数器,MapReduce 框架将跨所有 map 和 reduce 聚集这些计数器,并在作业结束的时候产生一个最终的结果。</p>
<p>计数器的值可以在 mapper 或者 reducer 中添加。多个计数器可以由一个 java 枚举类型来定义,以便对计数器分组。一个作业可以定义的枚举类型数量不限,个个枚举类型所包含的数量也不限。</p>
<p>枚举类型的名称即为组的名称,枚举类型的字段即为计数器名称。</p>
<p>在 TaskInputOutputContext 中的 counter</p>
<pre class='brush:java'> public Counter getCounter(Enum<?> counterName) {
return reporter.getCounter(counterName);
}
public Counter getCounter(String groupName, String counterName) {
return reporter.getCounter(groupName, counterName);
}
</pre><h4>计数器递增</h4>
<p>org.apache.hadoop.mapreduce.Counter类</p>
<pre class='brush:java'> public synchronized void increment(long incr) {
value += incr;
}
</pre><h4>计数器使用</h4>
<ul>
<li>WebUI 查看(50030);</li>
<li>命令行方式:hadoop job [-counter <job-id> <group-name> <counter-name>];</li>
<li>使用Hadoop API。
通过job.getCounters()得到Counters,而后调用counters.findCounter()方法去得到计数器对象;可参见《Hadoop权威指南》第8章 示例 8-2 MissingTemperaureFields.java</li>
</ul>
<h4>命令行方式示例</h4>
<pre class='brush:shell'>$ ./bin/hadoop job -counter job_201402211848_0004 FileSystemCounters HDFS_BYTES_READ
177
</pre><h3>自定义计数器</h3>
<p>统计词汇行中词汇数超过2个或少于2个的行数。 源代码: <a href="https://github.com/kangfoo/hadoop1.study/blob/master/kangfoo/study.hdfs/src/main/java/com/kangfoo/study/hadoop1/mp/counter/TestCounter.java">TestCounter.java</a>TestCounter.java</p>
<h4>输入数据文件值 counter.txt:</h4>
<pre class='brush:text'>hello world
hello
hello world 111
hello world 111 222
</pre><p>执行参数</p>
<pre class='brush:java'>hdfs://master11:9000/counter/input/a.txt hdfs://master11:9000/counter/output1
</pre><p>计数器统计(hadoop eclipse 插件执行)结果:</p>
<pre class='brush:shell'>2014-02-21 00:03:38,676 INFO mapred.JobClient (Counters.java:log(587)) - ERROR_COUNTER
2014-02-21 00:03:38,677 INFO mapred.JobClient (Counters.java:log(589)) - Above_2=2
2014-02-21 00:03:38,677 INFO mapred.JobClient (Counters.java:log(589)) - BELOW_2=1
</pre>]]></content>
</entry>
<entry>
<title type="html"><![CDATA[Hadoop MapReduce RecordReader 组件]]></title>
<link href="http://kangfoo.u.qiniudn.com//article/2014/03/hadoop-mapreduce-recordreader-zu-jian/"/>
<updated>2014-03-03T22:21:00+08:00</updated>
<id>http://kangfoo.u.qiniudn.com//article/2014/03/hadoop-mapreduce-recordreader-zu-jian/</id>
<content type="html"><![CDATA[<p>由 RecordReader 决定每次读取以什么样的方式读取数据分片中的一条数据。Hadoop 默认的 RecordReader 是 LineRecordReader(TextInputFormat 的 getRecordReader() 方法返回即是 LineRecordReader。二进制输入 SequenceFileInputFormat 的 getRecordReader() 方法返回即是SequenceFileRecordReader。)。LineRecordReader是用每行的偏移量作为 map 的 key,每行的内容作为 map 的 value;</p>
<p>它可作用于,自定义读取每一条记录的方式;自定义读入 key 的类型,如希望读取的 key 是文件的路径或名字而不是该行在文件中的偏移量。</p>
<h3>自定义RecordReader一般步骤</h3>
<ol>
<li>继承抽象类 RecordReader,实现 RecordReader 的实例;</li>
<li>实现自定义 InputFormat 类,重写 InputFormat 中 createRecordReader() 方法,返回值是自定义的 RecordReader 实例;
(3)配置 job.setInputFormatClass() 设置自定义的 InputFormat 类型;</li>
</ol>
<h3>TextInputFormat类源代码理解</h3>
<p>源码见 org.apache.mapreduce.lib.input.TextInputFormat 类(新API);</p>
<p>Hadoop 默认 TextInputFormat 使用 LineRecordReader。具体分析见注释。</p>
<pre class='brush:java'> public RecordReader<LongWritable, Text>
createRecordReader(InputSplit split,
TaskAttemptContext context) {
return new LineRecordReader();
}
// --> LineRecordReader
public void initialize(InputSplit genericSplit,
TaskAttemptContext context) throws IOException {
FileSplit split = (FileSplit) genericSplit;
Configuration job = context.getConfiguration();
this.maxLineLength = job.getInt("mapred.linerecordreader.maxlength",
Integer.MAX_VALUE);
start = split.getStart(); // 当前分片在整个文件中的起始位置
end = start + split.getLength(); // 当前分片,在整个文件的位置
final Path file = split.getPath();
compressionCodecs = new CompressionCodecFactory(job);// 压缩
codec = compressionCodecs.getCodec(file);
//
// open the file and seek to the start of the split
FileSystem fs = file.getFileSystem(job);
FSDataInputStream fileIn = fs.open(split.getPath()); // 获取 FSDataInputStream
//
if (isCompressedInput()) {
decompressor = CodecPool.getDecompressor(codec);
if (codec instanceof SplittableCompressionCodec) {
final SplitCompressionInputStream cIn =
((SplittableCompressionCodec)codec).createInputStream(
fileIn, decompressor, start, end,
SplittableCompressionCodec.READ_MODE.BYBLOCK);
in = new LineReader(cIn, job); //一行行读取
start = cIn.getAdjustedStart(); // 可能跨分区读取
end = cIn.getAdjustedEnd();// 可能跨分区读取
filePosition = cIn;
} else {
in = new LineReader(codec.createInputStream(fileIn, decompressor),
job);
filePosition = fileIn;
}
} else {
fileIn.seek(start);// 调整到文件起始偏移量
in = new LineReader(fileIn, job);
filePosition = fileIn;
}
// If this is not the first split, we always throw away first record
// because we always (except the last split) read one extra line in
// next() method.
if (start != 0) {
start += in.readLine(new Text(), 0, maxBytesToConsume(start));
}
this.pos = start; // 在当前分片的位置
}
// --> getFilePosition() 指针读取到哪个位置
// filePosition 为 Seekable 类型
private long getFilePosition() throws IOException {
long retVal;
if (isCompressedInput() && null != filePosition) {
retVal = filePosition.getPos();
} else {
retVal = pos;
}
return retVal;
}
//
// --> nextKeyValue()
public boolean nextKeyValue() throws IOException {
if (key == null) {
key = new LongWritable();
}
key.set(pos);
if (value == null) {
value = new Text();
}
int newSize = 0;
// We always read one extra line, which lies outside the upper
// split limit i.e. (end - 1)
// 预读取下一条纪录
while (getFilePosition() <= end) {
newSize = in.readLine(value, maxLineLength,
Math.max(maxBytesToConsume(pos), maxLineLength));
if (newSize == 0) {
break;
}
pos += newSize; // 下一行的偏移量
if (newSize < maxLineLength) {
break;
}
//
// line too long. try again
LOG.info("Skipped line of size " + newSize + " at pos " +
(pos - newSize));
}
if (newSize == 0) {
key = null;
value = null;
return false;
} else {
return true;
}
}
</pre><h3>自定义 RecordReader 演示</h3>
<p>假设,现有如下数据 10 ~ 70 需要利用自定义 RecordReader 组件分别计算数据奇数行和偶数行的数据之和。结果为:奇数行之和等于 160,偶数和等于 120。<strong>出自于 <a href="http://new.osforce.cn/?mu=20140227220525KZol8ENMYdFQ6SjMveU26nEZ">开源力量</a> LouisT 老师的<a href="http://new.osforce.cn/course/101?mc101=20140301233857au7XG16o9ukfev1pmFCOfv2s">开源力量培训课-Hadoop Development</a>课件。</strong></p>
<p>数据:</br>
10</br>
20</br>
30</br>
40</br>
50</br>
60</br>
70</br></p>
<h4>源代码</h4>
<p><a href="https://github.com/kangfoo/hadoop1.study/blob/master/kangfoo/study.hdfs/src/main/java/com/kangfoo/study/hadoop1/mp/typeformat/TestRecordReader.java">TestRecordReader.java</a></p>
<h4>数据准备</h4>
<pre class='brush:shell'>$ ./bin/hadoop fs -mkdir /inputreader
$ ./bin/hadoop fs -put ./a.txt /inputreader
$ ./bin/hadoop fs -lsr /inputreader
-rw-r--r-- 2 hadoop supergroup 21 2014-02-20 21:04 /inputreader/a.txt
</pre><h4>执行</h4>
<pre class='brush:shell'>$ ./bin/hadoop jar study.hdfs-0.0.1-SNAPSHOT.jar TestRecordReader /inputreader /inputreaderout1
##
$ ./bin/hadoop fs -lsr /inputreaderout1
-rw-r--r-- 2 hadoop supergroup 0 2014-02-20 21:12 /inputreaderout1/_SUCCESS
drwxr-xr-x - hadoop supergroup 0 2014-02-20 21:11 /inputreaderout1/_logs
drwxr-xr-x - hadoop supergroup 0 2014-02-20 21:11 /inputreaderout1/_logs/history
-rw-r--r-- 2 hadoop supergroup 16451 2014-02-20 21:11 /inputreaderout1/_logs/history/job_201402201934_0002_1392901901142_hadoop_TestRecordReader
-rw-r--r-- 2 hadoop supergroup 48294 2014-02-20 21:11 /inputreaderout1/_logs/history/job_201402201934_0002_conf.xml
-rw-r--r-- 2 hadoop supergroup 23 2014-02-20 21:12 /inputreaderout1/part-r-00000
-rw-r--r-- 2 hadoop supergroup 23 2014-02-20 21:12 /inputreaderout1/part-r-00001
##
$ ./bin/hadoop fs -cat /inputreaderout1/part-r-00000
偶数行之和: 120
##
$ ./bin/hadoop fs -cat /inputreaderout1/part-r-00001
奇数行之和: 160
</pre>]]></content>
</entry>
<entry>
<title type="html"><![CDATA[Hadoop MapReduce Partitioner 组件]]></title>
<link href="http://kangfoo.u.qiniudn.com//article/2014/03/hadoop-mapreduce-partitioner--zu-jian/"/>
<updated>2014-03-03T22:20:00+08:00</updated>
<id>http://kangfoo.u.qiniudn.com//article/2014/03/hadoop-mapreduce-partitioner--zu-jian/</id>
<content type="html"><![CDATA[<p>Partitioner 过程发生在循环缓冲区发生溢写文件之后,merge 之前。可以让 Map 对 Key 进行分区,从而可以根据不同的 key 来分发到不同的 reducer 中去处理;</p>
<p>Hadoop默认的提供的是HashPartitioner。</p>
<p>可以自定义 key 的分发规则,自定义Partitioner:</p>
<ul>
<li>继承抽象类Partitioner,实现自定义的getPartition()方法;</li>
<li>通过job.setPartitionerClass()来设置自定义的Partitioner;</li>
</ul>
<h3>Partitioner 类</h3>
<p>旧api</p>
<pre class='brush:java'>public interface Partitioner<K2, V2> extends JobConfigurable {
int getPartition(K2 key, V2 value, int numPartitions);
}
</pre><p>新api</p>
<pre class='brush:java'>public abstract class Partitioner<KEY, VALUE> {
public abstract int getPartition(KEY key, VALUE value, int numPartitions);
}
</pre><h3>Partitioner应用场景演示</h3>
<p>需求:利用 Hadoop MapReduce 作业 Partitioner 组件分别统计每种商品的周销售情况。源代码 <a href="https://github.com/kangfoo/hadoop1.study/blob/master/kangfoo/study.hdfs/src/main/java/com/kangfoo/study/hadoop1/mp/typeformat/TestPartitioner.java">TestPartitioner.java</a>。<strong>出自于 <a href="http://new.osforce.cn/?mu=20140227220525KZol8ENMYdFQ6SjMveU26nEZ">开源力量</a> LouisT 老师的<a href="http://new.osforce.cn/course/101?mc101=20140301233857au7XG16o9ukfev1pmFCOfv2s">开源力量培训课-Hadoop Development</a>课件。</strong> (可使用 PM2.5 数据代替此演示程序)</p>
<ul>
<li><p>site1的周销售清单(a.txt,以空格分开):</p>
<pre class='brush:text'>shoes 20
hat 10
stockings 30
clothes 40
</pre></li>
<li><p>site2的周销售清单(b.txt,以空格分开):</p>
<pre class='brush:text'>shoes 15
hat 1
stockings 90
clothes 80
</pre></li>
<li><p>汇总结果:</p>
<pre class='brush:text'>shoes 35
hat 11
stockings 120
clothes 120
</pre></li>
<li><p>准备测试数据</p>
<pre class='brush:shell'>$ ./bin/hadoop fs -mkdir /testPartitioner/input
$ ./bin/hadoop fs -put a.txt /testPartitioner/input
$ ./bin/hadoop fs -put b.txt /testPartitioner/input
$ ./bin/hadoop fs -lsr /testPartitioner/input
-rw-r--r-- 2 hadoop supergroup 52 2014-02-18 22:53 /testPartitioner/input/a.txt
-rw-r--r-- 2 hadoop supergroup 50 2014-02-18 22:53 /testPartitioner/input/b.txt
</pre></li>
<li><p>执行 MapReduce 作业
此处使用 hadoop jar 命令执行,eclipse 插件方式有一定的缺陷。(hadoop eclipse 执行出现java.io.IOException: Illegal partition for hat (1))</p>
<pre class='brush:shell'>$ ./bin/hadoop jar study.hdfs-0.0.1-SNAPSHOT.jar TestPartitioner /testPartitioner/input /testPartitioner/output10
</pre></li>
<li><p>结果。 四个分区,分别存储上述四种产品的总销量的统计结果值。</p>
<pre class='brush:shell'>-rw-r--r-- 2 hadoop supergroup 9 2014-02-19 00:18 /testPartitioner/output10/part-r-00000
-rw-r--r-- 2 hadoop supergroup 7 2014-02-19 00:18 /testPartitioner/output10/part-r-00001
-rw-r--r-- 2 hadoop supergroup 14 2014-02-19 00:18 /testPartitioner/output10/part-r-00002
-rw-r--r-- 2 hadoop supergroup 12 2014-02-19 00:18 /testPartitioner/output10/part-r-00003
</pre></li>
</ul>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[Hadoop MapReduce Combiner 组件]]></title>
<link href="http://kangfoo.u.qiniudn.com//article/2014/03/hadoop-mapreduce-combiner--zu-jian/"/>
<updated>2014-03-03T22:19:00+08:00</updated>
<id>http://kangfoo.u.qiniudn.com//article/2014/03/hadoop-mapreduce-combiner--zu-jian/</id>
<content type="html"><![CDATA[<!-- 参考博客,学习怎么写明了。 http://blog.csdn.net/heyutao007/article/details/5725379-->
<p>combiner 作用是把一个 map 产生的多个 <key,valeu> 合并成一个新的 <key,valeu>,然后再将新的 <key,valeu> 作为 reduce 的输入;</p>
<p>combiner 函数在 map 函数与 reduce 函数之间,目的是为了减少 map 输出的中间结果,减少 reduce 复制 map 输出的数据,减少网络传输负载;</p>
<p>并不是所有情况下都能使用 Combiner 组件,它适用于对记录汇总的场景(如求和,平均数不适用)</p>
<h4>什么时候运行 Combiner</h4>
<ul>
<li>当 job 设置了 Combiner,并且 spill 的个数达到 min.num.spill.for.combine (默认是3)的时候,那么 combiner 就会 Merge 之前执行;</li>
<li>但是有的情况下,Merge 开始执行,但 spill 文件的个数没有达到需求,这个时候 Combiner 可能会在Merge 之后执行;</li>
<li>Combiner 也有可能不运行,Combiner 会考虑当时集群的一个负载情况。</li>
</ul>
<h4>测试 Combinner 过程</h4>
<p>代码 <a href="https://github.com/kangfoo/hadoop1.study/blob/fa6e68e52aa12ed0e22f98e6109f376ffbb6431f/kangfoo/study.hdfs/src/main/java/com/kangfoo/study/hadoop1/mp/typeformat/TestCombiner.java">TestCombiner</a></p>
<ol>
<li><p>以 wordcount.txt 为输入的词频统计</p>
<pre class='brush:shell'>$ ./bin/hadoop fs -lsr /test3/input
drwxr-xr-x - hadoop supergroup 0 2014-02-18 00:28 /test3/input/test
-rw-r--r-- 2 hadoop supergroup 983 2014-02-18 00:28 /test3/input/test/wordcount.txt
-rw-r--r-- 2 hadoop supergroup 626 2014-02-18 00:28 /test3/input/test/wordcount2.txt
</pre></li>
<li><p><strong>不启用 Reducer</strong> (输出,字节变大)</p>
<pre class='brush:shell'>drwxr-xr-x - kangfoo-mac supergroup 0 2014-02-18 00:29 /test3/output1
-rw-r--r-- 3 kangfoo-mac supergroup 0 2014-02-18 00:29 /test3/output1/_SUCCESS
-rw-r--r-- 3 kangfoo-mac supergroup 1031 2014-02-18 00:29 /test3/output1/part-m-00000 (-m 没有 reduce 过程的中间结果,每个数据文件对应一个数据分片,每个分片对应一个map任务)
-rw-r--r-- 3 kangfoo-mac supergroup 703 2014-02-18 00:29 /test3/output1/part-m-00001
</pre><p>结果如下(map过程并不合并相同key的value值):</p>
<pre class='brush:shell'>drwxr-xr-x 1
- 1
hadoop 1
supergroup 1
0 1
2014-02-17 1
21:03 1
/home/hadoop/env/mapreduce 1
drwxr-xr-x 1
- 1
hadoop 1
</pre></li>
<li><p><strong>启用 Reducer</strong></p>
<pre class='brush:shell'>drwxr-xr-x - kangfoo-mac supergroup 0 2014-02-18 00:29 /test3/output1
-rw-r--r-- 3 kangfoo-mac supergroup 0 2014-02-18 00:29 /test3/output1/_SUCCESS
-rw-r--r-- 3 kangfoo-mac supergroup 1031 2014-02-18 00:29 /test3/output1/part-m-00000
-rw-r--r-- 3 kangfoo-mac supergroup 703 2014-02-18 00:29 /test3/output1/part-m-00001
drwxr-xr-x - kangfoo-mac supergroup 0 2014-02-18 00:31 /test3/output2
-rw-r--r-- 3 kangfoo-mac supergroup 0 2014-02-18 00:31 /test3/output2/_SUCCESS
-rw-r--r-- 3 kangfoo-mac supergroup 705 2014-02-18 00:31 /test3/output2/part-r-00000
</pre><p>结果:</p>
<pre class='brush:text'>0:17:31,680 6
014-02-18 1
2014-02-17 11
2014-02-18 5
21:02 7
</pre></li>
<li><p>在日志或者 http://master11:50030/jobtracker.jsp 页面查找是否执行过 Combine 过程。
日志截取如下:</p>
<pre class='brush:text'>2014-02-18 00:31:29,894 INFO SPLIT_RAW_BYTES=233
2014-02-18 00:31:29,894 INFO Combine input records=140
2014-02-18 00:31:29,894 INFO Reduce input records=43
2014-02-18 00:31:29,894 INFO Reduce input groups=42
2014-02-18 00:31:29,894 INFO Combine output records=43
2014-02-18 00:31:29,894 INFO Reduce output records=42
2014-02-18 00:31:29,894 INFO Map output records=140
</pre></li>
</ol>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[Hadoop MapReduce 类型与格式]]></title>
<link href="http://kangfoo.u.qiniudn.com//article/2014/03/hadoop-mapreduce--lei-xing-yu-ge-shi/"/>
<updated>2014-03-03T22:18:00+08:00</updated>
<id>http://kangfoo.u.qiniudn.com//article/2014/03/hadoop-mapreduce--lei-xing-yu-ge-shi/</id>
<content type="html"><![CDATA[<p>MapReduce 的 map和reduce函数的输入和输出是键/值对(key/value pair) 形式的数据处理模型。</p>
<h2>MapReduce 的类型</h2>
<p>Hadoop1.x MapReduce 有2套API.旧api偏向与接口,新api偏向与抽象类,如无特殊默认列举为旧的api作讨论.</p>
<p>在Hadoop的MapReduce中,map和reduce函数遵循如下格式:</p>
<ul>
<li>map(K1, V1) –> list (K2, V2) // map:对输入分片数据进行过滤数据,组织 key/value 对等操作<br /></li>
<li>combine(K2, list(V2)) –> list(K2, V2) // 在map端对输出进行预处理,类似 reduce。combine 不一定适用任何情况,如:对总和求平均数。选用。</li>
<li>partition(K2, V2) –> integer // 将中间键值对划分到一个 reduce 分区,返回分区索引号。实际上,分区单独由键决定(值是被忽略的),分区内的键会排序,相同的键的所有值会合成一个组(list(V2))<br /></li>
<li>reduce(K2, list(V2)) –> list(K3, V3) // 每个 reduce 会处理具有某些特性的键,每个键上都有值的序列,是通过对所有 map 输出的值进行统计得来的,reduce 根据所有map传来的结果,最后进行统计合并操作,并输出结果。</li>
</ul>
<p>旧api类代码</p>
<pre class='brush:java'>public interface Mapper<K1, V1, K2, V2> extends JobConfigurable, Closeable {
void map(K1 key, V1 value, OutputCollector<K2, V2> output, Reporter reporter) throws IOException;
}
//
public interface Reducer<K2, V2, K3, V3> extends JobConfigurable, Closeable {
void reduce(K2 key, Iterator<V2> values, OutputCollector<K3, V3> output, Reporter reporter) throws IOException;
}
//
public interface Partitioner<K2, V2> extends JobConfigurable {
int getPartition(K2 key, V2 value, int numPartitions);
}
</pre><p>新api类代码</p>
<pre class='brush:java'>public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
… …
protected void map(KEYIN key, VALUEIN value,
Context context) throws IOException, InterruptedException {
context.write((KEYOUT) key, (VALUEOUT) value);
}
… …
}
//
public class Reducer<KEYIN,VALUEIN,KEYOUT,VALUEOUT> {
… …
protected void reduce(KEYIN key, Iterable<VALUEIN> values, Context context
) throws IOException, InterruptedException {
for(VALUEIN value: values) {
context.write((KEYOUT) key, (VALUEOUT) value);
}
}
… …
}
//
public interface Partitioner<K2, V2> extends JobConfigurable {
int getPartition(K2 key, V2 value, int numPartitions);
}
</pre><p>默认的 partitioner 是 HashPartitioner,对键进行哈希操作以决定该记录属于哪个分区让 reduce 处理,每个分区对应一个 reducer 任务。总槽数 solt=集群中节点数 * 每个节点的任务槽。实际值应该比理论值要小,以空闲一部分在错误容忍是备用。</p>
<p>HashPartitioner的实现</p>
<pre class='brush:java'>public class HashPartitioner<K, V> extends Partitioner<K, V> {
public int getPartition(K key, V value, int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
</pre><p>hadooop1.x 版本中</p>
<ul>
<li>旧的api,map 默认的 IdentityMapper, reduce 默认的是 IdentityReducer</li>
<li>新的api,map 默认的 Mapper, reduce 默认的是 Reducer</li>
</ul>
<p>默认MapReduce函数实例程序</p>
<pre class='brush:java'>public class MinimalMapReduceWithDefaults extends Configured implements Tool {
@Override
public int run(String[] args) throws Exception {
Job job = JobBuilder.parseInputAndOutput(this, getConf(), args);
if (job == null) {
return -1;
}
//
job.setInputFormatClass(TextInputFormat.class);
job.setMapperClass(Mapper.class);
job.setMapOutputKeyClass(LongWritable.class);
job.setMapOutputValueClass(Text.class);
job.setPartitionerClass(HashPartitioner.class);
job.setNumReduceTasks(1);
job.setReducerClass(Reducer.class);
job.setOutputKeyClass(LongWritable.class);
job.setOutputValueClass(Text.class);
job.setOutputFormatClass(TextOutputFormat.class);
return job.waitForCompletion(true) ? 0 : 1;
}
//
public static void main(String[] args) throws Exception {
int exitCode = ToolRunner.run(new MinimalMapReduceWithDefaults(), args);
System.exit(exitCode);
}
}
</pre><h2>输入格式</h2>
<h3>输入分片与记录</h3>
<p>一个输入分片(input split)是由单个 map 处理的输入块,即每一个 map 只处理一个输入分片,每个分片被划分为若干个记录( records ),每条记录就是一个 key/value 对,map 一个接一个的处理每条记录,输入分片和记录都是逻辑的,不必将他们对应到文件上。数据分片由数据块大小决定的。</p>
<p>注意,一个分片不包含数据本身,而是指向数据的引用( reference )。</p>
<p>输入分片在Java中被表示为InputSplit抽象类</p>
<pre class='brush:java'>public interface InputSplit extends Writable {
long getLength() throws IOException;
String[] getLocations() throws IOException;
}
</pre><p>InputFormat负责创建输入分片并将它们分割成记录,抽象类如下:</p>
<pre class='brush:java'>public interface InputFormat<K, V> {
InputSplit[] getSplits(JobConf job, int numSplits) throws IOException;
RecordReader<K, V> getRecordReader(InputSplit split,
JobConf job,
Reporter reporter) throws IOException;
}
</pre><p>客户端通过调用 getSpilts() 方法获得分片数目(怎么调到的?),在 TaskTracker 或 NodeManager上,MapTask 会将分片信息传给 InputFormat 的
createRecordReader() 方法,进而这个方法来获得这个分片的 RecordReader,RecordReader 基本就是记录上的迭代器,MapTask 用一个 RecordReader 来生成记录的 key/value 对,然后再传递给 map 函数,如下步骤:</p>
<ol>
<li>jobClient调用getSpilts()方法获得分片数目,将numSplits作为参数传入,以参考。InputFomat实现有自己的getSplits()方法。</li>
<li>客户端将他们发送到jobtracker</li>
<li>jobtracker使用其存储位置信息来调度map任务从而在tasktracker上处理分片数据</li>
<li>在tasktracker上,map任务把输入分片传给InputFormat上的getRecordReader()方法,来获取分片的RecordReader。</li>
<li>map 用一个RecordReader来生成纪录的键值对。</li>
<li>RecordReader的next()方法被调用,知道返回false。map任务结束。</li>
</ol>
<p>MapRunner 类部分代码(旧api)</p>
<pre class='brush:java'>public class MapRunner<K1, V1, K2, V2>
implements MapRunnable<K1, V1, K2, V2> {
… …
public void run(RecordReader<K1, V1> input, OutputCollector<K2, V2> output,
Reporter reporter)
throws IOException {
try {
// allocate key & value instances that are re-used for all entries
K1 key = input.createKey();
V1 value = input.createValue();
//
while (input.next(key, value)) {
// map pair to output
mapper.map(key, value, output, reporter);
if(incrProcCount) {
reporter.incrCounter(SkipBadRecords.COUNTER_GROUP,
SkipBadRecords.COUNTER_MAP_PROCESSED_RECORDS, 1);
}
}
} finally {
mapper.close();
}
}
……
}
</pre><h3>FileInputFormat类</h3>
<p>FileInputFormat是所有使用文件为数据源的InputFormat实现的基类,它提供了两个功能:一个定义哪些文件包含在一个作业的输入中;一个为输入文件生成分片的实现,把分片割成记录的作业由其子类来完成。</p>
<p><strong>下图为InputFormat类的层次结构</strong>:
<img src="http://zhaomingtai.u.qiniudn.com/FileInputFormat.png" alt="image" /></p>
<h4>FileInputFormat 类输入路径</h4>
<p>FileInputFormat 提供四种静态方法来设定 Job 的输入路径,其中下面的 addInputPath() 方法 addInputPaths() 方法可以将一个或多个路径加入路径列表,setInputPaths() 方法一次设定完整的路径列表(可以替换前面所设路 径)</p>
<pre class='brush:java'>public static void addInputPath(Job job, Path path);
public static void addInputPaths(Job job, String commaSeparatedPaths);
public static void setInputPaths(Job job, Path... inputPaths);
public static void setInputPaths(Job job, String commaSeparatedPaths);
</pre><p>如果需要排除特定文件,可以使用 FileInputFormat 的 setInputPathFilter() 设置一个过滤器:
<code>public static void setInputPathFilter(Job job, Class<? extends PathFilter> filter);</code>
它默认过滤隐藏文件中以”_“和”.“开头的文件</p>
<pre class='brush:java'> private static final PathFilter hiddenFileFilter = new PathFilter(){
public boolean accept(Path p){
String name = p.getName();
return !name.startsWith("_") && !name.startsWith(".");
}
};
</pre><h4>FileInputFormat 类的输入分片</h4>
<p>FileInputFormat 类一般分割超过 HDFS 块大小的文件。通常分片与 HDFS 块大小一样,然后分片大小也可以改变的,下面展示了控制分片大小的属性:</p>
<p>待补。 TODO</p>
<pre class='brush:java'>FileInputFormat computeSplitSize(long goalSize, long minSize,long blockSize) {
return Math.max(minSize, Math.min(goalSize, blockSize));
}
</pre><p>即:
<code>minimumSize < blockSize < maximumSize 分片的大小即为块大小。</code></p>
<p>重载 FileInputFormat 的 isSplitable() =false 可以避免 mapreduce 输入文件被分割。</p>
<h4>小文件与CombineFileInputFormat</h4>
<ol>
<li><p>CombineFileInputFormat 是针对小文件设计的,CombineFileInputFormat 会把多个文件打包到一个分片中,以便每个 mapper 可以处理更多的数据;减少大量小文件的另一种方法可以使用 SequenceFile 将这些小文件合并成一个或者多个大文件。</p>
</li>
<li><p>CombineFileInputFormat 不仅对于处理小文件实际上对于处理大文件也有好处,本质上,CombineFileInputFormat 使 map 操作中处理的数据量与 HDFS 中文件的块大小之间的耦合度降低了</p>
</li>
<li><p>CombineFileInputFormat 是一个抽象类,没有提供实体类,所以需要实现一个CombineFileInputFormat 具体
类和 getRecordReader() 方法(旧的接口是这个方法,新的接口InputFormat中则是createRecordReader())</p>
</li>
</ol>
<h4>把整个文件作为一条记录处理</h4>
<p>有时,mapper 需要访问问一个文件中的全部内容。即使不分割文件,仍然需要一个 RecordReader 来读取文件内容为 record 的值,下面给出实现这个功能的完整程序,详细解释见《Hadoop权威指南》。</p>
<h4>文本处理</h4>
<ol>
<li><p><strong>TextInputFileFormat</strong> 是默认的 InputFormat,每一行就是一个纪录</p>
</li>
<li><p>TextInputFileFormat 的 key 是 LongWritable 类型,存储该行在整个文件的偏移量,value 是每行的数据内容,不包括任何终止符(换行符和回车符),它是Text类型.
如下例
On the top of the Crumpetty Tree</br>
</br>
The Quangle Wangle sat,</br>
But his face you could not see,</br>
On account of his Beaver Hat.</br>
每条记录表示以下key/value对</br>
(0, On the top of the Crumpetty Tree)</br>
(33, The Quangle Wangle sat,)</br>
(57, But his face you could not see,)</br>
(89, On account of his Beaver Hat.</p>
</li>
<li><p>输入分片与 HDFS 块之间的关系:TextInputFormat 每一条纪录就是一行,很可能某一行跨数据库存放。</p>
</li>
</ol>
<p><img src="http://zhaomingtai.u.qiniudn.com/Figure%207-3.%20Logical%20records%20and%20HDFS%20blocks%20for%20TextInputFormat.png" alt="image" /></p>
<ol>
<li><p><strong>KeyValueTextInputFormat</strong>。对下面的文本,KeyValueTextInputFormat 比较适合处理,其中可以通过
mapreduce.input.keyvaluelinerecordreader.key.value.separator 属性设置指定分隔符,默认
值为制表符,以下指定”→“为分隔符
</br>
line1→On the top of the Crumpetty Tree</br>
line2→The Quangle Wangle sat,</br>
line3→But his face you could not see,</br>
line4→On account of his Beaver Hat.</p>
</li>
<li><p><strong>NLineInputFormat</strong>。如果希望 mapper 收到固定行数的输入,需要使用 NLineInputFormat 作为 InputFormat 。与 TextInputFormat 一样,key是文件中行的字节偏移量,值是行本身。</p>
</li>
</ol>
<p>N 是每个 mapper 收到的输入行数,默认时 N=1,每个 mapper 会正好收到一行输入,mapreduce.input.lineinputformat.linespermap 属性控制 N 的值。以刚才的文本为例。
如果N=2,则每个输入分片包括两行。第一个 mapper 会收到前两行 key/value 对:</p>
<p>(0, On the top of the Crumpetty Tree)</br>
(33, The Quangle Wangle sat,)</br>
另一个mapper则收到:</br>
(57, But his face you could not see,)</br>
(89, On account of his Beaver Hat.)</br></p>
<h4>二进制输入</h4>
<p><strong>SequenceFileInputFormat</strong>
如果要用顺序文件数据作为 MapReduce 的输入,应用 SequenceFileInputFormat。key 和 value 顺序文件,所以要保证map输入的类型匹配</p>
<p>SequenceFileInputFormat 可以读 MapFile 和 SequenceFile,如果在处理顺序文件时遇到目录,SequenceFileInputFormat 类会认为值正在读 MapFile 数据文件。</p>
<p><strong>SequenceFileAsTextInputFormat</strong> 是 SequenceFileInputFormat 的变体。将顺序文件(其实就是SequenceFile)的 key 和 value 转成 Text 对象</p>
<p><strong>SequenceFileAsBinaryInputFormat</strong>是 SequenceFileInputFormat 的变体。将顺序文件的key和value作为二进制对象</p>
<h4>多种输入</h4>
<p>对于不同格式,不同表示的文本文件输出的处理,可以用 <strong>MultipleInputs</strong> 类里处理,它允许为每条输入路径指定 InputFormat 和 Mapper。</p>
<p>MultipleInputs 类有一个重载版本的 addInputPath()方法:</p>
<ul>
<li>旧api列举<pre class='brush:java'>public static void addInputPath(JobConf conf, Path path, Class<? extends InputFormat> inputFormatClass)
</pre></li>
<li>新api列举<pre class='brush:java'>public static void addInputPath(Job job, Path path, Class<? extends InputFormat> inputFormatClass)
</pre>在有多种输入格式只有一个mapper时候(调用Job的setMapperClass()方法),这个方法会很有用。</li>
</ul>
<h4>DBInputFormat</h4>
<p>JDBC从关系数据库中读取数据的输入格式(参见权威指南)</p>
<h2>输出格式</h2>
<p>OutputFormat类的层次结构</p>
<p><img src="http://zhaomingtai.u.qiniudn.com/Figure%207-4.%20OutputFormat%20class%20hierarchy.png" alt="image" /></p>
<h3>文本输出</h3>
<p>默认输出格式是 <strong>TextOutputFormat</strong>,它本每条记录写成文本行,key/value 任意,这里 key和value 可以用制表符分割,用 mapreduce.output.textoutputformat.separator 书信可以改变制表符,与TextOutputFormat 对应的输入格式是 KeyValueTextInputFormat。</p>
<p>可以使用 NullWritable 来省略输出的 key 和 value。</p>
<h3>二进制输出</h3>
<ul>
<li><strong>SequenceFileOutputFormat</strong> 将它的输出写为一个顺序文件,因为它的格式紧凑,很容易被压缩,所以易于作为 MapReduce 的输入</li>
<li>把key/value对作为二进制格式写到一个 SequenceFile 容器中</li>
<li>MapFileOutputFormat 把 MapFile 作为输出,MapFile 中的 key 必需顺序添加,所以必须确保 reducer 输出的 key 已经排好序。</li>
</ul>
<h3>多个输出</h3>
<ul>
<li><p><strong>MultipleOutputFormat</strong> 类可以将数据写到多个文件中,这些文件名称源于输出的键和值。MultipleOutputFormat是个抽象类,它有两个子类:<strong>MultipleTextOutputFormat</strong> 和 <strong>MultipleSequenceFileOutputFormat</strong> 。它们是 TextOutputFormat 的和 SequenceOutputFormat 的多版本。</p>
</li>
<li><p><strong>MultipleOutputs</strong> 类
用于生成多个输出的库,可以为不同的输出产生不同的类型,无法控制输出的命名。它用于在原有输出基础上附加输出。输出是制定名称的。</p>
</li>
</ul>
<h4>MultipleOutputFormat和MultipleOutputs的区别</h4>
<p>这两个类库的功能几乎相同。MultipleOutputs 功能更齐全,但 MultipleOutputFormat 对 目录结构和文件命令更多de控制。</p>
<div style="height:0px;border-bottom:1px dashed red"></div>
<table width="100%" border="1" cellpadding="3" cellspacing="0" bordercolor="#eeeeee">
<tbody>
<tr>
<td><em>特征 </em></td>
<td><em>MultipleOutputFormat </em></td>
<td><em>MultipleOutputs </em></td>
</tr>