Pig Latin分析报告
对海量数据的按需分析处理需求不断增加,尤其是对于因特网公司,它们的技术革新主要依赖于对每天收集的数据的分析处理能力。要提高如此巨大数据集的存储和分析效率,必须采用高度并行的系统,例如:shared-nothing cluster。并行数据库产品,如:Teradata 提供了一种解决方案,但是这种方案的web规模扩展开销太大,性价比不高,而且为程序员提供了一种不太自然的编程语言-SQL。
事实上很多程序员更加偏爱过程式程序设计语言,因为这种语言的数据流更加清晰,便于分析和调试。这也是map/reduce编程模型流行的原因之一。另外map/reduce的运行于普通商业硬件组成的集群,软件框架保证了可靠性和可用性,通过低端硬件实现了高性能计算,这也是map/reduce的优势所在。
尽管如此,map/ reduce也存在一些限制,它的单输入,两阶段数据流编程模式过于苛刻,对于超出该限制之外的数据分析任务,需要进行一些额外的数据转换。另外,它没有通用操作子,即使是对最通用的操作,如:projection和filtering。这些限制导致map/reduce代码重用性和可维护性不高,任务的分析语义不够清晰,将对系统性能优化造成影响。
基于以上问题,Yahoo!开发了一种新的基于数据流的大规模数据分析处理语言-Pig Latin,该语言借鉴了SQL和map/reduce两者的优点,既具有类似SQL的灵活可变式性,又有过程式语言的数据流特点。
首先以一个例子来直观感受PigLatin的特点:
假设我们有一个表urls:(url,category,pagerank)
下面是一个简单的SQL查询,对每个足够大的分类,找出其包含的高pagerank的平均值。查询如下:
SELECT category,AVG(pagerank)
FROM urls WHERE pagerank>0.2
GROUP BY category HAVING COUNT(*)>106
与上述SQL等价的Pig Latin程序如下:
good_urls=FILTER urls BYpagerank>0.2;
groups=GROUP good_urls BY category;
big_groups=FILTER groups BYCOUNT(good_urls)>106;
output=FOREACH big_groups GENERATE
category, AVG(good_urls.pagerank);
可见,Pig Latin是一种一步一步顺序执行的编程语言,每一步都完成一次数据转换。这种风格为很多程序员所喜爱。同时每步的转换也都是高级抽象,例如:filtering,grouping,aggregation,抽象级别类似于SQL。
事实上,Pig Latin程序更像是对查询执行计划的规范。因此更易于程序员理解和控制。Pig Latin的实现系统叫pig,Yahoo!的程序员使用pig对大规模数据进行分析处理。Pig Latin程序被编译为mapreduce作业,在hadoop上执行。
Pig Latin语言在编程风格上与SQL有明显差别,前者凸显了程序的执行数据流,后者只关注最终结果。SQL更适合于编程经验不丰富的程序员,适合小规模数据集;Pig Latin更适合有经验的程序员和大规模数据集。
尽管Pig Latin程序提供了显示的操作序列,但实际的执行计划不一定按照该顺序执行,编译器会根据一定的规则对执行顺序进行一定的优化。举例如下:
假设某人对类型为spam网页的url感兴趣,并且要求高pagerank,则实现为:
spam_urls=FILTERurls BY isSpam(url);
highPR_spam_urls=FILTERspam_urls BY pagerank>0.8
按照上述给出的语句,程序会先找出所有的spam url,然后按照pagerank对spam url进行过滤。但是这种执行顺序可能是不高效的,尤其是当isSpam(url)是一个用户定义的大开销函数时。显然,这种情况下更高效的方式是先对全部url按照pagerank进行过滤,再对过滤后的url集合判别是否是spam_url,得出最终想要的结果集合。
Pig支持对各种存储格式的查询处理,前提是用户要给出特定的处理函数,该函数能够分析文件内容,提取需要的元组,因此Pig不需要像传统数据库管理系统那样在查询数据之前要花费大量时间导入数据。类似地,Pig程序的输出也会按照用户自定义函数将元组类型转换为特定格式的字节序列。这就简化了后续应用程序对Pig输出结果的使用,例如可视化应用程序或者类Exel的电子表格应用程序。
在像Yahoo!这样的大公司里,数据生态系统中包含大量的应用,而Pig只是其中的一个,因此使Pig具有操作外部文件中的数据,又具有一定的操作权限是非常重要的,目前Pig可以容易地与生态系统中其他应用进行交互。
传统数据库管理系统需要将数据导入到系统管理表主要是出于一下三点考虑:
1) 保证事务一致性
2) 通过元组标识符实现高效的点式查询
3) 为用户管理数据,记录模式信息以方便其他用户理解
但是,Pig只支持只读式数据分析,对数据的读取是顺序的,因此不需要事务一致性保证和基于索引的查询。Pig通常被用于分析每天或者每两天产生的临时数据集,不需要长期保留,因此数据托管和模式管理也是多余的。
Pig对模式的支持是可有可无的,用户可以在任何时候提供模式信息,也可以不提供。以我们在第一部分给出的例子为例,如果用户知道存储url表文件的第三个域是pagerank而不想在程序中提供模式,则第一行可以等价表示为:
good_urls=FILTERurls BY $2>0.2
很多时候,程序员想使用嵌套数据结构,例如:为获取术语在一组文件中出现的位置信息,程序员就一定希望为每个术语创建一个类似于Map<documentId,Set<positions>>的结构。
对于数据库而言,只允许扁平式表的存在,即只有原子字段可以作为列,否则会违反第一范式(1NF)。在满足第一范式的条件下抓取术语位置信息就要求创建两个表,对数据进行标准化处理:
term_info:(termId,termString,…)
position_info:(termId,documentId,positon)
然后按照termId进行表连接,再按照termId和documentId进行分组。
Pig Latin具有灵活的,完全嵌套的数据模型,允许复杂的非原子数据类型(set,map,tuple)作为一个表的字段。嵌套数据类型比1NF更适合Pig的执行环境,主要是出于一下几点考虑:
l 嵌套式数据模型更符合程序员在编程过程中的需求,对他们来讲这种数据模型比标准化数据格式更加自然。
l 存储在磁盘上的数据本身往往具有嵌套的特点。比如,web爬虫程序为每个url和来自该url的外部连接集合产生输出。Pig是直接对文件进行操作,将这种web规模的数据进行标准化,然后再通过多次表连接进行数据重组显然是不现实的。
l 嵌套数据模型有助于完成前边介绍的代数式语言目标,即一步一步执行,每步只完成一次数据转换。例如,GROUP原语产生的每个输出元组都会有一个非原子字段(来自于GROUP输入中属于每个组的所有元组的嵌套集合)。
l 嵌套数据模型有助于用户写出各式各样的用户定义函数(UDF),关于UDF将在下一节给出介绍。
对搜索日志,爬虫数据,点击流等数据分析的关键就是定制化处理。例如,一个用户对搜索词的自然语言填充感兴趣,或者判断一个特殊的网页是否是spam等都需要定制化处理。
为完成这种特殊的数据处理任务,Pig Latin提供UDF实现扩展支持。事实上,Pig Latin的所有处理(包括grouping,filtering,joining和per-tuple processing)都可以通过UDF定制。
UDF的输入和输出都可以使用嵌套数据模型,即可以使用非原子参数作为输入,输出也可以表示为非原子参数。考虑下面一个例子。
继续以第一个例子的设置为例,假设我们想为每个category,根据url的pagerank找出top10 urls。可以使用Pig Latin表示为:
groups= GROUP urls BY category;
output= FOREACH groups GENERATE
category, top10(urls)
其中top10就是一个UDF,接收一组url作为输入(每次针对一个组),为每个组输出一个按pagerank排序前10位的urls,该输出包含了一个非原子字段。
目前,Pig的UDF需要用java实现,正在开发对其他语言的支持。
Pig Latin的目标是处理web规模的数据,因此它在实现时屏蔽了所有非并行的计算,即它只实现了一些容易并行处理的原语,不容易并行处理的原语(如:非等价表连接以及相关子查询)都没有被实现。
但是,这些操作可以通过UDF来实现,Pig Latin不显式提供非并行原语,用户就可以很容易发现程序中瓶颈。
任何一种语言实现的应用程序,都要经历一个运行-调试-运行的循环迭代过程。这就需要一个调试环境,而对于Pig处理的数据规模而言,每次运行往往花费几十分钟甚至数小时,因此需要定制一种高效的调试环境。Pig开发了一种特定的交互式调试环境,对程序运行的每个步骤都以一个简单测试数据表来测试每步产生的输出结果。测试数据尽可能模拟真实数据,也基本可以完全表达程序的语义,并且,测试数据会随着程序的进化而自动调整,而不是固定不变的。
一步一步执行的测试数据有助于尽早发现错误(甚至可能在全数据集上第一次迭代执行之前),可以准确定位错误位置。
Pig的数据模型主要有以下四种类型构成:
Atom:一个原子类型包括一个简单的原子值(字符串、数等),如:’alice’。
Tuple:一个元组类型由一些列字段组成,每个字段可以是任意类型,例如:
(‘alice’,’lakers’)。
Bag:一组tuples构成一个bag,组成bag的每个元组是很灵活的,即bag中的每个tuple不需要具有相同的字段数和字段类型。Tuple可以嵌套,看一个bag实例:
Map:一个map由一组数据项和它们关联的索引键组成。和bag类型一样,map中每个数据项的模式也是很灵活的,即所有数据项不需要具有相同类型。但是,索引键必须是原子类型的数据,主要是为了高效查询。下面是一个map的例子:
上例的map中,索引键‘fan of’被映射为一个包含两个tuple类型的bag类型,而索引键‘age’被映射为原子类型20。Map类型对建模模式随时间变化的数据集非常有用,,例如:web服务器可能需要新添加一个新字段来存储日志信息,新字段可以通过在map中添加一个索引键来完成,不需改变现有程序,又可以通过新的程序完成对新字段的访问。
再来看看PigLatin的各种表达式类型以及对它们的操作。如下表所示:
由上表可见PigLatin语言的数据模型的灵活性,它可以任意嵌套,这种灵活性有助于完成”嵌套数据类型”一节给出的设计动机。
规范数据输入是PigLatin程序的第一步,这一步规范了输入数据的路径,以及数据解序列化方式,即采用什么方法将输入数据转化为PigLatin的数据模型。下面是一个LOAD示例:
上面的命令规范了三个方面的内容:
l 输入文件名为:query_log.txt
l 定制的文件解序列化方法为myLoad()
l 加载的元祖由三个字段组成,分别为userId,queryString和timestamp。
LOAD返回一个bag句柄,在上例中该句柄指向queries,可以直接作为后续命令的输入。实际上,该操作的实际效果并没有像数据库那样将数据载入表中,bag句柄在PigLatin中只是一个逻辑上的概念,即LOAD命令仅仅规范了输入文件名以其读取方式,而并没有将数据读取出来,除非用户显式给出命令,如STORE命令,该命令将在后面具体介绍。
数据通过LOAD命令载入后,就要指定对数据处理的方法。其中一个基本的操作是对数据集中每个元组的处理操作,通过FOREACH命令完成。举例如下:
上述命令对queries中每个元组(tuple)分别处理,每次处理产生一个输出元组。输出元组中第一个字段是输入元组的userId字段,第二个字段由用户自定义函数expandQuery产生,expandQuery的输入是输入元组的queryString字段。假设expandQuery会产生一个bag类型,作为输入查询串的扩展。则上述命令完成的数据转换功能可以用图1表示:
图1 在FOREACH+FLATTEN实例
在FOREACH命令中,对每个元组的处理是相互独立的,可以完全并行处理,这符合PigLatin语言的完全并行化目标。
图1中最后一步数据转换涉及一个flattening处理,它的作用是对嵌套数据类型的消解,每个FLATTEN消除一层嵌套。命令如下:
该命令PigLatin中又一个非常常用的命令,功能是保留一些想要的子数据集,丢弃其余,下面是一个具体的例子:
该例子完成的功能是过滤掉用户‘bot’的信息,采用的操作子为neq,是一种字符串比较操作子,与其对应的还有eq,另外还有其他类型的过滤操作子,如:数字比较操作子(==,!=)和逻辑连接子(AND,OR,NOT)。
FILTER命令的比较标准可以采用任意的表达式实现,因此,我们也可以采用UDFs的方式实现上述功能,考虑,上述命令在实际中可能不会很好地实现我们想要达到的效果,因为用户‘bot’可能不唯一,因此一种更好的实现方式如下:
数据处理过程中经常需要将一个或多个数据集中的元组按照它们的相关性进行分组聚集,以便于对它们进行联合处理。在PigLatin中这种功能通过COGROUP命令完成。假设我们通过LOAD命令得到两个数据集,分别为:
其中results包含了不同的搜索串,用url表示的搜索结果以及它们在搜索结果列表中出现的位置。而revenue包含不同的搜索串,广告位置以及搜索串关联的广告在指定位置获取的广告费收入。下面按照queryString字段对两个数据集进行分组聚集,命令如下:
该命令产生的结果如图2所示:
图2 COGROUP与JOIN
一般,COGROUP命令的输出包含一些列元组,每个组对应一个元组。每个元组的第一个字段是组标识符,唯一标识了一个组。后续每个字段都是一个bag,每个来自于一个输入数据集。第i个bag包含第i个输入中对应每个组的所有输入元组。
有人可能会质疑COGROUP原语的必要性,因为它的功能和JOIN操作很类似,图2对两者做出了比较。显然,JOIN等价于对COGROUP结果中每个bag字段进行叉积。其实JOIN只是COGROUP的目标之一,在叉积之前,通过对COGROUP结果进行定制化操作可以得到多种结果,而不单单是JOIN的效果。这一点可以通过下面一个例子做进一步解释说明:
该例子的目标是找出搜索产生的每个url对搜索产生的广告收入的贡献,从而判断每个url价值,可以采用如下命令完成:
DistributeRevenue是一个用户定制的处理函数,它以某次查询串搜索产生的results和revenue信息作为输入参数。输出一个由一些列(url,revenue)元组构成的bag。假设该函数的处理逻辑是搜索串产生的top位置广告收入全部来自于url列表中的第一个,slide位置的广告收入平均分配到每个url。这种处理逻辑下产生的输出结果如图2所示。
要使用SQL来完成同样的功能,首先JOINBY queryString,然后GROUP BY queryString,最后应用一个定制化的aggregation。但是在执行连接操作时系统会计算两个表的叉积,而执行aggregation是又要去掉叉积的效果,整个处理过程会相当低效,查询也会变得难以理解。
其实,COGROUP语句是PigLatin与SQL之间的关键区别,COGROUP遵循前边阐述的PigLatin代数式语言的设计目标,即分布执行,每步只处理一个数据转换。COGROUP只完成将对所有输入数据集的元组聚集,对于之后的处理,用户可以选择在这些元组上应用一个aggregation,或者对它们进行叉积以实现JOIN的效果,还可以定制其他处理逻辑。而在SQL中,分组操作不能独立存在,要么与aggregation联合(group-by-aggregate查询),要么根叉积操作联合(JOIN操作)。
需要注意的是,PigLatin的嵌套式数据模型是COGROUP能独立存在的根本原因。它将所有输入的元组按照一定的规则进行分组,然后存入嵌套bags中。这类原语不可能在SQL中出现,原因是它的数据模型是扁平化的,不允许嵌套。当然,嵌套数据可能会引起执行效率的问题,对这一点,在后面进行详细说明。
GROUP是COGROUP在单一输入数据集上的特例。这种情况就直接使用关键字GROUP,继续上边的例子,假如我们想统计每个查询串带来的总体广告收入(一个典型的group-by-aggregate查询),可以通过下列语句完成:
上边通过COGROUP与JOIN的对比,体现了COGROUP的灵活性,但是,并不是所有的用户都需要这种灵活性,很多情况下,只需要一个简单的等价连接就可以满足用户要求。因此,PigLatin通过关键字JOIN为用户提供了等价连接命令,举例如下:
事实上,JOIN在语义上与COGROUP后接flattening是等价的,因此上边JOIN命令可以改写为如下等价命令:
在PigLatin中map-reduce不再作为一个编程模型而存在,它们实际上以及成为了PigLatin的UDFs。map函数每次对一个输入元组进行操作,输出一个bag,该bag包含一组key-value,而reduce函数每次对一个key关联的一组values进行操作,产生最后的结果。举例如下:
第一行语句对输入数据集的每个元组应用map操作(这里的*号与SQL中意义相同,表示输入元组的每个字段都传入map),然后对map输出扁平化处理,产生一组key-value对。
用户可以通过该命令将PigLatin表达式序列结果序列化到一个具体的文件,具体命令如下:
该例采用了一个定制化函数myStore()完成具体的序列化功能。也可以省略USING短语,这是系统会默认使用一个基于空格分隔的纯文本文件序列化函数,系统还提供了内置的序列化和去序列化函数,支持LOAD/STORE任意嵌套数据。
PigLatin有很多类似于SQL的命令,分别为:
l UNION:返回两个或者多个bags的联合体。
l CROSS:返回两个或者多个bags的叉积。
l ORDER:按照某些字段对一个bag进行排序。
l DISTINCT:实现一个bag中的元组去重,实际上,该命令是首先按照一个bag的所有字段进行分组聚集,然后进行投影的简化表示。
PigLatin的实现系统叫做Pig,它的架构目标是高扩展性,允许不同系统以插件的方式作为PigLatin的执行平台。目前的实现,使用Hadoop作为执行平台,Hadoop是一个开源可扩展的map-reduce实现,PigLatin程序被编译为map-reduce作业,然后在hadoop执行。下面具体介绍设计上的三个关键技术。
客户端提交PigLatin命令后,首先由Pig解释器对命令进行分析,检验输入文件和语句中出现的bags的有效性。例如,’c=COGROUP a BY ….,b BY …..’命令,Pig会检验a和b两个bags已经被定义,逻辑计划的构建过程具有递归性和依赖性,以上例进行说明,c的逻辑计划要依赖于a和b的逻辑计划,因此只有当a的逻辑计划和b的逻辑计划构建完毕才能构建c的逻辑计划。
所有逻辑计划,只是一个处理流程,而没有任何处理过程发生,处理过程是在调用STORE之后发生的。调用STORE之后,逻辑计划就被编译成具体的执行计划并被调度执行。这种懒惰执行方式有很多好处,比如允许内存管道,再比如跨多个PigLatin命令的重定序。
Pig系统采用预处理与处理分离的架构,即程序的分析,逻辑计划的生成独立于执行平台。只有逻辑计划被编译为具体的执行计划时才依赖于具体的执行平台。目前,Pig采用比较成熟的开源map-reduce实现hadoop作为执行平台。这种分离体系结构,有助于系统的发展。
图3MAP-REDUCE计划
PigLatin逻辑计划到具体的MAP-REDUCE执行计划的编译过程遵循MAP-REDUCE处理模式的本质特点,MAP-REDUCE处理模式本质上具有大规模分组聚集的能力,即map任务通过指派keys对数据进行分组,reduce任务每次处理一个组的数据。Pig编译器遵循这种特点将逻辑计划中每个COGROUP命令转化为一个不同的map-reduce作业。如图3所示。
与COGROUP命令C相关联的map函数首先根据C中的BY短语将keys指派为相应的元组,而reduce最初不进行任何操作。这样每个COGROUP命令就成为两个不同的map-reduce任务的界限。第一个COGROUP命令C1的命令都由C1对应的map函数处理,如图3所示。记第i个COGROUP命令为Ci,则在Ci与Ci+1之间的命令序列要么(a)由Ci的reduce函数处理,要么(b)由Ci+1的map函数处理。考虑grouping后常跟aggregation,Pig选择方案(a),认为这种方案有利于减少不同map-reduce作业之间序列化的数据量。
当COGROUP操作的输入为多数据集时,map函数会为每个元组附加一个字段以标识元组是源于哪个数据集。相应的reduce函数对该信息进行解码,利用该信息将元组插入到适当的位置。
LOAD的并行性通过HDFS实现(文件存储在HDFS上)。对于一个map-reduce作业,多个map和reduce运行实例是并行的,从而可以实现FILTER和FOREACH操作的自动并行化。多个map实例产生的输出按照一定的划分规则划分为不同部分,每个对应一个reduce实例,它们以并行化方式执行,从而实现了(CO)GROUP的并行性。
ORDER命令被编译为两个map-reduce作业。第一个作业通过对输入进行取样来确定排序键的数量。第二个作业根据排序键数量确定划分区域(保证均匀划分),接着在reduce阶段进行本地排序,最后产生一个全局的排序文件。
在逻辑计划到map-reduce任务的转化过程中会产生一些额外开销,这些开销是由map-reduce处理模式带来的。例如,在两个连续的map-reduce作业之间必须对数据以冗余的方式序列化HDFS之上,处理多个输入数据集时必须对每个元组添加一个额外字段以标识数据来源。虽然map-reduce模型给Pig带来了一点额外开销,但是带来的好处是更大的,如:并行化,负载均衡,容错等。其实,除了map-reduce模式执行平台,我们也可以插入其他的执行平台以补充map-reduce执行平台的不足之处,减少不必要的开销。
本节主要介绍一些Yahoo!实现的一些数据分析的例子,具体如下:
Rollup聚集:使用pig进行一般的分类处理都会涉及到对用户日志,web爬虫数据以及其他数据集的各种Rollup聚集处理。例如:计算搜索词,可以按照几天,几周,几个月,或者地域等不同标准进行聚集。某些任务可能需要两次或者多词连续的聚集过程。例如,计算每个用户的搜索次数,然后计算每个用户的平均次数。有些任务可能还需要先进行JOIN在聚集,如对在web页面锚文本串中匹配搜索串,并计算每个web页面的匹配数。在这种情况下,Pig代表用户精心设计了一系列map-reduce作业,以完成任务目标。
采用Pig而非数据库/OLAP进行rollup分析的主要原因在于搜索日志并且增加,因此不适合载入数据库进行托管,只能以文件形式管理。Pig提供一种简单的处理方式,即直接对在文件上进行聚集计算,这种方式也使得定制化处理变得简单。如IP到地域的映射,n-gram提取。
时序分析:对搜索日志的时序分析主要包括对搜索查询如果随时间变化的变化规律。要实现该任务,一般是先对一段时间的搜索串与另一个过去时间段搜索串进行联合分组,然后对每个组的查询进行定制处理。
会话分析:这里所说的会话是特指web用户会话,即用户浏览的web页面序列和点击序列。通过分析这些数据计算各种度量,如用户会话的平均持续之间,用户在离开一个站点之前点击了多少连接,每天/周/月点击方式的变化规律。该分析任务主要由两步操作组成,即第一步:按照用户和/或web站点对活动日志进行分组。第二步:按照时间戳对每组的活动进行排序。该Pig使用场景中,PigLatin的嵌套数据模型刚好提供了一种对会话表示和操纵自然抽象。