# 五、HBase 上的外部表

HBase 是一种大数据存储技术，可提供对海量数据的实时访问。我们在这里不做过多的详细介绍，但是 Syncfusion 已经在他们的简洁系列中用另一个免费电子书覆盖了它— **简洁**(也是我写的)。

HBase 的体系结构允许您从特定的行和列中快速读取单元格，但是它的半结构化特性意味着单元格中的数据有时很难处理。通过将 HBase 表映射为 Hive 表，您可以获得固定结构的所有好处，您可以使用 HiveQL 进行查询，同时还可以获得 HBase 的所有速度优势。

在 HBase 中，数据作为行存储在表中。所有行都有一个行键作为唯一标识符，所有表都指定了一个或多个列族。列族是动态结构，可以包含同一表中不同行的不同列。

Hive 允许您基于 HBase 创建一个表，该表将公开列族中的特定列，或者将整个列族公开为以键值对表示的 MAP 列。与 HDFS 一样，Hive 不从 HBase 导入任何数据，因此当您使用 Hive 查询 HBase 表时，它将作为映射/缩减作业执行，并且它将使用 HBase Java API 执行任务。

结合使用糖化血红蛋白酶和蜂巢比单独使用糖化血红蛋白酶有更大的优势。HBase 不提供索引——您将需要按行键查询表，这可能是一个缓慢的过程。但是，使用 Hive，您可以在一个 HBase 表中的任何列上创建一个索引，这意味着您可以有效地查询除行键之外的其他字段上的 HBase。

在 Hive 中，使用 HBase 作为存储的表被定义为外部表，它们使用与 HDFS 存储的表相同的命令语法。没有必要用 HBase 指定数据格式或 SerDe，因为 Hive 使用 HBase API 访问数据，内部数据格式不需要知道。

必须在 stored by 子句中使用特定的存储引擎来声明 HBase 表，该子句包含标识 HBase 表名称的属性。代码清单 35 显示了一个 create table 语句，用于访问名为 device-events 的 HBase 表中的数据。

 35:在配置单元中映射一个糖化血红蛋白表

```
  CREATE EXTERNAL TABLE
  device_events(rowkey STRING, data STRING)
  STORED BY
  'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
  WITH SERDEPROPERTIES
  ('hbase.columns.mapping' = ':key,cf1:data')
  TBLPROPERTIES ('hbase.table.name' =
  'device-events');

```

我们只使用这个简单的语句映射来自 HBase 表的行键，以及来自一个列族的一列。存储处理程序为所有 HBase 表提供了:键列，cf1:data 表示 cf1 列族中的数据列(这是我的表所特有的)。

对于外部 HBase 表，您需要指定以下条款:

*   STORED BY—这将是所有 hbase 表的固定值 org . Apache . Hadoop . hive . HbASE . HbASESSTORAGHANDler。存储处理程序是 Hive 发行版的一部分，但是您需要为 Hive 提供额外的 HBase 库(我们将在本章后面看到)。
*   WITH SERDEPROPERITIES-HbASE 的源列在 hbase.columns.mapping 属性中指定。它们是位置性的，因此表定义中的第一列被映射到属性列表中的第一列。
*   TBLPROPERTIES—至少在 hbase.table.name 属性中提供源表名。您也可以提供模式名。

HBase 将所有数据存储为字节数组，将数组解码为相关格式是客户端的责任——在本例中是 Hive 存储处理程序的责任。为 Hive 中的列声明数据类型时，必须确保 HBase 中的编码字节数组可以解码为您指定的类型。如果 Hive 无法解码数据，它将返回空值。

为了最小化存储并最大化访问性能，HBase 中的表通常只包含一个或两个名称非常短的列族。使用 Hive，我们可以将列族中的单个列映射为基本数据类型，或者将整个列族映射为一个映射。

我在 HBase 的设备事件表中使用了两个列族，e 表示事件数据，m 表示元数据属性。如果我想通过 Hive 公开该表，包含特定的事件列和所有的元数据列，我可以使用 with serdeproperties 子句，如代码清单 36 所示。

 36:映射特定的糖化血红蛋白柱

```
  CREATE EXTERNAL TABLE
  device_events(rowkey STRING, eventName STRING, receivedAt STRING, payload
  STRING, metadata MAP<string, string>)
  STORED BY
  'org.apache.hadoop.hive.hbase.HBaseStorageHandler'
  WITH SERDEPROPERTIES
  ('hbase.columns.mapping' = ':key,e:n,e:t,e:p,m:')
  TBLPROPERTIES ('hbase.table.name' =
  'device-events');

```

列映射以逗号分隔的字符串形式提供，其中特定列使用{column family}:{column name}语法命名，整个系列使用{column family}:语法命名。

表 1 显示了每个配置单元列的糖化血红蛋白来源。

 1:糖化血红蛋白中的表结构

| 蜂巢柱 | 糖化血红蛋白柱系列 | HBase 列 | 笔记 |
| 行键 | - | - | 内置:关键属性 |
| eventName | e | n |  |
| 时间戳 | e | t |  |
| 有效载荷 | e | p |  |
| 元数据 | m | - | 全家人 |

通过这种映射，我们可以以更结构化的格式读取 Hive 中的 HBase 数据，并且我们可以使用更高级别的 HiveQL 功能从数据中导出有用的信息。

代码清单 37 显示了当使用 HBase Shell 读取设备事件表中行关键字为 rk1 的所有单元格时，原始数据在 HbSe 中的外观。

 37:读取 HBase 中的数据

```
  hbase(main):011:0> get 'device-events', 'rk1'
  COLUMN                            
  CELL                                                                                            

   e:n                               timestamp=1453966450613,
  value=power.on                                                          
   e:p                              
  timestamp=1453966518206,
  value={"some":"json"}                                                  

   e:t                               timestamp=1453966495748,
  value=1453562878                                                        
   m:d                              
  timestamp=1453966537534,
  value=device-id                                                         
   m:u                               timestamp=1453966556996,
  value=elton                                                             
   m:v                              
  timestamp=1453966547593,
  value=1.0.0                                                             

```

请注意，显示的时间戳是内部字段，其中 HBase 记录了单元格值的最后修改时间。单元格值都存储为字符串，这简化了 HBase 和其他工具之间的互操作。

代码清单 38 显示了通过 Hive 获取的同一行。

 38:读取蜂箱中的糖化血红蛋白数据

```
  > select * from device_events where rowkey='rk1';
  +-----------------------+--------------------------+-----------------------
  | device_events.rowkey  |
  device_events.eventname  | device_events.receivedat  | device_events.payload 
  |           device_events.metadata           |
  +-----------------------+--------------------------+-----------------------
  | rk1                   | power.on                 |
  1453562878                | {"some":"json"}        |
  {"d":"device-id","u":"elton","v":"1.0.0"} 
  |
  +-----------------------+--------------------------+-----------------------

```

代码清单 39 描述了我们如何使用基本的 HiveQL 来更好地理解数据——在本例中，在 receivedAt 字段中将 UNIX 时间戳显示为日期，并从元数据映射中提取用户名。

 39:格式化配置单元中的基本数据

```
  > select eventname, from_unixtime(cast(receivedat as int)),
  payload, metadata['u'] from device_events where rowkey='rk1';
  +------------+----------------------+------------------+--------+--+
  | eventname  |         _c1          |    
  payload      |  _c3   |
  +------------+----------------------+------------------+--------+--+
  | power.on   | 2016-01-23 15:27:58  |
  {"some":"json"}  | elton  |
  +------------+----------------------+------------------+--------+--+

```

由于 HBase 将所有数据存储为字节数组，因此如果在多平台环境中使用 HBase，可能会出现系统之间的转换问题。在这些情况下，通常会牺牲一些存储性能来支持可互操作的数据，并将所有 HBase 数据存储为标准的 UTF-8 编码字符串。

我们将在第 9 章“使用 HiveQL 查询”中看到更多的 HiveQL 语法，但是在这里知道强制转换函数在基本数据类型之间进行转换，并且我们可以在视图中包含数据转换，以便对 HBase 数据进行更多的逻辑访问是非常有用的。

对行的直接访问总是通过 HBase 中的行键来实现的，这些键通常是多个值的神秘组合。我们可以使用 HiveQL 中的字符串函数将行键拆分成组成部分，在视图中将它们显示为类型化的列，并使数据更具可读性。

代码清单 40 显示了我们的 HBase 表中一行的一个示例单元格，其中所有值都存储为编码字符串。

 40:在糖化血红蛋白中提取一个细胞

```
  hbase(main):002:0> get 'device-events', 'uuid|20160128',
  'e:n'
  COLUMN                            
  CELL                                                                                            

   e:n                               timestamp=1454002528064,
  value=power.off                                                         

```

行关键字由设备标识(实际数据库中的真实 UUID)和由管道字符分隔的日期周期构成。HBase 中的一个常见问题是，行键设计必须支持主数据访问向量(在本例中是设备 ID)，如果要通过辅助向量(日期周期)进行查询，则必须进行全表扫描。

代码清单 41 展示了如何在 Hive 表上创建一个视图，通过将行键分成两部分，以一种更有用的方式公开 HBase 数据。

 41:使用配置单元视图拆分 HBase 行键

```
  CREATE VIEW device_events_period(rowkey, deviceId, period,
  eventname, receivedat) AS
  SELECT rowkey, split(rowkey, '\\|')[0], split(ROWKEY, '\\|')[1],
  eventname, receivedat FROM device_events;

```

| ![](img/00009.jpeg) | 提示:在本例中，视图将整行键作为单独的列进行维护。这是一个很好的实践，因为您的结果包含行键，如果您想读取源数据，可以使用该行键直接查询 HBase。 |

现在，我们可以使用清晰且符合逻辑的 HiveQL 语句来搜索给定时间段内的行，如代码清单 42 所示。

 42:按部分行键查询糖化血红蛋白

```
  1: jdbc:hive2://127.0.0.1:10000> select * from
  device_events_period where substr(period, 1, 6) = '201601';
  +------------------------------+--------------------------------+----------
  | device_events_period.rowkey  |
  device_events_period.deviceid  | device_events_period.period  |
  device_events_period.eventname  | device_events_period.receivedat  |
  +------------------------------+--------------------------------+----------
  | uuid|20160128                |
  uuid                           | 20160128                     |
  power.off                       | 1453564612                       |
  +------------------------------+--------------------------------+----------

```

这里，我们使用 substr 函数将字符串字段的前六个字符与文字“201601”进行比较，因此我们返回日期在 2016 年 1 月的行。这个简单的查询将通过匹配行键中间的一部分来读取糖化血红蛋白数据的子集，这是单独使用糖化血红蛋白无法做到的。

Hive 不能在视图上创建索引，这将允许我们通过使用行键的部分来为 HBase 表创建二级索引，这是组件目前缺少的一个特性。我们也不能在 create index 语句中包含函数；如果我们想要一个行键部分的二级索引，我们将需要一个 ETL 过程(我们将在第 6 章中看到 Hive 的 ETL)。

因为列族在 HBase 中是动态的，所以我们在 Hive 中定义的映射可能不适用于每一行。事实上，对于我们期望找到的特定列，行可能没有单元格，或者映射单元格中的数据可能不是期望的格式。

Hive 对 HBase 采取了与其他来源相同的乐观方法。当行不包含预期数据时，它将映射任何正确的列，并为任何无法映射的列返回空值。即使大多数列不能被映射，Hive 也将返回一个包含大多数空值的行，而不是根本没有行。

表 2 显示了在代码清单 36 中映射的 HBase 表中的两行，它们不符合预期的 Hive 结构。

 2:糖化血红蛋白中的意外数据

| 排 | 行键 | 事件名称 | 在 HBase 中有效 | 在蜂巢中有效 |
| one | uuid3&#124;20160128 | - | 是 | 是 |
| Two | uuid2 | 关闭电源 | 是 | 不 |

第一行包括有效格式的行键，但在 eventName (e:n)列中没有数据。第二行的 eventName 列中有数据，但有意外的行键格式。两者在 HBase 中都有效，HBase 不要求行有列或遵循显式行键格式。

Hive 会尽可能利用这些数据。如果我们显式映射一个不存在的列，Hive 将不会在响应中包含任何没有该列的行。但是，如果我们从查询中排除该列，那么会返回缺少值的行，如代码清单 43 所示，其中 uuid3 的 rowkey 出现在第一组结果中，而不是第二组结果中。

 43:查询配置单元中的意外糖化血红蛋白数据

```
  > select
  rowkey, payload from device_events;
  +-----------------+-------------------+--+
  |    
  rowkey      |      payload      |
  +-----------------+-------------------+--+
  |
  uuid2           | {"other":"json"}  |
  |
  uuid3|20160128  | {"more":"json"}   |
  |
  uuid|20160128   | {"other":"json"}  |
  +-----------------+-------------------+--+
  3 rows
  selected (0.32 seconds)
  > select
  rowkey, eventname from device_events;
  +----------------+------------+--+
  |    
  rowkey     | eventname  |
  +----------------+------------+--+
  |
  uuid2          | power.off  |
  |
  uuid|20160128  | power.off  |
  +----------------+------------+--+

```

吞噬异常意味着 Hive 可能不会返回源中的所有数据，但也意味着长时间运行的查询不会被坏数据破坏。

Hive 可以在 HBase 集群上运行，也可以在配置为与 HBase 对话的独立 Hadoop 集群上运行。大多数人根据使用需求选择一个选项，如果您共享硬件，您将争夺资源。

如果您将在高带宽使用的同时运行 Hive 负载，那么单独的集群是一个不错的选择。但是，如果您的 Hive 查询是夜间作业，而 HBase 是在白天使用的，那么共享集群就可以了。

Hive 与 HBase 存储处理程序打包在一起，如果所有的 HBase 库都可用，您只需要用 HBase Zookeeper 仲裁的地址来配置 Hive。

当您从 Hive 查询 HBase 数据时，Hive 编译器会遵从 HBase 存储处理程序来生成数据访问作业。在这种情况下，Hive 将使用本机 Java API 生成 Java 代码来查询 HBase，它将使用配置的 Zookeeper 仲裁来查找 HMaster 和 HRegion 节点的地址。

HBase 是为实时访问而设计的，查询通常执行得非常快。为了获得最佳性能，您的表应该结构化，以便存储处理程序可以通过扫描行的子集而不是整个表来查询 HBase(这是我们这里没有空间的地方，但是如果您感兴趣，研究的领域是“过滤器下推”)。

HBase 使用 ZooKeeper 进行配置和通知，包括发送和侦听心跳，以确定哪些服务器处于活动状态。ZooKeeper 包含了 HBase 集群中服务器的连接细节，这意味着，如果 Hive 可以到达 ZooKeeper，它就可以获得它需要的所有其他信息。

代码清单 44 显示了 hive-site.xml 的一个片段，其中包含了 HBase ZooKeeper 仲裁地址——通常是多个服务器(这里命名为 zk1 到 zk3)，hive 应该可以通过 DNS 或使用完全限定的域名来访问它们。

 44:配置 HBase 动物园管理员地址

```
  <property>
        <name>hbase.zookeeper.quorum</name>
        <value>zk1,zk2,zk3</value>
  </property>

```

Hive 附带了 HBase 存储处理程序和 Zookeeper 库，但是您需要为您的 HBase 版本添加正确的依赖项(您可以从正在运行的 HBase 服务器中发现)，并使它们在 HDFS 可用。

Hive 在运行时的两个时间点使用 HBase 库——在编译查询时在服务器上，以便构建映射/缩减作业，在作业运行时也在集群中的每个数据节点上。Hive 通过使库在 HDFS 可用并在配置中列出所有依赖项来支持将依赖项发送到数据节点。代码清单 45 显示了配置单元中配置的一个示例 HBase 库——简洁的 Docker 映像。

 45:指定辅助库

```
  <property>
        <name>hive.aux.jars.path</name>
        <value>hdfs://localhost:9000/hive/auxlib/hbase-server-1.1.2.jar,hdfs://localhost:9000/hive/auxlib/protobuf-java-2.5.0.jar,
        ...</value>
  </property>

```

| ![](img/00009.jpeg) | 提示:您可以通过登录到 HBase 主节点并运行命令行 hbase mapredcp &#124; tr 来获取正确的 HBase 依赖项列表:“”\n。您可以下载 HBase tarball，提取列表中的文件，将它们放入 HDFS，然后将它们添加到 hive-site.xml config 中。 |

为了尝试将 Hive 连接到远程 HBase 服务器，我们可以使用两个 Docker 容器，并使用 Docker Compose 对它们进行配置，Docker Compose 是 Docker 的扩展，用于指定作为一个逻辑单元一起运行的容器组。

最简单的方法是在 Docker Hub 上使用我简洁的 hbase 和简洁的 hive 图像，您可以使用我的 GitHub 存储库上的 [Docker Compose YAML 文件与本课程的代码连接在一起。](https://github.com/sixeyed/hive-succinctly/blob/master/docker/docker-compose.yml)

Docker Compose 在 YAML 文件中使用了非常简单的语法。HBase 容器公开了 Hive 需要的所有端口，并被赋予了 hbase 主机名。Hive 容器被链接到 HBase 容器，这样它就可以访问公开的端口(这些端口没有公开，因此在 Docker 容器之外是不可用的)。Hive 容器将在其 HOSTS 文件中有一个 hbase 条目，这样它就可以通过主机名连接到 HBase 容器。

在 hive-简洁地说是容器映像中，配置包含 HBase Zookeeper 仲裁地址(设置为 HBase)，Hive 库文件夹(/hive/auxlib)将已经拥有与 hbase 对话所需的所有附加库，这意味着您不需要自己配置任何东西。

您将需要安装 [Docker Compose](https://docs.docker.com/compose/install/) 以及 [Docker](https://docs.docker.com/engine/installation/) ，然后下载 docker-compose.yml 并导航到您保存它的目录。代码清单 46 展示了如何通过 Docker Compose 控制容器的生命周期。

 46:启动和停止容器

```
  docker-compose up -d
  docker-compose stop
  docker-compose start
  docker-compose kill
  docker-compose rm

```

下面是代码清单 46 的关键术语的定义:

*   up—获取最新的映像，然后创建、配置和启动容器。
*   停止—停止容器运行，但将它们保留在原位，状态保持不变。
*   启动—启动(或重新启动)保存的容器。
*   杀死——停止容器并破坏它们的状态。
*   RM—移除容器。

这两个容器都已经用本章中的示例数据、表和视图进行了设置，因此您可以连接到 Hive 容器，运行 Beeline，并开始查询位于 HBase 容器中的数据，该容器将在后台运行。

| ![](img/00009.jpeg) | 提示:这两个容器都在伪分布式模式下使用 HDFS，所以当您第一次运行它们时，它们需要几分钟的时间来准备好。如果您在完成后使用 docker-compose stop，它们将在您下次运行 docker-compose start 时立即准备好。 |

当您在 Hive 中运行 HBase 查询时，编译器会构建一个使用 HBase API 的作业，然后 Hive 从 HBase 服务器获取数据并将其映射到表格式。HBase 负责读取(和写入)数据，Hive 负责查询准备和数据翻译。

Hive 与 HBase 配合良好，利用实时数据访问进行快速查询，并在 HBase 的半结构化数据之上添加结构化层。

HBase 存储的本质意味着列名和系列名通常非常短(一个字符是常见的)，以便最大限度地减少磁盘使用。与此同时，行键通常是多个数据项串联在一起的神秘结构。Hive 可以在结构化、类型化的列中公开这些数据，增加了一层含义，使查询更容易理解。

与 HDFS 一样，Hive 为 HBase 提供了一个一致的 SQL 接口，因此用户无需具备编程知识即可进行查询(HBase 提供了多个服务器接口，但它们都需要编程)。

我们现在已经看到了如何使用内部存储、外部 HDFS 文件和 HBase 表来定义 Hive 表。当您希望以更容易访问的方式使用现有数据时，外部表是一个强大的 Hive 概念，但是 Hive 的全部功能仅适用于内部表。

当您需要内部表附带的更高级的功能时，您仍然可以使用 HDFS 和 HBase 文件作为源，并使用 ETL 函数将它们加载到 Hive 表中，这将在下一章中介绍。