Elasticsearch核心技术(二):深入讲解

ES是基于Lucene分布式搜索服务,可以存储整个对象或文档。主要用于大量数据的索引查询。

Elasticsearch索引的精髓:一切设计都是为了提高搜索的性能
以空间换时间。

1. 简介

1.1 优点

  1. 高性能:搜索和分析很快,涵盖了多种查询语句和数据结构。
  2. 支持横向扩展:通过增加结点数量扩展搜索和分析能力。可以扩展到上百台服务器,处理PB级结构化或非结构化数据。
  3. 实时( NRT,near real time)检索:Elasticsearch是一个接近实时的搜索平台,从建立索引,到这个索引可以被搜索只需要很小的延迟,通常是1秒。可以进行refresh参数的调整来缩短这个延迟时间。
  4. 高可用性、可靠性:支持数据备份和恢复。
  5. 全文索引:把文档转换成可搜索的结构化数据。

1.2 场景

  1. 搜索引擎
    快速检索文档、商品、新闻。
  2. 日志分析
    通过分析日志数据,了解性能、监控变化。
  3. 数据分析;

1.3 常见概念

索引文件(index)–>数据库
一个拥有几分相似特征的文档的集合。

文档对象(document)–>行
一个文档是一个可被索引的基础信息单元。

类型(type)–>表
每一批索引中,利用type定义海量的document为同一种类型。是逻辑分类。

字段域(field)–>列

映射(mapping)
就是对索引库中索引的字段名及其数据类型进行定义。

分片(shards)
一个分片本身就是一个完整的搜索引擎。
文档存储在分片中,而分片则会被分配到集群中节点中,随着集群的扩大和缩小,es会自动地将分片在节点之间进行迁移,以保证集群能保持一种平衡。
每个index可以分成多个shards,一个index也可以被复制0次或多次。一旦复制了,每个索引就有了主分片和复制分片。分片和复制的数量可以在索引创建的时候指定。在索引创建之后,可以在任何时候动态地改变复制的数量,但是不能再改变分片的数量。

副本(replica)
冗余备份,防止数据丢失。
每个分片有多个副本,这些副本称为replication group,在添加或删除文档时这些副本也必须保持同步,否则在数据读取时就会出现数据紊乱,保持分片副本的同步并从中提供读取的过程就是我们所说的data replication model。

集群(cluster)
一个集群就是由一个或多个节点组织在一起, 这些节点共同持有全部的索引数据, 并共同提供索引和搜索功能。
一个集群由一个唯一的名字标识(默认就是“elasticsearch”)。
一个节点只能通过指定某个集群的名字,来加入这个集群。一个集群中只包含一个节点是合法的。另外,你也可以拥有多个集群,集群以名字区分。

节点(node)
一个节点是你集群中的一个服务器,作为集群的一部分,它存储你的数据,参与集群的索引和搜索功能。
每一个节点都有一个唯一的UUID标识。

master节点
集群中的一个节点会被选为master节点,它将负责管理集群范畴的变更,例如创建或删除索引,添加节点到集群或从集群删除节点。master节点无需参与文档层面的变更和搜索,这意味着仅有一个master节点并不会因流量增长而成为瓶颈。任意一个节点都可以成为 master 节点

data节点
持有数据和倒排索引。默认情况下,每个节点都可以通过设定配置文件elasticsearch.yml中的node.data属性为true(默认)成为数据节点。如果需要一个专门的主节点,应将其node.data属性设置为false

2. 检索原理

  1. 用户将数据提交到Elastic Search 数据库中
  2. 通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据
  3. 用户搜索数据时候,再根据权重将结果排名,打分,再将返回结果呈现给用户

2.1 为什么快?

空间换时间。

  1. 通过分布式部署,将数据存储在多个节点。
  2. 索引分片,查询可并发操作。
  3. 把每个索引划分成多个分片,可以让查询并行进行。
  4. 倒排索引(核心):将文档中的每个词与该词出现在哪些文档中进行映射并储存。检索时通过倒排索引快速找到包含所有搜索次的文档,不需要重新计算。
  5. 索引优化:支持索引覆盖、索引下推等优化技术。快速通过跳表、Bitmap索引等组织检索,通过联合索引定位范围。
  6. 异步请求处理。请求到达时立刻返回部分结果。
  7. 内存存储:读写数据时减少磁盘访问次数。
  8. 压缩:通过压缩技术尽可能的在内存中多放数据。

2.2 正向索引(正排索引)

从文档中查找字符串。关系型数据库使用的是正向索引。

2.3 反向索引(倒排索引)

从字符串查找文档。搜索引擎lucene使用的是反向索引。

假设有这么几条数据:

IDNameAgeSex
1Kate24Female
2John24Male
3Bill29Male

ID是Elasticsearch自建的文档id,那么Elasticsearch建立的索引如下:

Name:

TermPosting List
Kate1
John2
Bill3

Age:

TermPosting List
24[1,2]
293

Sex:

TermPosting List
Female1
Male[2,3]

Lucene的倒排索引(Inverted Index)是先在内存里生成,然后定期以段文件(segment file)的形式刷到磁盘的。

往Elasticsearch里插入一条记录,其实就是直接PUT一个json的对象,这个对象有多个fields,比如上面例子中的name, sex, age, about, interests,那么在插入这些数据到Elasticsearch的同时,Elasticsearch为这些字段建立索引–倒排索引,因为Elasticsearch最核心功能是搜索。

Elasticsearch使用的倒排索引比关系型数据库的B-Tree索引快,为什么呢?
二叉树查找效率是logN,同时插入新的节点不必移动全部节点,所以用树型结构存储索引,能同时兼顾插入和查询的性能。因此在这个基础上,再结合磁盘的读取特性(顺序读/随机读),传统关系型数据库采用了B-Tree/B+Tree这样的数据结构。为了提高查询的效率,减少磁盘寻道次数,将多个值作为一个数组通过连续区间存放,一次寻道读取多个数据,同时也降低树的高度。

2.4 es索引

2.4.1 Posting List

Elasticsearch分别为每个field都建立了一个倒排索引,Kate, John, 24, Female这些叫term,而[1,2]就是Posting List。Posting list就是一个int的数组,存储了所有符合某个term的文档id。
通过posting list这种索引方式似乎可以很快进行查找,比如要找age=24对应id是1,2的同学。但是,如果这里有上千万的记录呢?如果是想通过name来查找呢?

2.4.2 Term Dictionary

Elasticsearch为了能快速找到某个term,将所有的term排个序,二分法查找term,logN的查找效率,就像通过字典查找一样,这就是Term Dictionary。现在再看起来,似乎和传统数据库通过B-Tree的方式类似啊,为什么说比B-Tree的查询快呢?

2.4.3 Term Index

B-Tree通过减少磁盘寻道次数来提高查询性能,Elasticsearch也是采用同样的思路,直接通过内存查找term,不读磁盘,但是如果term太多,term dictionary也会很大,放内存不现实,于是有了Term Index,就像字典里的索引页一样,A开头的有哪些term,分别在哪页,可以理解term index是一颗树:


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


所以term index不需要存下所有的term,而仅仅是他们的一些前缀与Term Dictionary的block之间的映射关系,再结合FST(Finite State Transducers)的压缩技术,可以使term index缓存到内存中。从term index查到对应的term dictionary的block位置之后,再去磁盘上找term,大大减少了磁盘随机读的次数。

FSTs are finite-state machines that map a term (byte sequence) to an arbitrary output.

假设我们现在要将mop, moth, pop, star, stop and top(term index里的term前缀)映射到序号:0,1,2,3,4,5(term dictionary的block位置)。最简单的做法就是定义个Map<string, integer=“”>,大家找到自己的位置对应入座就好了,但从内存占用少的角度想想,有没有更优的办法呢?答案就是:FST。

⭕️表示一种状态

–>表示状态的变化过程,上面的字母/数字表示状态变化和权重

将单词分成单个字母通过⭕️和–>表示出来,0权重不显示。如果⭕️后面出现分支,就标记权重,最后整条路径上的权重加起来就是这个单词对应的序号。
FST以字节的方式存储所有的term,这种压缩方式可以有效的缩减存储空间,使得term index足以放进内存,但这种方式也会导致查找时需要更多的CPU资源。

2.4.4 压缩技巧

Elasticsearch里除了上面说到用FST压缩term index外,对posting list也有压缩技巧。
如果Elasticsearch需要对同学的性别进行索引,会怎样?如果有上千万个同学,而世界上只有男/女这样两个性别,每个posting list都会有至少百万个文档id。 Elasticsearch是如何有效的对这些文档id压缩的呢?

增量编码压缩,将大数变小数,按字节存储

首先,Elasticsearch要求posting list是有序的(为了提高搜索的性能,再任性的要求也得满足),这样做的一个好处是方便压缩,看下面这个图例:

原理就是通过增量,将原来的大数变成小数仅存储增量值,再精打细算按bit排好队,最后通过字节存储,而不是即使是2也用int(4个字节)来存储。

2.4.5 Roaring bitmaps

说到Roaring bitmaps,就必须先从bitmap说起。Bitmap是一种数据结构,假设有某个posting list:[1,3,4,7,10]
对应的bitmap就是:[1,0,1,1,0,0,1,0,0,1]
非常直观,用0/1表示某个值是否存在,比如10这个值就对应第10位,对应的bit值是1,这样用一个字节就可以代表8个文档id,旧版本(5.0之前)的Lucene就是用这样的方式来压缩的,但这样的压缩方式仍然不够高效,如果有1亿个文档,那么需要12.5MB的存储空间,这仅仅是对应一个索引字段(我们往往会有很多个索引字段)。于是有人想出了Roaring bitmaps这样更高效的数据结构。

Bitmap的缺点是存储空间随着文档个数线性增长,Roaring bitmaps需要打破这个魔咒就一定要用到某些指数特性:

将posting list按照65535为界限分块,比如第一块所包含的文档id范围在0~65535之间,第二块的id范围是65536~131071,以此类推。再用<商,余数>的组合表示每一组id,这样每组里的id范围都在0~65535内了,剩下的就好办了,既然每组id不会变得无限大,那么我们就可以通过最有效的方式对这里的id存储。

“为什么是以65535为界限?”
程序员的世界里除了1024外,65535也是一个经典值,因为它=2^16-1,正好是用2个字节能表示的最大数,一个short的存储单位,注意到上图里的最后一行“If a block has more than 4096 values, encode as a bit set, and otherwise as a simple array using 2 bytes per value”,如果是大块,用节省点用bitset存,小块就豪爽点,2个字节我也不计较了,用一个short[]存着方便。

那为什么用4096来区分大块还是小块呢?
4096*2bytes = 8192bytes < 1KB, 磁盘一次寻道可以顺序把一个小块的内容都读出来,再大一位就超过1KB了,需要两次读。

2.4.6 联合索引

上面说了半天都是单field索引,如果多个field索引的联合查询,倒排索引如何满足快速查询的要求呢?

1. 利用跳表(Skip list)的数据结构快速做“与”运算

2. 利用上面提到的bitset按位“与”

先看看跳表的数据结构:

将一个有序链表level0,挑出其中几个元素到level1及level2,每个level越往上,选出来的指针元素越少,查找时依次从高level往低查找,比如55,先找到level2的31,再找到level1的47,最后找到55,一共3次查找,查找效率和2叉树的效率相当,但也是用了一定的空间冗余来换取的。

假设有下面三个posting list需要联合索引:

如果使用跳表,对最短的posting list中的每个id,逐个在另外两个posting list中查找看是否存在,最后得到交集的结果。
如果使用bitset,就很直观了,直接按位与,得到的结果就是最后的交集。

2.5 Elasticsearch的索引思路

将磁盘里的东西尽量搬进内存,减少磁盘随机读取次数(同时也利用磁盘顺序读特性),结合各种奇技淫巧的压缩算法,用及其苛刻的态度使用内存。

所以,对于使用Elasticsearch进行索引时需要注意:

  1. 不需要索引的字段,一定要明确定义出来,因为默认是自动建索引的
  2. 同样的道理,对于String类型的字段,不需要analysis的也需要明确定义出来,因为默认也是会analysis的
  3. 选择有规律的ID很重要,随机性太大的ID(比如java的UUID)不利于查询

关于最后一点,压缩算法等都是对Posting list里的大量ID进行压缩的,那如果ID是顺序的,或者是有公共前缀等具有一定规律性的ID,压缩比会比较高;另外,通过Posting list里的ID到磁盘中查找Document信息时,因为Elasticsearch是分Segment存储的,根据ID这个大范围的Term定位到Segment的效率直接影响了最后查询的性能,如果ID是有规律的,可以快速跳过不包含该ID的Segment,从而减少不必要的磁盘读次数。

3. 顺序扫描法和全文检索

Lucene是一个高效的,基于Java的全文检索库;开源免费。

3.1 顺序扫描法

遍历所有文档,通常应用于数据量较小的场景,比如经常使用的grep命令就是这种查找方式。

全文检索和顺序扫描的区别在于:顺序扫描每次都必须从头至尾扫描全部数据,但全文检索的索引创建过程只需一次,以后查询无需再次创建,但却可以一直为搜索所用。

3.2 全文检索

从非结构化数据中抽取部分结构化数据,并建立索引,再对索引进行搜索的过程,我们成为全文索引。

3.2.1 索引创建

分词处理

将文档交给切词组件(Tokenizer)进行切词处理。

对文档进行粉刺,并过滤无用分词(如标点符号、语气助词)。

自然语言同化处理

将得到的词元(Token)交给语言处理组件。

将分词转换为同一种表达方式(如students,Student,stodent 转换为student)。

创建词典

将得到的词(Term)交给索引组件。

索引组件主要做以下几件事情:

  1. 利用得到的词创建一个词典;
  2. 对字典按字母顺序进行排序;
  3. 合并相同的词并建立倒排链表。
建立索引

由索引组件针对词建立索引。

3.2.2 搜索索引

搜索可以分为以下几步:

输入

用户输入查询语句

自然语言处理

对查询语句进行词法分析、语法分析、语言处理。

  1. 词法分析主要用来识别单词和关键字;
  2. 语言分析主要根据查询语句来生成一颗语法树;
    受限进行语法判断,如果不满足语法要求,则直接报错;若满足语法要求,则可以生成下面的语法树;
  3. 语言同化处理

搜索索引

搜索索引,得到符合语法树的文档。

比如说我们要查找同时包含lucene和hadoop的文档,我们只需以下几步:

  1. 取出包含lucene的文档链表
  2. 取出包含hadoop的文档链表
  3. 合并链表,取出交集。

比如在反向索引链表里,找到包含lucene、learn和hadoop得文档,需要做如下几步:

  1. 取出包含lucene的文档链表
  2. 取出包含hadoop的文档链表
  3. 合并链表,取出交集。
  4. 将得到交集链表与hadoop得文档链表进行差集操作,去除包含hadoop得文档。
排序

根据得到的文档和查询语句相关性,对结果进行排序予以展示。

4. 分布式

Lucene是一个开源的分部署搜索引擎的实现,ElasticSearch底层就是基于Lucene实现的,之所以能够实现分布式搜索,原因就在于每一个文档在出库入库之前,就已经被进行切词分析处理,针对每个词建立索引,是一种空间换时间的做法。

基于路由机制,索引操作将被定向到主分片上并执行,在主分片完成操作后,如果需要,再将更新操作分发到副本分片上。

4.1 Wait For Active Shardsedit

为了提高写入系统的 resiliency(弹性),索引操作可以配置为在继续索引之前等待一定数量的活动副本分片,如果所需的活动副本分片数没有达到指定数量,那么写入操作必须等待并且重试,直到必需的副本分片数已启动,或者发生了超时为止。

在默认情况下,只需要主分片处于活动状态,写操作就会继续,开发者可以通过设置 index.write.wait_for_active_shards来动态地在索引设置中覆盖此默认值。要只是需要更改每个操作的此行为,则可以使用 wait_for_active_shards请求参数,参数有效值是 all或任何不大于副本分片数的正整数,如果指定负值或者大于副本分片数的数字将抛出错误。

例如,假设我们有一个集群,该集群有三个节点A,B和C,我们创建一个索引,索引副本数设置为3。默认情况下,索引操作将仅确保每个分片的主副本在操作之前可用。这意味着,即使B和C下线了,只要A托管了主副本分片,索引操作仍然执行。如果请求设置 wait_for_active_shards为3(并且3个节点都已启动),则索引操作将在执行之前需要3个活动副本分片,这是必须满足的要求,因为在集群有3个活动节点,每个节点有一个分片的副本。但是,如果我们将 wait_for_active_shards设置为 all(即4),索引操作将不会执行,因为索引中的每个分片的4没有四个副本,那么该操作将超时,除非在集群中启动新节点以托管分片的第四个副本。

重要的是要注意,这个设置极大地减少了写操作不写入所需数量的副本分片的可能性,但是它不能完全消除这种可能性,因为这种检查在写操作开始之前发生,一旦写操作正在进行,复制仍然可能在任意数量的副本分片上失败,但在主分片上成功。写操作响应的 _shard字段显示复制成功/失败的副本分片的数量。

4.2 Noop Updates

当使用索引API更新文档时,即使文档没有更改,也始终创建新版本的文档。如果这不可接受,请使用将 detectnoop设置为true的update API 。此选项在索引API上不可用,因为索引api无法提取旧的文档,当然也无法和新的文档进行比较。

4.3 Timeout

执行索引操作时分配的主分片可能不可用,原因各种个样,此时,索引操作将在主分片上等待最多1分钟,然后失败并响应错误。 timeout参数可以用于显式指定等待时间。以下是将其设置为5分钟的示例:

 curl -X PUT "localhost:9200/twitter/_doc/1?timeout=5m" -H 'Content-Type: application/json' -d'
    {
        "user" : "kimchy",
        "post_date" : "2009-11-15T14:12:12",
        "message" : "trying out Elasticsearch"
    }

5. 实时搜索

es和磁盘之间有一层FileSystem Cache的系统缓存,造就了es有更快的搜索响应能力。

es慢的瓶颈——磁盘搜索:

一个index是由若干个segment组成,随着每个segment的不断增长,我们索引一条数据后可能要经过分钟级别的延迟才能被搜索,为什么有种这么大的延迟,这里面的瓶颈点主要在磁盘。

持久化一个segment需要fsync操作用来确保segment能够物理的被写入磁盘以真正的避免数据丢失,但是fsync操作比较耗时,所以它不能在每索引一条数据后就执行一次,如果那样索引和搜索的延迟都会非常之大。

fsync函数同步内存中所有已修改的文件数据到储存设备。

5.1 FileSystem Cache

es新增的document会被收集到 indexing buffer 区后被重写成一个segment然后直接写入filesystem cache中,这个操作是非常轻量级的,相对耗时较少,之后经过一定的间隔或外部触发后才会被flush到磁盘上,这个操作非常耗时。

但只要sengment文件被写入cache后,这个sengment就可以打开和查询,从而确保在短时间内就可以搜到,而不用执行一个full commit也就是fsync操作,这是一个非常轻量级的处理方式而且是可以高频次的被执行,而不会破坏es的性能。

5.2 refresh

在elasticsearch里面,这个轻量级的写入和打开一个cache中的segment的操作叫做refresh,默认情况下,es集群中的每个shard会每隔1秒自动refresh一次,这就是我们为什么说es是近实时的搜索引擎而不是实时的,也就是说给索引插入一条数据后,我们需要等待1秒才能被搜到这条数据,这是es对写入和查询一个平衡的设置方式,这样设置既提升了es的索引写入效率同时也使得es能够近实时检索数据。

POST /_refresh   //刷新所有的索引
POST /blogs/_refresh  //刷新指定的索引

refresh操作相比commit操作是非常轻量级的但是它仍然会耗费一定的性能,所以不建议在每插入一条数据后就执行一次refresh命令,es默认的1秒的延迟对于大多数场景基本都可以接受。

当然并不是所有的业务场景都需要每秒都refresh一次,如果你短时间内要索引大量的数据,为了优化索引的写入速度,我们可以设置更大的refresh间隔,从而提升写入性能,命令如下:

    PUT /my_logs
    {
      "settings": {
        "refresh_interval": "30s" 
      }
    }

上面的参数是可以随时动态的设置到一个存在的索引里面,如果我们正在插入超大索引时,我们完全可以先关闭掉这个refresh机制,等写入完毕之后再重新打开,这样以来就能大大提升写入速度。

PUT /my_logs/_settings
{ "refresh_interval": -1 }  //禁用刷新机制
 
PUT /my_logs/_settings
{ "refresh_interval": "1s" }  //设置每秒刷新一次

注意refresh_interval的参数是可以带时间周期的,如果你只写了个1,那就代表每隔1毫秒刷新一次索引,所以设置这个参数时务必要谨慎。

6. 故障转移机制

6.1 复制分片

Elasticsearch允许创建分片的一份或多份拷贝,这些拷贝叫做复制分片(复制)。

复制之所以重要,有两个主要原因:

  1. 在分片/节点失败的情况下,复制提供了高可用性。复制分片不与原/主要分片置于同一节点上是非常重要的。
  2. 因为搜索可以在所有的复制上并行运行,复制可以扩展你的搜索量/吞吐量

在Elasticsearch7.0版本之前,默认情况下,Elasticsearch中的每个索引分配5个主分片和1个复制。这意味着,如果你的集群中至少有两个节点,你的索引将会有5个主分片和另外5个复制分片(1个完全拷贝),这样每个索引总共就有10个分片(根据官方文档,在7.0版本上,默认的分片数量会有所变化)。

Elasticsearch的数据复制模型基于 主-备模型,在这个模型下,分片分为主分片和副本分片,主分片是所有索引操作的主要入口点,它负责验证并确保所有操作是正确的,一旦主分片接受了索引操作,主分片在索引操作执行成功后还要负责将操作复制到其他副本。

7. 读写模型

7.1 写模型

Elasticsearch中的每个索引操作首先通过路由解析到replication group,这一操作通常基于文档ID,一旦replication group被确定后,索引操作将在内部转发到replication group的当前主分片上,主分片将负责验证操作并将操作转发到其他副本。由于副本可以离线,因此不需要将主分片复制到所有副本,Elasticsearch会维护一个应该接收操作的分片副本列表,这个列表称为同步副本并由主节点维护。顾名思义,这些是“好”分片副本的集合。

主分片遵循以下基本流程:

  • 验证输入操作并在结构无效时拒绝它(例如:想要一个数字结果给了一个对象)
  • 先在本地执行操作,例如索引或删除相关文档,如果执行出错时也将拒绝(例如:关键字值太长,无法在Lucene中进行索引)
  • 将操作转发到当前同步副本集中的每个副本。如果有多个副本,则并行执行该操作
  • 一旦所有副本成功执行了操作并响应给主服务器,主服务器就会确认成功完成对客户端的请求

7.1.1 故障处理

在索引的过程可能会出现各种各样的异常情况,例如:1.磁盘损坏;2.节点相互断开连接;3.由于配置错误导致复制副本上的操作失败,尽管它在主服务器上操作成功,等等。虽然这些问题并不一定常见,但是开发者还是有必要作出相应的预案。

在主分片本身发生故障的情况下,托管主分片的节点将向Master发送有关它的消息,此时索引操作将等待(默认情况下最多1分钟),以便Master将其中一个副本提升为新主分片,然后,该操作将被转发到新的主分片处理。

请注意,Master还会监控节点的运行状况,并可能决定主动对主分片进行降级(这通常是由于网络问题导致的)。一旦在主分片上成功执行了操作,主分片就必须处理在副本上执行操作时存在的潜在故障,这些潜在的故障可能是由副本上的实际故障或由于网络问题导致操作无法到达副本(或阻止副本响应)引起的。所有这些都具有相同的最终结果:同步副本集中的一部分副本错过了即将被确认的操作。此时,主分片向Master发送消息,请求从同步副本集中删除有问题的分片。只有在Master确认删除了分片后,主分片才会确认操作。注意,Master还将指示另一个节点开始构建新的分片副本,以便将系统还原到正常状态。

在将操作转发到副本时,主分片将使用副本来验证它仍然是活动主分片。如果主分片由于网络原因(或长GC)而被分离,它依然可能会在被降级之前继续处理传入的索引操作,此时副本将拒绝来自旧主分片的操作。主分片收到副本的拒绝请求后会请求Master节点,Master会告诉旧的主分片你已经被替换掉,然后操作会被路由到新的主分片。

7.1.2 如果没有副本会怎么样

由于索引的配置原因或者所有副本都已失效,在这种情况下,会发生主分片没有副本。此时,主分片处理操作而没有任何外部验证,这可能看起来有问题。另一方面,主分片本身不能使其他分片失效,但可以请求Master代表它执行此操作。这意味着Master知道主分片是唯一的好副本。因此,我们保证Master不会将任何其他(过时的)分片副本提升为新的主分片,并且任何索引到主分片的操作都不会丢失。当然,由于此时我们只使用单个数据副本运行,因此物理硬件问题可能导致数据丢失。

7.2 读模型

Elasticsearch中的读取操作,可以是按照ID查找这种非常轻量级的操作,也可以是具有复杂聚合的大量搜索请求,这些聚合操作会占用非常大的CPU算力。 主-备模型的优点之一是它使所有分片副本保持一致(除了飞行中的操作)。基于此,单个同步的副本足以处理读取请求。

当节点收到读取请求时,该节点负责将其转发到保存相关分片的节点,整理响应并对客户端做出响应。此时,我们将该节点称为该请求的协调节点,该节点的基本工作流程如下:

  • 对读取请求进行解析,然后将请求分发到不同分片上。请注意,由于大多数搜索请求将被发送到一个或多个索引,因此它们通常需要从多个分片中读取,每个分片代表数据的不同子集。
  • 从replication group中选择每个相关分片的可用副本,可以是主分片或副本。默认情况下,Elasticsearch将简单地在分片副本之间循环。
  • 将分片级读取请求发送到所选副本。
  • 整合请求结果并给客户端作出响应,注意,在通过ID查找的情况下,只有一个分片是相关的,并且可以跳过此步骤(即不需要整合请求结果,用过MyCat的读者,可能会发现这个步骤的作用和MyCat比较类似)。

7.2.1 故障处理

当分片无法响应读取请求时,协调节点将从同一复制组中选择另一个副本,并将分片级别搜索请求发送到该副本,不过要是重复失败可能导致没有可用的分片副本。在某些情况下,例如search请求中,Elasticsearch更愿意快速响应,而不是等待问题得到解决(此时虽然只有部分结果,部分结果会在shards中指出)。

8. 版本控制(乐观锁)

数据库锁有悲观锁和乐观锁之分:

  • 悲观锁,顾名思义就是很悲观,每次操作数据时都认为数据也会被其他线程修改,因此屏蔽一切有可能破坏数据完整性的操作,在传统的关系型数据库中,常见的行锁、表锁、读/写锁都用到了这种锁机制。
  • 乐观锁,顾名思义就是很乐观,认为每一次的数据操作都不会发生并发访问冲突,因此不会锁定要操作的数据资源,只是在每次提交时检查操作是否违反了数据完整性,Elasticsearch中就是采用了这种锁机制,使用乐观锁的一个好处是可以提高系统的吞吐量。

9. 路由机制

Elasticsearch是一个分布式系统,当一个文档要被索引时,该文档会被索引到系统中的某一个分片上,那么到底是哪一个分片呢?

分片位置的计算公式如下:position=hash(routing) % numberofprimary_shards

routing是一个任意字符串,Elasticsearch默认是将文档的id作为routing值,通过hash函数计算后,生成一个数字,这个数字再和主分片的总数取余,得到一个处于 [0,number_of_primary_shards-1]区间内的数,该数字就是该文档所在的分片。基于这样的映射模式,Elasticsearch不支持索引创建成功后,修改分片数量,即分片数量要一开始就确定好,以后不能修改,否则会导致之前计算出来的position失效(即查找时找不到之前的文档,因此numberofprimary_shards已经变了)。

默认情况下,这种路由机制会通过id将文档平均分配在所有的分片上,这也导致了Elasticsearch无法确定一个文档的具体位置,当有查询请求时,它需要将查询请求广播到所有分片上去执行,这无疑降低的查询的效率,对于这个问题,读者可以使用自定义路由模式去解决,如下请求:

curl -X POST "localhost:9200/twitter/_doc/1?pretty&routing=sang" -H 'Content-Type: application/json' -d'
{
    "user" : "kimchy",
    "post_date" : "2009-11-15T14:12:12",
    "message" : "trying out Elasticsearch"
}

开发者在添加文档时指定路由,在查询的时候也指定路由,这样就可以避免Elasticsearch向所有的分片发送查询请求,减少系统资源的消耗,查询请求如下:

    curl -X GET "localhost:9200/twitter/_search?pretty&routing=sang"

不过这种方式又会带来另外一个问题,即路由相同的文档总是被分在同一个分片上,无法做到将文档平均分配在不同的分片上,因此,两种不同的方式,需要读者在开发中根据实际需求进行取舍。

10. 优化

10.1 内存占用优化

es是JAVA应用,底层存储引擎是基于Lucene的。

官方建议的heap size不要超过系统可用内存的一半。heap以外的内存并不会被浪费,os会利用他们来cache被用读取过的段文件。

10.2 java优化

es使用过程中避免jvm频繁gc:

应用层面生成大量长生命周期的对象,是给heap造成压力的主要原因,例如读取一大片数据在内存中进行排序,或者在heap内部建cache缓存大量数据。如果GC释放的空间有限,而应用层面持续大量申请新对象,GC频度就开始上升,同时会消耗掉很多CPU时间。严重时可能恶性循环,导致整个集群停工。

遵从官方的建议,将xms和xmx设置成和heap一样大小,避免动态分配heap size就好了。有针对性的调整JVM参数可以带来些许GC效率的提升。

10.3 Lucene优化

Lucene的倒排索引(Inverted Index)是先在内存里生成,然后定期以段文件(segment file)的形式刷到磁盘的。

每个段实际就是一个完整的倒排索引,并且一旦写到磁盘上就不会做修改。 API层面的文档更新和删除实际上是增量写入的一种特殊文档,会保存在新的段里。不变的段文件易于被操作系统cache,热数据几乎等效于内存访问。

10.4 es耗内存操作

segment memory

一个segment是一个完备的lucene倒排索引,而倒排索引是通过词典 (Term Dictionary)到文档列表(Postings List)的映射关系,快速做查询的。 由于词典的size会很大,全部装载到heap里不现实,因此Lucene为词典做了一层前缀索引(Term Index),这个索引在Lucene4.0以后采用的数据结构是FST (Finite State Transducer)。 这种数据结构占用空间很小,Lucene打开索引的时候将其全量装载到内存中,加快磁盘上词典查询速度的同时减少随机磁盘访问次数。

ES的data node存储数据并非只是耗费磁盘空间的,为了加速数据的访问,每个segment都有会一些索引数据驻留在heap里。因此segment越多,瓜分掉的heap也越多,并且这部分heap是无法被GC掉的! 理解这点对于监控和管理集群容量很重要,当一个node的segment memory占用过多的时候,就需要考虑删除、归档数据,或者扩容了。

怎么知道segment memory占用情况呢? CAT API可以给出答案。

1. 查看一个索引所有segment的memory占用情况:

2. 查看一个node上所有segment占用的memory总和:

那么有哪些途径减少data node上的segment memory占用呢? 总结起来有三种方法:

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

filter cache (5.x里叫做Request cache)

Filter cache是用来缓存使用过的filter的结果集的,需要注意的是这个缓存也是常驻heap,在被evict掉之前,是无法被GC的。我的经验是默认的10% heap设置工作得够好了,如果实际使用中heap没什么压力的情况下,才考虑加大这个设置。

field data cache

在有大量排序、数据聚合的应用场景,可以说field data cache是性能和稳定性的杀手。 对搜索结果做排序或者聚合操作,需要将倒排索引里的数据进行解析,按列构造成docid->value的形式才能够做后续快速计算。
对于数据量很大的索引,这个构造过程会非常耗费时间,因此ES 2.0以前的版本会将构造好的数据缓存起来,提升性能。但是由于heap空间有限,当遇到用户对海量数据做计算的时候,就很容易导致heap吃紧,集群频繁GC,根本无法完成计算过程。
ES2.0以后,正式默认启用Doc Values特性(1.x需要手动更改mapping开启),将field data在indexing time构建在磁盘上,经过一系列优化,可以达到比之前采用field data cache机制更好的性能。因此需要限制对field data cache的使用,最好是完全不用,可以极大释放heap压力。 需要注意的是,很多同学已经升级到ES2.0,或者1.0里已经设置mapping启用了doc values,在kibana里仍然会遇到问题。 这里一个陷阱就在于kibana的table panel可以对所有字段排序。 设想如果有一个字段是analyzed过的,而用户去点击对应字段的排序表头是什么后果? 一来排序的结果并不是用户想要的,排序的对象实际是词典; 二来analyzed过的字段无法利用doc values,需要装载到field data cache,数据量很大的情况下可能集群就在忙着GC或者根本出不来结果。

bulk queue

一般来说,Bulk queue不会消耗很多的heap,但是见过一些用户为了提高bulk的速度,客户端设置了很大的并发量,并且将bulk Queue设置到不可思议的大,比如好几千。 Bulk Queue是做什么用的?当所有的bulk thread都在忙,无法响应新的bulk request的时候,将request在内存里排列起来,然后慢慢清掉。 这在应对短暂的请求爆发的时候有用,但是如果集群本身索引速度一直跟不上,设置的好几千的queue都满了会是什么状况呢? 取决于一个bulk的数据量大小,乘上queue的大小,heap很有可能就不够用,内存溢出了。一般来说官方默认的thread pool设置已经能很好的工作了,建议不要随意去“调优”相关的设置,很多时候都是适得其反的效果。

indexing buffer

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

state buffer(Cluster State Buffer)

ES被设计成每个node都可以响应用户的api请求,因此每个node的内存里都包含有一份集群状态的拷贝。这个cluster state包含诸如集群有多少个node,多少个index,每个index的mapping是什么?有少shard,每个shard的分配情况等等 (ES有各类stats api获取这类数据)。 在一个规模很大的集群,这个状态信息可能会非常大的,耗用的内存空间就不可忽视了。并且在ES2.0之前的版本,state的更新是由master node做完以后全量散播到其他结点的。 频繁的状态更新就可以给heap带来很大的压力。 在超大规模集群的情况下,可以考虑分集群并通过tribe node连接做到对用户api的透明,这样可以保证每个集群里的state信息不会膨胀得过大。

超大搜索聚合结果集的fetch

ES是分布式搜索引擎,搜索和聚合计算除了在各个data node并行计算以外,还需要将结果返回给汇总节点进行汇总和排序后再返回。无论是搜索,还是聚合,如果返回结果的size设置过大,都会给heap造成很大的压力,特别是数据汇聚节点。超大的size多数情况下都是用户用例不对,比如本来是想计算cardinality,却用了terms aggregation + size:0这样的方式; 对大结果集做深度分页;一次性拉取全量数据等等。

对高cardinality字段做terms aggregation

所谓高cardinality,就是该字段的唯一值比较多。 比如client ip,可能存在上千万甚至上亿的不同值。 对这种类型的字段做terms aggregation时,需要在内存里生成海量的分桶,内存需求会非常高。如果内部再嵌套有其他聚合,情况会更糟糕。 在做日志聚合分析时,一个典型的可以引起性能问题的场景,就是对带有参数的url字段做terms aggregation。 对于访问量大的网站,带有参数的url字段cardinality可能会到数亿,做一次terms aggregation内存开销巨大,然而对带有参数的url字段做聚合通常没有什么意义。 对于这类问题,可以额外索引一个url_stem字段,这个字段索引剥离掉参数部分的url。可以极大降低内存消耗,提高聚合速度。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值