腾讯万亿级 Elasticsearch 内存效率提升技术解密

标签: dev | 发表时间:2020-06-16 00:00 | 作者:
出处:http://itindex.net/relian


作者:morningchen,腾讯 TEG 后台开发工程师

Elasticsearch( ES )是一款功能强大的开源分布式实时搜索引擎,在日志分析(主要应用场景)、企业级搜索、时序分析等领域有广泛应用,几乎是各大公司搜索分析引擎的开源首选方案。

Tencent ES 是内核级深度优化的 ES 分支,持续地进行高可用、高性能、低成本等全方位优化,已支撑的单集群规模达到千级节点、万亿级吞吐。Tencent ES 已在公司内部开源,同时也积极贡献开源社区,截止目前已向社区提交 PR 25+。腾讯联合 Elastic 官方在腾讯云上提供了内核增强版 ES 云服务,支撑公司内部云、外部云、专有云达 60PB+ 的数据存储,服务 蘑菇街、知乎、B 站、凤凰网等业内头部客户。

本文主要介绍 Tencent ES 的主要优化点之一:零拷贝 内存 Off Heap,提升内存使用效率,降低存储成本。最终达到,在读写性能与源生逻辑一致的前提下,堆内存使用率降低 80%,单节点存储量从 5TB 提升至 50TB 的效果。

问题:日志分析场景数据量大,ES 内存瓶颈导致存储成本较高

上节提到,日志分析是 ES 的主要应用场景(占比 60%),而日志数据的特点显著:

  • 数据量大,成本是主要诉求:我们大批线上大客户,数据量在几百 TB 甚至 PB 级,单集群占用几百台机器。如此规模的数据量,带来了较高的成本,甚至有些客户吐槽,日志的存储成本已超越产品自身的成本。
  • 数据访问冷热特性明显:如下图所示,日志访问近多远少,历史极少访问却占用大量成本。

分析:成本瓶颈在哪里:堆内存使用率过高

我们对线上售卖的集群做硬件成本分析后,发现成本主要在磁盘和内存。为了降低磁盘成本,我们采取冷热分离、Rollup、备份归档、数据裁剪等多种方式降成本。在冷热分离的集群,我们通过大容量的冷存储机型,来存储历史数据,使得磁盘成本下降 60% 左右。

问题也随之而来:如上图所示,大容量的冷机型,存在磁盘使用率过低的问题( 40 % 以下),原因是堆内存使用率过高了( 70 % 左右),制约磁盘使用率无法提升。(其中单节点磁盘使用率 40%,约 13TB 左右,这已经是 Tencent ES 优化后的效果,源生只能支持到 5 TB 左右)所以,为了提升低成本的冷机型磁盘使用率,同时也为了降低内存成本,我们需要降低 ES 的堆内存使用率。

堆内存使用率为什么会高?

ES 是通过 JAVA 语言编写的,在介绍如何降低堆内存使用率之前,先了解下 JAVA 的堆内存:

  • 堆内存就是由 JVM (JAVA 虚拟机)管理的内存。建立在堆内存中的对象有生命周期管理机制,由垃圾回收机时自动回收过期对象占用的内存。
  • 堆外内存是由用户程序管理的内存,堆外内存中的对象过期时,需要由用户代码显示释放。
1. 运营侧调整装箱策略能否解决问题?

了解了 JAVA 堆内存后,我们看,能否通过调整运营策略来提升堆内存容量呢?

  • 堆内存分配大一点行不行?超过 32GB,指针压缩失效,内存浪费, 50GB 堆内存性能与 31GB 接近且垃圾回收压力大,也影响性能;
  • 多节点部署提高堆内存总量是否可行?多节点部署,占用机器量更大,用户成本上升大客户节点数过多( 几百个 ),集群元数据管理瓶颈,可用性下降 反向推动云上用户拆分集群,阻力很大。

所以,简单的运营侧策略调整无法解决堆内存使用率过高的问题。那么我们就需要确认 ES 的堆内存是被什么数据占用了,能否优化。

2. 堆内存被什么数据占用了?

我们对线上集群的堆内存分布情况做统计分析后,发现绝大部分堆内存主要被 FST( Finite State Transducer )占用了:

  • FST 内存占用量占分堆内存总量的 50% ~ 70%
  • FST 与 磁盘数据量成正比:10TB 磁盘数据量,其对应的 FST 内存占用量在 10GB ~ 15GB 左右。

因此,我们的目标就是就是通过内核层的优化,降低 FST 的堆内存占用量。

方案:降低 FST 堆内存占用量

什么是 FST ?

在介绍具体的方案前,先来了解下 FST 到底是什么。

如上图所示,ES 底层存储采用 Lucene(搜索引擎),写入时会根据原始数据的内容,分词,然后生成倒排索引。查询时,先通过查询倒排索引找到数据地址(DocID)),再读取原始数据(行存数据、列存数据)。但由于 Lucene 会为原始数据中的每个词都生成倒排索引,数据量较大。所以倒排索引对应的倒排表被存放在磁盘上。这样如果每次查询都直接读取磁盘上的倒排表,再查询目标关键词,会有很多次磁盘 IO,严重影响查询性能。为了解磁盘 IO 问题,Lucene 引入排索引的二级索引 FST [Finite State Transducer] 。原理上可以理解为前缀树,加速查询。

其原理如下:

  1. 将原本的分词表,拆分成多个 Block ,每个 Block 会包含 25 ~ 48 个词(Term)。图中做了简单示意,Allen 和 After 组成一个 Block 。

  2. 将每个 Block 中所有词的公共前缀抽取出来,如 Allen 和 After 的公共前缀是 A 。

  3. 将各个 Block 的公共前缀,按照类似前缀树的逻辑组合成 FST,其叶子节点携带对应 Block 的首地址 。(实际 FST 结构更为复杂,前缀后缀都有压缩,来降低内存占用量)

  4. 为了加速查询,FST 永驻堆内内存,无法被 GC 回收。

  5. 用户查询时,先通过关键词(Term)查询内存中的 FST ,找到该 Term 对应的 Block 首地址。再读磁盘上的分词表,将该 Block 加载到内存,遍历该 Block ,查找到目标 Term 对应的 DocID。再按照一定的排序规则,生成 DocID 的优先级队列,再按该队列的顺序读取磁盘中的原始数据(行存或列存)。

由此可知, FST 常驻堆内内存,无法被 GC 回收 , 长期占用 50% ~ 70% 的堆内存 !

解决方案

既然 FST 是常驻堆内内存,导致堆内存使用率过高,那么解决问题的思路有两种:

  1. 降低 FST 在堆内的内存使用量
  2. 将 FST 从堆内存(OnHeap,有 32GB 容量限制)移到堆外内存(OffHeap)。因为堆外内存无容量上限,可通过扩充机器内存来提升容量。(堆外内存容量限制近似为 物理内存 - JAVA 堆内存)自然也就有了相应方案:
解决方案一:降低 FST 在堆内的内存使用量

在 Tencent ES 成立前期,我们采用过这种方案。具体的做法是,将 FST 对应的 Block 大小,从 25 ~ 48,放大一倍至 49 ~ 96 。这样,在 关键词 Term 数相同的情况下,Block 数量降低了一倍,对应的 FST 内存理论上也会下降一倍。

  • 优点:我们实测发现,这种方案下,FST 的堆内存占用量下降了 40% 左右。
  • 缺点:(1)由于 Block 内的 Term 数变多了,那么每次遍历 Block 查找目标 Term 时,需要从磁盘读取的数据量更大了,因此也带来了明显的查询性能损耗,约 20% 。(2)该方案只是让 FST 占用的内存下降了一半,仍无法控制 FST 占用的内存总量。不同场景下,FST 数据量大小差异也很大,在全文检索的字段较多时,仍然存在 FST 内存过高的问题。

由此我们可以看出,简单的降低 FST 的堆内存使用量,并不是一个普适性的方案,需要更为通用、彻底限制住 FST 总大小的方案。

解决方案二:将 FST 从堆内存(OnHeap)移到堆外内存(OffHeap)

将 FST 从堆内存(OnHeap)移到堆外内存(OffHeap),几乎可以完全释放 FST 在堆内存占据的使用空间,这也是 JAVA 实践方向上一个普遍使用的方案。对于 JAVA 的堆内存不足,将部分内存移到堆外内存(OffHeap)的问题,ES 社区 和 其他 JAVA 系产品都有相应的解决方案。

  1. ES 社区方案:该方案是将 FST 从堆内存中剔除, 直接交由 MMAP 管理。FST 在磁盘上也是有对应的持久化文件的,Lucene 的 .tip 文件,该方案每次查询时直接通过 MMap 读取 .tip 文件,通过文件系统缓存 FST 数据。
  • 优点:这种方实现简单,代码改动量小
  • 缺点:(1)我们早期也试用过这种方式实现,但是由于 MMAP 属于 page cache 可能被系统回收掉。而且 ES 的大查询也会使用大量的系统缓存导致 FST 占用的内存被冲掉,瞬间产生较多的读盘操作,从而带来性能的 N 倍损耗,容易产生查询毛刺。特别是在 SATA 盘上,严重时查询时延有 10 倍的毛刺。(2)Lucene 8.x 、ES 7.x 后才支持该功能,存量的 6.x 用户无法使用。
  1. HBase 方案 HBase 的方案是,在堆外搭建一个 Cache,将其一部分堆内存(Bucket Cache,Data Block 缓存)移到堆外,释放堆内内存。
  • 优点:数据缓存放在堆外,释放大量堆内内存
  • 缺点:(1)淘汰策略完全依赖 LRU 策略,(2)只是把数据缓存放置在堆外,索引的缓存还在堆内
  1. Tencent ES 方案 我们的方案总体上接近 HBase 的方案,相比之下:
  • 优点:(1)相比于 ES 社区方案,我们堆外的 Cache 保证 FST 的内存空间不受系统影响。(2)相比于 HBase 方案,我们实现了更精准的数据淘汰策略,提高了内存使用率。也通过多级 Cache 解决性能问题,所以我们敢于把索引放置在堆外。

实现:全链路 0 拷贝 FST OffHeap Cache

下面通过将由浅入深地向大家介绍我们实现 FST OffHeap 的过程,及其中碰见的问题和解决方案。

总体架构

在实现 OffHeap 方案的初期,我们的架构如上图所示。先来看下源生逻辑是怎样访问 FST 的:

  • 数据写入:ES 的一次 Refresh / Merge 动作,会生成一个新的 Lucene Segment,相应的在磁盘上生成该 Segment 对应的各种数据文件。其中 .tip 文件里面存储的就是该 Segment 各个字段的 FST 信息。在生成 .tip 文件后,Lucene 也会将每个字段( Field )的 FST 数据解析后,拷贝至该 Field 在 OnHeap 内存中的对象里,作为一个成员变量永驻内存,直到该 Segment 被删除 ( Index 被删除、Segment Merge 时 )。
  • 数据查询:查询时,直接访问 OnHeap 的 FST 。

再来看下优化后的 Tencent ES 是怎样访问 FST 的:

  • 数据写入:在 OffHeap 内存放置一个 LRU Cache,在生成新的 Segment 时,不再将 .tip 中的 FST 数据拷贝至 OffHeap LRU Cache。将其对应的 Key 放置在 OnHeap 的 Field 中,不再将 FST 拷贝至 OnHeap 的 Field 中。这样就把 FST 从 OnHeap 移到了 OffHeap。
  • 数据查询:查询时,通过 OnHeap Field 中保存的 Key,去 OffHeap LRU Cache 中查询 FST 数据,将其拷贝至 OnHeap 后,做查询操作。若 Cache Miss ,则从磁盘的 .tip 文件中的相应位置读取该 Field 对应的 FST 做查询,同时将其放置到 OffHeap LRU Cache 中。

将两种方案做个对比,如下表所示:

那么可以总结出 Tencent ES 优化后的 FST 访问逻辑的优势和劣势:

  • 优势:在 OnHeap 我们用 100B 左右的 Key 置换 MB 级别的 FST,大大降低的内存占用量,使得单节点最大支持的磁盘数据量有了 5 倍以上的提升。
  • 劣势:FST 在每次查询时都要从 OffHeap LRU Cache 拷贝至 OnHeap,相比于源生逻辑直接访问 OnHeap 的 FST ,读写都多了拷贝的动作,造成高并发读写时有 20%+ 的性能损耗。

所以,我们要对 OffHeap LRU Cache 的读写路径做优化,减少 Copy 次数,提升读写性能。具体的实现方案是全链路零拷贝 OffHeap FST 访问逻辑。

全链路零拷贝 OffHeap FST 访问逻辑

ES 源生逻辑访问 FST 只支持堆内的操作,怎样做到让它能直接访问堆外的数据呢?

为此,我们做了两方面优化:

  • OffHeap LRU Cache:改造读数据逻辑,Cache 只返回数据地址,封装为一个 Buffer,堆内只存数据地址,这样就把 FST 的访问从先拷贝至 OnHeap 再访问优化为直读 OffHeap 内存。
  • ES:重构 FST 读写逻辑,实现 FST 访问直读 OffHeap 内存:
  • FST 抽象为一个 FST Buffer,对外提供 FST 形式的各种访问接口。内部实现按 FST 的数据结构读取 OffHeap Buffer 的逻辑,作为访问 OffHeap FST 的代理。
  • 将 ES 访问 FST 的所有链路全部改造为 FST Buffer 接口的形式,优化 FST 的读写路径如下所示:

经过上述优化,把 FST 的数据访问由 1 次 Copy 优化为 0 Copy,实现了全链路零拷贝 OffHeap FST 访问逻辑。同时也将 FST 的数据写入从 2 次 Copy 优化为 1 次 Copy。读写性能损耗从 20%+ 下降至 7%。

虽然这样性能影响已经比较小了,但我们还是想挑战下自己,能否将性能优化到极致呢?

多级 Cache 将性能优化到极致

要进一步优化性能,需要搞清楚一个问题:7% 的性能损耗在哪里?Perf 分析后发现,Hot 堆栈是 OffHeap Cache 计算 Hash、校验 Key 等逻辑。为什么会有频繁读 Cache 的操作呢?我们分析 Lucene 的源码发现,在高并发读写时,一次读写入上千条数据,则会有读 Cache 上千次。例如,一个 bulk update 写入 3000 条数据,3 分片,每个分片大概有 1000 条数据 update 操作,那么就有 1000 次读 Cache 的逻辑。而这 1000 次读 Cache,基本上是读的同一个 Key (_id 对应的 Key),能否做到让这 1000 次查询的 Key,稍微缓存一会,防止那么多次读 Cache 的操作呢?

我们的优化方案是:OnHeap + OffHeap 的两级 Cache 架构,降低 OffHeap Cache 访问频率。而堆内的 Cache 一定要轻量,最少的占用 OnHeap 内存,否则就违背了我们要将 FST 从 OnHeap 移出去的初衷。所以,我们最终选用堆内的弱引用机制( WeakRefrence )来缓存 OffHeap FST 的指针,作为 OnHeap 的轻量级 Cache,利用 JVM 的 GC 自动释放无效的弱引用,同时堆外内存。

相比于直接设置一个 OnHeap Cache,弱引用有占用内存小,避免拷贝等优势。

这样我们访问 FST 的逻辑,会先查询堆内的各个查询共享的 WeakRefrence,当其已经被释放时,才会访问 OffHeap Cache。这样就大大降低了 OffHeap Cache 的访问频率。

这里的挑战是,两级 Cache,key 的关联释放问题。JVM GC 时会销毁 WeakRefrence 对应的 OnHeap 对象,但 Java 无析构函数,无法自动释放堆外指针。而我们期望,在堆内的 WeakRefrence 释放时,同时释放堆外指针,从而对 OffHeap Cache 的 Key 的引用计数减一,使其可以根据 LRU 规则自动回收无效的 FST 数据。

深入分析 JAVA 垃圾回收、弱引用机制后,发现可以通过注册一个 WeakRefrence Queue,在 WeakRefrence 释放前,可以捕获到它。进而改造 WeakRefrence 的数据结构,使其在被捕获后,对 OffHeap Cahe 的 Key 引用计数减一,然后才被回收。

经过上述 OnHeap WeakRefrence + OffHeap LRU Cache 两级 Cache 架构的优化后,高并发读写性能基本与源生逻辑持平。

其他优化点

除了上述的性能优化外,Tencent ES 的 FST OffHeap 还做了一些其他优化:

  • 精准控制 Cache 淘汰策略,内存高效使用:
  • LSM Tree 底层文件合并过程,及时淘汰无效数据
  • 字段粒度 Cache 控制:有去重需求:主键(_id )写入 Cache,提升写入性能 无去重需求:主键(_id )不写入 Cache,降低内存成本【20%-40%】
  • CAS 并发控制:解决 Cache Miss 后,并发读文件的 IO 放大问题
  • 不停服动态调整 Cache 大小:用户可根据业务情况,在不停服的情况下随时调整 Cache 大小
  • 不停服动态开关 OffHeap Cache :
  • 用户按需开关 OffHeap Cache 功能
  • 存量集群上线部署兼容性

优化效果

最后来看下我们层层优化后,最终的效果:

压测效果

通过 ES 官方 Bench 工具 ES Rally 压测,结果如下:

线上效果

根据线上集群的实际运行效果看,当开启 OffHeap 功能后,集群整体平均 JVM 的内存使用率从 70%+ 下降 至 30% 左右。

Tencent ES 将持续地进行高可用、高性能、低成本等全方位优化:可用性方面,将提升 ES 的故障自愈能力、故障自动分析诊断,达到零接触运维的目标;高性能方面,将进一步提升 ES 的海量数据实时分析能力;低成本方面,将提供存储与计算分离的能力,基于腾讯自研的共享文件系统 CFS,进一步缩减成本。

最后,欢迎各位对 ES 内核技术有兴趣的同学扫描下方的二维码与我们展开交流,同时也欢迎大家在腾讯云体验 CES 云服务。


相关 [腾讯 万亿 elasticsearch] 推荐:

腾讯万亿级 Elasticsearch 内存效率提升技术解密

- - IT瘾-dev
作者:morningchen,腾讯 TEG 后台开发工程师. Elasticsearch( ES )是一款功能强大的开源分布式实时搜索引擎,在日志分析(主要应用场景)、企业级搜索、时序分析等领域有广泛应用,几乎是各大公司搜索分析引擎的开源首选方案. Tencent ES 是内核级深度优化的 ES 分支,持续地进行高可用、高性能、低成本等全方位优化,已支撑的单集群规模达到千级节点、万亿级吞吐.

腾讯云Elasticsearch集群多可用区容灾实现原理及最佳实践

- - InfoQ推荐
目前腾讯云 ES 集群可以支持双可用区及三可用区的集群部署,且支持单可用区平滑升级到多可用区集群. 当一个可用区出现故障时,剩余可用区依然能够保障集群的稳定性、服务的可用性和数据的完整性. 当客户选择了跨多可用区的集群架构部署时,集群的数据节点必须是多可用区的倍数,如客户选择的是三可用区部署,则数据节点个数应为 3,6,9,12 等,以此类推.

[译]elasticsearch mapping

- - an74520的专栏
es的mapping设置很关键,mapping设置不到位可能导致索引重建. 请看下面各个类型介绍^_^. 每一个JSON字段可以被映射到一个特定的核心类型. JSON本身已经为我们提供了一些输入,支持 string,  integer/ long,  float/ double,  boolean, and  null..

Elasticsearch as Database - taowen - SegmentFault

- -
【北京上地】滴滴出行基础平台部招聘 Elasticsearch 与 Mysql binlog databus 开发工程师. 内推简历投递给: [email protected]. 推销Elasticsearch. 时间序列数据库的秘密(1)—— 介绍. 时间序列数据库的秘密(2)——索引.

ElasticSearch 2 的节点调优(ElasticSearch性能)

- - 行业应用 - ITeye博客
一个ElasticSearch集群需要多少个节点很难用一种明确的方式回答,但是,我们可以将问题细化成一下几个,以便帮助我们更好的了解,如何去设计ElasticSearch节点的数目:. 打算建立多少索引,支持多少应用. elasticsearch版本: elasticsearch-2.x. 需要回答的问题远不止以上这些,但是第五个问题往往是容易被我们忽视的,因为单个ElasticSearch集群有能力支持多索引,也就能支持多个不同应用的使用.

Elasticsearch:使用 Elasticsearch 进行语义搜索

- - 掘金 后端
在数字时代,搜索引擎在通过浏览互联网上的大量可用信息来检索数据方面发挥着重要作用. 此方法涉及用户在搜索栏中输入特定术语或短语,期望搜索引擎返回与这些确切关键字匹配的结果. 虽然关键字搜索对于简化信息检索非常有价值,但它也有其局限性. 主要缺点之一在于它对词汇匹配的依赖. 关键字搜索将查询中的每个单词视为独立的实体,通常会导致结果可能与用户的意图不完全一致.

elasticsearch的javaAPI之query

- - CSDN博客云计算推荐文章
elasticsearch的javaAPI之query API. the Search API允许执行一个搜索查询,返回一个与查询匹配的结果(hits). 它可以在跨一个或多个index上执行, 或者一个或多个types. 查询可以使用提供的 query Java API 或filter Java API.

Elasticsearch基础教程

- - 开源软件 - ITeye博客
转自:http://blog.csdn.net/cnweike/article/details/33736429.     Elasticsearch有几个核心概念. 从一开始理解这些概念会对整个学习过程有莫大的帮助.     接近实时(NRT).         Elasticsearch是一个接近实时的搜索平台.

ElasticSearch索引优化

- - 行业应用 - ITeye博客
ES索引的过程到相对Lucene的索引过程多了分布式数据的扩展,而这ES主要是用tranlog进行各节点之间的数据平衡. 所以从上我可以通过索引的settings进行第一优化:. 这两个参数第一是到tranlog数据达到多少条进行平衡,默认为5000,而这个过程相对而言是比较浪费时间和资源的. 所以我们可以将这个值调大一些还是设为-1关闭,进而手动进行tranlog平衡.

elasticsearch集群搭建

- - zzm
之前对于CDN的日志处理模型是从 . logstash agent==>>redis==>>logstash index==>>elasticsearch==>>kibana3,对于elasticsearch集群搭建,可以把索引进行分片存储,一个索引可以分成若干个片,分别存储到集群里面,而对于集群里面的负载均衡,副本分配,索引动态均衡(根据节点的增加或者减少)都是elasticsearch自己内部完成的,一有情况就会重新进行分配.