es

Es分布式原理

ES分布式集群特点

  • 一个集群拥有相同的cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。
  • 主节点负责管理集群的变更例如增加、删除索引,或者增加、删除节点、shard副本的选举和迁移等。 而主节点并不需要涉及到文档级别的变更和搜索等操作。
  • 屏蔽了分布式系统的复杂性
  • 集群内的扩容能力
    • 垂直扩容和水平扩容
    • 真正的扩容能力是来自于水平扩容–为集群添加更多的节点,并且将负载压力和稳定性分散到这些节点中
  • Coordinating Node 节点:协调节点主要负责协调客户端的请求,将接收到的请求分发给合适的节点,并把结果汇集到一起。比如客户端请求查询某个索引的数据,协调节点将会把请求分发给保存相关的数据的 DataNode 节点,找到相应的分片,并把查询到的结果都汇集返回。并且每个节点都默认起到了 Coordinating Node 的职责。

ES 集群架构

 image-20230106164908030

 ES的架构总体如上图所示,从下到上分为网关、搜索引擎、四大组件、自动发现、通信和Restful API

  1、Gateway网关

    其作用是用来对数据进行持久化以及ES重启后重新恢复数据。

    es支持多种类型的gateway,有本地文件系统、分布式文件系统、Hadoop的HDFS等。

    其存储的信息包括索引信息、集群信息、mapping等

  2、districted lucene directory搜索引擎

    Gateway上层就是Lucene的分布式检索框架。

    ES是分布式的搜索引擎,虽然底层用的是Lucene,但是需要在每个节点上都运行Lucene进行相应的索引、查询、更新等操作,所以需要做成一个分布式的运行框架来满足业务需要。

  3、四大组件模块

    districted lucene directory之上就是ES的四大模块。

    Index Model:索引模块,对数据建立索引(通常是建立倒排索引)

    Seacher Model:搜索模块,就是对数据进行查询搜索

    Mapping Model:是数据映射与解析模块,数据的每个字段可以根据建立的表结构通过mapping进行映射解析;如果没有建立表结构,那么ES会根据数据类型来推测数据结构,并自动生成一个mapping,然后根据mapping进行解析

    River Model:在es2.0之后被取消了,表示可以使用插件处理。例如可以通过一些自定义脚本将传统数据库的数据实时同步到es中。

  4、自动发现Discovery Script

    es集群中各个节点通过discovery相互发现的,默认使用的是Zen discovery(底层实现算法依赖Bully)。es是一个基于p2p的系统,他先通过广播寻找存在的节点,再通过多播协议来进行节点之间的通信,同时也支持点对点的交互。

    es还可以支持多种script脚本语言,例如mvel、js、python等。

  5、通信(Transport)

    代表es内部节点或集群与客户的交互方式,默认内部使用tcp协议进行交互,同时其还支持http协议,thrift、servlet、memcached、zeroMQ等通信协议。

    节点间通信端口默认9300-9400

  6、Restful接口

    最上层就是ES暴漏给我们的访问接口。 

Es分布式数据存储形式

image-20220223142346702

Elasticsearch 的数据是以shard的形式存储在每个es节点的。

shard是Elasticsearch数据存储的最小单位,index的存储容量为所有shard的存储容量之和。Elasticsearch集群的存储容量则为所有index存储容量之和。

一个shard就对应了一个lucene的library。对于一个shard,Elasticsearch增加了translog的功能,类似于HBase WAL,是数据写入过程中的中间数据,其余的数据都在lucene库中管理的。

ES分片Shard的特点

  • Elasticsearch 是利用分片将数据分发到集群内各处
  • 分片是数据的容器,文档保存在分片内
  • 分片又被分配到集群内的各个节点里
  • 当集群规模变动,ES会自动在各个节点中迁移分片。使得数据任然均匀分布在集群中
  • 副分片是主分片的一个拷贝,作为硬件故障时的备份。并提供返回文档读操作
  • 在创建索引时,确定主分片数,但是副分片可以在后面进行更改

如何路由一个文档到一个分片中

  • 当索引一个文档时,我们怎么知道这个文档在什么位置
  • 使用下面的这个路由选择公式
1
shard = hash(routing) % number_of_primary_shards
  • shard: 哪个分片, 也就是分片id
  • routing: 一个可变值,默认是文档的id
  • hash: 一个哈希函数,对rounting字段进行哈希计算生成一个数字
  • number_of_primary_shards: 主分片的数量,routing字段经过hash函数计算之后的值,将对 主分片的数量也就是 number_of_primary_shards 进行取与,之后得到的shard就是我们文档所在的分片的位置

分片全文检索过程

对于全文检索而言,文档可能分散在各个节点上

可以分为搜索和取回两个阶段

image-20220223140043517

查询阶段:

  1. 客户端发送一个search(搜索)请求给Node3,Node 3创建一个长度为from+size的空优先级队列;

  2. Node3转发这个搜索请求到索引中每个副本的主分片或副本分片,每个分片执行本地查询并将结果存储到一个本地的from+size长度的优先队列里;

  3. 每个分片返回document的ID和优先队列里所有document的排序值给到协调节点Node3。Node3把这些值合并到自己的优先队列里产生全局排序结果;

    image-20220223140524303 *

取回阶段:

  1. 协调节点先辨别哪些document需要取回,并且向相关分片发出get请求;
  2. 每个分片加载document并且丰富他们,然后返回给协调节点;
  3. 一旦所有的document都被取回,协调节点会将结果返回给客户端;

Es 数据存储原理

写入流程总结

image-20220223153823350

translog 其实也是先写入 os cache 的,默认每隔 5 秒刷一次到磁盘中去,所以默认写translog也不能保证数据不丢失,极端情况会丢失5s的数据;

整体流程:

  1. 数据写入buffer缓冲和translog日志文件中。
    当你写一条数据document的时候,一方面写入到mem buffer缓冲中,一方面同时写入到translog日志文件中。
  2. buffer满了或者每隔1秒(可配),refresh将mem buffer中的数据生成index segment文件并写入os cache,此时index segment可被打开以供search查询读取,这样文档就可以被搜索到了(注意,此时文档还没有写到磁盘上);然后清空mem buffer供后续使用。可见,refresh实现的是文档从内存移到文件系统缓存的过程。
  3. 重复上两个步骤,新的segment不断添加到os cache,mem buffer不断被清空,而translog的数据不断增加,随着时间的推移,translog文件会越来越大。
  4. 当translog长度达到一定程度的时候,会触发flush操作,否则默认每隔30分钟也会定时flush,其主要过程:
    4.1. 执行refresh操作将mem buffer中的数据写入到新的segment并写入os cache,然后打开本segment以供search使用,最后再次清空mem buffer。
    4.2. 一个commit point被写入磁盘,这个commit point中标明所有的index segment。
    4.3. filesystem cache(os cache)中缓存的所有的index segment文件被fsync强制刷到磁盘os disk,当index segment被fsync强制刷到磁盘上以后,就会被打开,供查询使用。
    4.4. translog被清空和删除,创建一个新的translog。

Segment段合并

通过每秒自动刷新创建新的段,用不了多久段的数量就爆炸了,每个段消费大量文件句柄,内存,cpu资源。更重要的是,每次搜索请求都需要依次检查每个段。段越多,查询越慢。

ES通过后台合并段解决这个问题。ES利用段合并的时机来真正从文件系统删除那些version较老或者是被标记为删除的文档。被删除的文档(或者是version较老的)不会再被合并到新的更大的段中。

image-20220223161438056

segment 合并的过程

  • 新的段flush到硬盘
  • 编写一个包含新段的新提交点,并排除旧的较小段。
  • 新的段打开供搜索
  • 旧的段被删除

合并完成后新的段可被搜索,旧的段被删除,如下图所示:

image-20220223161501064

注意:段合并过程虽然看起来很爽,但是大段的合并可能会占用大量的IO和CPU,如果不加以控制,可能会大大降低搜索性能。段合并的optimize API 不是非常特殊的情况下千万不要使用,默认策略已经足够好了。不恰当的使用可能会将你机器的资源全部耗尽在段合并上,导致无法搜索、无法响应。

LUCENE 数据删除

删除一个ES文档不会立即从磁盘上移除,它只是被标记成已删除。因为段是不可变的,所以文档既不能从旧的段中移除,旧的段也不能更新以反映文档最新的版本。

ES的做法是,每一个提交点包括一个.del /.liv文件(还包括新段),包含了段上已经被标记为删除状态的文档。所以,当一个文档被做删除操作,实际上只是在.del / .liv文件中将该文档标记为删除,依然会在查询时被匹配到,只不过在最终返回结果之前会被从结果中删除。ES将会在用户之后添加更多索引的时候,在后台进行要删除内容的清理。

LUCENE 数据更新

文档的更新操作和删除是类似的:当一个文档被更新,旧版本的文档被标记为删除,新版本的文档在新的段中索引。
该文档的不同版本都会匹配一个查询,但是较旧的版本会从结果中删除。

*ES如何处理冲突 ( 比如并发修改同一docId ) *

  • 使用乐观并发控制
  • 利用 _version 号来确保 应用中相互冲突的变更不会导致数据丢失
  • 通过外部系统使用版本控制

LUCENE 内文件构成

Name Extension Brief Description
Segment Info .si segment的元数据文件
Compound File .cfs, .cfe 一个segment包含了如下表的各个文件,为减少打开文件的数量,在segment小的时候,segment的所有文件内容都保存在cfs文件中,cfe文件保存了lucene各文件在cfs文件的位置信息
Fields .fnm 保存了fields的相关信息
Field Index .fdx 正排存储文件的元数据信息
Field Data .fdt 存储了正排存储数据,写入的原文存储在这
Term Dictionary .tim 倒排索引的元数据信息
Term Index .tip 倒排索引文件,存储了所有的倒排索引数据
Frequencies .doc 保存了每个term的doc id列表和term在doc中的词频
Positions .pos Stores position information about where a term occurs in the index 全文索引的字段,会有该文件,保存了term在doc中的位置
Payloads .pay Stores additional per-position metadata information such as character offsets and user payloads 全文索引的字段,使用了一些像payloads的高级特性会有该文件,保存了term在doc中的一些高级特性
Norms .nvd, .nvm 文件保存索引字段加权数据
Per-Document Values .dvd, .dvm lucene的docvalues文件,即数据的列式存储,用作聚合和排序
Term Vector Data .tvx, .tvd, .tvf Stores offset into the document data file 保存索引字段的矢量信息,用在对term进行高亮,计算文本相关性中使用
Live Documents .liv / .del 记录了segment中删除的doc

es 索引 REINDEX

应用背景:

1、当你的数据量过大,而你的索引最初创建的分片数量不足,导致数据入库较慢的情况,此时需要扩大分片的数量,此时可以尝试使用Reindex。

2、当数据的mapping需要修改,但是大量的数据已经导入到索引中了,重新导入数据到新的索引太耗时;但是在ES中,一个字段的mapping在定义并且导入数据之后是不能再修改的,

所以这种情况下也可以考虑尝试使用Reindex。

如何进行reindex:

ES提供了_reindex这个API。相对于我们重新导入数据肯定会快不少,实测速度大概是bulk导入数据的5-10倍。

Lucene 数据结构

Lucene的数据类型

Index:索引,由很多的Document组成。
Document:由很多的Field组成,是Index和Search的最小单位。
Field:由很多的Term组成,包括Field Name和Field Value。
Term:由很多的字节组成。一般将Text类型的Field Value分词之后的每个最小单元叫做Term。

各种查询结构总结:

  1. FST(有穷状态装换器):保存字符穿类型term字典索引,可以在FST上实现单Term、Term范围、Term前缀和通配符查询等。
  2. Doc-dictionary 的倒排链:保存了每个term对应的docId的列表,采用skipList的结构保存,用于快速跳跃。
  3. BKD-Tree(数值类型多维二叉搜索树):BKD-Tree是一种保存多维空间点的数据结构,用于数值类型(包括空间点)的快速查找。
  4. DocValues:基于docId的列式存储,由于列式存储的特点,可以有效提升排序聚合的性能。

如何实现返回结果进行排序聚合

通过之前介绍可以看出lucene通过倒排的存储模型实现term的搜索,那对于有时候我们需要拿到另一个属性的值进行聚合,或者希望返回结果按照另一个属性进行排序。

  • 在lucene4之前需要把结果全部拿到再读取原文进行排序,这样效率较低,还比较占用内存,为了加速lucene实现了fieldcache,把读过的field放进内存中。这样可以减少重复的IO,但是也会带来新的问题,就是占用较多内存。
  • 新版本的lucene中引入了DocValues,DocValues是一个基于docid的列式存储。当我们拿到一系列的docid后,进行排序就可以使用这个列式存储,结合一个堆排序进行。当然额外的列式存储会占用额外的空间,lucene在建索引的时候可以自行选择是否需要DocValue存储和哪些字段需要存储。

默认情况下,Lucene会按照Score排序,即算分后的分数值,如果指定了其他的Sort字段,就会按照指定的字段排序。那么,排序会非常影响性能吗?

首先,排序并不会对所有命中的doc进行排序,而是构造一个堆(n个数取前k大问题),保证前(Offset+Size)个数的doc是有序的,所以排序的性能取决于(Size+Offset)和命中的文档数,另外就是读取docValues的开销。因为(Size+Offset)并不会太大,而且docValues的读取性能很高,所以排序并不会非常的影响性能。

Roaring Bitmap 数据结构

ES会缓存频率比较高的filter查询,其中的原理也比较简单,即生成(fitler, segment)和id列表的映射,但是和倒排索引不同,我们只把常用的filter缓存下来而倒排索引是保存所有的,并且filter缓存应该足够快,不然直接查询不就可以了。ES直接把缓存的filter放到内存里面,映射的posting list放入磁盘中。

ES在filter缓存使用的压缩方式和倒排索引的压缩方式并不相同,filter缓存使用了roaring bitmap的数据结构,在查询的时候相对于上面的Frame of Reference方式CPU消耗要小,查询效率更高,代价就是需要的存储空间(磁盘)更多。

倒排链的数据结构(数据量较大的时候)

image-20211222114227314

每个RoaringBitmap中都包含一个RoaringArray,名字叫highLowContainerhighLowContainer存储了RoaringBitmap中的全部数据。

这个名字意味着,会将32位的整形(int)拆分成高16位和低16位两部分(两个short)来处理。

RoaringArray的数据结构很简单,核心为以下三个成员:

1
2
3
short[] keys;
Container[] values;
int size;

每个32位的整形,高16位会被作为key存储到short[] keys中,低16位则被看做value,存储到Container[] values中的某个Container中。keys和values通过下标一一对应。size则标示了当前包含的key-value pair的数量,即keys和values中有效数据的数量。

keys数组永远保持有序,方便二分查找。

三种Container

下面介绍到的是RoaringBitmap的核心,三种Container。

通过上面的介绍我们知道,每个32位整形的高16位已经作为key存储在RoaringArray中了,那么Container只需要处理低16位的数据。

ArrayContainer

1
2
3
static final int DEFAULT_MAX_SIZE = 4096

short[] content;

因为这种Container存储数据没有任何压缩,因此只适合存储少量数据。

ArrayContainer占用的空间大小与存储的数据量为线性关系,每个short为2字节,因此存储了N个数据的ArrayContainer占用空间大致为2N字节。存储一个数据占用2字节,存储4096个数据占用8kb。

根据源码可以看出,常量DEFAULT_MAX_SIZE值为4096,当容量超过这个值的时候会将当前Container替换为BitmapContainer。

BitmapContainer

1
final long[] bitmap;

这种Container使用long[]存储位图数据。每个long有64位,因此需要1024个long来提供65536个比特。

因此,每个BitmapContainer在构建时就会初始化长度为1024的long[]。这就意味着,不管一个BitmapContainer中只存储了1个数据还是存储了65536个数据,占用的空间都是同样的8kb。

RunContainer

1
2
3
private short[] valueslength;

int nbrruns = 0;

RunContainer中的Run指的是行程长度压缩算法(Run Length Encoding),对连续数据有比较好的压缩效果。

它的原理是,对于连续出现的数字,只记录初始数字和后续数量。即:

对于数列11,它会压缩为11,0;
对于数列11,12,13,14,15,它会压缩为11,4;
对于数列11,12,13,14,15,21,22,它会压缩为11,4,21,1;
源码中的 short[] valueslength 中存储的就是压缩后的数据。

这种压缩算法的性能和数据的连续性(紧凑性)关系极为密切,对于连续的100个short,它能从200字节压缩为4字节,但对于完全不连续的100个short,编码完之后反而会从200字节变为400字节。

如果要分析RunContainer的容量,我们可以做下面两种极端的假设:

最好情况,即只存在一个数据或只存在一串连续数字,那么只会存储2个short,占用4字节
最坏情况,0~65535的范围内填充所有的奇数位(或所有偶数位),需要存储65536个short,128kb

RoaringBitmap的优化策略

创建时:

  • 创建包含单个值的Container时,选用ArrayContainer
  • 创建包含一串连续值的Container时,比较ArrayContainer和RunContainer,选取空间占用较少的
    转换:

针对ArrayContainer:

  • 如果插入值后容量超过4096,则自动转换为BitmapContainer。因此正常使用的情况下不会出现容量超过4096的ArrayContainer。
  • 调用runOptimize()方法时,会比较和RunContainer的空间占用大小,选择是否转换为RunContainer。

针对BitmapContainer:

  • 如果删除某值后容量低至4096,则会自动转换为ArrayContainer。因此正常使用的情况下不会出现容量小于4096的BitmapContainer。
  • 调用runOptimize()方法时,会比较和RunContainer的空间占用大小,选择是否转换为RunContainer。

针对RunContainer:

  • 只有在调用runOptimize()方法才会发生转换,会分别和ArrayContainer、BitmapContainer比较空间占用大小,然后选择是否转换

常见问题

ES 的一些优化手段

设计调优

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

写入调优

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

查询调优

  1. 禁用wildcard;(通配符模式,类似于%like%)
  2. 禁用批量terms(成百上千的场景);
  3. 充分利用倒排索引机制,能keyword类型尽量keyword;
  4. 数据量大时候,可以先基于时间敲定索引再检索;
  5. 设置合理的路由机制。

linux 部署调优

  1. 关闭缓存swap;

原因:大多数操作系统会将内存使用到文件系统缓存,会将应用程序未用到的内存交换出去。会导致jvm的堆内存交换到磁盘上。交换会导致性能问题。会导致内存垃圾回收延长。会导致集群节点响应时间变慢,或者从集群中断开。

  1. 堆内存设置为:Min(节点内存/2, 32GB);
  2. 设置最大文件句柄数;
  3. 调整线程池和队列大小
  4. 磁盘存储 raid 方式

Es mapping 的作用

Mapping 类似于数据库中的表结构定义schema,它的主要作用是:用来定义索引中的字段的名称、定义字段的数据类型、定义字段类型的一些其它参数,比如字符串、数字、布尔字段,倒排索引的相关配置,设置某个字段为不被索引、记录 position 等。每一种数据类型都有对应的使用场景,并且每个文档都有映射,但是在大多数使用场景中,我们并不需要显示的创建映射,因为ES中实现了动态映射。

mapping 常用参数:text类型为例

  • analyzer:指明该字段用于索引时和搜索时的分析字符串的分词器(使用search_analyzer可覆盖它)。 默认为索引分析器或标准分词器
  • fielddata:指明该字段是否可以使用内存中的fielddata进行排序,聚合或脚本编写?默认值为false,可取值true或false。(排序,分组需要指定为true)
  • fields:【多数类型】text类型字段会被分词搜索,不能用于排序,而当字段既要能通过分词搜索,又要能够排序,就要设置fields为keyword类型进行聚合排序。
  • index:【是否被索引】设置该字段是否可以用于搜索。默认为true,表示可以用于搜索
  • search_analyzer:设置在搜索时,用于分析该字段的分析器,默认是【analyzer】参数的值。
  • index_options:【索引选项】用于控制在索引过程中哪些信息会被写入到倒排索引中。
    • docs:只索引文档号到倒排索引中,但是并不会存储;
    • freqs:文档号和关键词的出现频率会被索引,词频用于给文档进行评分,重复词的评分会高于单个次评分;
    • positions:文档号、词频和关键词 term 的相对位置会被索引,相对位置可用于编辑距离计算和短语查询(不分词那种);
    • offsets:文档号、词频、关键词 term 的相对位置和该词的起始字符串偏移量。

elasticsearch master 选举的流程

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

Term 和Match 的区别

  • term: 不会对搜索词进行分词,只会和doc的内容进行精准匹配,一般是keyword类型字段使用该搜索方式
  • match:会对搜索词进行分词, 搜索包含分词后内容的doc

ES为什么不使用B+树作为索引

  • 全文索引的文本字段通常会比较长,索引值本身会占用较大空间,从而会加大 B+ 树的深度,影响查询效率。
  • 全文索引往往需要全文搜索,不遵循最左匹配原则,使用 B+ 树可能导致索引失效。

ES 怎么实现拼写纠错

通过fuzzy查询实现

ES 如何避免脑裂的

discovery.zen.minimum_master_nodes:1

该参数的意思是,当具备成为主节点的从节点的个数满足这个数字且都认为主节点挂了则会进行选举产生新的主节点。

例如:es集群有三个从节点有资格成为主节点,这时这三个节点都认为主节点挂了则会进行选举,此时如果这个参数的值是4则不会进行选举。

我们可以适当的把这个值改大,减少出现脑裂的概率,官方给出的建议是(n/2)+1,n为有资格成为主节点的节点数node.master=true。

当然elasticsearch 会通过阉割n个master备选阶段的选举权利, 来满足新出master节点的条件

刘小恺(Kyle) wechat
如有疑问可联系博主