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

从事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参数

    相关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?

    "mapping": {
      "type": "binary",
      "doc_values": "false",
      "norms": "false",
      "fielddata": "false",
      "store": "false"
    }
    
  12. 对ip字段进行聚合,希望聚合结果返回每个ip的一条数据,该怎么实现?

先使用terms聚合,再使用top\_hits子聚合能达到目的,使用 collapse 配合 inner\_hits也可以实现
  1. 有一张消费明细表(一个人有多条消费记录),首先想计算出一个人的总消费金额,然后想得到总消费大于500美金的所有人数,query DSL该怎么写?
    {
    "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"
            }
        }
    }
}
  1. 使用Logstash迁移ES的数据,简单的配置文件
    input {
        elasticsearch {
            hosts => ["http://x.x.x.x:9200"]
            index => "*"
            docinfo => true
        }
    }
    output {
        elasticsearch {
            hosts => ["http://x.x.x.x:9200"]
            index => "%{[@metadata][_index]}"
        }
    }
  1. 一个有用的脚本,用于追加netsted objects
    {
      "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
          }
        }
      }
    }
  1. mustache小胡子脚本,用于把一个数组类型的字段复制到另外一个字段,高版本7.x可以使用set processor的copy_from, 低版本不支持copy_from
    {
  "pipeline": {
    "processors": [
      {
        "set": {
          "field": "a",
          "value": "{{#b}}{{.}},{{/b}}"
        }
      },
      {
        "split": {
          "field": "a",
          "separator": ","
        }
      },
      {
        "convert": {
          "field": "a",
          "type": "integer"
        }
      }
    ]
  },
  "docs": [
    {
      "_source": {
        "b": [
          1,
          2
        ]
      }
    }
  ]
}
  1. 查询时对结果进行排序,如果文档的分值相同,需要返回顺序是随机的,可以通过script来进行处理
    {
      "_script": {
        "script": "Math.random()",
        "type": "number",
        "order": "asc"
      }
    }
  1. 取消reindex任务
    列出运行中的任务
    _tasks
    _tasks?nodes=nodeId1,nodeId2
    取消任务
    _tasks/node_id:task_id/_cancel
    取消重建索引任务
    _tasks/_cancel?nodes=nodeId1,nodeId2&action=*reindex
  1. 查看阻塞在队列中的索引
    GET  _tasks?pretty\&detailed  | grep description | awk -F 'index' '{print $2}' | sort | uniq -c | sort -n
  1. cancel掉所有存量的查询,释放内存
    POST _tasks/_cancel?actions='indices:data/read/search*'

三、优化类的问题记录

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

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

    image

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

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

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

    echo %2的系统总内存(单位kb) > /proc/sys/vm/min_free_kbytes
    
    echo 1 > /proc/sys/vm/compact_memory
    
  3. es节点上TCP全连接队列参数设置,防止节点数大于100的这种的集群中,节点异常重启时全连接队列在启动瞬间打满,造成节点hung住,导致集群响应迟滞:

    echo "net.ipv4.tcp_abort_on_overflow = 1" >>/etc/sysctl.conf
    echo "net.core.somaxconn = 2048" >>/etc/sysctl.conf
    
  4. 查询时需要返回文档原文中的几个字段,从行存改为从列存读取,高压力查询场景性能可以提升 50%。从行存读取涉及到解压的开销,列存则可直接取对应字段的部分block,性能会更高:

    查询body 中的取source 部分:

    "_source": {
        "includes": [
          "a",
          "b",
          "c"
        ]
      } 
    

    调整为从列存读取字段:

      "docvalue_fields":  [
          "a",
          "b",
          "c"
        ],
      "stored_fields": "_none_", // 关闭行存读取
    
  5. 部署es时磁盘挂载时的可选配置

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

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

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

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

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

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

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

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

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

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

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

    • 取得最佳匹配查询子句的score
    • 将其它每个匹配的子句的分值乘以tie_breaker
    • 将以上得到的分值进行累加并规范化
      通过tie_breaker参数,所有匹配的子句都会起作用,只不过最佳匹配子句的作用更大。
  10. min_score参数设置后,多次查询结果可能会不一致,因为查询primary shard和replica shard的结果可能不一致。

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

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

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

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

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

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

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

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

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

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

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

    • 数据可靠性:通过分片副本和事务日志保障数据安全
    • 服务可用性:在可用性和一致性的取舍方面,默认ES更倾向于可用性,只要主分片可用即可执行写入操作
    • 一致性:弱一致性?最终一致性?只要主分片写入成功,数据就能被读取
    • 原子性:数据读写是原子操作,不会出现中间状态,但是bulk不是原子操作,不能用来实现事务
    • 扩展性:主分片和副本分片都可以承担读请求,分担系统负载
  21. 腾讯云Elasticsearch有自研的熔断器,默认情况下当jvm old 区使用率超过85% ,拒绝写入;当jvm old 区使用率超过90% ,拒绝查询;日志报错有"pressure too high"字样; 详细介绍见https://cloud.tencent.com/document/product/845/56272。

  22. 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性能有一定影响。

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

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

    * 最高优先级:  -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

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

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

  3. zen2相比zen的优势?

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

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

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

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

    • C是一致性:最终一致性,主分片写完后再写副本分片,可能存在主分片写完之后可读,副本分片还没有refresh读不到数据
    • A是可用性:通过副本和translog保证数据可靠性
    • P是分区容错性:master和data节点间互相ping,进行故障节点检测与恢复

    在CAP三个特性上如何做折中?写数据时主分片写完之后,写副本分片时不要求所有的副本分片都能成功,在一致性与可用性上倾向于可用性。

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

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

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

你可能感兴趣的:(这份Elasticsearch 工作笔记,值得收藏)