在这篇文章中,我将谈谈Hive表有很多小分区和文件的问题,并详细介绍我的解决方案。
在我的项目中,我们将很多数据保存在HDFS中。其中大部分是原始数据,但也有相当一部分是许多数据加工后的最终产物。为了方便管理所有的数据流水线,所有Hive表的默认分区方式是小时DateTime分区(例如:dt='2019041316')。
对于在HDFS中保存这么多分区文件,这是一种不好的做法。假设这些表必须存储在HDFS中--我们需要面对一些关于存储管理的课题,也就是我们所说的"分区管理"。
我在之前的几篇文章中写过,Hadoop的一个主要问题就是 "多小文件"问题。当我们有一个数据进程,每隔一个小时就会给某个表增加一个新的分区,并且它已经运行了2年以上,我们需要开始处理这个表。2年时间里有243652(17520)个小时,所以我们会有一个近2万个分区的表。而我要说明的是,我们在这里讨论的体积是每小时1MB左右。现在想象一下,有500张这样的表。
我不知道你们有没有人试过为了读取20GB的数据而扫描20000个分区(即文件),开销是巨大的。不管是什么技术。Spark,Hive,MapReduce,Impala,Presto--当有太多的小分区时,它们的性能都非常糟糕。现在想象一下,每天有上千次查询,每张表扫描上千个分区。
HDFS的问题,在于它只是一个分布式文件系统。这就是为什么我个人建议你把你的最终产品表存储在一个像Apache Kudu这样的存储中,或者像MySQL或PostgreSQL这样的RDBMS中。但如果出于某些原因,你把数据保存在HDFS中,你就需要编写自己的存储管理层。
那么,这个存储管理层到底应该做什么--要看你的具体问题。例如,在我们的案例中,有3个目标。
1. 合并选定表上的分区
我想让 "分区管理器 "定期将小时分区合并为月分区。我之所以选择月维度作为合并标准,是因为它能生成最佳大小的分区(100mb-1gb)。例如,我不希望一张表按月分区,另一张表按年分区,因为我想让我们的用户(包括分析师和数据开发人员)感到简单。合并过程将在后面详细介绍。
2. 对冷数据进行归档
有时我们想把某些表的数据保存多年,尽管旧数据可能会少用很多。我希望我的存储管理层能够对2年或3年以上的分区进行 "归档"(当然这取决于你的使用情况)。这是通过将数据移动到另一个版本的表中,并使用更积极的压缩算法,如GZIP(与 "热 "表中的SNAPPY相比)。
3. 删除分区
当然,我们可能希望为我们要从HDFS中删除其旧数据的表选择一定的阈值(很可能是时间阈值)。这是我们希望存储管理层提供的一个非常基本(但必要)的功能。
这3个功能都很重要,但我认为第一个功能是最棘手的,也是我开始写这篇文章的实际原因,是为了解释我认为应该怎么做。
首先,我不得不说,这个任务的复杂程度真的取决于你的情况。在我们的情况下,定期合并分区的任务并不简单,因为有以下要求。
生产表对停机时间的容忍度为零 尤其是超过几秒钟的时候就不行了。
不丢失时间解析--我们发现有些表是按DT分区的,但没有其他匹配的时间列。这意味着如果我们要把 "2018041312 "合并到 "201804",用户就会失去这些分区上的每日和每小时的维度数据。
尽可能的无缝--我们的目标是让分区合并对用户来说是无缝的。我们发现,在某些情况下,用当前的分区方法根本不可能,但在不同的分区方法中,这是很可能的。后面会详细介绍。
所以,现在我们意识到这可能不是一个简单的问题,我们来看看我们是如何解决的。
如何合并Hive表中的分区?
这个过程非常简单。这里描述的所有查询都是Impala查询,但在其他技术中,如Hive、SparkSQL、Presto等,其语法相当相似(有时甚至相同)。另一个需要记住的是,在 "合并查询 "之前,你可能需要执行的设置,以使它们生成最佳大小的文件。例如在Impala中,你可能要执行。
set num_nodes=1;
执行合并查询--下面的例子演示了将2019年4月的所有小分区合并到一个月度分区。
INSERT OVERWRITE tbl PARTITION(dt) AS SELECT t.col1, t.col2, ..., SUBSTR(t.dt, 1, 6) AS dt FROM tbl t WHERE t.dt LIKE '201904%';
从元存储中删除旧的分区(如果是外部表,只有分区元数据会被删除),例如:
ALTER TABLE tbl DROP PARTITION(dt='2019040101')。
从HDFS中删除每个旧分区的目录。
curl -i -X DELETE "http://:/webhdfs/v1/?op=DELETE&recursive=true"
如果你使用的是Impala,你应该使表的元数据缓存无效。
INVALIDATE METADATA tbl;
很简单,不是吗,好吧,这样我们就解决了第一个问题--没有停机时间,但权衡的结果是,有一个时间窗口,用户会看到一个重复的数据(直到所有的DROP PARTITIONS完成)。我们决定这样做很好。
那么好吧,现在我们没有统一的小时DT分区,而是统一的月DT分区。这完全不是无缝的,我们可能会在某些表中失去时间分辨率。在处理分辨率损失之前,我们先解决第一个问题。
用户视图中的合并分区
我们的用户分为。"普通 "用户、分析师、数据科学家和数据工程师。数据科学家和数据工程师是一个小规模的技术群体,所以他们很容易适应这种变化。问题是分析师和普通用户,我们有很多人。
反正这么简单的变化,为什么很难适应呢?很简单,就是201801而不是2018010101。好吧,原因是--如果用户会用下面的方式查询一张表。
SELECT * FROM tbl WHERE dt > '2018011220' AND < '2018022015';
它将只扫描'201802'的分区。不仅如此,它还会得到所有的月份,而不仅仅是他想要的日期。为了得到正确的结果,他们将不得不改变DT过滤,并添加另一个时间列(在下面的例子中 "insertion_time")过滤。
SELECT * FROM tbl WHERE (dt BETWEEN '201801' AND '201802') AND (from_timestamp(insertion_time, 'yyyMMddHH') BETWEEN '2018011220' AND '2018022015');
```
但是我们并没有处理所有表的合并分区,只是处理有问题的表(那些由许多小文件组成,并且经常被查询的表)。所以我不能告诉我的用户,从现在开始所有的表都是这样管理的(过去2个月--每小时,超过2个月的--每月)。因为只有部分表是这样处理的,所以要求他们在查询之前先检查一下。这是无缝的反面。
对于普通用户,我们花了一点力气就解决了这个问题:我们改变了他们使用的BI系统中的查询模板,以适应新的分区。普通用户是通过某个BI工具来查询我们的datalake。这样一来,查询本身对用户是透明的。我可以很容易的把模板从:
```
... WHERE (dt BETWEEN from_timestamp({from}, 'yyyyMMddHH') AND from_timestamp({to}, 'yyyyMMddHH')) AND (insertion_time BETWEEN {from} and {to});
```
到:
```
... WHERE (dt BETWEEN from_timestamp({from}, 'yyyyMM') AND from_timestamp({to}, 'yyyyMMddHH')) AND (insertion_time BETWEEN {from} and {to});
```
注意,我将{to}的格式保持为小时(yyyyMMddHH)。因为,例如,如果一个用户想查询最近6个月的数据--我不想让他错过最后一个月,就像下面的查询。
```
SELECT * FROM tbl WHERE dt BETWEEN '201810' AND '201904';
```
这个查询会错过2019年4月的所有分区, 因为它们仍然是以小时为单位的格式, 相反,我希望这个查询看起来像这样:
```
SELECT * FROM tbl WHERE dt BETWEEN '201810' AND '2019041519';
```
好处是,即使用户只想查询旧的月份,比如说2018年10月到12月,也能正常工作,得到所有相关的月度分区。
嗯,这对我们来说其实已经很好了,因为我们绝大多数用户都是用BI工具,自己不写SQL。至于那些确实会写SQL的分析师,我们决定他们必须检查分区管理器中是否管理了一张表,并相应地调整他们的查询--当然我们必须支持并帮助他们适应这种方法。
我们是如何处理解析损失的
这只需要改变表本身:增加一个 "original_dt "列,并确保填充该表的数据过程 "知道 "它的存在。当然,我们需要应用同样的过程,改变BI系统中的相关查询模板,并让分析师知道这个变化。
嵌套的日期时间分区
最简单的方法是当你有嵌套DT分区而不是平面DT时,就可以进行无缝分区合并。
year=2019/month=04/day=16/hour=19
这样的话,在合并小分区的同时,将省略的分辨率作为列添加到表格中,对用户来说将是完全透明的。例如,如果我想将小时分区合并为月分区,我将按照以下步骤操作。
在原表的基础上创建一个合并版本,像这样:
```
CREATE EXTERNAL TABLE tbl_merged_nested (
col1 STRING,
col2 STRING,
...,
day STRING,
hour STRING
) PARTITIONED BY (
year STRING,
month STRING
)
STORED AS PARQUET;
```
执行合并查询。
```
INSERT OVERWRITE TABLE tbl_merged_nested PARTITION(year, month)
SELECT col1, col2, ..., day, hour, year, month FROM tbl_original_nested WHERE year='2019' AND month='04';
```
从metastore中删除原表的旧分区(当然之后也要从HDFS中删除这个目录)。
```
ALTER TABLE tbl_original_nested DROP PARTITION(year='2019', month='04', day='17', hour='20')
```
然后,如果我想要一个 "热 "的表和一个 "合并 "的表,我可以创建一个视图来联合这两个表,这对用户来说将是完全无缝的,他们不会关心年、月、日或小时列是否是分区。
```
CREATE VIEW union_view AS SELECT * FROM tbl_original_nested UNION ALL SELECT * FROM tbl_merged_nested
```
因此,要想真正无缝合并DT分区,最重要的条件就是要让它们嵌套而不是扁平化。
### 总结
不要在HDFS中存储小型的、经常查询的表,尤其是当它们由数千个文件组成时更不要。将它们存储在另一个地方,比如RDBMS(MySQL、PostgreSQL等)或Apache Kudu中,如果你想留在Hadoop生态系统中。当然,你必须提供一个解决方案来执行这些表和Hive表之间的连接查询,我推荐Presto(如果你使用Kudu,Impala也可以)。
如果你必须将它们存储在HDFS中,一定要有一个存储管理层("分区管理器")来处理分区合并,防止出现表有很多小文件的情况。
如果你想让分区合并对用户透明化,那么分区合并是很困难的。但与扁平的DT分区相比,嵌套的分区让无缝合并变得更加容易。
希望这篇文章能对一些人有所帮助,也请大家发表评论,分享你对Hadoop中分区管理的看法。
内容来源于网络,如有侵权,请联系作者删除!