聊聊 Elasticsearch 的查询毛刺 - easyice的个人空间 - OSCHINA

标签: | 发表时间:2020-08-04 08:00 | 作者:
出处:https://my.oschina.net

如果业务对查询延迟很敏感,Elasticsearch 查询延迟中的毛刺现象就是比较困扰的一类问题,由于出现毛刺的时间点已经过去,无法稳定复现,对于根因的分析比较困难,无法用系统化调试的思想,从现象出发逐步推理,定位问题,能做的通常就是看一下监控系统对应时间点的指标情况,而在 es 中,导致查询延迟发生波动的因素非常多,今天我们来列举一下可能的因素,并尝试用对应的方法来定位和解决他们。

通常一个系统中会有多种不同的查询同时存在,他们本身正常的查询延迟就可能存较大差异,因此即使系统在理想状态下,查询延迟的曲线也可能存在较大波动,特别是查询条件不固定,某些查询本身就耗时较长。我们只讨论一个特定的查询语句在某个时刻产生了较大延迟,即这个查询语句正常不应该耗时那么久。

另外 es 和 lucene 层面的查询缓存只是一种优化,查询缓存本身并不能保证查询延迟,因此不在本文讨论范畴。

GC 的影响

查询延迟受 GC 的影响是常见因素之一,一个查询被转发的相关分片,任意节点产生一个长时间的 GC 都会导致整个查询耗时变长。

定位方式:
查看对应时间点的节点 GC 指标,参考 kibana 或 gc log

解决方式:
堆内存不足可能的因素比较多,例如配置的 JVM内存较小,open 的索引过多,导致 FST 占用空间过大(未开启 offheap 的情况下),聚合占用了大量内存,netty 层占用大量内存,以及 cache 占用的内存等,主要是根据自己的业务特点,找到内存被谁占用了,然后合理规划JVM 内存空间。可以通过 REST API 或 MAT 分析内存,参考命令:

      curl-sXGET"http://localhost:9200/_cat/nodes?h=name,port,segments.memory,segments.index_writer_memory,segments.version_map_memory,segments.fixed_bitset_memory,fielddata.memory_size,query_cache.memory_size,request_cache.memory_size&v"        

HBase 通过 offheap 的方式降低 JVM 占用,来避免 FGC,es 将 FST offheap 后也大幅降低了 JVM 占用情况,不过 FST offheap 之后有可能会被系统清理,再次查询 FST 就会发生 io,也会造成查询延迟不稳定,不过这种概率非常小。而在 es 中聚合,scroll等操作都可能导致 JVM 被大幅占用,增加了不确定性。

系统 cache 失效

查询,以及聚合,需要访问磁盘上不同的文件,es 建议为系统 cache 保留一半的物理内存空间,当系统 cache 失效,发生磁盘 io,对查询延迟产生明显的影响。pagecache 什么时候会失效?使用 pagecache 的地方很多,linux 默认会缓存绝大部分的文件读写,例如查询,写日志,入库写 segment 文件,merge 时读写的文件,以及es 所在节点部署的其他的程序、脚本文件执行的对 io 上面的操作等都会抢占 pagecache。linux 按一定策略和阈值来清理 pagecache,应用层无法控制哪些文件不被清理。

因此我们需要了解一个查询语句在 io 上的需求,主要是以下两个问题:

  • 查询过程需要实时读取哪些文件?

  • 一次查询需要几次 io?读取多少字节?消耗多少时间?

查询过程需要实时读取哪些文件

es 中的查询是一个复杂的过程,不同的查询类型需要访问不同的 lucene 文件,我将常见类型的查询可能访问的文件整理如下:

真正查询过程中,并非所有文件都会实时读取,有些文件已经在 open 索引的时候读取完毕常驻内存,有些元信息文件也是在 open 的时候解析一次。为了验证搜索过程实际访问的文件与预期是否一致,我写了一个 systemtap 脚本来 hook 系统调用的 read 及 pread 函数,并把调用情况打印出来,验证过程样本数据使用 geonames 索引,为了便于演示,将索引 forcemerge 为单个分段,并将 store 设置为 niofs。

仅查询,不取回
分布式搜索由两阶段组成,当请求中 size=0时,只执行查询阶段,不需要取回。因此 term 查询或 match 查询,因此查询过程一般只需要用到倒排索引,因此,如下类型的查询:

      _search?size=0        
{
"query": {
"match": {
"name": {
"query":"Farasi"
}
}
}
}

只需要读取 tim 文件。因为tip 是在内存常驻的,而 size=0的时候只需要返回 hit 数量,es 在实现的时候有一个提前终止的优化,直接从 tim 中取 docFreq 作为 hit,不需要访问 postings list。

但是当查询含有 post_filter ,自定义的terminate_after等情况时,不会走提前终止的优化过程。再者就是类似如下的多个查询条件时,lucene 需要对每个字段的查询结果做交并集,这就需要拿到 postings list才行:

      _search?size=0        
{
"query": {
"bool": {
"must": {"match": {"name":"Farasi"}},
"must_not": {"match": {"feature_code":"CMP"}}
}
}
}

因此会读取 .doc 文件:

当 size!=0 时,term 查询和 match 查询需要读取的文件不一样,因此下面单独讨论。

term查询,加取回
带上 fetch 阶段后,原来查询过程需要访问的文件不变,fetch 过程需要从 stored fields 中取,因为 _source 字段本身就是存储到 stored fields 中的。

      _search?size=1        
{
"query": {
"term": {
"country_code.raw": {
"value":"CO"
}
}
}
}

因此,需要相比仅查询的过程,还需要多访问 fdt,fdx文件。

match查询,加取回

match 查询由于需要计算评分,需要使用 Norms 信息,因此在 term 查询加取回的基础上还要多访问 Norms 文件

      _search?size=10        
{
"query": {
"match": {
"name": {
"query":"Farasi"
}
}
}
}

需要读取 Norms中的 nvd 文件:

数值类型查询
数值类型的字段使用 BKD-tree 建立索引,不会存储到倒排,因此查询过程需要读取 Point Value。取回过程与 term 查询相同。

      _search?size=0        
{
"query": {
"range": {
"geonameid": {
"gte":3682501,
"lte":3682504
}
}
}
}

查询过程只需要读取 dim 文件:


聚合

对于 metric 和 bucket 聚合,需要访问的文件相同,当 size=0时,只需要读取 dvd 文件。

      _search?size=0        
{
"aggs": {
"name": {
"terms": {"field":"name.raw"}
}
}
}

以下为部分截图,省略了后面的3万多条记录。


GET API

使用 GET API获取单条文档时,与 fetch 过程并不相同

      _doc/IrOMznAB5onF36XmwY4W        

以下结果想必会出乎意料:

_id 字段是被建立了索引的。这个 _id 是 es 层面概念,并非 lucene 倒排表里的 docid,因此根据 _id 单条 GET 的时候,需要先执行一次 lucene 查询(termsEnum.seekExact)来获取 lucene 中数字类型的 docid,查询过程自然需要查找 FST,读取 tim。

然后根据这个 docid 去 stored field 中读取 _source,因此需要读取 fdx,fdt 文件

最后,GET API 除了返回 _source 之外,还要返回该文档的元信息字段,包括:_version、_seq_no、_primary_term,这三个字段是保存在 docvalue 中的,因此需要读取 dvd 文件。

两阶段的查询过程中,query 阶段返回的 docid 是 lucene 内部数字类型的 id,fetch 的时候可以直接获取了。

查询需要几次 io?

在了解了查询会涉及到动态读取哪些文件之后,我们还需要知道在 io 上需要多大的代价,为了验证实际搜索过程的 io 情况,我们再编写一个新的 systemtap 脚本,将查询过程对每个文件读取的字节数,耗费时间等信息打印出来:

为了观测到查询在 io 上的影响,我们需要排除一些干扰因素:

无 pagecache 的测试:

  • 用 vmtouch 驱逐该索引在 pagecache 的缓存

  • 执行 _cache/clear 清理 es 层面的缓存

有 pagecache 的测试:

  • 执行 _cache/clear 清理 es 层面的缓存

  • 使用相同查询执行第2次

此外,系统环境干净,单节点,没有写入操作,没有其他无关进程影响。然后对几种常见类型的查询进行统计,结果如下表:

你可能不想看这种明细表,我来总结一下:多数查询所需的 read 调用次数及需要读取的数据量都不大,但是有两种情况需要较多的 io,因为他们都与数据量有关:

  • 聚合的时候,所需 io 取决于参与聚合的数据量。

  • 数值类型的 range query,所需 io 取决于命中的结果集大小。

业务对于上述两种类型的查询要特别关注。可以考虑设法优化,例如聚合前尽量通过查询条件缩小参与聚合的结果集,以及 range 查询的时候尽量缩小范围。其次还有两种情况需要的 io 相对较多,但比上面的要少一个数量级:

  • 多条件查询时,需要对多个字段的结果集做交并,结果集较大时,需要读取doc 文件的次数较多,本例中有几十次。

  • 深度翻页,要取决于要取回的数据量。因为单条 GET 那读那么多的文件,代价略大。

结论:在仅查询的场景下,访问 doc 和 dim 文件的次数可能会比较多,通常业务的查询语句都比较复杂,混合多种查询条件,io 量虽然是很大,但是当磁盘比较繁忙,而 page cache又未命中的情况下,查询延迟可能会比较大。 

FST offheap 后的影响

FST 的 offheap 通过 mmap tip 文件,让 FST 占用的内存空间从堆内转向 pagecache 来实现,既然在 pagecache 中,当被 pagecache 驱逐后,就会产生 io,产生明显的查询延迟。简单来说就是这种 offheap 的效果有可能导致 FST 不在 heap 了。

虽然 tip 被逐出 pagecache 的几率很小,但是,随着集群规模变大,偶然因素就会变成必现情况。

解决方式:自研一种 offheap也很简单, FST 的查找过程就是在数组里跳来跳去的找,所以比 HBase 的 offheap 简单很多。如果不想改代码,解决方式参考上一条。

如何观测查询在 io 上的延迟

当生产环境查询延迟产生毛刺,我们想要确定这个较高的延迟是否受 io 的影响导致,但是很不幸,目前还很难观测到,即使查询延迟毛刺发生在当下,Profile API 也无法给出在 io 上的耗时(通过 systemtap 脚本中为 pread 过程注入延迟,发现读取 tim 文件的耗时在 Profile结果里体现不出来的,fdt,fdx文件的读取延迟体现在create_weight字段,dim 文件的读取延迟体现在build_scorer字段等,难以界定问题)发现如果想要观测到这些指标,需要在 lucene 层面做出一些改进,然后在 Profile API 和 Slow log 中展示出来,而且还仅限于使用 niofs 的情况下才能拿到指标。

既然搜索需要不可控次数的 io,搜索延迟就注定是无法保障的。例如:

  • 索引写入会占用io,虽然不多,但是存在瞬间刷盘的时刻

  • 如果有 update,会比 index 操作占用更多 io util

  • 如果存在巨大的 shard,查询可能会占用较大 io util

  • 单个节点的多个磁盘之间可能是负载不均的。

  • merge,recovery,甚至更新集群状态,都需要 io

磁盘 io 导致的问题,就用 ssd或内存来解决,HDFS 里将存储类型分为 RAM,SSD,DISK 等几种类型,再根据不同的存储策略控制副本在不同存储介质的分布,在 es 里也是类似的机制:

  • 第一种是索引级别的冷热分离,用 node.attr 配合索引级别的 allocation策略来实现,让热索引存储到 ssd,索引的写入和查询过程都不变。

  • 第二种可以考虑让主分片放到 RAM,例如 /dev/shm,副分片放到 ssd 或普通磁盘,可以通过 awareness来实现,先为部分节点配置纯 RAM 存储,配置为hot,其他节点使用普通存储,配置为cool,awareness会保证分片的不同副本放到不同区域,类似 hdfs 的机架感知。但是由于内存数据容易丢失,最好在写入过程中将 wait_for_active_shards设置为 all,读取的时候通过 preference来控制优先读取 hot 节点。如果你就想要一个低延迟的搜索,把lucene 文件都加载到内存吧!

还有一种最简单的是 vmtouch 等方式让 lucene 文件被系统 cache住,但什么时候被清理不可预期。pagecache 命中率可以用 cachestat来查看,并且对 mmapfs 有效。

Search Queue 堆积

如果客户端发送的查询并发过高,导致 search 线程池占满,查询请求进入队列等待,也会导致查询过程产生较高延迟。

定位方式:
kinaba 中暂时还没有关于线程池的指标,需要自己监控

解决方式:
控制好客户端的查询并发,客户端的一个查询请求如果涉及到某个数据节点的三个分片,就会在该节点占用3个 search 线程。目前指标上还看不到请求排队花费的时间。

题外话:

es 使用 max_concurrent_shard_requests 参数来控制单个查询请求在某个节点上的查询并发,避免单个请求把整个集群的查询资源占满,协调节点在构建完本次查询请求涉及的目的 shard 列表后,根据 max_concurrent_shard_requests 进行并发控制,超过并发的会放入到队列中,不过这个队列并不占用 search queue,因此即使并发受限,其查询延迟不会受此因素影响。

总结

Lucene 并不是为低延迟而设计的系统,查询毛刺主要受 GC 和 IO 的影响,GC 层面在于合理的规划JVM内存,避免频繁 GC 和 FGC,IO 层面的可以考虑使用 SSD,RAMDISK 或预留足够的 pagecache来解决。

特别感谢:陆徐刚@蚂蚁,以及军义。


相关 [elasticsearch easyice 个人] 推荐:

聊聊 Elasticsearch 的查询毛刺 - easyice的个人空间 - OSCHINA

- -
如果业务对查询延迟很敏感,Elasticsearch 查询延迟中的毛刺现象就是比较困扰的一类问题,由于出现毛刺的时间点已经过去,无法稳定复现,对于根因的分析比较困难,无法用系统化调试的思想,从现象出发逐步推理,定位问题,能做的通常就是看一下监控系统对应时间点的指标情况,而在 es 中,导致查询延迟发生波动的因素非常多,今天我们来列举一下可能的因素,并尝试用对应的方法来定位和解决他们.

elasticsearch 优化写入速度 | easyice

- -
translog flush 间隔调整. 索引刷新间隔调整: refresh_interval. bulk 线程池和队列大小. 调整字段 Mappings. 对于 Analyzed 的字段禁用 Norms. index_options 设置. 基于版本: 2.x – 5.x. 在 es 的默认设置,是综合考虑数据可靠性,搜索实时性,写入速度等因素的,当你离开默认设置,追求极致的写入速度时,很多是以牺牲可靠性和搜索实时性为代价的.有时候,业务上对两者要求并不高,反而对写入速度要求很高,例如在我的场景中,要求每秒200w 条的平均写入速度,每条500字节左右.

Elasticsearch调优篇-慢查询分析笔记 - 个人文章 - SegmentFault 思否

- -
elasticsearch提供了非常灵活的搜索条件给我们使用,在使用复杂表达式的同时,如果使用不当,可能也会为我们带来了潜在的风险,因为影响查询性能的因素很多很多,这篇笔记主要记录一下慢查询可能的原因,及其优化的方向. 最直观的现象就是提供查询的服务响应超时. 我们有时候写查询,为了图方遍,经常使用通配符*来查询,这有可能会匹配到多个索引,由于索引下分片太多,超过了集群中的核心数.

[译]elasticsearch mapping

- - an74520的专栏
es的mapping设置很关键,mapping设置不到位可能导致索引重建. 请看下面各个类型介绍^_^. 每一个JSON字段可以被映射到一个特定的核心类型. JSON本身已经为我们提供了一些输入,支持 string,  integer/ long,  float/ double,  boolean, and  null..

Elasticsearch as Database - taowen - SegmentFault

- -
【北京上地】滴滴出行基础平台部招聘 Elasticsearch 与 Mysql binlog databus 开发工程师. 内推简历投递给: [email protected]. 推销Elasticsearch. 时间序列数据库的秘密(1)—— 介绍. 时间序列数据库的秘密(2)——索引.

ElasticSearch 2 的节点调优(ElasticSearch性能)

- - 行业应用 - ITeye博客
一个ElasticSearch集群需要多少个节点很难用一种明确的方式回答,但是,我们可以将问题细化成一下几个,以便帮助我们更好的了解,如何去设计ElasticSearch节点的数目:. 打算建立多少索引,支持多少应用. elasticsearch版本: elasticsearch-2.x. 需要回答的问题远不止以上这些,但是第五个问题往往是容易被我们忽视的,因为单个ElasticSearch集群有能力支持多索引,也就能支持多个不同应用的使用.

Elasticsearch:使用 Elasticsearch 进行语义搜索

- - 掘金 后端
在数字时代,搜索引擎在通过浏览互联网上的大量可用信息来检索数据方面发挥着重要作用. 此方法涉及用户在搜索栏中输入特定术语或短语,期望搜索引擎返回与这些确切关键字匹配的结果. 虽然关键字搜索对于简化信息检索非常有价值,但它也有其局限性. 主要缺点之一在于它对词汇匹配的依赖. 关键字搜索将查询中的每个单词视为独立的实体,通常会导致结果可能与用户的意图不完全一致.

elasticsearch的javaAPI之query

- - CSDN博客云计算推荐文章
elasticsearch的javaAPI之query API. the Search API允许执行一个搜索查询,返回一个与查询匹配的结果(hits). 它可以在跨一个或多个index上执行, 或者一个或多个types. 查询可以使用提供的 query Java API 或filter Java API.

Elasticsearch基础教程

- - 开源软件 - ITeye博客
转自:http://blog.csdn.net/cnweike/article/details/33736429.     Elasticsearch有几个核心概念. 从一开始理解这些概念会对整个学习过程有莫大的帮助.     接近实时(NRT).         Elasticsearch是一个接近实时的搜索平台.

ElasticSearch索引优化

- - 行业应用 - ITeye博客
ES索引的过程到相对Lucene的索引过程多了分布式数据的扩展,而这ES主要是用tranlog进行各节点之间的数据平衡. 所以从上我可以通过索引的settings进行第一优化:. 这两个参数第一是到tranlog数据达到多少条进行平衡,默认为5000,而这个过程相对而言是比较浪费时间和资源的. 所以我们可以将这个值调大一些还是设为-1关闭,进而手动进行tranlog平衡.