阅读 123

ES

1 前言

1 大规模数据如何检索

当系统数据量上了10亿、100亿条的时候,我们在做系统架构的时候通常会从以下角度去考虑:
1)用什么数据库好?(MySQL、sybase、Oracle、达梦、神通、MongoDB、Hbase…)
2)如何解决单点故障?(lvs、F5、A10、Zookeeper、MQ…)
3)如何保证数据安全性?(热备、冷备、异地多活)
4)如何解决检索难题?(数据库代理中间件mysql-proxy、Cobar、MaxScale…)
5)如何解决统计分析问题?(离线、近实时)

2 传统数据库的应对解决方案

对于关系型数据,我们通常采用以下或类似架构去解决查询瓶颈和写入瓶颈:
1)通过主从备份解决数据安全性问题;
2)通过数据库代理中间件心跳监测,解决单点故障问题;
3)通过代理中间件将查询语句分发到各个slave节点进行查询,并汇总结果;
4)通过分表分库解决读写效率问题;

3 非关系型数据库的解决方案

对于Nosql数据库,以redis为例,其它原理类似:
1)通过副本备份保证数据安全性;
2)通过节点竞选机制解决单点问题;
3)先从配置库检索分片信息,然后将请求分发到各个节点,最后由路由节点合并汇总结果;

4 完全把数据放入内存怎么样

完全把数据放在内存中是不可靠的,实际上也不太现实,
当我们的数据达到PB级别时,按照每个节点96G内存计算,在内存完全装满的数据情况下,
我们需要的机器是:1PB=1024T=1048576G 节点数=1048576/96=10922个
实际上,考虑到数据备份,节点数往往在2.5万台左右。成本巨大决定了其不现实!
从前面我们了解到,把数据放在内存也好,不放在内存也好,都不能完完全全解决问题。
全部放在内存速度问题是解决了,但成本问题上来了。
为解决以上问题,从源头着手分析,通常会从以下方式来寻找方法:
1 存储数据时按有序存储;
2 将数据和索引分离;
3 压缩数据;

2 全文检索技术

2.1 什么是全文检索

Lucene 是一个高效的,基于Java 的全文检索库。
什么叫做全文检索呢?这要从我们生活中的数据说起。
我们生活中的数据总体分为两种:结构化数据和非结构化数据。
结构化数据:指具有固定格式或有限长度的数据,如数据库、元数据等。
非结构化数据:指不定长或无固定格式的数据,如邮件、word文档等。
当然有的地方还会提到第三种,半结构化数据,如XML,HTML等,当根据需要可按结构化数据来处理,也可抽取出纯文本按非结构化数据来处理。
非结构化数据又一种叫法叫全文数据。

按照数据的分类,搜索也分为两种:
对结构化数据的搜索: 如对数据库的搜索,用SQL语句。再如对元数据的搜索,如利用windows 搜索对文件名,类型,修改时间进行搜索等。
对非结构化数据的搜索: 如利用windows的搜索也可以搜索文件内容,Linux下的grep命令,再如用Google和百度可以搜索大量内容数据。

对非结构化数据也即全文数据的搜索主要有两种方法:顺序扫描法和全文检索

所谓顺序扫描,比如要找内容包含某一个字符串的文件,就是一个文档一个文档的看,对于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下一个文件,直到扫描完所有的文件。如利用windows的搜索也可以搜索文件内容,只是相当的慢。如果你有一个80G硬盘,如果想在上面找到一个内容包含某字符串的文件,不花他几个小时,怕是做不到。Linux下的grep命令也是这一种方式。大家可能觉得这种方法比较原始,但对于小数据量的文件,这种方法还是最直接,最方便的。但是对于大量的文件,这种方法就很慢了。
有人可能会说,对非结构化数据顺序扫描很慢,对结构化数据的搜索却相对较快(由于结构化数据有一定的结构可以采取一定的搜索算法加快速度),那么把我们的非结构化数据想办法弄得有一定结构不就行了吗?这种想法很天然,却构成了全文检索的基本思路,也即将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引 。这种说法比较抽象,举几个例子就很容易明白,比如字典,字典的拼音表和部首检字表就相当于字典的索引,对每一个字的解释是非结构化的,如果字典没有音节表和部首检字表,在茫茫辞海中找一个字只能顺序扫描。然而字的某些信息可以提取出来进行结构化处理,比如读音,就比较结构化,分声母和韵母,分别只有几种可以一一列举,于是将读音拿出来按一定的顺序排列,每一项读音都指向此字的详细解释的页数。我们搜索时按结构化的拼音搜到读音,然后按其指向的页数,便可找到我们的非结构化数据——也即对字的解释。这种先建立索引,再对索引进行搜索的过程就叫全文检索(Full-text Search)

下面这幅图来自《Lucene in action》,但却不仅仅描述了Lucene的检索过程,而是描述了全文检索的一般过程。
全文检索大体分两个过程,索引创建 (Indexing) 和搜索索引 (Search) 。

01.png

索引创建:将现实世界中所有的结构化和非结构化数据提取信息,创建索引的过程。
搜索索引:就是得到用户的查询请求,搜索创建的索引,然后返回结果的过程。

2.2 索引里面究竟存些什么

索引里面究竟需要存些什么呢?
首先我们来看为什么顺序扫描的速度慢。其实是由于我们想要搜索的信息和非结构化数据中所存储的信息不一致造成的。
非结构化数据中所存储的信息是每个文件包含哪些字符串,也即已知文件,欲求字符串相对容易,也即是从文件到字符串的映射。而我们想搜索的信息是哪些文件包含此字符串,也即已知字符串,欲求文件,也即从字符串到文件的映射。两者恰恰相反。于是如果索引总能够保存从字符串到文件的映射,则会大大提高搜索速度。由于从字符串到文件的映射是文件到字符串映射的反向过程,于是保存这种信息的索引称为反向索引
反向索引的所保存的信息一般如下:
假设我的文档集合里面有100篇文档,为了方便表示,我们为文档编号从1到100,得到下面的结构

02.png

左边保存的是一系列字符串,称为词典 。每个字符串都指向包含此字符串的文档(Document)链表,此文档链表称为倒排表 (Posting List)。有了索引,便使保存的信息和要搜索的信息一致,可以大大加快搜索的速度。比如说,我们要寻找既包含字符串“lucene”又包含字符串“solr”的文档,我们只需要以下几步:
① 取出包含字符串“lucene”的文档链表。
② 取出包含字符串“solr”的文档链表。
③ 通过合并链表,找出既包含“lucene”又包含“solr”的文件。

03.png

看到这个地方,有人可能会说,全文检索的确加快了搜索的速度,但是多了索引的过程,两者加起来不一定比顺序扫描快多少。的确,加上索引的过程,全文检索不一定比顺序扫描快,尤其是在数据量小的时候更是如此,而对一个很大量的数据创建索引也是一个很慢的过程,然而两者还是有区别的。顺序扫描是每次都要扫描,而创建索引的过程仅仅需要一次,以后便是一劳永逸的了,每次搜索,创建索引的过程不必经过,仅仅搜索创建好的索引就可以了。这也是全文搜索相对于顺序扫描的优势之一:一次索引,多次使用

2.3 如何创建索引

全文检索的索引创建过程一般有以下几步:

第一步:一些要索引的原文档(Document)。

为了方便说明索引创建过程,这里特意用两个文件为例:
文件一:Students should be allowed to go out with their friends, but not allowed to drink beer.
文件二:My friend Jerry went to school to see his students but found them drunk which is not allowed.

第二步:将原文档传给分词器(Tokenizer)。

分词器(Tokenizer)会做以下几件事情(此过程称为Tokenize) :
① 将文档分成一个一个单独的单词。
② 去除标点符号。
③ 去除停词(Stop word) 。
所谓停词(Stop word)就是一种语言中最普通的一些单词,由于没有特别的意义,因而大多数情况下不能成为搜索的关键词,因而创建索引时,这种词会被去掉而减少索引的大小。英语中停词如:“the”,“a”,“this”等。对于每一种语言的分词组件(Tokenizer),都有一个停词(stop word)集合。经过分词(Tokenizer) 后得到的结果称为词元(Token) 。在我们的例子中,便得到以下词元(Token):

“Students”,“allowed”,“go”,“their”,“friends”,“allowed”,“drink”,“beer”,“My”,“friend”,
“Jerry”,“went”,“school”,“see”,“his”,“students”,“found”,“them”,“drunk”,“allowed”。

第三步:将得到的词元(Token)传给语言处理组件(Linguistic Processor)。

语言处理组件(linguistic processor)主要是对得到的词元(Token)做一些同语言相关的处理。
对于英语,语言处理组件(Linguistic Processor) 一般做以下几点:
① 变为小写(Lowercase) 。
② 将单词缩减为词根形式,如“cars ”到“car ”等。这种操作称为:stemming 。
③ 将单词转变为词根形式,如“drove ”到“drive ”等。这种操作称为:lemmatization 。

Stemming 和 lemmatization的异同
相同之处:Stemming和lemmatization都要使词汇成为词根形式。
两者的方式不同
Stemming采用的是“缩减”的方式:“cars”到“car”,“driving”到“drive”。
Lemmatization采用的是“转变”的方式:“drove”到“drove”,“driving”到“drive”。
两者的算法不同
Stemming主要是采取某种固定的算法来做这种缩减,如去除“s”,去除“ing”加“e”,将“ational”变为“ate”,将“tional”变为“tion”。
Lemmatization主要是采用保存某种字典的方式做这种转变。比如字典中有“driving”到“drive”,“drove”到“drive”,“am, is, are”到“be”的映射,做转变时,只要查字典就可以了。
Stemming和lemmatization不是互斥关系,是有交集的,有的词利用这两种方式都能达到相同的转换。
语言处理组件(linguistic processor)的结果称为词(Term)
在我们的例子中,经过语言处理,得到的词(Term)如下:

“student”,“allow”,“go”,“their”,“friend”,“allow”,“drink”,“beer”,“my”,“friend”,
“jerry”,“go”,“school”,“see”,“his”,“student”,“find”,“them”,“drink”,“allow”。

也正是因为有语言处理的步骤,才能使搜索drove,而drive也能被搜索出来。

第四步:将得到的词(Term)传给索引组件(Indexer)。

索引组件(Indexer)主要做以下几件事情:
① 利用得到的词(Term)创建一个字典。
在我们的例子中字典如下:

Term Document ID
student 1
allow 1
go 1
their 1
friend 1
allow 1
drink 1
beer 1
my 2
friend 2
jerry 2
go 2
school 2
see 2
his 2
student 2
find 2
them 2
drink 2
allow 2

② 对字典按字母顺序进行排序。

Term Document ID
allow 1
allow 1
allow 2
beer 1
drink 1
drink 2
find 2
friend 1
friend 2
go 1
go 2
his 2
jerry 2
my 2
school 2
see 2
student 1
student 2
their 1
them 2

③ 合并相同的词(Term) 成为文档倒排(Posting List) 链表。


04.png

在此表中,有几个定义:
Document Frequency 即文档频次,表示总共有多少文件包含此词(Term)。
Frequency 即词频率,表示此文件中包含了几个此词(Term)。
所以对词(Term) “allow”来讲,总共有两篇文档包含此词(Term),从而词(Term)后面的文档链表总共有两项,第一项表示包含“allow”的第一篇文档,即1号文档,此文档中,“allow”出现了2次,第二项表示包含“allow”的第二个文档,是2号文档,此文档中,“allow”出现了1次。
到此为止,索引已经创建好了,我们可以通过它很快的找到我们想要的文档
而且在此过程中,我们惊喜地发现,搜索“drive”,“driving”,“drove”,“driven”也能够被搜到。因为在我们的索引中,“driving”,“drove”,“driven”都会经过语言处理而变成“drive”,在搜索时,如果您输入“driving”,输入的查询语句同样经过我们这里的一到三步,从而变为查询“drive”,从而可以搜索到想要的文档。

2.4 如何对索引进行搜索

到这里似乎我们可以宣布“我们找到想要的文档了”。然而事情并没有结束,找到了仅仅是全文检索的一个方面。不是吗?如果仅仅只有一个或十个文档包含我们查询的字符串,我们的确找到了。然而如果结果有一千个,甚至成千上万个呢?哪个又是您最想要的文件呢?
打开Google吧,比如说您想在微软找份工作,于是您输入“Microsoft job”,您却发现总共有22600000个结果返回。好大的数字呀,突然发现找不到是一个问题,找到的太多也是一个问题。在如此多的结果中,如何将最相关的放在最前面呢?


05.png

当然Google做的很不错,您一下就找到了jobs at Microsoft。想象一下,如果前几个全部是“Microsoft does a good job at software industry…”将是多么可怕的事情呀。如何像Google一样,在成千上万的搜索结果中,找到和查询语句最相关的呢?如何判断搜索出的文档和查询语句的相关性呢?这要回到我们第三个问题:如何对索引进行搜索?
搜索主要分为以下几步:

第一步:用户输入查询语句。

查询语句同我们普通的语言一样,也是有一定语法的。不同的查询语句有不同的语法,如SQL语句就有一定的语法。查询语句的语法根据全文检索系统的实现而不同。最基本的有比如:AND, OR, NOT等。举个例子,用户输入语句:lucene AND learned NOT hadoop。说明用户想找一个包含lucene和learned然而不包括hadoop的文档。

第二步:对查询语句进行词法分析、语法分析及语言处理。

由于查询语句有语法,因而也要进行词法分析、语法分析及语言处理。

  1. 词法分析主要用来识别单词和关键字。
    如上述例子中,经过词法分析,得到单词有lucene,learned,hadoop, 关键字有AND, NOT。如果在词法分析中发现不合法的关键字,则会出现错误。如lucene AMD learned,其中由于AND拼错,导致AMD作为一个普通的单词参与查询。

  2. 语法分析主要是根据查询语句的语法规则来形成一棵语法树。
    如果发现查询语句不满足语法规则,则会报错。如lucene NOT AND learned,则会出错。如上述例子,lucene AND learned NOT hadoop形成的语法树如下:


    06.png
  3. 语言处理同索引过程中的语言处理几乎相同。
    如learned变成learn等。经过第二步,我们得到一棵经过语言处理的语法树。


    07.png

第三步:搜索索引,得到符合语法树的文档。

此步骤又分几小步。首先,在反向索引表中,分别找出包含lucene,learn,hadoop的文档链表。其次,对包含lucene,learn的链表进行合并操作,得到既包含lucene又包含learn的文档链表。然后,将此链表与hadoop的文档链表进行差操作,去除包含hadoop的文档,从而得到既包含lucene又包含learn而且不包含hadoop的文档链表。此文档链表就是我们要找的文档。

第四步:根据得到的文档和查询语句的相关性,对结果进行排序。

虽然在上一步,我们得到了想要的文档,然而对于查询结果应该按照与查询语句的相关性进行排序,越相关者越靠前。
如何计算文档和查询语句的相关性呢?不如我们把查询语句看作一片短小的文档,对文档与文档之间的相关性(relevance)进行打分(scoring),分数高的相关性好,就应该排在前面
那么又怎么对文档之间的关系进行打分呢? 这可不是一件容易的事情!下面看一下如何判断文档之间的关系。
首先,一个文档有很多词(Term)组成 ,如search, lucene, full-text, this, a, what等。
其次对于文档之间的关系,不同的Term重要性不同 。比如对于本篇文档,search, Lucene, full-text就相对重要一些,this, a , what可能相对不重要一些。所以如果两篇文档都包含search, Lucene,fulltext,这两篇文档的相关性好一些,然而就算一篇文档包含this, a, what,另一篇文档不包含this, a, what,也不能影响两篇文档的相关性。
因而判断文档之间的关系,首先找出哪些词(Term)对文档之间的关系最重要,如search, Lucene, fulltext。然后判断这些词(Term)之间的关系。
找出词(Term) 对文档的重要性的过程称为计算词的权重(Term weight) 的过程。计算词的权重(term weight)有两个参数,第一个是词(Term),第二个是文档(Document)。词的权重(Term weight)表示此词(Term)在此文档中的重要程度,越重要的词(Term)有越大的权重(Term weight),因而在计算文档之间的相关性中将发挥更大的作用。
判断词(Term) 之间的关系从而得到文档相关性的过程应用一种叫做向量空间模型的算法(Vector Space Model) 。下面仔细分析一下这两个过程

1 计算权重(Term weight)的过程。
影响一个词(Term)在一篇文档中的重要性主要有两个因素:Term Frequency (tf):即此Term在此文档中出现了多少次。tf 越大说明越重要。Document Frequency (df):即有多少文档包含次Term。df 越大说明越不重要。容易理解吗?词(Term)在文档中出现的次数越多,说明此词(Term)对该文档越重要,如“搜索”这个词,在本文档中出现的次数很多,说明本文档主要就是讲这方面的事的。然而在一篇英语文档中,this出现的次数更多,就说明越重要吗?不是的,这是由第二个因素进行调整,第二个因素说明,有越多的文档包含此词(Term), 说明此词(Term)太普通,不足以区分这些文档,因而重要性越低。
这也如我们程序员所学的技术,对于程序员本身来说,这项技术掌握越深越好(掌握越深说明花时间看的越多,tf越大),找工作时越有竞争力。然而对于所有程序员来说,这项技术懂得的人越少越好(懂得的人少df小),找工作越有竞争力。人的价值在于不可替代性就是这个道理。道理明白了,我们来看看公式:


08.png

这仅仅只term weight计算公式的简单典型实现。实现全文检索系统的人会有自己的实现,Lucene就与此稍有不同。
2 判断Term之间的关系从而得到文档相关性的过程,也即向量空间模型的算法(VSM)。
我们把文档看作一系列词(Term),每一个词(Term)都有一个权重(Term weight),不同的词(Term)根据自己在文档中的权重来影响文档相关性的打分计算。于是我们把所有此文档中词(term)的权重(term weight) 看作一个向量。
Document = {term1, term2, …… ,term N}
Document Vector = {weight1, weight2, …… ,weight N}
同样我们把查询语句看作一个简单的文档,也用向量来表示。
Query = {term1, term 2, …… , term N}
Query Vector = {weight1, weight2, …… , weight N}
我们把所有搜索出的文档向量及查询向量放到一个N维空间中,每个词(term)是一维。
如图:


09.png

我们认为两个向量之间的夹角越小,相关性越大。
所以我们计算夹角的余弦值作为相关性的打分,夹角越小,余弦值越大,打分越高,相关性越大。
有人可能会问,查询语句一般是很短的,包含的词(Term)是很少的,因而查询向量的维数很小,而文档很长,包含词(Term)很多,文档向量维数很大。你的图中两者维数怎么都是N呢?
在这里,既然要放到相同的向量空间,自然维数是相同的,不同时,取二者的并集,如果不含某个词(Term)时,则权重(Term Weight)为0。
相关性打分公式如下:


10.png

举个例子,查询语句有11个Term,共有三篇文档搜索出来。其中各自的权重(Term weight),如下表格。

t1 t2 t3 t4 t5 t6 t7 t8 t9 t10 t11
D1 0 0 .477 0 .477 .176 0 0 0 .176 0
D2 0 .176 0 .477 0 0 0 0 .954 0 .176
D3 0 .176 0 0 0 .176 0 0 0 .176 .176
Q 0 0 0 0 0 .176 0 0 .477 0 .176

于是计算,三篇文档同查询语句的相关性打分分别为:


11-1.png

11-2.png

11-3.png

于是文档二相关性最高,先返回,其次是文档一,最后是文档三。
到此为止,我们可以找到我们最想要的文档了。

以上信息检索技术(Information retrieval)中的基本理论,Lucene是对这种基本理论的一种基本的的实践。对上述索引创建和搜索过程所一个总结,如图:
此图参照http://www.lucene.com.cn/about.htm 中文章《开放源代码的全文检索引擎Lucene》

12.png

1 索引过程:
a) 有一系列被索引文件
b) 被索引文件经过语法分析和语言处理形成一系列词(Term) 。
c) 经过索引创建形成词典和反向索引表。
d) 通过索引存储将索引写入硬盘。

2 搜索过程:
a) 用户输入查询语句。
b) 对查询语句经过语法分析和语言分析得到一系列词(Term) 。
c) 通过语法分析得到一个查询树。
d) 通过索引存储将索引读入到内存。
e) 利用查询树搜索索引,从而得到每个词(Term) 的文档链表,对文档链表进行交,差,并得到结果文档。
f) 将搜索到的结果文档对查询的相关性进行排序。
g) 返回查询结果给用户。

2.5 全文检索场景

搜索引擎
站内搜索
系统文件搜索

2.6 全文检索相关技术

2.6.1 Lucene

Lucene 是一个高效的,基于Java 的全文检索库。Lucene 能够为文本类型的数据建立索引,所以你只要能把你要索引的数据格式转化的文本的,Lucene 就能对你的文档进行索引和搜索。比如你要对一些 HTML 文档、PDF 文档进行索引的话,你就首先需要把 HTML 文档和 PDF 文档转化成文本格式的,然后将转化后的内容交给 Lucene 进行索引,然后把创建好的索引文件保存到磁盘或者内存中,最后根据用户输入的查询条件在索引文件上进行查询。不指定要索引的文档的格式也使 Lucene 能够几乎适用于所有的搜索应用程序。
如果使用该技术实现,需要对Lucene的API和底层原理非常了解,而且需要编写大量的Java代码。

2.6.2 Solr

solr是一个高性能的、采用Java5开发的、基于Lucene的全文搜索服务器。同时对其进行了扩展,提供了比Lucene更为丰富的查询语言。同时实现了可配置,可扩展并对查询性能进行了优化,并且提供了一个完善的功能管理界面,是一款非常优秀的全文搜索引擎。
solr的工作方式是文档通过Http利用XML加到一个搜索集合中,查询该集合也是通过http收到一个XML/JSON响应来实现。它的主要特性包括:高效、灵活的缓存功能,垂直搜索功能,高亮显示搜索结果,通过索引复制来提高可用性,提供一套强大Data Schema来定义字段,、类型和设置文本分析,提供基于Web的管理界面等。总结一句话就是用户可以通过http请求向搜索引擎服务器提交一定格式的xml文件生成索引,也可以通过http get操作提出查询的请求得到xml/json格式的返回结果

2.6.3 ElasticSearch

ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java开发的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。

2.6.4 Solr和ES的比较

检索速度的比较
当单纯的对已有数据进行搜索时,Solr更快。

13.png

当实时建立索引时, Solr会产生io阻塞,查询性能较差,Elasticsearch具有明显的优势。


14.png

随着数据量的增加,Solr的搜索效率会变得更低,而Elasticsearch却没有明显的变化。


15.png

大型互联网公司,实际生产环境测试,将搜索引擎从Solr转到Elasticsearch以后的平均查询速度有了50倍的提升。


16.png

17.png

最终的结论:Solr 是传统搜索应用的有力解决方案,但 Elasticsearch 更适用于新兴的实时搜索应用。

3 ElasticSearch介绍

3.1 ES基本概述

Elasticserach由来:许多年前,一个叫Shay Banon的待业工程师跟随他的新婚妻子来到伦敦,他的妻子想在伦敦学习做一名厨师。而他在伦敦寻找工作的期间,接触到了Lucene的早期版本,他想为自己的妻子开发一个方便搜索菜谱的应用。
Elasticsearch发布的第一个版本是在2010年的二月份,从那之后,Elasticsearch便成了Github上最受人瞩目的项目之一,并且很快就有超过300名开发者加入进来贡献了自己的代码。后来Shay和另一位合伙人成立了公司专注打造Elasticsearch,他们对Elasticsearch进行了一些商业化的包装和支持。但是,Elasticsearch承诺,永远都将是开源并且免费的。
Elastic为主体的公司提供了很多优秀的解决方案,拿到很多的投资,现已上市,后来收购 logstash、kibana及一些其他的服务。

  • ElasticSearch
  • Logstash
  • Kibana

3.2 ES是什么

Elasticsearch是一个开源的高扩展的分布式全文检索引擎。Elasticsearch也使用Java开发并使用Lucene作为其核心来实现所有索引和搜索的功能。但是它的目的是通过简单的RESTfulAPI来隐藏Lucene的复杂性,从而让全文搜索变得简单。Elasticsearch是面向文档型数据库,一条数据在这里就是一个文档,用JSON作为文档序列化的格式,比如下面这条用户数据:

{ 
"name" : "John", 
"sex" : "Male", 
"age" : 25, 
"birthDate": "1990/05/01", 
"about" : "I love to go rock climbing", 
"interests": [ "sports", "music" ] 
}

3.3 为什么要使用ES

1)2013年初,GitHub抛弃了Solr,采取ElasticSearch 来做PB级的搜索。
2)维基百科启动以elasticsearch为基础的核心搜索架构。
3)SoundCloud使用ElasticSearch为1.8亿用户提供即时而精准的音乐搜索服务。
4)百度目前广泛使用ElasticSearch作为文本数据分析,采集百度所有服务器上的各类指标数据及用户自定义数据,通过对各种数据进行多维分析展示,辅助定位分析实例异常或业务层面异常。目前覆盖百度内部20多个业务线(包括casio、云分析、网盟、预测、文库、直达号、钱包、风控等),单集群最大100台机器,200个ES节点,每天导入30TB+数据。

实际项目开发实战中,几乎每个系统都会有一个搜索的功能,当搜索做到一定程度时,维护和扩展起来难度就会慢慢变大,所以很多公司都会把搜索单独独立出一个模块,用ElasticSearch等来实现。

近年ElasticSearch发展迅猛,已经超越了其最初的纯搜索引擎的角色,现在已经增加了数据聚合分析可视化的特性,如果你有数百万的文档需要通过关键词进行定位时,ElasticSearch肯定是最佳选择。

当然,如果你的文档是JSON的,你也可以把ElasticSearch当作一种“NoSQL数据库”,应用ElasticSearch数据聚合分析(aggregation)的特性,针对数据进行多维度的分析。

3.4 ES有什么能力

Elasticsearch 是一个分布式可扩展的实时搜索和分析引擎,一个建立在全文搜索引擎 Apache Lucene(TM) 基础上的搜索引擎.
实际项目开发实战中,几乎每个系统都会有一个搜索的功能,当数据达到很大且搜索要做到一定程度时,维护和扩展难度就会越来越高,并且在全文检索的速度上、结果内容的推荐、分析以及统计聚合方面也很难达到我们预期效果。Elasticsearch ,它不仅包括了全文搜索功能,还可以进行以下工作:

  • 分布式实时文件存储,并将每一个字段都编入索引,使其可以被搜索。
  • 实时分析的分布式搜索引擎。
  • 可以扩展到上百台服务器,处理PB级别的结构化或非结构化数据。

3.5 使用场景

3.5.1 存储

ES天然支持分布式,具备存储海量数据的能力,其搜索和数据分析的功能都建立在ES存储的海量的数据之上;ES很方便的作为海量数据的存储工具,特别是在数据量急剧增长的当下,ES结合爬虫等数据收集工具可以发挥很大用处

3.5.2 搜索

ES使用倒排索引,每个字段都被索引且可用于搜索,更是提供了丰富的搜索api,在海量数据下近实时实现近秒级的响应,基于Lucene的开源搜索引擎,为搜索引擎(全文检索,高亮,搜索推荐等)提供了检索的能力。

具体场景:

  1. Stack Overflow(国外的程序异常讨论论坛),IT问题,程序的报错,提交上去,有人会跟你讨论和回答,全文检索,搜索相关问题和答案,程序报错了,就会将报错信息粘贴到里面去,搜索有没有对应的答案
  2. GitHub(开源代码管理),搜索上千亿行代码
  3. 电商网站,检索商品
  4. 日志数据分析,logstash采集日志,ES进行复杂的数据分析(ELK技术)

3.5.3 数据分析

ES也提供了大量数据分析的api和丰富的聚合能力,支持在海量数据的基础上进行数据的分析和处理。

具体场景:
爬虫爬取不同电商平台的某个商品的数据,通过ElasticSearch进行数据分析(各个平台的历史价格、购买力等等)

3.5.4 数据预警

结合第三方插件wathcer监控数据

4 ES的架构

18.png

4.1 网关Gateway层

Gateway是ES用来存储索引文件的一个文件系统且它支持很多类型。例如:本地磁盘、共享存储(做snapshot的 时候需要用到)、hadoop的hdfs分布式存储、亚马逊的S3。

职责:它的主要职责是用来对数据进行长持久化以及整个集群重启之后可以通过gateway重新恢复数据。
代表 es索引的持久化存储方式,es默认是先把索引存放到内存中,当内存满了时再持久化到硬盘。

数据安全:当这个es集群关闭再重新启动时就会从gateway中读取索引数据。es支持多种类型的gateway,有本地文件系统(默认)、分布式文件系统、Hadoop的HDFS和amazon的s3云存储服务。

存储数据: 存储索引信息、集群信息、mapping 等等

4.2 DLD

Gateway上层就是一个分布式的lucene框架。lucene是做检索的,但是它是一个单机的搜索引擎,像这种es分布式搜索引擎系统,虽然底层用lucene,但是需要在每个节点上都运行lucene进行相应的索引、查询以及更新,所以需要做成一个分布式的运行框架来满足业务的需要。

4.3 四大模块组件

DDL 之上就是一些ES的四大模块 :

  • Index Module是索引模块,就是对数据建立索引也就是通常所说的建立一些倒排索引等;
  • Search Module是搜索模块,就是对数据进行查询搜索;
  • Mapping Module是数据映射与解析模块,就是你的数据的每个字段可以根据你建立的表结构通过mapping进行映射解析。如果你没有建立表结构,es就会根据你的数据类型推测你的数据结构之后自己生成mapping,然后都是根据这个mapping进行解析你的数据;
  • River Module,在es2.0之后应该是被取消了,它的意思表示是第三方插件,例如可以通过一些自定义的脚本将传统的数据库(mysql)等数据源通过格式化转换后直接同步到es集群里。这个River大部分是自己写的,写出来的东西质量参差不齐,将这些东西集成到es中会引发很多内部bug,严重影响了es的正常应用,所以在es2.0之后考虑将其去掉。

4.4 Discovery、Scripting 和第三方插件

Discovery是ES的节点发现模块,不同机器上的ES节点要组成集群需要进行消息通信,集群内部需要选举master节点,这些工作都是由Discovery模块完成。支持多种发现机制,如 Zen 、EC2、gce、Azure。Scripting用来支持在查询语句中插入javascript、python等脚本语言,scripting模块负责解析这些脚本,使用脚本语句性能稍低。ES也支持多种第三方插件。

4.5 Transport、JMX

再上层是ES的传输模块和JMX传输模块支持多种传输协议,如 Thrift、memecached、http,默认使用http。JMX是java的管理框架,用来管理ES应用。

4.6 RESTful接口层

最上层就是ES暴露给我们的访问接口。官方推荐的方案就是这种Restful接口,直接发送http请求,方便后续使用nginx做代理、分发包括可能后续会做权限的管理,通过http很容易做这方面的管理。 如果使用java客户端,它是直接调用api,在做负载均衡以及权限管理还是不太好做。

5 ES集群架构

5.1 核心概念

5.1.1 集群(Cluster)

ES集群是一个或多个节点的集合,它们共同存储了整个数据集,并提供了联合索引以及可跨所有节点的搜索能力。多节点组成的集群拥有冗余能力,它可以在一个或几个节点出现故障时保证服务的整体可用性。集群靠其独有的名称进行标识,默认名称为“elasticsearch”。节点靠其集群名称来决定加入哪个ES集群,一个节点只能属一个集群。
集群中有多个节点(node),其中有一个为主节点,这个主节点是可以通过选举产生的, 主从节点是对于集群内部来说的。
ES的一个概念就是去中心化,字面上理解就是无中心节点,这是对于集群外部来说的,因为从外部来看ES 集群,在逻辑上是个整体,你与任何一个节点的通信和与整个ES集群通信是等价的。

5.1.2 节点(node)

一个节点是一个逻辑上独立的服务,可以存储数据并参与集群的索引和搜索功能,一个节点也有唯一的名字,群集通过节点名称进行管理和通信.

5.1.2.1 主节点

主节点的主要职责是和集群操作相关的内容,如创建或删除索引、跟踪哪些节点是群集的一部分、并决定哪些分片分配给相关的节点。稳定的主节点对集群的健康是非常重要的。
虽然主节点也可以协调节点,路由搜索和从客户端新增数据到数据节点,但最好不要使用这些专用的主节点。一个重要的原则是,尽可能做尽量少的工作。
对于大型的生产集群来说,推荐使用一个专门的主节点来控制集群,该节点将不处理任何用户请求。
主节点配置如下(elasticsearch.yml):

node.master: true 
node.data: false

5.1.2.2 数据节点

持有数据和倒排索引。负责存储数据,提供建立索引和搜索索引的服务。 data节点消耗内存和磁盘IO的性能比较大。
数据节点配置如下(elasticsearch.yml):

node.master: false 
node.data: true

5.1.2.3 协调节点(客户端节点)

它既不能保持数据也不能成为主节点,该节点可以响应用户的情况,把相关操作发送到其他节点。客户端节点会将客户端请求路由到集群中合适的分片上。对于读请求来说,协调节点每次会选择不同的分片处理请求,以实现负载均衡。
数据节点配置如下(elasticsearch.yml):

node.master: false 
node.data: false

注意:master设置为true表示此节点可以成为master,但并不一定是master。data设置为true表示可以存储数据,设置为false表示不可以存储数据。ES节点master和data属性均默认为true,默认情况下是三功能节点合一的,既可以成为master候选节点,又可以存储数据,还可以成为协调节点。

5.1.2.4 部落节点

部落节点可以跨越多个集群,它可以接收每个集群的状态,然后合并成一个全局集群的状态,它可以读写所有节点上的数据。

5.1.3 索引(Index)

索引是具有类似特性的文档的集合,ES将数据存储于一个或多个索引中。类比传统的关系型数据库领域来说,索引相当于SQL中的一个数据库,或者一个数据存储方案(schema)。索引由其名称(必须为全小写字符)进行标识,并通过引用此名称完成文档的创建、搜索、更新及删除操作。一个ES集群中可以按需创建任意数目的索引。

5.1.4 文档类型(Type)

类型是索引内部的逻辑分区(category/partition),然而其意义完全取决于用户需求。因此,一个索引内部可定义一个或多个类型(type)。一般来说,类型就是为那些拥有相同的域的文档做的预定义。例如,在索引中,可以定义一个用于存储用户数据的类型,一个存储日志数据的类型,以及一个存储评论数据的类型。类比传统的关系型数据库领域来说,类型相当于“表”。

5.1.5 文档(Document)

文档是Lucene索引和搜索的原子单位,它是包含了一个或多个域的容器,基于JSON格式进行表示。文档由一个或多个域组成,每个域拥有一个名字及一个或多个值,有多个值的域通常称为“多值域”。每个文档可以存储不同的域集,但同一类型下的文档至应该有某种程度上的相似之处。相当于数据库的“记录”。

5.1.6 映射(Mapping)

相当于数据库中的schema,用来约束字段的类型,不过 Elasticsearch 的 mapping 可以自动根据数据创建。
ES中,所有的文档在存储之前都要首先进行分析。用户可根据需要定义如何将文本分割成token、哪些token应该被过滤掉以及哪些文本需要进行额外处理等等。

5.1.7 分片(shard)

ES的分片(shard)机制可将一个索引内部的数据分布地存储于多个节点,它通过将一个索引切分为多个底层物理的Lucene索引完成索引数据的分割存储功能,这每一个物理的Lucene索引称为一个分片(shard)。
每个分片其内部都是一个全功能且独立的索引,因此可由集群中的任意主机存储。创建索引时,用户可指定其分片的数量,默认数量为5个。
Shard有两种类型:primary和replica,即主shard及副本shard。

5.1.7.1 Primary shard

用于文档存储,每个新的索引会自动创建5个Primary shard,当然此数量可在索引创建之前通过配置自行定义,不过,一旦创建完成,其Primary shard的数量将不可更改

5.1.7.2 Replica shard

是Primary Shard的副本,用于冗余数据及提高搜索性能。每个Primary shard默认配置了一个Replica shard,但也可以配置多个,且其数量可动态更改。ES会根据需要自动增加或减少这些Replica shard的数量。

ES集群可由多个节点组成,各Shard分布式地存储于这些节点上。ES可自动在节点间按需要移动shard,例如增加节点或节点故障时。简而言之,分片实现了集群的分布式存储,而副本实现了其分布式处理及冗余功能。

5.2 核心原理

  • ElasticSearch 的主旨是随时可用和按需扩容。 扩容可以通过购买性能更强大(垂直扩容或纵向扩容) 或者数量更多的服务器(水平扩容或横向扩容 )来实现。
  • 虽然 Elasticsearch 可以获益于更强大的硬件设备,但是垂直扩容是有极限的。真正的扩容能力是来自于水平扩容,即为集群添加更多的节点,并且将负载压力和稳定性分散到这些节点中。
  • 对于大多数的数据库而言,通常需要对应用程序进行非常大的改动,才能利用上横向扩容的新增资源。 与之相反的是,ElastiSearch天生就是分布式的 ,它知道如何通过管理多节点来提高扩容性和可用性。 这也意味着你的应用无需关注这个问题。
  • 一个运行中的 Elasticsearch 实例称为一个 节点,而集群是由一个或者多个拥有相同 cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。
  • 当一个节点被选举成为主节点时, 它将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等,而主节点并不需要涉及到文档级别的变更和搜索等操作。所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。 任何节点都可以成为主节点。我们的示例集群就只有一个节点,所以它同时也成为了主节点。
  • 作为用户,我们可以将请求发送到集群中的任何节点 ,包括主节点。 每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。 无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端。 Elasticsearch 对这一切的管理都是透明的。

5.2.1 集群健康:

Elasticsearch 的集群监控信息中包含了许多的统计数据,其中最为重要的一项就是集群健康 , 它在 status 字段中展示为 green 、 yellow 或者 red 。

GET /_cluster/health

在一个不包含任何索引的空集群中,它将会有一个类似于如下所示的返回内容:

{
   "cluster_name":          "elasticsearch",
   "status":                "green", 
   "timed_out":             false,
   "number_of_nodes":       1,
   "number_of_data_nodes":  1,
   "active_primary_shards": 0,
   "active_shards":         0,
   "relocating_shards":     0,
   "initializing_shards":   0,
   "unassigned_shards":     0
}
  • green:所有的主分片和副本分片都正常运行。
  • yellow:所有的主分片都正常运行,但不是所有的副本分片都正常运行。
  • red:有主分片没能正常运行。

5.2.2 添加索引

我们往Elasticsearch添加数据时需要用到索引 —— 保存相关数据的地方。索引实际上是指向一个或者多个物理 分片的逻辑命名空间 。
一个分片是一个底层的工作单元 ,它仅保存了全部数据中的一部分。在分片内部机制中,我们将详细介绍分片是如何工作的,而现在我们只需知道一个分片是一个 Lucene 的实例,以及它本身就是一个完整的搜索引擎。 我们的文档被存储和索引到分片内,但是应用程序是直接与索引而不是与分片进行交互
Elasticsearch 是利用分片将数据分发到集群内各处的。分片是数据的容器,文档保存在分片内,分片又被分配到集群内的各个节点里。 当你的集群规模扩大或者缩小时, Elasticsearch 会自动的在各节点中迁移分片,使得数据仍然均匀分布在集群里
一个分片可以是主分片或者副本分片。索引内任意一个文档都归属于一个主分片,所以主分片的数目决定着索引能够保存的最大数据量。技术上来说,一个主分片最大能够存储 Integer.MAX_VALUE - 128 个文档,但是实际最大值还需要参考你的使用场景,包括你使用的硬件、文档的大小和复杂程度、索引和查询文档的方式以及你期望的响应时长。一个副本分片只是一个主分片的拷贝。 副本分片作为硬件故障时保护数据不丢失的冗余备份,并为搜索和返回文档等读操作提供服务。在索引建立的时候就已经确定了主分片数,但是副本分片数可以随时修改
让我们在包含一个空节点的集群内创建名为 blogs 的索引。 索引在默认情况下会被分配5个主分片,但是为了演示目的,我们将分配3个主分片和一份副本(每个主分片拥有一个副本分片):

PUT /blogs
{
   "settings" : {
      "number_of_shards" : 3,
      "number_of_replicas" : 1
   }
}

下图是拥有一个索引的单节点集群:


19.png

如果我们现在查看集群健康, 我们将看到如下内容:

{
  "cluster_name": "elasticsearch",
  "status": "yellow", 
  "timed_out": false,
  "number_of_nodes": 1,
  "number_of_data_nodes": 1,
  "active_primary_shards": 3,
  "active_shards": 3,
  "relocating_shards": 0,
  "initializing_shards": 0,
  "unassigned_shards": 3, 
  "delayed_unassigned_shards": 0,
  "number_of_pending_tasks": 0,
  "number_of_in_flight_fetch": 0,
  "task_max_waiting_in_queue_millis": 0,
  "active_shards_percent_as_number": 50
}

集群的健康状况为 yellow 则表示全部主分片都正常运行(集群可以正常服务所有请求),但是副本分片没有全部处在正常状态。 实际上,所有3个副本分片都是 unassigned —— 它们都没有被分配到任何节点。 在同一个节点上既保存原始数据又保存副本是没有意义的,因为一旦失去了那个节点,我们也将丢失该节点上的所有副本数据。
当前我们的集群是正常运行的,但是在硬件故障时有丢失数据的风险。

5.2.3 添加故障转移

当集群中只有一个节点在运行时,意味着会有一个单点故障问题——没有冗余。 幸运的是,我们只需再启动一个节点即可防止数据丢失。
为了测试第二个节点启动后的情况,你可以在同一个目录内,完全依照启动第一个节点的方式来启动一个新节点(参考安装并运行 Elasticsearch)。多个节点可以共享同一个目录。当你在同一台机器上启动了第二个节点时,只要它和第一个节点有同样的 cluster.name 配置,它就会自动发现集群并加入到其中。但是在不同机器上启动节点的时候,为了加入到同一集群,你需要配置一个可连接到的单播主机列表。 详细信息请查看最好使用单播代替组播

20.png

当第二个节点加入到集群后,3个副本分片将会分配到这个节点上——每个主分片对应一个副本分片。这意味着当集群内任何一个节点出现问题时,我们的数据都完好无损。(TODO)
所有新近被索引的文档都将会保存在主分片上,然后被并行的复制到对应的副本分片上。这就保证了我们既可以从主分片又可以从副本分片上获得文档。
cluster-health 现在展示的状态为 green ,这表示所有6个分片(包括3个主分片和3个副本分片)都在正常运行。

{
  "cluster_name": "elasticsearch",
  "status": "green", 
  "timed_out": false,
  "number_of_nodes": 2,
  "number_of_data_nodes": 2,
  "active_primary_shards": 3,
  "active_shards": 6,
  "relocating_shards": 0,
  "initializing_shards": 0,
  "unassigned_shards": 0,
  "delayed_unassigned_shards": 0,
  "number_of_pending_tasks": 0,
  "number_of_in_flight_fetch": 0,
  "task_max_waiting_in_queue_millis": 0,
  "active_shards_percent_as_number": 100
}

5.2.4 水平扩容

怎样为我们的正在增长中的应用程序按需扩容呢?当启动了第三个节点,我们的集群如下图所示。

21.png

Node 1 和 Node 2 上各有一个分片被迁移到了新的 Node 3 节点,现在每个节点上都拥有2个分片,而不是之前的3个。 这表示每个节点的硬件资源(CPU, RAM, I/O)将被更少的分片所共享,每个分片的性能将会得到提升。
分片是一个功能完整的搜索引擎,它拥有使用一个节点上的所有资源的能力。 我们这个拥有6个分片(3个主分片和3个副本分片)的索引可以最大扩容到6个节点,每个节点上存在一个分片,并且每个分片拥有所在节点的全部资源。如果我们想扩容超过6个节点怎么办?主分片的数目在索引创建时就已经确定了下来。实际上,这个数目定义了这个索引能够存储的最大数据量。 但是,读操作(搜索和返回数据)可以同时被主分片或副本分片所处理,所以当你拥有越多的副本分片时,也将拥有越高的吞吐量。
在运行中的集群上是可以动态调整副本分片数目的 ,我们可以按需伸缩集群。让我们把副本数从默认的 1 增加到 2 :

PUT /blogs/_settings
{
   "number_of_replicas" : 2
}

blogs 索引现在拥有9个分片:3个主分片和6个副本分片。这意味着我们可以将集群扩容到9个节点,每个节点上一个分片。相比原来3个节点时,集群搜索性能可以提升3倍。


22.png

当然,如果只是在相同节点数目的集群上增加更多的副本分片并不能提高性能,因为每个分片从节点上获得的资源会变少。 你需要增加更多的硬件资源来提升吞吐量。但是更多的副本分片数提高了数据冗余量:按照上面的节点配置,我们可以在失去2个节点的情况下不丢失任何数据。

5.2.5 应对故障

Elasticsearch 可以应对节点故障,接下来让我们尝试下这个功能。 如果我们关闭第一个节点,这时集群的状态为:


23.png

我们关闭的节点是一个主节点。而集群必须拥有一个主节点来保证正常工作,所以发生的第一件事情就是选举一个新的主节点 Node 2 。
在我们关闭 Node 1 的同时也失去了主分片 1 和 2 ,并且在缺失主分片的时候索引也不能正常工作。 如果此时来检查集群的状况,我们看到的状态将会为 red :不是所有主分片都在正常工作。
幸运的是,在其它节点上存在着这两个主分片的完整副本, 所以新的主节点立即将这些分片在 Node 2 和 Node 3 上对应的副本分片提升为主分片, 此时集群的状态将会为 yellow 。
这个提升主分片的过程是瞬间发生的,如同按下一个开关一般。
为什么我们集群状态是 yellow 而不是 green 呢?虽然我们拥有所有的三个主分片,但是同时设置了每个主分片需要对应2份副本分片,而此时只存在一份副本分片。 所以集群不能为 green 的状态。
不过我们不必过于担心,如果我们同样关闭了 Node 2 ,我们的程序依然可以保持在不丢任何数据的情况下运行,因为 Node 3 为每一个分片都保留着一份副本。
如果我们重新启动 Node 1 ,集群可以将缺失的副本分片再次进行分配,那么集群的状态也将如图 “将参数 number_of_replicas 调大到 2”所示。 如果 Node 1 依然拥有着之前的分片,它将尝试去重用它们。同时仅从主分片复制发生了修改的数据文件。

5.2.6 文档存储原理

创建索引的时候我们只需要指定分片数和副本数,ES 就会自动将文档数据分发到对应的分片和副本中。那么文件究竟是如何分布到集群的,又是如何从集群中获取的呢? Elasticsearch 虽然隐藏这些底层细节,让我们好专注在业务开发中,但是我们深入探索这些核心的技术细节,这能帮助你更好地理解数据如何被存储到这个分布式系统中。

5.2.6.1 文档是如何路由到分片中的

当索引一个文档的时候,文档会被存储到一个主分片中。 Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?当我们创建文档时,它如何决定这个文档应当被存储在分片 1 还是分片 2 中呢?
首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。实际上,这个过程是根据下面这个公式决定的:

shard = hash(routing) % number_of_primary_shards

routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。

PUT /index/item/id?routing = _id (默认) 
PUT /index/item/id?routing = user_id(自定义路由)---- 指定把某些值固定路由到某个分片上面。 

routing 通过 hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到 余数 。这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。
这就解释了为什么我们要在创建索引的时候就确定好主分片的数量并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了
你可能觉得由于 Elasticsearch 主分片数量是固定的会使索引难以进行扩容,所以在创建索引的时候合理的预分配分片数是很重要的。
所有的文档 API( get 、 index 、 delete 、 bulk 、 update 以及 mget )都接受一个叫做 routing 的路由参数 ,通过这个参数我们可以自定义文档到分片的映射。一个自定义的路由参数可以用来确保所有相关的文档——例如所有属于同一个用户的文档——都被存储到同一个分片中。

5.2.6.2 主分片和副本分片如何交互

上面介绍了一个文档是如何路由到一个分片中的,那么主分片是如何和副本分片交互的呢?
假设有个集群由三个节点组成, 它包含一个叫 user 的索引,有两个主分片,每个主分片有两个副本分片。相同分片的副本不会放在同一节点,所以我们的集群看起来如下图所示:

24.png

我们可以发送请求到集群中的任一节点。每个节点都有能力处理任意请求。每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。 在下面的例子中,将所有的请求发送到 Node 1 ,我们将其称为协调节点(coordinating node)。
当发送请求的时候,为了扩展负载,更好的做法是轮询集群中所有的节点。
对文档的新建、索引和删除请求都是写操作,必须在主分片上面完成之后才能被复制到相关的副本分片。


25.png

以下是在主副分片和任何副本分片上面成功新建索引和删除文档所需要的步骤顺序:
①客户端向 Node 1 发送新建、索引或者删除请求。
②节点使用文档的 _id 确定文档属于分片 0 。请求会被转发到 Node 3,因为分片 0 的主分片目前被分配在 Node 3 上。
③Node 3 在主分片上面执行请求。如果成功了,它将请求并行转发到 Node1 和 Node2 的副本分片上。一旦所有的副本分片都报告成功,Node 3 将向协调节点报告成功,协调节点向客户端报告成功。
在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。
在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。
在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。

5.2.7 索引存储原理

创建索引的时候,我们通过Mapping 映射定义好索引的基本结构信息,接下来我们肯定需要往 ES 里面新增业务文档数据了,例如用户、日志等业务数据。新增的业务数据,我们根据 Mapping 来生成对应的倒排索引信息 。
、我们一直说,Elasticsearch是一个基于Apache Lucene 的开源搜索引擎。Elasticsearch的搜索高效的原因并不是像Redis那样重依赖内存的,而是通过建立特殊的索引数据结构--倒排索引--实现的。由于它的使用场景(处理PB级结构化或非结构化数据)数据量大且需要持久化防止断电丢失,所以 Elasticsearch 的数据和索引存储是依赖于服务器的硬盘。倒排索引可以说是Elasticsearch搜索高效和支持非结构化数据检索的主要原因了,但是倒排索引被写入磁盘后是不可改变的,它永远不会修改

5.2.7.1 段和提交点

倒排索引的不可变性,这点主要是因为 Elasticsearch 的底层是基于 Lucene,而在 Lucene 中提出了按段搜索的概念,即,将一个索引文件拆分为多个子文件,则每个子文件叫作段,每个段都是一个独立的可被搜索的数据集,并且段具有不变性,一旦索引的数据被写入硬盘,就不可再修改

段的概念提出主要是因为:在早期全文检索中为整个文档集合建立了一个很大的倒排索引并将其写入磁盘中。如果索引有更新,就需要重新全量创建一个索引来替换原来的索引,这种方式在数据量很大时效率很低,并且由于创建一次索引的成本很高,所以对数据的更新不能过于频繁,也就不能保证时效性。在底层采用分段的存储模式,不仅解决上述问题,而且使它在读写时几乎完全避免了锁的出现,大大提升了读写性能。其实原理和ConcurrentHashMap 的分段锁点类似。

Elasticsearch 中的倒排索引被设计成不可变的,有以下几个方面优势:
①不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
②一旦索引被读入内核的文件系统缓存,便会留在那里。由于其不变性,只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
③其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。
④写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O和需要被缓存到内存的索引的使用量。

每一个段本身都是一个倒排索引,但索引在 Lucene 中除表示所有段的集合外,还增加了提交点的概念。

为了提升写的性能,Lucene并没有每新增一条数据就增加一个段,而是采用延迟写的策略,每当有新增的数据时,就将其先写入内存中,然后批量写入磁盘中。若有一个段被写到硬盘,就会生成一个提交点,提交点就是一个列出了所有已知段和记录所有提交后的段信息的文件

26.png

5.2.7.2 写索引的流程

索引库存储流程.png

当分片所在的节点接收到来自协调节点的请求后,会将该请求写入translog,并将文档加入内存缓存。如果请求在主分片上成功处理,该请求会并行发送到该分片的副本上。当translog被同步到全部的主分片及其副本上后,客户端才会收到确认通知。

内存缓冲会被周期性刷新(默认是1秒),内容将被写到文件系统缓存中的一个新段(segment)上。虽然这个段并没有被同步(fsync),但它是开放的,内容可以被搜索到。

每30分钟或者当translog很大的时候,translog会被清空,文件系统缓存会被同步,这个过程在ES中称为冲洗(flush)。在冲洗过程中,内存中的缓冲将被清除,内容被写入一个新段。段的同步(fsync)将创建一个新的提交点,并将内容刷新到磁盘。旧的translog将被删除并开始一个新的translog。


27.png

从整个流程我们可以了解到以下几个问题:

  • 为什么说 ES 搜索是近实时的?
    因为文档索引在从内存缓存被写入到文件缓存系统时,虽然还没有进行提交、未被 flush 到磁盘,但是缓冲区的内容已经被写入一个新段(segment6)中且新段可被搜索。这就是为什么我们说 Elasticsearch 是近实时搜索: 文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。
  • Elasticsearch 是怎样保证更新被持久化在断电时也不丢失数据?
    新索引文档被写入到内存缓存时,同时会记录一份到事务日志(translog)中,translog 提供所有还没有被刷到磁盘的操作的一个持久化纪录。当 Elasticsearch 启动的时候, 它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放 translog 中所有在最后一次提交后发生的变更操作。
    translog 也被用来提供实时 CRUD 。当你试着通过ID查询、更新、删除一个文档,它会在尝试从相应的段中检索之前先检查 translog 任何最近的变更。这意味着它总是能够实时地获取到文档的最新版本。
段合并
28.png

由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。 每一个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段,所以段越多搜索也就越慢。

Elasticsearch通过在后台进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。段合并的时候会将那些旧的已删除文档从文件系统中清除,被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中。


29.png

5.2.7.3 如何更新索引

上文阐述了索引的持久化流程和倒排索引被设定为不可修改以及这样设定的好处。因为它是不可变的,你不能修改它,但是如果你需要让一个新的文档可被搜索,这就涉及到索引的更新了。索引不可被修改但又需要更新,这种看似矛盾的要求,我们需要怎么做呢?

ES 的解决方法就是用更多的索引。什么意思?就是原来的索引不变,我们对新的文档再创建一个索引。这样说完不知道大家有没有疑惑或者没理解,我们通过图表的方式说明下。
假如我们现有两个日志信息的文档,信息如下:

Doc 1:the request param is name = 'zhang san' and age is 20.
Doc 2:the response result is code = 0000 and msg = 'success'.

这时候我们得到的倒排索引内容(省略一部分)是:

词项(term) 文档(Doc)
the doc 1,doc 2
request doc 1
param doc 1,doc 2
is doc 1,doc 2
name doc 1
response doc 2
result doc 2
... ...

如果我们这时新增一个文档 doc 3:the request param is name = 'li si' and sex is femal,或者修改文档 doc 2的内容为:the response result is code = 9999 and msg = 'false',这时 ES 是如何处理的呢?正如上文所述的,为了保留索引不变性,ES 会创建一个新的索引
对于新增的文档索引信息如下:

词项(term) 文档(Doc)
the doc 3
request doc 3
param doc 3
is doc 3
name doc 3
sex doc 3
... ...

对于修改的文档索引信息如下;

词项(term) 文档(Doc)
the doc 2
response doc 2
result doc 2
is doc 2
code doc 2
sex doc 2
... ...

通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到(从最早的开始),查询完后再对结果进行合并。

正如上文所述那样,对于修改的场景来说,同一个文档这时磁盘中同时会有两个索引数据一个是原来的索引,另一个是修改之后的索引。

以正常逻辑来看,我们知道搜索的时候肯定以新的索引为标准,但是段是不可改变的,所以既不能从把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。 取而代之的是,每个提交点会包含一个 .del文件,文件中会列出这些被删除文档的段信息当一个文档被 “删除” 时,它实际上只是在.del 文件中被记删除。一个被标记删除的文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。

文档更新也是类似的操作方式。当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。

5.3 集群选举

5.3.1 涉及配置参数

如果同时启动,按照nodeid进行排序,取出最小的做为master节点。
如果不是同时启动,则先启动的候选master节点,会竞选为master节点。

# 如果 node.master 设置为了false,则该节点没资格参与 master 选举。 
node.master = true 
# 默认3秒,最好增加这个参数值,避免网络慢或者拥塞,确保集群启动稳定性 
discovery.zen.ping_timeout: 3s 
# 用于控制选举行为发生的集群最小master节点数量,防止脑裂现象 
discovery.zen.minimum_master_nodes : 2 
# 新节点加入集群的等待时间 
discovery.zen.join_timeout : 10s

5.3.2 新节点加入

节点完成选举后,新节点加入,会发送 join request 到 master 节点。默认会重试20次。

5.3.3 宕机再次选举

如果宕机,集群node会再次进行ping 过程,并选出一个新的master 。一旦一个节点被明确设为一个客户端节点( node.client设为true ),则不能再成为主节点( node.master会自动设为false )。

5.4 发现机制

那么有一个问题,ES内部是如何通过一个相同的设置cluster.name 就能将不同的节点连接到同一个集群的?答案是Zen Discovery。

Zen Discovery是Elasticsearch的内置默认发现模块(发现模块的职责是发现集群中的节点以及选举master节点)。它提供单播和基于文件的发现,并且可以扩展为通过插件支持云环境和其他形式的发现。Zen Discovery 与其他模块集成,例如,节点之间的所有通信都使用Transport模块完成。节点使用发现机制通过Ping的方式查找其他节点。

Elasticsearch 默认被配置为使用单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。

如果集群的节点运行在不同的机器上,使用单播,你可以为 Elasticsearch 提供一些它应该去尝试连接的节点列表。 当一个节点联系到单播列表中的成员时,它就会得到整个集群所有节点的状态,然后它会联系 master 节点,并加入集群。

这意味着单播列表不需要包含集群中的所有节点, 它只是需要足够的节点,当一个新节点联系上其中一个并且说上话就可以了。

如果你使用 master 候选节点作为单播列表,你只要列出三个就可以了。 这个配置在 elasticsearch.yml 文件中:

discovery.zen.ping.unicast.hosts: ["host1", "host2:port"]

节点启动后先 ping ,如果discovery.zen.ping.unicast.hosts 有设置,则 ping 设置中的 host ,否则尝试 ping localhost 的几个端口, Elasticsearch 支持同一个主机启动多个节点, Ping 的 response 会包含该节点的基本信息以及该节点认为的 master 节点。 选举开始,先从各节点认为的 master 中选,规则很简单,按照 id 的字典序排序,取第一个。 如果各节点都没有认为的 master ,则从所有节点中选择,规则同上。

这里有个限制条件就是 discovery.zen.minimum_master_nodes ,如果节点数达不到最小值的限制,则循环上述过程,直到节点数足够可以开始选举。 最后选举结果是肯定能选举出一个 master ,如果只有一个 local 节点那就选出的是自己。 如果当前节点是 master ,则开始等待节点数达到 discovery.zen.minimum_master_nodes,然后提供服务。 如果当前节点不是 master ,则尝试加入 master 。

Elasticsearch 将以上服务发现以及选主的流程叫做 ZenDiscovery 。

由于它支持任意数目的集群( 1- N ),所以不能像 Zookeeper 那样限制节点必须是奇数,也就无法用投票的机制来选主,而是通过一个规则,只要所有的节点都遵循同样的规则,得到的信息都是对等的,选出来的主节点肯定是一致的。

但分布式系统的问题就出在信息不对等的情况,这时候很容易出现脑裂( Split-Brain )的问题,大多数解决方案就是设置一个 quorum 值,要求可用节点必须大于 quorum (一般是超过半数节点),才能对外提供服务。 Elasticsearch 中,这个 quorum 的配置就是 discovery.zen.minimum_master_nodes 。

5.5 故障探查

ES有两种集群故障探查机制:

  • 通过master进行的,master会ping集群中所有的其他node,确保它们是否是存活着的。
  • 每个node都会去ping master来确保master是存活的,否则会发起一个选举过程。

有下面三个参数用来配置集群故障的探查过程:

ping_interval : 每隔多长时间会ping一次node,默认是1s 
ping_timeout : 每次ping的timeout等待时长是多长时间,默认是30s 
ping_retries : 如果一个node被ping多少次都失败了,就会认为node故障,默认是3次

5.6 脑裂问题

5.6.1 脑裂现象

由于部分节点网络断开,集群分成两部分,且这两部分都有master选举权,就成形成一个与原集群一样名字的集群,这种情况称为集群脑裂(split-brain)现象。这个问题非常危险,因为两个新形成的集群会同时索引和修改集群的数据。

5.6.2 解决方案

# 决定选举一个master最少需要多少master候选节点。默认是1。 
# 这个参数必须大于等于为集群中master候选节点的quorum数量,也就是大多数。 
# quorum算法:master候选节点数量 / 2 + 1 
# 例如一个有3个节点的集群,minimum_master_nodes 应该被设置成 3/2 + 1 = 2
discovery.zen.minimum_master_nodes:2 
# 等待ping响应的超时时间,默认值是3秒。如果网络缓慢或拥塞,会造成集群重新选举,建议略微调大这个值。 
# 这个参数不仅仅适应更高的网络延迟,也适用于在一个由于超负荷而响应缓慢的节点的情况。 
discovery.zen.ping.timeout:10s 
# 当集群中没有活动的Master节点后,该设置指定了哪些操作(read、write)需要被拒绝(即阻塞执行)。
# 有两个设置值:all和write,默认为wirte。 
discovery.zen.no_master_block : write

5.6.3 场景分析

一个生产环境的es集群,至少要有3个节点,同时将discovery.zen.minimum_master_nodes设置为2,那么这个是参数是如何避免脑裂问题的产生的呢?
比如我们有3个节点,quorum是2。现在网络故障,1个节点在一个网络区域,另外2个节点在另外一个网络区域,不同的网络区域内无法通信。这个时候有两种情况情况:
(1)如果master是单独的那个节点,另外2个节点是master候选节点,那么此时那个单独的master节点因为没有指定数量的候选master node在自己当前所在的集群内,因此就会取消当前master的角色,尝试重新选举,但是无法选举成功。然后另外一个网络区域内的node因为无法连接到master,就会发起重新选举,因为有两个master候选节点,满足了quorum,因此可以成功选举出一个master。此时集群中就会还是只有一个master。
(2)如果master和另外一个node在一个网络区域内,然后一个node单独在一个网络区域内。那么此时那个单独的node因为连接不上master,会尝试发起选举,但是因为master候选节点数量不到quorum,因此无法选举出master。而另外一个网络区域内,原先的那个master还会继续工作。这也可以保证集群内只有一个master节点。
综上所述,通过在elasticsearch.yml 中配置discovery.zen.minimum_master_nodes: 2 ,就可以避免脑裂问题的产生。
但是因为ES集群是可以动态增加和下线节点的,所以可能随时会改变 quorum 。所以这个参数也是可以通过api随时修改的,特别是在节点上线和下线的时候,都需要作出对应的修改。而且一旦修改过后,这个配置就会持久化保存下来。

PUT /_cluster/settings 
{ "persistent" : { "discovery.zen.minimum_master_nodes" : 2 } }

作者:MiniSoulBigBang

原文链接:https://www.jianshu.com/p/0d04240ea850

文章分类
后端
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐