写点什么

2022-Java 后端工程师面试指南 -(Elasticsearch)

作者:自然
  • 2022 年 8 月 03 日
  • 本文字数:6926 字

    阅读完需:约 23 分钟

前言

文本已收录至我的 GitHub 仓库,欢迎 Star:https://github.com/bin392328206/six-finger

种一棵树最好的时间是十年前,其次是现在

Tips

面试指南系列,很多情况下不会去深挖细节,是小六六以被面试者的角色去回顾知识的一种方式,所以我默认大部分的东西,作为面试官的你,肯定是懂的。


https://www.processon.com/view/link/600ed9e9637689349038b0e4


上面的是脑图地址


Es 其实用的很多,而且如果体量大点的话,基本上都需要使用到它,所以掌握它还是很有必要的,那么我们来一起看看吧

说说什么是 Elasticsearch

  • Elasticsearch,基于 lucene.分布式的 Restful 实时搜索和分析引擎(实时)

  • 分布式的实时文件存储,每个字段都被索引并可被搜索

  • 高扩展性,可扩展至上百台服务器,处理 PB 级结构化或非结构化数据

  • Elasticsearch 用于全文检索,结构化搜索,分析/合并使用

聊聊 Elasticsearch 的特性:

  • Elasticsearch 没有典型意义的事务(无事务性)

  • Elasticsearch 是一种面向文档的数据库

  • Elasticsearch 没有提供授权和认证特性

什么是全文检索和 Lucene?

全文检索,倒排索引


全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。全文搜索搜索引擎数据库中的数据。


lucenelucene,就是一个 jar 包,里面包含了封装好的各种建立倒排索引,以及进行搜索的代码,包括各种算法。我们就用 java 开发的时候,引入 lucene jar,然后基于 lucene 的 api 进行去进行开发就可以了。

那你聊聊 Elasticsearch 的核心概念,就是我们经常用的那些。

近实时近实时,两个意思,从写入数据到数据可以被搜索到有一个小延迟(大概 1 秒);基于 es 执行搜索和分析可以达到秒级。


Cluster(集群)集群包含多个节点,每个节点属于哪个集群是通过一个配置(集群名称,默认是 elasticsearch)来决定的,对于中小型应用来说,刚开始一个集群就一个节点很正常


Node(节点)


集群中的一个节点,节点也有一个名称(默认是随机分配的),节点名称很重要(在执行运维管理操作的时候),默认节点会去加入一个名称为“elasticsearch”的集群,如果直接启动一堆节点,那么它们会自动组成一个 elasticsearch 集群,当然一个节点也可以组成一个 elasticsearch 集群。


Index(索引-数据库)索引包含一堆有相似结构的文档数据,比如可以有一个客户索引,商品分类索引,订单索引,索引有一个名称。一个 index 包含很多 document,一个 index 就代表了一类类似的或者相同的 document。比如说建立一个 product index,商品索引,里面可能就存放了所有的商品数据,所有的商品 document。


Type(类型-表)每个索引里都可以有一个或多个 type,type 是 index 中的一个逻辑数据分类,一个 type 下的 document,都有相同的 field,比如博客系统,有一个索引,可以定义用户数据 type,博客数据 type,评论数据 type。


Document(文档-行)文档是 es 中的最小数据单元,一个 document 可以是一条客户数据,一条商品分类数据,一条订单数据,通常用 JSON 数据结构表示,每个 index 下的 type 中,都可以去存储多个 document。


Field(字段-列)Field 是 Elasticsearch 的最小单位。一个 document 里面有多个 field,每个 field 就是一个数据字段。


shard 单台机器无法存储大量数据,es 可以将一个索引中的数据切分为多个 shard,分布在多台服务器上存储。有了 shard 就可以横向扩展,存储更多数据,让搜索和分析等操作分布到多台服务器上去执行,提升吞吐量和性能。每个 shard 都是一个 lucene index。


replica 任何一个服务器随时可能故障或宕机,此时 shard 可能就会丢失,因此可以为每个 shard 创建多个 replica 副本。replica 可以在 shard 故障时提供备用服务,保证数据不丢失,多个 replica 还可以提升搜索操作的吞吐量和性能。primary shard(建立索引时一次设置,不能修改,默认 5 个),replica shard(随时修改数量,默认 1 个),默认每个索引 10 个 shard,5 个 primary shard,5 个 replica shard,最小的高可用配置,是 2 台服务器。

说说 Elasticsearch 乐观并发控制

Elasticsearch 是分布式的。当文档被创建、更新或删除,文档的新版本会被复制到集群的其它节点。Elasticsearch 即是同步的又是异步的,意思是这些复制请求都是平行发送的,并无序(out of sequence)的到达目的地。这就需要一种方法确保老版本的文档永远不会覆盖新的版本。上文我们提到 index、get、delete 请求时,我们指出每个文档都有一个_version 号码,这个号码在文档被改变时加一。Elasticsearch 使用这个_version 保证所有修改都被正确排序。当一个旧版本出现在新版本之后,它会被简单的忽略。我们利用_version 的这一优点确保数据不会因为修改冲突而丢失。我们可以指定文档的 version 来做想要的更改。如果那个版本号不是现在的,我们的请求就失败了。用 version 来保证并发的顺序一致性

聊聊 text,keyword 类型的区别

  • text:当一个字段是要被全文搜索的,比如 Email 内容、产品描述,应该使用 text 类型。设置 text 类型以后,字段内容会被分析,在生成倒排索引以前,字符串会被分析器分成一个一个词项。text 类型的字段不用于排序,很少用于聚合。

  • keyword:keyword 类型适用于索引结构化的字段,比如 email 地址、主机名、状态码和标签。如果字段需要进行过滤(比如查找已发布博客中 status 属性为 published 的文章)、排序、聚合。keyword 类型的字段只能通过精确值搜索到。

那你说说查询 api 返回的主要包含什么东西

hits 响应中最重要的部分是 hits,它包含了 total 字段来表示匹配到的文档总数,hits 数组还包含了匹配到的前 10 条数据。hits 数组中的每个结果都包含_index、_type 和文档的_id 字段,被加入到_source 字段中这意味着在搜索结果中我们将可以直接使用全部文档。这不像其他搜索引擎只返回文档 ID,需要你单独去获取文档。每个节点都有一个_score 字段,这是相关性得分(relevance score),它衡量了文档与查询的匹配程度。默认的,返回的结果中关联性最大的文档排在首位;这意味着,它是按照_score 降序排列的。这种情况下,我们没有指定任何查询,所以所有文档的相关性是一样的,因此所有结果的_score 都是取得一个中间值 1max_score 指的是所有文档匹配查询中_score 的最大值。


tooktook 告诉我们整个搜索请求花费的毫秒数。


shards_shards 节点告诉我们参与查询的分片数(total 字段),有多少是成功的(successful 字段),有多少的是失败的(failed 字段)。通常我们不希望分片失败,不过这个有可能发生。如果我们遭受一些重大的故障导致主分片和复制分片都故障,那这个分片的数据将无法响应给搜索请求。这种情况下,Elasticsearch 将报告分片 failed,但仍将继续返回剩余分片上的结果。


timeout


time_out 值告诉我们查询超时与否。一般的,搜索请求不会超时。如果响应速度比完整的结果更重要,你可以定义 timeout 参数为 10 或者 10ms(10 毫秒),或者 1s(1 秒)

聊聊 shard&replica 机制

  • index 包含多个 shard

  • 每个 shard 都是一个最小工作单元,承载部分数据,lucene 实例,完整的建立索引和处理请求的能力

  • 增减节点时,shard 会自动在 nodes 中负载均衡

  • primary shard 和 replica shard,每个 document 肯定只存在于某一个 primary shard 以及其对应的 replica shard 中,不可能存在于多个 primary shard

  • replica shard 是 primary shard 的副本,负责容错,以及承担读请求负载

  • primary shard 的数量在创建索引的时候就固定了,replica shard 的数量可以随时修改

  • primary shard 的默认数量是 5,replica 默认是 1,默认有 10 个 shard,5 个 primary shard,5 个 replica shard

  • primary shard 不能和自己的 replica shard 放在同一个节点上(否则节点宕机,primary shard 和副本都丢失,起不到容错的作用),但是可以和其他 primary shard 的 replica shard 放在同一个节点上

ES 是如何实现 master 选举的?

前置条件:


  • 只有是候选主节点(master:true)的节点才能成为主节点。

  • 最小主节点数(min_master_nodes)的目的是防止脑裂。


Elasticsearch 的选主是 ZenDiscovery 模块负责的,主要包含 Ping(节点之间通过这个 RPC 来发现彼此)和 Unicast(单播模块包含一个主机列表以控制哪些节点需要 ping 通)这两部分;获取主节点的核心入口为 findMaster,选择主节点成功返回对应 Master,否则返回 null。


选举流程大致描述如下:


  • 第一步:确认候选主节点数达标,elasticsearch.yml 设置的值 discovery.zen.minimum_master_nodes;

  • 第二步:对所有候选主节点根据 nodeId 字典排序,每次选举每个节点都把自己所知道节点排一次序,然后选出第一个(第 0 位)节点,暂且认为它是 master 节点。

  • 第三步:如果对某个节点的投票数达到一定的值(候选主节点数 n/2+1)并且该节点自己也选举自己,那这个节点就是 master。否则重新选举一直到满足上述条件。

如何解决 ES 集群的脑裂问题

所谓集群脑裂,是指 Elasticsearch 集群中的节点(比如共 20 个),其中的 10 个选了一个 master,另外 10 个选了另一个 master 的情况。


当集群 master 候选数量不小于 3 个时,可以通过设置最少投票通过数量(discovery.zen.minimum_master_nodes)超过所有候选节点一半以上来解决脑裂问题;当候选数量为两个时,只能修改为唯一的一个 master 候选,其他作为 data 节点,避免脑裂问题。

聊聊 es 的写入流程

Elasticsearch 采用多 Shard 方式,通过配置 routing 规则将数据分成多个数据子集,每个数据子集提供独立的索引和搜索功能。当写入文档的时候,根据 routing 规则,将文档发送给特定 Shard 中建立索引。这样就能实现分布式了。


每个 Index 由多个 Shard 组成(默认是 5 个),每个 Shard 有一个主节点和多个副本节点,副本个数可配。但每次写入的时候,写入请求会先根据_routing 规则选择发给哪个 Shard,Index Request 中可以设置使用哪个 Filed 的值作为路由参数,如果没有设置,则使用 Mapping 中的配置,如果 mapping 中也没有配置,则使用_id 作为路由参数,然后通过_routing 的 Hash 值选择出 Shard(在 OperationRouting 类中),最后从集群的 Meta 中找出出该 Shard 的 Primary 节点。


请求接着会发送给 Primary Shard,在 Primary Shard 上执行成功后,再从 Primary Shard 上将请求同时发送给多个 Replica Shard,请求在多个 Replica Shard 上执行成功并返回给 Primary Shard 后,写入请求执行成功,返回结果给客户端。

那你说说具体在 shard 上的写入流程呗

在每一个 Shard 中,写入流程分为两部分,先写入 Lucene,再写入 TransLog。


写入请求到达 Shard 后,先写 Lucene 文件,创建好索引,此时索引还在内存里面,接着去写 TransLog,写完 TransLog 后,刷新 TransLog 数据到磁盘上,写磁盘成功后,请求返回给用户。这里有几个关键点:


和数据库不同,数据库是先写 CommitLog,然后再写内存,而 Elasticsearch 是先写内存,最后才写 TransLog,一种可能的原因是 Lucene 的内存写入会有很复杂的逻辑,很容易失败,比如分词,字段长度超过限制等,比较重,为了避免 TransLog 中有大量无效记录,减少 recover 的复杂度和提高速度,所以就把写 Lucene 放在了最前面。


写 Lucene 内存后,并不是可被搜索的,需要通过 Refresh 把内存的对象转成完整的 Segment 后,然后再次 reopen 后才能被搜索,一般这个时间设置为 1 秒钟,导致写入 Elasticsearch 的文档,最快要 1 秒钟才可被从搜索到,所以 Elasticsearch 在搜索方面是 NRT(Near Real Time)近实时的系统。


每隔一段比较长的时间,比如 30 分钟后,Lucene 会把内存中生成的新 Segment 刷新到磁盘上,刷新后索引文件已经持久化了,历史的 TransLog 就没用了,会清空掉旧的 TransLog。


Lucene 缓存中的数据默认 1 秒之后才生成 segment 文件,即使是生成了 segment 文件,这个 segment 是写到页面缓存中的,并不是实时的写到磁盘,只有达到一定时间或者达到一定的量才会强制 flush 磁盘。如果这期间机器宕掉,内存中的数据就丢了。如果发生这种情况,内存中的数据是可以从 TransLog 中进行恢复的,TransLog 默认是每 5 秒都会刷新一次磁盘。但这依然不能保证数据安全,因为仍然有可能最多丢失 TransLog 中 5 秒的数据。这里可以通过配置增加 TransLog 刷磁盘的频率来增加数据可靠性,最小可配置 100ms,但不建议这么做,因为这会对性能有非常大的影响。一般情况下,Elasticsearch 是通过副本机制来解决这一问题的。即使主分片所在节点宕机,丢失了 5 秒数据,依然是可以通过副本来进行恢复的。


总结一下,数据先写入内存 buffer,然后每隔 1s,将数据 refresh 到 os cache,到了 os cache 数据就能被搜索到(所以我们才说 es 从写入到能被搜索到,中间有 1s 的延迟)。每隔 5s,将数据写入 translog 文件(这样如果机器宕机,内存数据全没,最多会有 5s 的数据丢失),translog 大到一定程度,或者默认每隔 30mins,会触发 commit 操作,将缓冲区的数据都 flush 到 segment file 磁盘文件中。

说说 es 的更新流程吧

Lucene 中不支持部分字段的 Update,所以需要在 Elasticsearch 中实现该功能,具体流程如下:


  • 到 Update 请求后,从 Segment 或者 TransLog 中读取同 id 的完整 Doc,记录版本号为 V1。

  • 将版本 V1 的全量 Doc 和请求中的部分字段 Doc 合并为一个完整的 Doc,同时更新内存中的 VersionMap。获取到完整 Doc 后,Update 请求就变成了 Index 请求。

  • 加锁。

  • 再次从 versionMap 中读取该 id 的最大版本号 V2,如果 versionMap 中没有,则从 Segment 或者 TransLog 中读取,这里基本都会从 versionMap 中获取到。

  • 检查版本是否冲突(V1==V2),如果冲突,则回退到开始的“Update doc”阶段,重新执行。如果不冲突,则执行最新的 Add 请求。

  • 在 Index Doc 阶段,首先将 Version + 1 得到 V3,再将 Doc 加入到 Lucene 中去,Lucene 中会先删同 id 下的已存在 doc id,然后再增加新 Doc。写入 Lucene 成功后,将当前 V3 更新到 versionMap 中。

  • 释放锁,部分更新的流程就结束了

详细描述一下 ES 搜索的过程?

搜索被执行成一个两阶段过程,即 Query Then Fetch;


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

说说 es 的写一致性

我们在发送任何一个增删改操作的时候,比如说 put /index/type/id,都可以带上一个 consistency 参数,指明我们想要的写一致性是什么?put /index/type/id?consistency=quorum


  • one:要求我们这个写操作,只要有一个 primary shard 是 active 活跃可用的,就可以执行

  • all:要求我们这个写操作,必须所有的 primary shard 和 replica shard 都是活跃的,才可以执行这个写操作

  • quorum:默认的值,要求所有的 shard 中,必须是大部分的 shard 都是活跃的,可用的,才可以执行这个写操作

聊聊 elasticsearch 深度分页以及 scroll 滚动搜索

深度分页深度分页其实就是搜索的深浅度,比如第 1 页,第 2 页,第 10 页,第 20 页,是比较浅的;第 10000 页,第 20000 页就是很深了。搜索得太深,就会造成性能问题,会耗费内存和占用 cpu。而且 es 为了性能,他不支持超过一万条数据以上的分页查询。那么如何解决深度分页带来的问题,我们应该避免深度分页操作(限制分页页数),比如最多只能提供 100 页的展示,从第 101 页开始就没了,毕竟用户也不会搜的那么深,我们平时搜索淘宝或者京东也就看个 10 来页就顶多了。


滚动搜索一次性查询 1 万+数据,往往会造成性能影响,因为数据量太多了。这个时候可以使用滚动搜索,也就是 scroll 。滚动搜索可以先查询出一些数据,然后再紧接着依次往下查询。在第一次查询的时候会有一个滚动 id,相当于一个锚标记 ,随后再次滚动搜索会需要上一次搜索滚动 id,根据这个进行下一次的搜索请求。每次搜索都是基于一个历史的数据快照,查询数据的期间,如果有数据变更,那么和搜索是没有关系的。

es 在数据量很大的情况下如何提高性能

filesystem


es 每次走 fileSystem cache 查询速度是最快的所以将每个查询的数据 50% 容量= fileSystem cache 容量。


数据预热数据预热是指,每隔一段时间,将热数据手动在后台查询一遍,将热数据刷新到 fileSystem cache 上


冷热分离类似于 MySQL 的分表分库将热数据单独建立一个索引 分配 3 台机器只保持热机器的索引另外的机器保持冷数据的索引,但有一个问题,就是事先必须知道哪些是热数据 哪些是冷数据


不可以深度分页


跟产品经理说,你系统不允许翻那么深的页,默认翻的越深,性能就越差。


类似于 app 里的推荐商品不断下拉出来一页一页的类似于微博中,下拉刷微博,刷出来一页一页的,你可以用 scroll api

结束

es 可能我自己用的也比较少,就用来做一些搜索,没有用来做 bi,所以呢?也不是那么深入吧,希望对大家有帮助,接下来复习下队列

日常求赞

好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是真粉


创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见


微信 搜 "六脉神剑的程序人生" 回复 888 有我找的许多的资料送给大家

发布于: 刚刚阅读数: 3
用户头像

自然

关注

还未添加个人签名 2020.03.01 加入

小六六,目前负责营收超百亿的支付中台

评论

发布
暂无评论
2022-Java后端工程师面试指南-(Elasticsearch)_Elastic Search_自然_InfoQ写作社区