一起学 Elasticsearch 系列 - 写入和检索调优
本文已收录至 Github,推荐阅读 👉 Java随想录
微信公众号:Java随想录
当涉及到大规模数据存储和检索时,Elasticsearch 以其快速、高效和强大的搜索能力而闻名,并被广泛应用于各种场景,例如日志分析、全文搜索和实时数据分析。
然而,并不是只要将数据存入 ES 就可以立即获得最佳性能和查询效率。正如任何强大的工具一样,ES 也需要进行调优,以充分发挥其潜力并满足特定业务需求。
在这篇文章中,我们将探讨 ES 写入调优和查询调优的关键方面,并提供一些实用的技巧和建议,帮助您优化 ES 集群的性能和响应速度。
写入调优
基本原则
写入性能调优是建立在 Elasticsearch 的写入原理之上的。
ES 数据写入具有一定的延时性,这是为了减少频繁的索引文件产生。默认情况下 ES 每秒生成一个 Segment 文件,当达到一定阈值的时候会执行 merge,merge 过程发生在 JVM 中,频繁的生成 Segmen 文件可能会导致频繁的触发 FGC,导致 OOM。
为了避免这种情况,通常采取的手段是降低 Segment 文件的生成频率,办法有两个:一个是增加时间阈值,另一个是增大 Buffer 的空间阈值,因为缓冲区写满也会生成 Segment 文件。
生产经常面临的写入可以分为两种情况:
高频低量:高频的创建或更新索引或文档,一般发生在 C 端业务场景下。
低频高量:一般情况为定期重建索引或批量更新文档数据。
在搜索引擎的业务场景下,用户一般并不需要那么高的写入实时性。比如你在网站发布一条征婚信息,或者二手交易平台发布一个商品信息。其他人并不是马上能搜索到的,这其实也是正常的处理逻辑。
这个延时的过程需要处理很多事情,比如:你的信息需要后台审核。
你发布的内容在搜索服务中需要建立索引,而且你的数据可能并不会马上被写入索引,而是等待要写入的数据达到一定数量之后,批量写入。
这种操作优点类似于我们快递物流的场景,只有当快递数量达到一定量级的时候,比如能装满整个车的时候,快递车才会发车。因为反正是要跑一趟,装的越多,平均成本越低。
这和我们数据写入到磁盘的过程是非常相似的,我们可以把一条文档数据看做是一个快递,而快递车每次发车就是向磁盘写入数据的一个过程,这个过程不宜太多,太多只会降低性能,就是体现在运输成本上面,而对于我们数据写入而言就是体现在我们硬件性能损耗上面。
优化手段
以下为常见数据写入的调优手段,写入调优均以提升写入吞吐量和并发能力为目标,而非提升写入实时性。
增加 flush 时间间隔
flush 的过程是非常消耗资源的。增加 flush 的时间间隔目的是减小数据写入磁盘的频率,降低磁盘 IO 频率。
增加 refresh_interval 参数的值
增加 refresh_interval 参数的值,目的是减少 segment 文件的创建,降低 merge 次数,因为 merge 是发生在 jvm 中的,有可能导致 full GC。
ES 的 refresh 行为非常昂贵,并且在正在进行的索引活动时经常调用,会降低索引速度。
默认情况下,Elasticsearch 每秒定期刷新索引,如果没有搜索流量或搜索流量很少(例如每 5 分钟不到一个搜索请求),可以适当调大此参数的值。
增加 Buffer 大小
本质也是减小 refresh 的时间间隔,因为导致 segment 文件创建的原因不仅有时间阈值,还有 buffer 空间大小,写满了也会创建。 默认值为 JVM 空间的 10%。
关闭副本
当需要单次写入大量数据的时候,建议关闭副本,暂停搜索服务,或选择在检索请求量谷值区间时间段来完成。
关闭副本可以带来如下好处:
减小读写之间的资源抢占,读写分离。
当检索请求数量很少的时候,可以减少甚至完全删除副本分片,关闭 segment 的自动创建以达到高效利用内存的目的,因为副本的存在会导致主从之间频繁的进行数据同步,大大增加服务器的资源占用。
具体可通过设置index.number_of_replicas
为 0 以加快索引速度。没有副本意味着丢失单个节点可能会导致数据丢失,因此数据保存在其他地方很重要,以便在出现问题时可以重试初始加载。初始加载完成后,可以设置index.number_of_replicas
改回其原始值。
禁用 swap
大多数操作系统尝试将尽可能多的内存用于文件系统缓存,并急切地换掉未使用的应用程序内存。这可能导致部分 JVM 堆甚至其可执行页面被换出到磁盘。
交换对性能和节点稳定性非常不利,应该不惜一切代价避免。它可能导致垃圾收集持续几分钟而不是几毫秒,并且可能导致节点响应缓慢甚至与集群断开连接。在 Elastic 分布式系统中,让操作系统杀死节点更有效。
使用多个工作线程
发送批量请求的单个线程不太可能最大化 Elasticsearch 集群的索引容量。为了使用集群的所有资源,应该从多个线程或进程发送数据。除了更好地利用集群的资源外,还有助于降低每个 fsync 的成本。
确保注意 TOO_MANY_REQUESTS
响应代码:429。(EsRejectedExecutionException 使用 Java 客户端),这是 Elasticsearch 告诉我们它无法跟上当前索引速度的方式。发生这种情况时,应该在重试之前暂停索引,最好使用随机指数退避。
与调整批量请求的大小类似,只有测试才能确定最佳工作线程数量是多少。这可以通过逐渐增加线程数量来测试,直到集群上的 I/O 或 CPU 饱和。
max_result_window 参数
max_result_window
是分页返回的最大数值,默认值为 10000。max_result_window 本身是对 JVM 的一种保护机制,通过设定一个合理的阈值,避免初学者分页查询时由于单页数据过大而导致 OOM。
设置一个合理的大小是需要通过你的各项指标参数来衡量确定的,比如你用户量、数据量、物理内存的大小、分片的数量等等。通过监控数据和分析各项指标从而确定一个最佳值,并非越大越好。
查询调优
读写性能不可兼得
首先要明确一点:鱼和熊掌不可兼得。读写性能调优在很多场景下是只能二选一的。牺牲 A 换 B 的行为非常常见。索引本质上也是通过空间换取时间。牺牲写入实时性就是为了提高检索的性能。
当你在二手平台或者某垂直信息网站发布信息之后,是允许有信息写入的延时性的。但是检索不行,甚至 1 秒的等待时间对用户来说都是无法接受的。满足用户的要求甚至必须做到 10 ms 以内。
优化手段
避免单次召回大量数据
搜索引擎最擅长的事情是从海量数据中查询少量相关文档,而非单次检索大量文档。非常不建议动辄查询上万数据。如果有这样的需求,建议使用滚动查询
避免单个文档过大
鉴于默认http.max_content_length
设置为 100MB,Elasticsearch 将拒绝索引任何大于该值的文档。您可能决定增加该特定设置,但 Lucene 仍然有大约 2GB 的限制。
即使不考虑硬性限制,大型文档通常也不实用。大型文档对网络、内存使用和磁盘造成了更大的压力,即使对于不请求的搜索请求也是如此。
有时重新考虑信息单元应该是什么是有用的。例如,您想让书籍可搜索的事实并不一定意味着文档应该包含整本书。使用章节甚至段落作为文档可能是一个更好的主意,然后在这些文档中拥有一个属性来标识它们属于哪本书。这不仅避免了大文档的问题,还使搜索体验更好。例如,如果用户搜索两个单词 fooand bar,则不同章节之间的匹配可能很差,而同一段落中的匹配可能很好。
单次查询 10 条文档 好于 10 次查询每次一条
批量请求将产生比单文档索引请求更好的性能。但是每次查询多少文档最佳,不同的集群最佳值可能不同,为了获得批量请求的最佳阈值,建议在具有单个分片的单个节点上运行基准测试。
首先尝试一次索引 100 个文档,然后是 200 个,然后是 400 个等。在每次基准测试运行中,批量请求中的文档数量翻倍。当索引速度开始趋于平稳时,就可以获得已达到数据批量请求的最佳大小。在相同性能的情况下,当大量请求同时发送时,太大的批量请求可能会使集群承受内存压力,因此建议避免每个请求超过几十兆字节。
数据建模
很多人会忽略对 Elasticsearch 数据建模的重要性。
nested 属于 object 类型的一种,是 Elasticsearch 中用于复杂类型对象数组的索引操作。Elasticsearch 没有内部对象的概念,因此,ES 在存储复杂类型的时候会把对象的复杂层次结果扁平化为一个键值对列表。
特别是,应避免 Join 连接。Nested 可以使查询慢几倍,Join 会使查询慢数百倍。两种类型的使用场景应该是:Nested 针对字段值为非基本数据类型的时候,而 Join 则用于当子文档数量级非常大的时候。
给系统留足够的内存
Lucene 的数据的 fsync 是发生在 OS cache 的,要给 OS cache 预留足够的内存大小。
预索引
利用查询中的模式来优化数据的索引方式。例如,如果所有文档都有一个 price 字段,并且大多数查询 range 在固定的范围列表上运行聚合,可以通过将范围预先索引到索引中并使用聚合来加快聚合速度。
使用 filter 代替 query
query 和 filter 的主要区别在: filter 是结果导向的而 query 是过程导向。query 倾向于“当前文档和查询的语句的相关度”,而 filter 倾向于“当前文档和查询的条件是不是相符”。即在查询过程中,query 是要对查询的每个结果计算相关性得分的,而 filter 不会。另外 filter 有相应的缓存机制,可以提高查询效率。
避免深度分页
避免单页数据过大,可以参考百度或者淘宝的做法。es 提供两种解决方案 scroll search 和 search after。
使用 Keyword 类型
并非所有数值数据都应映射为数值字段数据类型。Elasticsearch 为查询优化数字字段,例如 integeror long。如果不需要范围查找,对于 term 查询而言,keyword 比 integer 性能更好。
避免使用脚本
Scripting 是 Elasticsearch 支持的一种专门用于复杂场景下支持自定义编程的强大的脚本功能。相对于 DSL 而言,脚本的性能更差,DSL 能解决 80% 以上的查询需求,如非必须,尽量避免使用 Script。
版权声明: 本文为 InfoQ 作者【码农BookSea】的原创文章。
原文链接:【http://xie.infoq.cn/article/911f73de56ee9825614a85cf2】。文章转载请联系作者。
评论