这份​Elasticsearch 工作笔记,值得收藏

2022-03-14 21:56:11 浏览数 (3)

从事Elasticsearch云产品的研发已经四年多了,在服务公有云客户的过程中也遇到了各种各样的使用方式以及问题,本文就把过去几年记录的一些问题和解决办法进行归类和总结,常读常新。

目录:

  • 一、es内核bug类的问题记录
  • 二、使用方式类的问题记录
  • 三、优化类的问题记录
  • 四、原理咨询类的问题记录

一、es内核bug类的问题记录

1 . 集群升级到7.5版本后自定义的normalizer无法使用了

es内核的bug,7.0版本对自定义analyzer这部分的代码进行了重构,导致所有的自定义normalizer都无法正常使用。

相关issue: https://github.com/elastic/elasticsearch/issues/48650

相关PR:https://github.com/elastic/elasticsearch/pull/48866

2 . 使用_search/template API查询时返回结果总量不准

在_search/template API的处理逻辑中,虽然rest_total_hits_as_int设置为了true, trackTotalHitsUpTo值却没有被设置,因此只能获取到最多为10000的total hits。

相关issue: https://github.com/elastic/elasticsearch/issues/52801

相关PR:https://github.com/elastic/elasticsearch/pull/53155

3 . 处理字符串类型数据的ingest processor, 不支持传入的field字段值为数组

对Lowercase Processors、Uppercase Processors、Trim Processors等处理字符串类型数据的ingest processor, 都支持要处理的字段类型为数组类型:

相关issue: https://github.com/elastic/elasticsearch/issues/51087

相关PR:https://github.com/elastic/elasticsearch/pull/53343

4 . reindex api在max_docs参数小于slices时,会报错max_docs为0

调用reindex api,当max_docs参数<slices时,会报错max_docs为0,实际上是因为没有提前校验max_docs是否<slices,导致max_docs被设置为0。

相关issue: https://github.com/elastic/elasticsearch/issues/52786

相关PR:https://github.com/elastic/elasticsearch/pull/54901

5 . ingest pipeline simulate API 在传入的docs参数是空列表时,没有响应

在调用_ingest/pipeline/_simulate API时,如果传入的docs参数是空列表,则什么结果都不会返回。

Bug产生的原因是,在异步请求的ActionListener中没有对docs参数进行判空,导致始终没有响应给客户端。

相关issue: https://github.com/elastic/elasticsearch/issues/52833

相关PR:https://github.com/elastic/elasticsearch/pull/52937

6 . Rename inegst processor对于设置ignore_missing参数无效

当使用template snippets时,如果取到的字段不存在,此时如果设置了ignore_missing, 仍然会报错。

相关issue: https://github.com/elastic/elasticsearch/issues/74241

相关PR:https://github.com/elastic/elasticsearch/pull/74248

7 . ILM中的Shrink Action,如果设置的目标分片数不合适,也就是不是原索引分片数的因子时,Shrink Action会卡住

在Shrink Action中增加校验,如果设置的目标分片数不合适,就提前中断ILM的执行。

相关issue: https://github.com/elastic/elasticsearch/issues/72724

相关PR:https://github.com/elastic/elasticsearch/pull/74219

8 . Get Snapshot API如果指定了ignore_unavailable为true时,会把当前正在执行的所有快照都返回

相关issue: https://github.com/elastic/elasticsearch/issues/68090

相关PR:https://github.com/elastic/elasticsearch/pull/68091

9 . 在ILM中使用的AllocationDeciders,会忽略掉用户自定义的cluster level的routing allocation 配置,导致在某些场景下需要移动分片时把分片移动了到了错误的节点上。

相关issue: https://github.com/elastic/elasticsearch/issues/64529

相关PR:https://github.com/elastic/elasticsearch/pull/65037

10 . 在执行bulk写入时,如果body里指定了pipeline, 执行结果是错误的

在bulk写入时,如果有的请求带有ingest pipeline, 有的没有,那么执行结果就是完全乱序的,也就是文档内容和指定的docId对应不上。

相关issue: https://github.com/elastic/elasticsearch/issues/60437

相关PR:https://github.com/elastic/elasticsearch/pull/60818、

二、使用方式类的问题记录

11 . 二进制字段如何设置mapping?

代码语言:txt复制
	"mapping": {
	  "type": "binary",
	  "doc_values": "false",
	  "norms": "false",
	  "fielddata": "false",
	  "store": "false"
	}

12 . 对ip字段进行聚合,希望聚合结果返回每个ip的一条数据,该怎么实现?

先使用terms聚合,再使用top_hits子聚合能达到目的,使用 collapse 配合 inner_hits也可以实现

13 . 有一张消费明细表(一个人有多条消费记录),首先想计算出一个人的总消费金额,然后想得到总消费大于500美金的所有人数,query DSL该怎么写?

代码语言:txt复制
	{
    "aggs":{
        "one":{
            "terms":{
                "field":"mobile_nbr"
            },
            "aggs":{
                "x":{
                    "sum":{
                        "field":"trans_amt"
                    }
                },
                "sum_bucket_filter":{
                    "bucket_selector":{
                        "buckets_path":{
                            "totalSum":"x"
                        },
                        "script":"params.totalSum > 500"
                    }
                }
            }
        },
        "stats_buckets":{
            "stats_bucket":{
                "buckets_path":"one.x"
            }
        }
    }
}

14 . 使用Logstash迁移ES的数据,简单的配置文件

代码语言:txt复制
	input {
	    elasticsearch {
	        hosts => ["http://x.x.x.x:9200"]
	        index => "*"
	        docinfo => true
	    }
	}
	output {
	    elasticsearch {
	        hosts => ["http://x.x.x.x:9200"]
	        index => "%{[@metadata][_index]}"
	    }
	}

15 . 一个有用的脚本,用于追加netsted objects

代码语言:txt复制
	{
	  "script": {
	    "lang": "painless",
	    "inline": " if (ctx._source.redu!=null) {ctx._source.redu.add(params.object);} else {Object[] temp= new Object[]{params.object};ctx._source.redu= temp;}",
	    "params": {
	      "object": {
	        "visit_time": "2020-03-15 22:00:00",
	        "visit_cnt": 1000,
	        "visit_scene": 2
	      }
	    }
	  }
	}

16 . mustache小胡子脚本,用于把一个数组类型的字段复制到另外一个字段,高版本7.x可以使用set processor的copy_from, 低版本不支持copy_from

代码语言:txt复制
	{
  "pipeline": {
    "processors": [
      {
        "set": {
          "field": "a",
          "value": "{{#b}}{{.}},{{/b}}"
        }
      },
      {
        "split": {
          "field": "a",
          "separator": ","
        }
      },
      {
        "convert": {
          "field": "a",
          "type": "integer"
        }
      }
    ]
  },
  "docs": [
    {
      "_source": {
        "b": [
          1,
          2
        ]
      }
    }
  ]
}

17 . 查询时对结果进行排序,如果文档的分值相同,需要返回顺序是随机的,可以通过script来进行处理

代码语言:txt复制
	{
      "_script": {
        "script": "Math.random()",
        "type": "number",
        "order": "asc"
      }
    }

18 . 取消reindex任务

代码语言:txt复制
	列出运行中的任务
	_tasks
	_tasks?nodes=nodeId1,nodeId2
	取消任务
	_tasks/node_id:task_id/_cancel
	取消重建索引任务
	_tasks/_cancel?nodes=nodeId1,nodeId2&action=*reindex

19 . 查看阻塞在队列中的索引

代码语言:txt复制
	GET  _tasks?pretty&detailed  | grep description | awk -F 'index' '{print $2}' | sort | uniq -c | sort -n

20 . cancel掉所有存量的查询,释放内存

代码语言:txt复制
	POST _tasks/_cancel?actions='indices:data/read/search*'

三、优化类的问题记录

21 . 在需要批量拉取聚合结果时,可以使用index sorting composite 聚合来代替term 聚合,composite聚合可以根据排序优化聚合提前结束并且支持分页。

22 . 系统高阶内存不足导致的节点离线

线上某个写入量比较大的集群,不定时的会出现某个节点离线又加回集群的情况。经过定位发现是虚拟机高阶内存不足,导致网卡收发包异常。

es和Lucene 会大量使用堆外内存,在应用层面的内存分配都是申请的低阶内存(0阶、1阶),会将高阶内存(3阶及以上)逐步拆分用掉。而系统内核层面的网卡驱动会优先分配高阶内存,如果高阶内存不足会再尝试分配低阶内存,这个过程会有一定延时,可能导致节点短暂收发包异常,短暂脱离集群。当应用层面将高阶内存拆分申请完毕后,就会出现这一高阶内存不足的现象。系统会有内存整理的过程但是不会那么及时。

解决办法是通过设定系统参数预留系统内存:

代码语言:txt复制
	echo %2的系统总内存(单位kb) > /proc/sys/vm/min_free_kbytes
	
	echo 1 > /proc/sys/vm/compact_memory

23 . es节点上TCP全连接队列参数设置,防止节点数大于100的这种的集群中,节点异常重启时全连接队列在启动瞬间打满,造成节点hung住,导致集群响应迟滞:

代码语言:txt复制
	echo "net.ipv4.tcp_abort_on_overflow = 1" >>/etc/sysctl.conf
	echo "net.core.somaxconn = 2048" >>/etc/sysctl.conf

24 . 查询时需要返回文档原文中的几个字段,从行存改为从列存读取,高压力查询场景性能可以提升 50%。从行存读取涉及到解压的开销,列存则可直接取对应字段的部分block,性能会更高:

查询body 中的取source 部分:

代码语言:txt复制
	"_source": {
	    "includes": [
	      "a",
	      "b",
	      "c"
	    ]
	  }	

调整为从列存读取字段:

代码语言:txt复制
	  "docvalue_fields":  [
	      "a",
	      "b",
	      "c"
	    ],
	  "stored_fields": "_none_", // 关闭行存读取

25 . 部署es时磁盘挂载时的可选配置

代码语言:txt复制
* noatime:禁止记录访问时间戳,提高文件系统读写性能
* data=writeback: 不记录data journal,提高文件系统写入性能
* barrier=0:barrier保证journal先于data刷到磁盘,上面关闭了journal,这里的barrier也就没必要开启了
* nobh:关闭buffer_head,防止内核打断大块数据的IO操作

四、原理咨询类的问题记录

26 . 为了满足查询时延,是不是索引的分片数设置的越少越好?

如果单次搜索的时延可以满足业务上的要求,可以设置索引为1分片多副本。 如果时延过高,可以增加shard数量,代价是每次搜索的并发两增大,带来的额外开销更大,因而集群能支撑的峰值QPS可能会降低。 原则上,在满足搜索时延的前提下,划分尽量少的分片数。

另外有一种场景划分更多的分片数是合理的,那就是集群大多数搜索都会用到某个字段做过滤,比如城市id。 这个时候,可以用该字段做为routing_key,将相关联的数据route到某个或某几个(如果用到routing partition)shard。 适当多划分一些分片,可以让单个分片上的数据集较小,搜素速度快,同时因为搜索不会hit所有的分片,规避了划分过多的分片带来的并发过高,以及需要汇总的数据过多引起的性能问题。

27 . filter缓存的策略是怎么样的?

es会基于使用频次自动缓存查询。如果一个非评分查询在最近的 256 次查询中被使用过(次数取决于查询类型),那么这个查询就会作为缓存的候选。但是,并不是所有的segment都能保证缓存 bitset 。只有那些文档数量超过 10000 (或超过总文档数量的 3% )的segment才会缓存 bitset 。因为小的片段可以很快的进行搜索和合并。一旦缓存了,非评分计算的 bitset 会一直驻留在缓存中直到它被剔除。剔除规则是基于 LRU 的,一旦缓存满了,最近最少使用的过滤器会被剔除。

28 . bool查询是如何计算得到文档的分值的?

以should子句为例,先运行should子句中的两个查询,然后把子句查询返回的分值相加,相加得到的分值乘以匹配的查询子句的数量,再除以总的查询子句的数量得到最终的分值。

29 . 查询时的tie_breaker参数的作用是什么?

tie_breaker参数会让dis_max查询的行为更像是dis_max和bool的一种折中。它会通过下面的方式改变分值计算过程:

代码语言:txt复制
* 取得最佳匹配查询子句的score
* 将其它每个匹配的子句的分值乘以tie_breaker
* 将以上得到的分值进行累加并规范化通过tie_breaker参数,所有匹配的子句都会起作用,只不过最佳匹配子句的作用更大。
	

30 . min_score参数设置后,多次查询结果可能会不一致,因为查询primary shard和replica shard的结果可能不一致。

31 . 在plainless脚本中使用doc'field'取值和使用'_source'取值有什么不同?

使用doc'field'取值会把字段field的所有term都加载到内存(并且会被缓存),执行效率高,但是比较消耗内存,另外这种取值方式只能去简单类型的字段,不能对Object类型的字段取值;使用_source取值因为不会有缓存,所以每次都要把_source内容加载到内存并且解析,因此效率很低。推荐使doc'field'取值。

32 . scroll api里的scroll参数的作用是保持search context, 但是只需要设置为处理一个批次所需的时间即可。scroll时会在merge操作时依然保留merge前的old segments, 会带来存储上的开销以及需要更多文件描述符;search.max_open_scroll_context参数可以设置node上最大的context数量,默认无限制。scroll请求不会用到cache,因为使用cache在查询请求执行过程中会修改search context,会破坏掉scroll的context。

33 . es 5.6以后在search api中加入了pre filter shards 逻辑,当要查询的shards数量超过128并且查询可能会被重写为MatchNoneQuery时,会进行pre filter, 过滤掉shards,提高查询效率。在search时返回结果中的_shards.skipped表示了过滤掉了多少shard。

34 . es默认使用的用于打分的bm2.5相似度算法中,计算idf的部分,log(docCount 1/docFreq 0.5), docCount的值是所有包含要查询的field的文档数量;docFreq是所有包含field value的文档数量。

35 . es统计的索引大小是整个索引所占空间空间的大小,整个索引包括很多文件,比如tim词典,tip词典索引,pos位置信息,fdt存储字段信息(_source实际存储的文件),等等。es中"codec": "best_compression" ,是对fdt这个文件进行压缩,其他的是不会进行压缩的。

36 . 横向增加节点扩容时,不能搬迁已经close的索引到新的节点上, 需要先手动处理这种索引才可以。

37 . fielddata是在堆内存的,docvalues是在堆外内存的;docvalues默认对所有not_analyzed字段开启(index时生成),如果要对analyzed字段进行聚合,就要使用fielddata了(使用时把所有的数据全都加载进内存)。如果不需要对analyzed字段进行聚合,就可以降低堆内存的使用。

38 . ES 写入异常流程总结:

  • 如果请求在协调节点的路由阶段失败,则会等待集群状态更新,拿到更新后,进行重试,如果再次失败,则仍旧等集群状态更新,直至1分钟超时为止,超时后则进行整体请求失败处理
  • 在主分片写入过程中,写入是阻塞的;只有写入成功,才会发起写副本请求;如果主分片写失败,则整个请求被认为处理失败;如果有部分副本分片写失败,则整个请求被认为是处理成功的,会在结果中返回多少个分片成功,多少个分片失败;
  • 无论主分片还是副本分片,当写一个doc失败时,集群不会重试,而是关闭本地shard,然后向master汇报。

39 . ES写入流程存在的一些问题:

  • 副本分片写入过程重新写入数据,不能单纯复制数据,浪费计算能力,影响写入速度
  • 磁盘管理能力较差,对坏盘检查和容忍性比HDFS差不少;在配置多次盘路径的情况下,有一块坏盘就无法启动节点。

40 . ES在分布式上表现的一些特性:

  • 数据可靠性:通过分片副本和事务日志保障数据安全
  • 服务可用性:在可用性和一致性的取舍方面,默认ES更倾向于可用性,只要主分片可用即可执行写入操作
  • 一致性:弱一致性?最终一致性?只要主分片写入成功,数据就能被读取
  • 原子性:数据读写是原子操作,不会出现中间状态,但是bulk不是原子操作,不能用来实现事务
  • 扩展性:主分片和副本分片都可以承担读请求,分担系统负载

41. 腾讯云Elasticsearch有自研的熔断器,默认情况下当jvm old 区使用率超过85% ,拒绝写入;当jvm old 区使用率超过90% ,拒绝查询;日志报错有"pressure too high"字样; 详细介绍见https://cloud.tencent.com/document/product/845/56272。

42 . term聚合,在High Cardinality下,性能越来越差的原因是什么?

字段唯一值非常多,对该字段进行terms聚合时需要构建Global Ordinals(内部实现),对旧的索引只需构建一次也就是首次查询时构建一次,后续查询就可以直接使用缓存中的Global Ordinals; 而对持续写入的索引,每当底层segment发生变化时(有新数据写入导致产生新的Segment、Segment Merge), 就需要重新构建Global Ordinals,随着数据量的增大,字段的唯一值越来越多,构建Global Ordinals越来越慢,所以对持续写入的索引,聚合查询会越来越慢。

terms聚合查询使用的Global Ordinals是shard级别的,把字符串转为整型,目的是为了在聚合时降低内存的使用;最后再reduce阶段,也就是收集各个shard的聚合结果的时候并且汇总完毕后,再把整型转换为原始字符串。

当底层的Segment发生变化时,Global Ordinals就失效了,再次查询时就需要重新构建;默认的构建时机是search查询时,在6.x版本引入了eager_global_ordinals,把构建全局序数放在了index索引时,但是对index性能有一定影响。

43 . es使用了CMS垃圾回收器,默认情况下JVM堆内存young区和old区的大小是多少?

JVM堆内存参数中用于控制young区和old区大小的参数如下:

代码语言:txt复制
	* 最高优先级:  -XX:NewSize=1024m和-XX:MaxNewSize=1024m 
	* 次高优先级:  -Xmn1024m  (默认等效效果是:-XX:NewSize==-XX:MaxNewSize==1024m) 
	* 最低优先级:-XX:NewRatio=2 es用的是CMS垃圾回收器,所以young区的大小是JVM根据系统的配置计算得到的,newRatio默认虽然为2但是不起作用;除非显式的配置-Xmn或者-XX:NewSize, young区大小的计算公式为:const size_t preferred_max_new_size_unaligned =
    MIN2(max_heap/(NewRatio 1), ScaleForWordSize(young_gen_per_worker * parallel_gc_threads));

其中ScaleForWordSize大约为64M (64位机器) (机器cpu核数) 13 / 10

44 . update操作不一定会触发refresh, 如果update的doc_id已经是可以被searcher检索到的,比如已经存在于某个segment里,就无需refresh。 但是如果update的doc_id存在于index writter buffer里,还未refresh,典型的就是同一个bulk操作里写入了多个重复的id, 实时GET就会触发refresh。

45 . 一般1 TB的磁盘数据,需要 2- 5GB 左右的 FST内存开销,这个只是FST的开销(常驻内存),一般FST占用50%左右的堆内内存。如果查询和写入压力稍微大一点,32GB Heap,内存很容易成为瓶颈。

46 . zen2相比zen的优势?

  • mininum_master_nodes被移除,es自己决定哪些节点作为candidate master nodes;而6.x版本的zen协议,可能会因为该参数配置错误导致集群无法选主,另外在扩缩容节点时也需要调整该参数
  • 典型的主节点选举可以在1s内完成,相比6.x, es通过延迟几秒钟的时间再进行选举防止各种各样的配置错误,意味着有几秒钟的时间集群不可用
  • 增长和缩小集群变得更安全,更容易,并且错误配置导致数据丢失的机会变少了。
  • 点增加更多的记录状态的日志,帮助诊断无法加入集群或无法选举出主节点的原因

47 . 什么是adaptive replica selection?

默认情况下,ES的协调节点选择在处理查询请求时,对于有多个副本的某个分片,选择哪个分片进行查询,依据的准则1是shard allocation awareness, 也就是和协调节点在同一个位置(location)的节点,比如协调节点在可用区1,那么如果可用区1有要查询的副本分片,则会优先选择可用区1的节点进行查询;依据的准则2是:

(1) 协调节点和候选节点之前查询的响应时间,响应时间越短,优先选择

(2) 之前的查询中,候选节点执行查询任务的处理时间(took time),处理时间越短,优先选择

(3) 候选节点的查询队列,队列中查询任务越少,优先选择

adaptive replica selection旨在降低查询延迟,可以通过动态修改cluster.routing.use_adaptive_replica_selection配置为false关闭该特性,关闭之后,查询时将会采用round robin策略,可能会使得查询延迟增加。

48 . es为什么不适用一致性hash做数据的sharding?

es使用简单的hash方式:hash(routing) % number_of_shards,实现起来较为简单,效率也更高。当需要扩展分片数量的时候,可以通过创建新索引 别名的方式解决。

为什么不用一致性hash?对es来说,底层的存储是lucene 的segment, 它的不可变特性造成了仅仅移动5%左右的文档到新的分片,代价就会很大(因为使用一致性hash需要考虑rehash,所以需要移动文档到新的分片)。所以通过创建新的分片数量更大的索引进行读写,实现要简单的多,不必考虑移动文档造成的系统资源开销。

49 . ES在CAP理论上的实践:

  • C是一致性:最终一致性,主分片写完后再写副本分片,可能存在主分片写完之后可读,副本分片还没有refresh读不到数据
  • A是可用性:通过副本和translog保证数据可靠性
  • P是分区容错性:master和data节点间互相ping,进行故障节点检测与恢复 在CAP三个特性上如何做折中?写数据时主分片写完之后,写副本分片时不要求所有的副本分片都能成功,在一致性与可用性上倾向于可用性。

50 . es的读写模型相比原生的PacificA算法的区别

  • Prepare阶段:PacificA中有Prepare阶段,保证数据在所有节点Prepare成功后才能Commit,保证Commit的数据不丢,ES中没有这个阶段,数据会直接写入。
  • 读一致性:ES中所有InSync的Replica都可读,提高了读能力,但是可能读到旧数据。另一方面是即使只能读Primary,ES也需要Lease机制等避免读到Old Primary。因为ES本身是近实时系统,所以读一致性要求可能并不严格。

总结来讲,就是原生的PacificA算法具有两阶段提交的过程,ES并没有,数据会直接写入副本分片;ES中副本分片也是可以承担读请求的,因为ES本身设计之初是为了满足搜索场景,也是一个近实时系统,所以在读一致性上可能要求并不严格。

1 人点赞