ElasticSearch 7.7.0 进阶篇

本文深入探讨了Elasticsearch的分布式架构,包括分片与副本机制、集群发现、负载均衡和容错机制。详细解释了如何在不同节点环境下实现容错,并介绍了扩容机制。此外,还讨论了文档操作,如全量替换、强制创建、删除及部分更新,并讲解了内置脚本功能。最后,分析了ES的并发控制原理,包括悲观锁与乐观锁,并通过示例展示了_version字段在并发控制中的作用。
摘要由CSDN通过智能技术生成

前言

上一篇讲述了ES的基本操作和基本概念,这篇会更加深入了解ES相关操作以及相关操作的背后原理。当然其内容主要是概念和基本原理为主,并且穿插一些实战操作来加深体会。

一 ES分布式架构

我们主要了解分片&副本机制,集群发现机制 ,负载机制,容错机制,扩容机制等。

1.1 shard&replica机制

  1. index包含多个shard
  2. 每个shard都是一个最小工作单元,承载部分数据,lucene实例,完整的建立索引和处理请求的能力
  3. 增减节点时,shard会自动在nodes中负载均衡
  4. primary shard和replica shard,每个document肯定只存在于某一个primary shard以及其对应的replica shard中,不可能存在于多个primary shard
  5. replica shard是primary shard的副本,负责容错,以及承担读请求负载
  6. primary shard的数量在创建索引的时候就固定了,replica shard的数量可以随时修改
  7. primary shard的默认数量是5,replica默认是1,默认有10个shard,5个primary shard,5个replica shard
  8. primary shard不能和自己的replica shard放在同一个节点上(否则节点宕机,primary shard和副本都丢失,起不到容错的作用),但是可以和其他primary shard的replica shard放在同一个节点上。

其中第6条“为啥primary shard的数量在创建索引的时候就固定了?”,这个也很好理解,我们之前玩数据库分表分库的时候道理一样,就是我们根据id值hash取模(shard = hash(routing) % number_of_primary_shards)的时候会路由到不同地址的库,如果改变了分片数,那么数据的路由地址都要重新变更。因此分片数一般不能变化。

1.2 容错机制

1.2.1 单node环境下的容错

假定Elasticsearch集群只有一个node,primary shard设置为3,replica shard设置为1,这样1个索引就应该有3个primary shard,3个replica shard,但primary shard不能与其replica shard放在一个node里,导致replica shard无法分配,这样集群的status为yellow,示例图如下:

 

集群可以正常工作,一旦出现node宕机,数据全部丢失,并且集群不可用。

结论:单node环境容错性为0

1.2.2 两台node环境下的容错

primary shard与replica shard的设置与上文相同,此时Elasticsearch集群只有2个node,shard分布如下图所示:

 

如果其中一台宕机,如node-2宕机,如图所示:

 

此时node-1节点的R2(replica shard)会升为P2(primary shard),此时集群还能正常用,数据未丢失。

结论:双node环境容错性为1台。

1.2.3 三台node环境下的容错

我们先按primary shard为3,replica shard为1进行容错性计算。此时每台node存放2个shard,如果一台宕机,此时另外2台肯定还有完整的数据,如果两台宕机,剩下的那台就只有2/3的数据,数据丢失1/3,容错性为1台。如果是这样设置,那3台的容错性和2台的容错性一样,就存在资源浪费的情况。那怎么样提升容错性呢?把replica shard的值改成2,这样每台node存放3个shard,如下图所示:

 

如果有2台宕机,就剩下node-2,此时集群的数据还是完整的,replica会升成primary shard继续提供服务,如下图所示:

 

结论:3台node环境容错性最大可以是2台。

1.2.4 容错过程与选举机制

Elasticsearch集群中,所有的node都是对等的角色,所有的node都能接收请求,并且能自动转请求到相应的节点上(数据路由),最后能将其他节点处理的数据进行响应收集,返回给客户端。在集群中,也存在一个master节点,它的职责多一些,需要管理与维护集群的元数据,索引的创建与删除和节点的增加和删除,它都会收到相应的请求,然后进行相应的数据维护。master node在承担索引、搜索请求时,与其他node一起分摊,并不承担所有的请求,因而不存在单点故障这个问题。

我们假设一下集群有3台node,其中node-1宕机的过程,如果node-1是master node,关键步骤如下:

 

  1. 由于P1丢失,cluster.status瞬间状态变成red。
  2. 重新进行master选举,自动选另一个node作为master。
  3. 新的master将丢失了P1对应的R1(在node-3上面)提升为primary shard ,现全部primary shard active,但是P1,P2的replica shard无法启动,cluster.status变成yellow。
  4. 重启故障的node-1节点,新的master会将缺失的副本都copy一份到node-1上,node-1会使用之前已有的数据,并且同步一下宕机期间的数据修改,此时所有的shard全部active状态,cluster.status重新变成green。

1.3 扩容机制

垂直扩容:采购更强大的服务器,成本非常高昂,而且会有瓶颈,因为单个硬件的性能参数是有限的。

水平扩容:业界经常采用的方案,采购越来越多的普通服务器,性能比较一般,但是很多普通服务器组织在一起,就能构成强大的计算和存储能力。

1.3.1 极限扩容

根据上面3个场景,我们可以知道,如果shard总数是6个(包含primary shard 和replica shard),那么node数量上限也为6,即每台node存储1个shard,这个数据即为扩容极限,如果要突破极限,可以通过增大replica的值(也只能改变这个值)来实现,这样有更多的replica shard去分担查询请求,占用更多的节点,整个集群的CPU、IO、Memory资源更多,整体吞吐量也越高。

说白了极限扩招只能增加replica节点,提高es读的性能(后面会介绍replica只能处理读请求)

二 ES document(文档)相关操作和说明

2.1 _index元数据

索引名称必须是小写的,不能用下划线开头,不能包含逗号

代表一个document存放在哪个index中,类似的数据放在一个索引,非类似的数据放不同索引。

2.2 _type元数据(已弃用将要被移除)

type名称可以是大写或者小写,但是同时不能用下划线开头,不能包含逗号

代表document属于index中的哪个类别(type)。一个索引通常会划分为多个type,逻辑上对index中有些许不同的几类数据进行分类:因为一批相同的数据,可能有很多相同的fields,但是还是可能会有一些轻微的不同,可能会有少数fields是不一样的,举个例子,就比如说,商品,可能划分为电子商品,生鲜商品,日化商品,等等。

2.3 _id元数据

自动生成的id,长度为20个字符,URL安全,base64编码,GUID,分布式系统并行生成时不可能会发生冲突

代表document的唯一标识,与index和type一起,可以唯一标识和定位一个document。我们可以手动指定document的id(put /index/type/id),也可以不指定,由es自动为我们创建一个id。

一般来说,是从某些其他的系统中,导入一些数据到es时,会采取这种方式,就是使用系统中已有数据的唯一标识,作为es中document的id。

举个例子,比如说,我们现在在开发一个电商网站,做搜索功能,或者是OA系统,做员工检索功能。这个时候,数据首先会在网站系统或者IT系统内部的数据库中,会先有一份,此时就肯定会有一个数据库的primary key(自增长,UUID,或者是业务编号)。如果将数据导入到es中,此时就比较适合采用数据在数据库中已有的primary key。

如果说,我们是在做一个系统,这个系统主要的数据存储就是es一种,也就是说,数据产生出来以后,可能就没有id,直接就放es一个存储,那么这个时候,可能就不太适合说手动指定document id的形式了,因为你也不知道id应该是什么,此时可以采取下面要讲解的让es自动生成id的方式。

2.4 _source元数据定制返回结果

设置数据

put /test_index/test_type/1
{
 "test_field1": "test field1",
 "test_field2": "test field2"
}

请求定制结果

//正常的请求数据
get /test_index/test_type/1

{
  "_index": "test_index",
  "_type": "test_type",
  "_id": "1",
  "_version": 2,
  "found": true,
  "_source": {
    "test_field1": "test field1",
    "test_field2": "test field2"
  }
}
//定制返回的结果,指定_source中,返回哪些field

{
  "_index": "test_index",
  "_type": "test_type",
  "_id": "1",
  "_version": 2,
  "found": true,
  "_source": {
    "test_field2": "test field2"
  }
}
//定制返回的结果,指定_source中,返回哪些field
GET /test_index/test_type/1?_source=test_field1
//返回地址结果
{
  "_index" : "test_index",
  "_type" : "test_type",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "test_field2" : "test field1"
  }
}
GET /test_index/test_type/1?_source=test_field2
//返回地址结果
{
  "_index" : "test_index",
  "_type" : "test_type",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "test_field2" : "test field2"
  }
}

2.5 document的全量替换 强制创建 删除

2.5.1 document的全量替换

语法与创建文档是一样的,如果document id不存在,那么就是创建;如果document id已经存在,那么就是全量替换操作,替换document的json串内容。document是不可变的,如果要修改document的内容,第一种方式就是全量替换,直接对document重新建立索引,替换里面所有的内容。es会将老的document标记为deleted,然后新增我们给定的一个document,当我们创建越来越多的document的时候,es会在适当的时机在后台自动删除标记为deleted的document。

 

全量替换这种更新操作我们一般是不推荐的,因为多了一次查询请求,这是其一,其二 他的更新操作是在客户端编辑的,假如客户端页面编辑10分钟,那么这个并发冲突是很大概率发生的。

2.5.2 document的强制创建

创建文档与全量替换的语法是一样的,有时我们只是想新建文档,不想替换文档,如果强制进行创建呢?

PUT /index/type/id?op_type=create,PUT /index/type/id/_create

2.5.3 document的删除

DELETE /index/type/id,他不是物理删除,只会将其标记为deleted,当数据越来越多的时候,在后台自动删除。

2.6 document的partial update

内部获取document ,将传过来的field更新到document的json中,将老的document标记为deleted,然后修改后的新的document创建好

  1. 所有的查询,修改和写回操作,都发生在es中的一个shard内部,避免了所有的网络传输的开销,大大提升了性能
  2. 减少了查询和修改中的时间间隔,可以减少并发冲突情况

 

2.6 script 脚本

es是有个内置的脚本支持的,可以实现各种各样的复杂操作,我们可以感受下。

PUT /test_index/test_type/11
{
 "num": 0,
 "tags": []
}

设置document

POST /test_index/test_type/11/_update
{
  "script" : "ctx._source.num+=1"
}
//返回结果
GET /test_index/test_type/11
{
 "_index": "test_index",
 "_type": "test_type",
 "_id": "11",
 "_version": 2,
 "found": true,
 "_source": {
   "num": 1,
   "tags": []
 }
}

2.6.1 内置脚本

2.6.2 外部脚本

已过时,就不讲解了。

2.6.3 upsert操作

POST /test_index/test_type/11/_update
{
 "doc": {
   "num": 1
 }
}
上面的操作如果document存在,则更新,如果不存在则404错误

针对上面的问题,我们可以这么做
如果指定的document不存在,就执行upsert中的初始化操作;如果指定的document存在,就执行doc或者script指定的partial update操作
POST /test_index/test_type/11/_update
{
  "script" : "ctx._source.num+=1",
  "upsert": {
      "num": 0,
      "tags": []
  }
}

2.7 mget 批量查询

一般来说,在进行查询的时候,如果一次性要查询多条数据的话,那么一定要用batch批量操作的api。尽可能减少网络开销次数,可能可以将性能提升数倍,甚至数十倍,非常非常之重要。

2.7.1 单条查询

GET /test_index/test_type/1

GET /test_index/test_type/2

2.7.2 mget批量查询

GET /_mget

{
  "docs" : [
     {
        "_index" : "test_index",
        "_type" :  "test_type",
        "_id" :    1
     },
     {
        "_index" : "test_index",
        "_type" :  "test_type",
        "_id" :    2
     }
  ]
}

2.7.3 如果查询的document是一个index下的不同type种的话

GET /test_index/_mget

{
  "docs" : [
     {
        "_type" :  "test_type",
        "_id" :    1
     },
     {
        "_type" :  "test_type",
        "_id" :    2
     }
  ]
}

2.7.4 如果查询的数据都在同一个index下的同一个type下,最简单了

GET /test_index/test_type/_mget

{
  "ids": [1, 2]
}

2.8 bulk 批量增删改

每一个操作要两个json串,语法如下:

{"action": {"metadata"}}

{"data"}

例如,创建一个文档:

{"index": {"_index": "test_index", "_type", "test_type", "_id": "1"}}

{"test_field1": "test1", "test_field2": "test2"}

bulk操作类型总结:

  1. delete:删除一个文档,只要1个json串就可以了
  2. create:PUT /index/type/id/_create,强制创建
  3. index:普通的put操作,可以是创建文档,也可以是全量替换文档
  4. update:执行的partial update操作

注意事项:

  1. bulk api对json的语法,有严格的要求,每个json串不能换行,只能放一行,同时一个json串和一个json串之间,必须有一个换行
  2. bulk request会加载到内存里,如果太大的话,性能反而会下降,因此需要反复尝试一个最佳的bulk size。一般从1000~5000条数据开始,尝试逐渐增加。另外,如果看大小的话,最好是在5~15MB之间。
  3. bulk操作中,任意一个操作失败,是不会影响其他的操作的,但是在返回结果里,会告诉你异常日志

POST /_bulk

{ "delete": { "_index": "test_index", "_type": "test_type", "_id": "3" }}  
{ "create": { "_index": "test_index", "_type": "test_type", "_id": "12" }}
{ "test_field":    "test12" }
{ "index":  { "_index": "test_index", "_type": "test_type", "_id": "2" }}
{ "test_field":    "replaced test2" }
{ "update": { "_index": "test_index", "_type": "test_type", "_id": "1", "_retry_on_conflict" : 3} }
{ "doc" : {"test_field2" : "bulk test1"} }

三 ES并发控制原理

当我们使用ES更新document的时候,先读取原始文档,再做修改,然后把document重新索引,如果有多人同时在做相同的操作,不做并发控制的话,就极有可能会发生修改丢失的。可能有些场景,丢失一两条数据不要紧(比如文章阅读数量统计,评论数量统计),但有些场景对数据严谨性要求极高,丢失一条可能会导致很严重的生产问题,比如电商系统中商品的库存数量,丢失一次更新,可能会导致超卖的现象。

我们还是以电商系统的下单环节举例,某商品库存100个,两个用户下单购买,都包含这件商品,常规下单扣库存的实现步骤

  1. 客户端完成订单数据校验,准备执行下单事务。
  2. 客户端从ES中获取商品的库存数量。
  3. 客户端提交订单事务,并将库存数量扣减。
  4. 客户端将更新后的库存数量写回到ES。

 

如果没有并发控制,这件商品的库存就会更新成99(实际正确的值是98),这样就会导致超卖现象。假定http-1比http-2先一步执行,出现这个问题的原因是http-2在获取库存数据时,http-1还未完成下单扣减库存后,更新到ES的环节,导致http-2获取的数据已经是过期数据,后续的更新肯定也是错的。

上述的场景,如果更新操作越是频繁,并发数越多,读取到更新这一段的耗时越长,数据出错的概率就越大。

3.1 悲观锁与乐观锁概念

3.1.1 悲观并发控制

悲观锁的含义:我认为每次更新都有冲突的可能,并发更新这种操作特别不靠谱,我只相信只有严格按我定义的粒度进行串行更新,才是最安全的,一个线程更新时,其他的线程等着,前一个线程更新完成后,下一个线程再上。

关系型数据库中广泛使用该方案,常见的表锁、行锁、读锁、写锁,依赖redis或memcache等实现的分布式锁,都属于悲观锁的范畴。明显的特征是后续的线程会被挂起等待,性能一般来说比较低,不过自行实现的分布式锁,粒度可以自行控制(按行记录、按客户、按业务类型等),在数据正确性与并发性能方面也能找到很好的折衷点。

3.1.2 乐观并发控制

乐观锁的含义:我认为冲突不经常发生,我想提高并发的性能,如果真有冲突,被冲突的线程重新再尝试几次就好了。

在使用关系型数据库的应用,也经常会自行实现乐观锁的方案,有性能优势,方案实现也不难,还是挺吸引人的。

Elasticsearch默认使用的是乐观锁方案,前面介绍的_version字段,记录的就是每次更新的版本号,只有拿到最新版本号的更新操作,才能更新成功,其他拿到过期数据的更新失败,由客户端程序决定失败后的处理方案,一般是重试。

3.2 ES 乐观锁实现原理

我们还是以上面的案例为背景,若http-2向ES提交更新数据时,ES会判断提交过来的版本号与当前document版本号,document版本号单调递增,如果提交过来的版本号比document版本号小,则说明是过期数据,更新请求将提示错误,过程图如下:

 

3.3 Replica Shard 数据同步并发控制

在Elasticsearch内部,每当primary shard收到新的数据时,都需要向replica shard进行数据同步,这种同步请求特别多,并且是异步的。如果同一个document进行了多次修改,Shard同步的请求是无序的,可能会出现"后发先至"的情况,如果没有任何的并发控制机制,那结果将无法相像。

Shard的数据同步也是基于内置的_version进行乐观锁并发控制的。

例如Java客户端向Elasticsearch某条document发起更新请求,共发出3次,Java端有严谨的并发请求控制,在ElasticSearch的primary shard中写入的结果是正确的,但Elasticsearch内部数据启动同步时,顺序不能保证都是先到先得,情况可能是这样,第三次更新请求比第二次更新请求先到,如下图:

 

如果Elasticsearch内部没有并发的控制,这个document在replica的结果可能是text2,并且与primary shard的值不一致,这样肯定错了。

预期的更新顺序应该是text1-->text2-->text3,最终的正确结果是text3。那Elasticsearch内部是如何做的呢?

Elasticsearch内部在更新document时,会比较一下version,如果请求的version与document的version相等,就做更新,如果document中的version不等于请求的version,说明此数据是旧数据,此时会丢弃当前的请求,最终的结果为text3。

此时的更新顺序为text1-->text3,最终结果也是对的。

3.4 实战体验ES version的乐观锁

上面阐述了乐观锁,悲观锁的概念,以及es的乐观锁实现原理,下面我们将结合案例体会下乐观锁并发控制。

第一次创建一个document的时候,它的_version内部版本号就是1;以后,每次对这个document执行修改或者删除操作,都会对这个_version版本号自动加1;哪怕是删除,也会对这条数据的版本号加1。

3.4.1 构造数据

PUT /test_index/test_type/7
{
  "test_field": "test test"
}

3.4.2 模拟两个客户端都获取到了同一条数据

GET test_index/test_type/7

  1. 然后其中一个client 先更新下数据,同时带上数据的版本号,确保es中的数据的版本号,跟客户端中的数据的版本号是相同的,才能修改。

 PUT /test_index/test_type/7?if_seq_no=12&if_primary_term=1

{
  "test_field": "test client 1"
}

2.另外一个客户端,尝试基于version=1的数据去进行修改,同样带上version版本号,进行乐观锁的并发控制。

 PUT /test_index/test_type/7?if_seq_no=12&if_primary_term=1

{
  "test_field": "test client 2"
}

3.基于最新的数据和版本号,去进行修改,修改后,带上最新的版本号,可能这个步骤会需要反复执行好几次,才能成功,特别是在多线程并发更新同一条数据很频繁的情况下。

先查询最新的版本号。

GET /test_index/test_type/7

然后再次更新。

 PUT /test_index/test_type/7?if_seq_no=12&if_primary_term=1

{
 "test_field": "test client 2"
}

3.5 实战体验ES external version的乐观锁

我们可以不用它提供的内部_version版本号来进行并发控制,可以基于自己维护的一个版本号来进行并发控制。比如你的数据在mysql里也有一份,然后我们应用系统本身也维护了一个版本号。此时,进行乐观锁并发控制的时候,可能并不是想要用es内部的_version来进行控制,而是用自己维护的那version来进行控制。

3.5.1  external version与verison的区别

version(url?version=1) 与  external version( url?version=1&version_type=external)唯一的区别在于:

  1. version:只有当你提供的version与es中的_version一模一样的时候,才可以进行修改,只要不一样,就报错。
  2. external version(version_type=external):只有当你提供的version比es中的_version大的时候,才能完成修改

例如:

version:

如果es中的document _version=1 ,那么请求url?version=1,则可以执行成功。

external version:

如果es中的document _version=1 ,那么请求url?version>1&version_type=external,则可以执行成功。(url?version=2&version_type=external)

3.5.2 构造测试数据

PUT /test_index/test_type/8

{
  "test_field": "test"
}

3.5.3 模拟两个客户端都获取到了同一条数据

GET /test_index/test_type/8

{
  "test_field": "test client 1"
}

1.第一个客户端先进行修改,此时客户端程序是在自己的数据库中获取到了这条数据的最新版本号,比如说是2

PUT /test_index/test_type/8?version=2&version_type=external

2.第二个客户端,同时拿到了自己数据库中维护的那个版本号,也是2,同时基于version=2发起了修改

PUT /test_index/test_type/8?version=2&version_type=external

{
 "test_field": "test client 2"
}

结果显而易见并发冲突,应该会更新失败。

3.重新基于最新的版本号发起更新

先查询数据获取新的版本号

GET /test_index/test_type/8

再基于新的版本号发起更新

PUT /test_index/test_type/8?version=3&version_type=external

{
 "test_field": "test client 2"
}

四 document相关操作原理

4.1 document路由原理

    如果我们有分布式基础就会很容易理解分片路由原理,就是对id 进行hash取模 然后路由到对应的分片。

  1. 路由算法 shard = hash(routing) % number_of_primary_shards
  2. 默认的routing就是_id也可以手动指定一个routing value,比如说put /index/type/id?routing=user_id。手动指定routing value是很有用的,可以保证某一类document一定被路由到一个shard上去,那么在后续进行应用级别的负载均衡,以及提升批量读取的性能的时候,是很有帮助的。
  3. primary shard数量不可变,如果分片数量变了,那么之前数据的路由地址和现在的地址也就变了,我们无法正确的匹配了,当然也是可以解决的,就是重新将数据灌入到es中,比较麻烦一般不推荐。

4.2 document增删改原理

客户端选择一个node发送请求过去,这个node就是 coordinating node(协调节点)。coordinating node对document进行路由,将请求转发给对应的node(primary shard,因为只有primary shard才能处理增删改操作),然后将数据同步到replica node。coordinating node如果发现primary node和所有replica node都搞定之后,接收请求的node返回document给coordinate node,coordinate node 就返回响应结果给客户端。

4.3 document查询原理

客户端发送请求到任意一个node,成为coordinate node,然后 coordinate node对document进行路由,将请求转发到对应的node,此时会使用round-robin随机轮询算法,在primary shard以及其所有replica中随机选择一个(读操作 primary shard 和 replica 都可以处理),让读请求负载均衡。接收请求的node返回document给coordinate node,coordinate node 就返回响应结果给客户端。

ps:document如果还在建立索引过程中,可能只有primary shard有,任何一个replica shard都没有,此时可能会导致无法读取到document,但是document完成索引建立之后,primary shard和replica shard就都有了。

4.4 document写一致性原理&quorum机制

我们在发送任何一个增删改操作的时候,比如说put /index/type/id,都可以带上一个consistency参数,指明我们想要的写一致性是什么?

put /index/type/id?consistency=quorum

es 提供的 consistency 有三种:

  1. one(primary shard) 要求我们这个写操作,只要有一个primary shard是active活跃可用的,就可以执行
  2. all(all shard) 要求我们这个写操作,必须所有的primary shard和replica shard都是活跃的,才可以执行这个写操作
  3. quorum(default)要求所有的shard中,必须是大部分的shard都是活跃的,可用的,才可以执行这个写操作

quorum机制,写之前必须确保大多数shard都可用,quroum = int( (primary + number_of_replicas) / 2 ) + 1,且number_of_replicas>1时才生效。

例如:

3个primary shard,number_of_replicas=1,总共有3 + 3 * 1 = 6个shard 则 quorum = int( (3 + 1) / 2 ) + 1 = 3

     所以,要求6个shard中至少有3个shard是active状态的,才可以执行这个写操作。

  1. 那么如果节点数少于quorum数量,可能导致quorum不齐全,进而导致无法执行任何写操作。
  2. es提供了一种特殊的处理场景,就是说当number_of_replicas>1时才生效,因为假如说,你就一个primary shard,replica=1,此时就2个shard(1 + 1 / 2) + 1 = 2,要求必须有2个shard是活跃的,但是可能就1个node,此时就1个shard是活跃的,如果你不特殊处理的话,导致我们的单节点集群就无法工作。
  3. quorum不齐全时 wait机制,默认1分钟 。timeout机制,100,30s 等待期间。期望活跃的shard数量可以增加。最后实在不行,就会timeout我们其实可以在写操作的时候,加一个timeout参数,比如说put /index/type/id?timeout=30,这个就是说自己去设定quorum不齐全的时候。

4.5 了解bulk json的数据格式

经过前面了解我们的批处理json格式是两个json串约定好中间一个换行符例如:

{"action": {"meta"}}\n
{"data"}\n

这样的话,我们阅读就很不清晰,而且json明明可以兼容表示的例如:

[{
  "action": {
  },
  "data": {
  }
}]

我们常见都是这样操作的搞个json list对象,不香吗,格式完整简单易读易理解。那么es是基于什么角度考虑的呢。下面我们来反例论证下。

假设采取我们标准的json串格式,那么:

  1. 将json数组解析为JSONArray对象,然后就会在内存中出现一模一样的两份数据,一份数据是json文本,一份数据是JSONArray对象
  2. 解析json数组里的每个json,对每个请求中的document进行路由
  3. 为路由到同一个shard上的多个请求,创建一个请求数组
  4. 将这个请求数组序列化
  5. 将序列化后的请求数组发送到对应的节点上去

我们之前提到过bulk size最佳大小的那个问题,一般建议说在几千条那样,然后大小在10MB左右,所以说,可怕的事情来了。假设说现在100个bulk请求发送到了一个节点上去,然后每个请求是10MB,100个请求,就是1000MB = 1GB,然后每个请求的json都copy一份为jsonarray对象,此时内存中的占用就会翻倍,就会占用2GB的内存,甚至还不止。因为弄成jsonarray之后,还可能会多搞一些其他的数据结构,2GB+的内存占用。

占用更多的内存可能就会积压其他请求的内存使用量,比如说最重要的搜索请求,分析请求,等等。此时就可能会导致其他请求的性能急速下降

此外,占用更多的内存就会导致java虚拟机的垃圾回收次数更多,每次要回收的垃圾对象更多,耗费的时间更长,导致es的 jvm STW时间更长。

严重导致的缺点:

耗费更多内存,更多的jvm gc开销

而按照约定规则换行符操作:

  1. 不用将其转换为json对象,不会出现内存中的相同数据的拷贝,直接按照换行符切割json
  2. 对每两个一组的json,读取meta,进行document路由
  3. 直接将对应的json发送到node上去

最大的优势在于:

不需要将json数组解析为一个JSONArray对象,形成一份大数据的拷贝,浪费内存空间,尽可能地保证性能

总结

本篇文章主要讲述了 es的基础架构 、es document的高阶操作、 es的并发控制原理,以及es document相关操作的背后原理。让大家更进一步理解和掌握es,虽然本篇文档涉及原理性的东西偏多些,但是都是偏常见的易于理解的原理,基本读上两遍就能理解的。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值