目录[-]
Impala 表使用 Parquet 文件格式
Impala 帮助你创建、管理、和查询 Parquet 表。Parquet 是一种面向列的二进制文件格式,设计目标是为 Impala 最擅长的大规模查询类型提供支持(Parquet is a column-oriented binary file format intended to be highly efficient for the types of large-scale queries that Impala is best at)。Parquet 对于查询扫描表中特定的列特别有效,例如查询一个包含许多列的"宽"表,或执行需要处理列中绝大部分或全部的值的如 SUM(),AVG() 等聚合操作(Parquet is especially good for queries scanning particular columns within a table, for example to query "wide" tables with many columns, or to perform aggregation operations such as SUM() and AVG()that need to process most or all of the values from a column)。每个数据文件包含行集(行组)的值(Each data file contains the values for a set of rows (the "row group"))。在数据文件里,每一列的值都被重组,以便他们相邻,从而对这些列的值进行良好的压缩(the values from each column are organized so that they are all adjacent, enabling good compression for the values from that column)。针对 Parquet 表的查询可以快速并最小 I/O 的从任意列获取并分析这些数据(table can retrieve and analyze these values from any column quickly and with minimal I/O)。
参考以下章节,以了解关于 Impala 表如何使用 Parquet 数据文件的详细信息:
在 Impala 中创建 Parquet 表
请使用类似下面的命令,创建名为 PARQUET_TABLE_NAME 并使用 Parquet 格式的表,请替换为你自己的表名、列名和数据类型:
[impala-host:21000] > create table parquet_table_name(x INT, y STRING) STORED AS PARQUET;
或者,克隆现有表的列名和数据类型:
[impala-host:21000] > create table parquet_table_name LIKE other_table_name STORED AS PARQUET;
当创建了表之后,请使用类似下面的命令插入数据到表中,请再次使用你自己的表名:
[impala-host:21000] > insert overwrite table parquet_table_name select * from other_table_name;
假如 Parquet 表具有与其他表不同数量的列或不同的列名,请在对其他表的 SELECT 语句中指定列名而不是使用 * 来代替。
加载数据到 Parquet 表
根据原始数据是否已经在 Impala 表中,或者在 Impala 之外存在原始数据文件,来选择下面的技术加载数据到 Parquet 表里。
假如你的数据已经在 Impala 或 Hive 表里,可能是在不同的文件格式或分区模式下,你可以直接使用 Impala INSERT...SELECT 语法传输这些数据到 Parquet 表。你可以在同一个 INSERT 语句中,对数据执行转换、过滤、重新分区,以及其他类似操作。参见 Snappy and GZip Compression for Parquet Data Files 了解一些演示如何插入数据到 Parquet 的例子。
当插入到分区表中,特别是使用 Parquet 文件格式的,你可以在 INSERT 语句中包含一个提示(hint)以减少同时写入 HDFS 文件的数量,以及为不同的分区保存数据提供的 1GB 内存缓存的个数(and the number of 1GB memory buffers holding data for individual partitions)。请将 hint 关键字 [SHUFFLE]/[NOSHUFFLE] 放在紧跟表名之后。当 INSERT 语句运行失败,或当所有节点上试图构造所有分区的数据而导致的低效时,使用 [SHUFFLE] 提示。这一提示仅在 Impala 1.2.2 及以上版本可用。
Parquet 表的任意 INSERT 语句需要 HDFS 文件系统中有足够写入一块的空间。因为 Parquet 数据文件默认 1GB/块, 假如你的 HDFS 所剩无几,那么 INSERT 可能失败(即使非常少量的数据)。
避免对 Parquet 表使用 INSERT...VALUES 语法,因为 INSERT...VALUES 为每一个 INSERT...VALUES 语句产生一个包含少量数据的单独的数据文件,而 Parquet 的实力在于它以 1GB 块的方式处理数据(压缩、并行、等等操作)(and the strength of Parquet is in its handling of data (compressing, parallelizing, and so on) in 1GB chunks)。
假如你具有一个或多个 Impala 之外生成的 Parquet 数据文件,使用以下某一个方法,你可以快速的使这些数据可以在 Impala 中查询:
- 使用 LOAD DATA 语句移动单个数据文件或某个目录下所有数据文件到 Impala 表对应的数据目录中。这不会验证或转换数据。原始的数据文件必须在 HDFS 中,而不能是在本地文件系统中。
- 使用包含 LOCATION 子句的 CREATE TABLE 语句创建一个数据继续存放在 Impala 数据目录之外的表。原始的数据文件必须在 HDFS 中,而不能是在本地文件系统中。为了加强安全性,假如这些数据是长时间存在并被其他应用重用,你可以使用 CREATE EXTERNAL TABLE 语法,这样 Impala DROP TABLE 语句不会删除这些数据文件。
- 假如 Parquet 表已经存在,你可以直接复制 Parquet 数据文件到表的目录中,然后使用 REFRESH 语句使得 Impala 识别到新添加的数据。请记住对 Parquet 数据文件使用 hdfs distcp -pb 命令而不是 -put/-cp 操作,以保留Parquet 数据文件的 1GB 块大小。参见 Example of Copying Parquet Data Files 了解这种操作的例子。
假如数据在 Impala 之外,并且是其他格式,请结合使用之前提到的技术。首先,使用 LOAD DATA 或 CREATE EXTERNAL TABLE ... LOCATION 语句把数据存入使用对应文件格式的 Impala 表中。然后,使用 INSERT...SELECT 语句复制数据到 Parquet 表中,作为这一处理过程的一部分转换为 Parquet 格式。
加载数据到 Parquet 表是内存密集型操作,因为接受的数据会被缓存一直到达到 1GB 大小,然后这些块的数据在写出之前在内存中被组织和压缩。当插入数据到分区 Parquet 表中时,内存消耗会更大,因为对每一种分区键值的组合都写入一个单独的数据文件,同时可能有几个 1GB 的块在操作(potentially requiring several 1GB chunks to be manipulated in memory at once)。
当向分区 Parquet 表插入数据时,Impala 在节点之间重新分布数据以减少内存消耗。但当插入操作时,你可能仍然需要临时增加 Impala 专用的内存量,或者把加载操作拆分到几个 INSERT 语句中,或者两种方式都采用(You might still need to temporarily increase the memory dedicated to Impala during the insert operation, or break up the load operation into several INSERT statements, or both)。
Impala Parquet 表的查询性能
Parquet 表的数据是划分成一个个的 1GB 的数据文件的("行组(row groups)"),查询时读取以压缩格式存储的每一列的数据,并消耗 CPU 解压数据。因此 Parquet 表的查询性能依赖于查询中 SELECT 列表和 WHERE 子句中需要处理的列的个数,以及是否可以跳过较多的数据文件(分区表),降低 I/O 并减少 CPU 负载(Query performance for Parquet tables depends on the number of columns needed to process the SELECT list and WHERE clauses of the query, the way data is divided into 1GB data files ("row groups"), the reduction in I/O by reading the data for each column in compressed format, which data files can be skipped (for partitioned tables), and the CPU overhead of decompressing the data for each column)。
例如,下面对 Parquet 表的查询是高效的:
select avg(income) from census_data where state = 'CA';
这个查询只处理一大堆列中的两个列。假如表是根据 STATE 分区的,它甚至更有效率,因为查询仅仅需要对每个数据文件读取和解码 1 列,并且它可以只读取 state 'CA' 分区目录下的数据文件,跳过其他 states 的、物理上的位于其他目录的所有数据文件。
select * from census_data;Impala 将不得不读取每一个 1GB 数据文件的整个内容,并解压每一个行组中每一列的内容,浪费了面向列格式的 I/O 优化。对于 Parquet 表,这一查询可能比其他格式的表更快,但是它没有从 Parquet 数据文件格式独有的优势中受益。
Parquet 表的分区
就如 Partitioning 中的描述一样,对 Impala 来说,分区是一项重要而通用的性能技术。本章节介绍一些关于分区 Parquet 表的性能考虑。
Parquet 文件格式非常适合包含许多列,并且绝大部分查询只设计表的少量列表。就如在 How Parquet Data Files Are Organized 中描述的那样,Parquet 数据文件的物理分布使得对于许多查询 Impala 只需要读取数据的一小部分。当你结合使用 Parquet 表和分区时,这一性能受益会更加扩大。基于 WHERE 子句中引用的分区键的比较值,Impala 可以跳过特定分区实体的数据文件。例如,分区表上的查询通常基于年、月、日、或者地理位置的列进行时间段的分析(queries on partitioned tables often analyze data for time intervals based on columns such as YEAR, MONTH, and/or DAY, or for geographic regions)。请记住 Parquet 数据文件使用 1GB 的块大小,所以在确定如何精细的分区数据时,请尝试找到一个粒度,每一个分区都有 1GB 或更多的数据,而不是创建大量属于多个分区的的小文件。
插入到分区 Parquet 表示一个资源密集型(resource-intensive)操作,因为每一个 Impala 节点对于每一个分区键的不同组合都可能潜在的写一个单独的数据文件。大量的同时打开的文件数可能会达到 HDFS "transceivers" 限制。考虑采用以下技术,避免达到这一限制:
- 使用包含特定值的 PARTITION 子句的单独的 INSERT 语句加载不同的数据子集,如 PARTITION (year=2010)
- 增加 HDFS 的 "transceivers" 值,有时候写作 "xcievers" (sic)。在配置文件 hdfs-site.xml 中为 dfs.datanode.max.xcievers 属性。例如,假如你加载 12 年的数据,而分区包含年、月、日,甚至这个值设置为 4096 也可能不够。这一博客使用 HBase 例子作为说明,探讨了设置增加或减少这一值大小时的考虑
- 在数据被复制的源表上采集列统计信息,这样 Impala 查询可以评估分区键列中不同值的个数从而分布工作负载
Parquet 数据文件的 Snappy 和 GZip 压缩
当 Impala 使用 INSERT 语句写入 Parquet 数据文件时,底层的压缩受 PARQUET_COMPRESSION_CODEC 查询选项控制。这一查询选项允许的值包括 snappy (默认值), gzip, 和 none。选项值不区分大小写。假如选项值设置为一个未确认的值,因为无效的选项值,所有查询都将失败,不仅仅是涉及到 Parquet 表的查询。
使用 Snappy 压缩的 Parquet 表
默认的,Parquet 表底层的数据文件采用 Snappy 压缩。快速压缩和解压的组合使得对于许多数据集来说这是一个好选择。为了确保使用了 Snappy 压缩,例如试验了其他压缩编解码之后,请在插入数据之前设置 PARQUET_COMPRESSION_CODEC 查询选项为 snappy:
[localhost:21000] > create database parquet_compression; [localhost:21000] > use parquet_compression; [localhost:21000] > create table parquet_snappy like raw_text_data; [localhost:21000] > set PARQUET_COMPRESSION_CODEC=snappy; [localhost:21000] > insert into parquet_snappy select * from raw_text_data; Inserted 1000000000 rows in 181.98s
使用 GZip 压缩的 Parquet 表
假如你需要更深入的(more intensive)压缩(当查询时需要更多的 CPU 周期以进行解压),请在插入数据之前设置 PARQUET_COMPRESSION_CODEC 查询选项为 gzip :
[localhost:21000] > create table parquet_gzip like raw_text_data; [localhost:21000] > set PARQUET_COMPRESSION_CODEC=gzip; [localhost:21000] > insert into parquet_gzip select * from raw_text_data; Inserted 1000000000 rows in 1418.24s
未压缩的 Parquet 表
假如你的数据压缩作用非常有限,或者你想避免压缩和解压缩实体的 CPU 负载,请在插入数据前设置 PARQUET_COMPRESSION_CODEC 查询选项为 none:
[localhost:21000] > create table parquet_none like raw_text_data; [localhost:21000] > insert into parquet_none select * from raw_text_data; Inserted 1000000000 rows in 146.90s
压缩 Parquet 表的大小和速度
下面的例子演示了 10 亿条复合数据在数据大小和查询速度方面的差异,他们分别使用了不同的编解码器进行压缩(Here are some examples showing differences in data sizes and query speeds for 1 billion rows of synthetic data, compressed with each kind of codec)。与往常一样,使用你自己真实的数据集进行类似的测试。实际的压缩比、对应的插入和查询速度,将取决于实际数据的特征而有所不同。
在例子中,压缩方式从 Snappy 换到 GZip 能减少 40% 的大小,而从 Snappy 换到不压缩将增加 40% 的大小(In this case, switching from Snappy to GZip compression shrinks the data by an additional 40% or so, while switching from Snappy compression to no compression expands the data also by about 40%):
$ hdfs dfs -du -h /user/hive/warehouse/parquet_compression.db 23.1 G /user/hive/warehouse/parquet_compression.db/parquet_snappy 13.5 G /user/hive/warehouse/parquet_compression.db/parquet_gzip 32.8 G /user/hive/warehouse/parquet_compression.db/parquet_none
因为 Parquet 数据文件通常大小是 1GB 左右,每一个目录都包含不同数量的数据文件并安排不同的行组(each directory will have a different number of data files and the row groups will be arranged differently)。
同时,更小的压缩比,那么解压速度就更快。在上面包含 10 亿行记录的表中,对于评估特定列所有值的查询,不使用压缩比使用 Snappy 压缩的快,使用 Snappy 压缩的比使用 Gzip 压缩的快。查询性能依赖于几个不同的因素,所以请一如既往的使用你自己的数据进行自己的基准测试,以获得数据大小、CPU 效率、以及插入和查询操作的速度等方面理想的平衡。
[localhost:21000] > desc parquet_snappy; Query finished, fetching results ... +-----------+---------+---------+ | name | type | comment | +-----------+---------+---------+ | id | int | | | val | int | | | zfill | string | | | name | string | | | assertion | boolean | | +-----------+---------+---------+ Returned 5 row(s) in 0.14s [localhost:21000] > select avg(val) from parquet_snappy; Query finished, fetching results ... +-----------------+ | _c0 | +-----------------+ | 250000.93577915 | +-----------------+ Returned 1 row(s) in 4.29s [localhost:21000] > select avg(val) from parquet_gzip; Query finished, fetching results ... +-----------------+ | _c0 | +-----------------+ | 250000.93577915 | +-----------------+ Returned 1 row(s) in 6.97s [localhost:21000] > select avg(val) from parquet_none; Query finished, fetching results ... +-----------------+ | _c0 | +-----------------+ | 250000.93577915 | +-----------------+ Returned 1 row(s) in 3.67s
复制 Parquet 数据文件
下面是最后一个例子,演示了使用不同压缩编解码器的数据文件在读操作上是如何相互兼容的。关于压缩格式的元数据会写入到每个数据文件中,并且在读取时不管当时 PARQUET_COMPRESSION_CODEC 设置为什么值,都可以正常解码。在这个例子中,我们从之前例子中使用的 PARQUET_SNAPPY,PARQUET_GZIP, PARQUET_NONE 表中复制数据文件,这几个表中每一个表都包含 10 亿行记录, 全都复制到新表 PARQUET_EVERYTHING 的数据目录中。一对简单的查询展示了新表现在包含了使用不同压缩编解码器的数据文件的 30 亿的记录。
首先,我们在 Impala 中创建表以便在 HDFS 中有一个存放数据文件的目标目录:
[localhost:21000] > create table parquet_everything like parquet_snappy; Query: create table parquet_everything like parquet_snappy
然后在 shell 中,我们复制对应的数据文件到新表的数据目录中。不采用 hdfs dfs -cp 这一通常复制文件的方式,我们使用 hdfs distcp -pb 命令以确保 Parquet 数据文件特有的 1GB 块大小继续保留。
$ hdfs distcp -pb /user/hive/warehouse/parquet_compression.db/parquet_snappy \ /user/hive/warehouse/parquet_compression.db/parquet_everything ...MapReduce output... $ hdfs distcp -pb /user/hive/warehouse/parquet_compression.db/parquet_gzip \ /user/hive/warehouse/parquet_compression.db/parquet_everything ...MapReduce output... $ hdfs distcp -pb /user/hive/warehouse/parquet_compression.db/parquet_none \ /user/hive/warehouse/parquet_compression.db/parquet_everything ...MapReduce output...
回到 impala-shell,我们使用 REFRESH 语句让 Impala 服务器识别到表中新的数据文件,然后我们可以运行查询展示数据文件包含 30 亿条记录,并且其中一个数值列的值与原来小表的匹配:
[localhost:21000] > refresh parquet_everything; Query finished, fetching results ... Returned 0 row(s) in 0.32s [localhost:21000] > select count(*) from parquet_everything; Query finished, fetching results ... +------------+ | _c0 | +------------+ | 3000000000 | +------------+ Returned 1 row(s) in 8.18s [localhost:21000] > select avg(val) from parquet_everything; Query finished, fetching results ... +-----------------+ | _c0 | +-----------------+ | 250000.93577915 | +-----------------+ Returned 1 row(s) in 13.35s
与其他 Hadoop 组件交流 Parquet 数据文件
自 CDH 4.5 开始,你可以在 Hive、Pig、MapReduce 中读取和写入 Parquet 数据文件。参考 CDH 4 Installation Guide 了解详细信息。
之前,不支持在 Impala 中创建 Parquet 数据然后在 Hive 中重用这个表。现在从 CDH 4.5 中的 Hive 开始支持 Parquet,在 Hive 中重用已有的 Impala Parquet 数据文件需要更新表的元数据。假如你已经使用 Impala 1.1.1 或更高版本,请使用下面的命令:
ALTER TABLE table_name SET FILEFORMAT PARQUET;
假如你使用比 Impala 1.1.1 更老的版本,通过 Hive 执行元数据的更新:
ALTER TABLE table_name SET SERDE 'parquet.hive.serde.ParquetHiveSerDe'; ALTER TABLE table_name SET FILEFORMAT INPUTFORMAT "parquet.hive.DeprecatedParquetInputFormat" OUTPUTFORMAT "parquet.hive.DeprecatedParquetOutputFormat";
Impala 1.1.1 及以上版本可以重用 Hive 中创建的 Parquet 数据文件,不需要执行任何操作。
Impala 支持你可以编码成 Parquet 数据文件的标量数据类型,但不支持复合(composite)或嵌套(nested)类型如 maps/arrays。假如表中使用了任意不支持的类型,Impala 将无法访问这个表。
假如你在不同节点、乃至在相同节点的不同目录复制 Parquet 数据文件,请使用 hadoop distcp -pb 命令以确保保留原有的块大小。请执行 hdfs fsck -blocks HDFS_path_of_impala_table_dir 并检查平均块大小是否接近 1GB,以验证是否保留了块大小(Hadoop distcp 操作通常会生出一些子目录,名称为 _distcp_logs_*,你可以从目标目录中删除这些目录)。参见 Hadoop DistCP Guide 了解详细信息。
Parquet 数据文件如何组织
尽管 Parquet 是一个面向列的文件格式,不要期望每列一个数据文件。Parquet 在同一个数据文件中保存一行中的所有数据,以确保在同一个节点上处理时一行的所有列都可用。Parquet 所做的是设置 HDFS 块大小和最大数据文件大小为 1GB,以确保 I/O 和网络传输请求适用于大批量数据(What Parquet does is to set an HDFS block size and a maximum data file size of 1GB, to ensure that I/O and network transfer requests apply to large batches of data)。
在成G的空间内,一组行的数据会重新排列,以便第一行所有的值被重组为一个连续的块,然后是第二行的所有值,依此类推。相同列的值彼此相邻,从而 Impala 可以对这些列的值使用高效的压缩技术(Within that gigabyte of space, the data for a set of rows is rearranged so that all the values from the first column are organized in one contiguous block, then all the values from the second column, and so on. Putting the values from the same column next to each other lets Impala use effective compression techniques on the values in that column)。
Parquet 数据文件的 HDFS 块大小是 1GB,与 Parquet 数据文件的最大大小相同,一边每一个数据文件对应一个 HDFS 块,并且整个文件可以在单个节点上处理,不需要任何远程读取。假如在文件复制时块大小重设为较低的值,你将发现涉及到这些文件的查询性能更低,并且 PROFILE 语句将会揭示一些 I/O 是次优的,会通过远程读取。参见 Example of Copying Parquet Data Files 了解当复制 Parquet 数据文件时如何保留块大小的例子。
当 Impala 检索或测试特定列的数据时,它将打开所有的数据文件,但只会读取每一个文件中这些列的值连续存放的位置(but only reads the portion of each file where the values for that column are stored consecutively)。假如其他的列在 SELECT 列表或 WHERE 子句中列出,在同一个数据文件中同一行的所有列的数据都可用。
假如一个 INSERT 语句带来少于 1GB 的数据,结果的数据文件小于理想大小。因此, 如何你是把一个 ETL 作业拆分成多个 INSERT 语句,请尽量保障每一个 INSERT 语句插入的数据接近 1GB 或 1GB 的倍数。
Parquet 数据文件的行程编码(RLE)和字典编码
Parquet 使用一些自动压缩技术,例如行程编码(run-length encoding,RLE) 和字典编码(dictionary encoding),基于实际数据值的分析。一当数据值被编码成紧凑的格式,使用压缩算法,编码的数据可能会被进一步压缩。Impala 创建的 Parquet 数据文件可以使用 Snappy, GZip, 或不进行压缩;Parquet 规格还支持 LZO 压缩,但是目前 Impala 不支持 LZO 压缩的 Parquet 文件。
除了应用到整个数据文件的 Snappy 或 GZip 压缩之外,RLE 和字段编码是 Impala 自动应用到 Parquet 数据值群体的压缩技术。这些自动优化可以节省你的时间和传统数据仓库通常需要的规划。例如,字典编码减少了创建数字 IDs 作为长字符串的缩写的需求。
行程编码(RLE)压缩了一组重复数据值。例如,假如许多连续的行具有相同的国家编码,这些重复的值可以表示为值和紧跟其后的值连续出现的次数。
字典编码取出存在于列中的不同的值,每一个值都表示为一个 2 字节的组合而不是使用原始的可能有多个字节的值(对这些压实的值进行了压缩,额外节省了空间)。当列的不同值的个数少于 2**16 (16,384)个时,使用这一类型的编码。他不会对 BOOLEAN 类型的列使用,因为已经足够短。TIMESTAMP 的列有时每行都有不同的值,这时候可能很快就超过 2**16 个不同值的限制。列的这一 2**16 的限制被重置为每一个数据文件内的限制,这样如果几个不同的数据文件每个都包含 10,000 个不同的城市名,每一个数据文件中的城市名列仍然可以使用字典编码来凝练。