天天看点

Hive总结(十)Hive 输入输出适配类(输出CSV,XML)

在最初使用 hive ,应该说上手还是挺快的。 Hive 提供的类 SQL 语句与 mysql 语句极为相似,语法上有大量相同的地方,这给我们上手带来了很大的方便,但是要得心应手地写好这些语句,还需要对 hive 有较好的了解,才能结合 hive 特色写出精妙的语句。

关于 hive 语言的详细语法可参考官方 wiki 的语言手册:http://wiki.apache.org/hadoop/Hive/LanguageManual

虽然语法风格为我们提供了便利,但初次使用遇到的问题还是不少的,下面针对业务场景谈谈我们遇到的问题,和对 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 方法即可,示例代码如下:

1.  public boolean next(LongWritable key, BytesWritable value)
2.          throws IOException {
3.  
4.          while ( reader .next(key, text ) ) {
5.  
6.          String strReplace = text .toString().toLowerCase().replace( "@$_$@" , "\001" );
7.  
8.          Text txtReplace = new Text();
9.  
10.          txtReplace.set(strReplace );
11.  
12.          value.set(txtReplace.getBytes(), 0, txtReplace.getLength());
13.  
14.          return true ;
15.  
16.        }
17.  
18.           return false ;
19.  
20.  }
21.  
22.          重写 HiveIgnoreKeyTextOutputFormat 中 RecordWriter 中的 write 方法,示例代码如下:
23.  
24.      public void write (Writable w) throws IOException {
25.  
26.        String strReplace = ((Text)w).toString().replace( "\001" , "@$_$@" );
27.  
28.        Text txtReplace = new Text();
29.  
30.        txtReplace.set(strReplace);
31.  
32.        byte [] output = txtReplace.getBytes();
33.  
34.        bytesWritable .set(output, 0, output. length );
35.  
36.        writer .write( bytesWritable );
37.  
38.  }      

复制代码

自定义 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;

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 格式输出,由于代码较简单,这里给出源码示例:

1.  public String evaluate(String... strs) {
2.         StringBuilder sb = new StringBuilder();
3.  
4.         for ( int i = 0; i < strs. length ; i++) {
5.  
6.             sb.append(ConvertCSVField(strs[i])).append( ',' );
7.  
8.         }
9.  
10.         sb.deleteCharAt(sb.length()-1);
11.  
12.         return sb.toString();
13.  
14.  }复制代码
      

需要注意的是,要使用 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 格式,被简化的示例代码如下:

1.  public void write(Writable w) throws IOException {
2.             String[] strFields = ((Text) w).toString().split( "\001" );
3.  
4.             StringBuffer sbXml = new StringBuffer();
5.  
6.             if ( strResultType .equals( "keyword" )) {
7.  
8.      sbXml.append( "<record><keyword>" ).append(strFields[0]).append(
9.  
10.      "</keyword><count>" ).append(strFields[1]).append(           "</count><increment>" ).append(strFields[2]).append(
11.  
12.      "</increment><rate>" ).append(strFields[3]).append(
13.  
14.  "</rate></result>" );
15.  
16.             }
17.  
18.             Text txtXml = new Text();
19.  
20.             byte [] strBytes = sbXml.toString().getBytes( "utf-8" );
21.  
22.             txtXml.set(strBytes, 0, strBytes. length );
23.  
24.             byte [] output = txtXml.getBytes();
25.  
26.             bytesWritable .set(output, 0, output. length );
27.  
28.             writer .write( bytesWritable );
29.  
30.      }      

其中的 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 这类所有行都是有效数据的文件。