转自: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:
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,再读取,相当于边读取边解析。
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 的初始化就是简单地将文件读进数组:
public void init(DataInput in, long numBytes) {bytesArray = new byte[(int) numBytes];in.readBytes(bytesArray, 0, bytesArray.length);
}
因此,在 Off-Heap 的情况下,mmap 像数组一样读取就可以了。
如何查看文件的缓存情况
如果想要查看文件被 page cache 缓存的百分比,可以使用以下工具:
vmtouch(推荐)pcstathcachefincore
要确认某个 .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 会检查文件的校验和,把整个文件读取一遍:
// BlockTreeTermsReader constructor
CodecUtil.checksumEntireFile(indexIn);
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 不被回收呢?所以总体来说,可能会让查询延迟增加不确定性,且不便重现和诊断。不过也不用太担心,这种情况一般很少发生。
参考资料
- Elasticsearch Issue #38390
- Elasticsearch Pull Request #42838
- Elasticsearch Pull Request #43150
- Elasticsearch Pull Request #52518
- Elasticsearch 7.3.0 Release Notes
- Linux MM Page Replacement Design
转自:https://www.easyice.cn/archives/346