转自:https://www.easyice.cn/archives/346
前言
一直以来,Elasticsearch(ES)堆内存中占据比重最大的是 FST,即 .tip
(terms index)文件。这些文件占据的空间很大,1TB 的索引大约需要 2GB 或更多的内存。为了确保节点稳定运行,业界通常认为一个节点打开的索引不应超过 5TB。
从 ES 7.3 版本开始,.tip
文件被修改为通过 mmap
的方式加载,这使得 FST 占据的内存从堆内转移到了堆外,由操作系统的 page cache 管理。
参考 ES 7.3 的 release notes:
Also mmap terms index (.tip) files for hybridfs #43150 (issue: #42838)
现在,我们来深入探讨其中的一些细节。
hybridfs 的工作原理
hybridfs
是索引默认的 store 类型,它根据操作系统类型自动选择 nio
或者 mmap
。那么究竟哪些文件被 mmap
方式打开呢?手册中提到:
Currently only the Lucene term dictionary, norms and doc values files are memory mapped. All other files are opened using Lucene NIOFSDirectory.
对应到文件扩展名,就是 .nvd
(norms)、.dvd
(doc values)、.tim
(term dictionary)、.tip
(term index)、.cfs
(compound)类型的文件使用 mmap
方式加载,其余使用 nio
:
java
switch(extension) {
case "nvd":
case "dvd":
case "tim":
case "tip":
case "cfs":
return true;
default:
return false;
}
为什么使用 mmap 实现 Off-Heap
你可能会问,为什么把 .tip
文件通过 mmap
方式读取就实现了 Off-Heap?像 HBase 实现 Off-Heap 需要将数据转移到堆外的数据结构,为什么 ES 不需要?
FST 的查找过程
在堆内存(On-Heap)的情况下,Lucene 将 .tip
文件的数据读进一个数组。在 FST 查找时,seek
到某个位置,读取一些字节,然后再次 seek
,再读取,相当于边读取边解析。
java
private Arc<T> findTargetArc(BytesReader in, ...) {
// ...
in.setPosition(follow.target);
arc.numArcs = in.readVInt();
arc.bytesPerArc = in.readVInt();
arc.posArcsStart = in.getPosition();
arc.nextArc = arc.posArcsStart;
// ...
}
在 On-Heap 的情况下,这个 BytesReader
的初始化就是简单地将文件读进数组:
java
public void init(DataInput in, long numBytes) {
bytesArray = new byte[(int) numBytes];
in.readBytes(bytesArray, 0, bytesArray.length);
}
因此,在 Off-Heap 的情况下,mmap
像数组一样读取就可以了。
如何查看文件的缓存情况
如果想要查看文件被 page cache 缓存的百分比,可以使用以下工具:
vmtouch
(推荐)pcstat
hcache
fincore
要确认某个 .tip
文件是否被 mmap
方式读取的,可以使用 pmap
命令,被 mmap
映射的文件会在这里列出来。
Off-Heap 后的效果
使用 geonames
数据集写入索引 1TB,使用 _cat/segments
API 查看 segments.memory
内存占用量,对比 Off-Heap 后的内存占用效果:
store.type | segments.memory |
---|---|
niofs | 4.7GB |
hybridfs | 1.06GB |
JVM 内存占用量降低了约 78%。不同数据样本结果不同,其他的可能会降低更多。
通过 _cat/segments
观测到的 segments.memory
指标,会比实际占用的 JVM 内存少一些,不过相差不大,上述结果可作为参考。
Page Cache 的管理
由于 Off-Heap 后的堆外内存由操作系统的 page cache 管理,什么时候被驱逐出去由操作系统决定,进程无法控制。如果 .tip
文件的内容被驱逐出 page cache,对 FST 的查找会涉及到磁盘 IO,对查询延迟有比较大的影响。
Page Cache 的回收策略
Linux 系统的 page cache 回收有两种情况:
- 系统内存不足时的自动回收 :当系统可用内存不足时,系统会自动回收 page cache 缓存的数据,其中可能包括
mmap
映射的.tip
文件。 - 手工回收 :通过改写
/proc/sys/vm/drop_caches
或posix_fadvise
调用来手工回收。
当索引处于打开(open)状态时,由 mmap
映射到 page cache 的 .tip
数据并不会被回收;而如果索引处于关闭(close)状态,则会被完全回收。
Page Cache 的回收算法
在 Linux 2.6.34 的内核中,对 page cache 的回收策略使用双链策略,参考《Linux 内核设计与实现(第三版)》。算法描述大致如下:
- 引入两个链表:
active list
和inactive list
。 - 两个链表都是从尾部加入,头部移出。
- 页面换出操作只在
inactive list
执行。 - 对于文件缓存,当第一次访问的时候加入到
inactive list
,再次访问的时候把它提升到active list
。 - 当
active list
大小大于inactive list
,就将active list
头部的页面降级到inactive list
。
更多 page cache 的信息可以参考 Linux MM Page Replacement Design。
mmap 的原理
依据 mmap
的原理,文件描述符(fd)被映射为指针(或者说字节数组)供进程直接访问,仅在进程访问到相应位置的时候才去读取磁盘,是根据内容按需读取磁盘。
你可能会想,既然如此,_open
索引是不是变快了?原来 nio
需要把整个文件读进堆内存,现在 mmap
一下就结束了,那么等索引首次被查询的时候才会加载到 page cache?实际上,_open
索引并没变快,因为在 _open
索引的过程中,Lucene 会检查文件的校验和,把整个文件读取一遍:
java
// BlockTreeTermsReader constructor
CodecUtil.checksumEntireFile(indexIn);
java
ChecksumIndexInput in = new BufferedChecksumIndexInput(clone);
// 读取文件到目标位置,并更新校验和
in.seek(in.length() - footerLength());
return checkFooter(in);
关于 _id 字段的 Off-Heap 问题
Lucene 支持字段级的 Off-Heap 设置。ES 7.3 中将 .tip
Off-Heap 时并不包含 _id
字段,#52518 中提到,这是因为担心降低写入速度。不过在经历了一些测试之后发现影响并不大。
一般来说,只有在使用显式 IDs 时,索引速率才会受到影响,因为否则 Elasticsearch 几乎不会在索引过程中查找 terms dictionary。因此,强制将
_id
字段的 terms index 保持在堆内存中,对于那些具有仅追加工作负载的用户来说是相当浪费的。此外,我使用http_logs
数据集在索引时进行了基准测试,结果表明,即使在使用显式 IDs 的情况下,速度下降也足够小,可能不值得强制将 terms index 保持在堆内存中。
题外话 :这段内容提到,使用外部 doc id
方式入库时需要从 term dictionary 中查询,这是因为使用外部 ID 写入时,ES 需要判断该 ID 是否存在,以便执行更新(update)或追加(append)操作。因此在分片中对 _id
字段执行 Lucene 的 seekExact
查询来判断此 ID 是否存在,所以使用外部 ID 入库时写入速度会降低一些(约 20%)。这也是 _id
字段需要写入 FST 的一个原因。
在将 _id
字段 Off-Heap 之后,使用 http_logs
数据集和外部 ID 的方式执行写入测试,写入速度降低了 1.8%,JVM 内存降低了 100 倍。
因此,在 ES 7.7 版本中,会将 _id
字段也放到堆外。
结语
把 FST 放到堆外可以让节点能够持有更多的数据,这对 ES 集群能处理的数据规模有重大提升,意义重大。但是 .tip
文件需要加载到内存的意义比 .tim
等文件要重要得多,page cache 总会有需要回收的时候,谁能保证 .tip
不被回收呢?所以总体来说,可能会让查询延迟增加不确定性,且不便重现和诊断。不过也不用太担心,这种情况一般很少发生。