elasticsearch源码

2018-12-04

https://segmentfault.com/a/1190000014480912

选举

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

集群

https://segmentfault.com/a/1190000014183767

写入

获取分片的方法
1shard = hash(_routing) % (num_of_primary_shards)

segment

Lucene索引是由多个段组成,段本身是一个功能齐全的倒排索引。
段是不可变的,允许Lucene将新的文档增量地添加到索引中,而不用从头重建索引。
对于每一个搜索请求而言,索引中的所有段都会被搜索,并且每个段会消耗CPU的时钟周、文件句柄和内存。这意味着段的数量越多,搜索性能会越低。
为了解决这个问题,Elasticsearch会合并小段到一个较大的段,提交新的合并段到磁盘,并删除那些旧的小段。

查看memory占用

GET /_cat/segment/

GET /_cat/nodes?v&h=name,port

如何减少占用
删除不用的索引
关闭索引 (文件仍然存在于磁盘,只是释放掉内存)。需要的时候可以重新打开。
定期对不再更新的索引做optimize (ES2.0以后更改为force merge api)。这Optimze的实质是对segment file强制做合并,可以节省大量的segment memory。

Field Data cache
在有大量排序、数据聚合的应用场景,可以说field data cache是性能和稳定性的杀手。 对搜索结果做排序或者聚合操作,需要将倒排索引里的数据进行解析,然后进行一次倒排。 这个过程非常耗费时间,因此ES 2.0以前的版本主要依赖这个cache缓存已经计算过的数据,提升性能。但是由于heap空间有限,当遇到用户对海量数据做计算的时候,就很容易导致heap吃紧,集群频繁GC,根本无法完成计算过程。 ES2.0以后,正式默认启用Doc Values特性(1.x需要手动更改mapping开启),将field data在indexing time构建在磁盘上,经过一系列优化,可以达到比之前采用field data cache机制更好的性能。因此需要限制对field data cache的使用,最好是完全不用,可以极大释放heap压力。

Indexing Buffer
Indexing Buffer是用来缓存新数据,当其满了或者refresh/flush interval到了,就会以segment file的形式写入到磁盘。 这个参数的默认值是10% heap size。根据经验,这个默认值也能够很好的工作,应对很大的索引吞吐量。 但有些用户认为这个buffer越大吞吐量越高,因此见过有用户将其设置为40%的。到了极端的情况,写入速度很高的时候,40%都被占用,导致OOM。

部署优化

  • 关闭缓存swap;
  • 堆内存设置为:Min(节点内存/2, 32GB);
  • 设置最大文件句柄数;
  • 线程池+队列大小根据业务需要做调整;
  • 磁盘存储raid方式——存储有条件使用RAID10,增加单节点性能以及避免单节点存储故障。

设计优化

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

写入优化

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

查询调优

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

routing设计

FST

FST 数据结构有两个优点:
1)空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间;
2)查询速度快,O(len(str))字符长度的查询时间复杂度。


elasticsearch源码_第1张图片
image.png

倒排链skipList


elasticsearch源码_第2张图片
image.png

BKDTree

为了支持高效的数值类或者多维度查询,lucene引入类BKDTree。BKDTree是基于KDTree,对数据进行按照维度划分建立一棵二叉树确保树两边节点数目平衡。在一维的场景下,KDTree就会退化成一个二叉搜索树,在二叉搜索树中如果我们想查找一个区间,logN的复杂度就会访问到叶子结点得到对应的倒排链


elasticsearch源码_第3张图片
image.png

为什么 Lucene 检索比 mysql 快

Mysql 只有 term dictionary 这一层,是以 b-tree 排序的方式存储在磁盘上的。检索一个 term 需要若干次的 random access 的磁盘操作。而 Lucene 在 term dictionary 的基础上添加了 term index 来加速检索,term index 以树的形式缓存在内存中。从 term index 查到对应的 term dictionary 的 block 位置之后,再去磁盘上找 term,大大减少了磁盘的 random access 次数。

额外值得一提的两点是:term index 在内存中是以 FST(finite state transducers)的形式保存的,其特点是非常节省内存。Term dictionary 在磁盘上是以分 block 的方式保存的,一个 block 内部利用公共前缀压缩,比如都是 Ab 开头的单词就可以把 Ab 省去。这样 term dictionary 可以比 b-tree 更节约磁盘空间。

如何联合使用两个索引

使用 skip list 数据结构。同时遍历 gender 和 age 的 posting list,互相 skip;
使用 bitset 数据结构,对 gender 和 age 两个 filter 分别求出 bitset,对两个 bitset 做 AND 操作。(如果查询的 filter 缓存到了内存中,以 bitset 的形式)

cardinality 度量

提供一个字段的基数,即该字段的distinct或者unique值的数目。它是基于HLL算法的。HLL 会先对我们的输入作哈希运算,然后根据哈希运算的结果中的 bits 做概率估算从而得到基数。其特点是:可配置的精度,用来控制内存的使用(更精确 = 更多内存);小的数据集精度是非常高的;我们可以通过配置参数,来设置去重需要的固定内存使用量。无论数千还是数十亿的唯一值,内存使用量只与你配置的精确度相关。

写入数据

  • 数据先写入到buffer里面,在buffer里面的数据时搜索不到的,同时将数据写入到 translog 日志文件之中

  • 如果 buffer 快满了,或者到一定时间,就会将内存 buffer 数据 refresh 到一个新的 segment file 中,但是此时数据不是直接进入 segment file 磁盘文件,而是先进入 os cache。这个过程就是 refresh。(操作系统里面,磁盘文件其实都有一个东西,叫做 os cache,即操作系统缓存,就是说数据写入磁盘文件之前,会先进入 os cache,先进入操作系统级别的一个内存缓存中去。只要 buffer 中的数据被 refresh 操作刷入 os cache中,这个数据就可以被搜索到了。)

  • 每隔 1 秒钟,es 将 buffer 中的数据写入一个新的 segment file,每秒钟会产生一个新的磁盘文件 segment file,这个 segment file 中就存储最近 1 秒内 buffer
    中写入的数据。但是如果 buffer 里面此时没有数据,那当然不会执行 refresh 操作,如果buffer里面有数据,默认 1 秒钟执行一次 refresh 操作,刷入一个新的 segment file 中。(为什么叫 es 是准实时的? NRT,全称 near-real-time。默认是每隔 1 秒 refresh 一次的,所以 es 是准实时的,因为写入的数据 1 秒之后才能被看到。可以通过 es 的 restful API 或者 java api,手动执行一次 refresh 操作,就是手动将 buffer 中的数据刷入 os cache中,让数据立马就可以被搜索到。只要数据被输入 os cache 中,buffer 就会被清空了,因为不需要保留 buffer 了,数据在 translog 里面已经持久化到磁盘去一份了。)

  • 重复上面的步骤,新的数据不断进入 buffer 和 translog,不断将 buffer 数据写入一个又一个新的 segment file 中去,每次 refresh 完 buffer 清空,translog保留。随着这个过程推进,translog 会变得越来越大。当 translog 达到一定长度的时候,就会触发 commit 操作。commit 操作发生第一步,就是将 buffer 中现有数据 refresh 到 os cache 中去,清空 buffer。然后,将一个 commit point 写入磁盘文件,里面标识着这个 commit point 对应的所有 segment file,同时强行将 os cache 中目前所有的数据都 fsync 到磁盘文件中去。最后清空 现有 translog 日志文件,重启一个 translog,此时 commit 操作完成。这个 commit 操作叫做 flush。默认 30 分钟自动执行一次 flush,但如果 translog 过大,也会触发 flush。flush 操作就对应着 commit 的全过程,我们可以通过 es api,手动执行 flush 操作,手动将 os cache 中的数据 fsync 强刷到磁盘上去。

  • translog 日志文件的作用是什么?你执行 commit 操作之前,数据要么是停留在 buffer 中,要么是停留在 os cache 中,无论是 buffer 还是 os cache 都是内存,一旦这台机器死了,内存中的数据就全丢了。所以需要将数据对应的操作写入一个专门的日志文件 translog 中,一旦此时机器宕机,再次重启的时候,es 会自动读取 translog 日志文件中的数据,恢复到内存 buffer 和 os cache 中去。translog 其实也是先写入 os cache 的,默认每隔 5 秒刷一次到磁盘中去,所以默认情况下,可能有 5 秒的数据会仅仅停留在 buffer 或者 translog 文件的 os cache中,如果此时机器挂了,会丢失 5 秒钟的数据。但是这样性能比较好,最多丢 5 秒的数据。也可以将 translog 设置成每次写操作必须是直接 fsync 到磁盘,但是性能会差很多。(这里说明一个情况:es 是准实时的,数据写入 1 秒后可以搜索到;可能会丢失数据的。有 5 秒的数据,停留在 buffer、translog os cache、segment file os cache 中,而不在磁盘上,此时如果宕机,会导致 5 秒的数据丢失)

删除数据

  • 如果是删除操作,commit 的时候会生成一个 .del 文件,里面将某个 doc 标识为 deleted 状态,那么搜索的时候根据 .del 文件就知道这个 doc 是否被删除了

  • 如果是更新操作,就是将原来的 doc 标识为 deleted 状态,然后新写入一条数据。

每次 merge 的时候,会将多个 segment file 合并成一个,同时这里会将标识为 deleted 的 doc 给物理删除掉,然后将新的 segment file 写入磁盘,这里会写一个 commit point,标识所有新的 segment file,然后打开 segment file 供搜索使用,同时删除旧的 segment file。

查询数据

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

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

搜索数据

  • 客户端发送请求到一个 coordinate node
  • 协调节点将搜索请求转发到所有的 shard 对应的 primary shard 或 replica shard,都可以
  • query phase:每个 shard 将自己的搜索结果(其实就是一些 doc id)返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果
  • fetch phase:接着由协调节点根据 doc id 去各个节点上拉取实际的 document 数据,最终返回给客户端

Lucene

每个分片上对应着就是一个 Lucene Index(底层索引文件)
Lucene Index 是一个统称。由多个 Segment (段文件,就是倒排索引)组成。每个段文件存储着就是 Doc 文档

分词器

分词器(Analyzer)由一个分解器(Tokenizer)、零个或多个词元过滤器(TokenFilter)组成。
分词插件的核心就是提供各种分词器(Analyzer)、分解器(Tokenizer)、词元过滤器(TokenFilter);根据依赖的核心分词包(分词算法)的不同显现出不同的差异性。
IK
HanLP
jieba
THULAC

索引写入

无论是索引的创建还是删除,都必须在master上进行。因此,如果写入的请求是发到了非master节点,该节点会讲对应的创建或者删除的请求转发给master,master会创建并修改元数据和路由信息,并将对应的修改同步到其他的候选的master机器上,至少需要需要一半以上的候选master返回后才算写入成功。

文档写入

文档的写入的前提是所有的写入都必须先发送到主分片上,大致的步骤为:
a、文档写入请求发送到任意的一个节点
b、节点根据shard = hash(routing) % number_of_primary_shards确定数据所在的分片以及根据元数据确认主分片是在哪台机器
c、在主分片上执行写入操作和translog写入操作,并且将请求发送到副本上进行写入和translog写入
d、默认是同步操作,必须主分片和副本分片都些成功财返回结果,也可以人为调整为异步操作,不过会有数据安全性问题。

merge限制

index.merge.policy.expunge_deletes_allowed: 指删除了的文档数在一个segment里占的百分比,默认是10,大于这个值时,在执行expungeDeletes 操作时将会merge这些segments.
index.merge.policy.floor_segment: 官网的解释我没大看懂,我的个人理解是ES会避免产生很小size的segment,小于这个阈值的所有的非常小的segment都会做merge直到达到这个floor 的size,默认是2MB.
index.merge.policy.max_merge_at_once: 一次最多只操作多少个segments,默认是10.
index.merge.policy.max_merge_at_once_explicit: 显示调用optimize 操作或者 expungeDeletes时可以操作多少个segments,默认是30.
index.merge.policy.max_merged_segment: 超过多大size的segment不会再做merge,默认是5g.
index.merge.policy.segments_per_tier: 每个tier允许的segement 数,注意这个数要大于上面的at_once数,否则这个值会先于最大可操作数到达,就会立刻做merge,这样会造成频繁
index.reclaim_deletes_weight: 考虑merge的segment 时删除文档数量多少的权重,默认即可.
index.compund_format: 还不知道干啥用的,默认即可.

你可能感兴趣的:(elasticsearch源码)