科普文:深入理解ElasticSearch体系结构

概叙

Elasticsearch是什么?

Elasticsearch(简称ES)是一个分布式、可扩展、实时的搜索与数据分析引擎。ES不仅仅只是全文搜索,还支持结构化搜索、数据分析、复杂的语言处理、地理位置和对象间关联关系等。

官网地址:Elastic --- The Search AI Company | Elastic

ES具有如下特点:

  1. 一个分布式的实时文档存储引擎,每个字段都可以被索引与搜索
  2. 一个分布式实时分析搜索引擎,支持各种查询和聚合操作
  3. 能胜任上百个服务节点的扩展,并可以支持PB级别的结构化或者非结构化数据

ElasticSearch (弹性搜索):是一款开源的分布式、RESTful 风格的搜索和数据分析引擎,它底层基于 Apache Lucene 开源库进行封装,其不仅仅提供分布式多用户能力的全文搜索引擎。

ElasticSearch 搜索整体架构如下图所示:

ElasticSearch特点

优点:

1、天生分片,天生集群,从ES出生开始就天然的支持分布式的特征,且无需第三方组件,自带。

2、天生索引,ES 所有数据都是默认进行索引的,这点和mysql正好相反,mysql是默认不加索引,要加索引必须特别说明,ES只有不加索引才需要说明。

3、支持PB级海量数据实时全文搜索。

4、支持多语言访问,支持TCP和RESTful API两种方式访问。

缺点

1、不适合做复杂聚合,会影响ES集群性能。

2、不支持高并发写入数据。

3、ES耗CPU和内存资源,需要用高配置的机器来搭建集群,使用成本比较高。

elasticsearch的作用

elasticsearch(es)是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容。例如在以下的应用场景中,都会使用到es

  1. 在Github上搜索代码
  2. 在电商网站搜索商品
  3. 在百度搜索答案
  4. 在打车软件搜索附近的车(具体这个为什么后续系列会详细说明的)

ES 的版本问题

在决定使用 Elasticsearch 的时候首先要考虑的是版本问题,Elasticsearch (排除 0.x 和 1.x)目前有如下常用的稳定的主版本:2.x,5.x,6.x,7.x(current)。

你可能会发现没有 3.x 和 4.x,ES 从 2.4.6 直接跳到了 5.0.0。其实是为了 ELK(ElasticSearch,Logstash,Kibana)技术栈的版本统一,免的给用户带来混乱。

在 Elasticsearch 是 2.x (2.x 的最后一版 2.4.6 的发布时间是 July 25, 2017) 的情况下,Kibana 已经是 4.x(Kibana 4.6.5 的发布时间是 July 25, 2017)。

那么在 Kibana 的下一主版本肯定是 5.x 了,所以 Elasticsearch 直接将自己的主版本发布为 5.0.0 了。

统一之后,我们选版本就不会犹豫困惑了,我们选定 Elasticsearch 的版本后再选择相同版本的 Kibana 就行了,不用担忧版本不兼容的问题。

Elasticsearch 是使用 Java 构建,所以除了注意 ELK 技术的版本统一,我们在选择 Elasticsearch 的版本的时候还需要注意 JDK 的版本。

因为每个大版本所依赖的 JDK 版本也不同,目前 7.2 版本已经可以支持 JDK11。

Lucene

ES的底层依赖Lucene,Lucene可以说是当下最先进、高性能、全功能的搜索引擎库。但是Lucene仅仅只是一个库。为了充分发挥其功能,你需要使用Java并将Lucene直接集成到应用程序中。更糟糕的是,您可能需要获得信息检索学位才能了解其工作原理,因为Lucene非常复杂。

Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发。

Lucene官网地址:Apache Lucene - Welcome to Apache Lucene

鉴于Lucene如此强大却难以上手的特点,诞生了ES。ES也是使用Java编写的,它的内部使用Lucene做索引与搜索,它的目的是隐藏Lucene的复杂性,取而代之的提供一套简单一致的RESTful API。

通过对生活中数据的类型作了一个简短了解之后,我们知道关系型数据库的 SQL 检索是处理不了这种非结构化数据的。

这种非结构化数据的处理需要依赖全文搜索,而目前市场上开放源代码的最好全文检索引擎工具包就属于 Apache 的 Lucene了。

但是 Lucene 只是一个工具包,它不是一个完整的全文检索引擎。Lucene 的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。

目前以 Lucene 为基础建立的开源可用全文搜索引擎主要是 Solr 和 Elasticsearch。

Solr 和 Elasticsearch 都是比较成熟的全文搜索引擎,能完成的功能和性能也基本一样。

但是 ES 本身就具有分布式的特性和易安装使用的特点,而 Solr 的分布式需要借助第三方来实现,例如通过使用 ZooKeeper 来达到分布式协调管理。

不管是 Solr 还是 Elasticsearch 底层都是依赖于 Lucene,而 Lucene 能实现全文搜索主要是因为它实现了倒排索引的查询结构。

Lucene的写流程和读流程

Lucene的写流程和读流程

其中,虚线箭头(a、b、c、d)表示写索引的主要过程,实线箭头(1-9)表示查询的主要过程。

Lucene中的主要模块(见图5-1)及模块说明如下:

  1. analysis:主要负责词法分析及语言处理,也就是我们常说的分词,通过该模块可最终形成存储或者搜索的最小单元Term。
  2. index模块:主要负责索引的创建工作。
  3. store模块:主要负责索引的读写,主要是对文件的一些操作,其主要目的是抽象出和平台文件系统无关的存储。
  4. queryParser模块:主要负责语法分析,把我们的查询语句生成Lucene底层可以识别的条件。
  5. search模块:主要负责对索引的搜索工作。
  6. similarity模块:主要负责相关性打分和排序的实现。

Lucene中的核心术语

ElasticSearch核心技术分词与倒排索引都是基于Lucene实现的。

下面介绍Lucene中的核心术语。

  1. ▪️Term:是索引里最小的存储和查询单元,对于英文来说一般是指一个单词,对于中文来说一般是指一个分词后的词。
  2. ▪️词典(Term Dictionary,也叫作字典):是Term的集合。词典的数据结构可以有很多种,每种都有自己的优缺点,比如:排序数组通过二分查找来检索数据:HashMap(哈希表)比排序数组的检索速度更快,但是会浪费存储空间;fst(finite-state transducer)有更高的数据压缩率和查询效率,因为词典是常驻内存的,而fst有很好的压缩率,所以fst在Lucene的最新版本中有非常多的使用场景,也是默认的词典数据结构。
  3. ▪️倒排序(Posting List):一篇文章通常由多个词组成,倒排表记录的是某个词在哪些文章中出现过。
  4. ▪️正向信息:原始的文档信息,可以用来做排序、聚合、展示等。
  5. ▪️段(segment):索引中最小的独立存储单元。一个索引文件由一个或者多个段组成。在Luence中的段有不变性,也就是说段一旦生成,在其上只能有读操作,不能有写操作。

Lucene的底层存储格式如下图所示。下图中由词典和倒排序两部分组成,其中的词典就是Term的集合。词典中的Term指向的文档链表的集合,叫做倒排表。词典和倒排表是Lucene中很重要的两种数据结构,是实现快速检索的重要基石。词典和倒排表是分两部分存储的,在倒排序中不但存储了文档编号,还存储了词频等信息。

Lucene的底层存储格式

如上图所示:的词典部分包含三个词条(Term):elasticsearch、lucene和solr。词典数据是查询的入口,所以这部分数据是以fst的形式存储在内存中的。

在倒排表中,"lucene"指向有序链表3,7,15,30,35,67,表示字符串"lucene"在文档编号为3、7、15、30、35、67的文章中出现过,elasticsearch和solr同理。

1、正向索引

正排表是以文档的 ID 为关键字,表中记录文档中每个 Term 的位置信息,查找时扫描表中每个文档中 Term 的信息直到找出所有包含查询关键字的文档。

因为索引是基于文档建立的,若是有新的文档加入,直接为该文档建立一个新的索引块,挂接在原来索引文件的后面。若是有文档删除,则直接找到该文档号文档对应的索引信息,将其直接删除。但是在查询的时候需对所有的文档进行扫描以确保没有遗漏,数据量较大时,这样就使得检索时间大大延长,检索效率低下。

在我们关系型库中索引为了兼顾插入和查询的性能,都采用了排序树例如:B-Tree/B+Tree这样的数据结构来存储索引。

2、分词

分词就是把字符串按照一定规则分成多个独立的词元(token),Elasticsearch 内置的分词器对中文不友好,会把中文分成单个字来进行全文检索,不能达到想要的结果 。其中IK分词器对中文很好,一般都使用它。

  • 分词是指将文本转换成一系列单词的过程,也叫作文本分析,在es里面称为analysis
分词器
  • 分词器是es中专门处理分词的组件,英文为analy,它的组成如下:
    • Character Filters

      • 针对原始文本进行处理,比如去除HTML特殊标记符
    • Tokenizer

      • 将原始文本按照一定规则切分为单词
    • Token Filters

      • 针对tokenizer处理的单词进行再加工,比如转小写、删除或新增等处理

        Analyize API
    • es提供了一个测试分词的api接口,方便验证分词效果,endpoint是_analyze

      • 可以直接指定analyzer进行测试
      • 可以直接指定索引中的字段进行测试
      • 可以自定义分词器进行测试
  • 直接指定analyzer进行测试,接口如下:

    POST _analyze
    {
    "analyzer":"standard", #分词器
    "text":"hello,world"  #测试文本
    }
    
  • 自定义分词器进行测试,接口如下:

    POST _analyze
    {
    "tokenizer":"standard",
    "filterf":["lowercase"],  #自定义analyzer
    "text":"hello world"
    }
    
预定义的分词器
  • es自带如下的分词器
    • Standard
    • Simple
    • Whitespace
    • Stop
    • Keyword
    • Pattern
    • Language
  • Standard Analyzer
    • 默认分词器
    • 其组成如下,特性为:
      • 按词切分,支持多语言
      • 小写处理
  • Simple Analyzer
    • 其组成如下,特性为:
      • 按照非字母切分
      • 小写处理
  • Whitespace Analyzer
    • 其组成如下,特性为:
      • 按照空格切分
  • Stop Analyzer
    • Stop Word指语气助词等修饰性的词语,比如the、an、的、这等等
    • 其组成如图,特性为:
      • 相比Simple Analyzer多了stop Word处理
  • Keyword Analyzer
    • 其组成如下,特性为:
      • 不分词,直接将输入作为一个单词输出
  • Pattern Analyze
    • 其组成如下,特性为:
      • 通过正则表达式自定义分隔符
      • 默认是\W+,即非字词的符号作为分隔符
  • language Analyze
    • 提供了30+常见语言的分词器
    • Arabic,Armenian,basque,bengali,Brazilian,Bulgarian,catAlan,cjk,Czech,Danish,Dutch,English...
中文分词
  • 难点
    • 中文分词指的是将一个汉字序列切分成一个一个单独的词。在英文中,单词之间是以空格作为自然分界符,汉语中没有一个形式上的分界符
    • 上下文不同,分词结果迥异,比如交叉歧义问题,比如下面两种分词都合理
      • 乒乓球拍/卖/完了
      • 乒乓球/拍卖/完了
  • 常用分词系统
  • 基于自然语言处理的分词系统
自定义分词
  • 当自带的分词无法满足需求时,可以自定义分词

    • 通过自定义Character Filters、Tokenizer Filter实现
  • Character Filters

    • 在Tokenizer之前对原始文本进行处理,比如增加、删除或替换字符等
    • 自带的如下:
      • HTML Strip去除html标签和转换html实体
      • Mapping进行字符替换操作
      • Pattern Replace进行正则匹配替换
    • 会影响后续Tokenizer解析的position和offset信息
  • Character Filters测试时可以采用如下api:

    POST _analyze
    {
    "tokenizer":"keyword", #keyword类型的Tokenizer可以直接看到输出结果
    "char_filter":["html_strip"], #指明要使用的char_filter
    "text":"<p>I'm so
    <b>happy</b>!</p>"
    }
    
  • Tokenizer

    • 将原始文本按照一定规则且分为单词(term or token)

    • 自带的如下:

      • standard按照单词进行分割
      • letter按照非字符类进行分割
      • whitespace按照空格进行分割
      • UAX URL Email按照standard分割,但不会分割邮箱和url
      • NGram和Edge NGram连词分割
      • Path Hierarchy按照文件路径进行分割
    • Tokenizer测试时可以采用如下api:

      POST _analyze
      {
      "tokenizer":"path_hierarchy",
      "text":"/one/two/three"
      }
      
  • Token Filters

    • 对于Tokenizer输出的单词(term)进行增加、删除、修改等操作

    • 自带的如下:

      • lowercase将所有term转换为小写
      • stop删除stop words
      • NGram和Edge NGram连词分割
      • Synonym添加近义词的term
    • Filter测试时可以采用如下api:

      POST _analyze
      {
      "text":"a hello world",
      "tokenizer":"standard",
      "filter":[
      "stop",
      "lowercase",
      {
      "type":"ngram",
      "min_gram":4,
      "max_gram":4
      }
      ]
      }
      
自定义分词的api
  • 自定义分词的api
    • 自定义分词需要在索引的配置中设定,如下所示:

      PUT test_index
      {
      "settings":{
      "analysis":{
      "char_filter":{},
      "tokenizer":{},
      "filter":{},
      "analyzer":{}
      }
      }
      }
      
分词使用说明
  • 分词会在如下两个时机使用:

    • 创建或更新文档时(Index Time),会对相应的文档进行分词处理
    • 查询时(Search Time),会对查询语句进行分词
  • 索引时分词是通过配置Index Mapping中每个字段的analyzer属性实现的,如下:

    • 不指定分词时,默认使用standard

      PUT test_index
      {
      "mappings":{
      "doc":{
      "properties":{
      "title":{
        "type":"text",
        "analyzer":"whitespace" #指定分词器
      }
      }
      }
      }
      }
      
  • 查询时分词的指定方式有如下几种:

    • 查询时通过analyzer指定分词器

      POST test_index/_search
      {
      "query":{
      "match":{
      "message":{
      "query":"hello",
      "analyzer":"standard"
      }
      }
      }
      }
      
  • 通过index mapping设置search_analyzer实现

    PUT test_index
    {
    "mappings":{
    "doc":{
    "properties":{
    "title":{
    "type":"text",
    "analyzer":"whitespace",
    "search_analyzer":"standard"
    }
    }
    }
    }
    }

  • 一般不需要特别指定查询时分词器,直接使用索引时分词器即可,否则会出现无法匹配的情况下

分词使用建议
  • 明确字段是否需要分词,不需要分词的字段就将type设置为keyword,可以节省空间和提高写性能
  • 善用_analyze API,查看文档的具体分词结果
  • 动手测试

3、倒排索引

逻辑结构部分是一个倒排索引列表:

1、将要搜索的文档内容分词,所有不重复的词组成分词列表。

2、将搜索的文档最终以Document方式存储起来。

3、每个词和document都有关联。

倒排索引也可以称反向索引,倒排索引是搜索引擎到核心,主要包括两部分:

(1)单词词典(Term Dictionary)

记录所有文档的单词,一般都比较大

记录单词到倒排列表的关联信息(文档ID)

(2)倒排列表(Posting List):

记录了单词对应的文档集合,由倒排索引项(Posting)组成

单词词典的实现一般是 B+ Tree

倒排索引项(Posting)主要包含如下信息:

1、文档Id,用于获取原始信息

2、单词频率(TF, Term Frequency),记录该单词在该文档中的出现次数,用于后续相关性算法

3、位置(Position),记录单词在文档中的分词位置(多个),用于做词语搜索(Phrase Query)

4、偏移(Offset),记录单词在文档的开始和结束位置,用于做高亮显示

例子1:

分词和倒排查询时间复杂度都是 O(1),整个搜索的时间复杂度取决于「求list< id>的交集」,因此搜索实际上问题也变成了求两个集合的交集。

例子2:

为了创建倒排索引,我们通过分词器将每个文档的内容域拆分成单独的词(我们称它为词条或 Term),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。

结果如下所示:

bash 复制代码
Term          Doc_1    Doc_2   Doc_3
-------------------------------------
Java        |   X   |        |
is          |   X   |   X    |   X
the         |   X   |   X    |   X
best        |   X   |   X    |   X
programming |   x   |   X    |   X
language    |   X   |   X    |   X
PHP         |       |   X    |
Javascript  |       |        |   X
-------------------------------------

这种结构由文档中所有不重复词的列表构成,对于其中每个词都有一个文档列表与之关联。

这种由属性值来确定记录的位置的结构就是倒排索引。带有倒排索引的文件我们称为倒排文件。

我们将上面的内容转换为图的形式来说明倒排索引的结构信息,如下图所示:

其中主要有如下几个核心术语需要理解:

  1. 词条(Term):索引里面最小的存储和查询单元,对于英文来说是一个单词,对于中文来说一般指分词后的一个词。
  2. 词典(Term Dictionary):或字典,是词条 Term 的集合。搜索引擎的通常索引单位是单词,单词词典是由文档集合中出现过的所有单词构成的字符串集合,单词词典内每条索引项记载单词本身的一些信息以及指向"倒排列表"的指针。
  3. 倒排表(Post list):一个文档通常由多个词组成,倒排表记录的是某个词在哪些文档里出现过以及出现的位置。
  4. 每条记录称为一个倒排项(Posting)。倒排表记录的不单是文档编号,还存储了词频等信息。
  5. 倒排文件(Inverted File):所有单词的倒排列表往往顺序地存储在磁盘的某个文件里,这个文件被称之为倒排文件,倒排文件是存储倒排索引的物理文件。

像 B+ 树一样,可以在页里实现二分查找。

Lucene 的倒排索引,增加了最左边的一层「字典树」term index,它不存储所有的单词,只存储单词前缀,通过字典树找到单词所在的块,也就是单词的大概位置,再在块里二分查找,找到对应的单词,再找到单词对应的文档列表。

Lucene 的实现会要更加复杂,针对不同的数据结构采用不同的字典索引,使用了FST模型、BKDTree等结构。

真实的倒排记录也并非一个链表,而是采用了SkipList、BitSet等结构

4、正向和倒排

那么为什么一个叫做正向索引,一个叫做倒排索引呢?

正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程。

而倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程。

正向索引:

优点:

可以给多个字段创建索引

根据索引字段搜索、排序速度非常快

缺点:

根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。

倒排索引:

优点:

根据词条搜索、模糊搜索时,速度非常快

缺点:

只能给词条创建索引,而不是字段

无法根据字段做排序

Elasticsearch 的基本概念

首先,从架构的角度来看,Elasticsearch是一个分布式的搜索和分析引擎,它能够存储、搜索和分析大量数据。为了实现这些功能,Elasticsearch采用了分片和副本的机制,使得数据可以分布在多个节点上,并且具有容错性和可扩展性。简要介绍Elasticsearch的架构:

一、ES工作原理

我们现在来详细了解一下 Elasticsearch 是如何工作的。

数据的写入

当一个文档需要写入 Elasticsearch 时,它首先会通过 HTTP 请求发送到一个节点,我们称为协调节点。协调节点是负责接受请求的 Elasticsearch 节点,它会根据文档的哈希值把它路由到对应的主分片中,主分片再将文档写入自己的本地硬盘。

同时,主分片会将文档的副本异步传输给其他节点的副本分片,以便在主分片失效时,其他副本分片可以顶替其工作。当所有的分片都写入完毕后,协调节点会向客户端返回写入成功的响应。

数据的搜索

当用户需要搜索数据时,它会向协调节点发起一个搜索请求,协调节点会将此请求转发到所有与请求相关的分片,每个分片都会在本地检索索引,并将检索结果返回给协调节点。

协调节点在收到所有分片返回的结果后,会进行结果的合并和排序,并将最终的结果返回给客户端。由于每个分片都只需要检索自己本地的数据,因此 Elasticsearch 具有很高的横向扩展能力,可以处理大规模数据的搜索和分析任务。

可伸缩性

Elasticsearch 具有很强的可伸缩性,可以方便地进行水平和垂直扩展。水平扩展指的是通过添加新的节点来分散索引和分片的存储,垂直扩展指的是通过升级硬件来提高单个节点的性能。

Elasticsearch 还提供了一些功能来实现自动化的扩展和负载均衡,例如:自动分片、负载均衡、节点失效检测等。

实时搜索

Elasticsearch 具有近乎实时的搜索能力,当一个文档写入后,它可以立即被搜索引擎检索。这是因为 Elasticsearch 采用了一种倒排索引(Inverted Index)的数据结构,它可以快速地找到符合搜索条件的文档。

倒排索引是一种将文档中的每个单词映射到该单词所在文档的索引,它一般包含两部分:词典和倒排表。词典是所有文档中出现的单词的集合,它以字典序排序。倒排表是每个单词所对应的文档编号列表,每个文档编号都按照某种方式排序,以便支持更快地搜索和排名计算。

搜索的优化

Elasticsearch 提供了多种搜索优化方式,例如:文本分析、过滤器、聚合查询等。

文本分析是将原始文本转换为标准化格式的过程,它包括文本分词、词干提取、同义词转换等操作。文本分析可以帮助 Elasticsearch 更准确地匹配搜索关键字和文档内容。

过滤器是一种能够过滤掉不符合条件的文档的查询方式,它可以用来限定搜索结果的数量和质量。过滤器可以通过缓存和布尔运算等方式来提高搜索效率。

聚合查询可以用来对数据进行统计、分析和计算等操作,例如:计算平均值、最小值、最大值、总量等。聚合查询可以帮助我们更深入地理解我们的数据,并提供更好的业务决策支持。

下面是详细的ES工作流程

1.1 数据的导入和解析

在Elasticsearch中,首先需要导入数据。这个过程通常涉及将数据格式化为 JSON 格式,因为 Elasticsearch 使用 JSON 作为数据交换格式。

在这张图中,你可以看到一个示例数据"The cat in the tree",这段文本被转换成 JSON 格式并准备导入到 Elasticsearch 中。

1.2 数据的分析和索引

导入Elasticsearch后,数据会被分析和索引。

这一步骤是通过Elasticsearch 的分词器完成的,它将文本拆分成更易于搜索的单词或短语,即"tokens"。

这些tokens随后被用来创建一个倒排索引,这是一种特殊的数据结构,用于快速全文搜索。

倒排索引将每个唯一的单词映射到包含该单词的所有文档,这在动图中通过连接线和节点表示。

1.3 分布式架构

Elasticsearch是一个分布式搜索引擎,这意味着数据可以跨多个服务器(称为节点)存储和检索。

这种架构提高了系统的扩展性和容错能力。在图中,你可以看到数据被存储在不同的服务器上,这有助于分散负载和提高查询效率。

1.4 查询解析和执行

当用户通过一个搜索界面(如图中的笔记本电脑)输入查询时,Elasticsearch会解析这个查询请求。

解析过程包括理解查询中的关键词以及可能的查询意图,然后使用这些信息来检索倒排索引。

1.5 得分和排序

一旦Elasticsearch找到了所有相关的文档,它将基于相关性给这些文档打分

打分机制通常依赖于因素如关键词的出现频率、文档中的位置等。

这些分数用于对结果进行排序,以确保最相关的结果排在最前面。

1.6 返回结果

最后,搜索结果会被返回给用户,通常也是以JSON格式。用户可以看到最相关的文档排在最前面,这使得用户能够快速有效地找到他们需要的信息。

这整个过程不仅高效而且具有很高的可扩展性,使Elasticsearch成为处理大规模数据集的理想选择。

通过这种方式,Elasticsearch支持复杂的全文搜索功能,广泛应用于各种场景中,如日志分析、实时数据监控和复杂搜索需求。

二、分布式架构

节点与集群:Elasticsearch由多个节点(Node)组成,这些节点可以组成一个集群(Cluster)。每个节点都可以处理读写请求,并且数据在集群中的节点之间进行分布和复制,以实现高可用性和扩展性。

分片与副本:为了支持大规模数据,Elasticsearch将索引划分为多个分片(Shard),每个分片可以独立存储和处理数据。此外,每个分片可以有多个副本(Replica),用于提供数据冗余、故障恢复和读取负载均衡。

1、Cluster:集群

Elasticsearch可以作为一个独立的单个搜索服务器,不过,为了处理大型数据集,实现容错和高可用性,Elasticsearch可以运行在许多互相合作的服务器上。这些服务器的集合称为集群。

Cluster:集群,ES可以作为一个独立的单个搜索服务器。不过,为了处理大型数据集,实现容错和高可用性,ES可以运行在许多互相合作的服务器上。这些服务器的集合称为集群。

2、Node:节点

形成集群的每个服务器称为节点。

Node:节点,形成集群的每个服务器称为节点,一个节点可以包含多个shard

3、Shard:分片

当有大量的文档时,由于内存的限制、磁盘处理能力不足、无法足够快的响应客户端的请求等,一个节点可能不够。这种情况下,数据可以分为较小的分片。每个分片放到不同的服务器上。 当你查询的索引分布在多个分片上时,Elasticsearch会把查询发送给每个相关的分片,并将结果组合在一起,而应用程序并不知道分片的存在。即:这个过程对用户来说是透明的。

一个 索引 可以存储超出单个结点硬件限制的大量数据。比如,一个具有 10亿文档的索引占据 1TB 的磁盘空间,而任一节点都没有这样大的磁盘空间;或者单个节点处理搜索请求,响应太慢。

为了解决这个问题,Elasticsearch 提供了将索引划分成多份的能力,这些份就叫做 分片。当你创建一个索引的时候,你可以指定你想要的 分片的数量。每个分片本身也是一个功能完善并且独立的 索引,这个 索引 可以被放置到集群中的任何节点上。

分片之所以重要,主要有两方面的原因:

  1. 允许你水平分割/扩展你的内容容量
  2. 允许你在分片(潜在地,位于多个节点上)之上进行分布式的、并行的操作,进而提高性能/吞吐量
  3. 一个分片怎样分布,它的文档怎样聚合回搜索请求,是完全由 Elasticsearch 管理的,对于作为用户的你来说,这些都是透明的

4、Replia:副本

为提高查询吞吐量或实现高可用性,可以使用分片副本。 副本是一个分片的精确复制,每个分片可以有零个或多个副本。ES中可以有许多相同的分片,其中之一被选择更改索引操作,这种特殊的分片称为主分片。 当主分片丢失时,如:该分片所在的数据不可用时,集群将副本提升为新的主分片。

Replia:副本,为提高查询吞吐量或实现高可用性,可以使用分片副本。

副本是一个分片的精确复制,每个分片可以有零个或多个副本。副本的作用:

  1. 提高系统的容错性,当某个节点某个分片损坏或丢失时,可以从副本中恢复。
  2. 提高 ES 查询效率,ES 会自动对搜索请求进行负载均衡。

5、全文检索

全文检索就是对一篇文章进行索引,可以根据关键字搜索,类似于mysql里的like语句。 全文索引就是把内容根据词的意义进行分词,然后分别创建索引,例如"你们的激情是因为什么事情来的" 可能会被分词成:"你们","激情","什么事情","来" 等token,这样当你搜索"你们" 或者 "激情" 都会把这句搜出来。

6、索引与搜索

Document:文档,指一行数据;

Index:索引,是多个document的集合(和sql数据库的表对应);

倒排索引:Elasticsearch使用Lucene作为其底层的搜索库。Lucene构建倒排索引(Inverted Index)来加速搜索过程。倒排索引将文档中的单词映射到包含这些单词的文档列表,从而实现快速查找和检索。

查询执行:当客户端发送搜索请求时,请求首先到达一个协调节点。协调节点解析查询语句,确定需要访问的分片,并将请求转发给相应的数据节点。数据节点在本地执行查询,并将结果返回给协调节点。协调节点聚合来自各个数据节点的结果,并进行排序、分页等处理,最终将结果返回给客户端。

集群(Cluster)

ES 的集群搭建很简单,不需要依赖第三方协调管理组件,自身内部就实现了集群的管理功能。

ES 集群由一个或多个 Elasticsearch 节点组成,每个节点配置相同的 cluster.name 即可加入集群,默认值为 "elasticsearch"。

确保不同的环境中使用不同的集群名称,否则最终会导致节点加入错误的集群。

一个 Elasticsearch 服务启动实例就是一个节点(Node)。节点通过 node.name 来设置节点名称,如果不设置则在启动时给节点分配一个随机通用唯一标识符作为名称。

①发现机制

那么有一个问题,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 将以上服务发现以及选主的流程叫做 Zen Discovery 。

由于它支持任意数目的集群( 1- N ),所以不能像 Zookeeper 那样限制节点必须是奇数,也就无法用投票的机制来选主,而是通过一个规则。

只要所有的节点都遵循同样的规则,得到的信息都是对等的,选出来的主节点肯定是一致的。

但分布式系统的问题就出在信息不对等的情况,这时候很容易出现脑裂(Split-Brain)的问题。

大多数解决方案就是设置一个 Quorum 值,要求可用节点必须大于 Quorum(一般是超过半数节点),才能对外提供服务。

而 Elasticsearch 中,这个 Quorum 的配置就是 discovery.zen.minimum_master_nodes 。

②节点的角色

每个节点既可以是候选主节点也可以是数据节点,通过在配置文件 ../config/elasticsearch.yml 中设置即可,默认都为 true。

1.  node.master: true //是否候选主节点 

2.  node.data: true //是否数据节点 

数据节点负责数据的存储和相关的操作,例如对数据进行增、删、改、查和聚合等操作,所以数据节点(Data 节点)对机器配置要求比较高,对 CPU、内存和 I/O 的消耗很大。

通常随着集群的扩大,需要增加更多的数据节点来提高性能和可用性。

候选主节点可以被选举为主节点(Master 节点),集群中只有候选主节点才有选举权和被选举权,其他节点不参与选举的工作。

主节点负责创建索引、删除索引、跟踪哪些节点是群集的一部分,并决定哪些分片分配给相关的节点、追踪集群中节点的状态等,稳定的主节点对集群的健康是非常重要的。

一个节点既可以是候选主节点也可以是数据节点,但是由于数据节点对 CPU、内存核 I/O 消耗都很大。

所以如果某个节点既是数据节点又是主节点,那么可能会对主节点产生影响从而对整个集群的状态产生影响。

因此为了提高集群的健康性,我们应该对 Elasticsearch 集群中的节点做好角色上的划分和隔离。可以使用几个配置较低的机器群作为候选主节点群。

主节点和其他节点之间通过 Ping 的方式互检查,主节点负责 Ping 所有其他节点,判断是否有节点已经挂掉。其他节点也通过 Ping 的方式判断主节点是否处于可用状态。

虽然对节点做了角色区分,但是用户的请求可以发往任何一个节点,并由该节点负责分发请求、收集结果等操作,而不需要主节点转发。

这种节点可称之为协调节点,协调节点是不需要指定和配置的,集群中的任何节点都可以充当协调节点的角色。

③脑裂现象

同时如果由于网络或其他原因导致集群中选举出多个 Master 节点,使得数据更新时出现不一致,这种现象称之为脑裂,即集群中不同的节点对于 Master 的选择出现了分歧,出现了多个 Master 竞争。

"脑裂"问题可能有以下几个原因造成:

  • 网络问题:集群间的网络延迟导致一些节点访问不到 Master,认为 Master 挂掉了从而选举出新的 Master,并对 Master 上的分片和副本标红,分配新的主分片。
  • 节点负载:主节点的角色既为 Master 又为 Data,访问量较大时可能会导致 ES 停止响应(假死状态)造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。
  • 内存回收:主节点的角色既为 Master 又为 Data,当 Data 节点上的 ES 进程占用的内存较大,引发 JVM 的大规模内存回收,造成 ES 进程失去响应。

为了避免脑裂现象的发生,我们可以从原因着手通过以下几个方面来做出优化措施:

  • 适当调大响应时间,减少误判。通过参数 discovery.zen.ping_timeout 设置节点状态的响应时间,默认为 3s,可以适当调大。

如果 Master 在该响应时间的范围内没有做出响应应答,判断该节点已经挂掉了。调大参数(如 6s,discovery.zen.ping_timeout:6),可适当减少误判。

  • 选举触发。我们需要在候选集群中的节点的配置文件中设置参数 discovery.zen.munimum_master_nodes 的值。

这个参数表示在选举主节点时需要参与选举的候选主节点的节点数,默认值是 1,官方建议取值(master_eligibel_nodes/2)+1,其中 master_eligibel_nodes 为候选主节点的个数。

这样做既能防止脑裂现象的发生,也能最大限度地提升集群的高可用性,因为只要不少于 discovery.zen.munimum_master_nodes 个候选节点存活,选举工作就能正常进行。

当小于这个值的时候,无法触发选举行为,集群无法使用,不会造成分片混乱的情况。

  • 角色分离。即是上面我们提到的候选主节点和数据节点进行角色分离,这样可以减轻主节点的负担,防止主节点的假死状态发生,减少对主节点"已死"的误判。

分片(Shards)

ES 支持 PB 级全文搜索,当索引上的数据量太大的时候,ES 通过水平拆分的方式将一个索引上的数据拆分出来分配到不同的数据块上,拆分出来的数据库块称之为一个分片。

这类似于 MySQL 的分库分表,只不过 MySQL 分库分表需要借助第三方组件而 ES 内部自身实现了此功能。

在一个多分片的索引中写入数据时,通过路由来确定具体写入哪一个分片中,所以在创建索引的时候需要指定分片的数量,并且分片的数量一旦确定就不能修改。

分片的数量和下面介绍的副本数量都是可以通过创建索引时的 Settings 来配置,ES 默认为一个索引创建 5 个主分片, 并分别为每个分片创建一个副本。

PUT /myIndex 

{ 

 "settings" : { 

 "number_of_shards" : 5, 

 "number_of_replicas" : 1 

   } 

} 

ES 通过分片的功能使得索引在规模上和性能上都得到提升,每个分片都是 Lucene 中的一个索引文件,每个分片必须有一个主分片和零到多个副本。

副本(Replicas)

副本就是对分片的 Copy,每个主分片都有一个或多个副本分片,当主分片异常时,副本可以提供数据的查询等操作。

主分片和对应的副本分片是不会在同一个节点上的,所以副本分片数的最大值是 N-1(其中 N 为节点数)。

对文档的新建、索引和删除请求都是写操作,必须在主分片上面完成之后才能被复制到相关的副本分片。

ES 为了提高写入的能力这个过程是并发写的,同时为了解决并发写的过程中数据冲突的问题,ES 通过乐观锁的方式控制,每个文档都有一个 _version (版本)号,当文档被修改时版本号递增。

一旦所有的副本分片都报告写成功才会向协调节点报告成功,协调节点向客户端报告成功。

从上图可以看出为了达到高可用,Master 节点会避免将主分片和副本分片放在同一个节点上。

假设这时节点 Node1 服务宕机了或者网络不可用了,那么主节点上主分片 S0 也就不可用了。

幸运的是还存在另外两个节点能正常工作,这时 ES 会重新选举新的主节点,而且这两个节点上存在我们所需要的 S0 的所有数据。

我们会将 S0 的副本分片提升为主分片,这个提升主分片的过程是瞬间发生的。此时集群的状态将会为 Yellow。

为什么我们集群状态是 Yellow 而不是 Green 呢?虽然我们拥有所有的 2 个主分片,但是同时设置了每个主分片需要对应两份副本分片,而此时只存在一份副本分片。所以集群不能为 Green 的状态。

如果我们同样关闭了 Node2 ,我们的程序依然可以保持在不丢失任何数据的情况下运行,因为 Node3 为每一个分片都保留着一份副本。

如果我们重新启动 Node1 ,集群可以将缺失的副本分片再次进行分配,那么集群的状态又将恢复到原来的正常状态。

如果 Node1 依然拥有着之前的分片,它将尝试去重用它们,只不过这时 Node1 节点上的分片不再是主分片而是副本分片了,如果期间有更改的数据只需要从主分片上复制修改的数据文件即可。

小结:

  • 将数据分片是为了提高可处理数据的容量和易于进行水平扩展,为分片做副本是为了提高集群的稳定性和提高并发量。
  • 副本是乘法,越多消耗越大,但也越保险。分片是除法,分片越多,单分片数据就越少也越分散。
  • 副本越多,集群的可用性就越高,但是由于每个分片都相当于一个 Lucene 的索引文件,会占用一定的文件句柄、内存及 CPU。

并且分片间的数据同步也会占用一定的网络带宽,所以索引的分片数和副本数也不是越多越好。

映射(Mapping)

映射是用于定义 ES 对索引中字段的存储类型、分词方式和是否存储等信息,就像数据库中的 Schema ,描述了文档可能具有的字段或属性、每个字段的数据类型。

只不过关系型数据库建表时必须指定字段类型,而 ES 对于字段类型可以不指定然后动态对字段类型猜测,也可以在创建索引时具体指定字段的类型。

对字段类型根据数据格式自动识别的映射称之为动态映射(Dynamic Mapping),我们创建索引时具体定义字段类型的映射称之为静态映射或显示映射(Explicit Mapping)。

在讲解动态映射和静态映射的使用前,我们先来了解下 ES 中的数据有哪些字段类型?之后我们再讲解为什么我们创建索引时需要建立静态映射而不使用动态映射。

ES(v6.8)中字段数据类型主要有以下几类:

Text 用于索引全文值的字段,例如电子邮件正文或产品说明。这些字段是被分词的,它们通过分词器传递 ,以在被索引之前将字符串转换为单个术语的列表。

分析过程允许 Elasticsearch 搜索单个单词中每个完整的文本字段。文本字段不用于排序,很少用于聚合。

Keyword 用于索引结构化内容的字段,例如电子邮件地址,主机名,状态代码,邮政编码或标签。它们通常用于过滤,排序,和聚合。Keyword 字段只能按其确切值进行搜索。

通过对字段类型的了解我们知道有些字段需要明确定义的,例如某个字段是 Text 类型还是 Keyword 类型差别是很大的,时间字段也许我们需要指定它的时间格式,还有一些字段我们需要指定特定的分词器等等。

如果采用动态映射是不能精确做到这些的,自动识别常常会与我们期望的有些差异。

所以创建索引的时候一个完整的格式应该是指定分片和副本数以及 Mapping 的定义,如下:

PUT my_index 

{ 

 "settings" : { 

 "number_of_shards" : 5, 

 "number_of_replicas" : 1 

   } 

 "mappings": { 

 "_doc": {  

 "properties": {  

 "title":    { "type": "text"  },  

 "name":     { "type": "text"  },  

 "age":      { "type": "integer" },   

 "created":  { 

 "type":   "date",  

 "format": "strict_date_optional_time||epoch_millis" 

        } 

      } 

    } 

  } 

} 

三、ES的索引数据结构

什么是 index (索引) ?

一个 索引 就是一个拥有几分相似特征的文档的集合。ES 将数据存储于一个或多个索引中,索引 就相当于 SQL 中的一个数据库。

什么是 Type(类型)?

类型是索引内部的逻辑分区(category/partition),然而其意义完全取决于用户需求。因此,一个索引内部可定义一个或多个类型(type)。一般来说,类型就是为那些拥有相同的域的文档做的预定义。类比传统的关系型数据库领域来说,类型 相当于 表,7.x 版本默认使用 _doc 作为 type 。

什么是 Document(文档)?

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

什么是 Field (字段)?

Field 是相当于数据库中的 Column

什么是 Mapping(映射)?

Mapping 是定义文档及其包含的字段如何存储和索引的过程。MappingES 中的一个很重要的内容,它类似于传统关系型数据中 tableschema,用于定义一个索引(index)的某个类型(type)的数据结构。

四、Elasticsearch写索引原理

写入流程:当文档被写入Elasticsearch时,它们首先被放置在内存中的一个缓冲区中,并同时记录到事务日志(Translog)中以确保数据的持久性。随着时间的推移或达到一定的条件,缓冲区中的数据会被刷新(Refresh)到Lucene的索引中,形成新的段(Segment)。这些段是不可变的,一旦被写入就不能被修改。最终,通过flush操作将内存中的数据以及Translog中的更改持久化到磁盘上。

段合并:为了优化存储和搜索性能,Lucene会定期进行段合并(Segment Merging)操作。合并过程中会将多个小的段合并成更大的段,并删除重复和已删除的文档以释放存储空间。

写索引原理

下图描述了 3 个节点的集群,共拥有 12 个分片,其中有 4 个主分片(S0、S1、S2、S3)和 8 个副本分片(R0、R1、R2、R3),每个主分片对应两个副本分片,节点 1 是主节点(Master 节点)负责整个集群的状态。

写索引是只能写在主分片上,然后同步到副本分片。这里有四个主分片,一条数据 ES 是根据什么规则写到特定分片上的呢?

这条索引数据为什么被写到 S0 上而不写到 S1 或 S2 上?那条数据为什么又被写到 S3 上而不写到 S0 上了?

首先这肯定不会是随机的,否则将来要获取文档的时候我们就不知道从何处寻找了。

实际上,这个过程是根据下面这个公式决定的:

shard = hash(routing) % number_of_primary_shards

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

Routing 通过 Hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到余数。

这个在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。

这就解释了为什么我们要在创建索引的时候就确定好主分片的数量并且永远不会改变这个数量:因为如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了。

由于在 ES 集群中每个节点通过上面的计算公式都知道集群中的文档的存放位置,所以每个节点都有处理读写请求的能力。

在一个写请求被发送到某个节点后,该节点即为前面说过的协调节点,协调节点会根据路由公式计算出需要写到哪个分片上,再将请求转发到该分片的主分片节点上。

假如此时数据通过路由计算公式取余后得到的值是 shard=hash(routing)%4=0。

则具体流程如下:

  • 客户端向 ES1 节点(协调节点)发送写请求,通过路由计算公式得到值为 0,则当前数据应被写到主分片 S0 上。
  • ES1 节点将请求转发到 S0 主分片所在的节点 ES3,ES3 接受请求并写入到磁盘。
  • 并发将数据复制到两个副本分片 R0 上,其中通过乐观并发控制数据的冲突。一旦所有的副本分片都报告成功,则节点 ES3 将向协调节点报告成功,协调节点向客户端报告成功。

存储原理

上面介绍了在 ES 内部索引的写处理流程,这个流程是在 ES 的内存中执行的,数据被分配到特定的分片和副本上之后,最终是存储到磁盘上的,这样在断电的时候就不会丢失数据。

具体的存储路径可在配置文件 ../config/elasticsearch.yml 中进行设置,默认存储在安装目录的 Data 文件夹下。

建议不要使用默认值,因为若 ES 进行了升级,则有可能导致数据全部丢失:

1.  path.data: /path/to/data //索引数据 

2.  path.logs: /path/to/logs  //日志记录 
①分段存储

索引文档以段的形式存储在磁盘上,何为段?索引文件被拆分为多个子文件,则每个子文件叫作段,每一个段本身都是一个倒排索引,并且段具有不变性,一旦索引的数据被写入硬盘,就不可再修改。

在底层采用了分段的存储模式,使它在读写时几乎完全避免了锁的出现,大大提升了读写性能。

段被写入到磁盘后会生成一个提交点,提交点是一个用来记录所有提交后段信息的文件。

一个段一旦拥有了提交点,就说明这个段只有读的权限,失去了写的权限。相反,当段在内存中时,就只有写的权限,而不具备读数据的权限,意味着不能被检索。

段的概念提出主要是因为:在早期全文检索中为整个文档集合建立了一个很大的倒排索引,并将其写入磁盘中。

如果索引有更新,就需要重新全量创建一个索引来替换原来的索引。这种方式在数据量很大时效率很低,并且由于创建一次索引的成本很高,所以对数据的更新不能过于频繁,也就不能保证时效性。

索引文件分段存储并且不可修改,那么新增、更新和删除如何处理呢?

  • 新增,新增很好处理,由于数据是新的,所以只需要对当前文档新增一个段就可以了。
  • 删除,由于不可修改,所以对于删除操作,不会把文档从旧的段中移除而是通过新增一个 .del 文件,文件中会列出这些被删除文档的段信息。

这个被标记删除的文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。

  • 更新,不能修改旧的段来进行反映文档的更新,其实更新相当于是删除和新增这两个动作组成。会将旧的文档在 .del 文件中标记删除,然后文档的新版本被索引到一个新的段中。

可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就会被移除。

段被设定为不可修改具有一定的优势也有一定的缺点,优势主要表现在:

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

段的不变性的缺点如下:

  • 当对旧数据进行删除时,旧数据不会马上被删除,而是在 .del 文件中被标记为删除。而旧数据只能等到段更新时才能被移除,这样会造成大量的空间浪费。
  • 若有一条数据频繁的更新,每次更新都是新增新的标记旧的,则会有大量的空间浪费。
  • 每次新增数据时都需要新增一个段来存储数据。当段的数量太多时,对服务器的资源例如文件句柄的消耗会非常大。
  • 在查询的结果中包含所有的结果集,需要排除被标记删除的旧数据,这增加了查询的负担。
②延迟写策略

介绍完了存储的形式,那么索引写入到磁盘的过程是怎样的?是否是直接调 Fsync 物理性地写入磁盘?

答案是显而易见的,如果是直接写入到磁盘上,磁盘的 I/O 消耗上会严重影响性能。

那么当写数据量大的时候会造成 ES 停顿卡死,查询也无法做到快速响应。如果真是这样 ES 也就不会称之为近实时全文搜索引擎了。

为了提升写的性能,ES 并没有每新增一条数据就增加一个段到磁盘上,而是采用延迟写的策略。

每当有新增的数据时,就将其先写入到内存中,在内存和磁盘之间是文件系统缓存。

当达到默认的时间(1 秒钟)或者内存的数据达到一定量时,会触发一次刷新(Refresh),将内存中的数据生成到一个新的段上并缓存到文件缓存系统 上,稍后再被刷新到磁盘中并生成提交点。

这里的内存使用的是 ES 的 JVM 内存,而文件缓存系统使用的是操作系统的内存。

新的数据会继续的被写入内存,但内存中的数据并不是以段的形式存储的,因此不能提供检索功能。

由内存刷新到文件缓存系统的时候会生成新的段,并将段打开以供搜索使用,而不需要等到被刷新到磁盘。

在 Elasticsearch 中,写入和打开一个新段的轻量的过程叫做 Refresh (即内存刷新到文件缓存系统)。

默认情况下每个分片会每秒自动刷新一次。这就是为什么我们说 Elasticsearch 是近实时搜索,因为文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。

我们也可以手动触发 Refresh,POST /_refresh 刷新所有索引,POST /nba/_refresh 刷新指定的索引。

Tips:尽管刷新是比提交轻量很多的操作,它还是会有性能开销。当写测试的时候, 手动刷新很有用,但是不要在生产>环境下每次索引一个文档都去手动刷新。而且并不是所有的情况都需要每秒刷新。

可能你正在使用 Elasticsearch 索引大量的日志文件, 你可能想优化索引速度而不是>近实时搜索。

这时可以在创建索引时在 Settings 中通过调大 refresh_interval = "30s" 的值 , 降低每个索引的刷新频率,设值时需要注意后面带上时间单位,否则默认是毫秒。当 refresh_interval=-1 时表示关闭索引的自动刷新。

虽然通过延时写的策略可以减少数据往磁盘上写的次数提升了整体的写入能力,但是我们知道文件缓存系统也是内存空间,属于操作系统的内存,只要是内存都存在断电或异常情况下丢失数据的危险。

为了避免丢失数据,Elasticsearch 添加了事务日志(Translog),事务日志记录了所有还没有持久化到磁盘的数据。

添加了事务日志后整个写索引的流程如上图所示:

  • 一个新文档被索引之后,先被写入到内存中,但是为了防止数据的丢失,会追加一份数据到事务日志中。

不断有新的文档被写入到内存,同时也都会记录到事务日志中。这时新数据还不能被检索和查询。

  • 当达到默认的刷新时间或内存中的数据达到一定量后,会触发一次 Refresh,将内存中的数据以一个新段形式刷新到文件缓存系统中并清空内存。这时虽然新段未被提交到磁盘,但是可以提供文档的检索功能且不能被修改。
  • 随着新文档索引不断被写入,当日志数据大小超过 512M 或者时间超过 30 分钟时,会触发一次 Flush。

内存中的数据被写入到一个新段同时被写入到文件缓存系统,文件系统缓存中数据通过 Fsync 刷新到磁盘中,生成提交点,日志文件被删除,创建一个空的新日志。

通过这种方式当断电或需要重启时,ES 不仅要根据提交点去加载已经持久化过的段,还需要工具 Translog 里的记录,把未持久化的数据重新持久化到磁盘上,避免了数据丢失的可能。

③段合并

由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。

每一个段都会消耗文件句柄、内存和 CPU 运行周期。更重要的是,每个搜索请求都必须轮流检查每个段然后合并查询结果,所以段越多,搜索也就越慢。

Elasticsearch 通过在后台定期进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。

段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档不会被拷贝到新的大段中。合并的过程中不会中断索引和搜索。

段合并在进行索引和搜索时会自动进行,合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中,这些段既可以是未提交的也可以是已提交的。

合并结束后老的段会被删除,新的段被 Flush 到磁盘,同时写入一个包含新段且排除旧的和较小的段的新提交点,新的段被打开可以用来搜索。

段合并的计算量庞大, 而且还要吃掉大量磁盘 I/O,段合并会拖累写入速率,如果任其发展会影响搜索性能。

Elasticsearch 在默认情况下会对合并流程进行资源限制,所以搜索仍然有足够的资源很好地执行。

五、es的数据写入流程与原理

在写入数据时,Elasticsearch遵循以下原理和步骤:

  1. 客户端请求与协调节点

客户端向Elasticsearch集群发送一个写入请求,这个请求可以发送到集群中的任何一个节点。

接收到请求的节点会充当协调节点的角色。协调节点负责处理客户端的请求,并将请求路由到正确的数据节点。

  1. 路由与主分片处理

协调节点会根据文档的_id和索引的设置(如分片数量)来确定文档应该写入到哪个主分片。这是通过一个哈希函数和模运算来实现的,确保同一个_id的文档总是路由到同一个主分片。

确定目标主分片后,协调节点将请求转发给该主分片所在的数据节点。

数据节点上的主分片接收到请求后,会先将文档写入到内存中的Lucene索引结构里。这个过程包括将文档转换成倒排索引的形式,以便后续的搜索和分析。

  1. 数据同步与副本分片

一旦文档被写入到主分片,主分片会开始将数据同步到其对应的副本分片上。这是为了保证数据的冗余和可用性。

副本分片是主分片的完整拷贝,它们可以处理搜索请求并提供数据恢复的能力。当主分片不可用时,副本分片可以被提升为新的主分片。

数据同步是异步进行的,这意味着写入请求在主分片处理完毕后就可以返回给客户端,而不需要等待所有副本分片都完成同步。

  1. 写入确认与响应

当主分片和足够数量的副本分片(根据配置可能是全部或大多数)都成功写入了文档后,协调节点会收到这些分片的确认信息。

一旦收到足够的确认信息,协调节点就会向客户端发送一个成功的响应,表示文档已经被成功写入。

  1. 底层写入机制

在Elasticsearch中,底层写入机制是确保数据可靠、持久化并可以被高效搜索的关键部分:

5.1. 缓冲区(Buffer)和事务日志(Translog)

当文档被写入Elasticsearch时,它们首先被放置在内存中的一个缓冲区中。这个缓冲区是临时的,用于快速接收并处理写入请求。

同时,为了确保数据的持久性和可靠性,每一个写入操作也会被记录到事务日志(Translog)中。Translog是一个追加写入的日志文件,它记录了所有对索引的更改。这种机制类似于数据库中的写前日志(WAL)或重做日志(redo log),用于在系统崩溃后恢复数据。

5.2. 刷新(Refresh)操作

随着时间的推移,缓冲区中的数据会积累到一定量,此时需要将这些数据刷新(refresh)到Lucene的索引中。刷新操作会创建一个新的Lucene段(segment),并将缓冲区中的数据写入这个段中。

Lucene段是不可变的,一旦被写入就不能被修改,这保证了数据的一致性和搜索的高效性。新的段会被添加到索引中,使得新写入的数据可以被搜索到。

刷新操作是周期性的,可以通过配置来控制刷新的频率。频繁的刷新会提高数据的实时性,但也会增加I/O负担和CPU使用率;而较少的刷新则会减少I/O操作,但可能会降低数据的实时性。

5.3. Flush操作

与刷新不同,flush操作会将内存中的数据以及Translog中的更改持久化到磁盘上。这意味着数据被真正写入到了物理存储中,而不仅仅是保存在操作系统的文件系统缓存中。

Flush操作会调用操作系统的fsync函数来确保数据被写入磁盘,并且会清空相关的缓存和文件(如Translog)。这样做可以释放内存空间,并为后续的写入操作做好准备。

Flush操作的频率通常比刷新操作要低得多,因为它涉及到磁盘I/O操作,相对较慢。但是,在Elasticsearch中,flush操作是自动管理的,会根据索引的大小、写入速率和磁盘I/O能力等因素来动态调整。

通过这个底层写入机制,Elasticsearch能够在保证数据可靠性的同时提供高效的搜索和分析功能。缓冲区、事务日志、刷新和flush操作共同协作,确保数据被正确、快速地写入到索引中,并可以被用户查询到。

基于以上这些原理和步骤,Elasticsearch能够实现高效、可靠和可扩展的数据写入功能。

六、es读取数据流程

下面来解释Elasticsearch读取数据的流程,包括其中的关键步骤和涉及的组件。

  1. 客户端发送请求

当用户想要从Elasticsearch中检索数据时,他们会通过Elasticsearch的客户端API发送一个搜索请求。这个请求包含了查询的详细信息,如要搜索的索引、查询类型(如匹配查询、范围查询等)、过滤条件等。

  1. 请求到达协调节点

请求首先到达Elasticsearch集群中的一个节点,这个节点被称为协调节点(Coordinating Node)。协调节点负责接收客户端的请求,处理请求的路由逻辑,并与数据节点(Data Node)进行通信以获取实际的数据。

  1. 解析查询并确定目标分片

协调节点接收到请求后,会解析查询语句,并根据索引的映射(Mapping)和设置(Settings)信息来确定需要查询哪些分片(Shard)。Elasticsearch中的每个索引都被分割成多个分片,并且这些分片可以分布在集群的多个节点上以提高可扩展性和性能。

  1. 将请求转发给数据节点

协调节点根据分片的位置信息将查询请求转发给包含目标分片的数据节点。每个数据节点上都存储着一部分索引的数据,并负责处理与这些数据相关的查询请求。

  1. 在数据节点上执行查询

数据节点接收到查询请求后,会使用Lucene库来执行实际的搜索操作。Lucene是一个高性能、全功能的文本搜索引擎库,它提供了强大的索引和搜索功能。数据节点会根据查询条件在Lucene索引中检索匹配的文档,并生成一个结果集。

6.聚合和排序结果

数据节点将查询结果返回给协调节点。如果查询涉及多个分片,协调节点需要聚合来自不同分片的结果,并根据需要对结果进行排序、分页等处理。这个过程可能需要消耗一定的计算资源,特别是当结果集很大时。

7.返回结果给客户端

一旦结果准备好,协调节点会将它们封装成一个统一的响应格式,并返回给客户端。响应中包含了查询的结果、匹配的文档数量、聚合数据(如果有的话)等信息。客户端可以解析这个响应来获取所需的数据。

七、缓存和优化策略

优化策略

查询缓存:Elasticsearch会对某些查询结果进行缓存,以便快速响应相同的查询请求。这可以减少对Lucene索引的重复访问,提高查询性能。然而,由于缓存空间有限,只有部分查询结果会被缓存。Elasticsearch会对某些查询结果进行缓存以加速重复查询的响应速度。此外,还有分片请求缓存等机制用于减少不必要的计算和I/O操作。

优化策略:为了提高性能,Elasticsearch还提供了多种优化策略,如使用合适的分析器(Analyzer)和查询语句、合理配置索引设置、利用聚合和过滤操作等。这些优化可以减少查询的复杂性和计算开销,提高查询速度和响应时间。

分片请求缓存:数据节点上的分片请求缓存可以存储查询请求的结果。当相同的查询再次到达时,可以直接从缓存中获取结果,而无需再次访问Lucene索引。这有助于减少对磁盘I/O的依赖,提高查询速度。

优化查询语句:为了提高查询性能,用户应该编写高效的查询语句。避免使用高开销的查询操作(如通配符查询、正则表达式查询等),合理使用过滤器和聚合操作,以及优化索引结构都可以帮助提高查询速度。

基于这些详细的步骤和优化策略,Elasticsearch能够高效地处理读取数据的请求,并提供快速、准确的结果给用户。

所以,Elasticsearch的架构结合了分布式处理、索引与搜索技术、数据写入与持久化机制以及缓存与性能优化策略等多个方面来实现高效、可靠的数据存储和检索功能。

性能优化

存储设备

磁盘在现代服务器上通常都是瓶颈。Elasticsearch 重度使用磁盘,你的磁盘能处理的吞吐量越大,你的节点就越稳定。

这里有一些优化磁盘 I/O 的技巧:

  • 使用 SSD。就像其他地方提过的, 他们比机械磁盘优秀多了。
  • 使用 RAID 0。条带化 RAID 会提高磁盘 I/O,代价显然就是当一块硬盘故障时整个就故障了。不要使用镜像或者奇偶校验 RAID 因为副本已经提供了这个功能。
  • 另外,使用多块硬盘,并允许 Elasticsearch 通过多个 path.data 目录配置把数据条带化分配到它们上面。
  • 不要使用远程挂载的存储,比如 NFS 或者 SMB/CIFS。这个引入的延迟对性能来说完全是背道而驰的。
  • 如果你用的是 EC2,当心 EBS。即便是基于 SSD 的 EBS,通常也比本地实例的存储要慢。
内部索引优化

Elasticsearch 为了能快速找到某个 Term,先将所有的 Term 排个序,然后根据二分法查找 Term,时间复杂度为 logN,就像通过字典查找一样,这就是 Term Dictionary。

现在再看起来,似乎和传统数据库通过 B-Tree 的方式类似。但是如果 Term 太多,Term Dictionary 也会很大,放内存不现实,于是有了 Term Index。

就像字典里的索引页一样,A 开头的有哪些 Term,分别在哪页,可以理解 Term Index是一棵树。

这棵树不会包含所有的 Term,它包含的是 Term 的一些前缀。通过 Term Index 可以快速地定位到 Term Dictionary 的某个 Offset,然后从这个位置再往后顺序查找。

在内存中用 FST 方式压缩 Term Index,FST 以字节的方式存储所有的 Term,这种压缩方式可以有效的缩减存储空间,使得 Term Index 足以放进内存,但这种方式也会导致查找时需要更多的 CPU 资源。

对于存储在磁盘上的倒排表同样也采用了压缩技术减少存储所占用的空间。

调整配置参数

调整配置参数建议如下:

  • 给每个文档指定有序的具有压缩良好的序列模式 ID,避免随机的 UUID-4 这样的 ID,这样的 ID 压缩比很低,会明显拖慢 Lucene。
  • 对于那些不需要聚合和排序的索引字段禁用 Doc values。Doc Values 是有序的基于 document=>field value 的映射列表。
  • 不需要做模糊检索的字段使用 Keyword 类型代替 Text 类型,这样可以避免在建立索引前对这些文本进行分词。
  • 如果你的搜索结果不需要近实时的准确度,考虑把每个索引的 index.refresh_interval 改到 30s 。
  • 如果你是在做大批量导入,导入期间你可以通过设置这个值为 -1 关掉刷新,还可以通过设置 index.number_of_replicas: 0 关闭副本。别忘记在完工的时候重新开启它。
  • 避免深度分页查询建议使用 Scroll 进行分页查询。普通分页查询时,会创建一个 from+size 的空优先队列,每个分片会返回 from+size 条数据,默认只包含文档 ID 和得分 Score 给协调节点。
  • 如果有 N 个分片,则协调节点再对(from+size)×n 条数据进行二次排序,然后选择需要被取回的文档。当 from 很大时,排序过程会变得很沉重,占用 CPU 资源严重。
  • 减少映射字段,只提供需要检索,聚合或排序的字段。其他字段可存在其他存储设备上,例如 Hbase,在 ES 中得到结果后再去 Hbase 查询这些字段。
  • 创建索引和查询时指定路由 Routing 值,这样可以精确到具体的分片查询,提升查询效率。路由的选择需要注意数据的分布均衡。
JVM 调优

JVM 调优建议如下:

  • 确保堆内存最小值( Xms )与最大值( Xmx )的大小是相同的,防止程序在运行时改变堆内存大小。

Elasticsearch 默认安装后设置的堆内存是 1GB。可通过 ../config/jvm.option 文件进行配置,但是最好不要超过物理内存的50%和超过 32GB。

  • GC 默认采用 CMS 的方式,并发但是有 STW 的问题,可以考虑使用 G1 收集器。
  • ES 非常依赖文件系统缓存(Filesystem Cache),快速搜索。一般来说,应该至少确保物理上有一半的可用内存分配到文件系统缓存。

总结

Elasticsearch的读写流程是一个精心设计的、分布式的处理过程。在写入数据时,它通过缓冲区、事务日志、刷新和flush操作等机制,确保了数据的可靠性与持久性。同时,借助Lucene的强大索引能力,将文档快速转换成可被搜索的形式。在读取数据时,Elasticsearch利用协调节点将请求路由到正确的数据节点,利用Lucene进行高效检索,并聚合、排序结果,最终返回给客户端。这一流程结合了缓存、优化查询语句和分布式处理等技术,确保了查询的高性能与低延迟。通过这些设计,Elasticsearch为企业和开发者提供了强大而灵活的数据存储与检索解决方案。

相关推荐
材料苦逼不会梦到计算机白富美34 分钟前
golang分布式缓存项目 Day 1
分布式·缓存·golang
拓端研究室TRL38 分钟前
【梯度提升专题】XGBoost、Adaboost、CatBoost预测合集:抗乳腺癌药物优化、信贷风控、比特币应用|附数据代码...
大数据
黄焖鸡能干四碗41 分钟前
信息化运维方案,实施方案,开发方案,信息中心安全运维资料(软件资料word)
大数据·人工智能·软件需求·设计规范·规格说明书
想进大厂的小王1 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
Java 第一深情1 小时前
高性能分布式缓存Redis-数据管理与性能提升之道
redis·分布式·缓存
编码小袁1 小时前
探索数据科学与大数据技术专业本科生的广阔就业前景
大数据
WeeJot嵌入式2 小时前
大数据治理:确保数据的可持续性和价值
大数据
晨欣2 小时前
Elasticsearch和Lucene之间是什么关系?(ChatGPT回答)
elasticsearch·chatgpt·lucene
ZHOU西口2 小时前
微服务实战系列之玩转Docker(十八)
分布式·docker·云原生·架构·数据安全·etcd·rbac
zmd-zk2 小时前
kafka+zookeeper的搭建
大数据·分布式·zookeeper·中间件·kafka