数十亿级别数据量下,ES 有哪些特定的查询优化技巧?
在数十亿级别数据量下,以下是一些特定的 Elasticsearch(ES)查询优化技巧:
- 利用缓存:
- Filesystem Cache(文件系统缓存):ES 严重依赖底层的文件系统缓存。尽量为文件系统缓存分配足够多的内存,使其能容纳所有的索引数据文件(idxsegmentfile)。理想情况下,机器内存至少能容纳总数据量的一半。比如有 100GB 的索引数据,就尽量保证有 50GB 以上的内存用于缓存,这样查询时基本都走内存,性能会大幅提升,从秒级提升到毫秒级。
- 查询缓存:ES 提供查询缓存功能,可以缓存查询结果。对于相同的查询语句,下次查询时可直接从缓存中获取结果,避免重复计算。但要注意,缓存适合那种查询结果不经常变化的场景。
- 过滤器缓存:缓存过滤器的结果,对于经常使用的过滤器能提高查询效率。合理设置缓存大小和过期时间,比如对于一些常用的、数据变动不频繁的筛选条件对应的过滤器进行缓存。
- 数据预热:对于经常被访问的热数据,建立专门的缓存预热子系统。例如,在电商系统中,可以提前将热门商品的数据通过后台程序每隔一段时间(如几分钟)主动访问一次,刷到文件系统缓存中。这样当真正有用户访问这些热数据时,就可以直接从内存中快速获取,极大地提高响应速度。像微博中一些大 V 的数据、热门话题等,都可以通过这种方式进行预热。
- 冷热分离:类似于 MySQL 的水平拆分,将访问很少、频率很低的冷数据单独写一个索引,将访问频繁的热数据单独写一个索引。比如,把近期一个月内频繁访问的数据放入热数据索引,而一个月之前很少访问的数据放入冷数据索引。确保热数据在被预热之后,尽量都留在文件系统缓存里,避免被冷数据冲刷掉。同时,在查询时,可以优先查询热数据索引,提高查询性能。如果有 6 台机器,可以 3 台机器放热数据索引,另外 3 台机器放冷数据索引。这样,即使冷数据查询性能稍差,但因为大部分时间都在访问热数据索引(热数据可能只占总数据量的 10%左右),所以整体的访问性能仍然可以得到保障。
- 优化索引:
- 合理设置分片和副本:
- 分片数量:分片是 ES 分布式存储的基本单位,需要根据数据的特点、查询需求以及服务器的硬件资源来合理设置分片数量。如果数据量巨大且查询负载高,可以适当增加分片数量,但也不宜过多,否则会增加管理和协调的成本。例如,对于数十亿级别的数据,如果服务器性能较强,可以考虑将每个索引分成 100 - 200 个分片左右。
- 副本数量:副本可以提高数据的可用性和查询的并发能力。一般在多节点集群中,可以设置每个分片有 2 - 3 个副本。这样既能保证在节点故障时数据仍然可用,又能在一定程度上提高查询的并行处理能力。不过,副本数量增多也会占用更多的存储空间和网络资源,所以需要根据实际情况进行权衡。
- 选择合适的字段类型:在创建索引时,为字段选择合适的数据类型能减少存储空间占用并提高查询效率。比如,对于数值类型的字段,根据数据的范围和精度选择合适的整数类型(如 int、long)或小数类型(如 float、double);对于文本类型的字段,如果只需要进行精确匹配查询,使用 keyword 类型,而需要进行全文搜索时,使用 text 类型,并选择合适的分析器(如 standard、ik 等)进行分词处理。例如,存储用户的年龄字段,选择 int 类型;存储用户的姓名字段,如果只进行精确匹配,选择 keyword 类型,若要进行全文搜索,则选择 text 类型并搭配相应分析器。
- 控制写入数据量:只写入 ES 中要用来检索的少数几个字段,避免写入不必要的字段占用文件系统缓存空间。比如,有一行数据包含 id、name、age 等 30 个字段,但实际搜索只需要根据 id、name、age 三个字段来搜索,那么就只写入这三个字段到 ES 中,其他字段的数据可以存储在 MySQL 或 HBase 等数据库中。当从 ES 中根据 name 和 age 搜索得到结果的 docid 后,再根据 docid 到其他数据库中查询每个 docid 对应的完整数据。这样既能保证查询性能,又能合理利用存储空间。例如,在一个日志分析系统中,只将日志中的关键信息(如时间、日志级别、关键事件描述等)写入 ES 用于检索,而详细的日志内容存储在其他数据库中。
- 合理设置分片和副本:
- 查询语句优化:
- 选择合适的查询类型:根据具体的查询需求选择合适的查询语句类型。例如,对于精确匹配查询,使用 term 查询;对于范围查询,使用 range 查询;对于全文搜索,使用 match 查询等。并且,合理使用查询参数,如 size(返回结果数量)、from(结果偏移量)等,避免返回过多不必要的结果,减少查询时间和内存占用。比如,要查询年龄为 30 岁的用户,使用 term 查询语句:
{ "query": { "term": { "age": 30 } } }
;要查询年龄在 25 到 35 岁之间的用户,使用 range 查询语句:{ "query": { "range": { "age": { "gte": 25, "lte": 35 } } } }
。 - 避免复杂查询操作:尽量避免在 ES 中进行复杂的关联查询(如 join 操作)、嵌套查询(nested query)、父子关系查询(parent-child query)等,因为这些操作的性能开销较大。如果业务中确实需要关联查询等复杂操作,建议在应用程序层面(如在 Java 代码中)先完成关联,将关联好的数据直接写入 ES 中,这样在搜索时就不需要利用 ES 的搜索语法来完成关联搜索,从而提高查询性能。
- 利用过滤器:对于一些条件固定、不经常变化的筛选条件,尽量使用过滤器(filter)而不是查询(query)。过滤器不参与评分计算,执行速度更快,并且可以被缓存,能够提高查询效率。例如,要查询某个特定城市且年龄大于 30 岁的用户,可以使用过滤器:
{ "query": { "bool": { "filter": [ { "term": { "city": "New York" } }, { "range": { "age": { "gt": 30 } } } ] } } }
。
- 选择合适的查询类型:根据具体的查询需求选择合适的查询语句类型。例如,对于精确匹配查询,使用 term 查询;对于范围查询,使用 range 查询;对于全文搜索,使用 match 查询等。并且,合理使用查询参数,如 size(返回结果数量)、from(结果偏移量)等,避免返回过多不必要的结果,减少查询时间和内存占用。比如,要查询年龄为 30 岁的用户,使用 term 查询语句:
- 分页优化:ES 的分页查询在深度分页时性能较差。如果可以,尽量避免深度分页,或者与产品经理沟通,根据业务实际情况限制分页的深度。对于类似下拉分页的场景,可以使用 scroll API 或者 search_after 参数进行分页查询。scroll API 可以创建一个查询快照,在一段时间内可以重复使用该快照进行查询,避免了每次查询都要重新计算结果的问题,适合于那种类似微博下拉翻页的场景,不能随意跳到任何一页,并且需要注意设置合理的快照保存时间,防止因超时导致查询失败。search_after 参数则可以根据上一页的结果进行下一页的查询,避免了使用 from 参数导致的性能问题,但也不支持随意跳页。例如,使用 scroll API 进行分页查询:
{ "query": { "match_all": {} }, "size": 10, "scroll": "1m" }
,然后使用 scroll_id 进行后续的查询;使用 search_after 参数:{ "query": { "match_all": {} }, "size": 10, "sort": [ { "id": "asc" } ], "search_after": [10] }
,其中 search_after 参数的值是上一页最后一个结果的排序值。