Google在2001年发布图像搜索功能时,只有2.5亿索引图像,不到10年,这个巨大的搜索功能已经可以检索超过100亿个图像了,每分钟有35小时的内容上传到YouTube。据称,Twitter每天平均处理5500万tweet。今年早些时候,搜索功能每天记录6亿条查询记录。这 就是我们讨论大数据的意义所在。
如此大规模的数据一度仅限于大企业、学校和政府机构 — 这些机构有能力购买昂贵的超级计算机、能够雇用员工保障其运行。今天,由于存储成本的降低和处理能力的商品化,一些小公司,甚至个人都可以存储和挖掘同样的数据,推动新一轮的应用程序创新。
大数据革命技术之一是MapReduce,一个编程模式,是Google针对大规模、分布式数据而开发的。在本文中,我将介绍Apache的开源MapReduce实现、Hadoop,也有人将其称之为云计算的杀手应用程序。
关于Hadoop
Apache的Hadoop框架本质上是一个用于分析大数据集的机制,不一定位于数据存储中。Hadoop提取出了MapReduce的大规模数据分析引擎,更易于开发人员理解。Hadoop可以扩展到无数个节点,可以处理所有活动和相关数据存储的协调。
Hadoop的众多特性和配置使其成为一个十分有用且功能强大的框架,其用途和功能令人惊讶。Yahoo!以及其他许多组织已经找到了一个高效机制来分析成堆的字节数。在单个节点上运行Hadoop也很容易;您所需要的只是一些需要分析的数据,以及熟悉一般的Java代码。Hadoop也可和 uby、Python以及C++一起使用。
作为处理大数据集的概念框架,MapReduce对于使用许多计算机来解决分布式问题而言是高度优化的。顾名思义,这个框架由两个函数构成。map 函数专用于获取大数据输入,并将其分成小片段,然后交由其他进程进行操作。reduce函数整理map收集的各个回应,然后显示最后的输出。
在Hadoop中,您可以通过扩展Hadoop自身的基类来定义map和reduce实现。实现和输入输出格式被一个指定它们的配置联系在一起。Hadoop非常适合处理包含结构数据的大型文件。Hadoop可以对输入文件进行原始解析,这一点特别有用,这样您就可以每次处理一行。定义一个map函数实际上只是一个关于确定您从即将输入的文本行中捕获什么内容的问题。
数据,无处不在的数据!
美国政府产生大量数据,只有一部分是普通民众所感兴趣的。各种政府机构免费发布关于US经济健康状况和更改社会人口统计资料的数据。U.S. Geological Survey (USGS)发布国内外地震数据。
世界各地每天都有很多个小型地震发生。其中大多数发生在地壳深处,没有人能感觉到,尽管如此,但是监听站仍然会进行记录。USGS以CSV(或逗号分隔值)文件的格式发布每周地震数据。
每周文件平均不是很大 — 只有大约100KB左右。但是,它可以作为学习Hadoop的基础。记住,Hadoop有能力处理更 大的数据集。
跟踪震动
我近期从USGS网站下载的CSV文件有大约920多行。如 清单 1 所示:
清单 1.一个USGS地震数据文件的行数统计
以下是引用片段: $> wc -l eqs7day-M1.txt 920 eqs7day-M1.txt |
CVS文件内容如清单2所示(这是前两行):
清单 2. CVS文件的前两行
以下是引用片段: $> head -n 2 eqs7day-M1.txt Src,Eqid,Version,Datetime,Lat,Lon,Magnitude,Depth,NST,Region ci,14896484,2,"Sunday, December 12, 2010 23:23:20 UTC",33.3040,-116.4130,1.0,11.70,22, "Southern California" |
我第一个想到的就是使用简单的grep命令来搜索每天的地震数。看看这个文件,我发现数据记录是从12月12开始的。因此我对该字符串执行了一次grep-c,其结果如清单3所示:
清单 3.12月12有多少次地震发生?
以下是引用片段: $> grep -c 'December 12' eqs7day-M1.txt 98 |
现在,我知道在12月12日有98条记录,也就是说有98次地震。我只能沿着这条记录向下,对12月10日的记录执行一次grep,接着是 11 号,等等。这听起来有点乏味。更糟糕的是,我还需要知道在该文件中的是哪几天。我确实不关心这些,甚至有时候我可能无法获取该信息。事实上,我只想知道在七天这样一个时间段内任何一天的地震次数,使用Hadoop我就可以很容易的获取这一信息。
Hadoop只需要几条信息就可以回答我的第一个和第二个问题:即,要处理哪条输入以及如何处理map和reduce。我也必须提供了一个可以将每件事都联系起来的作业。在我开始处理这些代码之前,我需要花点时间确定我的CSV数据整齐有序。
使用opencsv进行数据解析
除了地震CSV文件的第一行之外,第一行是文件头,每一行都是一系列逗号分隔数据值。我只对数据的3个部分感兴趣:日期、地点和震级。为了获取这些资料,我将使用一个很棒的开源库opencsv,它将会帮助我分析CSV文件。
作为一个测试优先的工具,我首先编写一个快捷JUnit测试,确认我可以从CSV文件的一个样例行获取的我所需要的信息,如清单 4 所示:
清单 4. 解析一个CSV行
以下是引用片段: public class CSVProcessingTest { private final String LINE = "ci,14897012,2,\"Monday, December 13, 2010 " + "14:10:32 UTC\",33.0290,-115." + "5388,1.9,15.70,41,\"Southern California\""; @Test public void testReadingOneLine() throws Exception { String[] lines = new CSVParser().parseLine(LINE); assertEquals("should be Monday, December 13, 2010 14:10:32 UTC", "Monday, December 13, 2010 14:10:32 UTC", lines[3]); assertEquals("should be Southern California", "Southern California", lines[9]); assertEquals("should be 1.9", "1.9", lines[6]); } } |
正如您在清单4中所看到的,opencsv处理逗号分隔值非常容易。该解析器仅返回一组String,所以有可能获取位置信息(别忘了,在 Java语言中数组和集合的访问是从零开始的)。
转换日期格式
当使用MapReduce进行处理时,map函数的任务是选择一些要处理的值,以及一些键。这就是说,map主要处理和返回两个元素:一个键和一个值。回到我之前的需求,我首先想知道每天会发生多少次地震。因此,当我在分析地震文件时,我将发布两个值:键是日期,值是一个计数器。reduce函数将对计数器(只是一些值为1的整数)进行总计。因此,提供给我的是在目标地震文件中某一个日期出现的次数。
由于我只对24小时时段内的信息感兴趣,我得剔除每个文件中的日期的时间部分。在 清单5中,我编写了一个快速测试,验证如何将一个传入文件中的特定日期信息转换成一个更一般的24小时日期:
清单 5.日期格式转换
以下是引用片段: @Test public void testParsingDate() throws Exception { String datest = "Monday, December 13, 2010 14:10:32 UTC"; SimpleDateFormat formatter = new SimpleDateFormat("EEEEE, MMMMM dd, yyyy HH:mm:ss Z"); Date dt = formatter.parse(datest); formatter.applyPattern("dd-MM-yyyy"); String dtstr = formatter.format(dt); assertEquals("should be 13-12-2010", "13-12-2010", dtstr); } |
Hadoop的map和reduce
现在我已经找到了处理CSV文件以及其日期格式的解决方法。我要开始在Hadoop中实施我的map和reduce函数了。这个过程需要理解 Java 泛型,因为 Hadoop 选择使用显式类型,为了安全起见。
当我使用 Hadoop 定义一个映射实现时,我只扩展Hadoop的Mapper类。然后我可以使用泛型来为传出键和值指定显式类。类型子句也指定了传入键和值,这对于读取文件分别是字节数和文本行数。
EarthQuakesPerDateMapper 类扩展了Hadoop的Mapper对象。它显式地将其输出键指定为一个Text对象,将其值指定为一个IntWritable,这是一个Hadoop特定类,实质上是一个整数。还要注意,class子句的前两个类型是LongWritable和Text,分别是字节数和文本行数。
由于类定义中的类型子句,我将传入map方法的参数类型设置为在context.write子句内带有该方法的输出。如果我想指定其他内容,将会出现一个编译器问题,或Hadoop将输出一个错误消息,描述类型不匹配的消息。
清单 6.一个映射实现
以下是引用片段: public class EarthQuakesPerDateMapper extends Mapper<LongWritable, Text, Text, IntWritable> { @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { if (key.get() > 0) { try { CSVParser parser = new CSVParser(); String[] lines = parser.parseLine(value.toString()); SimpleDateFormat formatter = new SimpleDateFormat("EEEEE, MMMMM dd, yyyy HH:mm:ss Z"); Date dt = formatter.parse(lines[3]); formatter.applyPattern("dd-MM-yyyy"); String dtstr = formatter.format(dt); context.write(new Text(dtstr), new IntWritable(1)); } catch (ParseException e) {} } } } |
清单 7.一个 map 输出和 reduce 输入的逻辑视图
以下是引用片段: "13-12-2010":[1,1,1,1,1,1,1,1] "14-12-2010":[1,1,1,1,1,1] "15-12-2010":[1,1,1,1,1,1,1,1,1] |
注意,context.write(new Text(dtstr), new IntWritable(1))(在清单6中)构建了如 清单7所示的逻辑集合。正如您所了解的,context是一个保存各种信息的Hadoop数据结构。context被传递到reduce实现,reduce获取这些值为1的值然后总和起来。因此,一个 reduce 实现逻辑上创建如 清单8所示的数据结构:
清单 8.一个reduce输出视图
以下是引用片段: "13-12-2010":8 "14-12-2010":6 "15-12-2010":9 |
清单 9.reduce实现
以下是引用片段: public class EarthQuakesPerDateReducer extends Reducer<Text, IntWritable, Text, IntWritable> { @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { int count = 0; for (IntWritable value : values) { count++; } context.write(key, new IntWritable(count)); } } |
以下是引用片段: "13-12-2010":[1,1,1,1,1,1,1,1] -> "13-12-2010":8 |
定义一个Hadoop Job
现在我已经对我的map和reduce实现进行了编码,接下来所要做的是将所有这一切链接到一个Hadoop Job。定义一个Job比较简单:您需要提供输入和输出、map和reduce实现(如清单6和清单9所示)以及输出类型。在本例中我的输出类型和 reduce 实现所用的是同一个类型。
清单 10. 一个将map和redece绑在一起的Job
以下是引用片段: public class EarthQuakesPerDayJob { public static void main(String[] args) throws Throwable { Job job = new Job(); job.setJarByClass(EarthQuakesPerDayJob.class); FileInputFormat.addInputPath(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); job.setMapperClass(EarthQuakesPerDateMapper.class); job.setReducerClass(EarthQuakesPerDateReducer.class); job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); System.exit(job.waitForCompletion(true) ? 0 : 1); } } |
为了执行这个小框架,我需要将这些类打包。我还需要告知 Hadoop 在哪里可以找到opencsv二进制文件。然后可以通过命令行执行Hadoop,如 清单11所示:
清单 11.执行 Hadoop
以下是引用片段: $> export HADOOP_CLASSPATH=lib/opencsv-2.2.jar $> hadoop jar target/quake.jar com.b50.hadoop.quake.EarthQuakesPerDayJob ~/temp/mreduce/in/ ~/temp/mreduce/out |
完成这些后,您可以使用任何编辑器查看输出文件内容。还可以选择直接使用hadoop命令。正如 清单12所示:
清单 12.读取Hadoop输出
以下是引用片段: $> hadoop dfs -cat part-r-00000 05-12-2010 43 06-12-2010 143 07-12-2010 112 08-12-2010 136 09-12-2010 178 10-12-2010 114 11-12-2010 114 12-12-2010 79 |
编写另一个Mapper
接下来,我想找到地震发生在哪里,以及如何快速计算出在我的研究范围内记录地震次数最多的是哪个区域。当然,您已经猜到了,Hadoop可以轻松地做到。在这个案例中,键不再是日期而是区域。因此,我编写了一个新的Mapper类。
清单 13.一个新的map实现
以下是引用片段: public class EarthQuakeLocationMapper extends Mapper<LongWritable, Text, Text, IntWritable> { @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { if (key.get() > 0) { String[] lines = new CSVParser().parseLine(value.toString()); context.write(new Text(lines[9]), new IntWritable(1)); } } } |
相比一个庞大的位置和数字列表,我将结果限制在那些7天内出现10次的区域。
清单 14.哪里的地震较多?
以下是引用片段: public class EarthQuakeLocationReducer extends Reducer<Text, IntWritable, Text, IntWritable> { @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { int count = 0; for (IntWritable value : values) { count++; } if (count >= 10) { context.write(key, new IntWritable(count)); } } } |
清单14中的代码和清单9中的代码非常类似;然而,在本例中,我限制了输出大于或等于10。接下来,我将map和reduce,以及其他 Job 实现绑在一起,进行打包,然后和平常一样执行Hadoop获取我的新答案。
使用hadoop dfs目录显示我所请求的新值:
清单 15.地震区域分布
以下是引用片段: $> hadoop dfs -cat part-r-00000 Andreanof Islands, Aleutian Islands, Alaska 24 Arkansas 40 Baja California, Mexico 101 Central Alaska 74 Central California 68 Greater Los Angeles area, California 16 Island of Hawaii, Hawaii 16 Kenai Peninsula, Alaska 11 Nevada 15 Northern California 114 San Francisco Bay area, California 21 Southern Alaska 97 Southern California 115 Utah 19 western Montana 11 |
从清单15还可以得到什么?首先,北美洲西海岸,从墨西哥到阿拉斯加是地震高发区。其次,阿肯色州明显位于断带层上,这是我没有意识到的。最后,如果您居住在北部或者是南加州(很多软件开发人员都居住于此),您周围的地方每隔 13 分钟会震动一次。
结束语
使用Hadoop分析数据轻松且高效,对于它对数据分析所提供的支持,我只是了解皮毛而已。Hadoop的设计旨在以一种分布式方式运行,处理运行map和reduce的各个节点之间的协调性。作为示例,本文中我只在一个JVM上运行Hadoop,该JVM仅有一个无足轻重的文件。
Hadoop本身是一个功能强大的工具,围绕它还有一个完整的、不断扩展的生态系统,可以提供子项目至基于云计算的Hadoop服务。Hadoop生态系统演示了项目背后丰富的社区活动。来自社区的许多工具证实了大数据分析作为一个全球业务活动的可行性。有了Hadoop,分布式数据挖掘和分析对所有软件创新者和企业家都是可用的,包括但不限于Google和Yahoo!这类大企业。