apache-hive / 3.1.1 / reference / Hive_Metadata_Caching_Proposal.html

Hive 元数据缓存建议

为什么要使用 Metastore 缓存

在 Hive 2 基准测试期间,我们发现 Hive Metastore 操作花费大量时间,因此减慢了 Hive 的编译速度。在某些极端情况下,它需要比实际查询运行时间更长的时间。特别是,我们发现云数据库的延迟非常高,并且总查询运行时的 90%正在 await 元存储 SQL 数据库操作。基于此观察结果,如果我们有一个存储数据库查询结果的内存结构,那么元存储操作的性能将大大提高。

服务器端与 Client 端缓存

我们正在考虑两个可能的缓存位置。一个在 metastoreClient 端,另一个在 metastore 服务器端。Client 端缓存和服务器缓存都必须是一个单例,并在 JVM 中共享。让我们以 Metastore 服务器端缓存为例,如下所示:

ms2.png

在这里,我们显示了使用单个远程元存储的两个 HiveServer2 实例。元存储服务器将具有一个缓存,因此由两个 HiveServer2 实例共享。实际上,我们通常将 HiveServer2 与嵌入式 metastore 一起使用。在此图中,元存储服务器代码位于 HS2 JVM 实例内部,并且元存储服务器缓存在 HiveServer2 中共享:

ms1.png

另一方面,MetastoreClient 端位于 Client 端 JVM 中,一旦 Client 端消失,它就会消失。

ms3.png

如果我们将 HiveServer2 与嵌入式 metastore 一起使用,则 Client 端和服务器端缓存都没有太大的区别,因为我们仅在 HiveServer2 上使用缓存的一个副本。我们需要确保如果选择同时实现 Client 端缓存和服务器缓存,则仅使用一个缓存,这样就不会浪费额外的内存。

通常,在遇到高速缓存命中的情况下,MetastoreClient 端高速缓存具有更好的性能,因为它进一步避免了到 Metastore 服务器的网络往返。由于更多 Client 端共享 Metastore 服务器缓存,因此缓存命中率更高。Client 端和服务器端缓存是独立的,没有什么可以阻止我们同时实现两者。但是,我们的实验表明,主要的瓶颈是数据库查询而不是网络流量。这有助于我们在最初的工作中专注于 metastore 服务器缓存。

Cache Consistency

如果我们在两个或多个 JVM 实例中有缓存,则需要处理缓存一致性问题。例如,假设元存储服务器 A 和 B 都缓存了表 X,并且此时 Client 端通过元存储 A 更改了表 X。元存储 A 可能会使缓存的表 X 无效并保持一致的缓存。但是,Metastore B 无法实现更改,而是 continue 使用陈旧的高速缓存表 X。我们当然可以采用高速缓存逐出策略来使旧条目无效,但是不可避免会有滞后。

为了解决此问题,我们设想了几种使远程 JVM 上的过时缓存无效的方法:

  • 基于时间的同步。缓存将定期从数据库中刷新。但是,我们需要确保刷新期间缓存保持一致。这是当前实施的一部分。

  • Metastore 有一个事件日志(当前用于实现复制 v2)。事件日志捕获对元数据对象的所有更改。因此,我们将能够监视每个缓存实例上的事件日志,并使更改的条目无效(HIVE-18056-获取问题详细信息... STATUS)。由于事件传播,这可能会有较小的滞后,但是应比缓存逐出要短得多。

  • 维护 SQL 数据库中每个对象的唯一 ID(例如,修改的时间戳,版本 ID 或 md5 签名),每次我们更改 SQL 数据库中的对象时,ID 都会不同。我们将检查数据库是否针对每个缓存访问更改了对象。但是,如果数据库延迟很高,那么即使检查 SQL 数据库中的时间戳也可能需要一些时间

  • 另外,如果用户想要强制执行缓存刷新,我们可以选择在 Hive 中添加“刷新缓存”语句。但是,这应该是 Management 员权限声明,会使我们的安全模型复杂化。

如果存在需求,我们还可以在多个 Metastore 实例之间实现缓存一致性协议。这样的协议将需要将更改复制到所有活动的元存储,然后最终提交更改并响应 Client 端的写/更新请求(也许使用类似于两阶段提交协议的内容)。

案例研究:Presto

Presto 在其协调器中具有全局 metastoreClient 端缓存(相当于 HiveServer 2)。注意 Presto 当前在群集中只有 1 个协调器,因此,如果用户仅通过 Presto 更改对象,则不会遇到高速缓存一致性问题。但是,如果用户还通过 Hive 更改 metastore 中的对象,则会遇到相同的问题。

Presto 采用基于 Guava 的 LRU 缓存,其默认过期时间为 1h,默认最大条目为 10000(可调)。缓存的元存储库是可插入的。缓存的 metastoreClient 端和非缓存的版本都是公共接口的实现,并且可以由配置激活。

Presto 具有以下缓存:

  • 点查找缓存

  • databaseCache

    • tableCache

    • partitionCache

    • userRolesCache

    • userTablePrivileges

  • 范围扫描缓存

  • databaseNamesCache:正则表达式->数据库名称,方便数据库搜索

    • tableNamesCache

    • viewNamesCache

    • partitionNamesCache:表名称->分区名称

  • Other

  • partitionFilterCache:PS->分区名称,有助于分区修剪

对于每个分区过滤条件,Presto 会将其分解为 tupleDomain 和余数:

AddExchanges.planTableScan:

DomainTranslator.ExtractionResult decomposedPredicate = DomainTranslator.fromPredicate(

metadata,

session,

deterministicPredicate,

types);

公共静态类 ExtractionResult

{

私人最终 TupleDomain<Symbol> tupleDomain;

私有最终表达式剩余表达式;

}

tupleDomain 是列->范围或精确值的 Map。转换为 PS 时,任何范围都将转换为通配符,并且仅考虑精确值:

HivePartitionManager.getFilteredPartitionNames:

对于(HiveColumnHandle partitionKey:partitionKeys){

如果(domain!= null && domain.isNullableSingleValue()){

filter.add(((Slice) value).toStringUtf8());

else {

filter.add(PARTITION_VALUE_WILDCARD);

}

}

例如,表达式“状态= CA 以及'201612'和'201701'之间的日期将细分为 PS(状态= CA),其余日期将介于'201612'和'201701'之间。Presto 将检索状态为=的分区从 PS->分区名称缓存和分区对象缓存中获取 CA,并为返回的每个分区评估“日期在'201612'和'201701'之间。与为每个表达式缓存分区名称相比,这是一个很好的平衡。

Our Approach

我们的设计是一个 metastore 服务器端缓存,我们将在收到 metastore 事件后执行 metastore 失效。以上各节讨论了这两种选择的原因。

此外,在我们的设计中,Metastore 将在启动时(预热)读取一次所有 Metastore 对象,并且此后没有逐出该 Metastore 对象。我们唯一更改缓存的时间是用户通过元存储 Client 端(例如,更改表,更改分区)请求更改时,以及在收到其他元存储服务器做出的更改的元存储事件时。请注意,在预热期间(如果元数据大小很大,可能会花费很长时间),我们将允许元存储库处理服务器请求。如果已经缓存了表,则可以从缓存中为该表(及其分区和统计信息)提供请求。如果尚未对该表进行预热,则将从数据库中满足对该表的请求(HIVE-18264-获取问题详细信息... STATUS)。

当前,可以通过缓存白名单和黑名单模式(HIVE-18056-获取问题详细信息... STATUS)的组合来限制 metastore 缓存的大小。在缓存表之前,将根据这些过滤器对其进行检查,以确定是否可以缓存该表。同样,读取表时,如果未通过上述过滤器,则会从数据库而不是从缓存中读取该表。

定量研究:内存占用量和预热时间

这种方法的主要问题是,元存储高速缓存将消耗多少内存,以及在启动时读取所有元存储对象(预热)的延迟时间。为此,我们进行了一些定量实验。

在我们的实验中,我们采用了下面讨论的一些内存优化,即将表/分区和存储 Descriptors 分开。我们仅缓存数据库/表/分区对象,不计算列统计信息/永久性功能/约束。在我们的设置中,内存占用空间由分区对象控制,分区对象总计为 58M(如下表所示)。这并不妨碍许多字符串可以被多个对象嵌入和共享的事实,根据 HIVE-16079 的说法,这可以另外节省 20%的事实。

object count 平均大小(字节)
table 895 1576
partition 97,863 591
storagedescriptor 412 680

如果我们为元存储缓存保留了 6G 内存,那么我们可以负担 10,000,000 个分区对象,并且我们认为这对于大多数用例来说已经足够。万一我们超出了内存范围,我们可以切换到非缓存的元存储并且不会崩溃。

我们还在相同的环境下测试了预热时间。 Metastore 从 MySql 数据库加载所有数据库/表/分区需要 25 秒。请注意,在速度较慢的数据库(例如 Azure)中,该数字可能会更大,但我们尚未进行测试。 Metastore 仅在预热后才打开监听端口。在预热之前,所有 MetastoreClient 请求将被拒绝。

Memory Optimization

我们计划采用两种内存优化技术。

第一个是将存储 Descriptors 与表/分区分开。这与我们在 HBaseMetastore 工作中使用的技术相同,并且基于观察到共享存储 Descriptors 的主要组件。可能在分区之间变化的唯一存储 Descriptors 组件是位置和参数。我们从共享存储 Descriptors 中提取这两个组件,并将其存储在分区级别。

第二种技术是 HIVE-16079 中建议的内部共享字符串,它基于观察到的大多数参数字符串都是相同的。

Cached Objects

存储在缓存中的关键对象是

  • Db

  • Table

  • Partition

  • Permanent functions

  • Constraints

  • ColumnStats

我们将把节俭对象缓存在缓存中,因此我们可以简单地在 RawStore API 之上实现包装器。

我们不打算缓存角色和特权,因为我们偏向于诸如 Ranger 之类的外部访问控制系统,而较少关注 SQLStdAuth。

Cache Update

对于更改对象的本地 metastore 请求(例如 alter table/alter 分区),更改请求将直写到包装的 RawStore 对象。同时,我们应该能够更新缓存,而无需进一步从 SQL 数据库中取回数据。

对于远程 Metastore 更新,我们将使用定期同步(当前方法),或者监视事件日志并从 SQL 数据库中获取受影响的对象(HIVE-18661-获取问题详细信息... STATUS)。在“缓存一致性”部分中已经讨论了这两个选项。

Aggregated Statistics

我们已经在 ObjectStore 中聚合了 stats 模块(HIVE-10382-获取问题详细信息... STATUS)。但是,基本列统计信息不会被缓存,并且每次需要都需要从 SQL 数据库中获取。我们计划将汇总的统计信息模块移植到 CachedStore,以使用缓存的列统计信息进行计算。一种尚待做出的设计选择是我们是否需要缓存汇总的统计信息,还是假设所有列的统计信息都在内存中都在 CachedStore 中动态计算它们。但是,无论哪种情况,一旦我们在 CacheStore 中打开了聚合统计信息,我们都将在 ObjectStore 中将其关闭(已经有一个开关),因此我们不会重复执行两次。

getPartitionsByExpr

这是我们要优化的 Hive Metastore 中最重要的操作之一。 ObjectStore 已经可以根据内存中的分区列表评估表达式。我们计划采用相同的方法。该假设是,即使我们需要评估内存中表的每个分区的表达式,尽管我们可以将某些表达式条件推送到 sql 数据库,但计算时间仍比数据库访问时间小得多。某些初始测试(如下所示)支持此操作,但以后需要进行更多评估。在出现问题的情况下,可以加快存储表达评估的存储索引。

Architecture

CachedStore 将实现 RawStore 接口。 CachedStore 在内部包装了一个真实的 RawStore 实现,可以是任何形式(ObjectStore 或 HBaseStore)。在 HiveServer2 嵌入式 metastore 或独立 metastore 设置中,我们将 hive.metastore.rawstore.impl 设置为 CachedStore,并将 hive.metastore.cached.rawstore.impl(包装的 RawStore)设置为 ObjectStore。如果我们将 HiveCli 与嵌入式 metastore 一起使用,则可能要跳过 CachedStore,因为我们可能不希望预热延迟。

Potential Issues

由于时间限制,初始版本中可能存在一些潜在的问题或未实现的功能:

  • 通过监视事件队列来使远程 Metastore 无效。上面讨论过,可能没有在版本 1 中进行讨论

  • getPartitionsByFilter 可能未实现。此 API 采用字符串形式的表达式,我们需要在 metastore 中进行解析和评估。此 API 仅在 HCatalog 中使用,并且用于向后兼容。 Hive 本身将使用 getPartitionsByExpr,它采用表达式的二进制形式并且已经解决了

  • 最好有一个缓存内存估计,以便我们可以控制缓存的内存使用情况。但是,内存估计可能会比较棘手,尤其是考虑到内联字符串时。在最初的版本中,我们可能只对分区数设置了限制,因为我们看到内存使用量是分区的主导。

  • 在当前设计中,一旦缓存使用率超过阈值(分区数超过阈值,或者在 Future 版本中内存使用率超过阈值),Metastore 将退出。如果 metastore 仍在缺少缓存的情况下运行,则诸如 getPartitionsByExpr 之类的某些操作将产生错误的结果。一种潜在的优化方法是我们逐出表级别的缓存,即,一旦内存已满,就逐出某些表及其所有分区。当我们想要丢失的表/分区时,请从 SQL 数据库中检索该表以及所有分区。这是可能的,但是给当前设计增加了很多复杂性。如果将来我们发现内存占用过多,我们可能只会考虑它。

  • 预热时,我们在一个 SQL 操作中获取表的所有分区。这可能是问题,也可能不是问题。但是,一种选择不是一个分区,因为这将花费很长时间。如果这成为问题,我们可能需要找到某种解决方法(例如分页)

与 Presto 比较

在我们的设计中,为了简化简单性和改善运行时性能,我们牺牲了预热时间和内存占用。通过监视事件队列,可以解决 Presto 中缺少的远程元存储一致性问题。在体系结构级别上,CachedStore 是包裹 true 的 RawStore 的轻量级缓存层,通过这种设计,除了当前的方法之外,没有什么可以阻止我们实施替代的缓存策略。