简介
Elasticsearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java语言开发的,并作为Apache许可条款下的开放源码发布,是一种流行的企业级搜索引擎。
Lucene是apache软件基金会 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。
Elasticsearch基本概念
1. NRT(Near Realtime):近实时
两方面: 写入数据时,过1 秒才会被搜索到,因为内部在分词、录入索引。 es搜索时:搜索和分析数据只需要秒级出结果。
2. Cluster:集群
包含一个或多个启动着es实例的机器群。 通常一台机器起一个es实例。 同一网络下,几名一样的多个es实例自动组成 集群,自动均衡分片等行为。默认集群名为“elasticsearch”。
3. Node:节点
每个es实例称为一个节点。节点名自动分配,也可以手动配置。
4. Index:索引
包含一堆有相似结构的文档数据。
5. Document:文档
es中的最小数据单元。一个document就像数据库中的一条记录。通常以json格式显示。多个document存储于一个索引(Index)中。
6. Field:字段
就像数据库中的列(Columns),定义每个document应该有的字段。
7. Type:类型
每个索引里都可以有一个或多个type, type是index中的一个逻辑数据分类,一个type下的document,都有相同的field。 注意: 6.0之前的版本有type(类型)概念, type相当于关系数据库的表, ES官方将在ES9.0版本中彻底删除type。这里type都为_doc。
8. shard:分片
index数据过大时,将index里面的数据,分为多个shard,分布式的存储在各个服务器上面。可以支持海量数据和高并发,提升性能和吞吐量,充分利用多台机器的cpu。
9. replica:副本
在分布式环境下,任何一台机器都会随时宕机,如果宕机, index的一个分片没有,导致此index不能搜索。所以,为了保证数据的安全,我们会将每个index的分片经行备份,存储在另外的机器上。保证少数机器宕机es集群仍可以搜索。 能正常提供查询和插入的分片我们叫做主分片(primary shard),其余的我们就管他们叫做备份的分片(replica shard)。
es6新建索引时,默认5分片2副本,也就是一主一备,共10个分片。所以, es集群最小规模为两台。
ES索引原理
倒排索引,也叫反向索引,有反向索引必有正向索引。通俗地来讲,正向索引是通过key找value,反向索引则是通过value找key。
几个基本概念:
- Term(单词):一段文本经过分析器分析以后就会输出一串单词,这一个一个的就叫做Term
- Term Dictionary(单词字典):顾名思义,它里面维护的是Term,可以理解为Term的集合
- Term Index(单词索引):为了更快的找到某个单词,我们为单词建立索引
- Posting List(倒排列表):倒排列表记录了出现过某个单词的所有文档的文档列表及单词在该文档中出现的位置信息,每条记录称为一个倒排项(Posting)。根据倒排列表,即可获知哪些文档包含某个单词。
如果类比现代汉语词典的话,那么Term就相当于词语,Term Dictionary相当于汉语词典本身,Term Index相当于词典的目录索引。实际的倒排列表中并不只是存了文档ID这么简单,还有一些其它的信息,比如:词频(Term出现的次数)、偏移量(offset)等。
ES分别为每个字段都建立了一个倒排索引。
TermDictionary排序
排序后可进行二分或B+tree查找,否则需遍历查找很慢;
但是随机访问磁盘很慢,所以引入了TermIndex放入内存。
TermIndex结构(前缀树)
这棵树不会包含所有的term,它包含的是term的一些前缀。通过term index可以快速地定位到term dictionary的某个offset,然后从这个位置再往后顺序查找。所以term index不需要存下所有的term,而仅仅是他们的一些前缀与Term Dictionary的block之间的映射关系。从term index查到对应的term dictionary的block位置之后,再去磁盘上找term,大大减少了磁盘随机读的次数。
压缩技巧
1.TermIndex以FST(Finite State Transducers)压缩技术,FST以字节的方式存储所有的term,这种压缩方式可以有效的缩减存储空间,使得term index足以放进内存,但这种方式也会导致查找时需要更多的CPU资源。
2.前缀后缀规则:Term dictionary在磁盘上是以分block的方式保存的,一个block内部利用公共前缀压缩,比如都是Ab开头的单词就可以把Ab省去。这样term dictionary可以比b-tree更节约磁盘空间。
3. 差值规则:id列表使用增量编码压缩,排序后将大数变增量小数,按字节存储压缩posting list。原理就是先排序,然后通过增量,将原来的大数变成小数仅存储增量值,再精打细算按bit排好队,最后通过字节存储,而不是大大咧咧的尽管是2也是用int(4个字节)来存储。https://lucene.apache.org/core/2_9_4/fileformats.html#Frequencies
4.或然跟随规则:一般的情况下,在A后面放置一个Byte,为0则后面不存在B,为1则后面存在B,或者0则后面存在B,1则后面不存在B。但这样要浪费一个Byte的空间,其实一个Bit就可以了。在Lucene中,采取以下的方式:A的值左移一位,空出最后一位,作为标志位,来表示后面是否跟随B,所以在这种情况下,A/2是真正的A原来的值。
联合索引
核心就是多个posting list取交集才是多个条件都满足的id列表。
1.利用跳表(Skip list)的数据结构快速做“与”运算——如果使用跳表,对最短的posting list中的每个id,逐个在其它结果的posting list中查找看是否存在,最后得到交集的结果。
2.利用postingList的bitset按位“与”——直接按位与,得到的结果就是最后的交集
磁盘文件结构
文章参考和图片来源:
https://lucene.apache.org/core/2_9_4/fileformats.html
Lucene索引结构层次
名词定义
- 索引(Index):在Lucene中一个索引是放在一个文件夹中的,同一文件夹中的所有的文件构成一个Lucene索引。
- 段(Segment):
- 一个索引可以包含多个段,段与段之间是独立的,添加新文档可以生成新的段,不同的段可以合并。
- 具有相同前缀文件的属同一个段,如"_0.fnm", "_0.fdx", "_0.fdt"
- segments.gen和segments_XX是段的元数据文件,也即它们保存了段的属性信息。
- 文档(Document):
- 文档是建索引的基本单位,不同的文档是保存在不同的段中的,一个段可以包含多篇文档。
- 新添加的文档是单独保存在一个新生成的段中,随着段的合并,不同的文档合并到同一个段中。
- 域(Field):
- 一篇文档包含不同类型的信息,可以分开索引,比如标题,时间,正文,作者等,都可以保存在不同的域里。
- 不同域的索引方式可以不同。
- 词(Term):
- 词是索引的最小单位,是经过词法分析和语言处理后的字符串。
文件类型
- segments_XX保存了此索引包含多少个段,每个段包含多少篇文档。
- XXX.fnm保存了此段包含了多少个域,每个域的名称及索引方式。
- XXX.fdx,XXX.fdt保存了此段包含的所有文档,每篇文档包含了多少域,每个域保存了那些信息。
- XXX.tvx,XXX.tvd,XXX.tvf保存了此段包含多少文档,每篇文档包含了多少域,每个域包含了多少词,每个词的字符串,位置等信息。(向量词)
- XXX.tis,XXX.tii保存了词典(Term Dictionary),也即此段包含的所有的词按字典顺序的排序。
- XXX.frq保存了倒排表,也即包含每个词的文档ID列表。
- XXX.prx保存了倒排表中每个词在包含此词的文档中的位置。
查找过程
1.(前缀查找)内存前缀树找到词前缀索引所在的文件位置tii
2.(词典查找)tii词典索引找到词对应的文件位置tis
3.(通过词找文档索引)tis找到词对应的多个文档索引所在的文件位置fdx
4.(文档索引找文档)通过文档索引fdx找到文档所在的文件位置fdt
1.词典文件(tii、tis)
- 词典文件(tis)
- TermCount:词典中包含的总的词数
- IndexInterval:为了加快对词的查找速度,也应用类似跳跃表的结构,假设IndexInterval为4,则在词典索引(tii)文件中保存第4个,第8个,第12个词,这样可以加快在词典文件中查找词的速度。
- SkipInterval:倒排表无论是文档号及词频,还是位置信息,都是以跳跃表的结构存在的,SkipInterval是跳跃的步数。
- MaxSkipLevels:跳跃表是多层的,这个值指的是跳跃表的最大层数。
- TermCount个项的数组,每一项代表一个词,对于每一个词,以前缀后缀规则存放词的文本信息(PrefixLength + Suffix),词属于的域的域号(FieldNum),有多少篇文档包含此词(DocFreq),此词的倒排表在frq,prx中的偏移量(FreqDelta, ProxDelta),此词的倒排表的跳跃表在frq中的偏移量(SkipDelta),这里之所以用Delta,是应用差值规则。
- 词典索引文件(tii)
- 词典索引文件是为了加快对词典文件中词的查找速度,保存每隔IndexInterval个词。
- 词典索引文件是会被全部加载到内存中去的。
- IndexTermCount = TermCount / IndexInterval:词典索引文件中包含的词数。
- IndexInterval同词典文件中的IndexInterval。
- SkipInterval同词典文件中的SkipInterval。
- MaxSkipLevels同词典文件中的MaxSkipLevels。
- IndexTermCount个项的数组,每一项代表一个词,每一项包括两部分,第一部分是词本身(TermInfo),第二部分是在词典文件中的偏移量(IndexDelta)。假设IndexInterval为4,此数组中保存第4个,第8个,第12个词。。。
2.词频(ID列表)和词位置(frq、prx)
- 词频文件(frq)
- 存有文档号倒排列表及文档的词频。
- 此文件包含TermCount个项,每一个词都有一项。
- 对于每项包括两部分,一部分是倒排表本身(也即一个数组的文档号及词频),另一部分是词对应的文档列表的跳表,为了更快的定位文档(每一元素包含指向倒排表的指针和指向词位置prx的指针)。
- 跳表元素有两个指针,
- 对于文档号和词频的存储应用的是差值规则(增量编码压缩)
- 词位置(prx)
- 词位置信息也是倒排表,也是以跳表形式查找。
- 此文件包含TermCount个项,每一个词都有一项,因为每一个词都有自己的词位置倒排表。
- 对于每一个词的都有一个DocFreq大小的数组,每项代表一篇文档,记录此文档中此词出现的位置。这个文档数组也是和frq文件中的跳跃表有关系的,从上面我们知道,在frq的跳跃表节点中有ProxSkip,当SkipInterval为3的时候,frq的跳跃表节点指向prx文件中的此数组中的第1,第4,第7,第10,第13,第16篇文档。
- 对于每一篇文档,可能包含一个词多次,因而有一个Freq大小的数组,每一项代表此词在此文档中出现一次,则有一个位置信息。
- 每一个位置信息包含:PositionDelta(采用差值规则),还可以保存payload,应用或然跟随规则。
3.数据文件(fdt、fcx)
- 域索引文件(fdx)
- 每篇文档包含的域的个数、每个存储域的值都是不一样的,因而域数据文件中segment size篇文档,每篇文档占用的大小也是不一样的,那么如何在fdt中辨别每一篇文档的起始地址和终止地址呢,如何能够更快的找到第n篇文档的存储域的信息呢?就是要借助域索引文件。
- 域索引文件也总共有segment size个项,每篇文档都有一个项,每一项都是一个long,大小固定,每一项都是对应的文档在fdt文件中的起始地址的偏移量,这样如果我们想找到第n篇文档的存储域的信息,只要在fdx中找到第n项,然后按照取出的long作为偏移量,就可以在fdt文件中找到对应的存储域的信息。
- 域数据文件(fdt):
- 真正保存存储域信息的是fdt文件
- 在一个段(segment)中总共有segment size篇文档,所以fdt文件中共有segment size个项,每一项保存一篇文档的域的信息
- 对于每一篇文档,一开始是一个fieldcount,即此文档包含的域的数目,接下来是fieldcount个项,每一项保存一个域的信息。
- 对于每一个域,fieldnum是域号,接着是一个8位的byte,最低一位表示此域是否分词(tokenized),倒数第二位表示此域是保存字符串数据还是二进制数据,倒数第三位表示此域是否被压缩,再接下来就是存储域的值,如"lucene in action"字符串。
集群Master选举
ES不需要依赖于ZK之类的外部服务,而是实现了一套自己的选举策略(比Paxos简单了许多的Bully算法):
- 每次选举每个node对该节点已知的有资格成为master的node进行sort by nodeId,然后取第一个get(0)认定他为master节点。
- 当某个节点获得的票数到达(有资格成为master的节点数 n/2 + 1,即discovery.zen.minimum_master_nodes)且该节点也投票给自己时,那么它成为master,否则重新选举。
- discovery.zen.minimum_master_nodes取值为半数+1,是为了避免brain split脑裂问题
即:
- 如果集群中存在master,认可该master,加入集群;
- 如果集群中不存在master,从具有master资格的节点中选id最小的节点作为master。
选举时机
- 集群启动:后台启动线程去ping集群中的节点,按照上述策略从具有master资格的节点中选举出master;
- 现有的master离开集群:后台一直有一个线程定时ping master节点,超过一定次数没有ping成功之后,重新进行master的选举。
ES避免脑裂的策略
过半原则,可以在ES的集群配置中设置选举时需要的节点连接数过半,即discovery.zen.minimum_master_nodes=N/2+1
对于网络波动比较大的集群:适当增加ping的间隔时间和ping的次数,一定程度上可以增加集群的稳定性。
ES的节点类型主要分为如下几种:
- Master Eligible主节点:每个节点启动后,默认就是Master Eligible节点,可以通过设置node.master: false 来禁止。Master Eligible可以参加选主流程,并成为Master节点(当第一个节点启动后,它会将自己选为Master节点);注意:每个节点都保存了集群的状态,只有Master节点才能修改集群的状态信息。
- Data数据节点:可以保存数据的节点。主要负责保存分片数据,利于数据扩展。
- Coordinating 请求分发节点:负责接收客户端请求,将请求发送到合适的节点,最终把结果汇集到一起
注意:每个节点默认都起到了Coordinating node的职责。一般在开发环境中一个节点可以承担多个角色,但是在生产环境中,还是设置单一的角色比较好,因为有助于提高性能。
集群的三种状态
Green
所有主分片和备份分片都准备就绪,分配成功, 即使有一台机器挂了(假设一台机器实例),数据都不会丢失(但是会变成yellow状态)。
Yellow
所有主分片准备就绪,但至少一个主分片(假设是A)对应的备份分片没有就绪,此时集群处于告警状态,意味着高可用和容灾能力下降.如果刚好A所在的机器挂了,并且你只设置了一个备份(且已处于未就绪状态), 那么A的数据就会丢失(查询不完整),此时集群将变成Red状态。
Red
至少有一个主分片没有就绪(直接原因是找不到对应的备份分片成为新的主分片),此时查询的结果会出现数据丢失(不完整)。
ES与数据库同步方案
mysql --(binLog)--> cannel ----> es