_index元数据
_index元数据代表一个
document存放在哪个index;非类似或业务场景差别大的数据要放在不同索引,避免数据耦合造成性能问题。例如电商业务中,商品数据一般用来共用户检索,而销售数据一般用来数据分析,前者要求快速响应,后者需要对数据做大量耗时的聚合操作。如果两者放在一个索引,shard对销售数据聚合处理占用大量节点分配给该shard的资源,影响对用户产品信息请求的响应。
索引名必须小写,不能以下划线开头。
_type元数据
代表document属于index的哪个类别。逻辑上对index中有些许不同的数据进行分类时可以划分为多个type。type名称可以大写或小写,不能以下划线开头。
_id元数据
代表document的唯一标识。创建一个文档时可以手动指定document的id,也可以不指定,由ES自动创建。一般来说,对已经存在数据库的数据创建document时,应该使用数据库中该行的主键id作为ES文档的id。但如果数据写入ES之前没有保存在任何其他的地方,则可以让系统自动分配id。
自动生成的id是一个长尾20字节的URL安全字符。
自动分配id的插入方式必须使用POST请求。
_source元数据
默认情况下查询文档的返回值中,_source字段会返回插入文档时body体中的所有内容。
如果希望_source字段只返回指定字段,则可以在对_search API的post请求中使用_source参数。
_version元数据和版本控制
ES中的每个文档都有一个_version属性,每次对文档执行修改或删除操作,ES都会对该文档的_version版本号自增1。
删除一个文档时,其本质是将其标记为deleted,同时_version自增。如果之后用户再重新创建这条文档,那么会在delete的version基础上再对_version自增。
相比于关系型数据库使用悲观锁做并发修改,ES使用乐观锁(也就是不加锁)和版本控制做到对同一个文档的并发修改。
举一个商品减库存的例子,包含3个动作:
1、读id=1商品数据,库存=100;
2、用户程序中库存-1;
3、库存写回DB;
线程A和B并发执行上述3个操作。
数据库使用悲观锁的具体做法是将上述3个步骤放到一个事务中执行,并且线程在读数据时就需要对id=1的数据加一个排他锁,保证线程A读数据时,线程B处于阻塞状态,避免2者都读到100的库存,然后都写回99的剩余库存回DB。
ES使用乐观锁的具体做法是不加任和锁,线程A和B可以同时读到id=1的数据,并且都拿到一个相同的版本号_version = 1,之后线程修改该条文档时必须带回读取数据时的_version版本号。
如果线程A先将库存=99回写到ES,并且带上的读数据时读到的版本号_version = 1,ES将当前shard中该文档的_version和线程A传来的_version比对,两者相等则写入成功,ES更新_version=2。
之后线程B将库存=99回写到ES,并且带上版本号_version = 1,小于shard中的_version=2,ES会放弃该修改。
此时线程B必须要重新从ES读取到 id=1, _version=2 的数据,重新在用户程序中执行减库存和回写ES的操作。如果再失败,则重复该过程。
相比而言,悲观锁的优点是对应用程序而言,并发和竞争对应用程序是透明的,而且应用程序不用重试,缺点则是并发度低。乐观锁的优点是无锁并发,并发能力强,缺点是用户线程写入失败后需要重试,会增加ES服务的压力,累积并发次数。
模拟并发修改文档
需要注意:
1、通过_search API读取到的数据是不会返回_version字段的。只有通过 GET /index/type/id号读取单条数据才能获得_version。
2、ES 7 以上版本不再支持version参数作为版本控制的参数,而是使用if_seq_no和if_primary_term。
_version和_seq_no都是document的版本号,不同的是_version只属于某一个document自身的版本号,而_seq_no是整个索引作用域下的全局变量,当前索引中任何type的任何document变更都会导致_seq_no自增。而某一个document的_seq_no等于当前变更该document的_seq_no。
_primary_term表示文档位于哪个shard。
3、如果做出了_update变更请求,但是没有实际的变更(例如原价格25,只变更价格字段,变更后价格仍是25),此时document的_version和_seq_no都是不会变的。
举个例子:
现在有id为1~5这5个文档,他们的_version 和 _seq_no如下:
现在对id=3和id=5的document分别进行1次修改。_version和_seq_no变为:
下面模拟客户端并发修改同一条数据并使用_seq_no进行版本控制。
步骤0:客户端1和客户端2同时查询一条数据,并拿到它的版本号 _seq_no=16。
步骤1:客户端1先更新该数据并带上版本号16。
此时_version 和 _seq_no都会发生改变。
步骤2:客户端2后更新该数据并带上版本号16。结果报错,原因是客户端2带上的版本号小于document的新版本号23:
此时如果客户端2要完成更改,需要重新获取id=1的文档的_seq_no并重新做更新请求。
根据批量获取文档
或者
或者
批量增删改 _bulk
使用_bulk接口可以进行批量操作,每个操作都对应_bulk请求的body体中的2个json串,语法如下:
操作的类型大致有这4种:
举个例子,批量插入5条语句:
其他操作
上面的写法等价于下面这两种写法:
需要注意:
1、bulk操作对json格式有严格要求,每个json内不能有换行,json与json间必须要换行。最后一个json也要以换行符结束。
2、bulk的多个操作中某个操作发生错误不会导致其他后续操作终止,响应结果会返回每条操作的结果和错误原因。
3、bulk请求的body会被ES全部加载到内存中,因此需要控制一个bulk请求的body体大小,官方建议一次bulk请求的操作数在1000~5000条之间,body体大小最大在5~15M之间。
文档路由原理
客户端程序curd一个文档的时候,可以将请求发向任意一个es节点,此时该es节点会作为一个协调节点,根据每个文档的routing值进行路由。将一个批量操作中的多个文档分发到所有其他目标节点,当所有节点操作完成之后,协调节点才会将响应返回给客户端。
routing值默认是id值,用户也可以通过在插入文档时指定routing参数来自己指定routing值的字段。
路由的规则是 目标节点 = hash(routing) % 节点总数。由于没有使用类似虚拟槽的分区算法,而是使用简单的区域分区算法,因此es的primary节点数量一旦定下来就不能改变。
对于增删改,协调节点是根据id将不同文档的操作请求精准的发到对应的shard。而对于_search查询,协调节点则是会将请求发到所有其他shard上。
写一致性
我们知道ES的文档是会备份到replica shard的,因此一个文档不仅要写在主分片,也要写到副分片。
ES在执行增删改操作时可以带上consistency参数指明用户想要的一致性级别。
one:表示只要写入primary shard就算写入成功。
all:写入到primary shard和其他所有replica shard才算写入成功。
quorum:写入到大部分shard就算写入成功。
quorum机制要求至少(primary + number_of_replicas)/ 2 + 1 个shard是可用的,且 number_of_replicas>1时才会起作用。
例如 primary shard 数量 = 3,number_of_replicas = 2,则一共有 3 + 3*2 = 9个,那么就要求有6个shard(可以是primary或者replica)是active状态才能写入成功。如果shard数量少于6,且用户还要求使用quorum级别的写一致性,就会导致写入失败。
此外node节点数量也会间接影响quorum。例如上述的情景中如果只有2台node可供使用,那么2个node最多也只能放下6个shard,此时只要有一个node宕机都会导致shard无法满足quorum机制要求而写入失败。
quorum下如果shard数量不足,ES会先等待默认1分钟的超时时间,在这1分钟内如果shard数量增加则可以写入成功,否则失败。
可以在PUT/POST写入操作带上timeout操作指定quorum超时时间,不过timeout机制不是专门为quorum设置的。
timeout机制
如果一个_search的结果集包含大量数据导致检索需要花费特别长的时间,而用户可能希望ES只要检索出部分数据展示给用户即可的场景下,此时可以使用timeout参数检索。例如电商场景满足搜索关键词的数据很多,但用户可能只要看前面几十条就够了,而不是让用户等几十秒或几分钟检索所有数据。
例如:
如果10ms内ES的所有shard检索出 1000条数据,而实际上可能满足搜索条件的文档有100w条。此时ES会只返回1000条记录。
multi-index和multi-type搜索模式
deep paging性能问题
deep paging就是偏移量大的分页查询。
看一个场景,假如某个index下有3个primary shard(P1~P3),没有replica shard,共6w个文档,用户要拿第10000~10010个文档(第1000页)。协调节点P1(它本身也是个处理查询请求的节点)广播请求到P2和P3后,ES会让每个shard都检索出第 10000~10010个文档,并且排序,返回给协调节点P1,协调节点再对这30030条数据排序,取出排序后的第10000~10010条数据返回给客户端。
和mysql的分页查询一样,数据库扫描的数据量等于偏移量,因此偏移量过大的分页搜索会导致扫描的数据多时间长。ES除了这个问题之外,由于协调节点需要收集其他shard的数据,因此还会占用内存,数据在shard之间传输的网络时间也不容忽视。
请尽量避免deep paging。