JAVA 使用 Elasticsearch 时 CPU 飙高?索引 Mapping 设计优化指南

好的,下面是关于“JAVA 使用 Elasticsearch 时 CPU 飙高?索引 Mapping 设计优化指南”的讲座内容。

各位同学,大家好!今天我们来聊聊在使用 Java 操作 Elasticsearch 时,经常遇到的一个让人头疼的问题:CPU 飙高。特别是在数据量逐渐增大,业务复杂度提升的情况下,这个问题会变得更加突出。与其临阵磨枪,不如防患于未然。而索引 Mapping 的设计,正是 Elasticsearch 性能优化的基石。一个合理的 Mapping 设计,能有效减少 CPU 负载,提升查询效率,反之则可能导致性能瓶颈。

一、CPU 飙高的常见原因分析

在使用 Elasticsearch 的过程中,CPU 飙高可能是多种因素共同作用的结果。我们需要逐一排查,才能找到症结所在。

  • Mapping 设计不合理: 这是最常见的原因之一。例如,将大量字段设置为 text 类型,并且没有进行合理的分析器配置;或者将不需要分析的字段也设置成了 text 类型。不恰当的 Mapping 会导致索引体积膨胀,查询时需要扫描更多的数据,从而消耗大量的 CPU 资源。
  • 查询语句复杂度过高: 复杂的查询,如大量的 wildcardfuzzyregexp 查询,或者深度嵌套的聚合,都会消耗大量的 CPU 资源。
  • 数据量过大: 当索引中的数据量达到一定规模时,即使是简单的查询也可能需要扫描大量的数据,导致 CPU 飙高。
  • 硬件资源不足: Elasticsearch 集群的硬件资源,如 CPU、内存、磁盘 I/O 等,是其性能的基石。如果硬件资源不足,即使 Mapping 设计再合理,查询语句再优化,也难以避免 CPU 飙高的问题。
  • 分片过多或过少: 过多的分片会导致集群的管理开销增大,过少的分片则可能导致单个分片的数据量过大,查询时需要扫描更多的数据。
  • JVM 内存设置不合理: JVM 的堆内存大小直接影响 Elasticsearch 的性能。如果堆内存设置过小,会导致频繁的 GC,从而消耗大量的 CPU 资源。

二、Mapping 优化策略详解

针对上述问题,我们可以采取以下策略来优化 Mapping 设计,从而降低 CPU 负载。

  1. 精确定义字段类型:

    这是 Mapping 优化的核心。我们需要根据字段的实际用途,选择最合适的字段类型。

    • keyword:用于精确匹配,例如用户 ID、商品 ID、状态码等。keyword 类型不会进行分词,直接存储原始值。
    • text:用于全文检索,例如文章内容、商品描述等。text 类型会进行分词,将文本拆分成多个词项,以便进行模糊匹配。
    • date:用于存储日期和时间。
    • integerlongfloatdouble:用于存储数值类型。
    • boolean:用于存储布尔类型。
    • geo_point:用于存储地理坐标。
    • nested:用于存储嵌套的对象数组。

    错误示例:

    // 不良示范:将所有字段都设置为 text 类型
    {
      "properties": {
        "user_id": { "type": "text" },
        "product_id": { "type": "text" },
        "order_time": { "type": "text" },
        "content": { "type": "text" }
      }
    }

    正确示例:

    // 优化后的 Mapping
    {
      "properties": {
        "user_id": { "type": "keyword" },
        "product_id": { "type": "keyword" },
        "order_time": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" },
        "content": { "type": "text" }
      }
    }

    Java 代码示例:

    import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
    import org.elasticsearch.client.RequestOptions;
    import org.elasticsearch.client.RestHighLevelClient;
    import org.elasticsearch.common.xcontent.XContentBuilder;
    import org.elasticsearch.common.xcontent.XContentFactory;
    
    import java.io.IOException;
    
    public class MappingExample {
    
        public static void createIndexWithMapping(RestHighLevelClient client, String indexName) throws IOException {
            CreateIndexRequest request = new CreateIndexRequest(indexName);
    
            XContentBuilder builder = XContentFactory.jsonBuilder();
            builder.startObject();
            {
                builder.startObject("properties");
                {
                    builder.startObject("user_id");
                    {
                        builder.field("type", "keyword");
                    }
                    builder.endObject();
    
                    builder.startObject("product_id");
                    {
                        builder.field("type", "keyword");
                    }
                    builder.endObject();
    
                    builder.startObject("order_time");
                    {
                        builder.field("type", "date");
                        builder.field("format", "yyyy-MM-dd HH:mm:ss");
                    }
                    builder.endObject();
    
                    builder.startObject("content");
                    {
                        builder.field("type", "text");
                    }
                    builder.endObject();
                }
                builder.endObject();
            }
            builder.endObject();
    
            request.mapping(builder);
    
            client.indices().create(request, RequestOptions.DEFAULT);
        }
    
        public static void main(String[] args) throws IOException {
            // 假设已经创建了 RestHighLevelClient 对象 client
            // RestHighLevelClient client = new RestHighLevelClient(...);
            String indexName = "my_index";
            // createIndexWithMapping(client, indexName);
            // client.close(); // 记得关闭 client
            System.out.println("请先配置RestHighLevelClient客户端并取消注释createIndexWithMapping方法");
        }
    }

    这个例子展示了如何使用 Java API 创建带有 Mapping 的索引。

  2. 选择合适的分析器 (Analyzer):

    text 类型字段需要指定分析器,用于将文本拆分成词项。Elasticsearch 提供了多种内置的分析器,例如 standardwhitespacesimpleenglish 等。

    • standard:默认的分析器,适用于大多数语言。
    • whitespace:按空格分割文本。
    • simple:将文本转换为小写,并移除标点符号。
    • english:针对英文的分析器,会移除停用词,并将单词转换为词根。

    除了内置的分析器,我们还可以自定义分析器,以满足特定的业务需求。例如,可以自定义一个分析器,用于处理中文分词。

    示例:

    // 使用 standard 分析器
    {
      "properties": {
        "content": {
          "type": "text",
          "analyzer": "standard"
        }
      }
    }
    
    // 使用自定义分析器
    {
      "settings": {
        "analysis": {
          "analyzer": {
            "my_analyzer": {
              "type": "custom",
              "tokenizer": "ik_max_word",
              "filter": [
                "lowercase",
                "stop"
              ]
            }
          }
        }
      },
      "mappings": {
        "properties": {
          "content": {
            "type": "text",
            "analyzer": "my_analyzer"
          }
        }
      }
    }

    Java 代码示例 (自定义分析器):

    import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
    import org.elasticsearch.client.RequestOptions;
    import org.elasticsearch.client.RestHighLevelClient;
    import org.elasticsearch.common.settings.Settings;
    import org.elasticsearch.common.xcontent.XContentBuilder;
    import org.elasticsearch.common.xcontent.XContentFactory;
    
    import java.io.IOException;
    
    public class CustomAnalyzerExample {
    
        public static void createIndexWithCustomAnalyzer(RestHighLevelClient client, String indexName) throws IOException {
            CreateIndexRequest request = new CreateIndexRequest(indexName);
    
            Settings settings = Settings.builder()
                .put("analysis.analyzer.my_analyzer.type", "custom")
                .put("analysis.analyzer.my_analyzer.tokenizer", "ik_max_word")
                .putList("analysis.analyzer.my_analyzer.filter", "lowercase", "stop")
                .build();
    
            request.settings(settings);
    
            XContentBuilder builder = XContentFactory.jsonBuilder();
            builder.startObject();
            {
                builder.startObject("properties");
                {
                    builder.startObject("content");
                    {
                        builder.field("type", "text");
                        builder.field("analyzer", "my_analyzer");
                    }
                    builder.endObject();
                }
                builder.endObject();
            }
            builder.endObject();
    
            request.mapping(builder);
    
            client.indices().create(request, RequestOptions.DEFAULT);
        }
    
        public static void main(String[] args) throws IOException {
             // 假设已经创建了 RestHighLevelClient 对象 client
             //RestHighLevelClient client = new RestHighLevelClient(...);
            String indexName = "my_index_with_analyzer";
            //createIndexWithCustomAnalyzer(client, indexName);
            //client.close(); // 记得关闭 client
            System.out.println("请先配置RestHighLevelClient客户端并取消注释createIndexWithCustomAnalyzer方法");
        }
    }

    这个例子展示了如何使用 Java API 创建带有自定义分析器的索引。 注意:ik_max_word 是一个中文分词器,需要安装相应的插件。

  3. 禁用 _all 字段:

    _all 字段会将所有字段的内容复制到一起,用于全文检索。但是,_all 字段会增加索引体积,降低查询效率。如果不需要使用 _all 字段,可以将其禁用。在 Elasticsearch 7.0 以后,_all 字段默认被禁用。

    示例:

    {
      "mappings": {
        "_all": {
          "enabled": false
        },
        "properties": {
          "user_id": { "type": "keyword" },
          "product_id": { "type": "keyword" },
          "order_time": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" },
          "content": { "type": "text" }
        }
      }
    }
  4. 使用 copy_to 字段:

    如果需要对多个字段进行组合查询,可以使用 copy_to 字段将多个字段的内容复制到一个新的字段中,然后对新的字段进行查询。这样可以避免使用复杂的查询语句,从而降低 CPU 负载。

    示例:

    {
      "properties": {
        "first_name": {
          "type": "text",
          "copy_to": "full_name"
        },
        "last_name": {
          "type": "text",
          "copy_to": "full_name"
        },
        "full_name": {
          "type": "text"
        }
      }
    }

    在这个例子中,first_namelast_name 字段的内容会被复制到 full_name 字段中。我们可以对 full_name 字段进行查询,以实现对姓名进行组合查询的目的。

  5. 合理使用 doc_values

    doc_values 是一种列式存储的数据结构,用于优化排序和聚合操作。默认情况下,大多数字段都会启用 doc_values。但是,对于不需要进行排序和聚合的字段,可以禁用 doc_values,以节省磁盘空间和内存。

    示例:

    {
      "properties": {
        "user_id": {
          "type": "keyword",
          "doc_values": false // 禁用 doc_values
        },
        "product_id": {
          "type": "keyword"
        },
        "order_time": {
          "type": "date",
          "format": "yyyy-MM-dd HH:mm:ss"
        },
        "content": {
          "type": "text"
        }
      }
    }
  6. 使用 index_prefixes 优化前缀搜索:

    对于 keyword 类型字段,如果需要进行前缀搜索,可以使用 index_prefixes 参数来优化性能。index_prefixes 参数会将字段的值拆分成多个前缀,并建立索引。

    示例:

    {
      "properties": {
        "product_name": {
          "type": "keyword",
          "index_prefixes": {
            "min_chars": 2,
            "max_chars": 5
          }
        }
      }
    }

    在这个例子中,product_name 字段的值会被拆分成长度为 2 到 5 个字符的前缀,并建立索引。这样可以加速前缀搜索的速度。

  7. 使用 normalizer 规范化 keyword 字段:

    对于 keyword 类型字段,可以使用 normalizer 参数来规范化字段的值。normalizer 可以将字段的值转换为小写,移除空格等,以提高匹配的准确性。

    示例:

    {
      "settings": {
        "analysis": {
          "normalizer": {
            "my_normalizer": {
              "type": "custom",
              "filter": [
                "lowercase",
                "asciifolding"
              ]
            }
          }
        }
      },
      "mappings": {
        "properties": {
          "user_name": {
            "type": "keyword",
            "normalizer": "my_normalizer"
          }
        }
      }
    }

    在这个例子中,user_name 字段的值会被转换为小写,并移除 ASCII 字符的变音符号。

三、其他优化技巧

除了 Mapping 优化,还有一些其他的技巧可以帮助降低 CPU 负载。

  • 优化查询语句: 避免使用复杂的查询,尽量使用简单的查询。例如,尽量避免使用 wildcardfuzzyregexp 查询。
  • 使用缓存: Elasticsearch 提供了多种缓存机制,例如节点查询缓存、索引请求缓存等。合理利用缓存可以减少查询的次数,从而降低 CPU 负载。
  • 调整分片大小: 分片的大小会影响查询的性能。一般来说,分片的大小应该在 30GB 到 50GB 之间。
  • 监控和调优: 使用 Elasticsearch 的监控工具,例如 Kibana,可以监控集群的性能指标,并根据实际情况进行调优。

四、案例分析

假设我们有一个电商网站,需要存储商品信息。商品信息包括商品 ID、商品名称、商品描述、商品价格、商品分类等。

原始 Mapping 设计:

{
  "properties": {
    "product_id": { "type": "text" },
    "product_name": { "type": "text" },
    "product_description": { "type": "text" },
    "product_price": { "type": "text" },
    "product_category": { "type": "text" }
  }
}

这个 Mapping 设计存在以下问题:

  • 将所有字段都设置为 text 类型,即使是 product_idproduct_price 这样的字段,也会进行分词,浪费存储空间和 CPU 资源。
  • 没有指定分析器,使用默认的 standard 分析器,可能不适合中文分词。

优化后的 Mapping 设计:

{
  "properties": {
    "product_id": { "type": "keyword" },
    "product_name": { "type": "text", "analyzer": "ik_max_word" },
    "product_description": { "type": "text", "analyzer": "ik_max_word" },
    "product_price": { "type": "double" },
    "product_category": { "type": "keyword" }
  }
}

这个 Mapping 设计的优化之处在于:

  • product_idproduct_category 字段设置为 keyword 类型,用于精确匹配。
  • product_nameproduct_description 字段设置为 text 类型,并使用 ik_max_word 分析器进行中文分词。
  • product_price 字段设置为 double 类型,用于存储数值类型。

通过这个案例,我们可以看到,合理的 Mapping 设计可以有效降低 CPU 负载,提升查询效率。

五、总结

优化 Elasticsearch 的 Mapping 设计是一个持续的过程,需要根据实际业务需求和数据特点进行调整。记住,没有一劳永逸的解决方案,只有不断地学习和实践,才能找到最适合自己的优化方案。 针对字段类型,分析器,doc_values 以及索引前缀等常见影响CPU的因素进行了优化策略的讲解和分析,并给出了相应的代码案例。

希望今天的讲座对大家有所帮助!谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注