ElasticSearch 亿级数据检索深度优化!

ElasticSearch 亿级数据检索深度优化!

一、前言

数据平台已迭代三个版本,从头开始遇到很多常见的难题,终于有片段时间整理一些已完善的文档,在此分享以供所需朋友的实现参考,少走些弯路,在此篇幅中偏重于 ES 的优化,关于 HBase,Hadoop 的设计优化估计有很多文章可以参考,不再赘述。

二、需求说明

项目背景:

在一业务系统中,部分表每天的数据量过亿,已按天分表,但业务上受限于按天查询,并且 DB 中只能保留 3 个月的数据(硬件高配),分库代价较高。

改进版本目标:
  1. 数据能跨月查询,并且支持 1 年以上的历史数据查询与导出。

  2. 按条件的数据查询秒级返回。

三、Elasticsearch 检索原理

3.1 关于 ES 和 Lucene 基础结构

谈到优化必须能了解组件的基本原理,才容易找到瓶颈所在,以免走多种弯路,先从 ES 的基础结构说起(如下图):

ElasticSearch 亿级数据检索深度优化!

一些基本概念:

  • Cluster: 包含多个 Node 的集群

  • Node: 集群服务单元

  • Index: 一个 ES 索引包含一个或多个物理分片,它只是这些分片的逻辑命名空间

  • Type: 一个 index 的不同分类,6.x 后只能配置一个 type,以后将移除

  • Document: 最基础的可被索引的数据单元,如一个 JSON 串

  • Shards : 一个分片是一个底层的工作单元,它仅保存全部数据中的一部分,它是一个 Lucence 实例 (一个 Lucene: 索引最大包含 2,147,483,519 (= Integer.MAX_VALUE – 128)个文档数量)

  • Replicas: 分片备份,用于保障数据安全与分担检索压力

    ES 依赖一个重要的组件 Lucene,关于数据结构的优化通常来说是对 Lucene 的优化,它是集群的一个存储于检索工作单元,结构如下图:

ElasticSearch 亿级数据检索深度优化!

在 Lucene 中,分为索引(录入)与检索(查询)两部分,索引部分包含分词器、过滤器、字符映射器等,检索部分包含查询解析器等。

一个 Lucene 索引包含多个 segments,一个 segment 包含多个文档,每个文档包含多个字段,每个字段经过分词后形成一个或多个 term。

通过 Luke 工具查看 ES 的 lucene 文件如下,主要增加了 _id 和 _source 字段:

ElasticSearch 亿级数据检索深度优化!

3.2 Lucene 索引实现

Lucene 索引文件结构主要的分为:词典、倒排表、正向文件、DocValues等,如下图:

ElasticSearch 亿级数据检索深度优化!
ElasticSearch 亿级数据检索深度优化!

Lucene 随机三次磁盘读取比较耗时。其中.fdt 文件保存数据值损耗空间大,.tim 和.doc 则需要 SSD 存储提高随机读写性能。另外一个比较消耗性能的是打分流程,不需要则可屏蔽。

关于 DocValues

倒排索引解决从词快速检索到相应文档 ID, 但如果需要对结果进行排序、分组、聚合等操作的时候则需要根据文档 ID 快速找到对应的值。

通过倒排索引代价缺很高:需迭代索引里的每个词项并收集文档的列里面 token。这很慢而且难以扩展:随着词项和文档的数量增加,执行时间也会增加。Solr docs 对此的解释如下:

在 lucene 4.0 版本前通过 FieldCache,原理是通过按列逆转倒排表将(field value ->doc)映射变成(doc -> field value)映射,问题为逐步构建时间长并且消耗大量内存,容易造成 OOM。

DocValues 是一种列存储结构,能快速通过文档 ID 找到相关需要排序的字段。在 ES 中,默认开启所有(除了标记需 analyzed 的字符串字段)字段的 doc values,如果不需要对此字段做任何排序等工作,则可关闭以减少资源消耗。

3.3 关于 ES 索引与检索分片

ES 中一个索引由一个或多个 lucene 索引构成,一个 lucene 索引由一个或多个 segment 构成,其中 segment 是最小的检索域。

数据具体被存储到哪个分片上:shard = hash(routing) % number_of_primary_shards

默认情况下 routing 参数是文档 ID (murmurhash3),可通过 URL 中的 _routing 参数指定数据分布在同一个分片中,index 和 search 的时候都需要一致才能找到数据,如果能明确根据 _routing 进行数据分区,则可减少分片的检索工作,以提高性能。

四、优化案例

在我们的案例中,查询字段都是固定的,不提供全文检索功能,这也是几十亿数据能秒级返回的一个大前提:

  1. ES 仅提供字段的检索,仅存储 HBase 的 Rowkey 不存储实际数据。

  2. 实际数据存储在 HBase 中,通过 Rowkey 查询,如下图。

  3. 提高索引与检索的性能建议,可参考官方文档(如 https://www.elastic.co/guide/en/elasticsearch/reference/current/tune-for-indexing-speed.html)。

一些细节优化项官方与其他的一些文章都有描述,在此文章中仅提出一些本案例的重点优化项。

ElasticSearch 亿级数据检索深度优化!

4.1 优化索引性能

  1. 批量写入,看每条数据量的大小,一般都是几百到几千。

  2. 多线程写入,写入线程数一般和机器数相当,可以配多种情况,在测试环境通过 Kibana 观察性能曲线。

  3. 增加 segments 的刷新时间,通过上面的原理知道,segment 作为一个最小的检索单元,比如 segment 有 50 个,目的需要查 10 条数据,但需要从 50 个 segment 分别查询 10 条,共 500 条记录,再进行排序或者分数比较后,截取最前面的 10 条,丢弃 490 条。在我们的案例中将此 “refresh_interval”: “-1” ,程序批量写入完成后进行手工刷新(调用相应的 API 即可)。

  4. 内存分配方面,很多文章已经提到,给系统 50%的内存给 Lucene 做文件缓存,它任务很繁重,所以 ES 节点的内存需要比较多(比如每个节点能配置 64G 以上最好)。

  5. 磁盘方面配置 SSD,机械盘做阵列 RAID5 RAID10 虽然看上去很快,但是随机 IO 还是 SSD 好。

  6. 使用自动生成的 ID,在我们的案例中使用自定义的 KEY,也就是与 HBase 的 ROW KEY,是为了能根据 rowkey 删除和更新数据,性能下降不是很明显。

  7. 关于段合并,合并在后台定期执行,比较大的 segment 需要很长时间才能完成,为了减少对其他操作的影响(如检索),elasticsearch 进行阈值限制,默认是 20MB/s,可配置的参数:”indices.store.throttle.max_bytes_per_sec” : “200mb” (根据磁盘性能调整)合并线程数默认是:Math.max(1, Math.min(4, Runtime.getRuntime().availableProcessors() / 2)),如果是机械磁盘,可以考虑设置为 1:index.merge.scheduler.max_thread_count: 1,在我们的案例中使用 SSD,配置了 6 个合并线程。

4.2 优化检索性能

  1. 关闭不需要字段的 doc values。

  2. 尽量使用 keyword 替代一些 long 或者 int 之类,term 查询总比 range 查询好 (参考 lucene 说明 http://lucene.apache.org/core/7_4_0/core/org/apache/lucene/index/PointValues.html)。

  3. 关闭不需要查询字段的 _source 功能,不将此存储仅 ES 中,以节省磁盘空间。

  4. 评分消耗资源,如果不需要可使用 filter 过滤来达到关闭评分功能,score 则为 0,如果使用 constantScoreQuery 则 score 为 1。

  5. 关于分页:

  • from + size: 每分片检索结果数最大为 from + size,假设 from = 20, size = 20,则每个分片需要获取 20 * 20 = 400 条数据,多个分片的结果在协调节点合并(假设请求的分配数为 5,则结果数最大为 400*5 = 2000 条) 再在内存中排序后然后 20 条给用户。这种机制导致越往后分页获取的代价越高,达到 50000 条将面临沉重的代价,默认 from + size 默认如下:index.max_result_window :10000

  • search_after: 使用前一个分页记录的最后一条来检索下一个分页记录,在我们的案例中,首先使用 from+size,检索出结果后再使用 search_after,在页面上我们限制了用户只能跳 5 页,不能跳到最后一页。

  • scroll 用于大结果集查询,缺陷是需要维护 scroll_id

  1. 关于排序:我们增加一个 long 字段,它用于存储时间和 ID 的组合(通过移位即可),正排与倒排性能相差不明显。

  2. 关于 CPU 消耗,检索时如果需要做排序则需要字段对比,消耗 CPU 比较大,如果有可能尽量分配 16cores 以上的 CPU,具体看业务压力。

  3. 关于合并被标记删除的记录,我们设置为 0 表示在合并的时候一定删除被标记的记录,默认应该是大于 10%才删除:“merge.policy.expunge_deletes_allowed”: “0”

五、性能测试

优化效果评估基于基准测试,如果没有基准测试无法了解是否有性能提升,在这所有的变动前做一次测试会比较好。在我们的案例中:

  1. 单节点 5 千万到一亿的数据量测试,检查单点承受能力。

  2. 集群测试 1 亿-30 亿的数量,磁盘 IO/内存/CPU/网络 IO 消耗如何。

  3. 随机不同组合条件的检索,在各个数据量情况下表现如何。

  4. 另外 SSD 与机械盘在测试中性能差距如何。

性能的测试组合有很多,通常也很花时间,不过作为评测标准时间上的投入有必要,否则生产出现性能问题很难定位或不好改善。对于 ES 的性能研究花了不少时间,最多的关注点就是 lucene 的优化,能深入了解 lucene 原理对优化有很大的帮助。

六、生产效果

目前平台稳定运行,几十亿的数据查询 100 条都在 3 秒内返回,前后翻页很快,如果后续有性能瓶颈,可通过扩展节点分担数据压力。

 


来自:https://www.jianshu.com/p/a6b875ff250d

© 版权声明

☆ END ☆
喜欢就点个赞吧
点赞0 分享
图片正在生成中,请稍后...