es_题库详解

1、 ES 的整体存储架构图

在这里插入图片描述

1.1、索引

一般意义上的索引是一种基于文档(数据)生成、建立的,用于快速定位指定文档的工具。
在这里插入图片描述

而 ElasticSearch 对索引的定义有所不同,ElasticSearch 中的索引对应 MySQL 中的 Database ,也就说 ElasticSearch 中的索引更像是一种数据存储集合,即用于存储文档。

ElasticSearch 中的数据根据业务以索引为单位进行划分,Type(类型) 就像 MySQL 中的 Table 一样,用于区分同一业务中不同的数据集合,如下图:

在这里插入图片描述

当然上图并不是指 ElasticSearch 中就真的这么存储数据,而是大概的表现方式。
不过在 6.x 版本后,就废弃了 Type ,因为设计者发现 ElasticSearch 这种与关系型数据类比的设计方式有缺陷。在关系型数据库中,每个数据表都是相互独立的,即在不同表中相同的数据域是互不关联的。而 ElasticSearch 底层所用的 Lucene 并没有关系型数据中的这种特性,在 ElasticSearch 同一个索引中,不同映射类型但是名称相同的数据域在 Lucene 中是同一个数据域,即作为同一类数据存放在一起。

ElasticSearch 6.x 版本废弃掉 Type 后,建议的是每个类型(业务)的数据单独放在一个索引中,这样其实回归到一般意义上的索引定义,索引定位文档。如下图:
在这里插入图片描述
上图也是一种大概的表现方式,不代表 ElasticSearch 以这种方式处理文档。
P.S.:如果 ElasticSearch 还是使用 5.x 或以下版本,建议每个索引只设置一个类型,做到一个索引存储一种数据。

单index,单type
未来发布的elasticsearch 6.0.0版本为保持兼容,仍然会支持单index,多type结构,但是作者已不推荐这么设置。在elasticsearch 7.0.0版本必须使用单index,单type,多type结构则会完全移除。

单index,多type结构弊端
人们经常会谈到index类似传统sql数据库的“database”,而type类似于"table"。现在想想,这是一个非常糟糕的比喻,而这个比喻会造成很多错误的假设。
在传统的sql数据库中,各个"table"之间是互相独立的,在一个表中的列都与另一个表相同名称的列无关。
①,而在我们elasticsearch中同一 Index 下,同名 Field 类型必须相同,即使不同的 Type;
②, 同一 Index 下,TypeA 的 Field 会占用 TypeB 的资源(互相消耗资源),会形成一种稀疏存储的情况。尤其是 doc value ,为什么这么说呢?doc value为了性能考虑会保留一部分的磁盘空间,这意味着 TypeB 可能不需要这个字段的 doc_value 而 TypeA 需要,那么 TypeB 就被白白占用了一部分没有半点用处的资源;
③,Score 评分机制是 index-wide 的,不同的type之间评分也会造成干扰。
④,索引元数据本身是放在主节点中维护的,CP 设计。意味着涉及到大量字段变更及元数据变更的操作,都会导致该 Index 被堵塞或假死。我们应该对这样的 Index 做隔离,避免影响到其他 Index 正常的增删改查。甚至当涉及到字段变更十分频繁且无法预定义 schema 的场景时,是否要使用 ES 都应该慎思熟虑了!

1.2、 Elasticsearch分片原理

1.2.1、 ES集群的基本概念

Cluster
代表一个集群,集群中有多个节点,其中有一个为主节点,这个主节点是可以通过选举产生的,主从节点是对于集群内部来说的。es的一个概念就是去中心化,字面上理解就是无中心节点,这是对于集群外部来说的,因为从外部来看es集群,在逻辑上是个整体,你与任何一个节点的通信和与整个es集群通信是等价的

Shards主分片
代表索引分片,es可以把一个完整的索引分成多个分片,这样的好处是可以把一个大的索引拆分成多个,分布到不同的节点上。构成分布式搜索。分片的数量只能在索引创建前指定,并且索引创建后不能更改,这里和索引分片的算法有关,因为是通过取模算法去判断分到哪,如果改变了 就无法正常查询之前的索引。

replicas分片副本
代表索引副本,es可以设置多个索引的副本,副本的作用一是提高系统的容错性,当某个节点某个分片损坏或丢失时可以从副本中恢复。二是提高es的查询效率,es会自动对搜索请求进行负载均衡。

Recovery
代表数据恢复或叫数据重新分布,es在有节点加入或退出时会根据机器的负载对索引分片进行重新分配,挂掉的节点重新启动时也会进行数据恢复。

1.2.2、ES为什么要实现集群

ES集群中索引可能由多个分片构成,并且每个分片可以拥有多个副本。通过将一个单独的索引分为多个分片,我们可以处理不能在一个单一的服务器上面运行的大型索引,简单的说就是索引的大小过大,导致效率问题。不能运行的原因可能是内存也可能是存储。由于每个分片可以有多个副本,通过将副本分配到多个服务器,可以提高查询的负载能力。同时提高容错性和高可用性

1.2.3、 ES集群核心原理分析

分片存储规则
1)、每个索引会被分成多个分片shards进行存储,默认创建索引是分配5个分片进行存储(需要注意的是es7.0默认索引分片数调整为1了。每个分片都会分布式部署在多个不同的节点上进行部署,该分片成为primary shards.
注意:索引的主分片primary shards定义好后,后面不能做修改。
2)、为了实现高可用数据的高可用,主分片可以有对应的备分片replics shards,replic shards分片承载了负责容错、以及请求的负载均衡.
注意: 每一个主分片为了实现高可用,都会有自己对应的备分片,他们之间的关系可以是一对多,主分片对应的备分片不能存放同一台服务器上(单台ES没有备用分片的)。主分片primary shards可以和其他replics shards存放在同一个node节点上。
在往主分片服务器存放数据时候,会对应实时同步到备用分片服务器,但是查询时候,所有(主、备)都进行查询:

在这里插入图片描述

Node1 :P1+P2+R3组成了完整的数据
实例演示:
下面是一个已经搭建好的集群.版本为ES7.6.0
在这里插入图片描述
创建一个索引
PUT /testindex
查询该索引
GET /testindex

在这里插入图片描述

7以下的版本这里会是5和1 这里是7.6.0版本所以是1 1
number_of_shards 相当于主分片数量 代表将索引分成几个分片 5就代表索引会分成5个分片
number_of_replicas 这里相当于是副分片的总数,和上面不同的是这里的1代表所有的副分片组成一份副本的意思.比如有5个主分片,
这里就是5个副分片组成1分总副分片,如果这里是2 代表会有10个副分片组成2份总的副分片.
修改分片数
分片数一般通过修改elasticsearch.yml文件中配置默认值
这里我们修改副分片数量为2

PUT testindex/_settings
{
    
    
  "index" : {
    
    
    "number_of_replicas" : 2
  }
}

然后查询索引分片信息
GET /testindex/_search_shards

在这里插入图片描述

可以看到上图 testindex索引主分片有1个 副分片变为2个 (这里只是针对testindex来说,如果建立其他的索引分片初始还是会变成默认值)
我们接下来尝试修改主分片数量

PUT testindex/_settings
{
    
    
  "index" : {
    
    
    "number_of_shards" : 3
  }
}

报错,证明索引创建后无法修改分片数
在这里插入图片描述

创建索引时指定分片数量
要修改分片数只能在索引创建的时候进行修改

PUT test
{
    
    
  "settings" : {
    
    
    "index" : {
    
    
      "number_of_shards" : 3,
      "number_of_replicas" : 2
    }
  }
}

GET /test/_settings 创建并修改成功
在这里插入图片描述
执行 GET /test/_search_shards
可以发现 shards信息中一共有3个主索引 6个副索引
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

每个节点上都有一份完整的数据 即使其他两个节点宕机也不会影响查询
数据路由
当客户端发起创建document的时候,es需要确定这个document放在该index哪个shard上。这个过程就是数据路由。
路由算法:shard = hash(routing) % number_of_primary_shards
这里的routing指的就是document的id
如果number_of_primary_shards在查询的时候取余发生的变化,无法获取到该数据

在这里插入图片描述

已知主分片数量为3,
路由算法: shard = hash(routing) % 主分片数量3
分片位置 p1 = 1% 3 , p2 =2%3 , p0=3%3
使用
GET test/_search_shards?routing=1
可以查看具体数据路由到哪个分片上面
在这里插入图片描述
如上 id为1的文档会路由到2号分片上

2、ES如何避免脑裂

所谓脑裂问题(类似于精神分裂),就是同一个集群中的不同节点,对于集群的状态有了不一样的理解。
情况描述,通过以下命令查看集群状态:
curl -XGET ‘es-1:9200/_cluster/health’
发现,集群的总体状态是red,本来9个节点的集群,在结果中只显示了4个;但是,将请求发向不同的节点之后,却发现即使是总体状态是red的,但是可用的节点数量却不一致。

es集群由多个数据节点和一个主节点(可以有多个备选主节点)组成。其中数据节点负责数据存储和具体操作,如执行搜索、聚合等任务,计算压力较大。主节点负责创建、删除索引、分配分片、追踪集群中的节点状态等工作,计算压力较轻。

正常情况下,当主节点无法工作时,会从备选主节点中选举一个出来变成新主节点,原主节点回归后变成备选主节点。但有时因为网络抖动等原因,主节点没能及时响应,集群误以为主节点挂了,选举了一个新主节点,此时一个es集群中有了两个主节点,其他节点不知道该听谁的调度,结果将是灾难性的!这种类似一个人得了精神分裂症,就被称之为“脑裂”现象。

造成es“脑裂”的因素有以下几个:
网络抖动
内网一般不会出现es集群的脑裂问题,可以监控内网流量状态。外网的网络出现问题的可能性大些。

节点负载
如果主节点同时承担数据节点的工作,可能会因为工作负载大而导致对应的 ES 实例停止响。
内存回收
由于数据节点上es进程占用的内存较大,较大规模的内存回收操作也能造成es进程失去响应。

避免es“脑裂”的措施主要有以下三个:
不要把主节点同时设为数据节点(node.master和node.data不要同时设为true)
将节点响应超时(discovery.zen.ping_timeout)稍稍设置长一些(默认是3秒),避免误判。
设置需要超过半数的备选节点同意,才能发生主节点重选,类似需要参议院半数以上通过,才能弹劾现任总统。(discovery.zen.minimum_master_nodes = 半数以上备选主节点数)

3、ES支持哪些类型的查询

mapping
在index中还有一个mapping,mapping管理了整个index的各个字段的属性,也就是定义了整个index中document的结构。

3.1、es数据类型

1)核心数据类型
(1)字符串类型: text, keyword
(2)数字类型:long, integer, short, byte, double, float, half_float, scaled_float
(3)日期:date
(4)日期 纳秒:date_nanos
(5)布尔型:boolean
(6)Binary:binary
(7)Range: integer_range, float_range, long_range, double_range, date_range

3.2、查询

1)精确值查找
term
2)范围检索
range
3)分词检索
match
match_all
过滤条件
must/filter的区别
match:请求意味着它们被用来评定每个文档的匹配度的评分;
filter:它们将过滤出不匹配的文档,但不会影响匹配文档的分数;
5)聚合查询
aggs
Bool查询包括四种子句:
must
Filter
Should
must_not
must, 返回的文档必须满足must子句的条件,并且参与计算分值
filter, 返回的文档必须满足filter子句的条件。但是跟Must不一样的是,不会计算分值, 并且可以使用缓存
从上面的描述来看,你应该已经知道,如果只看查询的结果,must和filter是一样的。区别是场景不一样。如果结果需要算分就使用must,否则可以考虑使用filter

4、ES更新和删除文档的过程

删除和更新也都是写操作,但是Elasticsearch中的文档是不可变的,因此不能被删除或者改动以展示其变更;
磁盘上的每个段都有一个相应的.del文件。当删除请求发送后,文档并没有真的被删除,而是在.del文件中被标记为删除。该文档依然能匹配查询,但是会在结果中被过滤掉。当段合并时,在.del文件中被标记为删除的文档将不会被写入新段。
在新的文档被创建时,Elasticsearch会为该文档指定一个版本号,当执行更新时,旧版本的文档在.del文件中被标记为删除,新版本的文档被索引到一个新段。旧版本的文档依然能匹配查询,但是会在结果中被过滤掉。

5、ES对于大数据量(上亿量级)的聚合如何实现

Elasticsearch 提供的首个近似聚合是cardinality 度量。它提供一个字段的基数,即该字段的distinct或者unique值的数目。它是基于HLL算法的。HLL 会先对我们的输入作哈希运算,然后根据哈希运算的结果中的 bits 做概率估算从而得到基数。其特点是:可配置的精度,用来控制内存的使用(更精确 = 更多内存);小的数据集精度是非常高的;我们可以通过配置参数,来设置去重需要的固定内存使用量。无论数千还是数十亿的唯一值,内存使用量只与你配置的精确度相关。

6、ES数据预热

es 集群中每个机器写入的数据量还是超过了 filesystem cache 一倍,比如说你写入一台机器 60G 数据,结果 filesystem cache 就 30G,还是有 30G 数据留在了磁盘上。

其实可以做数据预热。

举个例子,拿微博来说,你可以把一些大V,平时看的人很多的数据,你自己提前后台搞个系统,每隔一会儿,自己的后台系统去搜索一下热数据,刷到 filesystem cache 里去,后面用户实际上来看这个热数据的时候,他们就是直接从内存里搜索了,很快。

或者是电商,你可以将平时查看最多的一些商品,比如说 iphone 8,热数据提前后台搞个程序,每隔 1 分钟自己主动访问一次,刷到 filesystem cache 里去。

对于那些你觉得比较热的、经常会有人访问的数据,最好做一个专门的缓存预热子系统,就是对热数据每隔一段时间,就提前访问一下,让数据进入 filesystem cache 里面去。这样下次别人访问的时候,性能一定会好很多。

7、ES冷热分离

es 可以做类似于 mysql 的水平拆分,就是说将大量的访问很少、频率很低的数据,单独写一个索引,然后将访问很频繁的热数据单独写一个索引。最好是将冷数据写入一个索引中,然后热数据写入另外一个索引中,这样可以确保热数据在被预热之后,尽量都让他们留在 filesystem os cache 里,别让冷数据给冲刷掉。

你看,假设你有 6 台机器,2 个索引,一个放冷数据,一个放热数据,每个索引 3 个 shard。3 台机器放热数据 index,另外 3 台机器放冷数据 index。然后这样的话,你大量的时间是在访问热数据 index,热数据可能就占总数据量的 10%,此时数据量很少,几乎全都保留在 filesystem cache 里面了,就可以确保热数据的访问性能是很高的。但是对于冷数据而言,是在别的 index 里的,跟热数据 index 不在相同的机器上,大家互相之间都没什么联系了。如果有人访问冷数据,可能大量数据是在磁盘上的,此时性能差点,就 10% 的人去访问冷数据,90% 的人在访问热数据,也无所谓了。

8、ES分页优化

es 的分页是较坑的,为啥呢?举个例子吧,假如你每页是 10 条数据,你现在要查询第 100 页,实际上是会把每个 shard 上存储的前 1000 条数据都查到一个协调节点上,如果你有个 5 个 shard,那么就有 5000 条数据,接着协调节点对这 5000 条数据进行一些合并、处理,再获取到最终第 100 页的 10 条数据。

分布式的,你要查第 100 页的 10 条数据,不可能说从 5 个 shard,每个 shard 就查 2 条数据,最后到协调节点合并成 10 条数据吧?你必须得从每个 shard 都查 1000 条数据过来,然后根据你的需求进行排序、筛选等等操作,最后再次分页,拿到里面第 100 页的数据。你翻页的时候,翻的越深,每个 shard 返回的数据就越多,而且协调节点处理的时间越长,非常坑爹。所以用 es 做分页的时候,你会发现越翻到后面,就越是慢。

我们之前也是遇到过这个问题,用 es 作分页,前几页就几十毫秒,翻到 10 页或者几十页的时候,基本上就要 5~10 秒才能查出来一页数据了。

解决方案
不允许深度分页(默认深度分页性能很差)
跟产品经理说,你系统不允许翻那么深的页,默认翻的越深,性能就越差。

类似于 app 里的推荐商品不断下拉出来一页一页的

类似于微博中,下拉刷微博,刷出来一页一页的,你可以用 scroll api,关于如何使用,自行上网搜索。

scroll 会一次性给你生成所有数据的一个快照,然后每次滑动向后翻页就是通过游标 scroll_id 移动,获取下一页下一页这样子,性能会比上面说的那种分页性能要高很多很多,基本上都是毫秒级的。

但是,唯一的一点就是,这个适合于那种类似微博下拉翻页的,不能随意跳到任何一页的场景。也就是说,你不能先进入第 10 页,然后去第 120 页,然后又回到第 58 页,不能随意乱跳页。所以现在很多产品,都是不允许你随意翻页的,app,也有一些网站,做的就是你只能往下拉,一页一页的翻。

初始化时必须指定 scroll 参数,告诉 es 要保存此次搜索的上下文多长时间。你需要确保用户不会持续不断翻页翻几个小时,否则可能因为超时而失败。

除了用 scroll api,你也可以用 search_after 来做,search_after 的思想是使用前一页的结果来帮助检索下一页的数据,显然,这种方式也不允许你随意翻页,你只能一页页往后翻。初始化时,需要使用一个唯一值的字段作为 sort 字段。
————————————————

9、说说你们公司ES的集群架构?

根据实际情况回答即可,如果是我的话会这么回答:
我司有多个ES集群,下面列举其中一个。该集群有20个节点,根据数据类型和日期分库,每个索引根据数据量分片,比如日均1亿+数据的,控制单索引大小在200GB以内。 
下面重点列举一些调优策略,仅是我做过的,不一定全面,如有其它建议或者补充欢迎留言。
部署层面:
1)最好是64GB内存的物理机器,但实际上32GB和16GB机器用的比较多,但绝对不能少于8G,除非数据量特别少,这点需要和客户方面沟通并合理说服对方。
2)多个内核提供的额外并发远胜过稍微快一点点的时钟频率。
3)尽量使用SSD,因为查询和索引性能将会得到显著提升。
4)避免集群跨越大的地理距离,一般一个集群的所有节点位于一个数据中心中。
5)设置堆内存:节点内存/2,不要超过32GB。一般来说设置export ES_HEAP_SIZE=32g环境变量,比直接写-Xmx32g -Xms32g更好一点。
6)关闭缓存swap。内存交换到磁盘对服务器性能来说是致命的。如果内存交换到磁盘上,一个100微秒的操作可能变成10毫秒。 再想想那么多10微秒的操作时延累加起来。不难看出swapping对于性能是多么可怕。
7)增加文件描述符,设置一个很大的值,如65535。Lucene使用了大量的文件,同时,Elasticsearch在节点和HTTP客户端之间进行通信也使用了大量的套接字。所有这一切都需要足够的文件描述符。
8)不要随意修改垃圾回收器(CMS)和各个线程池的大小。
9)通过设置gateway.recover_after_nodes、gateway.expected_nodes、gateway.recover_after_time可以在集群重启的时候避免过多的分片交换,这可能会让数据恢复从数个小时缩短为几秒钟。
索引层面:
1)使用批量请求并调整其大小:每次批量数据 5–15 MB 大是个不错的起始点。
2)段合并:Elasticsearch默认值是20MB/s,对机械磁盘应该是个不错的设置。如果你用的是SSD,可以考虑提高到100-200MB/s。如果你在做批量导入,完全不在意搜索,你可以彻底关掉合并限流。另外还可以增加 index.translog.flush_threshold_size 设置,从默认的512MB到更大一些的值,比如1GB,这可以在一次清空触发的时候在事务日志里积累出更大的段。
3)如果你的搜索结果不需要近实时的准确度,考虑把每个索引的index.refresh_interval 改到30s。
4)如果你在做大批量导入,考虑通过设置index.number_of_replicas: 0 关闭副本。
5)需要大量拉取数据的场景,可以采用scan & scroll api来实现,而不是from/size一个大范围。
存储层面:
1)基于数据+时间滚动创建索引,每天递增数据。控制单个索引的量,一旦单个索引很大,存储等各种风险也随之而来,所以要提前考虑+及早避免。
2)冷热数据分离存储,热数据(比如最近3天或者一周的数据),其余为冷数据。对于冷数据不会再写入新数据,可以考虑定期force_merge加shrink压缩操作,节省存储空间和检索效率。

10、详细描述一下Elasticsearch搜索的过程?

搜索被执行成一个两阶段过程,即 Query Then Fetch;
Query阶段:
查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。PS:在搜索的时候是会查询Filesystem Cache的,但是有部分数据还在Memory Buffer,所以搜索是近实时的。
每个分片返回各自优先队列中 所有文档的 ID 和排序值 给协调节点,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。
Fetch阶段:
协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。每个分片加载并 丰富 文档,如果有需要的话,接着返回文档给协调节点。一旦所有的文档都被取回了,协调节点返回结果给客户端。

11、Elasticsearch 的内存分配:

为什么分配给ES的堆内存不能超过物理机内存的一半?
预留一半内存给Lucene使用:
为什么需要预留一半的内存给 Lucene,将所有的内存都分配给 Elasticsearch 不是更好吗?毋庸置疑,堆内存对于 ES 来说绝对是重要的,但还有另外一个非常重要的内存使用者——Lucene。

在讲 Lucene 前,我们先简单介绍一下 segment。每个 segment 段是分别存储到单个文件的,即 segment 文件。它其实是一个包含正排(空间占90~95%) + 倒排(占5~10%) 的完整索引文件,并且一旦写到磁盘上就不会再修改,ES中文档更新和删除实际上是增量写入的一种特殊文档,会保存在新的段里而不修改旧的段。

回到 Lucene,Lucene 实际目的就是把底层 OS 里的数据缓存到内存中。由于 Lucene 的段 segment 是不会变化的,所以很利于缓存,操作系统会将这些段文件缓存起来,以便更快的访问。这些段包括倒排索引(用于全文搜索)和文档值(用于聚合)。

Lucene 的性能依赖于与 OS 的这种交互,如果把所有的内存都给了ES的堆内存,而不留一点给 Lucene,那么全文检索的性能会很差的。所以官方建议是将可用内存的 50% 提供给ES堆,而其他 50% 的剩余内存也并不会被闲置,因为 Lucene 会利用他们来缓存被用读取过的段文件。

ES的文件存储类型默认使用的是 mmap 内存映射,将 Lucene 索引文件用映射到内存中,这样进程就能够直接从内存中读取 Lucene 数据了。由于使用了内存映射,ES 进程读取 Lucene 文件时读取到的数据就会占用了堆外内存的空间。

12、分配给 ES 的堆内存不要超过 32G:

堆内存为什么不能超过32GB?事实上 JVM 在内存小于 32 G 的时候会采用一种内存对象指针压缩技术。

在 Java 中,所有的对象都分配在堆上,然后有一个指针引用它。指向这些对象的指针大小通常是 CPU 的字长的大小,不是 32 bit 就是 64 bit,这取决于你的处理器,指针指向了你的值的精确位置。对于 32 位系统,你的内存最大可使用 4 G。对于 64 系统可以使用更大的内存。但是 64 位的指针意味着更大的浪费,因为你的指针本身大了。并且比浪费的空间更糟糕的是,更大的指针在主内存和缓存器(例如 LLC,L1 等)之间移动数据的时候,会占用更多的带宽。

Java 使用一个叫内存指针压缩的技术来解决这个问题。它的指针不再表示对象在内存中的精确位置,而是表示偏移量。这意味着 32 位的指针可以引用 40 亿个对象,而不是 40 亿个字节。最终,也就是说堆内存长到 32 G 的物理内存,也可以用 32 bit 的指针表示。

一旦越过那个神奇的 30 - 32 G 的边界,指针就会切回普通对象的指针,每个对象的指针都变长了,就会使用更多的 CPU 内存带宽,也就是说你实际上失去了更多的内存。事实上当内存到达 40 - 50 GB 的时候,有效内存才相当于使用内存对象指针压缩技术时候的 32 G 内存。

所以,即便你有足够的内存,也尽量不要超过 32 G,因为它浪费了内存,降低了 CPU 的性能,还要让 GC 应对大内存。

猜你喜欢

转载自blog.csdn.net/chuige2013/article/details/129545421