文档的容器,一类文档的集合
单个节点由于物理机硬件限制,存储的文档是有限的,如果一个索引包含海量文档,则不能在单个节点存储。ES提供分片机制,同一个索引可以存储在不同分片中,这些分片又可以存储在集群中不同节点上。分片分为 primary shard 和 replica shard
可搜索的最小单元,json格式保存
RDBMS | ES |
---|---|
Table | Index |
Row | Document |
Column | Field |
Schema | Mapping |
SQL | DSL |
多节点集群方案提高了整个系统的并发处理能力。
当索引一个文档的时候,文档会被存储到一个主分片中,那么如何判断一个文档存储到哪个主分片中呢?
shard = hash(routing) % number_of_primary_shards
routing 是一个可变值,默认是文档的 _id,也可以设置成一个自定义的值。number_of_prmary_shards,所以在创建索引时候就确定以好主分片的数量。
节点分为主节点、数据节点和客户端节点(只做请求的分发和汇总)。每个节点都可以接受客户端的请求,每个节点都知道集群中任意文档位置,所以可以直接将请求转发到需要的节点上,当接受请求后,节点变为协调节点,参与转发。
以局部更新文档为例:
1、客户端向 Node1 发送更新请求,Node1 根据 _id 确认属于P1
2、将请求转发到主分片P1所在的 Node3
3、Node3 从主分片检索文档,修改 _source 字段中的 JSON,并且尝试重新索引主分片的文档,如果文档已经被另一个进程修改,会重试步骤3,超过 retry_on_conflict 次后放弃
4、如果 Node3 成功的更新文档,它将新版本的文档并行转发到 Node1 和 Node2 的副本分片去重新建立索引,一旦所有副本分片都返回成功,Node3 向协调节点返回成功,协调节点向客户端返回成功。
基于文档的复制
当主分片把更改转发到副本分片时,不会转发更新请求。相反转发完成文档的新版本。需要注意的是这些更改将会异步转发到副本分片,并且不能保证它们以发送它们相同的顺序到达。如果ES仅转发更改请求,则可能以错误的顺序应用更改,导致得到损坏的文档。
原集群:索引设置的是 3 主 1 从(索引设置的3个主分片,每个主分片一个从分片)。Node1 是 master 节点,P0、P1、P2 是主分片,R0、R1、R2 是从分片。
问题1:文本如何分析?
为了提高可检索性(比如希望大小写单词全部返回),应当先用分词器,分析文档再对其索引,分析包括两个部分:
默认情况下,ES 使用标准分析器,使用了:
还有很多可用的分析器不在此列举,为了实现查询时能得到对应的结果,查询时应使用与索引时一致的分析器,对文档进行分析。
精确值一般不会被分词器分词,全文本需要分词器处理
问题2:倒排索引是什么?如何提升搜索速度?
下图就是一个倒排索引:
docid | Age | Sex |
---|---|---|
1 | 18 | 女 |
2 | 20 | 女 |
3 | 18 | 男 |
ID 是文档 id ,那么建立的索引如下:
Age:
term | PostingList |
---|---|
18 | [1, 3] |
20 | [2] |
Sex:
term | PostingList |
---|---|
男 | [3] |
女 | [1, 2] |
1、PostingList 是一个 int 的数组,存储所有符合某个 term 的文档 id
2、term 数量很多,查找某个指定的 term 会变慢,因此采用 Term Dictionary,对于 term 进行排序,采用二分查找查询 logN次才能查到
3、即便变成查询 logN 次,但是由于数据不可能全量放在内存,因此还是存在磁盘操作,磁盘操作导致耗时增加,因此通过 trie 树,构建 Term Index。一般的 Trie树实现,例如 abc、ab,cd,c,mn 这些term,建立 trie 树,如下所示:
该 trie 树不会存储所有的 terms。当查找对应的 term,根据 trie 树找到 Term Dictionary 对应的 offset, 从偏移的位置往后顺序查找。除此以外,term index 在内存中是以 FST(finite state transducers)的形式保存的,其特点是非常节省内存的。Term Dictionary 在磁盘上是以分 block 的方式保存的,一个 block 内部利用公共前缀压缩,比如都是 Ab 开头的单词就可以把 Ab省去。这样 term Dictionary 可以比 b-tree 更加节约磁盘空间。
由此可以看出 MySQL 和 ES 的不同,MySQL 采用 B+ 树建立索引,相当于做了 Term Dictionary 这一层。但是 es 还支持了 term index,并且采用有效的算法,能够快速定位到对应的 term,减少随机查询磁盘的次数。
问题3:倒排索引的特点?
倒排索引被写入磁盘后是不可以改变的;它永远不会修改,不变性有重要的价值:
1、不需要锁:如果一直不更新索引,就不需要担心多进程同时修改数据的问题
2、一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
3、其它缓存(像filter缓存),在索引的生命周期内始终有效,它们不需要再每次数据改变时被重建,因为数据不会变化
4、写入单个大的倒排索引允许数据被压缩,减少磁盘 IO 和需要被缓存到内存的索引的使用量
当然,一个不变的索引也有不好的地方,主要事实是它是不可变的,无法进行修改。如果需要让一个新的文档可被搜索,需要重建珍整个索引,要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。
上面讲到倒排索引的不变性,那么如果文档更新,该如何更新倒排索引呢?用更多的索引。
ES 底层基于 Lucene,最核心的概念就是 Segment(段),每个段本身就是一个倒排索引,ES 的 index 由多个段的集合和 commit point(提交点,一个列出了所有已知段的文件)文件组成。
一个 Lucene 索引,在 ES 称作分片,一个 ES 索引是分片的集合,当 ES 在索引中搜索的时候,发送查询到每一个属于索引的分片(Lucene 索引),然后像分布式检索提到的集群,合并每个分片的结果到一个全局的结果集。
新的文档首先被添加到内存索引缓存中,然后写入到一个基于磁盘的段,如下图所示:
一个 Lucene 索引(三个段加上一个提交点) + 内存索引缓存
当一个查询触发时,按段对所有已知的段查询,词项统计会对所有段的结果进行聚合,以保证每个词和每个文档的关联都被准确计算。这种方式可以用相对较低的成本将新文档添加到索引。那聚合过程中,对于更新的或者删除的数据如何处理的呢?
段是不可改变的,所以既不能从把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。取而代之的是每个提交点包含一个 .del 文件,文件中会列出这些被删除文档的段信息,当一个文档被 “删除” 时,它实际上只是在 .del 文件中被标记删除。一个被标记删除的文档仍然可以被查询匹配时,但它会在最终结果被返回前从结果集中移除,文档更新也是类似的操作方式;当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。
随着按段搜索的发展,一个新的文档从索引到可被搜索的延迟显著降低了,新文档在几分钟之内即可被检索,但这样还是不够快。因为提交一个新的段到磁盘需要一个 fsync 来确保段被物理性的写入磁盘,但是 fsync 的操作代价很大,如果每次索引一个文档都去执行一次的话会造成很大的性能问题。
在 ES 和磁盘之间是文件系统缓存,在内存索引缓冲区中的文档会被写入到一个新的段中,但是这里新段会被写入到文件系统缓存,稍后再被刷新到磁盘,不过只要文件已经在文件系统缓存中,就可以像其他文件一样被打开和读取了。
refresh
在 ES 中,写入和打开一个新段的轻量的过程叫做 refresh 。默认情况下每个分片会每秒自动刷新一次。这就是为什么说 ES 是近实时搜索:文档的变化并不是立即对搜索可见,但是会在一秒之内变为可见。一般情况下每秒会新增一个端。
之前讲到,如果不进行 fsync 操作,数据还是在文件系统缓存中,但系统断电,数据无法恢复,那如何保证数据变更记录在硬盘上。一次完成的提交会将段刷到磁盘,并写入一个包含所有段列表的提交点,ES 在启动或重新打开一个索引的过程中使用这个提交点来判断哪些段属于当前分片。ES 增加了一个 translog,或者叫事务日志,在每一次对 ES 进行非操作时均进行了日志记录,通过 translog,整个流程是下图所示:
1、一个文档被索引之后,就会被添加到内存缓冲区,并且追加到了 translog
2、刷新使分片处于下图的状态,分片每秒被刷新一次
· 这些在内存缓冲区的文档被写入到一个新的段中,且没有进行 fsync 操作
· 这个段被打开,使其可被搜索
· 内存缓冲区被清空
3、这个进程继续工作,更多的文档被添加到内存缓冲区和追加到事务日志
4、每隔一段时间,当 translog 变得越来越大时,索引被刷新,一个新的 translog 被创建,并且一个全量提交被执行
flush
在 ES 中,所有在内存中的 segment 被提交到了磁盘,同时清除 translog,一般 Flush 的时间间隔会比较久,默认 30 分钟,或者当 translog 达到一定的大小,也会触发 flush 操作。
ES 的 refresh 操作是为了让最新的数据可以立即被搜索到,而 flush 操作则是为了让数据持久化到磁盘中,另外 ES 的搜索是在内存中处理的,因此 flush 操作不影响数据是否被搜索到。
之前讲到每次 refresh,一般情况下每秒新增一个段,这样段的数量会比较快的增大。所以后台会进行段合并,小段合并成大段。并且在合并过程中会将旧的已删除文档从文件系统中清除,不会合并到大段中。
合并的过程:
1、当索引的时候,刷新操作会创建新的段并将段打开以供搜索使用,此时没有写入磁盘还会保存到磁盘缓存;
2、合并过程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中,这并不会中断索引和搜索;
3、合并结束,老的段删除,合并完成时的工作:
· 新的段被刷新到磁盘中,translog 写入一个包含新段且排除旧的和较小的段的新提交点
· 新的段被打开用来搜索
ES 并发写操作,采用的是乐观并发锁控制,由于 ES 中的文档是不可变更的,如果需要更新一个文档,先将当前文档标记为删除,同时新增一个文档,并且将文档 version + 1,目前有两种并发控制:
1、内部版本控制:if_seq_no + id_primary_term
eg. 首先 put 文档,_seq_no = 0, _primary_term = 1;再次 put,需要根据版本号处理才能更新成功
2、外部版本控制(使用外部数据库版本号控制):version + version_type
eg. 首先 put 文档,version = 10,version_type = external;再次 put 文档,必须增大 version,即 version = 11,version_type = external