全文搜索引擎Elasticsearch,这篇文章给讲透了!

前言

以前已经分享过Elasticsearch的使用和原理的知识,因为近期在公司内部作了一次内部分享,因此本篇主要是基于以前的博文的一个总结,但愿经过这篇文章能让读者大体了解Elasticsearch是作什么的以及它的使用和基本原理。html

生活中的数据

搜索引擎是对数据的检索,因此咱们先从生活中的数听说起。node

咱们生活中的数据整体分为两种:结构化数据非结构化数据mysql

结构化数据: 也称做行数据,是由二维表结构来逻辑表达和实现的数据,严格地遵循数据格式与长度规范,主要经过关系型数据库进行存储和管理。指具备固定格式或有限长度的数据,如数据库,元数据等。sql

非结构化数据: 又可称为全文数据,不定长或无固定格式,不适于由数据库二维表来表现,包括全部格式的办公文档、XML、HTML、word文档,邮件,各种报表、图片和咅频、视频信息等。数据库

说明:若是要更细致的区分的话,XML、HTML可划分为 半结构化数据。由于它们也具备本身特定的标签格式,因此既能够根据须要按结构化数据来处理,也可抽取出纯文本按非结构化数据来处理。apache

根据两种数据分类,搜索也相应的分为两种:结构化数据搜索非结构化数据搜索浏览器

对于结构化数据,由于它们具备特定的结构,因此咱们通常都是能够经过关系型数据库(mysql,oracle等)的 二维表(table)的方式存储和搜索,也能够创建索引。缓存

对于非结构化数据,也即对全文数据的搜索主要有两种方法:顺序扫描法全文检索性能优化

顺序扫描:经过文字名称也可了解到它的大概搜索方式,即按照顺序扫描的方式查询特定的关键字。
例如给你一张报纸,让你找到该报纸中“平安”的文字在哪些地方出现过。你确定须要从头至尾把报纸阅读扫描一遍而后标记出关键字在哪些版块出现过以及它的出现位置。服务器

这种方式无疑是最耗时的最低效的,若是报纸排版字体小,并且版块较多甚至有多份报纸,等你扫描完你的眼睛也差很少了。

全文搜索:对非结构化数据顺序扫描很慢,咱们是否能够进行优化?把咱们的非结构化数据想办法弄得有必定结构不就好了吗?将非结构化数据中的一部分信息提取出来,从新组织,使其变得有必定结构,而后对此有必定结构的数据进行搜索,从而达到搜索相对较快的目的。

这种方式就构成了全文检索的基本思路。这部分从非结构化数据中提取出的而后从新组织的信息,咱们称之索引。这种方式的主要工做量在前期索引的建立,可是对于后期搜索倒是快速高效的。

先说说Lucene

经过对生活中数据的类型做了一个简短了解以后,咱们知道关系型数据库的SQL检索是处理不了这种非结构化数据的。这种非结构化数据的处理须要依赖全文搜索,而目前市场上开放源代码的最好全文检索引擎工具包就属于 apache 的 Lucene了。

可是 Lucene 只是一个工具包,它不是一个完整的全文检索引擎。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础创建起完整的全文检索引擎。

目前以 Lucene 为基础创建的开源可用全文搜索引擎主要是 Solr 和 Elasticsearch。

Solr 和 Elasticsearch 都是比较成熟的全文搜索引擎,能完成的功能和性能也基本同样。可是 ES 自己就具备分布式的特性和易安装使用的特色,而Solr的分布式须要借助第三方来实现,例如经过使用ZooKeeper来达到分布式协调管理。

无论是 Solr 仍是 Elasticsearch 底层都是依赖于 Lucene,而 Lucene 能实现全文搜索主要是由于它实现了倒排索引的查询结构。

如何理解倒排索引呢?假如现有三份数据文档,文档的内容以下分别是:

  1. Java is the best programming language.
  2. PHP is the best programming language.
  3. Javascript is the best programming language.

为了建立倒排索引,咱们经过分词器将每一个文档的内容域拆分红单独的(咱们称它为词条或 Term),建立一个包含全部不重复词条的排序列表,而后列出每一个词条出如今哪一个文档。结果以下所示:

 1Term          Doc_1    Doc_2   Doc_3
2-------------------------------------
3Java        |   X   |        |
4is          |
   X   |   X    |   X
5the         |   X   |   X    |   X
6best        |
   X   |   X    |   X
7programming |   x   |   X    |   X
8language    |
   X   |   X    |   X
9PHP         |       |   X    |
10Javascript  |
       |        |   X
11-------------------------------------
复制代码

这种结构由文档中全部不重复词的列表构成,对于其中每一个词都有一个文档列表与之关联。这种由属性值来肯定记录的位置的结构就是倒排索引。带有倒排索引的文件咱们称为倒排文件。

咱们将上面的内容转换为图的形式来讲明倒排索引的结构信息,以下图所示,

其中主要有以下几个核心术语须要理解:

  • 词条(Term):索引里面最小的存储和查询单元,对于英文来讲是一个单词,对于中文来讲通常指分词后的一个词。
  • 词典(Term Dictionary):或字典,是词条Term的集合。搜索引擎的一般索引单位是单词,单词词典是由文档集合中出现过的全部单词构成的字符串集合,单词词典内每条索引项记载单词自己的一些信息以及指向“倒排列表”的指针。
  • 倒排表(Post list):一个文档一般由多个词组成,倒排表记录的是某个词在哪些文档里出现过以及出现的位置。每条记录称为一个倒排项(Posting)。倒排表记录的不单是文档编号,还存储了词频等信息。
  • 倒排文件(Inverted File):全部单词的倒排列表每每顺序地存储在磁盘的某个文件里,这个文件被称之为倒排文件,倒排文件是存储倒排索引的物理文件。

从上图咱们能够了解到倒排索引主要由两个部分组成:词典倒排文件。词典和倒排表是Lucene中很重要的两种数据结构,是实现快速检索的重要基石。词典和倒排文件是分两部分存储的,词典在内存中而倒排文件存储在磁盘上。

核心概念

一些基础知识的铺垫以后咱们正式进入今天的主角Elasticsearch的介绍, ES是使用Java编写的一种开源搜索引擎,它在内部使用Lucene作索引与搜索,经过对Lucene的封装,隐藏了Lucene的复杂性,取而代之的提供一套简单一致的 RESTful API。

然而,Elasticsearch 不只仅是 Lucene,而且也不只仅只是一个全文搜索引擎。 它能够被下面这样准确的形容:

  • 一个分布式的实时文档存储,每一个字段能够被索引与搜索。
  • 一个分布式实时分析搜索引擎。
  • 能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据。

官网对Elasticsearch的介绍是Elasticsearch 是一个分布式可扩展近实时的搜索与数据分析引擎。 咱们经过一些核心概念来看下Elasticsearch 是如何作到分布式,可扩展和近实时搜索的。

集群(Cluster)

ES的集群搭建很简单,不须要依赖第三方协调管理组件,自身内部就实现了集群的管理功能。ES集群由一个或多个Elasticsearch节点组成,每一个节点配置相同的 cluster.name 便可加入集群,默认值为 “elasticsearch”。确保不一样的环境中使用不一样的集群名称,不然最终会致使节点加入错误的集群。

一个Elasticsearch服务启动实例就是一个节点(Node)。节点经过node.name来设置节点名称,若是不设置则在启动时给节点分配一个随机通用惟一标识符做为名称。

发现机制

那么有一个问题,ES内部是如何经过一个相同的设置cluster.name 就能将不一样的节点链接到同一个集群的?答案是Zen Discovery

Zen Discovery是Elasticsearch的内置默认发现模块(发现模块的职责是发现集群中的节点以及选举master节点)。它提供单播和基于文件的发现,而且能够扩展为经过插件支持云环境和其余形式的发现。Zen Discovery 与其余模块集成,例如,节点之间的全部通讯都使用Transport模块完成。节点使用发现机制经过Ping的方式查找其余节点。

Elasticsearch 默认被配置为使用单播发现,以防止节点无心中加入集群。只有在同一台机器上运行的节点才会自动组成集群。

若是集群的节点运行在不一样的机器上,使用单播,你能够为 Elasticsearch 提供一些它应该去尝试链接的节点列表。 当一个节点联系到单播列表中的成员时,它就会获得整个集群全部节点的状态,而后它会联系 master 节点,并加入集群。

这意味着单播列表不须要包含集群中的全部节点, 它只是须要足够的节点,当一个新节点联系上其中一个而且说上话就能够了。若是你使用 master 候选节点做为单播列表,你只要列出三个就能够了。 这个配置在 elasticsearch.yml 文件中:

1discovery.zen.ping.unicast.hosts["host1", "host2:port"]
复制代码

节点启动后先 ping ,若是discovery.zen.ping.unicast.hosts 有设置,则 ping 设置中的 host ,不然尝试 ping localhost 的几个端口, Elasticsearch 支持同一个主机启动多个节点, Ping 的 response 会包含该节点的基本信息以及该节点认为的 master 节点。 选举开始,先从各节点认为的 master 中选,规则很简单,按照 id 的字典序排序,取第一个。 若是各节点都没有认为的 master ,则从全部节点中选择,规则同上。

这里有个限制条件就是 discovery.zen.minimum_master_nodes ,若是节点数达不到最小值的限制,则循环上述过程,直到节点数足够能够开始选举。 最后选举结果是确定能选举出一个 master ,若是只有一个 local 节点那就选出的是本身。 若是当前节点是 master ,则开始等待节点数达到 discovery.zen.minimum_master_nodes,而后提供服务。 若是当前节点不是 master ,则尝试加入 master 。 Elasticsearch 将以上服务发现以及选主的流程叫作 ZenDiscovery 。

因为它支持任意数目的集群( 1- N ),因此不能像 Zookeeper 那样限制节点必须是奇数,也就没法用投票的机制来选主,而是经过一个规则,只要全部的节点都遵循一样的规则,获得的信息都是对等的,选出来的主节点确定是一致的。但分布式系统的问题就出在信息不对等的状况,这时候很容易出现脑裂( Split-Brain )的问题,大多数解决方案就是设置一个 quorum 值,要求可用节点必须大于 quorum (通常是超过半数节点),才能对外提供服务。而 Elasticsearch 中,这个 quorum 的配置就是 discovery.zen.minimum_master_nodes

节点的角色

每一个节点既能够是候选主节点也能够是数据节点,经过在配置文件../config/elasticsearch.yml中设置便可,默认都为true

1node.master: true  //是否候选主节点
2node.datatrue    //是否数据节点
复制代码

数据节点负责数据的存储和相关的操做,例如对数据进行增、删、改、查和聚合等操做,因此数据节点(data节点)对机器配置要求比较高,对CPU、内存和I/O的消耗很大。一般随着集群的扩大,须要增长更多的数据节点来提升性能和可用性。

候选主节点能够被选举为主节点(master节点),集群中只有候选主节点才有选举权和被选举权,其余节点不参与选举的工做。主节点负责建立索引、删除索引、跟踪哪些节点是群集的一部分,并决定哪些分片分配给相关的节点、追踪集群中节点的状态等,稳定的主节点对集群的健康是很是重要的。

一个节点既能够是候选主节点也能够是数据节点,可是因为数据节点对CPU、内存核I/0消耗都很大,因此若是某个节点既是数据节点又是主节点,那么可能会对主节点产生影响从而对整个集群的状态产生影响。

所以为了提升集群的健康性,咱们应该对Elasticsearch集群中的节点作好角色上的划分和隔离。可使用几个配置较低的机器群做为候选主节点群。

主节点和其余节点之间经过Ping的方式互检查,主节点负责Ping全部其余节点,判断是否有节点已经挂掉。其余节点也经过Ping的方式判断主节点是否处于可用状态。

虽然对节点作了角色区分,可是用户的请求能够发往任何一个节点,并由该节点负责分发请求、收集结果等操做,而不须要主节点转发,这种节点可称之为协调节点,协调节点是不须要指定和配置的,集群中的任何节点均可以充当协调节点的角色。

脑裂现象

同时若是因为网络或其余缘由致使集群中选举出多个Master节点,使得数据更新时出现不一致,这种现象称之为脑裂,即集群中不一样的节点对于master的选择出现了分歧,出现了多个master竞争。

“脑裂”问题可能有如下几个缘由形成:

  1. 网络问题:集群间的网络延迟致使一些节点访问不到master,认为master挂掉了从而选举出新的master,并对master上的分片和副本标红,分配新的主分片

  2. 节点负载:主节点的角色既为master又为data,访问量较大时可能会致使ES中止响应(假死状态)形成大面积延迟,此时其余节点得不到主节点的响应认为主节点挂掉了,会从新选取主节点。

  3. 内存回收:主节点的角色既为master又为data,当data节点上的ES进程占用的内存较大,引起JVM的大规模内存回收,形成ES进程失去响应。

为了不脑裂现象的发生,咱们能够从缘由着手经过如下几个方面来作出优化措施:

  1. 适当调大响应时间,减小误判
    经过参数discovery.zen.ping_timeout设置节点状态的响应时间,默认为3s,能够适当调大,若是master在该响应时间的范围内没有作出响应应答,判断该节点已经挂掉了。调大参数(如6s,discovery.zen.ping_timeout:6),可适当减小误判。

  2. 选举触发
    咱们须要在候选集群中的节点的配置文件中设置参数discovery.zen.munimum_master_nodes的值,这个参数表示在选举主节点时须要参与选举的候选主节点的节点数,默认值是1,官方建议取值(master_eligibel_nodes/2) + 1,其中master_eligibel_nodes为候选主节点的个数。这样作既能防止脑裂现象的发生,也能最大限度地提高集群的高可用性,由于只要很多于discovery.zen.munimum_master_nodes个候选节点存活,选举工做就能正常进行。当小于这个值的时候,没法触发选举行为,集群没法使用,不会形成分片混乱的状况。

  3. 角色分离
    便是上面咱们提到的候选主节点和数据节点进行角色分离,这样能够减轻主节点的负担,防止主节点的假死状态发生,减小对主节点“已死”的误判。

分片(Shards)

ES支持PB级全文搜索,当索引上的数据量太大的时候,ES经过水平拆分的方式将一个索引上的数据拆分出来分配到不一样的数据块上,拆分出来的数据库块称之为一个分片

这相似于MySql的分库分表,只不过Mysql分库分表须要借助第三方组件而ES内部自身实现了此功能。

在一个多分片的索引中写入数据时,经过路由来肯定具体写入哪个分片中,因此在建立索引的时候须要指定分片的数量,而且分片的数量一旦肯定就不能修改。

分片的数量和下面介绍的副本数量都是能够经过建立索引时的settings来配置,ES默认为一个索引建立5个主分片, 并分别为每一个分片建立一个副本。

1PUT /myIndex
2{
3   "settings" : {
4      "number_of_shards" : 5,
5      "number_of_replicas" : 1
6   }
7}
复制代码

ES经过分片的功能使得索引在规模上和性能上都获得提高,每一个分片都是Lucene中的一个索引文件,每一个分片必须有一个主分片和零到多个副本。

副本(Replicas)

副本就是对分片的Copy,每一个主分片都有一个或多个副本分片,当主分片异常时,副本能够提供数据的查询等操做。主分片和对应的副本分片是不会在同一个节点上的,因此副本分片数的最大值是 n -1(其中n为节点数)。

对文档的新建、索引和删除请求都是写操做,必须在主分片上面完成以后才能被复制到相关的副本分片,ES为了提升写入的能力这个过程是并发写的,同时为了解决并发写的过程当中数据冲突的问题,ES经过乐观锁的方式控制,每一个文档都有一个 _version (版本)号,当文档被修改时版本号递增。一旦全部的副本分片都报告写成功才会向协调节点报告成功,协调节点向客户端报告成功。

从上图能够看出为了达到高可用,Master节点会避免将主分片和副本分片放在同一个节点上。

假设这时节点Node1服务宕机了或者网络不可用了,那么主节点上主分片S0也就不可用了。幸运的是还存在另外两个节点能正常工做,这时ES会从新选举新的主节点,并且这两个节点上存在咱们的所须要的S0的全部数据,咱们会将S0的副本分片提高为主分片,这个提高主分片的过程是瞬间发生的。此时集群的状态将会为 yellow。

为何咱们集群状态是 yellow 而不是 green 呢? 虽然咱们拥有全部的2个主分片,可是同时设置了每一个主分片须要对应两份副本分片,而此时只存在一份副本分片。 因此集群不能为 green 的状态。若是咱们一样关闭了 Node2 ,咱们的程序依然能够保持在不丢任何数据的状况下运行,由于Node3 为每个分片都保留着一份副本。

若是咱们从新启动Node1 ,集群能够将缺失的副本分片再次进行分配,那么集群的状态又将恢复到原来的正常状态。 若是Node1依然拥有着以前的分片,它将尝试去重用它们,只不过这时Node1节点上的分片再也不是主分片而是副本分片了,若是期间有更改的数据只须要从主分片上复制修改的数据文件便可。

小结:

一、将数据分片是为了提升可处理数据的容量和易于进行水平扩展,为分片作副本是为了提升集群的稳定性和提升并发量。
二、副本是乘法,越多消耗越大,但也越保险。分片是除法,分片越多,单分片数据就越少也越分散。
三、副本越多,集群的可用性就越高,可是因为每一个分片都至关于一个Lucene的索引文件,会占用必定的文件句柄、内存及CPU,而且分片间的数据同步也会占用必定的网络带宽,因此索引的分片数和副本数也不是越多越好。

映射(Mapping)

映射是用于定义ES对索引中字段的存储类型、分词方式和是否存储等信息,就像数据库中的 schema ,描述了文档可能具备的字段或属性、每一个字段的数据类型。只不过关系型数据库建表时必须指定字段类型,而ES对于字段类型能够不指定而后动态对字段类型猜想,也能够在建立索引时具体指定字段的类型。

对字段类型根据数据格式自动识别的映射称之为动态映射(Dynamic mapping),咱们建立索引时具体定义字段类型的映射称之为静态映射显示映射(Explicit mapping)

在讲解动态映射和静态映射的使用前,咱们先来了解下ES中的数据有哪些字段类型?以后咱们再讲解为何咱们建立索引时须要创建静态映射而不使用动态映射。

ES(v6.8)中字段数据类型主要有如下几类:

类别 数据类型
核心类型 text, keywords, long, integer, short, double, data, boolean等等
复杂类型 Object, Nested
地理类型 geo_point, geo_shape
特殊类型 ip, completion, token_count, join等等
…….

text 用于索引全文值的字段,例如电子邮件正文或产品说明。这些字段是被分词的,它们经过分词器传递 ,以在被索引以前将字符串转换为单个术语的列表。分析过程容许Elasticsearch搜索单个单词中每一个完整的文本字段。文本字段不用于排序,不多用于聚合。

keyword 用于索引结构化内容的字段,例如电子邮件地址,主机名,状态代码,邮政编码或标签。它们一般用于过滤,排序,和聚合。keyword字段只能按其确切值进行搜索。

经过对字段类型的了解咱们知道有些字段须要明肯定义的,例如某个字段是text类型仍是keword类型差异是很大的,时间字段也许咱们须要指定它的时间格式,还有一些字段咱们须要指定特定的分词器等等。若是采用动态映射是不能精确作到这些的,自动识别经常会与咱们指望的有些差别。

因此建立索引给的时候一个完整的格式应该是指定分片和副本数以及Mapping的定义,以下:

 1PUT my_index 
2{
3   "settings" : {
4      "number_of_shards" : 5,
5      "number_of_replicas" : 1
6   }
7  "mappings": {
8    "_doc": { 
9      "properties": { 
10        "title":    { "type": "text"  }, 
11        "name":     { "type": "text"  }, 
12        "age":      { "type": "integer" },  
13        "created":  {
14          "type":   "date", 
15          "format": "strict_date_optional_time||epoch_millis"
16        }
17      }
18    }
19  }
20}
21

复制代码

基本使用

在决定使用 Elasticsearch 的时候首先要考虑的是版本问题,Elasticsearch (排除 0.x 和 1.x)目前有以下经常使用的稳定的主版本:2.x,5.x,6.x,7.x(current)。你可能会发现没有 3.x 和 4.x,ES 从 2.4.6 直接跳到了 5.0.0。

实际上是为了ELK(ElasticSearch, logstash, kibana)技术栈的版本统一,免的给用户带来混乱。在 Elasticsearch 是 2.x (2.x 的最后一版 2.4.6 的发布时间是 July 25, 2017) 的状况下,kibana 已是 4.x(Kibana 4.6.5 的发布时间是 July 25, 2017),那么在 kibana 的下一主版本确定是 5.x 了,因此 Elasticsearch 直接将本身的主版本发布为 5.0.0 了。统一以后,咱们选版本就不会犹豫困惑了,咱们选定 elasticsearch 的版本后再选择相同版本的 kibana 就好了,不用担心版本不兼容的问题。

Elasticsearch是使用Java构建,因此除了注意 ELK 技术的版本统一,咱们在选择 Elasticsearch 的版本的时候还须要注意 JDK的版本。由于每一个大版本所依赖的 JDK版本也不一样,目前7.2版本已经能够支持 jdk11。

安装使用

一、下载和解压Elasticsearch,无需安装解压后便可用,解压后目录以下。

  • bin:二进制系统指令目录,包含启动命令和安装插件命令等。
  • config: 配置文件目录。
  • data: 数据存储目录。
  • lib:依赖包目录。
  • logs:日志文件目录。
  • modules:模块库,例如x-pack的模块。
  • plugins:插件目录。

二、安装目录下运行 bin/elasticsearch来启动 ES。
三、默认在9200端口运行,请求curl http://localhost:9200/ 或者浏览器输入http://localhost:9200,获得一个 JSON 对象,其中包含当前节点、集群、版本等信息。

 1{
2  "name" : "U7fp3O9",
3  "cluster_name" : "elasticsearch",
4  "cluster_uuid" : "-Rj8jGQvRIelGd9ckicUOA",
5  "version" : {
6    "number" : "6.8.1",
7    "build_flavor" : "default",
8    "build_type" : "zip",
9    "build_hash" : "1fad4e1",
10    "build_date" : "2019-06-18T13:16:52.517138Z",
11    "build_snapshot" : false,
12    "lucene_version" : "7.7.0",
13    "minimum_wire_compatibility_version" : "5.6.0",
14    "minimum_index_compatibility_version" : "5.0.0"
15  },
16  "tagline" : "You Know, for Search"
17}
复制代码

集群健康状态

要检查群集运行情况,咱们能够在 Kibana 控制台中运行如下命令 GET /_cluster/health,获得以下信息:

 1{
2  "cluster_name" : "wujiajian",
3  "status" : "yellow",
4  "timed_out" : false,
5  "number_of_nodes" : 1,
6  "number_of_data_nodes" : 1,
7  "active_primary_shards" : 9,
8  "active_shards" : 9,
9  "relocating_shards" : 0,
10  "initializing_shards" : 0,
11  "unassigned_shards" : 5,
12  "delayed_unassigned_shards" : 0,
13  "number_of_pending_tasks" : 0,
14  "number_of_in_flight_fetch" : 0,
15  "task_max_waiting_in_queue_millis" : 0,
16  "active_shards_percent_as_number" : 64.28571428571429
17}
复制代码

集群状态经过 绿,黄,红 来标识

  • 绿色:集群健康无缺,一切功能齐全正常,全部分片和副本均可以正常工做。
  • 黄色:预警状态,全部主分片功能正常,但至少有一个副本是不能正常工做的。此时集群是能够正常工做的,可是高可用性在某种程度上会受影响。
  • 红色:集群不可正常使用。某个或某些分片及其副本异常不可用,这时集群的查询操做还能执行,可是返回的结果会不许确。对于分配到这个分片的写入请求将会报错,最终会致使数据的丢失。

当集群状态为红色时,它将会继续从可用的分片提供搜索请求服务,可是你须要尽快修复那些未分配的分片。

机制原理

ES的基本概念和基本操做介绍完了以后咱们可能还有不少疑惑,它们内部是如何运行的?主分片和副本分片是如何同步的?建立索引的流程是什么样的?ES如何将索引数据分配到不一样的分片上的?以及这些索引数据是如何存储的?为何说ES是近实时搜索引擎而文档的 CRUD (建立-读取-更新-删除) 操做是实时的?以及Elasticsearch 是怎样保证更新被持久化在断电时也不丢失数据?还有为何删除文档不会马上释放空间?带着这些疑问咱们进入接下来的内容。

写索引原理

下图描述了3个节点的集群,共拥有12个分片,其中有4个主分片(S0、S一、S二、S3)和8个副本分片(R0、R一、R二、R3),每一个主分片对应两个副本分片,节点1是主节点(Master节点)负责整个集群的状态。

写索引是只能写在主分片上,而后同步到副本分片。这里有四个主分片,一条数据ES是根据什么规则写到特定分片上的呢?
这条索引数据为何被写到S0上而不写到S1或S2上?那条数据为何又被写到S3上而不写到S0上了?

首先这确定不会是随机的,不然未来要获取文档的时候咱们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:

1shard = hash(routing) % number_of_primary_shards
复制代码

routing 是一个可变值,默认是文档的 _id ,也能够设置成一个自定义的值。 routing 经过 hash 函数生成一个数字,而后这个数字再除以 number_of_primary_shards (主分片的数量)后获得余数 。这个在 0 到 number_of_primary_shards-1 之间的余数,就是咱们所寻求的文档所在分片的位置。

这就解释了为何咱们要在建立索引的时候就肯定好主分片的数量而且永远不会改变这个数量:由于若是数量变化了,那么全部以前路由的值都会无效,文档也再也找不到了。

因为在ES集群中每一个节点经过上面的计算公式都知道集群中的文档的存放位置,因此每一个节点都有处理读写请求的能力。在一个写请求被发送到某个节点后,该节点即为前面说过的协调节点,协调节点会根据路由公式计算出须要写到哪一个分片上,再将请求转发到该分片的主分片节点上。

假如此时数据经过路由计算公式取余后获得的值是 shard = hash(routing) % 4 = 0,则具体流程以下:

  1. 客户端向ES1节点(协调节点)发送写请求,经过路由计算公式获得值为0,则当前数据应被写到主分片S0上。
  2. ES1节点将请求转发到S0主分片所在的节点ES3,ES3接受请求并写入到磁盘。
  3. 并发将数据复制到两个副本分片R0上,其中经过乐观并发控制数据的冲突。一旦全部的副本分片都报告成功,则节点ES3将向协调节点报告成功,协调节点向客户端报告成功。

存储原理

上面介绍了在ES内部索引的写处理流程,这个流程是在ES的内存中执行的,数据被分配到特定的分片和副本上以后,最终是存储到磁盘上的,这样在断电的时候就不会丢失数据。具体的存储路径可在配置文件../config/elasticsearch.yml中进行设置,默认存储在安装目录的data文件夹下。建议不要使用默认值,由于若ES进行了升级,则有可能致使数据所有丢失。

1path.data: /path/to/data  //索引数据
2path.logs: /path/to/logs  //日志记录
复制代码

分段存储

索引文档以段的形式存储在磁盘上,何为?索引文件被拆分为多个子文件,则每一个子文件叫做, 每个段自己都是一个倒排索引,而且段具备不变性,一旦索引的数据被写入硬盘,就不可再修改。在底层采用了分段的存储模式,使它在读写时几乎彻底避免了锁的出现,大大提高了读写性能。

段被写入到磁盘后会生成一个提交点,提交点是一个用来记录全部提交后段信息的文件。一个段一旦拥有了提交点,就说明这个段只有读的权限,失去了写的权限。相反,当段在内存中时,就只有写的权限,而不具有读数据的权限,意味着不能被检索。

的概念提出主要是由于:在早期全文检索中为整个文档集合创建了一个很大的倒排索引,并将其写入磁盘中。若是索引有更新,就须要从新全量建立一个索引来替换原来的索引。这种方式在数据量很大时效率很低,而且因为建立一次索引的成本很高,因此对数据的更新不能过于频繁,也就不能保证时效性。

索引文件分段存储而且不可修改,那么新增、更新和删除如何处理呢?

  • 新增,新增很好处理,因为数据是新的,因此只须要对当前文档新增一个段就能够了。
  • 删除,因为不可修改,因此对于删除操做,不会把文档从旧的段中移除而是经过新增一个.del文件,文件中会列出这些被删除文档的段信息。这个被标记删除的文档仍然能够被查询匹配到, 但它会在最终结果被返回前从结果集中移除。
  • 更新,不能修改旧的段来进行反映文档的更新,其实更新至关因而删除和新增这两个动做组成。会将旧的文档在.del文件中标记删除,而后文档的新版本被索引到一个新的段中。可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就会被移除。

段被设定为不可修改具备必定的优点也有必定的缺点,优点主要表如今:

  • 不须要锁。若是你历来不更新索引,你就不须要担忧多进程同时修改数据的问题。
  • 一旦索引被读入内核的文件系统缓存,便会留在哪里,因为其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提高。
  • 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不须要在每次数据改变时被重建,由于数据不会变化。
  • 写入单个大的倒排索引容许数据被压缩,减小磁盘 I/O 和 须要被缓存到内存的索引的使用量。

段的不变性的缺点以下:

  • 当对旧数据进行删除时,旧数据不会立刻被删除,而是在.del文件中被标记为删除。而旧数据只能等到段更新时才能被移除,这样会形成大量的空间浪费。
  • 如有一条数据频繁的更新,每次更新都是新增新的标记旧的,则会有大量的空间浪费。
  • 每次新增数据时都须要新增一个段来存储数据。当段的数量太多时,对服务器的资源例如文件句柄的消耗会很是大。
  • 在查询的结果中包含全部的结果集,须要排除被标记删除的旧数据,这增长了查询的负担。

延迟写策略

介绍完了存储的形式,那么索引是写入到磁盘的过程是这怎样的?是不是直接调 fsync 物理性地写入磁盘?

答案是显而易见的,若是是直接写入到磁盘上,磁盘的I/O消耗上会严重影响性能,那么当写数据量大的时候会形成ES停顿卡死,查询也没法作到快速响应。若是真是这样ES也就不会称之为近实时全文搜索引擎了。

为了提高写的性能,ES并无每新增一条数据就增长一个段到磁盘上,而是采用延迟写的策略。

每当有新增的数据时,就将其先写入到内存中,在内存和磁盘之间是文件系统缓存,当达到默认的时间(1秒钟)或者内存的数据达到必定量时,会触发一次刷新(Refresh),将内存中的数据生成到一个新的段上并缓存到文件缓存系统 上,稍后再被刷新到磁盘中并生成提交点

这里的内存使用的是ES的JVM内存,而文件缓存系统使用的是操做系统的内存。新的数据会继续的被写入内存,但内存中的数据并非以段的形式存储的,所以不能提供检索功能。由内存刷新到文件缓存系统的时候会生成了新的段,并将段打开以供搜索使用,而不须要等到被刷新到磁盘。

在 Elasticsearch 中,写入和打开一个新段的轻量的过程叫作 refresh (即内存刷新到文件缓存系统)。 默认状况下每一个分片会每秒自动刷新一次。这就是为何咱们说 Elasticsearch 是近实时搜索,由于文档的变化并非当即对搜索可见,但会在一秒以内变为可见。咱们也能够手动触发 refresh,POST /_refresh 刷新全部索引,POST /nba/_refresh刷新指定的索引。

Tips: 尽管刷新是比提交轻量不少的操做,它仍是会有性能开销。 当写测试的时候, 手动刷新颇有用,可是不要在生产> 环境下每次索引一个文档都去手动刷新。并且并非全部的状况都须要每秒刷新。可能你正在使用 Elasticsearch 索引大量的日志文件, 你可能想优化索引速度而不是> 近实时搜索, 这时能够在建立索引时在settings中经过调大refresh_interval = "30s" 的值 , 下降每一个索引的刷新频率,设值时须要注意后面带上时间单位,不然默认是毫秒。当refresh_interval = -1时表示关闭索引的自动刷新。

虽然经过延时写的策略能够减小数据往磁盘上写的次数提高了总体的写入能力,可是咱们知道文件缓存系统也是内存空间,属于操做系统的内存,只要是内存都存在断电或异常状况下丢失数据的危险。

为了不丢失数据,Elasticsearch添加了事务日志(Translog),事务日志记录了全部尚未持久化到磁盘的数据。添加了事务日志后整个写索引的流程以下图所示。

  • 一个新文档被索引以后,先被写入到内存中,可是为了防止数据的丢失,会追加一份数据到事务日志中。不断有新的文档被写入到内存,同时也都会记录到事务日志中。这时新数据还不能被检索和查询。
  • 当达到默认的刷新时间或内存中的数据达到必定量后,会触发一次 refresh,将内存中的数据以一个新段形式刷新到文件缓存系统中并清空内存。这时虽然新段未被提交到磁盘,可是能够提供文档的检索功能且不能被修改。
  • 随着新文档索引不断被写入,当日志数据大小超过512M或者时间超过30分钟时,会触发一次 flush。内存中的数据被写入到一个新段同时被写入到文件缓存系统,文件系统缓存中数据经过 fsync 刷新到磁盘中,生成提交点,日志文件被删除,建立一个空的新日志。

经过这种方式当断电或须要重启时,ES不只要根据提交点去加载已经持久化过的段,还须要工具Translog里的记录,把未持久化的数据从新持久化到磁盘上,避免了数据丢失的可能。

段合并

因为自动刷新流程每秒会建立一个新的段 ,这样会致使短期内的段数量暴增。而段数目太多会带来较大的麻烦。 每个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每一个搜索请求都必须轮流检查每一个段而后合并查询结果,因此段越多,搜索也就越慢。

Elasticsearch经过在后台按期进行段合并来解决这个问题。小的段被合并到大的段,而后这些大的段再被合并到更大的段。段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档不会被拷贝到新的大段中。合并的过程当中不会中断索引和搜索。

段合并在进行索引和搜索时会自动进行,合并进程选择一小部分大小类似的段,而且在后台将它们合并到更大的段中,这些段既能够是未提交的也能够是已提交的。合并结束后老的段会被删除,新的段被 flush 到磁盘,同时写入一个包含新段且排除旧的和较小的段的新提交点,新的段被打开能够用来搜索。

段合并的计算量庞大, 并且还要吃掉大量磁盘 I/O,段合并会拖累写入速率,若是任其发展会影响搜索性能。Elasticsearch在默认状况下会对合并流程进行资源限制,因此搜索仍然有足够的资源很好地执行。

性能优化

存储设备

磁盘在现代服务器上一般都是瓶颈。Elasticsearch 重度使用磁盘,你的磁盘能处理的吞吐量越大,你的节点就越稳定。这里有一些优化磁盘 I/O 的技巧:

  • 使用 SSD。就像其余地方提过的, 他们比机械磁盘优秀多了。
  • 使用 RAID 0。条带化 RAID 会提升磁盘 I/O,代价显然就是当一块硬盘故障时整个就故障了。不要使用镜像或者奇偶校验 RAID 由于副本已经提供了这个功能。
  • 另外,使用多块硬盘,并容许 Elasticsearch 经过多个 path.data 目录配置把数据条带化分配到它们上面。
  • 不要使用远程挂载的存储,好比 NFS 或者 SMB/CIFS。这个引入的延迟对性能来讲彻底是背道而驰的。
  • 若是你用的是 EC2,小心 EBS。即使是基于 SSD 的 EBS,一般也比本地实例的存储要慢。

内部索引优化

Elasticsearch为了能快速找到某个term,先将全部的term排个序,而后根据二分法查找term,时间复杂度为logN,就像经过字典查找同样,这就是Term Dictionary。如今再看起来,彷佛和传统数据库经过B-Tree的方式相似。

可是若是term太多,term dictionary也会很大,放内存不现实,因而有了Term Index,就像字典里的索引页同样,A开头的有哪些term,分别在哪页,能够理解term index是一颗树。这棵树不会包含全部的term,它包含的是term的一些前缀。经过term index能够快速地定位到term dictionary的某个offset,而后从这个位置再日后顺序查找。

在内存中用FST方式压缩term index,FST以字节的方式存储全部的term,这种压缩方式能够有效的缩减存储空间,使得term index足以放进内存,但这种方式也会致使查找时须要更多的CPU资源。演示地址:Build your own FST

对于存储在磁盘上的倒排表一样也采用了压缩技术减小存储所占用的空间,更多能够阅读 Frame of Reference and Roaring Bitmaps

调整配置参数

  • 给每一个文档指定有序的具备压缩良好的序列模式ID,避免随机的UUID-4 这样的 ID,这样的ID压缩比很低,会明显拖慢 Lucene。

  • 对于那些不须要聚合和排序的索引字段禁用Doc values。Doc Values是有序的基于document => field value的映射列表;

  • 不须要作模糊检索的字段使用 keyword类型代替 text 类型,这样能够避免在创建索引前对这些文本进行分词。

  • 若是你的搜索结果不须要近实时的准确度,考虑把每一个索引的 index.refresh_interval 改到 30s 。若是你是在作大批量导入,导入期间你能够经过设置这个值为 -1 关掉刷新,还能够经过设置 index.number_of_replicas: 0关闭副本。别忘记在完工的时候从新开启它。

  • 避免深度分页查询建议使用Scroll进行分页查询。普通分页查询时,会建立一个from + size的空优先队列,每一个分片会返回from + size 条数据,默认只包含文档id和得分score给协调节点,若是有n个分片,则协调节点再对(from + size)× n 条数据进行二次排序,而后选择须要被取回的文档。当from很大时,排序过程会变得很沉重占用CPU资源严重。

  • 减小映射字段,只提供须要检索,聚合或排序的字段。其余字段可存在其余存储设备上,例如Hbase,在ES中获得结果后再去Hbase查询这些字段。

  • 建立索引和查询时指定路由routing值,这样能够精确到具体的分片查询,提高查询效率。路由的选择须要注意数据的分布均衡。

JVM调优

  • 确保堆内存最小值( Xms )与最大值( Xmx )的大小是相同的,防止程序在运行时改变堆内存大小。
    Elasticsearch 默认安装后设置的堆内存是 1 GB。可经过../config/jvm.option文件进行配置,可是最好不要超过物理内存的50%和超过32GB。

  • GC 默认采用CMS的方式,并发可是有STW的问题,能够考虑使用G1收集器。

  • ES很是依赖文件系统缓存(Filesystem Cache),快速搜索。通常来讲,应该至少确保物理上有一半的可用内存分配到文件系统缓存。


我的公众号:小菜亦牛

欢迎长按下图关注公众号:小菜亦牛!

按期为你奉上分布式,微服务等一线互联网公司相关技术的讲解和分析。