elasticsearch高级篇:核心概念和实现原理

1.elasticsearch核心概念

1.1 索引(index)

一个索引就是一个拥有几分相似特征的文档的集合。比如说,你可以有一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。一个索引由一个名字来标识(必须全部是小写字母),并且当我们要对这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。在一个集群中,可以定义任意多的索引。

能搜索的数据必须索引,这样的好处是可以提高查询速度,比如:新华字典前面的目录就是索引的意思,目录可以提高查询速度。

注意:我们平时使用索引这个词在不同环境具有不同语义

名词:一个elasticsearch集群中,可以创建很多个不同的索引,倒排索引或者关系型数据库中的b+树索引

动词:保存一个文档doc到elasticsearch中的过程也叫索引(indexing)

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用
Github地址https://github.com/plasticene/plasticene-boot-starter-parent
Gitee地址https://gitee.com/plasticene3/plasticene-boot-starter-parent

1.2 类型(type)

elasticsearch在一个索引中,你可以定义一种或多种类型,但是后来elasticsearch升级版本中,不断弱化type这个概念,直到elasticsearch7.0以后type正式在es中废除,7.0以后默认不再支持自定义索引类型,新建索引时会默认添加一个类型_doc,所以在之前elasticsearch基础篇说到的elasticsearch和关系型数据库的类别就不太准确,之前说索引对应mysql的数据库,这里我觉得索引对应mysql的表更合适。

1.3 文档(Document)

一个文档是一个可被索引的基础信息单元,也就是一条数据

比如:你可以拥有某一个客户的文档,某一个产品的一个文档,当然,也可以拥有某个订单的一个文档。文档以JSON(Javascript Object Notation)格式来表示,而JSON是一个到处存在的互联网数据交互格式。

在一个index中,你可以存储任意多的文档

1.4 字段(Field)

相当于是数据表的字段,对文档数据根据不同属性进行的分类标识

1.5 映射(mapping)

mapping是处理数据的方式和规则方面做一些限制,如:某个字段的数据类型、默认值、分析器、是否被索引等等。这些都是映射里面可以设置的,其它就是处理ES里面数据的一些使用规则设置也叫做映射,按照最优规则处理数据对性能提高很大,因此才需要建立映射,并且需要思考如何建立映射才能对性能更好。我们在创建索引时候添加mapping定义,如下所示:添加一个student索引并设置mapping

PUT student{  "settings": {    "number_of_shards": 3,     "number_of_replicas": 2    },  "mappings": {    "properties": {      "name":{      "type":"text"            },    "sex":{      "type":"keyword"      },    "age":{      "type":"long"      },    "rank":{      "type":"integer",      "index":"false"      }    }  }}

当我们创建索引没有设置mapping时,然后直接往索引里面写入文档,elasticsearch会根据我们写入的文档自动生成对应的mapping,但是有时候mapping的有些字段设置不是我们想要的,需要修改。个人建议我们在创建自己索引之前,可以先随便创建一个测试索引,然后写入我们的文档,然后查看测试索引生成的mapping,这时候我们在创建自己真正索引设置mapping时,就可以在前面得到的mapping上修改,不需要从无到有开始写mapping,极大提高效率。

1.6 分片(Shards)

一个索引可以存储超出单个节点硬件限制的大量数据。比如,一个具有10亿文档数据的索引占据1TB的磁盘空间,而任一节点都可能没有这样大的磁盘空间。或者单个节点处理搜索请求,响应太慢。为了解决这个问题,Elasticsearch提供了将索引划分成多份的能力,每一份就称之为分片。当你创建一个索引的时候,你可以指定你想要的分片的数量。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上。

分片很重要,主要有两方面的原因:

1)允许你水平分割 / 扩展你的内容容量。

2)允许你在分片之上进行分布式的、并行的操作,进而提高性能/吞吐量。

至于一个分片怎样分布,它的文档怎样聚合和搜索请求,是完全由Elasticsearch管理的,对于作为用户的你来说,这些都是透明的,无需过分关心。

被混淆的概念是,一个 Lucene 索引 我们在 Elasticsearch 称作 分片 。一个 Elasticsearch 索引 是分片的集合。当 Elasticsearch 在索引中搜索的时候, 他发送查询到每一个属于索引的分片(Lucene 索引),然后合并每个分片的结果到一个全局的结果集。

1.7 副本(Replicas)

在一个节点环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了,这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的。为此目的,Elasticsearch允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片(副本)。

复制分片之所以重要,有两个主要原因:

1)在分片/节点失败的情况下,提供了高可用性。因为这个原因,注意到复制分片从不与原/主要(original/primary)分片置于同一节点上是非常重要的

2)扩展你的搜索量/吞吐量,因为搜索可以在所有的副本上并行运行

总之,每个索引可以被分成多个分片。一个索引也可以被复制0次(意思是没有复制)或多次。一旦复制了,每个索引就有了主分片(作为复制源的原来的分片)和复制分片(主分片的拷贝)之别。分片和复制的数量可以在索引创建的时候指定。在索引创建之后,你可以在任何时候动态地改变复制的数量,但是你事后不能改变分片的数量。分片数量不能改变的原因下文会阐述。默认情况下,Elasticsearch中的每个索引被分片1个主分片和1个复制,这意味着,如果你的集群中至少有两个节点,注意,集群如果只有两个节点可能产生脑裂问题。对于脑裂问题,后面会讲述。你的索引将会有1个主分片和另外1个复制分片(1个完全拷贝),这样的话每个索引总共就有2个分片,我们需要根据索引需要确定分片个数。

1.8 分配(Allocation)

将分片分配给某个节点的过程,包括分配主分片或者副本。如果是副本,还包含从主分片复制数据的过程。这个过程是由master节点完成的。

2.elasticsearch系统架构与集群

2.1 系统架构

一个运行中的Elasticsearch 实例称为一个节点,而集群是由一个或者多个拥有相同cluster.name 配置的节点组成,它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。系统架构如下图所示:

当一个节点被选举成为主节点时,它将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。而主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。任何节点都可以成为主节点。

作为用户,我们可以将请求发送到集群中的任何节点,包括主节点。每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回给客户端。Elasticsearch 对这一切的管理都是透明的。

2.2 单节点集群部署

我们创建一个users,指定分片数为3,副本数是1

PUT users{  "settings": {    "number_of_shards": 3    , "number_of_replicas": 1  }}

这时候我们通过elasticsearch-head插件查看集群情况:

2.3 故障转移

当集群中只有一个节点在运行时,意味着会有一个单点故障问题——没有冗余。幸运的是,我们只需再启动一个节点即可防止数据丢失。当你在同一台机器上启动了第二个节点时,只要它和第一个节点有同样的cluster.name 配置,它就会自动发现集群并加入到其中。但是在不同机器上启动节点的时候,为了加入到同一集群,你需要配置一个可连接到的单播主机列表。之所以配置为使用单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。

如果启动了第二个节点,我们的集群将会拥有两个节点的集群: 所有主分片和副本分片都已被分配

2.4 水平扩容

怎样为我们的正在增长中的应用程序按需扩容呢?当启动了第三个节点,我们的集群将会拥有三个节点的集群: 为了分散负载而对分片进行重新分配

主分片的数目在索引创建时就已经确定了下来。实际上,这个数目定义了这个索引能够存储的最大数据量。(实际大小取决于你的数据、硬件和使用场景。)但是,读操作——搜索和返回数据——可以同时被主分片或副本分片所处理,所以当你拥有越多的副本分片时,也将拥有越高的吞吐量。

在运行中的集群上是可以动态调整副本分片数目的,我们可以按需伸缩集群。当然,**如果只是在相同节点数目的集群上增加更多的副本分片并不能提高性能,因为每个分片从节点上获得的资源会变少。你需要增加更多的硬件资源来提升吞吐量。**但是更多的副本分片数提高了数据冗余量:按照上面的节点配置,我们可以在失去2个节点的情况下不丢失任何数据。

这时候我们集群有3个节点,假如此时我们master主节点node-1发生故障不可用,es集群是怎样个状况呢?

我们关闭的节点是一个主节点。而集群必须拥有一个主节点来保证正常工作,所以发生的第一件事情就是选举一个新的主节点:Node 2 。在我们关闭Node 1 的同时也失去了主分片1 和2 ,并且在缺失主分片的时候索引也不能正常工作。如果此时来检查集群的状况,我们看到的状态将会为red :不是所有主分片都在正常工作。

幸运的是,在其它节点上存在着这两个主分片的完整副本,所以新的主节点立即将这些分片在Node 2 和Node 3 上对应的副本分片提升为主分片。这个提升主分片的过程是瞬间发生的,如同按下一个开关一般。

2.5 路由计算

当索引一个文档的时候,文档会被存储到一个主分片中。Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?当我们创建文档时,它如何决定这个文档应当被存储在分片1 还是分片2 中呢?首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:

routing 是一个可变值,默认是文档的_id ,也可以设置成一个自定义的值。routing 通过hash 函数生成一个数字,然后这个数字再除以number_of_primary_shards (主分片的数量)后得到余数。这个分布在0 到number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。这也是为什么我们要在创建索引的时候就确定好主分片的数量并且永远不会改变这个数量的原因:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了

所有的文档API(get 、index 、delete 、bulk 、update 以及mget )都接受一个叫做routing 的路由参数,通过这个参数我们可以自定义文档到分片的映射。一个自定义的路由参数可以用来确保所有相关的文档——例如所有属于同一个用户的文档——都被存储到同一个分片中。

2.6 es节点特性

我们可以发送请求到集群中的任一节点。每个节点都有能力处理任意请求。每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上,每个节点都能扮演协调节点(coordinating node) 角色。

当发送请求的时候,为了扩展负载,更好的做法是轮询集群中所有的节点。

3.elasticsearch增删改查操作流程

3.1 写流程

我们向三个节点组成集群中一个有2个分片,每个分片2个副本的索引中写文档数据,流程如下图所示:

具体流程执行步骤:

1)客户端向Node 1 发送新建、索引或者删除请求。此时 node 1就是我们的协调节点

2)节点使用文档的_id 确定文档属于分片0 。请求会被转发到Node 3,因为分片0 的主分片目前被分配在Node 3 上。

3)Node 3 在主分片上面执行请求。如果成功了,它将请求并行转发到Node 1 和Node 2 的副本分片上。一旦所有的副本分片都报告成功, Node 3 将向协调节点报告成功,协调节点向客户端报告成功。

3.2 读文档

我们可以从主分片或者从其它任意副本分片检索文档

具体步骤如下:

1)客户端向Node 1 发送获取请求。

2) 节点使用文档的_id 来确定文档属于分片0 。分片0 的副本分片存在于所有的三个节点上。在这种情况下,协调节点通过轮训方式访问分片节点,这里假设它将请求转发到Node 2 。

3) Node 2 将文档返回给Node 1 ,然后将文档返回给客户端。

在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。

3.3 更新文档

es的更新文档其实就是结合读流程和写流程,如下所示:

部分更新一个文档的步骤如下:

1)客户端向Node 1 发送更新请求。

2) 它将请求转发到主分片所在的Node 3 。

3)Node 3 从主分片检索文档,修改_source 字段中的JSON ,并且尝试重新索引主分片的文档。如果文档已经被另一个进程修改,它会重试步骤3 ,超过retry_on_conflict 次后放弃。

4)如果Node 3 成功地更新文档,它将新版本的文档并行转发到Node 1 和Node 2 上的副本分片,重新建立索引。一旦所有副本分片都返回成功,Node 3 向协调节点也返回成功,协调节点向客户端返回成功。

当主分片把更改转发到副本分片时,它不会转发更新请求。相反,它转发完整文档的新版本。请记住,这些更改将会异步转发到副本分片,并且不能保证它们以发送它们相同的顺序到达。如果Elasticsearch仅转发更改请求,则可能以错误的顺序应用更改,导致得到损坏的文档。

4.倒排索引和文档存储

4.1 倒排索引

传统的数据库每个字段存储单个值,但这对全文检索并不够。文本字段中的每个单词需要被搜索,对数据库意味着需要单个字段有索引多值的能力。最好的支持是一个字段多个值需求的数据结构是倒排索引。

Elasticsearch 使用一种称为倒排索引的结构,它适用于快速的全文搜索。

见其名,知其意,有倒排索引,肯定会对应有正向索引。正向索引(forward index),反向索引(inverted index)更熟悉的名字是倒排索引。所谓的正向索引,就是搜索引擎会将待搜索的文件都对应一个文件ID,搜索时将这个ID和搜索关键字进行对应,形成K-V对,然后对关键字进行统计计数。但是互联网上收录在搜索引擎中的文档的数目是个天文数字,这样的正向索引结构根本无法满足实时返回排名结果的要求。所以,搜索引擎会将正向索引重新构建为倒排索引,即把文件ID对应到关键词的映射转换为关键词到文件ID的映射,每个关键词都对应着一系列的文件,这些文件中都出现这个关键词。

一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。例如,假设我们有两个文档,每个文档的content 域包含如下内容:

  • The quick brown fox jumped over the lazy dog

  • Quick brown foxes leap over lazy dogs in summer

为了创建倒排索引,我们首先将每个文档的content 域拆分成单独的词(我们称它为词条或tokens ),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。

4.2 倒排索引不变性

早期的全文检索会为整个文档集合建立一个很大的倒排索引并将其写入到磁盘。一旦新的索引就绪,旧的就会被其替换,这样最近的变化便可以被检索到。

倒排索引被写入磁盘后是不可改变的:它永远不会修改。

不变性有重要的价值:

  • 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。

  • 一旦索引被读入内核的文件系统缓存,便会留在那里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。

  • 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。

  • 写入单个大的倒排索引允许数据被压缩,减少磁盘I/O 和需要被缓存到内存的索引的使用量。

当然,一个不变的索引也有不好的地方。主要事实是它是不可变的,你不能修改它。如果你需要让一个新的文档可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制

4.3 动态更新索引

如何在保留不变性的前提下实现倒排索引的更新?

答案是: 用更多的索引。通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到,从最早的开始查询完后再对结果进行合并。

Elasticsearch 基于Lucene, 这个java库引入了按段搜索的概念。每一段本身都是一个倒排索引,但索引在Lucene 中除表示所有段的集合外,还增加了提交点的概念,提交点就是一个列出了所有已知段的文件。

按段搜索会以如下流程执行:

1)新文档被收集到内存索引缓存,如下所示:一个在内存缓存中包含新文档的 Lucene 索引

2)不时地,缓存会被提交

  • 一个新的段:一个追加的倒排索引:被写入磁盘。

  • 一个新的包含新段名字的提交点被写入磁盘。

  • 磁盘进行 同步 :所有在文件系统缓存中等待的写入都刷新到磁盘,以确保它们被写入物理文件。

3)新的段被开启,让它包含的文档可见以被搜索。

4)内存缓存被清空,等待接收新的文档。

当一个查询被触发,所有已知的段按顺序被查询。词项统计会对所有段的结果进行聚合,以保证每个词和每个文档的关联都被准确计算。这种方式可以用相对较低的成本将新文档添加到索引。

4.4 文档删除和更新

段是不可改变的,所以既不能把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。取而代之的是,每个提交点会包含一个 .del 文件,文件中会列出这些被删除文档的段信息。

当一个文档被 “删除” 时,它实际上只是在 .del 文件中被 标记 删除。一个被标记删除的文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。

文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。

4.5 elasticsearch近实时搜索

随着按段(per-segment)搜索的发展,一个新的文档从索引到可被搜索的延迟显著降低了。新文档在几分钟之内即可被检索,但这样还是不够快。磁盘在这里成为了瓶颈。提交(Commiting)一个新的段到磁盘需要一个fsync(同步内存中所有已修改的文件数据到储存设备) 来确保段被物理性地写入磁盘,这样在断电的时候就不会丢失数据。但是fsync 操作代价很大; 如果每次索引一个文档都去执行一次的话会造成很大的性能问题。

我们需要的是一个更轻量的方式来使一个文档可被搜索,这意味着fsync 要从整个过程中被移除。在Elasticsearch和磁盘之间是文件系统缓存。像之前描述的一样,在内存索引缓冲区中的文档会被写入到一个新的段中。但是这里新段会被先写入到文件系统缓存—这一步代价会比较低,稍后再被刷新到磁盘—这一步代价比较高。不过只要文件已经在缓存中,就可以像其它文件一样被打开和读取了。

Lucene 允许新段被写入和打开,从而使其包含的文档在未进行一次完整提交时便对搜索可见。这种方式比进行一次完整提交代价要小得多,并且在不影响性能的前提下可以被频繁地执行。

如下所示:缓冲区的内容已经被写入一个可被搜索的段中,但还没有进行提交

在Elasticsearch 中,写入和打开一个新段的轻量的过程叫做refresh默认情况下每个分片会每秒自动刷新一次。这就是为什么我们说Elasticsearch 是近实时搜索: 文档的变化并不是立即对搜索可见,但会在一秒之内变为可见

并不是所有的情况都需要每秒刷新。可能你正在使用Elasticsearch 索引大量的日志文件,你可能想优化索引速度而不是近实时搜索,可以通过设置refresh_interval ,降低每个索引的刷新频率

PUT /my_logs{  "settings": {    "refresh_interval": "30s"   }}

refresh_interval 可以在既存索引上进行动态更新。在生产环境中,当你正在建立一个大的新索引时,可以先关闭自动刷新,待开始使用该索引时,再把它们调回来:

PUT /my_logs/_settings{ "refresh_interval": -1 } PUT /my_logs/_settings{ "refresh_interval": "1s" } 
4.6 持久化

如果没有用 fsync 把数据从文件系统缓存刷(flush)到硬盘,我们不能保证数据在断电甚至是程序正常退出之后依然存在。为了保证 Elasticsearch 的可靠性,需要确保数据变化被持久化到磁盘。

在前面我们说过一次完整的提交会将段刷到磁盘,并写入一个包含所有段列表的提交点。Elasticsearch 在启动或重新打开一个索引的过程中使用这个提交点来判断哪些段隶属于当前分片。

即使通过每秒刷新(refresh)实现了近实时搜索,我们仍然需要经常进行完整提交来确保能从失败中恢复。但在两次提交之间发生变化的文档怎么办?我们也不希望丢失掉这些数据。

Elasticsearch 增加了一个 translog ,或者叫事务日志,在每一次对 Elasticsearch 进行操作时均进行了日志记录。通过 translog ,整个流程看起来是下面这样:

1)一个文档被索引之后,就会被添加到内存缓冲区,并且追加到了translog

2)刷新(refresh):分片每秒被刷新(refresh)一次:

  • 这些在内存缓冲区的文档被写入到一个新的段中,且没有进行fsync 操作。

  • 这个段被打开,使其可被搜索

  • 内存缓冲区被清空

3)这个进程继续工作,更多的文档被添加到内存缓冲区和追加到事务日志,事务日志不断积累文档

4)每隔一段时间—例如translog 变得越来越大—索引被刷新(flush);一个新的translog 被创建,并且一个全量提交被执行

  • 所有在内存缓冲区的文档都被写入一个新的段。

  • 缓冲区被清空。

  • 一个提交点被写入硬盘。

  • 文件系统缓存通过fsync 被刷新(flush)。

  • 老的translog 被删除。

translog 提供所有还没有被刷到磁盘的操作的一个持久化纪录。当Elasticsearch 启动的时候,它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放translog 中所有在最后一次提交后发生的变更操作。

translog 也被用来提供实时CRUD 。当你试着通过ID查询、更新、删除一个文档,它会在尝试从相应的段中检索之前,首先检查translog 任何最近的变更。这意味着它总是能够实时地获取到文档的最新版本。

执行一个提交并且截断translog 的行为在Elasticsearch 被称作一次flush。分片每30分钟被自动刷新(flush),或者在translog 太大的时候也会刷新.

translog 的目的是保证操作不会丢失。这引出了这个问题:Translog 有多安全?

在文件被 fsync 到磁盘前,被写入的文件在重启之后就会丢失。默认 translog 是每 5 秒被 fsync 刷新到硬盘, 或者在每次写请求完成之后执行(e.g. index, delete, update, bulk)。这个过程在主分片和复制分片都会发生。最终, 基本上,这意味着在整个请求被 fsync 到主分片和复制分片的translog之前,你的客户端不会得到一个 200 OK 响应。

在每次请求后都执行一个 fsync 会带来一些性能损失,尽管实践表明这种损失相对较小(特别是bulk导入,它在一次请求中平摊了大量文档的开销)。

但是对于一些大容量的偶尔丢失几秒数据问题也并不严重的集群,使用异步的 fsync 还是比较有益的。比如,写入的数据被缓存到内存中,再每5秒执行一次 fsync

这个行为可以通过设置 durability 参数为 async 来启用:

PUT /my_index/_settings{    "index.translog.durability": "async",    "index.translog.sync_interval": "5s"}

这个选项可以针对索引单独设置,并且可以动态进行修改。如果你决定使用异步 translog 的话,你需要 保证 在发生crash时,丢失掉 sync_interval 时间段的数据也无所谓。请在决定前知晓这个特性。

如果你不确定这个行为的后果,最好是使用默认的参数( "index.translog.durability": "request" )来避免数据丢失。

4.7 段合并

由于自动刷新流程每秒会创建一个新的段,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。每一个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。

Elasticsearch通过在后台进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。

段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中。

启动段合并不需要你做任何事。进行索引和搜索时会自动进行。

1)当索引的时候,刷新(refresh)操作会创建新的段并将段打开以供搜索使用。

2)合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中。这并不会中断索引和搜索。

3)一旦合并结束,老的段被删除

  • 新的段被刷新(flush)到了磁盘。写入一个包含新段且排除旧的和较小的段的新提交点。

  • 新的段被打开用来搜索。

  • 老的段被删除。

合并大的段需要消耗大量的I/O和CPU资源,如果任其发展会影响搜索性能。Elasticsearch在默认情况下会对合并流程进行资源限制,所以搜索仍然有足够的资源很好地执行。

4.8 文档分析

我们前面讲到写入一个新文档,首先要对文档内容进行词条分析,然后再创建倒排索引。分析包含下面的过程:

1)将一块文本分成适合于倒排索引的独立的词条

2)将这些词条统一化为标准格式以提高它们的“可搜索性”,或者recall

分析器执行上面的工作。分析器实际上是将三个功能封装到了一个包里:

字符过滤器

首先,字符串按顺序通过每个字符过滤器。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉HTML,或者将& 转化成and等等。

分词器

其次,字符串被分词器分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。

Token过滤器

最后,词条按顺序通过每个token 过滤器。这个过程可能会改变词条(例如,小写化Quick ),删除词条(例如,像a,and,the 等无用词),或者增加词条(例如,像jump 和leap 这种同义词)。

5.elasticsearch优化建议

5.1 硬件选择

Elasticsearch的基础是Lucene,所有的索引和文档数据是存储在本地的磁盘中,具体的路径可在ES 的配置文件../config/elasticsearch.yml中配置,如下:

# ----------------------------------- Paths ------------------------------------## Path to directory where to store the data (separate multiple locations by comma):##path.data: /path/to/data## Path to log files:##path.logs: /path/to/logs#

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

1)使用SSD。就像其他地方提过的,他们比机械磁盘优秀多了。

2)使用RAID 0。条带化RAID 会提高磁盘I/O,代价显然就是当一块硬盘故障时整个就故障了。不要使用镜像或者奇偶校验RAID 因为副本已经提供了这个功能。

3)另外,使用多块硬盘,并允许Elasticsearch 通过多个path.data 目录配置把数据条带化分配到它们上面。

4)不要使用远程挂载的存储,比如NFS 或者SMB/CIFS。这个引入的延迟对性能来说完全是背道而驰

5.2 分片策略
5.2.1 设置合理分片数

分片和副本的设计为ES 提供了支持分布式和故障转移的特性,但并不意味着分片和副本是可以无限分配的。而且索引的分片完成分配后由于索引的路由机制,我们是不能重新修改分片数的。

可能有人会说,我不知道这个索引将来会变得多大,并且过后我也不能更改索引的大小,所以为了保险起见,还是给它设为1000 个分片吧。但是需要知道的是,一个分片并不是没有代价的。需要了解:

1)一个分片的底层即为一个Lucene 索引,会消耗一定文件句柄、内存、以及CPU 。

2)每一个搜索请求都需要命中索引中的每一个分片,如果每一个分片都处于不同的节点还好,但如果多个分片都需要在同一个节点上竞争使用相同的资源就有些糟糕了。

3)用于计算相关度的词项统计信息是基于分片的。如果有许多分片,每一个都只有很少的数据会导致很低的相关度。

一个业务索引具体需要分配多少分片可能需要架构师和技术人员对业务的增长有个预先的判断,横向扩展应当分阶段进行。为下一阶段准备好足够的资源。只有当你进入到下一个阶段,你才有时间思考需要作出哪些改变来达到这个阶段。一般来说,我们遵循一些原则:

1)控制每个分片占用的硬盘容量不超过ES的最大JVM的堆空间设置(一般设置不超过32G,参考下文的JVM设置原则),因此,如果索引的总容量在500G左右,那分片大小在16个左右即可;当然,最好同时考虑原则2。

2)考虑一下node数量,一般一个节点有时候就是一台物理机,如果分片数过多,大大超过了节点数,很可能会导致一个节点上存在多个分片,一旦该节点故障,即使保持了1个以上的副本,同样有可能会导致数据丢失,集群无法恢复。所以,一般都设置分片数不超过节点数的3倍。

3)主分片,副本和节点最大数之间数量,我们分配的时候可以参考以下关系:节点数<=主分片数*(副本数+1)

5.2.2 推迟分片分配

对于节点瞬时中断的问题,默认情况,集群会等待一分钟来查看节点是否会重新加入,如果这个节点在此期间重新加入,重新加入的节点会保持其现有的分片数据,不会触发新的分片分配。这样就可以减少ES 在自动再平衡可用分片时所带来的极大开销。

通过修改参数delayed_timeout ,可以延长再均衡的时间,可以全局设置也可以在索引级别进行修改:

PUT /_all/_settings{"settings":  {  "index.unassig ned.node_left.delayed_timeout": "5m"  }}
5.3 路由选择

当我们查询文档的时候,Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?它其实是通过下面这个公式来计算出来:

shard = hash(routing) % number_of_primary_shards

routing 默认值是文档的id,也可以采用自定义值,比如用户id

不带routing 查询

在查询的时候因为不知道要查询的数据具体在哪个分片上,所以整个过程分为2 个步骤

1)分发:请求到达协调节点后,协调节点将查询请求分发到每个分片上。

2)聚合: 协调节点搜集到每个分片上查询结果,在将查询的结果进行排序,之后给用户返回结果。

带routing 查询

查询的时候,可以直接根据routing 信息定位到某个分配查询,不需要查询所有的分配,经过协调节点排序。

向上面自定义的用户查询,如果routing 设置为userid 的话,就可以直接查询出数据来,效率提升很多。

5.4 文档写入速度优化

ES的默认配置,是综合了数据可靠性、写入速度、搜索实时性等因素。实际使用时,我们需要根据公司要求,进行偏向性的优化。

针对于搜索性能要求不高,但是对写入要求较高的场景,我们需要尽可能的选择恰当写优化策略。综合来说,可以考虑以下几个方面来提升写索引的性能:

1)加大Translog Flush ,目的是降低Iops、Writeblock。

2)增加Index Refresh 间隔,目的是减少Segment Merge 的次数。

3)调整Bulk 线程池和队列。

4)优化节点间的任务分布。

5)优化Lucene 层的索引建立,目的是降低CPU 及IO。

Lucene 以段的形式存储数据。当有新的数据写入索引时,Lucene 就会自动创建一个新的段。

随着数据量的变化,段的数量会越来越多,消耗的多文件句柄数及CPU 就越多,查询效率就会下降。由于Lucene 段合并的计算量庞大,会消耗大量的I/O,所以ES 默认采用较保守的策略,让后台定期进行段合并。

Lucene 在新增数据时,采用了延迟写入的策略,默认情况下索引的refresh_interval 为1 秒。

Lucene 将待写入的数据先写到内存中,超过1 秒(默认)时就会触发一次Refresh,然后Refresh 会把内存中的的数据刷新到操作系统的文件缓存系统中。如果我们对搜索的实效性要求不高,可以将Refresh 周期延长,例如30 秒。这样还可以有效地减少段刷新次数,但这同时意味着需要消耗更多的Heap内存。

Flush 的主要目的是把文件缓存系统中的段持久化到硬盘,当Translog 的数据量达到512MB 或者30 分钟时,会触发一次Flush。

index.translog.flush_threshold_size 参数的默认值是512MB,我们进行修改。

增加参数值意味着文件缓存系统中可能需要存储更多的数据,所以我们需要为操作系统的文件缓存系统留下足够的空间。

ES 为了保证集群的可用性,提供了Replicas(副本)支持,然而每个副本也会执行分析、索引及可能的合并过程,所以Replicas 的数量会严重影响写索引的效率。

当写索引时,需要把写入的数据都同步到副本节点,副本节点越多,写索引的效率就越慢。

如果我们需要大批量进行写入操作,可以先禁止Replica 复制,设置index.number_of_replicas: 0 关闭副本。在写入完成后,Replica 修改回正常的状态。

5.5 内存配置

ES 默认安装后设置的内存是1GB,对于任何一个现实业务来说,这个设置都太小了。

如果是通过解压安装的ES,则在ES 安装文件中包含一个jvm.option 文件,添加如下命令来设置ES 的堆大小,Xms 表示堆的初始大小,Xmx 表示可分配的最大内存,都是1GB。

确保Xmx 和Xms 的大小是相同的,其目的是为了能够在Java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源,可以减轻伸缩堆大小带来的压力。

假设你有一个64G 内存的机器,按照正常思维思考,你可能会认为把64G 内存都给ES 比较好,但现实是这样吗,越大越好?虽然内存对ES 来说是非常重要的,但是答案是否定的!

因为ES 堆内存的分配需要满足以下两个原则:

1)不要超过物理内存的50%:Lucene 的设计目的是把底层OS 里的数据缓存到内存中。

Lucene 的段是分别存储到单个文件中的,这些文件都是不会变化的,所以很利于缓存,同时操作系统也会把这些段文件缓存起来,以便更快的访问。

如果我们设置的堆内存过大,Lucene 可用的内存将会减少,就会严重影响降低Lucene 的全文本查询性能。

2)堆内存的大小最好不要超过32GB:在Java 中,所有对象都分配在堆上,然后有一个Klass Pointer 指针指向它的类元数据。

这个指针在64 位的操作系统上为64 位,64 位的操作系统可以使用更多的内存(2^64)。在32 位的系统上为32 位,32 位的操作系统的最大寻址空间为4GB(2^32)。

但是64 位的指针意味着更大的浪费,因为你的指针本身大了。浪费内存不算,更糟糕的是,更大的指针在主内存和缓存器(例如LLC, L1等)之间移动数据的时候,会占用更多的带宽。

最终我们都会采用31 G 设置

-Xms 31g

-Xmx 31g

假设你有个机器有128 GB 的内存,你可以创建两个节点,每个节点内存分配不超过32 GB。也就是说不超过64 GB 内存给ES 的堆内存,剩下的超过64 GB 的内存给Lucene

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凤舞飘伶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值