溢出到磁盘

概述

在内存密集型操作的情况下,Presto 允许将中间操作结果卸载到磁盘。该机制的目标是允许执行需要超过每个查询或每个节点限制的内存量的查询。

该机制类似于操作系统级别的页面交换。但是,它是为了满足 Presto 的特定需求而在应用程序级别实现的。

与溢出相关的属性在 溢出属性 中描述。

内存管理和溢出

默认情况下,如果查询执行请求的内存超过会话属性 query_max_memoryquery_max_memory_per_node,Presto 将会终止查询。此机制确保对查询的内存分配公平,并防止由内存分配引起的死锁。当集群中有很多小型查询时,它很有效,但会导致终止不符合限制的大型查询。

为了克服这种低效率,引入了可撤销内存的概念。查询可以请求不计入限制的内存,但此内存可以在任何时候被内存管理器撤销。当内存被撤销时,查询运行器会将中间数据从内存溢出到磁盘,并在稍后继续处理它。

在实践中,当集群处于空闲状态,所有内存都可用时,内存密集型查询可能会使用集群中的所有内存。另一方面,当集群没有太多可用内存时,相同的查询可能会被迫使用磁盘作为中间数据的存储。被迫溢出到磁盘的查询的执行时间可能比完全在内存中运行的查询长几个数量级。

为了在仍然允许更大的查询利用溢出到磁盘的同时实现更一致的查询性能,experimental.query-limit-spill-enabled 可以设置为 true。此属性将在查询的内存使用量超过每个节点的总内存限制时触发溢出,即使内存池没有满。有关详细信息,请参阅 溢出属性

请注意,启用溢出到磁盘并不能保证所有内存密集型查询的执行。某些内存密集型操作尚不支持溢出。此外,查询运行器仍有可能无法将中间数据分割成足够小的块,以至于每个块都能放入内存,从而导致在从磁盘加载数据时出现 Out of memory 错误。

可撤销内存和保留池

保留内存池和可撤销内存都旨在应对低内存情况。当用户内存池耗尽时,单个查询将被提升到保留池。在这种情况下,只有该查询被允许继续进行,从而降低了集群并发性。可撤销内存将尝试通过触发溢出来阻止这种情况。保留池的大小为 query.max-total-memory-per-node。如果 query.max-total-memory-per-node 比节点上可用的总内存大,则一般内存池可能没有足够的内存来运行更大的查询。如果启用了溢出,那么这会导致对每个节点消耗大量内存的查询进行过度溢出。如果禁用了溢出,这些查询会更快完成,因为它们将在保留池中执行。但是,这样做也会显着降低集群并发性。在这种情况下,我们建议通过 experimental.reserved-pool-enabled 配置属性来禁用保留内存池。

溢出磁盘空间

将中间结果溢出到磁盘并将其取回在 IO 操作方面成本很高。因此,使用溢出的查询可能会受到磁盘的限制。为了提高查询性能,建议在独立的本地设备上提供多个路径用于溢出(属性 spiller-spill-path溢出属性 中)。

系统驱动器不应用于溢出,尤其是不要用于 JVM 运行和写入日志的驱动器。这样做可能会导致集群不稳定。此外,建议监控配置的溢出路径的磁盘饱和度。

Presto 将溢出路径视为独立磁盘(参见 JBOD),因此无需对溢出使用 RAID。

溢出压缩

当启用溢出压缩时(spill-compression-enabled 属性在 溢出属性 中),溢出的页面将使用与交换压缩相同的实现进行压缩,前提是它们的可压缩性足够高。启用此功能可以减少磁盘 IO 量,但会增加压缩和解压缩溢出页面的额外 CPU 负载。

溢出加密

当启用溢出加密时(spill-encryption-enabled 属性在 溢出属性 中),溢出内容将使用随机生成的(每个溢出文件)密钥进行加密。启用此功能会降低溢出到磁盘的性能,但可以防止从写入磁盘的文件中恢复溢出数据。

注意:某些 Java 发行版附带的策略文件限制了可使用的加密密钥的强度。溢出加密使用 256 位 AES 密钥,可能需要无限制强度 JCE 策略文件才能正常工作。

支持的操作

并非所有操作都支持溢出到磁盘,并且每个操作对溢出的处理方式都不同。目前,该机制针对以下操作实现。

连接

在连接操作期间,被连接的表之一存储在内存中。该表称为构建表。来自另一个表的行流过并传递到下一个操作,前提是它们与构建表中的行匹配。连接中最消耗内存的部分是这个构建表。

当任务并发性大于一时,构建表会被分区。分区数量等于 task.concurrency 配置参数的值(参见 任务属性)。

当构建表被分区时,溢出到磁盘机制可以降低连接操作所需的峰值内存使用量。当查询接近内存限制时,构建表的一部分分区将被溢出到磁盘,以及落入这些相同分区的来自另一个表的行。被溢出的分区数量会影响所需的磁盘空间量。

之后,溢出的分区会逐一读回,以完成连接操作。

使用这种机制,连接操作使用的峰值内存可以减少到最大构建表分区的大小。假设没有数据倾斜,这将是 1 / task.concurrency 倍的整个构建表的大小。

聚合

聚合函数对一组值执行操作并返回一个值。如果您聚合的组数很大,则可能需要大量的内存。当启用溢出到磁盘时,如果没有足够的内存,则会将中间累积的聚合结果写入磁盘。在有可用内存时,它们会被加载回来并合并。

窗口

窗口函数对一组行执行操作,并为每行返回一个值。如果窗口中的行数很大,则可能需要大量的内存。当启用溢出到磁盘时,如果没有足够的内存,则会将中间结果写入磁盘,并在处理每个窗口时读取回来。如果单个窗口太大,查询仍然可能耗尽内存。

排序

当需要排序的行很多时,排序可能会使用大量内存。当启用溢出到磁盘时,如果没有足够的内存,则会将排序后的行写入磁盘,然后在内存中合并在一起。