hive简介
hive是一个基于hadoop的开源数据仓库工具,用于存储和处理海量结构化数据。它把海量数据存储于hadoop文件系统,而不是数据库,但提供了一套类数据库的数据存储和处理机制,并采用HQL(类SQL)语言对这些数据进行自动化管理和处理。我们可以把hive中海量结构化数据看成一个个的表,而实际上这些数据是分布式存储在HDFS中的。Hive经过对语句进行解析和转换,最终生成一系列基于hadoop的map/reduce任务,通过执行这些任务完成数据处理。
Hive诞生于facebook的日志分析需求,面对海量的结构化数据,hive以较低的成本完成了以往需要大规模数据库才能完成的任务,并且学习门槛相对较低,应用开发灵活而高效。
Hive自2009.4.29发布第一个官方稳定版0.3.0至今,不过一年的时间,正在慢慢完善,网上能找到的相关资料相当少,尤其中文资料更少,本文结合业务对hive的应用做了一些探索,并把这些经验做一个总结,所谓前车之鉴,希望读者能少走一些弯路。
Hive的官方wiki请参考这里:
官方主页在这里:
hive-0.5.0源码包和二进制发布包的下载地址
二、部署
由于Hive是基于hadoop的工具,所以hive的部署需要一个正常运行的hadoop环境。以下介绍hive的简单部署和应用。
部署环境:
操作系统:Red Hat Enterprise Linux AS release 4 (Nahant Update 7)
Hadoop:hadoop-0.20.2,正常运行
部署步骤如下:
1、下载最新版本发布包,传到hadoop的namenode节点上,解压得到hive目录。假设路径为:/opt/hadoop/hive-0.5.0-bin
2、设置环境变量HIVE_HOME,指向hive根目录/opt/hadoop/hive-0.5.0-bin。由于hadoop已运行,检查环境变量JAVA_HOME和HADOOP_HOME是否正确有效。
3、切换到$HIVE_HOME目录,hive配置默认即可,运行bin/hive即可启动hive,如果正常启动,将会出现“hive>”提示符。
4、在命令提示符中输入“show tables;”,如果正常运行,说明已部署成功,可供使用。
常见问题:
1、执行“show tables;”命令提示“FAILED: Error in metadata: java.lang.IllegalArgumentException: URI: does not have a scheme”,这是由于hive找不到存放元数据库的数据库而导致的,修改conf/hive-default.xml配置文件中的hive.metastore.local为true即可。由于hive把结构化数据的元数据信息放在第三方数据库,此处设置为true,hive将在本地创建derby数据库用于存放元数据。当然如果有需要也可以采用mysql等第三方数据库存放元数据,不过这时hive.metastore.local的配置值应为false。
2、如果你已有一套nutch1.0系统正在跑,而你不想单独再去部署一套hadoop环境,你可以直接使用nutch1.0自带的hadoop环境,但这样的部署会导致hive不能正常运行,提示找不到某些方法。这是由于nutch1.0使用了commons-lang-2.1.jar这个包,而hive需要的是commons-lang-2.4.jar,下载一个2.4版本的包替换掉2.1即可,nutch和hive都能正常运行。
三、应用场景
本文主要讲述使用hive的实践,业务不是关键,简要介绍业务场景,本次的任务是对搜索日志数据进行统计分析。
集团搜索刚上线不久,日志量并不大。这些日志分布在5台前端机,按小时保存,并以小时为周期定时将上一小时产生的数据同步到日志分析机,统计数据要求按小时更新。这些统计项,包括关键词搜索量pv,类别访问量,每秒访问量tps等等。
基于hive,我们将这些数据按天为单位建表,每天一个表,后台脚本根据时间戳将每小时同步过来的5台前端机的日志数据合并成一个日志文件,导入hive系统,每小时同步的日志数据被追加到当天数据表中,导入完成后,当天各项统计项将被重新计算并输出统计结果。
以上需求若直接基于hadoop开发,需要自行管理数据,针对多个统计需求开发不同的map/reduce运算任务,对合并、排序等多项操作进行定制,并检测任务运行状态,工作量并不小。但使用hive,从导入到分析、排序、去重、结果输出,这些操作都可以运用hql语句来解决,一条语句经过处理被解析成几个任务来运行,即使是关键词访问量增量这种需要同时访问多天数据的较为复杂的需求也能通过表关联这样的语句自动完成,节省了大量工作量。
四、Hive实战
初次使用hive,应该说上手还是挺快的。Hive提供的类SQL语句与mysql语句极为相似,语法上有大量相同的地方,这给我们上手带来了很大的方便,但是要得心应手地写好这些语句,还需要对hive有较好的了解,才能结合hive特色写出精妙的语句。
关于hive语言的详细语法可参考官方wiki的语言手册:
虽然语法风格为我们提供了便利,但初次使用遇到的问题还是不少的,下面针对业务场景谈谈我们遇到的问题,和对hive功能的定制。
1、分隔符问题
首先遇到的是日志数据的分隔符问题,我们的日志数据的大致格式如下:
2010-05-24 00:00:02@$_$@QQ2010@$_$@all@$_$@NOKIA_1681C@$_$@1@$_$@10@$_$@@$_$@-1@$_$@10@$_$@application@$_$@1
从格式可见其分隔符是“@$_$@”,这是为了尽可能防止日志正文出现与分隔符相同的字符而导致数据混淆。本来hive支持在建表的时候指定自定义分隔符的,但经过多次测试发现只支持单个字符的自定义分隔符,像“@$_$@”这样的分隔符是不能被支持的,但是我们可以通过对分隔符的定制解决这个问题,hive的内部分隔符是“\001”,只要把分隔符替换成“\001”即可。
经过探索我们发现有两条途径解决这个问题。
a)自定义outputformat和inputformat。
Hive的outputformat/inputformat与hadoop的outputformat/inputformat相当类似,inputformat负责把输入数据进行格式化,然后提供给hive,outputformat负责把hive输出的数据重新格式化成目标格式再输出到文件,这种对格式进行定制的方式较为底层,对其进行定制也相对简单,重写InputFormat中RecordReader类中的next方法即可,示例代码如下:
publicbooleannext(LongWritable key, BytesWritable value)
throwsIOException {
while( reader .next(key, text ) ) {
String strReplace = text .toString().toLowerCase().replace( "@$_$@" , "\001" );
Text txtReplace = newText();
txtReplace.set(strReplace );
value.set(txtReplace.getBytes(), 0, txtReplace.getLength());
returntrue;
}
returnfalse;
}
重写HiveIgnoreKeyTextOutputFormat中RecordWriter中的write方法,示例代码如下:
publicvoidwrite (Writable w) throwsIOException {
String strReplace = ((Text)w).toString().replace( "\001" , "@$_$@" );
Text txtReplace = newText();
txtReplace.set(strReplace);
byte[] output = txtReplace.getBytes();
bytesWritable .set(output, 0, output. length );
writer .write( bytesWritable );
}
自定义outputformat/inputformat后,在建表时需要指定outputformat/inputformat,如下示例:
stored as INPUTFORMAT 'com.aspire.search.loganalysis.hive.SearchLogInputFormat' OUTPUTFORMAT 'com.aspire.search.loganalysis.hive.SearchLogOutputFormat'
b)通过SerDe(serialize/deserialize),在数据序列化和反序列化时格式化数据。
这种方式稍微复杂一点,对数据的控制能力也要弱一些,它使用正则表达式来匹配和处理数据,性能也会有所影响。但它的优点是可以自定义表属性信息SERDEPROPERTIES,在SerDe中通过这些属性信息可以有更多的定制行为。
2、数据导入导出
a)多版本日志格式的兼容
由于hive的应用场景主要是处理冷数据(只读不写),因此它只支持批量导入和导出数据,并不支持单条数据的写入或更新,所以如果要导入的数据存在某些不太规范的行,则需要我们定制一些扩展功能对其进行处理。
我们需要处理的日志数据存在多个版本,各个版本每个字段的数据内容存在一些差异,可能版本A日志数据的第二个列是搜索关键字,但版本B的第二列却是搜索的终端类型,如果这两个版本的日志直接导入hive中,很明显数据将会混乱,统计结果也不会正确。我们的任务是要使多个版本的日志数据能在hive数据仓库中共存,且表的input/output操作能够最终映射到正确的日志版本的正确字段。
这里我们不关心这部分繁琐的工作,只关心技术实现的关键点,这个功能该在哪里实现才能让hive认得这些不同格式的数据呢?经过多方尝试,在中间任何环节做这个版本适配都将导致复杂化,最终这个工作还是在inputformat/outputformat中完成最为优雅,毕竟inputformat是源头,outputformat是最终归宿。具体来说,是在前面提到的inputformat的next方法中和在outputformat的 write 方法中完成这个适配工作。
b) Hive 操作本地数据
一开始,总是把本地数据先传到 HDFS ,再由 hive 操作 hdfs 上的数据,然后再把数据从 HDFS 上传回本地数据。后来发现大可不必如此, hive 语句都提供了“ local ”关键字,支持直接从本地导入数据到 hive ,也能从 hive 直接导出数据到本地,不过其内部计算时当然是用 HDFS 上的数据,只是自动为我们完成导入导出而已。
3、数据处理
日志数据的统计处理在这里反倒没有什么特别之处,就是一些SQL语句而已,也没有什么高深的技巧,不过还是列举一些语句示例,以示hive处理数据的方便之处,并展示hive的一些用法。
a)为hive添加用户定制功能,自定义功能都位于hive_contrib.jar包中
add jar /opt/hadoop/hive-0.5.0-bin/lib/hive_contrib.jar;
b)统计每个关键词的搜索量,并按搜索量降序排列,然后把结果存入表keyword_20100603中
create table keyword_20100603 as select keyword,count(keyword) as count from searchlog_20100603 group by keyword order by count desc;
c)统计每类用户终端的搜索量,并按搜索量降序排列,然后把结果存入表device_20100603中
create table device_20100603 as select device,count(device) as count from searchlog_20100603 group by device order by count desc;
d)创建表time_20100603,使用自定义的INPUTFORMAT和OUTPUTFORMAT,并指定表数据的真实存放位置在'/LogAnalysis/results/time_20100603'(HDFS路径),而不是放在hive自己的数据目录中
create external table if not exists time_20100603(time string, count int) stored as INPUTFORMAT 'com.aspire.search.loganalysis.hive.XmlResultInputFormat' OUTPUTFORMAT 'com.aspire.search.loganalysis.hive.XmlResultOutputFormat' LOCATION '/LogAnalysis/results/time_20100603';
e)统计每秒访问量TPS,按访问量降序排列,并把结果输出到表time_20100603中,这个表我们在上面刚刚定义过,其真实位置在'/LogAnalysis/results/time_20100603',并且由于XmlResultOutputFormat的格式化,文件内容是XML格式。
insert overwrite table time_20100603 select time,count(time) as count from searchlog_20100603 group by time order by count desc;
f)计算每个搜索请求响应时间的最大值,最小值和平均值
insert overwrite table response_20100603 select max(responsetime) as max,min(responsetime) as min,avg(responsetime) as avg from searchlog_20100603;
g)创建一个表用于存放今天与昨天的关键词搜索量和增量及其增量比率,表数据位于'/LogAnalysis/results/keyword_20100604_20100603',内容将是XML格式。
create external table if not exists keyword_20100604_20100603(keyword string, count int, increment int, incrementrate double) stored as INPUTFORMAT 'com.aspire.search.loganalysis.hive.XmlResultInputFormat' OUTPUTFORMAT 'com.aspire.search.loganalysis.hive.XmlResultOutputFormat' LOCATION '/LogAnalysis/results/keyword_20100604_20100603';
h)设置表的属性,以便XmlResultInputFormat和XmlResultOutputFormat能根据output.resulttype的不同内容输出不同格式的XML文件。
alter table keyword_20100604_20100603 set tblproperties ('output.resulttype'='keyword');
i)关联今天关键词统计结果表(keyword_20100604)与昨天关键词统计结果表(keyword_20100603),统计今天与昨天同时出现的关键词的搜索次数,今天相对昨天的增量和增量比率,并按增量比率降序排列,结果输出到刚刚定义的keyword_20100604_20100603表中,其数据文件内容将为XML格式。
insert overwrite table keyword_20100604_20100603 select cur.keyword, cur.count, cur.count-yes.count as increment, (cur.count-yes.count)/yes.count as incrementrate from keyword_20100604 cur join keyword_20100603 yes on (cur.keyword = yes.keyword) order by incrementrate desc;
j)
4、用户自定义函数UDF
部分统计结果需要以CSV的格式输出,对于这类文件体全是有效内容的文件,不需要像XML一样包含version,encoding等信息的文件头,最适合用UDF(user define function)了。
UDF函数可直接应用于select语句,对查询结构做格式化处理之后,再输出内容。自定义UDF需要继承org.apache.hadoop.hive.ql.exec.UDF ,并实现evaluate 函数,Evaluate 函数支持重载,还支持可变参数。我们实现了一个支持可变字符串参数的UDF,支持把select得出的任意个数的不同类型数据转换为字符串后,按CSV格式输出,由于代码较简单,这里给出源码示例:
publicString evaluate(String... strs) {
StringBuilder sb = newStringBuilder();
for( inti = 0; i < strs. length ; i++) {
sb.append(ConvertCSVField(strs[i])).append( ',' );
}
sb.deleteCharAt(sb.length()-1);
returnsb.toString();
}
需要注意的是,要使用UDF功能,除了实现自定义UDF外,还需要加入包含UDF的包,示例:
add jar /opt/hadoop/hive-0.5.0-bin/lib/hive_contrib.jar;
然后创建临时方法,示例:
CREATE TEMPORARY FUNCTION Result2CSv AS ‘com.aspire.search.loganalysis.hive. Result2CSv';
使用完毕还要drop方法,示例:
DROP TEMPORARY FUNCTION Result2CSv;
5、输出XML格式的统计结果
前面看到部分日志统计结果输出到一个表中,借助XmlResultInputFormat和XmlResultOutputFormat格式化成XML文件,考虑到创建这个表只是为了得到XML格式的输出数据,我们只需实现XmlResultOutputFormat即可,如果还要支持select查询,则我们还需要实现XmlResultInputFormat,这里我们只介绍XmlResultOutputFormat。
前面介绍过,定制XmlResultOutputFormat我们只需重写write即可,这个方法将会把hive的以’\001’分隔的多字段数据格式化为我们需要的XML格式,被简化的示例代码如下:
publicvoidwrite(Writable w) throwsIOException {
String[] strFields = ((Text) w).toString().split( "\001" );
StringBuffer sbXml = newStringBuffer();
if( strResultType .equals( "keyword" )) {
sbXml.append( "" ).append(strFields[0]).append(
"" ).append(strFields[1]).append( "" ).append(strFields[2]).append(
"" ).append(strFields[3]).append(
"" );
}
Text txtXml = newText();
byte[] strBytes = sbXml.toString().getBytes( "utf-8" );
txtXml.set(strBytes, 0, strBytes. length );
byte[] output = txtXml.getBytes();
bytesWritable .set(output, 0, output. length );
writer .write( bytesWritable );
}
其中的strResultType .equals( "keyword" ) 指定关键词统计结果,这个属性来自以下语句对结果类型的指定,通过这个属性我们还可以用同一个outputformat输出多种类型的结果。
alter table keyword_20100604_20100603 set tblproperties ('output.resulttype'='keyword');
仔细看看write函数的实现便可发现,其实这里只输出了XML文件的正文,而XML的文件头和结束标签在哪里输出呢?所幸我们采用的是基于outputformat的实现,我们可以在构造函数输出version,encoding等文件头信息,在close()方法中输出结束标签。
这也是我们为什么不使用UDF来输出结果的原因,自定义UDF函数不能输出文件头和文件尾,对于XML格式的数据无法输出完整格式,只能输出CSV这类所有行都是有效数据的文件。
五、总结
Hive是一个可扩展性极强的数据仓库工具,借助于hadoop分布式存储计算平台和hive对SQL语句的理解能力,我们所要做的大部分工作就是输入和输出数据的适配,恰恰这两部分IO格式是千变万化的,我们只需要定制我们自己的输入输出适配器,hive将为我们透明化存储和处理这些数据,大大简化我们的工作。本文的重心也正在于此,这部分工作相信每一个做数据分析的朋友都会面对的,希望对您有益。
本文介绍了一次相当简单的基于hive的日志统计实战,对hive的运用还处于一个相对较浅的层面,目前尚能满足需求。对于一些较复杂的数据分析任务,以上所介绍的经验很可能是不够用的,甚至是hive做不到的,hive还有很多进阶功能,限于篇幅本文未能涉及,待日后结合具体任务再详细阐述。