
# 溢出到磁盘

## 概述

对于内存密集型操作，openLooKeng允许将中间操作结果卸载到磁盘。此机制的目标是执行需要超过每个查询或每个节点限制的内存量的查询。

这种机制类似于操作系统级的页面交换。但是，该机制是在应用程序级别实现的，以满足openLooKeng的特定需要。

与溢出相关的属性在`tuning-spilling`中描述。

## 内存管理与溢出

默认情况下，如果查询执行所请求的内存超过会话属性`query_max_memory`或`query_max_memory_per_node`，openLooKeng将终止查询。这种机制保证了查询内存分配的公平性，避免了内存分配导致的死锁。当集群中有大量小查询时，效率很高，但是会导致超出限制的大型查询被终止。

为了克服这种低效率，引入了可回收内存的概念。查询可以请求不计入限制的内存，但内存管理器可以随时回收该内存。当内存被回收时，查询运行程序将中间数据从内存溢出到磁盘并稍后继续处理。

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

请注意，启用溢出到磁盘并不保证执行所有内存密集型查询。查询运行程序仍然可能无法将中间数据划分为足够小的块使每个块都适合放入内存，从而导致从磁盘加载数据时导致`Out of memory`错误。

## 可回收内存和保留池

保留内存池和可回收内存都是为了应对低内存条件而设计的。当用户内存池耗尽时，单个查询将提升到保留池。在这种情况下，只允许该查询进行，从而降低集群并发性。可回收内存将试图通过触发溢出来防止这种情况发生。保留池的大小为`query_max_memory_per_node`。这意味着，当`query_max_memory_per_node`较大，那么用户内存池可能比`query_max_memory_per_node`小得多。对于每个节点都需要消耗大量内存的查询，这将导致过多的溢出。当溢出被禁用时，这些查询可以更快地完成，因为它们在保留池中执行。在这种情况下，我们建议通过`experimental.reserved-pool-enabled`配置属性禁用保留内存池。

## 溢出磁盘空间

将中间结果溢出到磁盘并将其取回对于I/O操作来说代价较高。因此，使用溢出的查询可能会受到磁盘的限制。为了提高查询性能，建议在单独的本地设备上提供多个路径用于溢出（`tuning-spilling`中的属性`spiller-spill-path`）。

不应使用系统驱动器来溢出，尤其是不要溢出到JVM正在运行并写日志的驱动器上。这样做可能导致集群不稳定。另外，建议对配置的溢出路径进行磁盘饱和度监控。

openLooKeng将溢出路径视为独立的磁盘（参见[JBOD](https://en.wikipedia.org/wiki/Non-RAID_drive_architectures#JBOD )），因此无需使用RAID进行溢出。

## 溢出压缩

当启用溢出压缩（`tuning-spilling`中的`spill-compression-enabled`属性）时，溢出页将被压缩后再写入磁盘。启用此特性可以减少磁盘I/O，但会牺牲额外的CPU负载来压缩和解压缩溢出页。

## 溢出加密

当启用溢出加密（`tuning-spilling`中的`spill-encryption-enabled`属性）时，溢出内容将使用随机生成的（每个溢出文件）密钥进行加密。启用此功能将增加CPU负载并降低溢出到磁盘的吞吐量，但可以防止溢出的数据从溢出文件中恢复。考虑在启用溢出加密时减小`experimental.memory-revoking-threshold`的值，以应对溢出延迟的增加。

## 支持操作

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

### 联接

在联接操作期间，正在联接的表之一存储在内存中。此表称为构建表。如果来自另一表的行与构建表中的行匹配，则这些行将串流并传递到下一个操作。联接中占用内存最多的部分是这个构建表。

当任务并发度大于1时，对构建表进行分区。分区个数与`task.concurrency`配置参数（参见`task-properties`）的值一致。

在对构建表进行分区时，溢出到磁盘机制可以减少联接操作所需的峰值内存使用。当查询接近内存限制时，构建表的分区的一个子集会溢出到磁盘，另一表的行也会溢出到这些分区。溢出的分区数量会影响所需的磁盘空间量。然后，逐个回读溢出分区以完成联接操作。

通过这种机制，联接操作符使用的峰值内存可以降低到最大构建表分区的大小。假设没有数据倾斜，这个值将是整个构建表大小的`1 / task.concurrency`倍。

### 聚合

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

### 排序

如果尝试对大量数据进行排序，可能需要大量内存。当启用为排序溢出到磁盘时，如果内存不足，则中间排序结果将写入磁盘。结果被重新加载回来，并以较低的内存占用量合并。

### 窗口函数

窗口函数对行窗口执行运算符，并为每个行返回一个值。如果此行窗口很大，可能需要大量内存。当启用为窗口函数溢出到磁盘时，如果内存不足，则中间排序结果将写入磁盘。当内存可用时，结果被加载回来并合并。目前有一个限制，即溢出不会在所有情况下生效，例如当单个窗口非常大时。