NYC's Blog - Hive 2018-09-23T21:20:00+08:00 Typecho http://niyanchun.com/feed/atom/tag/hive/ <![CDATA[Hive的分桶和采样]]> http://niyanchun.com/bucket-and-sampling-in-hive.html 2018-09-23T21:20:00+08:00 2018-09-23T21:20:00+08:00 NYC https://niyanchun.com Hive除了有分区(Partition),还有分桶(Bucket),上一篇文章《Hive的分区》中介绍了分区,本文接着介绍分桶,以及和分桶经常一起使用的采样(Sampling)

其实不管是分区还是分桶都是为了更好的管理数据。分区将表的数据分到不同的目录存储,从而在查询的时候可以通过where条件过滤一部分数据,减小查询的数据量从而提高性能。但分区的这种机制往往在数据符合以下条件时才会表现的比较好:

  • 分区数目为有限个:一般也不能太大,不然太多的文件和目录对于HDFS的NameNode会造成比较大的内存压力。
  • 各个分区的数据量比较均衡:这个好理解,如果90%的数据跑到一个分区去,那分区的意义就不是很大了。

然而,并非所有场景都是符合上面两条的。比如比较常见的按照国家等地理位置去分区的时候,可能几个大国的数据就占了总数据量的百分之七八十,而剩下的所有国家只占了百分之二三十,也就是不符合上面的第二条。这个时候分区的好处就大打折扣了。为了克服这个问题,Hive增加了分桶的机制。我们先介绍一些分桶相关的基础概念,然后通过例子再做说明。

1. 分桶

1.1 基础理论

分桶的概念其实比较简单:指定桶的个数,选中表中的若干个列,通过对数据中这些列的值做哈希决定数据应该存储到哪个桶里面去,公式表示为:bucket num = hash_function(bucketing_column) mod num_buckets,不同的数据类型哈希函数不同。关于分桶还有以下一些关键点:

  • 根据分桶的原理,列(哈希列)值相同的的数据肯定存在同一个桶里面;
  • 在建表语句中使用CLUSTERED BY指定哈希列;
  • 物理上一个桶对应磁盘上一个文件,桶号从1开始计数;
  • 桶可以和分区一起使用,也可以单独使用;
  • 分桶机制可以保证基本上每部分的数据量基本是均衡的。

分桶也可以带来如下一些好处:

  • 分桶表比不分桶表的采样(samping)效率高。Hive提供了采样机制,用于我们从大量数据中提取一部分来做测试或调试,而分桶可以帮助我们更好的采样(见后面的例子)。
  • Map过程中的join会更快。主要有两方面原因:1. 分桶后,文件大小均衡。2. join的时候左表可以计算对应的右表符合条件的行在哪个桶里面,直接去获取数据即可。
  • 和分区一样,分桶后查询数据会更快
  • 我们在桶内做排序,这样可以让Map里面的join操作更快速。

下面我们看一个例子。

1.2 例子展示

场景需求还是使用上篇文章里面的用户数据UserRecords.txt,但这次只使用国家作为分区列,州做哈希列,也就是分桶列,并且桶内按照城市名排序。

和分区表一样,分桶表也不能使用LOAD DATA的方式导入数据,所以我们还是像前文一样,先把数据导到一张临时表temp_user里面,这个步骤省略。如果不清楚的,请看上一篇文章(Hive的分区)。然后我们创建分桶表:

# 语法
CREATE [TEMPORARY] [EXTERNAL] TABLE [IF NOT EXISTS] [db_name.]table_name      
[(col_name data_type [COMMENT col_comment], ... [constraint_specification])]
  [COMMENT table_comment]
  [PARTITIONED BY (col_name data_type [COMMENT col_comment], ...)]
  [CLUSTERED BY (col_name, col_name, ...) [SORTED BY (col_name [ASC|DESC], ...)] INTO num_buckets BUCKETS]

# 创建bucketed_user表
CREATE TABLE bucketed_user (
    firstname  VARCHAR(64),
    lastname    VARCHAR(64),
    address     STRING,
    city        VARCHAR(64),
    state           VARCHAR(64),
    post        STRING,
    phone1      VARCHAR(64),
    phone2      STRING,
    email       STRING,
    web         STRING
)
    COMMENT 'A bucketed sorted user table'
    PARTITIONED BY(country VARCHAR(64))
    CLUSTERED BY(state) SORTED BY(city) INTO 32 BUCKETS;

我们使用可以看到,不像分区列是一个虚拟列,分桶列是表中的字段。在插入字段之前,我们先介绍两个和分桶相关的配置项:

  • hive.enforce.bucketing:如果建表时设置了分桶,插入数据的时候是否强制分桶。Hive 2.x版本之前默认为false,之后去掉了该配置,并将默认情况改为true。见HIVE-12331.
  • hive.enforce.sorting:如果建表时设置了分桶,插入数据的时候是排序,Hive 2.x版本之前默认为false,之后去掉了该配置,并将默认情况改为true。见HIVE-12331.

在Hive 2.x之前,在定义表结构时通过CLUSTERED BY指定分桶信息,然后在插入数据的时候如果设置hive.enforce.bucketing为true,则会按照表定义里面的桶个数进行自动分桶(dynamic bucket);如果不设置为true,用户也可以指定如何分桶(主要是指定reduce的个数set mapred.reduce.tasks=xx)。这样导致的问题就是表结构里面虽然定义了分桶信息,但实际插入数据的时候可能并没有分桶或者分桶方式与表结构里面定义的不一致。这样后续操作的时候有了很多不确定性,容易产生各种问题,所以在2.x版本之后,去掉了该项配置,直接将默认情况设置为了true。如果你用的Hive是2.x之前的版本,使用分桶的时候记得将这两个选项置为true。另外需要说明的是桶的个数决定了reduce的个数。

下面我们来插入数据:

# 因为我们使用了动态分区,所以和前文一样,设置动态分区相关的参数,参数含义见《Hive的分区》一文
set hive.exec.dynamic.partition=true;
set hive.exec.dynamic.partition.mode=nonstrict;
set hive.exec.max.dynamic.partitions.pernode=1000;

# 覆盖式插入数据
INSERT OVERWRITE TABLE bucketed_user
    PARTITION(country)
    SELECT firstname, lastname, address, city, state, post, phone1, phone2, email, web,
    country
    FROM temp_user;

执行插入语句的时候,大家可以看打印的日志,一共有32个reducers,和桶的个数一致。插入完成之后我们来看HDFS上面的数据:

➜  ~ hadoop fs -ls -R /user/hive/warehouse/bucketed_user
drwxrwxr-x   - allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000000_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000001_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000002_0
-rwxrwxr-x   1 allan supergroup        806 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000003_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000004_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000005_0
-rwxrwxr-x   1 allan supergroup      17140 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000006_0
-rwxrwxr-x   1 allan supergroup        950 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000007_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000008_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000009_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000010_0
-rwxrwxr-x   1 allan supergroup      11314 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000011_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000012_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000013_0
-rwxrwxr-x   1 allan supergroup       4427 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000014_0
-rwxrwxr-x   1 allan supergroup       6132 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000015_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000016_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000017_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000018_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000019_0
-rwxrwxr-x   1 allan supergroup      12405 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000020_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000021_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000022_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000023_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000024_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000025_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000026_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000027_0
-rwxrwxr-x   1 allan supergroup      15262 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000028_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000029_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000030_0
-rwxrwxr-x   1 allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=AU/000031_0
drwxrwxr-x   - allan supergroup          0 2018-09-23 17:40 /user/hive/warehouse/bucketed_user/country=CA
-rwxrwxr-x   1 allan supergroup
……省略后续输出……

可以看到每个分区内的数据被分成了32份,因为数据量太小,所以很多桶内是空的。如果数据量大的话,各个桶的数据量会比较均衡。

2. 采样

从大量数据中采样得到少量数据进行测试或者调试是数据分析中非常常见的操作,抛开业务在数据库我们可以使用LIMIT语句实现该功能,但Hive提供了一个TABLESAMPLE语法用来更好的实现采样,TABLESAMPLE语句可以放到任何FROM语句中。目前支持两种采样方式:基于表的采样Sampling Bucketized Table和基于块的采样Block Sampling

2.1 Sampling Bucketized Table

基于分桶表的采样的语法格式如下:

table_sample: TABLESAMPLE (BUCKET x OUT OF y [ON colname] [table_alias])
  • colname表示基于表中哪个字段进行采样,这个字段可以是分桶列中的某个字段,也可以不是;还可以使用rand()表示基于整行而不是单独某个字段进行采样。
  • x out of y:表示要取哪些桶的数据。假设表有32个桶:

    • 3 out of 32表示总共取32/32=1个桶的数据,取的是第3个桶的数据;
    • 3 out of 16表示总共取32/16=2个桶的数据,分别取第3个、第19(3+16)个桶的数据;
    • 3 out of 64表示总共取32/64=0.5个桶的数据,取第3个桶一半的数据。
  • table_alias给表起的别名,类似MySQL的AS。

一些例子:

# 基于state字段采样
0: jdbc:hive2://localhost:10000> SELECT firstname, country, state, city FROM bucketed_user TABLESAMPLE(BUCKET 3 OUT OF 32 ON state);
+-------------+----------+--------------+---------------------------------+
|  firstname  | country  |    state     |              city               |
+-------------+----------+--------------+---------------------------------+
| Marleen     | CA       | BC           | Abbotsford                      |
| Carole      | CA       | BC           | Abbotsford                      |
| Lasandra    | CA       | BC           | Abbotsford                      |
| Yvette      | CA       | AB           | Big Valley                      |
| Annamae     | CA       | BC           | Burnaby                         |
| Adela       | CA       | BC           | Burnaby                         |
……省略……
| Shelia      | UK       | Cumbria      | Silloth-on-Solway               |
| Mauricio    | UK       | Cumbria      | Walney North Ward               |
| Quentin     | US       | MN           | Burnsville                      |
| Cyndy       | US       | MN           | Burnsville                      |
| Novella     | US       | HI           | Hilo                            |
| Brandon     | US       | HI           | Honolulu                        |
| Angella     | US       | HI           | Honolulu                        |
| Fatima      | US       | MN           | Hopkins                         |
| Skye        | US       | MN           | Minneapolis                     |
| Rodolfo     | US       | MN           | Northfield                      |
| Rolande     | US       | HI           | Pearl City                      |
| Chantell    | US       | MN           | Saint Paul                      |
| Matthew     | US       | MN           | Shakopee                        |
+-------------+----------+--------------+---------------------------------+
131 rows selected (1.06 seconds)

# 使用rand()采样
0: jdbc:hive2://localhost:10000> SELECT firstname, country, state, city FROM bucketed_user TABLESAMPLE(BUCKET 3 OUT OF 32 ON rand());
+-------------+----------+---------------------+---------------------------------+
|  firstname  | country  |        state        |              city               |
+-------------+----------+---------------------+---------------------------------+
| Nenita      | AU       | NS                  | Botany                          |
| Aide        | AU       | NS                  | Rhodes                          |
| Hester      | AU       | NS                  | The Risk                        |
| Annita      | AU       | NT                  | Karama                          |
| Mariko      | AU       | WA                  | Hamel                           |
| Emelda      | AU       | WA                  | Nedlands                        |
| Leatha      | AU       | WA                  | Two Rocks                       |
| Kenny       | AU       | TA                  | Nicholls Rivulet                |
| Eveline     | AU       | VI                  | Camberwell West                 |
……省略……
| Martina     | US       | FL                  | Orlando                         |
| Billye      | US       | MS                  | Pearl                           |
| Timothy     | US       | NY                  | Staten Island                   |
| Pamella     | US       | CO                  | Denver                          |
| Minna       | US       | PA                  | Kulpsville                      |
| Fabiola     | US       | PA                  | York                            |
| Junita      | US       | NJ                  | Cedar Grove                     |
| Helaine     | US       | NJ                  | Jersey City                     |
| Heike       | US       | NJ                  | Little Falls                    |
| Eladia      | US       | NJ                  | Ramsey                          |
| Felicidad   | US       | NJ                  | Riverton                        |
+-------------+----------+---------------------+---------------------------------+
58 rows selected (0.595 seconds)

需要注意的是这种采样方式并不要求表一定要是分桶的,如果没有分桶或者分桶了但采样的字段不在分桶字段里面,那也是可以正常采样的,只不过采样时会扫描全表数据,不是很高效而已。所以大多数情况这种采样方式都是和分桶一起使用的,采样的字段就是分桶的字段,这样采样时只扫描对应的桶就行,可以大大提高效率。

2.2 Block Sampling

基于块的采样是后来新增的一项功能(从Hive 0.8版本开始,见HIVE-2121),这里的块指的是HDFS的Block。目前有三种方式,基本语法为:

# 基于百分比
block_sample: TABLESAMPLE (n PERCENT)

# 基于大小
block_sample: TABLESAMPLE (ByteLengthLiteral)

ByteLengthLiteral : (Digit)+ ('b' | 'B' | 'k' | 'K' | 'm' | 'M' | 'g' | 'G')

# 基于行数
block_sample: TABLESAMPLE (n ROWS)

基于百分比采样和基于大小的采样实质是一样的(见HIVE-3401),这两种方式目前不支持一些压缩的格式。如果采样失败了,就会返回整个表或者分区的数据。需要注意的是,因为是基于块采样的,所以最小的采样单位就是HDFS的一个block,也就是说返回的数据可能会比实际的数据多。比如%1的数据是100MB,但HDFS的一个block是256MB,那采样得到的数据将是256MB。一个示例:

# 采样0.1%的数据
0: jdbc:hive2://localhost:10000> SELECT firstname, country, state, city FROM bucketed_user TABLESAMPLE(0.1 PERCENT) sampled_bucketed_user;
+------------+----------+-------------+---------+
| firstname  | country  |    state    |  city   |
+------------+----------+-------------+---------+
| Soledad    | AU       | AC          | Barton  |
| Darell     | CA       | ON          | Ajax    |
| Allene     | UK       | Derbyshire  | Barlow  |
| Devorah    | US       | NM          | Clovis  |
+------------+----------+-------------+---------+
4 rows selected (0.467 seconds)

# 采样100MB数据
0: jdbc:hive2://localhost:10000> SELECT firstname, country, state, city FROM bucketed_user TABLESAMPLE(100B) sampled_bucketed_user;
+------------+----------+------------------+--------------------+
| firstname  | country  |      state       |        city        |
+------------+----------+------------------+--------------------+
| Santos     | AU       | NS               | Allworth           |
| Avery      | CA       | NS               | Amherst            |
| Lewis      | UK       | South Yorkshire  | Central Ward       |
| Weldon     | US       | IL               | Arlington Heights  |
+------------+----------+------------------+--------------------+
4 rows selected (0.187 seconds)

如果我们想保证每次采样的数据一样,可以设置种子:

set hive.sample.seednumber=<INTEGER>;

默认值是0,比如我们改为100,再采一次样:

0: jdbc:hive2://localhost:10000> set hive.sample.seednumber=100;
No rows affected (0.006 seconds)
0: jdbc:hive2://localhost:10000> SELECT firstname, country, state, city FROM bucketed_user TABLESAMPLE(0.1 PERCENT) sampled_bucketed_user;
+------------+----------+------------------+--------------------+
| firstname  | country  |      state       |        city        |
+------------+----------+------------------+--------------------+
| Santos     | AU       | NS               | Allworth           |
| Avery      | CA       | NS               | Amherst            |
| Lewis      | UK       | South Yorkshire  | Central Ward       |
| Weldon     | US       | IL               | Arlington Heights  |
+------------+----------+------------------+--------------------+
4 rows selected (0.26 seconds)

基于行数的采样和前面两种方式不太一样:

  • 没有数据格式的限制;
  • 采集的条数n会在每个分片(split)都运行一次,所以采集到的总条数和输入的分片数也有关系。
# 从输入的每个分片从采5条数据(这里只有一个分片)
0: jdbc:hive2://localhost:10000> SELECT firstname, country, state, city FROM bucketed_user TABLESAMPLE(5 ROWS) sampled_bucketed_user;
+------------+----------+--------+-----------------+
| firstname  | country  | state  |      city       |
+------------+----------+--------+-----------------+
| Soledad    | AU       | AC     | Barton          |
| Annamae    | AU       | AC     | Civic Square    |
| Katheryn   | AU       | AC     | Fyshwick        |
| Roy        | AU       | AC     | Red Hill        |
| Jamie      | AU       | AC     | Tuggeranong Dc  |
+------------+----------+--------+-----------------+
5 rows selected (0.411 seconds)

本文介绍了Hive的分桶和采样,分桶很好的弥补了分区的一些不足。同时分桶之后,可以帮助我们更好的实现采样。需要注意的是不论是分区还是分桶,都是存在计算的,所以分区或者分桶之后,数据导入会比不分区不分桶慢,但换来的是后面查询会更快速。

References

]]>
<![CDATA[Hive的分区]]> http://niyanchun.com/hive-partition.html 2018-09-22T13:26:00+08:00 2018-09-22T13:26:00+08:00 NYC https://niyanchun.com Hive提供了分区的概念,对应到后台就是不同的目录,主要是为了在查询的时候可以提高性能。而分区分为两种:

  • 静态分区(Static Partition,以下简称SP):用户指定分区列的值,数据中不包含分区列的值,所以分区与数据没有关系。举个例子比如分区名字为ds=20180922,并不代表分区内的数据都是这天的,这个需要由用户自己保证。静态分区在编译阶段确定。
  • 动态分区(Dynamic Partition,以下简称DP):分区列的值从数据中获取,所以数据列中包含分区列的值。比如数据中有一列数据为日期,如果指定该列为分区列的话,那将根据该列的值创建分区。动态分区在运行阶段才可以确定。

今天就来介绍这两种分区。

假设现在有这样一个场景:我们有一些用户数据(UserRecords.txt)需要存储在Hive里面,数据里面包含了国家和地区信息,根据业务需求我们要将不同国家不同地区的用户存储在不同的分区里面。也就是说我们需要使用国家和的确信息作为分区列。下面我们看如果通过两种分区去实现。

静态分区

静态分区非常简单,之前的文章中已经提到过,就是我们导入数据的时候指定分区列的值,然后到后台就会用这个值建对应的目录,多个分区列就是嵌套的多级目录(所以Hive里面的分区列的顺序是有意义的)。

首先我们先将UserRecords.txt数据导入到Hive里面的一张临时表temp_user里面:

# 原始表
CREATE TABLE temp_user (
    firstname  VARCHAR(64),
    lastname    VARCHAR(64),
    address     STRING,
    country     VARCHAR(64),
    city        VARCHAR(64),
    state       VARCHAR(64),
    post        STRING,
    phone1      VARCHAR(64),
    phone2      STRING,
    email       STRING,
    web         STRING
) 
    ROW FORMAT DELIMITED
        FIELDS TERMINATED BY ','
        LINES TERMINATED BY '\n';

# 导入原始数据
LOAD DATA LOCAL INPATH '/tmp/UserRecords.txt' INTO TABLE temp_user;

然后创建静态分区表,使用国家和地区作为分区列:

# 创建静态分区表
CREATE TABLE sp_user (
    firstname  VARCHAR(64),
    lastname    VARCHAR(64),
    address     STRING,
    city        VARCHAR(64),
    post        STRING,
    phone1      VARCHAR(64),
    phone2      STRING,
    email       STRING,
    web         STRING
)    
    PARTITIONED BY(country VARCHAR(64), state VARCHAR(64))
    ROW FORMAT DELIMITED
        FIELDS TERMINATED BY ','
        LINES TERMINATED BY '\n';
        

现在的问题来了,如何将temp_user中的数据按照分区导入到sp_user里面?静态分区是用户在导入数据的时候指定的,显然我们需要知道有哪些分区,知道以后,就可以使用下面的语句分多次导入了:

# 这里以导入数据US、CA的数据为例
INSERT INTO TABLE sp_user 
    PARTITION(country='US', state='CA')
    SELECT firstname, lastname, address, city, post, phone1, phone2, email, web
    FROM temp_user 
    WHERE country == 'US' AND state == 'CA';


# 对应HDFS上面的存储
➜  hadoop-2.7.7 hadoop fs -ls -R /user/hive/warehouse/sp_user
drwxrwxr-x   - anonymous supergroup          0 2018-09-21 16:31 /user/hive/warehouse/sp_user/country=US
drwxrwxr-x   - anonymous supergroup          0 2018-09-21 16:38 /user/hive/warehouse/sp_user/country=US/state=CA
-rwxrwxr-x   1 anonymous supergroup       9430 2018-09-21 16:38 /user/hive/warehouse/sp_user/country=US/state=CA/000000_0

显然,如果国家和地区的组合很多的话,这样做会累死人。这个时候其实便可以考虑使用动态分区了。

动态分区

动态分区的"动"主要体现在分区列的值不是用户指定的,而是在运行阶段根据列值确定的。对于刚才的需求我们先创建分区表:

# 动态分区表
CREATE TABLE dp_user (
    firstname  VARCHAR(64),
    lastname    VARCHAR(64),
    address     STRING,
    city        VARCHAR(64),
    post        STRING,
    phone1      VARCHAR(64),
    phone2      STRING,
    email       STRING,
    web         STRING
)
    PARTITIONED BY(country VARCHAR(64), state VARCHAR(64))
    ROW FORMAT DELIMITED
        FIELDS TERMINATED BY ','
        LINES TERMINATED BY '\n';
        

建表语句与静态分区表一模一样,没有任何区别,动态与静态的区别主要在数据导入上面。不过在导入之前,我们需要先简单介绍几个重要的跟动态分区相关的参数:

  • hive.exec.dynamic.partition:是否允许动态分区,0.9.0之前的版本里面默认为false,后续版本默认为true。要使用动态分区,该参数必须为true,可通过set hive.exec.dynamic.partition;命令查看。
  • hive.exec.dynamic.partition.mode:动态分区的模式,可选值为strict(默认值)和nonstrictstrict模式下要求分区列里面至少要有一个静态分区,而nonstrict模式没有这个要求。
  • hive.exec.max.dynamic.partitions:允许创建的最大分区数目,默认值为1000。
  • hive.exec.max.dynamic.partitions.pernode:每一个mapper/reducer节点允许创建的动态分区数据,默认值为100。
  • hive.exec.max.created.files:允许一个MR任务里面所有mapper/reducer创建的HDFS文件的最大数目,默认值为100000。
  • hive.error.on.empty.partition:是否允许动态分区产生没有数据的分区,默认为false。

在我们这个需求里面需要做如下修改:

# 我们的分区列里面全部是动态分区,所以需要修改为nonstrict
set hive.exec.dynamic.partition.mode=nonstrict;

# 创建的分区比较多,100不够用,调整为1000
set hive.exec.max.dynamic.partitions.pernode=1000;

然后就可以导入了:

INSERT INTO TABLE dp_user 
    PARTITION(country, state)
    SELECT firstname, lastname, address, city, post, phone1, phone2, email, web,
    country, state
    FROM temp_user;

可以看到和静态分区导入的区别主要有两点:

  • 静态分区我们指定了分区列的值,而动态分区没有指定分区列的值;
  • 静态分区导入的时候分区列没有在普通的字段里面,而动态分区导入的时候分区列也在普通字段里面。

然后我们看一下效果:

# Hive里面

0: jdbc:hive2://localhost:10000> SHOW PARTITIONS dp_user;
+--------------------------------------------+
|                 partition                  |
+--------------------------------------------+
| country=AU/state=AC                        |
| country=AU/state=NS                        |
| country=AU/state=NT                        |
| country=AU/state=QL                        |
| country=AU/state=SA                        |
| country=AU/state=TA                        |
| country=AU/state=VI                        |
| country=AU/state=WA                        |
| country=CA/state=AB                        |
| country=CA/state=BC                        |
| country=CA/state=MB                        |
| country=CA/state=NB                        |
| country=CA/state=NL                        |
| country=CA/state=NS                        |
| country=CA/state=ON                        |
| country=CA/state=PE                        |
| country=CA/state=QC                        |
| country=CA/state=SK                        |
| country=UK/state= Uphall and Winchburg%22  |
| country=UK/state= Uplawmoor and Newton%22  |
| country=UK/state=%22Yorkshire              |
| country=UK/state=Aberdeenshire             |
…… 限于篇幅,省略部分输出 ……

# HDFS上面

➜  hadoop-2.7.7 hadoop fs -ls -R /user/hive/warehouse/dp_user
18/09/21 17:07:38 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
drwxrwxr-x   - allan supergroup          0 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU
drwxrwxr-x   - allan supergroup          0 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=AC
-rwxrwxr-x   1 allan supergroup        788 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=AC/000000_0
drwxrwxr-x   - allan supergroup          0 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=NS
-rwxrwxr-x   1 allan supergroup      16765 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=NS/000000_0
drwxrwxr-x   - allan supergroup          0 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=NT
-rwxrwxr-x   1 allan supergroup        929 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=NT/000000_0
drwxrwxr-x   - allan supergroup          0 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=QL
-rwxrwxr-x   1 allan supergroup      14926 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=QL/000000_0
drwxrwxr-x   - allan supergroup          0 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=SA
-rwxrwxr-x   1 allan supergroup       5997 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=SA/000000_0
drwxrwxr-x   - allan supergroup          0 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=TA
-rwxrwxr-x   1 allan supergroup       4331 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=TA/000000_0
drwxrwxr-x   - allan supergroup          0 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=VI
-rwxrwxr-x   1 allan supergroup      12132 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=VI/000000_0
drwxrwxr-x   - allan supergroup          0 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=WA
-rwxrwxr-x   1 allan supergroup      11068 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=WA/000000_0
…… 限于篇幅,省略部分输出 ……

可以看到,动态分区非常方便的帮我们实现了上面的需求,创建哪些分区是由数据决定,Hive自动帮我们创建的。

动态分区虽然好,但用的不对也会产生非常多的问题:最常见的比如创建太多的目录和文件、耗用内存等,这也是早期版本默认关闭该功能,以及现在的版本虽然默认允许,但也限制为strict的部分原因。这里有一篇文章给出了一些最佳实践,有兴趣的可以看一下:HIVE Dynamic Partitioning tips

关于分区最后再提两个注意点:

  • 分区表不能使用LOAD DATA的方式导入数据,一般都是使用INSERT语句导入。
  • 动态分区和静态分区一起用的时候,静态分区列必须写在动态分区列之前

分区修改DDL

注意:在Hive里面,关键字以及各种名字(数据库、分区、字段等)都是不区分大小写的。

除了查看分区的命令意外,其它和分区有关的命令都是操作表语句的一个子选项。前面部分我们已经用到了分区的创建、查看命令,最后再补充一些分区修改的DDL,包括:add、rename、exchanged(moved)、drop、(un)archive。这些都是ALTER TABLE命令的子选项。

  • 增加分区(add)
# 语法
ALTER TABLE table_name ADD [IF NOT EXISTS] PARTITION partition_spec [LOCATION 'location'][, PARTITION partition_spec [LOCATION 'location'], ...];
 
partition_spec:
  : (partition_column = partition_col_value, partition_column = partition_col_value, ...)
  
# 例子
ALTER TABLE sp_user ADD PARTITION (country='CN', state='Shannxi') location '/user/hive/warehouse/sp_user/country=CN/state=Shannxi'
                        PARTITION (country='CN', state='Shanxi') location '/user/hive/warehouse/sp_user/country=CN/state=Shanxi';
    
# HDFS上                        
➜  hadoop-2.7.7 hadoop fs -ls -R /user/hive/warehouse/sp_user
drwxrwxr-x   - allan supergroup          0 2018-09-22 11:12 /user/hive/warehouse/sp_user/country=CN
drwxrwxr-x   - allan supergroup          0 2018-09-22 11:12 /user/hive/warehouse/sp_user/country=CN/state=Shannxi
drwxrwxr-x   - allan supergroup          0 2018-09-22 11:12 /user/hive/warehouse/sp_user/country=CN/state=Shanxi
drwxrwxr-x   - allan supergroup          0 2018-09-21 16:31 /user/hive/warehouse/sp_user/country=US
drwxrwxr-x   - allan supergroup          0 2018-09-21 16:38 /user/hive/warehouse/sp_user/country=US/state=CA
-rwxrwxr-x   1 allan supergroup       9430 2018-09-21 16:38 /user/hive/warehouse/sp_user/country=US/state=CA/000000_0                        
  • 重命名(rename)
# 语法
ALTER TABLE table_name PARTITION partition_spec RENAME TO PARTITION partition_spec;

# 例子
ALTER TABLE sp_user PARTITION(country='CN', state='Shanxi') RENAME TO PARTITION(country='CN', state='Beijing');

# HDFS上
➜  hadoop-2.7.7 hadoop fs -ls -R /user/hive/warehouse/sp_user
drwxrwxr-x   - allan supergroup          0 2018-09-22 11:14 /user/hive/warehouse/sp_user/country=CN
drwxrwxr-x   - allan supergroup          0 2018-09-22 11:12 /user/hive/warehouse/sp_user/country=CN/state=Beijing
drwxrwxr-x   - allan supergroup          0 2018-09-22 11:12 /user/hive/warehouse/sp_user/country=CN/state=Shannxi
drwxrwxr-x   - allan supergroup          0 2018-09-21 16:31 /user/hive/warehouse/sp_user/country=US
drwxrwxr-x   - allan supergroup          0 2018-09-21 16:38 /user/hive/warehouse/sp_user/country=US/state=CA
-rwxrwxr-x   1 allan supergroup       9430 2018-09-21 16:38 /user/hive/warehouse/sp_user/country=US/state=CA/000000_0
  • 移动分区(exchange/move partition):在两个结构一样的表之间移动分区
# 语法
-- Move partition from table_name_1 to table_name_2
ALTER TABLE table_name_2 EXCHANGE PARTITION (partition_spec) WITH TABLE table_name_1;
-- multiple partitions
ALTER TABLE table_name_2 EXCHANGE PARTITION (partition_spec, partition_spec2, ...) WITH TABLE table_name_1;

# 例子:将dp_user的country=AU/state=AC分区移动到sp_user
ALTER TABLE sp_user EXCHANGE PARTITION(country='AU', state='AC') WITH TABLE dp_user;

# HDFS上
➜  hadoop-2.7.7 hadoop fs -ls -R /user/hive/warehouse/sp_user
18/09/22 11:20:11 WARN util.NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable
drwxrwxr-x   - allan supergroup          0 2018-09-22 11:19 /user/hive/warehouse/sp_user/country=AU
drwxrwxr-x   - allan supergroup          0 2018-09-21 15:10 /user/hive/warehouse/sp_user/country=AU/state=AC
-rwxrwxr-x   1 allan supergroup        788 2018-09-21 15:10 /user/hive/warehouse/sp_user/country=AU/state=AC/000000_0
drwxrwxr-x   - allan supergroup          0 2018-09-22 11:14 /user/hive/warehouse/sp_user/country=CN
drwxrwxr-x   - allan supergroup          0 2018-09-22 11:12 /user/hive/warehouse/sp_user/country=CN/state=Beijing
drwxrwxr-x   - allan supergroup          0 2018-09-22 11:12 /user/hive/warehouse/sp_user/country=CN/state=Shannxi
drwxrwxr-x   - allan supergroup          0 2018-09-21 16:31 /user/hive/warehouse/sp_user/country=US
drwxrwxr-x   - allan supergroup          0 2018-09-21 16:38 /user/hive/warehouse/sp_user/country=US/state=CA
-rwxrwxr-x   1 allan supergroup       9430 2018-09-21 16:38 /user/hive/warehouse/sp_user/country=US/state=CA/000000_0

# 由前文的图知道原来第一个分区是country=AU/state=AC,现在被移走了
➜  hadoop-2.7.7 hadoop fs -ls -R /user/hive/warehouse/dp_user | head
drwxrwxr-x   - allan supergroup          0 2018-09-22 11:19 /user/hive/warehouse/dp_user/country=AU
drwxrwxr-x   - allan supergroup          0 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=NS
-rwxrwxr-x   1 allan supergroup      16765 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=NS/000000_0
drwxrwxr-x   - allan supergroup          0 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=NT
-rwxrwxr-x   1 allan supergroup        929 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=NT/000000_0
drwxrwxr-x   - allan supergroup          0 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=QL
-rwxrwxr-x   1 allan supergroup      14926 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=QL/000000_0
drwxrwxr-x   - allan supergroup          0 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=SA
-rwxrwxr-x   1 allan supergroup       5997 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=SA/000000_0
drwxrwxr-x   - allan supergroup          0 2018-09-21 15:10 /user/hive/warehouse/dp_user/country=AU/state=TA
  • 删除分区(drop):同时删除HDFS上面的数据和MetaStore里面的元数据
# 语法
ALTER TABLE table_name DROP [IF EXISTS] PARTITION partition_spec[, PARTITION partition_spec, ...]
  [IGNORE PROTECTION] [PURGE];            -- (Note: PURGE available in Hive 1.2.0 and later, IGNORE PROTECTION not available 2.0.0 and later)

# 例子
ALTER TABLE sp_user DROP IF EXISTS PARTITION(country='AU', state='AC');
  • 归档(archive):将分区的文件归档到Hadoop Archive(HAR),这样可以减少文件数量,但不做任何压缩.默认该功能是disabled的。
# 语法
ALTER TABLE table_name ARCHIVE PARTITION partition_spec;
ALTER TABLE table_name UNARCHIVE PARTITION partition_spec;
  • 恢复分区(recover):有时我们会绕过Hive直接在HDFS上面操作Hive文件,这样会导致Hive的元数据与HDFS上面的不一致,所以Hive提供了MSCK命令来同步HDFS上面的数据与MetaStore里面的元数据信息。
# 语法
MSCK [REPAIR] TABLE table_name [ADD/DROP/SYNC PARTITIONS];

# 例子
## 现在HDFS上创建分区目录
➜  hadoop-2.7.7 hadoop fs -mkdir /user/hive/warehouse/sp_user/country=CN/state=Shanghai
➜  hadoop-2.7.7 hadoop fs -ls -R /user/hive/warehouse/sp_user
drwxrwxr-x   - allan supergroup          0 2018-09-22 11:44 /user/hive/warehouse/sp_user/country=CN
drwxr-xr-x   - allan supergroup          0 2018-09-22 11:44 /user/hive/warehouse/sp_user/country=CN/state=Shanghai
drwxrwxr-x   - allan supergroup          0 2018-09-22 11:12 /user/hive/warehouse/sp_user/country=CN/state=Shannxi
drwxrwxr-x   - allan supergroup          0 2018-09-21 16:31 /user/hive/warehouse/sp_user/country=US
drwxrwxr-x   - allan supergroup          0 2018-09-21 16:38 /user/hive/warehouse/sp_user/country=US/state=CA
-rwxrwxr-x   1 allan supergroup       9430 2018-09-21 16:38 /user/hive/warehouse/sp_user/country=US/state=CA/000000_0

# 此时Hive里面的分区信息不包含创建的目录
0: jdbc:hive2://localhost:10000> SHOW PARTITIONS sp_user;
+---------------------------+
|         partition         |
+---------------------------+
| country=CN/state=Shannxi  |
| country=US/state=CA       |
+---------------------------+

# 使用MSCK同步元数据
0: jdbc:hive2://localhost:10000> MSCK REPAIR TABLE sp_user;
No rows affected (0.46 seconds)
0: jdbc:hive2://localhost:10000> SHOW PARTITIONS sp_user;
+----------------------------+
|         partition          |
+----------------------------+
| country=CN/state=Shanghai  |
| country=CN/state=Shannxi   |
| country=US/state=CA        |
+----------------------------+
4 rows selected (0.198 seconds)

需要注意:

  • MSCK REPAIR命令要慎重执行,因为非常耗费系统资源;
  • 我试了一下ADD/DROP/SYNC命令会报语法错误,暂时也没找到问题原因,不知道是不是版本不支持(我的Hive是2.3.3),但看官方说明(HIVE-17824)这个版本应该是支持的。

Hive的分区在实际中用的比较多,最常用的分区列就是时间、地理位置等,本文就介绍到这里。

References

]]>
<![CDATA[Hive架构介绍]]> http://niyanchun.com/hive-architecture-introduction.html 2018-09-15T10:12:00+08:00 2018-09-15T10:12:00+08:00 NYC https://niyanchun.com Hive安装使用》介绍了一下Hive的安装和数据模型,本文主要介绍Hive的架构及查询流程。

架构总览

先来一张官网的架构图:

Hive Architecture

这幅图清楚的展示了Hive和Hadoop的关系,并且展示了Hive一些重要的组件:

  • UI:主要是Hive的各种客户端。这是用户使用Hive的窗口,包括我们之前使用的HiveCli、Beeline等CLI,以及一些Web GUI接口。用户通过UI来提交自己的操作请求。
  • Driver:接收用户查询,并且实现了会话处理,基于JDBC/ODBC实现了执行、拉取数据等API。
  • Compiler:解析查询语句,做语义分析,最终借助在Metastore中查询到的表和分区的元数据生成执行计划(execution plan),这个和传统的RDBMS比较像。当然其实Hive也有优化器(Optimizer),图中没有画出来。
  • Metastore:存储表和分区的元数据信息,包括字段、字段类型、读写数据需要的序列化和反序列化信息。
  • Execution Engine:执行引擎,用来执行Compiler生成的执行计划,是Hive和Hadoop之间的桥梁。现在Hive支持的计算引擎包括MR(逐渐废弃)、Tez、Spark。

Tips:Hive中Compiler生成的执行计划是一个多阶段的DAG,像MR、Tez、Spark这些计算引擎都可以执行。

这便是Hive里面几个核心的组件。下面我们看看一下一次查询的完整流程(下面的step n对应图中的数组序号):

  1. 用户通过UI提交自己的查询请求到Driver(step 1);
  2. Driver创建一个会话来处理用户的这次请求,并将请求发到Compiler以生成执行计划(step 2);
  3. Compiler从Metastore获取一些必要的元数据信息(step 3、4),做类型检查以及一些优化操作,然后最终生成执行计划发送给Driver(step 5),Driver再将执行计划发送给Execution Engine(以下简称EE)。
  4. EE拿到执行计划之后,会发送给合适的组件(step 6.1、6.2、6.3)。Hive的数据存储在HDFS上,所以执行的时候必然要和HDFS打交道。比如要先去NameNode上面查询数据的位置,然后去DataNode上面获取数据。如果是DDL操作的话(比如CREATE、DROP、ALTER等),还要和Hive的MetaStore通信。图中画的是使用MR的情况,MR可能有多个阶段,中间也会生成一些临时文件,这些文件都存储在HDFS上面。如果是DML操作,最后会将临时文件直接重命名(HDFS的重命名是一个原子操作)为最终的表名。如果是查询语句,Driver会调用fetch语句,通过Execution Engine直接从HDFS上面读取临时文件。

这便是Hive内部的处理流程。

HiverServer2

HiverServer2(以下简称HS2)是Hive里面非常重要的一个模块,基于Thrift开发,所以有时也被称为Thrift Server,主要功能是提供客户端操作Hive的接口,默认端口是10000。最早提供该功能的是HiveServer(为了和HS2区分,有时也叫HS1,也是基于Thrift开发),因为缺乏并发支持和认证机制,在Hive 1.0.0版本中被移除,引入了HS2。HS2解决了HS1缺乏的并发和认证功能,并增加了一些新的特性,有兴趣的可以看一下这篇文章:How HiveServer2 Brings Security and Concurrency to Apache Hive

需要注意的是:HS2也是Hive的一个模块,和Hive的Driver、Compiler、Execution Engine模块一样。比如看下面的另外两种种Hive的架构图:

Hive Architecture2

这幅图中蓝色的"Hive Server"就是HS2。

Hive Architecture3

这幅图中那个"Thrift Server"就是HS2。

但实际中,会将Hive的HS2、Driver、Compiler、Execution Engine以及一个基于Jetty的Web UI(默认端口是10002)全部实现在一个进程里面,而这个进程对应的程序文件就是$HIVE_HOME/bin/hiveserver2。所以要注意HS2是一个单独的模块,而hiveserver2是包含HS2模块以及其它一些模块的一个整体的服务,有些地方对于HS2的描述没有对此作区分,但我们心里要清楚。

所以,正常情况我们可以看到Hive最多就三个进程:MetaStore、HiveServer2、WebHCat(可选,做Hive元数据管理的)。

更多关于HS2的信息,可参加:HiveServer2 Overview.

MetaStore

MetaStore是Hive必不可少的一个模块。

设计动机

MetaStore提供了两个非常重要的功能:数据抽象(data abstraction)数据发现(data discovery)

  • 数据抽象:使用Hive处理数据之前,我们必须先定义库、表、分区、序列化、反序列化等信息,这些信息都会作为元数据存储在MetaStore里面,后面操作表里面的数据的时候直接去MetaStore里面就可以获取到数据的这些元信息,而不用每次操作数据的时候再去看数据格式是什么样子,如何读取,如何加载等。
  • 数据发现:一方面用户可以通过元数据去了解数据,另一方面其它一些系统也可以基于Hive的元数据做一些功能,比如Impala。

存储部署

MetaStore里面的存储使用的是JPOX ORM(Data Nucleus) 方案,所以任何支持该方案的存储都可以作为MetaStore后端的存储,比如Apache Derby以及大多数RDBMS都支持。目前MetaStore支持的RDBMS见这里.

MetaStore有两种部署方式:

  • Local/Embedded Metastore Database (Derby):该方式一次只能有一个进程连接到MetaStore,所以仅用于测试。这种模式下,Hive客户端直接通过JDBC访问MetaStore。Local模式可以使用一些RDBMS,而Embedded就是使用内置的Derby。
  • Remote Metastore Database:这种模式下使用远程的RDBMS来作为存储(典型的就是MySQL),用于生产环境。此时,MetaStore通过Thrift方式提供服务,Hive客户端通过该服务访问MetaStore。

MetaStore的E/R图见这里

最后来一张图帮助理解:

MetaStore

配置

MetaStore的核心配置有4个:

  • javax.jdo.option.ConnectionURL:JDBC连接信息。比如使用Derby时的值可以为`jdbc:derby:;databaseName=
    ../build/test/junit_metastore_db;create=true;使用MySQL时的值可以为:jdbc:mysql:///?createDatabaseIfNotExist=true`.
  • javax.jdo.option.ConnectionDriverName:JDBC驱动类名,Derby模式下值为org.apache.derby.jdbc.EmbeddedDriver, MySQL为com.mysql.jdbc.Driver.
  • hive.metastore.uris:uri,如果为空则表示为local模式,否则为remote模式。
  • hive.metastore.warehouse.dir:默认表的位置。

配置在hive-site.xml或者hivemetastore-site.xml里面均可,后者优先级高于前者。

HCatalog && WebHCat

HCatalog是Apache下面的一个元数据管理工具,后来被集成到Hive里面,是的一些第三方工具比如Pig、MR、Spark等可以通过HCatalog直接访问HDFS上的数据。而WebHCat(以前称为Templeton)提供访问HCatalog的REST API。对于Hive而言,HCatalog和WebHCat是非必须的。

Hive的架构就介绍到这里。

References

]]>
<![CDATA[Hive安装使用]]> http://niyanchun.com/hive-introduction.html 2018-09-12T20:07:00+08:00 2018-09-12T20:07:00+08:00 NYC https://niyanchun.com Hive最初由Facebook开发,最后贡献给Apache基金会,最后成为了Apache的顶级项目。Hadoop的出现虽然解决了海量数据的存储,并且提供了MR计算框架。但MR的使用门槛还是比较高,所以Facebook便开发了Hive,底层依旧使用HDFS存储数据,但操作数据却不用自己写MR程序,而是使用类似SQL的语法操作数据(在Hive里面称为HQL),后台自动转换为MapReduce,极大的降低了用户的使用难度。一句话概括就是:Hadoop提供了海量数据的存储、计算方案,而Hive使得该方案实施起来更加方便。

官方对于Hive的介绍如下:

The Apache Hive™ data warehouse software facilitates reading, writing, and managing large datasets residing in distributed storage and queried using SQL syntax.

对于Hive的总体介绍先到这里,我们先安装使用一下。

安装使用

Hive的安装也比较简单,和Hadoop那些类似,这里简单介绍一下步骤(本文使用的是Hive 2.3.3版本):

  1. 确保Hadoop已经正确安装,如果还没有,可参考我之前的文章《大数据系列一——Hadoop安装部署》。
  2. 下载二进制安装包并解压,然后设置HIVE_HOMEPATH环境变量:

    export HIVE_HOME=$HOME/software/apache-hive-2.3.3-bin
    export PATH=$HIVE_HOME/bin:$PATH
  3. 在HDFS上面创建Hive需要的目录并设置权限:

    $HADOOP_HOME/bin/hadoop fs -mkdir       /tmp
    $HADOOP_HOME/bin/hadoop fs -mkdir       /user/hive/warehouse
    $HADOOP_HOME/bin/hadoop fs -chmod g+w   /tmp
    $HADOOP_HOME/bin/hadoop fs -chmod g+w   /user/hive/warehouse
  4. 初始化Hive的Metastore:

    $HIVE_HOME/bin/schematool -dbType derby -initSchema
  5. 为了后面能正常使用Hive自带的beeline,需要在$HADOOP_HOME/etc/hadoop/core-site.xml中增加如下设置(将里面的allan改为你自己的用户名):

    <property>
        <name>hadoop.proxyuser.allan.groups</name>
        <value>*</value>
    </property>
    <property>
        <name>hadoop.proxyuser.allan.hosts</name>
        <value>*</value>
    </property>

    同时,为了避免用户权限问题,在$HADOOP_HOME/etc/hadoop/hdfs-site.xml中关闭HDFS的权限校验:

    <property>
        <name>dfs.permissions.enabled</name>
        <value>false</value>
    </property>

至此,一个最简单的Hive就安装完成了,我们可以通过Hive提供的CLI(Command Line Interface)访问Hive。Hive目前提供了两个CLI:HiveCLI(对应hive命令)和Beeline(对应beeline命令),前者现在已经废弃,不推荐使用;现在推荐使用Beeline。所以本文也就只介绍Beeline了。使用Beeline需要先启动HiveServer2((确保HDFS正常运行)):

$HIVE_HOME/bin/hiveserver2`。

默认HiveServer2监听端口为10000,我们使用Beeline连接Hive:

cd $HIVE_HOME
bin/beeline -u jdbc:hive2://localhost:10000

连接成功之后,我们可以执行一些HQL来感受一下Hive,后面部分再继续介绍涉及到的概念:

# 创建pokes表
0: jdbc:hive2://localhost:10000> CREATE TABLE pokes (foo INT, bar STRING);
No rows affected (0.412 seconds)
0: jdbc:hive2://localhost:10000> desc pokes;
+-----------+------------+----------+
| col_name  | data_type  | comment  |
+-----------+------------+----------+
| foo       | int        |          |
| bar       | string     |          |
+-----------+------------+----------+
2 rows selected (0.195 seconds)

# 创建invites表,该表有一个分区
0: jdbc:hive2://localhost:10000> CREATE TABLE invites (foo INT, bar STRING) PARTITIONED BY (ds STRING);
No rows affected (0.158 seconds)
0: jdbc:hive2://localhost:10000> desc invites;
+--------------------------+-----------------------+-----------------------+
|         col_name         |       data_type       |        comment        |
+--------------------------+-----------------------+-----------------------+
| foo                      | int                   |                       |
| bar                      | string                |                       |
| ds                       | string                |                       |
|                          | NULL                  | NULL                  |
| # Partition Information  | NULL                  | NULL                  |
| # col_name               | data_type             | comment               |
|                          | NULL                  | NULL                  |
| ds                       | string                |                       |
+--------------------------+-----------------------+-----------------------+
8 rows selected (0.234 seconds)

# 往pokes表里面导入数据
0: jdbc:hive2://localhost:10000> LOAD DATA LOCAL INPATH './examples/files/kv1.txt' OVERWRITE INTO TABLE pokes;
No rows affected (1.016 seconds)

# 查看数据
0: jdbc:hive2://localhost:10000> select * from pokes limit 5;
+------------+------------+
| pokes.foo  | pokes.bar  |
+------------+------------+
| 238        | val_238    |
| 86         | val_86     |
| 311        | val_311    |
| 27         | val_27     |
| 165        | val_165    |
+------------+------------+
5 rows selected (3.1 seconds)

# 往invites表里面导入数据,并指定分区
0: jdbc:hive2://localhost:10000> LOAD DATA LOCAL INPATH './examples/files/kv2.txt' OVERWRITE INTO TABLE invites PARTITION (ds='2008-08-15');
No rows affected (0.619 seconds)
0: jdbc:hive2://localhost:10000> LOAD DATA LOCAL INPATH './examples/files/kv3.txt' OVERWRITE INTO TABLE invites PARTITION (ds='2008-08-08');
No rows affected (0.619 seconds)
0: jdbc:hive2://localhost:10000> select * from invites limit 5;
+--------------+--------------+-------------+
| invites.foo  | invites.bar  | invites.ds  |
+--------------+--------------+-------------+
| 238          | val_238      | 2008-08-08  |
| NULL         |              | 2008-08-08  |
| 311          | val_311      | 2008-08-08  |
| NULL         | val_27       | 2008-08-08  |
| NULL         | val_165      | 2008-08-08  |
+--------------+--------------+-------------+
5 rows selected (0.362 seconds)

# 创建新db
0: jdbc:hive2://localhost:10000> create database new_db;
No rows affected (0.547 seconds)
0: jdbc:hive2://localhost:10000> use new_db;
No rows affected (0.192 seconds)
0: jdbc:hive2://localhost:10000> CREATE TABLE pokes1 (foo INT, bar STRING);
No rows affected (0.216 seconds)
0: jdbc:hive2://localhost:10000> LOAD DATA LOCAL INPATH './examples/files/kv1.txt' OVERWRITE INTO TABLE pokes1;
No rows affected (0.524 seconds)

可以看到HQL和SQL非常的类似,本文的重点不是介绍HQL,所以这里就先不细述了,有兴趣的请移步LanguageManual.

数据模型

上一节我们看到Hive与传统的RDBMS非常的像,在Hive的数据模型里面主要包含如下一些概念:

  • 数据库(Database):和RDBMS里面的数据库、HBase里面的Namespace作用类似,主要用来做资源隔离,避免一些名字冲突。同时也可以用来做权限管理。Hive里面内置了一个默认的数据库叫default
  • 表(Tables):和RDBMS里面的表类似,用于存放格式一样的数据。
  • 分区(Partitions):每个表可以有多个分区列,用于决定数据如何存储,以及提高查询效率。分区列是一个虚拟列,并不是表中的某一列。实际使用中最常用的是使用时间列,比如上面的invites表中有一个分区列ds,里面存储的是时间,这样当我们根据时间查询的时候,就可以只查询某些分区。需要注意的是:分区列名为2008-08-08并不代表里面所有的数据都是这一天的,这个要应用程序自己去保证
  • 桶(Buckets):有时也称作Clusters。对于每个分区里面的数据,我们可以根据哈希函数或者表中的某一列数据再分桶。

注意:分区和桶都是可选的,一个表可以没有分区和桶。

前面提到过,Hive的数据是存储在HDFS上的(存储目录由hive.metastore.warehouse.dir指定,默认值为/user/hive/warehouse),所以不同于RDBMS,Hive数据模型中的这些概念对应到后台实质就是一些目录(bucket可能是文件)。比如,我们看一下上一节中我们建的数据库、表以及数据是如何存储的:

➜ hadoop fs -ls -R /user/hive/warehouse
drwxrwxr-x   - anonymous supergroup          0 2018-09-12 14:58 /user/hive/warehouse/invites
drwxrwxr-x   - anonymous supergroup          0 2018-09-12 14:58 /user/hive/warehouse/invites/ds=2008-08-08
-rwxrwxr-x   1 anonymous supergroup        216 2018-09-12 14:58 /user/hive/warehouse/invites/ds=2008-08-08/kv3.txt
drwxrwxr-x   - anonymous supergroup          0 2018-09-12 14:58 /user/hive/warehouse/invites/ds=2008-08-15
-rwxrwxr-x   1 anonymous supergroup       5791 2018-09-12 14:58 /user/hive/warehouse/invites/ds=2008-08-15/kv2.txt
drwxrwxr-x   - anonymous supergroup          0 2018-09-12 15:13 /user/hive/warehouse/new_db.db
drwxrwxr-x   - anonymous supergroup          0 2018-09-12 15:13 /user/hive/warehouse/new_db.db/pokes1
-rwxrwxr-x   1 anonymous supergroup       5812 2018-09-12 15:13 /user/hive/warehouse/new_db.db/pokes1/kv1.txt
drwxrwxr-x   - anonymous supergroup          0 2018-09-12 14:57 /user/hive/warehouse/pokes
-rwxrwxr-x   1 anonymous supergroup       5812 2018-09-12 14:57 /user/hive/warehouse/pokes/kv1.txt

可以看到:

  • 一个数据库对应一个目录,目录名格式为:数据库名.db,加后缀是为了和表名区分。因为内置的数据库defalut里面的表对应的目录和其它的数据库目录是平级的。而其它数据库里面的表则在数据库目录之下。
  • 一个表名对应一个目录,目录名即表名;
  • 一个分区对应一个目录,目录名即分区名,多个分区列的话就是多级目录。
  • 数据以文件的方式存储在HDFS上面。

如果我们删掉数据库、表或者分区,对应的目录也会删除(外部表例外,以后再介绍)。

其他

Hive的设计目标

  • easy data summarization
  • easy ad-hoc querying
  • easy analysis of large volumes of data

因为Hive底层使用Hadoop,同时做了数据抽象封装(库、表、分区、桶),还提供了类SQL的操作,使得上述的目标很容易达成。另外,Hive也支持用户自定义函数,增加了灵活性。

Hive不太适合的场景

  • Hive不适合做实时性的分析计算。Hive是基于Hadoop的,之前介绍Hadoop的时候也提到过,Hadoop定位是离线计算。Hive的查询等到后台是MapReduce任务,而且是从HDFS读取数据,所以不适合做实时计算。Hive的定位是OLAP,而不是OLTP。
  • Hive不适合做数据更新。Hive数据存储在HDFS上,而HDFS只支持append。老版本的Hive不支持Update和Delete操作,新版本的支持了,但最好还是避免使用,至少避免大量使用。如果有这种需求,应该考虑HBase等。

Hive的版本

如果看Hive的官方文档的话,一定会被各种版本号搞得吐血。文档里面使用的是一个版本号,而发布的又是另外一个版本号,我也没有逐一梳理过,官方给了几个:

PS:前面为Release的版本号,也就是我们经常说的Hive的版本号,而后面的是内部的版本号,主要在文档里面出现

  • 1.0.0对应于0.14.1
  • 1.1.0对应于0.15.0
  • 2.3.0对应于2.2.0

关于Hive的版本策略参见:Understanding Hive Branches.

Hive为什么需要/tmp目录

首先Hive需要两个临时目录:

  • HDFS上面的/tmp/hive-<username>(可以通过hive.exec.scratchdir配置项修改)
  • Hive客户端所在机器的/tmp/<username>目录(硬编码,不可更改)

这些目录主要用于存储客户端查询时的一些临时数据或者中间数据,比如往Hive表中写数据,都是先写到HDFS上的临时目录,然后再move过去。正常情况下,客户端查询结束后会删掉这些临时数据,如果客户端异常,数据可能会残留。

Hive的配置文件

Hive最主要的配置文件为hive-site.xml,在$HIVE_CONF_DIR指定的目录,默认为$HIVE_HOME/conf。现在Hive的安装包里面没有hive-site.xml,只有hive-default.xml.template。后者是根据HiveConf.java文件里面的默认配置生成的,但Hive并不会使用这个配置文件。如果我们需要配置Hive,需要创建hive-site.xml文件,或者直接复制hive-default.xml.templatehive-site.xml,然后修改里面的值。

同时Hive还支持hivemetastore-site.xmlhiveserver2-site.xml两个配置文件,MetaStore启动的时候除了会加载hive-site.xml,还会加载hivemetastore-site.xml;HS2启动的时候除了会加载hive-site.xml,还会加载hiveserver2-site.xml,如果HS2使用的是Embedded的MetaStore,也会加载hivemetastore-site.xml

整个配置的优先级从低到高依次为:
hive-site.xml -> hivemetastore-site.xml -> hiveserver2-site.xml -> '-hiveconf' commandline parameters -> set命令。

本文主要介绍了Hive的安装和基本使用,以及Hive的数据模型,可以说是非常粗的从“外面”介绍了一下Hive,旨在对Hive有一个直观的感受。下一篇文章会从“里面”介绍一下Hive,有兴趣的可以关注一下。

References:

]]>