再见了Hadoop MapReduce

        再见MapReduce,欢迎Cascading的到来
        我们最近作了大量的hadoop MapReduce处理,并且我们很快意识到手动编写MapReduce代码是多么痛苦的一件事情。在一些场景应用中我们的流程需要多达10个MapReduce作业的顺序执行,需要手动协调多个作业的中间数据和执行顺序。此外,任何有做过MapReduce工作流的人都知道基于MapReduce的思维是一件多么困难的事情。
        幸运的是,我们发现了新发现了一个名为Cascading伟大的开源项目,使用它能够减少我们的痛苦。Cascading是一个Chris Wensel先生智慧的产物,Chris Wensel先生作了很多优秀的开发工作和API编写使使得我们能够解决很多问题。Cascading远离抽象的MapReduce框架而将我们带入到一个更加自然的逻辑模型中,并且提供了工作流管理层用于处理像中间数据,过期数据。
        Cascading抽象出来的MapReduce逻辑模型被放置在tuples,pipes,tap模型中。数据被表示为"Tuples"的列表对象,例如我可以有一个tuple("url","stats"),其中"url"是一个hadoop 系统中的Text 对象,"stats"是我自己的"UrlStats"复杂对象,包含了一些方法,用于获取点击数量,平均时间等。Tuples在"Stream(流)"中被放在一起,并且流中所有tuples对象都有相同的field。
        在tuples流上面的操作被称为pipe。有很多种pipe,每个pipe包含了一类tuple流的处理。举例来说,每(Each)遍历一个pipe将对特定的tuple对象申请一个自定义函数。(GroupBy)分组pipe将首先将tuple对象合起来,按照tuple的field进行分组,并且所有的pipe针对所有的tuple对象在分组内针申请一个聚合函数。
        这里有个深入的demo用来描述"Each"遍历,假如我们tuple流是("number1","number2"),实际中可能如下:
        (1, 2) 
        (5, 7)
        (12, 5)
        假如这个tuple流当前在一个被称为工作流的pipe上。现在架设我们有一个"Double"类型的对象,并且它实现了一些操作方法,它能够使输入参数乘倍(*2)并且向某个被称为"double"的field输出结果,那让我们来看看下面的Java代码:
//”workflow” contains tuples of the form (“number1″, “number2″) 
workflow = new Each(workflow, new Fields("number1"), new Double(),new Fields("number2","double"));

 

        这是一个Tuple流中将"Double"操作应用于所有的tuple对象的代码。第二个参数"number1"字段应当被用于Double对象的双倍函数操作。当Double对象的函数操作完成时,我们的二元组tuple将由原来的("number1","number2")变为三元tuple组("number1","number2","double")。最后一个参数表明,我们仅获得tuple的"number2"项和"double"项之后就将该数据递归传递给下一个pipe。上述运行的这个demo最终将会产生一个如下tuple("number2","double")的流输出如下: 
        (2,2)
        (7,10)
        (5,24)
        
        现在让我们利用Hadoop WordCount的经典demo看一看GroupBy和Every这两个应用。假设我们有一个名为workflow的pipe,其包含了一个tuple流,其中tuple组成如("word","count")。假设这些tuple对象中的count是文本文件行纪录中单词出现次数,而下面这段代码则用来描述单词出现频率的最终统计:
workflow = new GroupBy(workflow, new Fields("word"));
workflow = new Every(workflow, new Fields("count"),new Sum("total"),new Fields("word","total"));

 

        让我来运行看看这段代码的执行过程,首先统计每行的词频信息如下:
        ("banana", 10)
        ("rose", 2)
        ("sleep", 5)
        ("rose", 7)
        ("rose", 10)
        ("banana", 2)
        
        然后让我们看看GroupBy这段代码的group流:
        "banana":
              ("banana", 10)
              ("banana", 2)
        "rose":
              ("rose", 2)
              ("rose", 7)
              ("rose", 10)
        "sleep":
              ("sleep", 6)
        
        然后在Every阶段我们将打散之前的结构并且产生如下内容:
        ("banana", 12)
        ("rose", 19)
        ("sleep", 6)
        
        在Every的例子中,第二行代码使用"count"字段进行求和运算,通过声明了Sum对象和其方法进行运算,并且将其输出的结果命名为"total" 字段。最后我们获得指定字段后就将该数据递归传递给下一个pipe。
        Cascading最强大的功能就是能够fork pipe,也能够把pipe归并在一起。例如你有一个pipe的tuple类似于(“customer_id”, “name”),另外一个pipe的tuple类似于 (“cust_id”, “age”),Cascading能够非常容易的把这些pipe Join在一起得到类似于(“name”, “age”)的pipe tuple。这种操作被称为CoGroup,你既可以做inner join,outer join,或者left join,right join等混合方式。对于Join方式的代码,看起来如下:
Pipe namePipe; // this contains (“customer_id”, “name”) 
Pipe agePipe; // this contains (“cust_id”, “age”)
// do the join
Pipe workflow = new CoGroup(namePipe, new Fields(“customer_id”),
agePipe, new Fields(“cust_id”), new Fields(“id1″, “name”, “id2″, “age”));
// strip away the “id” fields
workflow = new Each(workflow, new Fields(“name”, “age”), new Identity());.
        一旦你已经构建了你的操作放入pipe集中,你就可以告诉Cascading如何呼叫一个抽象的Tap 来检索和持久化数据。这些tap了解如何将存储数据转化为Tuple对象,反之亦然也知道如何存储数据,在哪里存储数据。Cascading有大量内置的tap,上述两个demo中通过使用SequenceFile和Text文本格式进行存储。如果你希望使用自定义数据格式存储,你需要声明自定义的tap对象。我们已经完成了tap分支并且能够无缝工作。
        一旦你已经声明了多个tap 对象,将其中一个tap对象作为pipe集的source,并且另外一个tap作为pipe集的sink,这将会创建一个流,有点类似cloudera公司的Flume项目的source/sink机制。 运行这个流将从source tap的输入中读取tuple集,通过pipe集运行这些tuple,并且将输出的最终数据写入到sink tap中。
       在底层, Cascading调用这些pipe集到一系列的MapReduce作业中。这些tap对象在指定输入输出数据路径时,同时指定输入和输出格式。Cascading管理着所有的中间数据,必须获得Map/Reduce作业顺序。例如,GroupBy之后紧跟着执行Every,Every之后紧跟着Each,依次执行。
        这是一个Cascading的官方demo,在这个demo中,利用正则表达式只需要通过Map阶段就可以解析出apache log数据格式,为了更进一步阐述Cascading 对于Map/Reduce简化的革命性变化,我还增加了两步,第一步通过"event"字段进行分组,第二步统计"event"的count值,标准的Map/Reduce简化。
        
//如果输出文件存在则删除
deleteFile(out);
//从HDFS系统中创建输入Tap对象,输入格式为Text File,以行偏移量作为key,以行内容作为value输入。
Tap localLogTap = new Hfs( new TextLine(),in);
//创建tuple对象的列集
Fields apacheFields = new Fields( "ip", "time", "method", "event", "status", "size" );
//声明正则表达式用于解析APACHE LOG
String apacheRegex = "^([^ ]*) +[^ ]* +[^ ]* +\\[([^]]*)\\] +\\\"([^ ]*) ([^ ]*) [^ ]*\\\" ([^ ]*) ([^ ]*).*$";
int[] allGroups = {1, 2, 3, 4, 5, 6};
//依赖于正则表达式,我们将得到apacheFields对象中的每一个字段值。创建tuple流的的解析对象
RegexParser parser = new RegexParser( apacheFields, apacheRegex, allGroups );
//创建pipe,以默认InputFormat输入的"line"作为输入,实用RegexParser对象解析,返回apacheFields6个字段
Pipe importPipe = new Each( "parser", new Fields( "line" ), parser );
//利用event字段进行group by,类似于默认使用Map/Reduce HashPartitioner算法,使用GroupCompator进行分组计算
importPipe=new GroupBy("groupby event",importPipe,new Fields("event"));
//求count运算,在reduce阶段利用分组后key所对应Iterable<value>遍历进行Count运算
importPipe=new Every(importPipe,new Fields("event"),new Count(),new Fields("event","count"));
Tap remoteLogTap = new Hfs( new TextLine(), out );
Properties properties = new Properties();
FlowConnector.setApplicationJarClass( properties, testEach.class );
Flow parsedLogFlow = new FlowConnector( properties ).connect( localLogTap, remoteLogTap, importPipe );
//开始执行。
parsedLogFlow.start();
//同步等待完成。
parsedLogFlow.complete();     
 
        最终呈现结果:
/ 10
/archives.html 3
/archives/000005.html 2
/archives/000021.html 1
/archives/000024.html 1
/archives/000026.html 1
/archives/000032.html 1
/archives/000047.html 2
/archives/000055.html 1
/archives/000064.html 2
/archives/000075.html 2
 
非常简单的代码描述,却实现了相对复杂的Map/Reduce执行。
yes,再见了Map/Reduce,的确如本文前半段的作者所描述的那样。。。

 

posted on 2012-03-24 10:03  reck for zhou  阅读(877)  评论(0编辑  收藏  举报

导航