原文:eBay网Elasticsearch性能优化实践
作者:Pei Wang
翻译:无阻我飞扬
摘要:Elasticsearch是基于Apache Lucene的开源搜索和分析引擎,允许用户以近乎实时的方式存储,搜索和分析数据。虽然Elasticsearch专为快速查询而设计,但其性能在很大程度上取决于用于应用程序的场景,索引的数据量以及应用程序和用户查询数据的速率。这篇文章概述了挑战和调优过程,以及Pronto团队以战略方式构建应对挑战的工具。它还以各种图形配置展示了进行基准测试的一些结果。以下是译文。
Elasticsearch是基于Apache Lucene的开源搜索和分析引擎,允许用户以近乎实时的方式存储,搜索和分析数据。Pronto是在eBay网站上托管Elasticsearch集群的平台,该平台使得eBay网内部客户易于部署,操作用于全文搜索,实时分析和日志/事件监控的大规模的Elasticsearch。当前Pronto平台管理着60多个Elasticsearch集群和2000多个节点,日采集量达到180亿份文档,日均搜索请求达到35亿份。平台提供从条款,补救以及安全到监控,报警和诊断的全方位的信息。
虽然Elasticsearch专为快速查询而设计,但其性能在很大程度上取决于用于应用程序的场景,索引的数据量以及应用程序和用户查询数据的速率。这篇文章概述了挑战和调优过程,以及Pronto团队以战略方式构建应对挑战的工具。它还以各种图形配置展示了进行基准测试的一些结果。
挑战
迄今为止所观察到的Pronto / Elasticsearch使用案例面临的挑战包括:
- 高吞吐量:一些集群每天摄取高达5TB的数据,一些集群每天的搜索请求超过4亿。如果Elasticsearch无法及时处理这些请求,那么这些请求将在上游累积。
- 搜索延迟低:对于性能关键的集群,尤其是面向站点的系统,低搜索延迟的特性是必须具有的,否则用户体验将会受到影响。
- 由于数据或查询是可变的,所以最佳设置总是在变化。所有情况都没有最佳设置。例如,将索引拆分成更多的分片(代表索引分片,Elasticsearch可以把一个完整的索引分成多个分片,这样的好处是可以把一个大的索引拆分成多个,分布到不同的节点上。构成分布式搜索。分片的数量只能在索引创建前指定,并且索引创建后不能更改。)对于耗时的查询是很有好处的,但是这可能会损害其它查询性能。
解决方案
为了帮助客户应对这些挑战,Pronto团队从用户案例开始入手并持续整个集群生命周期,构建性能测试、调优和监控的战略方法。
- 评估集群大小:在一个新的用户案例部署之前,收集客户提供的信息,诸如吞吐量,文档大小,文档数量和搜索类型,以评估Elasticsearch集群的初始大小。
- 优化索引设计:与客户一起评审索引设计。
- 调优索引性能:根据用户场景调优索引性能和搜索性能。
- 调优搜索性能:使用用户真实数据/查询运行性能测试,用Elasticsearch配置参数的组合比较和分析测试结果。
- 运行性能测试:在案例启动以后,集群将受到监控,每当数据发生变化,查询更改或者流量增加时,用户都可以自由地重新运行性能测试。
评估集群大小
Pronto团队为每种类型的机器和每个支持的Elasticsearch版本运行基准测试,以收集性能数据,然后将其与客户提供的信息一起用于评估集群的初始大小,这些信息包括:
- 索引吞吐量
- 文档大小
- 搜索吞吐量
- 查询类型
- 热索引文档计数
- 保留策略
- 响应时间要求
- SLA级别
优化索引设计
在开始摄取数据并运行查询之前,请三思而后行。索引代表着什么?Elastic的官方回答是“具有相似特征的文档集合”。那么下一个问题是“应该使用哪些特征来对数据进行分组?应该把所有文件放入一个索引还是多个索引呢?”答案是,这取决于所使用的查询。下面是关于如何根据最常用的查询分组索引的一些建议。
- 如果查询有一个过滤字段并且它的值是可枚举的,那么把数据分成多个索引。例如,有大量的全球产品信息被摄取到Elasticsearch中,大多数查询都有一个过滤子句“region”(区域),并且很少有机会运行跨区域查询。查询主体可以优化为:
{
"query": {
"bool": {
"must": {
"match": {
"title": "${title}" }
},
"filter": {
"term": {
"region": "US" }
}
}
}
}
在这种情况下,如果索引按照美国,欧洲等地区分成几个较小的索引,就可以获得更好的性能。然后可以从查询中删除过滤子句。如果需要运行一个跨区域查询,可以将多个索引或通配符传递给Elasticsearch。
如果查询具有过滤字段并且其值不可枚举,请使用路由。可以通过使用过滤字段值作为路由键来将索引拆分成多个分片,然后删除过滤条件。关于ElasticSearch里的路由功能请参见这篇文章。
例如,Elasticsearch有数以百万计的订单,大多数查询需要通过买家ID查询订单。为每个买家创建索引是不可能的,所以不能通过买家ID将数据拆分成多个索引。一个合适的解决方案是使用路由将具有相同买家ID的所有订单放入同一个分片中,然后几乎所有的查询都可以在匹配路由键的分片内完成。
如果查询具有日期范围过滤条件,则按日期分组数据。这适用于大多数日志记录或监控场景。可以以每天,每周或每月分组索引,然后可以在指定的日期范围内获得索引列表。Elasticsearch只需要查询一个较小的数据集而不是整个数据集。此外,当数据过期时,很容易缩小/删除旧的索引。
明确地设置映射。Elasticsearch可以动态地创建映射,但可能并不适用于所有场景。例如,Elasticsearch 5.x中默认的字符串字段映射是“关键字”和“文本”类型,这在很多场景下是没有必要的。
如果文档使用用户定义的ID或路由索引,请避免不平衡分片。 Elasticsearch采用随机ID生成器和哈希算法来确保文档均匀地分配给分片。当使用用户定义的ID或路由时,ID或路由键可能不够随机,并且一些分片可能明显比其它分片更大。在这种情况下,在这个分片上的读/写操作将比在其它分片上慢得多。可以优化ID /路由键或使用index.routing_partition_size (在5.3和更高版本中可用)。
使分片均匀分布在节点上。如果一个节点比其它节点有更多的分片,则会比其它节点承担更多的负载,并很有可能成为整个系统的瓶颈。
调优索引性能
用于索引诸如日志和监控之类的重场景,索引性能是关键指标。这里有一些建议:
- 使用批量请求。
- 使用多个线程/工作来发送请求。
- 增加刷新间隔。每次刷新事件发生时,Elasticsearch都会创建一个新的Lucene段,并在稍后进行合并。增加刷新间隔将降低创建/合并的成本。请注意,只有在刷新事件发生后才能进行文件搜索。
性能和刷新间隔之间的关系
从上图可以看出,随着刷新间隔的增大,吞吐量增加,响应时间变快。可以使用下面的请求来检查有多少段以及刷新和合并花费了多少时间。
Index/_stats?filter_path= indices.**.refresh,indices.**.segments,indices.**.merges
减少副本数量。Elasticsearch需要为每个索引请求将文档写入主要和所有副本分片。显然,一个大的副本数会减慢索引速度,但另一方面,增加副本数量将提高搜索性能。这个话题将在本文后面讨论。副本的作用一是提高系统的容错性,当某个节点某个分片损坏或丢失时可以从副本中恢复;二是提高Elasticsearch的查询效率,Elasticsearch会自动对搜索请求进行负载均衡
性能和副本数量之间的关系
从上面的图中,可以看到随着副本数量的增加,吞吐量下降,响应时间也变慢。
- 如果可能,使用自动生成的ID。 Elasticsearch自动生成的ID可以确保是唯一的,以避免版本查询。如果客户真的需要使用自定义的ID,建议选择一个对Lucene友好的ID,比如零填充顺序ID,UUID-1或者Nano time。这些ID具有一致的顺序模式,压缩良好。相比之下,像UUID-4这样的ID本质上仍旧是随机的,它提供了较差的压缩比,并降低了Lucene的速度。
调优搜索性能
使用Elasticsearch的主要原因是其支持通过数据进行搜索。用户应该能够快速地找到所需要查找的信息。搜索性能取决于很多因素:
- 如果可能的话,使用过滤语境而不是查询语境。一个查询子句用于回答“这个文档如何与查询子句匹配?” ,过滤子句用于回答“这个文档是否匹配这个过滤子句?”。Elasticsearch只需要回答“是”或“否”。它不需要计算过滤子句的相关性得分,并且可以高速缓存过滤结果。有关详细信息,请参阅查询和过滤语境。
增加刷新间隔。正如在调优索引性能部分所提到的,Elasticsearch每次刷新时都会创建一个新的段。增加刷新间隔将有助于减少段数并降低搜索的IO成本。而且一旦发生刷新并且数据改变,缓存将无效。增加刷新间隔可以使Elasticsearch更高效地利用缓存。
增加副本数量。Elasticsearch可以在主分片或副本分片上执行搜索。拥有的副本越多,搜索中涉及的节点就越多。
从上图可以看出,搜索吞吐量几乎与副本数量成线性关系。注意在这个测试中,测试集群有足够的数据节点来确保每个分片都有一个独占节点,如果这个条件不能满足,搜索吞吐量就不会那么好。
- 尝试不同的分片数量。“应该为索引设置多少分片?” 这可能是最常见的问题。不幸的是,所有场景都没有标准的数字,这完全取决于当时的实际情况。
太小的分片数量会使搜索无法扩展。例如,如果分片数量设置为1,则索引中的所有文档都将存储在一个分片中。对于每个搜索,只能涉及一个节点。如果有很多文件,那是很耗费时间的。另一方面,创建索引的分片太多也会对性能造成危害,因为Elasticsearch需要在所有分片上运行查询,除非在请求中指定了路由键,然后将所有返回的结果一起取出并合并。
根据经验来说,如果索引小于1G,可以将分片数设置为1。对于大多数情况,可以将分片数保留为默认值5,但是如果分片大小超过30GB,应该增加分片数量将索引分成更多的分片。创建索引后,分片数量不能更改,但是可以创建新的索引并使用reindex API转移数据。
在这里测试了一个拥有1亿个文档,大约150GB的索引,使用了100个线程发送搜索请求。
从上图中可以看出,优化后的分片数量为11个。开始的时候,搜索吞吐量增加(响应时间减少),但随着分片数量的增加,搜索吞吐量减少(响应时间增加)。
请注意,在这个测试中,就像在副本数量测试中一样,每个分片都有一个独占节点。如果这个条件不能满足,搜索吞吐量就不会像上图所示那样好。
在这种情况下,建议尝试一个小于优化值的分片数,因为如果使用大分片数,并且使每个分片都有一个独占数据节点,那么就需要很多个节点。
- 节点查询缓存。 节点查询缓存只缓存正在过滤语境中使用的查询。与查询子句不同,过滤子句是“是”或“否”的问题。Elasticsearch使用一个位设置机制来缓存过滤结果,以便后面的查询使用相同的过滤条件进行加速。请注意,只有保存超过10,000个文档的分段(或文档总数的3%,以较大者为准)才能启用节点查询缓存。有关缓存的更多详细信息,请参阅关于缓存。
可以使用下面的请求来检验一个节点查询缓存是否有效。
GET index_name/_stats?filter_path=indices.**.query_cache
{
"indices": {
"index_name": {
"primaries": {
"query_cache": {
"memory_size_in_bytes": 46004616,
"total_count": 1588886,
"hit_count": 515001,
"miss_count": 1073885,
"cache_size": 630,
"cache_count": 630,
"evictions": 0
}
},
"total": {
"query_cache": {
"memory_size_in_bytes": 46004616,
"total_count": 1588886,
"hit_count": 515001,
"miss_count": 1073885,
"cache_size": 630,
"cache_count": 630,
"evictions": 0
}
}
}
}
}
分片查询缓存。如果大多数查询是聚合查询,应该看看分片查询缓存,它可以缓存聚合结果,以便Elasticsearch直接以低成本提供请求。有几件事情需要注意:
o 设置“size”:0。分片查询缓存只缓存聚合结果和建议。它不会缓存操作过程,因此如果将大小设置为非零,则无法从缓存中获益。
o 有效负载JSON必须相同。分片查询缓存使用JSON主体作为缓存键,因此需要确保JSON主体不会更改,并确保JSON主体中的键具有相同的顺序。
o Round日期时间。不要直接在查询中使用像Date.now这样的变量,Round它。否则,每个请求都会有不同的有效负载主体,从而导致缓存始终无效。建议Round日期时间为小时或天,以便更有效地利用缓存。
可以使用下面的请求来检验分片查询缓存是否有效果。
GET index_name/_stats?filter_path=indices.**.request_cache
{
"indices": {
"index_name": {
"primaries": {
"request_cache": {
"memory_size_in_bytes": 0,
"evictions": 0,
"hit_count": 541,
"miss_count": 514098
}
},
"total": {
"request_cache": {
"memory_size_in_bytes": 0,
"evictions": 0,
"hit_count": 982,
"miss_count": 947321
}
}
}
}
}
仅检索必要的字段。如果文档很大,并且只需要几个字段,请使用 stored_fields 检索所需要的字段而不是所有字段。
避免搜索停用词。诸如“a”和“the”这样的停用词可能导致查询命中结果计数爆炸。设想有一百万个文件,搜索“fox”可能会返回几十个结果,但搜索“the fox”可能会返回索引中的所有文件,因为“the”出现在几乎所有的文件中。Elasticsearch需要对所有命中的结果进行评分和排序,导致像“the fox”这样的查询减慢整个系统。可以使用停止标记过滤来删除停用词,或使用“和”运算符将查询从“the fox”更改为“the AND fox”,以获得更精确的结果。
如果某些词在索引中经常使用,但不在默认停用词列表中,则可以使用截止频率来动态处理它们。
如果不关心文档返回的顺序,则按_doc排序。Elasticsearch使用“_score”字段按默认分数排序。如果不关心顺序,可以使用“sort”:“_doc”让Elasticsearch按索引顺序返回。
避免使用脚本查询来计算不固定的匹配。在索引时存储计算的字段。例如,有一个包含大量用户信息的索引,需要查询以“1234”开头的所有用户。或许想运行一个脚本查询,如“source”:“doc [‘num’].value.startsWith(’1234’)。” 这个查询是非常耗费资源的,并且减慢整个系统。索引时考虑添加一个名为“num_prefix”的字段,然后只需要查询“name_prefix”:“1234”。
避免通配符查询。
运行性能测试
对于每一次改变,都需要运行性能测试来验证变更是否适用。因为Elasticsearch是一个restful服务(基于RESTful web接口),所以可以使用诸如Rally,Apache Jmeter和Gatling等工具来运行性能测试。因为Pronto团队需要在每种类型的机器和Elasticsearch版本上运行大量的基准测试,而且需要在许多Elasticsearch集群上运行Elasticsearch配置参数组合的性能测试,所以这些工具并不能满足需求。
Pronto团队构建了基于Gatling的在线性能分析服务 ,帮助客户和我们运行性能测试并进行回归。该服务提供的功能使我们能够:
- 轻松添加/编辑测试。用户可以根据自己的输入查询或文档结构生成测试,而无需具有Gatling或Scala知识。
- 按顺序运行多个测试,无需人工干预。它可以检查状态并在每次测试之前/之后更改Elasticsearch设置。
- 帮助用户比较和分析测试结果分析。测试期间的测试结果和集群统计信息将保留下来,并可以通过预定义的Kibana可视化进行分析。
- 从命令行或Web UI运行测试。Rest API还提供了与其它系统的集成功能。
下图是架构
用户可以查看每个测试的Gatling报告,并查看Kibana预定义的可视化图像,以便进一步分析和比较,如下图所示。
总结
本文概述了索引/分片/副本设计以及在设计Elasticsearch集群时应该考虑的一些其它配置,以满足摄取和搜索性能的高期望。它还说明了Pronto团队如何在战略上帮助客户进行初始规模调整,索引设计和调优以及性能测试。截至今天,Pronto团队已经帮助包括订单管理系统(OMS)和搜索引擎优化(SEO)在内的众多客户实现了苛刻的性能目标,从而为eBay的关键业务做出了贡献。
Elasticsearch的性能取决于很多因素,包括文档结构,文档大小,索引设置/映射,请求率,数据集的大小,查询命中计数等等。针对一种情况的性能优化推荐不一定适用于另一种情况。彻底地测试性能,收集遥测数据,根据工作负载调整配置以及优化以满足性能要求非常重要。