ElasticSearch 磁盘 io 瓶颈问题解决方案探索

政采云技术团队.png

豌豆.png

问题描述

伴随整个政采云搜索业务的发展,商品总量也从千万级别快速膨胀到亿级别,伴随商品量的增长,带来的是索引体积的快速膨胀,进而导致ES 集群面临越来越严重的 io 读取瓶颈。搜索过程中面临越来越严重的 io-wait,磁盘 io-read 也逐渐接近性能极限,对查询的性能和集群的稳定性产生了很大的影响。

原因推测

在面临io问题后,我们推测问题主要出现在以下几个方面

  • 数据的快速膨胀,目前整个商品库的数据已经从之前的千万级膨胀到目前的亿级别。快速膨胀的数据占用的大量内存空间,导致 io 占用持续上升。
  • ES 的使用方式不当,没有最大化发挥 ES 的性能。
  • ES 集群的硬件性能不足,需要进行配置升级。

解决方案探索

调参方案

在问题的初期,我们主要通过参数调整的方式来解决问题。

减缓 segment 生成速率

  • ES 在底层还是依赖 Lucene 实现检索,而 Lucene 的最小执行单元就是 Segment 。因此 Segment 与整体的查询表现息息相关。

  • 一个 Shard 中的 Segment 的数量越多,在执行搜索时要扫描的 Segment 数量越多,整体的 io 开销也就越大。这个可以通过调整索引的 Refresh 刷新时间,通过降低数据实时性的方式解决,默认索引的刷新时间为 1s。

"refresh_interval": "60s"

merge参数调整

上文中提到可以通过提升索引的 Refresh 时间,通过降低 Segment 生成速率的方式来缓解io问题。还可以通过调整 Segment 的合并来解决问题,ES 提供了一些索引级别的动态配置,用来控制索引的 Segment 的合并。

整个 merge 参数主要分为以下三类:限速,新段生成,forcemerge 参数。

限速

  1. index.merge.scheduler.max_thread_count

    最大 merge 线程数,如果是 ssd 盘可以适当调大该数值

  2. index.merge.scheduler.auto_throttle

    是否开启io限流,默认限制为 20MB/s

  3. index.merge.scheduler.max_merge_count

    当前最大执行的 merge 任务数

  4. index.merge.policy.max_merge_at_onc

    单次最大允许多少个段合并

新段生成

  1. index.merge.policy.floor_segment

    小于此的段将“向上取整”到此大小,这是为了防止频繁刷新微小段,从而避免索引中出现长尾效应。默认阈值为 2mb。如果索引比较大,有很多小的 Segment 可以适当提升该阈值,这样可以降低 Segment 的总量,从而降低查询时产生的 io。提升该阈值会带来更频繁的 Merge 操作

  2. index.merge.policy.max_merged_segment

    最大的段大小默认是 5GB,ES 默认情况下不会生成大于 5GB 的段,在自动merge中,如果两个段合并后的大小会超过5gb,ES 会放弃该次合并操作。forcemerge 生成的段不受该阈值控制。

  3. index.merge.policy.segments_per_tier

    每层允许的段数。ES 在合并过程中,是分层合并的,具体合并流程可以参考 Chris 的博客。更小的值意味着更多的段合并操作和更少的段。该值必须大于等于 max_merge_at_once ,否则会带来频繁的段合并。

forcemerge参数

以上的参数都是控制 ES 的自动 merge,如果 ES 的自动 merge 不能满足需求,或者索引更新频率比较高,可以使用 ES 的 forcemerge api 来强制进行段合并,从而有效降低 Segment 数量和物理移除标记删除文档。

  1. index.merge.policy.expunge_deletes_allowed

    配合 only_expunge_deletes 参数使用,意味只删除比例高于该阈值的段进行段合并。

  2. index.merge.policy.max_merge_at_once_explicit

    控制 forcemerge 中单次最大段合并数量,用来控制f orcemerge 的段合并速率。

注意:forcemerge会带来大量的 io 和 cpu 占用,且 merge 会导致 cache 失效,一定要在业务低峰期进行,否则可能导致集群在高负载情况下发生雪崩。

translog落盘异步

{
    "translog":{
        "flush_threshold_size":"512mb",
        "sync_interval":"5s",
        "durability":"async"
    }
}

ES 默认的刷盘方式是同步的,在每次写入在操作完成后,都会立刻进行 translog 的落盘操作。同步刷盘可以提供更好的数据保障性工作,但是会带来IO的开销。异步刷盘牺牲了一定的可靠性保障,但是降低了 IO 的开销,性能相对更好。

查询优化

在我们经过参数调整,和每天业务低峰期的 forcemerge 任务后。将整个集群的 io-read 读取从高峰期的 500MB/s 降低到了400MB/s。

但是伴随商品量的膨胀,问题还会出现,为此还要探索更加彻底的解决方案。

我们的解决方案主要从查询上进行优化,总体上有以下两个方向:

降低单次扫描的数据量

ES 默认是将请求发送到所有 Shard 上,所有 Shard 并发执行搜索。如果可以降低数据的扫描范围,不光可以大幅提升检索效率,也大幅度降低 ES 需要打开的文档数。降低查询扫描的数据量,主要有以下三种方案:

  1. 利用es的路由策略

    ES 在写入和查询时均可以通过路由的策略,指定对应的分片,从而在查询时,避免不必要的 shard 扫描,进而大幅度减低查询时间和 io 开销。但是自定义 routing 之后,_id 将不在具有全局唯一的特性,如果业务对唯一性 id 强依赖的话,整体的改造成本会相对较高。

  2. 查询扫描限制

    除了在查询时指定路由的方式,指定数据扫描范围,也可以通过限制 fetch 时间和查询时遍历的数据量。在取 TopN 的场景和聚合统计场景,这种方式非常有效。

    {
      "timeout": "1s",      //时间维度限制ES向下遍历
      "terminate_after":1000 //限制每个分片扫描的数据量
    }
    
  3. 索引拆分

    索引拆分与数据库的分库分表操作大致相同。目前对于日志型数据可以采用时间拆分方式,配合 ES 的 ILM 索引生命周期管理,加上冷热数据节点,基本上可以完美解决数据膨胀问题。如果是业务数据,比如用户商品信息等,或者搜索场景,可以根据业务场景,进行索引的拆分,比如在电商场景中常见的按店铺维度,类目维度进行拆分,从而达到降低扫描的数据量目的。

采用性能更佳的查询字段

  1. 尽量避免使用 ES 处理关联关系

    在政采云的生产实践中发现,ES 对关联的关系处理性能并不好,父子文档检索的性能要比正常查询慢百倍左右,nested 方式查询虽然要好上很多,但是任然比正常查询慢几倍左右。更重要的是 nested 文档的每个子文档都是一个独立的 Lucene 文档,这样带来了文档数量几何级的增长,对磁盘占用,内存损耗都是一个天文数字。

    目前对关联关系的处理主要有以下四种方案:

    • 对象类型

    • 嵌套对象(Nested Object)

    • 父子关联关系(Parent/Child)

    • 应用端关联

      在主搜的场景改造中,有通过前台类目和政府采购目录检索商品的场景,之前这样绑定关系均存在每个商品上,通过 Nested 字段冗余的函数,实现了业务支持。每天通过 T+1 方案的数据同步,来解决绑定关系变更的问题。

      但是随着绑定关系变动频率的升高和商品数据量的增加,对于数据同步产生了很大压力,有时一天就要同步数千万的商品。同时通过慢日志分析显示,Nested 字段查询和聚合,在整个搜索耗时中占比相对较高。

    因此我们将整个关联关系由 ES 实现,改为由应用侧实现后,整体的 Lucene 文档数量下降了96%,从之前的单节点十亿级别的文档数,下降到了单节点百万级别。索引体积缩减上百 GB,整体 io 大幅度下降,从之前的日高峰 500+MB/s 的磁盘读取,下降到日高峰 20+MB/S 每秒。整个绑定关系的同步也从之前的 T+1,提升到秒级别。

    因此建议大家尽量避免在索引中使用 Nested 字段,而且使用 Nested 字段后,将不能使用索引预排序这个查询优化利器。

  2. 采用合适的字段类型进行搜索

    • 在实践中发现在精确匹配的场景,使用 keyword 字段类型的查询性能要优于数值类型。因此推荐大家在精确场景使用 keyowrod 类型,在范围查询场景使用数值类型。

    • 在生产环境中尽量避免使用 wildcard 语句进行查询,wildcard 会耗费大量的 cpu 资源,在大部分模糊查询场景都可以通过在 N-gram tokenizer上简单定制即可实现。

    这两个优化可能在 io 上优化较小,但是在整体查询性能上,还是有较大提升的。

升配

在解决 io 问题的过程中,我们也采用了配置升级方案,采用的升配方案有两种:

  • 升级磁盘到 SSD 盘
  • 通过升级内存大小解决 io 问题

升级到SSD盘

如果预算比较充裕可以考虑一键升级到 SSD,毕竟官方都建议使用 SSD 盘。升级 SSD 盘总体是没有问题的,但是要考虑本地 SSD 盘相比云厂商提供的云盘,带来的数据丢失问题。所有在使用 SSD 盘后,注意定期备份。同时通过多机架,多可用分区,多副本或者 CCR 的方式,来降低数据丢失风险。

在升级 SSD 盘的过程中,还可以配置冷热节点,热节点使用 SSD 盘,冷节点使用性能相对较差的硬盘,达到资源相对最大化的利用。

扩容内存

除了直接通过磁盘升级,解决 io 瓶颈的问题,扩容内容同样也是不错的方案。扩容内存,可以让 Lucene 将更多的数据加载到 PageCache 中,从而降低磁盘读取。

与磁盘升级方案相比,在数据量相对较少的场景,增加几百 GB 的内存,要远比磁盘升级来的便宜。如果使用云厂商的云盘,也避免了因为使用本地盘可能带来的数据丢失问题。在内存容量大于等于数据量的情况下,io 基本不会成为性能瓶颈。

在配置节点时,我们一般将 jvm 的最大内存容量设置为31GB,留出大部分内存提供给 Lucene 使用。内存容量和磁盘的比例,维持在1:10的比例。

总结

在本文的 io 瓶颈解决探索中,主要从 参数调整,查询改造,配置升级三个方面来阐述。就笔者个人的感受而言,升配带来的提升最直接,业务改造最彻底,效果最好,参数调整可以延长问题爆发的时间,但不是最终解决方案。

参考文献

ES官方文档

Lucene核心技术

Elasticsearch 核心技术与实战

铭毅天下

推荐阅读

人工智能 NLP 简述

浅析 ElasticJob-Lite 3.x 定时任务

雪花算法详解

基于流量域的数据全链路治理

招贤纳士

政采云技术团队(Zero),一个富有激情、创造力和执行力的团队,Base 在风景如画的杭州。团队现有300多名研发小伙伴,既有来自阿里、华为、网易的“老”兵,也有来自浙大、中科大、杭电等校的新人。团队在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注

政采云技术团队.png