【Elasticsearch】Elasticsearch中FST的Off-Heap优化详解

转自: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 回收有两种情况:

  1. 系统内存不足时的自动回收 :当系统可用内存不足时,系统会自动回收 page cache 缓存的数据,其中可能包括 mmap 映射的 .tip 文件。
  2. 手工回收 :通过改写 /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 不被回收呢?所以总体来说,可能会让查询延迟增加不确定性,且不便重现和诊断。不过也不用太担心,这种情况一般很少发生。

参考资料

转自:https://www.easyice.cn/archives/346

相关推荐
比奥利奥还傲.8 天前
哪吒监控面板实战:部署 Dashboard、接入 Agent、钉钉告警,再配置固定公网访问
网络·人工智能·分布式·1024程序员节
默 语9 天前
Java新手入门:从零开始安装JDK并配置环境变量
java·开发语言·python·mysql·group by·1024程序员节·数据去重
学传打活25 天前
【边打字.边学昆仑正义文化】_进阶篇5 宇宙星系的运转(续3)
微信公众平台·1024程序员节·汉字·昆仑正义文化
学传打活1 个月前
【边打字.边学昆仑正义文化】_进阶篇5 宇宙星系的运转(续1)
微信公众平台·1024程序员节·汉字·昆仑正义文化
fangzhanpeng1681 个月前
快速掌握C#语言基础知识点(1.基础数据类型)
开发语言·c#·1024程序员节
无己心1 个月前
面向多平台 AI 搜索引擎的适配层架构设计
前端·人工智能·搜索引擎·ai·1024程序员节
u0103055271 个月前
AI驱动的安卓UI自动生成功能实现
人工智能·1024程序员节
u0103055272 个月前
长株潭智能体赋能腾讯元器链接共享
1024程序员节
云贝贝贝2 个月前
OceanBase认证体系介绍与备考指南
运维·服务器·oceanbase·1024程序员节
u0103055272 个月前
Java图像处理实战指南
人工智能·1024程序员节