什么是 Elasticsearch?

Elasticsearch 是一个分布式的 RESTful 风格的搜索和数据分析引擎。

  • 查询:Elasticsearch 允许执行和合并多种类型(结构化、非结构化、地理位置、度量指标)的搜索,搜索方式随心而变。
  • 分析:找到与查询最匹配的十个文档是一回事。但是如果面对的是十亿行日志,又该如何解读呢?Elasticsearch 聚合让您能够从大处着眼,探索数据的趋势和模式。
  • 速度:Elasticsearch 很快。真的,真的很快。
  • 可扩展性:可以在笔记本电脑上运行。也可以在承载了 PB 级数据的成百上千台服务器上运行。
  • 弹性:Elasticsearch 运行在一个分布式的环境中,从设计之初就考虑到了这一点。
  • 灵活性:具备多个案例场景。数字、文本、地理位置、结构化、非结构化。所有的数据类型都欢迎。

在 ElasticSearch 中,集群(Cluster),节点(Node),分片(Shard),Indices(索引),replicas(备份)之间是什么关系?

  • Cluster:集群,一个 ES 集群由一个或多个节点(Node)组成,每个集群都有一个 cluster name 作为标识。

  • node:节点,一个 ES 实例就是一个 node,一个机器可以有多个实例,所以并不能说一台机器就是一个 node,大多数情况下每个 node 运行在一个独立的环境或虚拟机上。

  • index:索引,即一系列 documents 的集合。

  • shard:分片,ES 是分布式搜索引擎,每个索引有一个或多个分片,索引的数据被分配到各个分片上,相当于一桶水用了 N 个杯子装。

    • 分片有助于横向扩展,N 个分片会被尽可能平均地(rebalance)分配在不同的节点上(例如你有 2 个节点,4 个主分片(不考虑备份),那么每个节点会分到 2 个分片,后来你增加了 2 个节点,那么你这 4 个节点上都会有 1 个分片,这个过程叫 relocation,ES 感知后自动完成)。
    • 分片是独立的,对于一个 Search Request 的行为,每个分片都会执行这个 Request。
    • 每个分片都是一个 Lucene Index,所以一个分片只能存放 Integer.MAX_VALUE - 128 = 2,147,483,519 个 docs。[LUCENE-5843] IndexWriter should refuse to create an index with more than INT_MAX docs
  • replica:复制,可以理解为备份分片,相应地有 primary shard(主分片)。

    • 主分片和备分片不会出现在同一个节点上(防止单点故障),默认情况下一个索引创建 5 个分片一个备份(即 5 primary + 5 replica = 10 个分片)
    • 如果你只有一个节点,那么 5 个 replica 都无法分配(unassigned),此时 cluster status 会变成 Yellow。
    • replica 的作用主要包括:
      • 容灾:primary 分片丢失,replica 分片就会被顶上去成为新的主分片,同时根据这个新的主分片创建新的 replica,集群数据安然无恙
      • 提高查询性能:replica 和 primary 分片的数据是相同的,所以对于一个 query 既可以查主分片也可以查备分片,在合适的范围内多个 replica 性能会更优(但要考虑资源占用也会提升 cpu/disk/heap),另外 index request 只能发生在主分片上,replica 不能执行 index request。
    • 对于一个索引,除非重建索引否则不能调整分片的数目(主分片数,number_of_shards),但可以随时调整 replica 数(number_of_replicas)。

Elasticsearch 了解多少,说说你们公司 es 的集群架构,索引数据大小,分片有多少,以及一些调优手段。

面试官:想了解应聘者之前公司接触的 ES 使用场景、规模,有没有做过比较大规模的索引设计、规划、调优。

解答:如实结合自己的实践场景回答即可。

比如:ES 集群架构 13 个节点,索引根据通道不同共 20+ 索引,根据日期,每日递增 20+,索引:10分片,每日递增 1亿+ 数据,每个通道每天索引大小控制:150GB 之内。 仅索引层面调优手段:

  1. 设计阶段调优

    • 根据业务增量需求,采取基于日期模板创建索引,通过 roll over API 滚动索引;
    • 使用别名进行索引管理;
    • 每天凌晨定时对索引做 force_merge 操作,以释放空间;
    • 采取冷热分离机制,热数据存储到 SSD,提高检索效率;冷数据定期进行 shrink 操作,以缩减存储;
    • 采取 curator 进行索引的生命周期管理;
    • 仅针对需要分词的字段,合理的设置分词器;
    • Mapping 阶段充分结合各个字段的属性,是否需要检索、是否需要存储等。
  2. 写入调优

    • 写入前副本数设置为 0;
    • 写入前关闭 refresh_interval 设置为 -1,禁用刷新机制;
    • 写入过程中:采取 bulk 批量写入;
    • 写入后恢复副本数和刷新间隔;
    • 尽量使用自动生成的 id。
  3. 查询调优

    • 禁用 wildcard;
    • 禁用批量 terms(成百上千的场景);
    • 充分利用倒排索引机制,能 keyword 类型尽量 keyword;
    • 数据量大时候,可以先基于时间敲定索引再检索;
    • 设置合理的路由机制。
  4. 其他调优

    • 部署调优,业务调优等。

上面的提及一部分,面试者就基本对你之前的实践或者运维经验有所评估了。

ES 写数据过程

  1. 客户端选择一个 node 发送请求过去,这个 node 就是 coordinating node(协调节点)。
  2. coordinating node 对 document 进行路由,将请求转发给对应的 node(有 primary shard)。
  3. 实际的 node 上的 primary shard 处理请求,然后将数据同步到 replica node。
  4. coordinating node 如果发现 primary node 和所有 replica node 都搞定之后,就返回响应结果给客户端。

ES 读数据过程

可以通过 document id 来查询,会根据 document id 进行 hash,判断出来当时把 document id 分配到了哪个 shard 上面去,从那个 shard 去查询。

  1. 客户端发送请求到任意一个 node,成为 coordinate node。
  2. coordinate node 对 document id 进行哈希路由,将请求转发到对应的 node,此时会使用 round-robin 随机轮询算法,在 primary shard 以及其所有 replica 中随机选择一个,让读请求负载均衡。
  3. 接收请求的 node 返回 document 给 coordinate node。
  4. coordinate node 返回 document 给客户端。

索引数据多了怎么办,如何调优,部署?

索引数据的规划,应在前期做好规划,正所谓“设计先行,编码在后”,这样才能有效的避免突如其来的数据激增导致集群处理能力不足引发的线上客户检索或者其他业务受到影响。

  1. 动态索引层面

基于模板 + 时间 + rollover api 滚动创建索引,举例:设计阶段定义:blog 索引的模板格式为:blog_index_时间戳 的形式,每天递增数据。这样做的好处:不至于数据量激增导致单个索引数据量非常大,接近于上线 2 的 32 次幂 - 1,索引存储达到了 TB+ 甚至更大。

一旦单个索引很大,存储等各种风险也随之而来,所以要提前考虑,及早避免。

  1. 存储层面

冷热数据分离存储,热数据(比如最近 3 天或者一周的数据),其余为冷数据。

对于冷数据不会再写入新数据,可以考虑定期 force_merge 加 shrink 压缩操作,节省存储空间和检索效率。

  1. 部署层面

一旦之前没有规划,这里就属于应急策略。

结合 ES 自身的支持动态扩展的特点,动态新增机器的方式可以缓解集群压力,注意:如果之前主节点等规划合理,不需要重启集群也能完成动态新增的。

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

搜索拆解为 Query Then Fetch 两个阶段。

  1. query 阶段的目的:定位到位置,但不取。步骤拆解如下:

    1. 假设一个索引数据有 5 主 + 1 副本,共 10 分片,一次请求会命中(主或者副本分片中)的一个。
    2. 每个分片在本地进行查询,结果返回到本地有序的优先队列中。
    3. 第 2 步骤的结果发送到协调节点,协调节点产生一个全局的排序列表。
  2. fetch 阶段的目的:取数据。

    1. 路由节点获取所有文档,返回给客户端。

详细描述一下 Elasticsearch 更新和删除文档的过程。

删除和更新也都是写操作,但是 Elasticsearch 中的文档是不可变的,因此不能被删除或者改动以展示其变更;

磁盘上的每个段都有一个相应的 .del 文件。当删除请求发送后,文档并没有真的被删除,而是在 .del 文件中被标记为删除。该文档依然能匹配查询,但是会在结果中被过滤掉。当段合并时,在 .del 文件中被标记为删除的文档将不会被写入新段。

在新的文档被创建时,Elasticsearch 会为该文档指定一个版本号,当执行更新时,旧版本的文档在 .del 文件中被标记为删除,新版本的文档被索引到一个新段。旧版本的文档依然能匹配查询,但是会在结果中被过滤掉。

Elasticsearch 是如何实现 Master 选举的?

  • Elasticsearch 的选主是 ZenDiscovery 模块负责的,主要包含 Ping(节点之间通过这个 RPC 来发现彼此)和 Unicast(单播模块包含一个主机列表以控制哪些节点需要 ping 通)这两部分;
  • 对所有可以成为 master 的节点(node.master: true)根据 nodeId 字典排序,每次选举每个节点都把自己所知道的节点排一次序,然后选出第一个(第 0 位)节点,暂且认为它是 master 节点。
  • 如果对某个节点的投票数达到一定的值(可以成为 master 节点数 n/2+1)并且该节点自己也选举自己,那这个节点就是 master。否则重新选举一直到满足上述条件。
  • 补充:master 节点的职责主要包括集群、节点和索引的管理,不负责文档级别的管理;data 节点可以关闭 http 功能。

Elasticsearch 中的节点(比如共 20 个),其中的 10 个选了一个 master,另外 10 个选了另一个 master,怎么办?

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

在并发情况下,Elasticsearch 如果保证读写一致?

可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖,由应用层来处理具体的冲突;

另外对于写操作,一致性级别支持 quorum / one / all,默认为 quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,分片将会在一个不同的节点上重建。

对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置 replication 为 async 时,也可以通过设置搜索请求参数 _preference 为 primary 来查询主分片,确保文档是最新版本。

ES 集群的三种状态?

  • Green:所有主分片和备份分片都准备就绪(分配成功),即使有一台机器挂了(假设一台机器一个实例),数据都不会丢失,但会变成 Yellow 状态;
  • Yellow:所有主分片准备就绪,但存在至少一个主分片(假设是 A)对应的备份分片没有就绪,此时集群属于警告状态,意味着集群高可用和容灾能力下降,如果刚好 A 所在的机器挂了,并且你只设置了一个备份(已处于未就绪状态),那么 A 的数据就会丢失(查询结果不完整),此时集群进入 Red 状态;
  • Red:至少有一个主分片没有就绪(直接原因是找不到对应的备份分片成为新的主分片),此时查询的结果会出现数据丢失(不完整)。

ES Filter DSL?

  • term 过滤:主要用于精确匹配哪些值,比如数字,日期,布尔值或 not_analyzed 的字符串(未经分析的文本数据类型)。

    1
    2
    3
    4
    { "term": { "age":    26           }}
    { "term": { "date": "2014-09-01" }}
    { "term": { "public": true }}
    { "term": { "tag": "full_text" }}

  • terms 过滤:terms 跟 term 有点类似,但 terms 允许指定多个匹配条件。 如果某个字段指定了多个值,那么文档需要一起去做匹配。

    1
    { "term": { "tag": [ "tag1", "tag2", "tag3" ] }}

  • range 过滤:range过滤允许我们按照指定范围查找一批数据:

    1
    { "range": { "age": { "gte": 20, "lt": 30 } } }

    • gt:大于
    • gte:大于等于
    • lt:小于
    • lte:小于等于
  • exists 和 missing 过滤:exists 和 missing 过滤可以用于查找文档中是否包含指定字段或没有某个字段,类似于 SQL 语句中的 IS_NULL 条件。

    1
    { "exists": { "field": "title" } }

  • bool 过滤:可以用来合并多个过滤条件查询结果的布尔逻辑,它包含以下操作符,这些参数可以分别继承一个过滤条件或者一个过滤条件的数组。

    • must:多个查询条件的完全匹配,相当于 and。
    • must_not:多个查询条件的相反匹配,相当于 not。
    • should:至少有一个查询条件匹配, 相当于 or。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      { 
      "bool": {
      "must": { "term": { "folder": "inbox" }},
      "must_not": { "term": { "tag": "spam" }},
      "should": [
      { "term": { "starred": true }},
      { "term": { "unread": true }}
      ]
      }
      }

ES Query DSL?

  • match_all 查询:可以查询到所有文档,是没有查询条件下的默认语句。

    1
    { "match_all": {} }

  • match 查询:是一个标准查询,不管你需要全文本查询还是精确查询基本上都要用到它。如果你使用 match 查询一个全文本字段,它会在真正查询之前用分析器先分析 match 一下查询字符:

    1
    { "match": { "tweet": "About Search" } }

    如果用 match 下指定了一个确切值,在遇到数字,日期,布尔值或者 not_analyzed 的字符串时,它将为你搜索你给定的值:

    1
    2
    3
    4
    { "match": { "age":    26           }}
    { "match": { "date": "2014-09-01" }}
    { "match": { "public": true }}
    { "match": { "tag": "full_text" }}

    提示:做精确匹配搜索时,你最好用过滤语句,因为过滤语句可以缓存数据。

    match 查询只能就指定某个确切字段某个确切的值进行搜索,而你要做的就是为它指定正确的字段名以避免语法错误。

  • multi_match 查询:允许你做 match 查询的基础上同时搜索多个字段,在多个字段中同时查一个:

    1
    2
    3
    4
    5
    6
    {
    "multi_match": {
    "query": "full text search",
    "fields": [ "title", "body" ]
    }
    }

  • bool 查询:与 bool 过滤相似,用于合并多个查询子句。不同的是,bool 过滤可以直接给出是否匹配成功,而 bool 查询要计算每一个查询子句的 _score (相关性分值)。

    • must:查询指定文档一定要被包含。
    • must_not:查询指定文档一定不要被包含。
    • should:查询指定文档,有则可以为文档相关性加分。

    以下查询将会找到 title 字段中包含 "how to make millions",并且 "tag" 字段没有被标为 spam。 如果有标识为 "starred" 或者发布日期为 2014 年之前,那么这些匹配的文档将比同类网站等级高:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    { 
    "bool": {
    "must": { "match": { "title": "how to make millions" }},
    "must_not": { "match": { "tag": "spam" }},
    "should": [
    { "match": { "tag": "starred" }},
    { "range": { "date": { "gte": "2014-01-01" }}}
    ]
    }
    }

    提示: 如果 bool 查询下没有 must 子句,那至少应该有一个 should 子句。但是如果有 must 子句,那么没有 should 子句也可以进行查询。

  • wildcards 查询:允许使用通配符 *(代表 0 个或多个字符)和 ?(代表任意 1 个字符)来进行查询。

    1
    { "query": { "wildcard": { "postcode": "W?F*HW" } } }

  • regexp 查询:使用标准的 shell 通配符查询。

    1
    { "query": { "regexp": { "postcode": "W[0-9].+" } } }

  • prefix 查询:以什么字符开头的,可以更简单地用 prefix。

    1
    { "query": { "prefix": { "hostname": "wxopen" } } }

  • match_phrase 查询:当你需要寻找邻近的几个单词时,你会使用 match_phrase 查询。

    1
    2
    3
    4
    5
    6
    7
    {
    "query": {
    "match_phrase": {
    "title": "quick brown fox"
    }
    }
    }

    和 match 查询类似,match_phrase 查询首先解析查询字符串来产生一个词条列表。然后会搜索所有的词条,但只保留含有了所有搜索词条的文档,并且词条的位置要邻接。一个针对短语 quick fox 的查询不会匹配,我们的任何文档,因为没有文档含有邻接在一起的 quick 和 box 词条。

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "match": {
    "title": {
    "query": "quick brown fox",
    "type": "phrase"
    }
    }
    }

  • query_string查询:允许我们在单个查询字符串中指定 AND | OR | NOT 条件,同时也和 multi_match 一样,支持多字段搜索。

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "query": {
    "query_string" : {
    "fields" : ["title"],
    "query" : "系统学 AND es"
    }
    }
    }

keyword 和 text 类型的查询匹配规则

关键词 keyword text 支持分词
term 完全匹配 查询条件必须都是 text 分词中的,且不能多余,
多个分词时必须连续,顺序不能颠倒。
match 完全匹配 match 分词结果和 text 的分词结果有相同的即可,
不考虑顺序
match_phrase 完全匹配 match_phrase 的分词结果必须在 text 字段分词中都包含,
而且顺序必须相同,而且必须都是连续的。
query_string 完全匹配 query_string 中的分词结果至少有一个在 text 字段的
分词结果中,不考虑顺序