ES知识记录

目录

索引:

正向索引:

反向索引:

倒排索引的组成:

单词词典(Term Directory):

倒排列表(PostingList):

索引的更新策略:

索引基本更新思想:

倒排索引的四种更新模型:

分词器:

分词器的组成:

内置分词器:

中文分词器:

ES数据存储结构:

索引Index:

类型Type:

文档Document:

字段Field:

ES的基本命令:

ES的集群相关命令:

ES的索引CRUD相关命令:

ES的映射Mapping:

核心类型:

复杂类型:

地理数据类型:

特殊数据类型:

analyzer分词器:

Index是否索引:

null_value空值默认值:

动态映射:

ES的索引CRUD常用命令举例:

ES的分片:

主分片(默认5):

副本(默认1):

分片的设定:

ES数据架构:

基本概念:

ES数据Write流程(微观):

名词类比HBase:

Shard和Replication的路由规则:

数据安全策略:

具体的操作细节:

ES数据Delete流程:

ES数据Update流程:

ES数据Write流程(细节):

客户端节点:

主分片节点:

副本分片节点:

ES数据Reade流程:

ES实时性:

ES可靠性:

额外补充点:

ES调优推荐点:

ES出现脑裂的可能原因和预防办法:

优化预防:


索引:

正向索引是通过key找value,反向索引是通过value找key。

正向索引:

forward index,以文档的ID为关键字,表中记录文档中每个字的位置信息,查找时扫描表中每个文档中字的信息直到找出所有包含查询关键字的文档,这种组织方法再建立索引的时候结构比较简单,建立比较方便且易于维护。

若是有新的文档加入,直接为该文档建立一个新的索引块,挂接到原来索引文件的后面,若是有文档删除,则直接找到该文档号文档对应的索引信息,将其直接删除。

但是它文本检索效率太低。

反向索引:

一般也被称为倒排索引,倒排表以字或词为关键字进行索引,表中关键字所对应的记录表项记录了出现这个字或词的所有文档,一个表项就是一个字段,它记录了该文档ID和字符在该文档中出现的位置情况。

查询的时候由于可以一次得到查询关键字所对应的所有文档,查询效率高于正排索引。但是由于每个字或词对应的文档数量在动态变化,所以倒排表的建立和维护都较为复杂。

倒排索引的组成:

倒排索引主要由单词词典(Term Dictionary)和倒排列表(Posting List)组成。

单词词典(Term Directory):

倒排索引的重要组成,记录所有文档的单词,一般都比较大,记录单词到倒排列表的关联信息。单词词典一般用B+Trees来实现,存储到内存:

倒排列表(PostingList):

倒排列表记载了出现过某个单词的所有文档的文档列表以及单词在该文档中出现的位置信息以及频率(作关联性算分),每条记录成为一个倒排项。

倒排列表存储在磁盘文档中,主要包含一下信息:

文档ID

单词频率(TF:Term Frequency)

位置(Position)

偏移(Offset)

单词字段和倒排列表整合在一起的结构如下:

索引的更新策略:

搜索引擎需要处理的文档集合往往都是动态集合,即在建好初始的索引后,不断有新文档进入系统,同时原先的文档集合内有些文档可能被删除或者更改。

动态索引通过在内存中维护临时索引,可以实现对动态文档和实时搜索的支持。

服务器的内存总是有限的,随着新加入系统的文档越来越多,临时索引消耗的内存也会随之增加。

当最初分配的内存将被用完时,要考虑将临时索引的内容跟新到磁盘索引中,以释放内存空间来容纳后续的新进文档。

索引基本更新思想:

倒排索引:对初始文档集合建立的索引结构,一般单词词典都存储在内存,对应的倒排列表存储在磁盘文件中。

临时索引:在内存中实时建立的倒排索引,其结构和前述的倒排索引是一样的,区别在于词典和倒排列表都存储在内存当中。

新文档:进入系统时,实时解析文件并将其加入到临时索引结构中。

删除文档:列表则用来存储已被删除的文档的响应的文档ID,形成一个文档ID列表。

修改文档:可以认为是旧文档先被删除,然后系统再增加一篇新的文档,通过这种间接方式实现对内容更改的支持。

常见的索引更新策略有四种:完全重建策略,再合并策略,原地更新策略、混合策略。

倒排索引的四种更新模型:

①完全重建策略:

它是一个很直接的方法,当新增文档达到一定数量,将新增文档和原先的老文档进行合并,然后利用静态索引的方式,对所有文档重新建立索引,新索引建立完成后,老的索引被遗弃释放。

因为重建索引需要较长时间,在进行索引重建的过程中,内存中仍然需要维护老索引来对用户的查询进行反应。这种策略适合较小的文档集合。

②再合并策略:

在合并过程中,需要依次遍历增量索引和老索引单词词典中包含的单词以及其对应的倒排列表可以用两个指针分别指向增量索引和老索引目前需要合并的单词。

如果增量索引中指针指向的单词ID小于老索引中指针指向的单词ID,说明这个单词在老索引中没有出现过,直接将这个单词对应的倒排列表写入新索引的排序列表中,同时增量索引单词指针指向下一单词。

如果两个单词ID相等,则先将老索引中的这个单词对应的倒排列表加入新索引,然后再把增量索引这个单词对应的倒排列表追加到其后。

如果新索引指向的单词ID大于老索引指针指向的单词ID,则将老索引中对应的倒排列表加入新索引的的倒排列表中,老索引的单词指针指向下一单词。

两个索引的所有单词都遍历完后,新索引建成。再合并策略是效率非常高的一种索引策略,主要是因为对老索引进行遍历时没因为已经按照索引单词的词典顺序由低到高拍好顺序了,所以可以顺序读取文件内容,减少磁盘寻道时间。但是对于老索引中的很多单词来说,尽管其倒排列表并没有发生任何变化,但是也需要将其从老索引中读取出来并写入新索引中,这样就会照成很大的磁盘输入输出消耗。

③原地更新策略:

在索引更新过程中,如果老索引的倒排列表没有变化,可以不需要读取这些信息,而只是对那些倒排列表变化的单词进行处理,或者是直接将发生变化的倒排列表追加到老索引的末尾,即只更新增量索引里出现的单词相关信息,这样就可以减少大量的磁盘读写操作,提升系统执行效率。

存在的问题:

对于倒排文件中的两个相邻单词,为了在查询时加快读写速度,其倒排列表一般是顺序存储的,这导致没有空余位置来追加新信息,为了能够支持追加操作,原地更新策略在初始建立的索引中,会在每个单词的倒排列表末尾预留一定的磁盘空间,当预留空间不足时,需要在磁盘中找到一块完整的连续存储区,将增量索引对应的倒排列表追加到其后,实现倒排列表的“迁移”工作。

原地更新策略出发点好,但是实验数据证明其索引更新效率比再合并策略低,主要有两个原因:

在这种方法中,对倒排列表的“迁移”比较常见,这个策略需要对磁盘可用空间进行维护和管理,这种维护和查找成本非常高,这成为该方法效率的一个瓶颈。

由于存在数据迁移,某些单词及其对应的倒排列表会从老索引中溢出,这样破坏了单词的连续性,导致在进行索引合并时不能进行顺序读取,必须维护一个单词到其倒排文件相应位置的映射表,这样不仅降低了磁盘读取速度,而且需要大量 的内存来存储这种映射信息。

④混合策略:

它一般会将单词根据不同性质进行分类,不同类别的单词,对其所以采取不同的索引更新策略。

常见的做法:

根据单词的倒排列表长度进行区分,因为有些单词经常在不同的文档中出现,所以其对应的倒排列表较长,而有的单词较少见,则其倒排列表较短,根据这一性质将单词划分为长倒排列表单词和短倒排列表单词。

长倒排列表单词采取原地更新策略,短倒排列表单词采取再合并策略。

分词器:

文本分析是把全文本转换一系列单词(term/token)的过程,也叫做分词。

Analysis是通过Analyzer来实现的。

分词器的作用就是把整篇文档,按照一定的语义切分为一个一个的词条,目标是提升文档的召回率,并降低无效数据的噪音。

召回率:recall,也叫做可搜索性,指搜索的时候,增加能否搜索到的结果的数量。

降噪:指降低文档中一些低相关性词条对整体搜索排序结果的干扰。

分词器的组成:

由3中构件块组成:character filters,tokenizers,token filters。

analyzer = CharFilters(0个或多个)+Tokenizer(恰好一个)+TokenFilters(0个或多个)

character filters:

在一段文本进行分词之前,要先进行预处理,比如说常见的过滤html标签。

tokenizers:

英文分词可以根据空格将单词分开,中文分词比比较复杂,可以才用机器学习算法来分词。

token filters:

将切分的单词进行加工。例如大小写转换、去掉词语(例如a、and、the等),或者增加词(例如同义词)

内置分词器:

Standard Analyzer:

默认分词器,按词切分,小写处理,它提供了基于语法的标记化(基于Unicode文本分割算法),适用于大多数语言。

Simple Analyzer:

按照非字母切分(符号被过滤),小写处理,当它遇到的只要不是字母的字符,就将文本解析成term,而且所有的term都是小写的。

Whitespace Analyzer:

按照空格进行分词,不区分大小写,汉语不分词。

其他:

Stop Analyzer:小写处理,停用词过滤(the、a、is等)。

Keyword  Analyzer:不分词,直接把输入当作输出。

Patter Analyzer:正则表达式,默认\W(非字符分割)。

Language:提供了30多种常见语言的分词器。

Customer Analyzer:自定义分词器。

中文分词器:

IK分词器,还有smartCN、HanLP等。

IK分词器的粒度:

ik_smart:会做最粗力度的拆分。

ik_max_word:会将文本做最细粒度的拆分。

ES数据存储结构:

索引Index:

索引是文档Document的容器,是一类文档的集合。

索引(名词):

类比传统的关系型数据库领域来说,索引相当于SQL中的一个数据库Database。

索引由其名称(必须是全小写字符)进行标识。

索引(动词):

保存一个文档到索引(名词)的过程。

这非常类似于SQL语句中的Insert关键词,如果盖文档以及存在那就相当于数据库的UPDATE。

索引(倒排索引):

关系型数据库通过增加一个B+树索引到指定的列上,以便提升数据检索速度。这个再ES中用来达到相同目的的是倒排索引。

类型Type:

从6.0.0开始,单个索引中只能有一个类型,7.0.0以后将不简易使用,8.0.0以后完全不支持。

索引和文档中间还有个类型的概念,每个索引下可以建立多个类型,文档存储时需要指定index和type。

弃用原因:

通俗的去理解index比作SQL的database,type比作SQL的table,但是这并不准确,因为如果在SQL中,table之间相互独立,同名的字段在两个表中毫无关系,但是在ES中,同一个index下不同的type中如果有同名的字段,他们会被Luecence当作同一个Deprecated,在7.0开始,一个索引只能建一个Type为_doc。

文档Document:

Document Index里单条的记录称为Document(文档)。等同于关系型数据库表中的行。

_index:文档所属索引名称。

_type:文档所属类型名。

_id:文档的主键,在写入的时候,可以指定盖Doc的ID值,如果不指定,系统自动生成一个UUID值。

_version:文档的版本信息,ES通过使用version来保证对文档的变更能以正确的顺序执行,避免乱序照成的数据丢失。

_seq_no:严格递增的顺序号,每个文档一个,shard级别严格递增,保证后写入的Doc的_seq_no大于先写入的Doc的_seq_no。

_primary_term:primary_term也和_seq_no一样是一个整数,每当Primary Shard发生重新分配时,比如重启,Primary选举等,_primary_term会递增1.

found:查询的id正确那么true,如果id不正确,就查不到数据,found就是false。

_source:文档的原始Json数据。

字段Field:

ES的基本命令:

restrful中,GET/PUT/POST/DELETE格式:

创建:POST /uri

删除:DELETE /uri/xxx

更新或创建:PUT /uri/xxx

查看:GET /uri/xxx

在ES中,如果不确定文档的ID,那么就需要用POST,它可以自己生成唯一的文档ID,如果确定文档的ID,那么就可以使用PUT,当然也可以用POST,他们都可以创建或者修改文档。

PUT、GET、DELETE是幂等的,而POST并不一定。

ES的集群相关命令:

_cat命令:这个系列提供了查询ES集群状态的接口,每个命令都支持使用?v参数,让输出内容表格显示表头,pretty则让输出缩进更规范。

curl -X GET "localhost:9200/_cat/nodes?v&pretty"

结果字段:

heap.precent:堆内存占用百分比

ram.precent:内存占用百分比

cpu:CPU使用百分比

master:*标识节点是集群中的主节点

name:节点名

curl -X GET "localhost:9200/_cat/shards?v&pretty"

结果字段:

index:索引名称

shards:分片序号

prirep:p表示该分片是主分片,r表示该分片是复制分片

store:该分片占用存储空间

node:所属节点节点名

docs:分片存放的文档数

curl -X GET "localhost:9200/_cat/indices?v&pretty"

结果字段:

health:索引的健康状态

index:索引名

pri:索引主分片数量

rep:索引复制分片数

store.size:索引主分片,复制分片,总占用存储空间

pri.store.size:索引总占用空间,不计算复制分片,占用空间

curl -X GET "localhost:9200/_cluster/health"查看集群状态

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

绿色:集群健康完好,一切功能齐全正常,所有分片和副本都可以正常工作。

黄色:预警状态,所有主分片功能正常,但至少有一个副本是不能正常工作的。此时集群是可以正常工作的,但是高可用性在某种程度上会受影响。

红色:集群不可正常使用。某个或某些分片及其副本异常不可用,这时集群的查询操作还能执行,但是返回的结果会不准确。对于分配到这个分片的写入请求将会报错,最终会导致数据的丢失。

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

ES的索引CRUD相关命令:

创建是命名全部小写,不能使用_开头,中间不能使用“,”。

创建index:

curl -XPUT http://localhost:9200/test?pretty

获取索引:

curl -XGET http://localhost:9200/test?pretty

删除索引:

curl -XDELETE http://localhost:9200/test?pretty

ES的映射Mapping:

从7.X开始,一个Mapping只属于一个索引的type,默认type是_doc

ES的字段类型主要有,核心类型,复杂类型,地理类型以及特殊类型

核心类型:

字符串类型:

text:类型适合于需要被全文检索的字段,例如新闻正文、邮件正文等比较长的文字,text类型会被Lucene分词器(Analyzer)处理为一个个词项,并使用Lucene倒排索引存储,text字段不能用于排序,如果需要使用该类型的字段只需要在定义映射时指定JSON中对应字段的type为text。

keyword:不会被分词,适合简短、结构化字符串,例如主机名、姓名、商品名称等,可以用于过滤、排序、聚合检索、精确查询等(包括数字、日期、具体的字符串例如192.168.1.等)。

数字类型:

分为long、integer、short、byte、double、float、half_float、scaled_float。数字类型的字段在满足需求的前提下应当尽量选择范围较小的数据类型,字段长度越短搜索效率越高。

对于浮点数,可以优先考虑使用scaled_float类型,该类型可以通过缩放因子来精确浮点数,例如12.34可以转换为1234来存储。

日期类型:

ES底层依然才用的时间戳的形式存储,format:yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis。

布尔类型:

JSON文档中同样存在布尔类型,不过JSON字符串类型也可以被ES转换为布尔类型存储,前提是字符串的取值为true或者false,布尔类型常用于检索中的过滤条件。

二进制类型:

二进制类型binary接受BASE64编码的字符串,默认store属性为false,并且不可以被搜索。

范围类型:

范围类型可以用来表达一个数据的区间,可以分为5种:integer_range、float_range、long_range、double_range、date_range。

复杂类型:

对象类型:

JSON字符串允许嵌套对象,一个文旦更可以嵌套多个、多层对象,可以通过对象类型来存储二级文档,不过由于Lucene并没有内部对象的概念,ES会将JSON文档扁平化。

嵌套类型:

嵌套类型可以看成一个特殊的对象类型,可以让对象数组独立索引。

地理数据类型:

geo_point类型:

存储某个地理坐标的经纬度。

geo_shape类型:

存储地理多边形状。

特殊数据类型:

ip类型:

用于表示IPv4与IPv6的地址。

completion类型:

提供自动输入关联完成功能,如常见的baidu搜索框。

token_count类型:

用于计算字符串token的长度,使用时需提供analyzer定义。

percolate类型:

定义未percolate类型的字段会被ES分析为一个查询并保存下来,并可用在后继对文档的查询中。Percolate可以理解为一个预置的查询。

alias类型:

定义一个已经存在域的别名。

join类型:

该类型定义了文档对象之间的父子关系,即同一索引中多个文档对象可以存在依赖关系,如互联网中常见的博客文章与回复等。

analyzer分词器:

Index是否索引:

index可用于设置字段是否被索引,默认为true,false即为不可搜索。

null_value空值默认值:

需对Null值实现搜索时使用,只有keyword类型才支持设定null_value。

动态映射:

不需要提前创建index,定义mapping信息以及type类型,可以直接向ES中插入文档,ES会根据每个新field可能的数据类型自动为其配置type等mapping信息,这个过程就是动态映射(dynamic mapping)。

约束策略:

dynamic设置为true时,新增字段的文档写入时,Mapping同时会被更新。

dynamic设置为false时,Mapping不会被更新,新增字段的数据无法被索引,但是会出现在_source中。

dynamic设置为strict,文档将写入失败。

ES的索引CRUD常用命令举例:

ES的分片:

主分片(默认5):

主分片用于解决数据水平扩展的问题,通过分片,可以将数据分布到集群内的所有节点之上。一个分片是一个运行ES的实例,分片数在索引创建时指定,后续不允许修改,除非Reindex。

副本(默认1):

副本分片数,可以动态调整,增加副本数,还可以在一定程度上提高服务的可用性(读取的吞吐)。

分片的设定:

分片设置过大,数量少:

导致后续无法增加节点实现水平扩展,单个分片数据量过大,导致数据重新分片耗时。

分片设置过小,数量多:

影响搜索结构的相关性打分,影响统计结果的准确性。单个节点上过多分片会导致资源浪费,同时会影响性能。7.0之后,默认主分片是1,解决了Over-sharding问题(shard也是一种资源,shard过多会影响集群的稳定性,因为shard过多,元信息会变多,这些元信息会占用堆内存,shard过多也会影响读写性能,因为每个读写请求都需要一个线程)。

ES数据架构:

基本概念:

Index:一个ES集群中可以按需创建任意数目的索引。

Document:文档是索引和搜索的原子单位,它是包含了一个或多个域(Field)的容器,基于JSON格式进行表示,文档由一个或多个域组成,每个域拥有一个名字以及一个或多个值,有多个值的域通常称为“多值域”。每个文档可以存储不同的域集。

Node:集群是由一个或者多个拥有相同ckuster.name配置的节点组成。节点分为:主节点(负责管理集群范围内的所有变更即写操作)、数据节点(存储数据和其对应的倒排索引,默认每一个节点都是数据节点,包括主节点)、协调节点(如果node.master和node.data属性均为false,此节点称为协调节点,用来响应客户请求,均衡每个节点的负载)。

shard:一个索引中的数据保存在多个分片中,相当于水平分表。文档被存储和索引到分片内,但是应用程序是直接与索引而不是与分片进行交互。分片是数据的容器,文档保存在分片内,分片又被分配到集群内的各个节点里。索引建立的时候就确定了主分片数,但是副本分片数可以随时修改。

Replaction:一个分片可以是主分片或者副本分片,主分片可以用来写入和读取数据,副本分片负责读取数据,主分片和副本分片的状态决定了集群的健康状态,相同的副本分片不会存在于同一个节点中。

ES数据Write流程(微观):

名词类比HBase:

Shard -- Region

Lucene  --  buffer   --  系统缓存

Segment  --  MemStore

写出Segment file  --  stroefile

TransLog  --  Hlog

Shard和Replication的路由规则:

每个Index由多个Shard组成,每个Shard由一个主节点和多个副本节点,副本个数可配。但是每次写入时,写入请求会先根据_rounting规则(shard = hash(routing) % number_of_primary_shards,Routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值,这就解释了为什么要在创建索引的时候就确定好主分片的数量并且永远不会改变这个数量,因为如果分片数量变了,那么所有之前路由的值都会无效,文档也再也找不到了)选择发出给哪个Shard。

Index Request中可以设置使用哪个Filed的值作为路由参数。

如果Index没有设置,则使用Mapping中的配置。

如果mapping中也没有设置,则使用id作为路由参数。然后通过_id的Hash值选择Primary Shard。

请求会发数据送给Primary Shard,再Primary Shard上执行成功后,再从Primary Shard上将请求同时发送给多个Replica Shard。

数据安全策略:

ES里为了减少磁盘IO保证读写性能,一般是每个一段时间才会把Lucene的Segment写入磁盘持久化。ES学习了数据库中的处理方式,增加CommitLog模块,ES中叫TransLog。

写入请求到Shard后先写buffer文件,创建好索引,此时索引还在内存里,接着去写TransLog。写完后刷新TransLog数据到磁盘上,写磁盘成功后,请求返回给用户。

具体的操作细节:

ES数据Delete流程:

如果每一次删除都需从集群中找到存储那个数据的节点再进行删除,那么效率就太低了,ES如果是删除请求的话,提交时会生成一个.del文件,里面将某个doc标识为deleted状态,那么搜索的时候根据.del文件就知道这个doc被删除了,客户端搜索的时候,发现数据再.del文件中标志为删除的就不会搜索出来了。

ES数据Update流程:

ES的索引时不能修改的,因此更新和删除操作并不是直接在原索引上直接执行的。收到Update请求后,从Segment或者TransLog中读取同id的完整Doc,记录版本为v1 = 345。将版本v1全量Doc和请求中的部分字段Doc合并为一个完整的Doc,同时更新内存中的VersionMap。

获取到完整Doc后,Update请求就变成了Post/Put请求。

然后加锁。再次从versionMap中读取该id最大版本好v2 = 346,检查版本是否冲突(v1 == v2),如果冲突,则回退到开始的“Update doc”阶段,重新执行,如果不冲突,则执行最新的Add请求。

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

释放锁,部分更新的流程结束。

ES数据Write流程(细节):

红色:Client Node(协调节点)

绿色:Primary Node(主分片节点)

蓝色:Replica Node(副本分片节点)

客户端节点:

Ingest Pipeline:

在这异步可以对原始文档做一些处理,比如HTML解析,自定义的处理,具体处理逻辑可以通过插件来实现,在ES中,由于Ingest Pipeline会比较耗费CPU,可以设置专门的Ingest Node,专门用来处理Ingest Pipeline逻辑。如果当前Node不能执行Ingest Pipeline,则会将请求发送给另一台可以执行Ingest Pipeline的Node。

Auto Create Index:

判断当前Index是否存在,如果不存在,则需要自动创建Index,这里需要和Master交互,也可以通过配置关闭自动创建Index的功能。

Set Rounting:

设置路由条件,如果Request中指定了路由条件,则直接使用Request中的Rounting,否则使用Mapping中配置的,如果Mapping中无配置,则使用默认的_id字段值。这一步中,如果没有指定id字段,则会自动生成一个唯一的_id字段,目前用的是UUID。

Construct BulkShardRequest:

由于Bulk Request中会包括多个(index、update、delete)请求,这些请求根据routing可能会落到多个Shard上执行,这一步会按Shard挑拣Single

 Write Request,同一个Shard中的请求聚集在一起,构建BulkShardRequest,每个BulkShardRequest对应一个Shard。

Send Request To Primary:

这一步会将每一个BulkShardRequest请求发送给相应的Primary Node。

主分片节点:

Index or Update or Delete:

循环执行每个Single Write Request,对于每个Request,根据操作类型(Create/Index/Update/Delete)选择不同的处理逻辑。其中Create/Index是直接新增Doc,Delete是直接根据_id删除Doc,Update稍微复杂些,下面的是以Update为例。

Translate Update To Index or Delete:

是Update的特有步骤,会将Update请求转换为Index或者Delete请求,首先会通过GetRequest查询到已经存在的同id DOC(如果有)的完整字段和值(依赖source字段)。然后和请求中的DOC做合并,同时会获取到读到的Doc版本号,记作v1。

Parse Doc:

解析Doc中各个字段,生成ParseDocument对象,同时生成uid Term,在ES中,uid = type # _id,对用户,id可见,而ES中存储的是_uid,这一部分生成的ParseDocument中也有ES的系统字段,大部分会根据当前内容填充,部分未知的会在后面继续填充ParseDocument。

Update Mapping:

ES中有个自动更新Mapping的功能,就在这一步生效,会先挑选出Mapping中未包含的新Field,然后判断是否运行自动更新Mapping,如果允许,则自动更新Mapping。

Get Sequence Id and Version:

由于当前是Primary Shard,则会从SequenceNumber Service获取一个SequenceID和Version。SequenceID在Shard级别每次递增1,SequenceID在写入DOC成功后,会用来初始化LocalCheckPoint。Version则是根据当前Doc的最大Version递增1。

Add Doc To Lucene:

这一步开始的时候会给特定uid加锁,然后判断该uid对应的Version是否能与之前Translate Update To Index步骤里获取到的Version,如果不相等,则说明刚刚才读取Doc后,Doc又发生了变化,出现了版本冲突,这时候会抛出一个VersionConflict的异常,该异常会在Primary Node最开始处捕获,重新从“Translate Update To Index or Delete”开始执行。

如果Version相等,则继续执行,如果已经存在了同id的Doc,则会调用Lucene的UpdateDocument(uid,doc)接口,先根据uid删除Doc没然后在Index新Doc,如果是首次写入,则直接调用Lucene的AddDocument接口完成Doc的Index,AddDocument也是通过UpdateDocument实现。

这一步中有一个问题是如何保证Delete-Then-Add的原子性,怎么避免中间状态时被Refresh:

在开始Delete之前,会加一个Refresh Lock,禁止被Refresh,只有等Add完后释放了Refresh Lock后才能被Refresh,这样就保证了Delete-Then-Add的原子性。Lucene的UpdateDocument接口中就知识处理多个Field,会遍历每个Field逐个处理,处理的顺序时invert index,store field,doc values,point dimension。

Write Translog:

写完Lucene的Segment后,会以KeyValue的形式写TransLog,Key是id,Value是Doc内容,当查询时,如果请求是GetDocByID,则可以直接根据id从TransLog中读取到,满足NoSQL场景下的实时性要求。但是需要注意的是,这里知识写入内存的TransLog,是否Sunc到磁盘的逻辑还在后面。

这一步的最后,会标记当前的SequenceID已经成功执行,接着会更新当前Shard的LocalCHeckPoing。

Renew Bulk Request:

这里会重新钩爪Bulk Request,原因是前面已经将UpdateRequest翻译成了Index活Delete请求,则后续所有Replica中只需要执行Index或者Delete请求就可以了,不需要再执行Update逻辑,一是保证Replica中逻辑更简单,性能更好,二十保证同一个请求在Rpimary和Replica中的执行结果一样。

Flush Tanslog:

这里会根据TransLog的策略,选择不同的执行方式,要么是立即Flush到磁盘,要么是等到以后再Flush,Flush的频率越高,可靠性越高,对写入性能影响越大。

Send Requests To Replicas:

这里会将刚才构造的新的Bulk Request并行发送给多个Replica,然后等待Replica的返回,这里需要等待所有的Replica返回后(可能成功,也有可能失败),Primary Node才返回用户。如果某一个Replica失败了,则Primary会给Master发送一个Remove Shard请求,要求Master将该Replica Shard从可用节点中移除。这里同时会将SequenceID、PrimaryTerm、GlobalCheckPoint等传递给Replica。

发送给Replica的请求中,Action Name等于原始ActionName + [R],这里的R标识Replica,通过这个R的不同,可以找到处理Replica请求的Handler。

Receive Response From Replicas:

Replica中请求都处理完后,会更新Primary Node的LocalCHeckPoing。

副本分片节点:

Index or Delete:

更具请i去类型是Index还是Delete,选择不同的执行逻辑,这里没有Update,因为在Primary Node中已经将Update转换成了Index或者Delete请求了。

Parse Doc:

和Priamary Node的逻辑一样。

Update Mapping:

和Priamary Node的逻辑一样。

Get Sequence Id and Version:

Parimary Node中会生成Sequence ID和Version,然后放入EeplicaReques中,这里只需要从Request中获取到就行。

Add Doc To Lucene:

由于已经在Primary Node中将部分Update请求转换成了Index或Delete请求,这里只需要处理Index和Delete两种请求,不需要处理Update请求了,比Primary Node会更简单些。

Write Translog:

和Priamary Node的逻辑一样。

Flush Translog:

和Priamary Node的逻辑一样。

ES数据Reade流程:

查询的过程大体分为查询(query)和取回(fetch)两个阶段。

这个节点的任务是广播查询请求到所有的相关分片,并将它们的响应整合称全局排序后的结果集合,这个结果集合会返回给客户端。

查询过程:

当一个节点接收到一个搜索请求,则这个节点就变成了协调节点。

如果客户端要求返回结果排序中从from名开始的数量未size的结果集,则每个节点都要生成一个from + size大小的结果集。

分片仅会返回一个轻量级的结果给协调节点,包含结果集中的每一个文档中的ID和进行排序锁需要的信息。

协调节点会将所有分片的结果汇总,进行全局排序,得到最终的查询排序结果。

取回过程:

协调节点会确定实际需要返回的文档,并向含有该文档的分片发送get请求。

分片获取问昂返回给协调节点,协调节点将结果返回给客户端。

相关性计算:

判别文档与搜索条件的相关程度。

TFIDF。

BM25评分算法。

ES实时性:

ES主要应用场景就是实时,但是ES本身并非实时,而是near-real-time(近实时)。Inde的实时性是由refresh控制的,默认是1s,最快可到100ms,也就意味着index doc成功后,需要等待一秒钟后才能被搜索到。

ES中个Get请求也能保证是实时的,因为Get请求会直接读内存中尚未Flush到磁盘的TransLog,但是Get请求只支持通过doc_id进行查询,所以对于条件查询依然无法实现实时。

一般这个时间设置为1秒钟,导致写入ES的文档最快要1s才可被搜索到,所以ES在搜索方面是近实时的系统。

当ES作为NoSQL数据库时,查询方式时GetById,这种查询可以直接从TransLog中查询,这时候就成了real-time(实时)系统,只能根据ID进行查询。

ES可靠性:

搜索系统对可靠性要求都不高,一般数据的可靠性通过将原始数据存储在另外一个存储系统来保证,当搜索系统的数据发生丢失的时候,再从其他存储系统导一份数据过来rebuild就可以了。

在ES中,通过设置TransLog的Flush频率可以控制可靠性,要么时按请求,每次请求都Flush,要么时按时间,每隔一段时间Flush一次。

一般为了性能考虑,会设置成每隔5s或者1min来Flush一次,Flush间隔的时间越长,可靠性越低。

额外补充点:

ES调优推荐点:

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

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

3、不需要做模糊检索的字段使用Keyword类型代替Text类型,这样可以避免在建立索引前对这些文本进行分词。

4、如果你的搜索结果不需要近实时的准确度,考虑把每个索引的 index.refresh_interval 改到 30s 。

5、如果你是在做大批量导入,导入期间你可以通过设置这个值为-1,关掉刷新,还可以通过设置index.number_of_replicas: 0关闭副本。别忘记在完工的时候重新开启它。

6、避免深度分页查询建议使用Scroll进行分页查询。普通分页查询时,会创建一个from+size的空优先队列,每个分片会返回from+size条数据,默认只包含文档ID和得分Score给协调节点。

7、如果有 N 个分片,则协调节点再对(from+size)×n条数据进行二次排序,然后选择需要被取回的文档。当from很大时,排序过程会变得很沉重,占用CPU资源严重。

8、减少映射字段,只提供需要检索,聚合或排序的字段。其他字段可存在其他存储设备上,例如 Hbase,在ES中得到结果后再去Hbase查询这些字段。

9、创建索引和查询时指定路由Routing值,这样可以精确到具体的分片查询,提升查询效率。路由的选择需要注意数据的分布均衡。

10、确保堆内存最小值( Xms )与最大值( Xmx )的大小是相同的,防止程序在运行时改变堆内存大小。ES设置的堆内存最好不要超过物理内存的50%和超过32GB。

11、GC默认采用CMS的方式,并发但是有STW的问题,可以考虑使用G1收集器。

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

ES出现脑裂的可能原因和预防办法:

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

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

节点负载:主节点的角色既为Master又为 Data,访问量较大时可能会导致ES停止响应(假死状态)造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。

内存回收:主节点的角色既为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_nodes2)+1,其中 master_eligibel_nodes 为候选主节点的个数。这样做既能防止脑裂现象的发生,也能最大限度地提升集群的高可用性,因为只要不少于discovery.zen.munimum_master_nodes个候选节点存活,选举工作就能正常进行。当小于这个值的时候,无法触发选举行为,集群无法使用,不会造成分片混乱的情况。

3、角色分离。 即是上面我们提到的候选主节点和数据节点进行角色分离,这样可以减轻主节点的负担,防止主节点的假死状态发生,减少对主节点“已死”的误判。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值