更多优质内容
请关注公众号

Elasticsearch入门基础系列(三) ES document文档解析-阿沛IT博客

正文内容

Elasticsearch入门基础系列(三) ES document文档解析

栏目:数据库 系列:Elasticsearch 发布时间:2024-07-30 23:56 浏览量:601

_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请求。

POST /zbp_index/product/ { "name": "heimei yagao", "desc": "so cheap", "price": 15, "producer": "heimei producer", "tags": [ "fangzhu", "meibai" ] }



_source元数据


默认情况下查询文档的返回值中,_source字段会返回插入文档时body体中的所有内容。


如果希望_source字段只返回指定字段,则可以在对_search API的post请求中使用_source参数。

POST /zbp_index/product/_search { "_source":["name","desc"] }



_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=1|_version=1|_seq_no=1 id=2|_version=1|_seq_no=2 id=3|_version=1|_seq_no=3 id=4|_version=1|_seq_no=4 id=5|_version=1|_seq_no=5


现在对id=3和id=5的document分别进行1次修改。_version和_seq_no变为:

id=1|_version=1|_seq_no=1 id=2|_version=1|_seq_no=2 id=3|_version=2|_seq_no=6 id=4|_version=1|_seq_no=4 id=5|_version=2|_seq_no=7


下面模拟客户端并发修改同一条数据并使用_seq_no进行版本控制。


步骤0:客户端1和客户端2同时查询一条数据,并拿到它的版本号 _seq_no=16。

GET zbp_index/product/1 

{
    "_index": "zbp_index",
    "_type": "product",
    "_id": "1",
    "_version": 3,
    "_seq_no": 16,
    "_primary_term": 1,
    "found": true,
    "_source": {
        "name": "better gaolujie yagao",
        "desc": "youxiao fangzhu",
        "price": 20,
        "producer": "gaolujie producer",
        "tags": [
            "fangzhu",
            "meibai"
        ]
    }
}


步骤1:客户端1先更新该数据并带上版本号16。

POST zbp_index/product/1/_update?if_seq_no=16&if_primary_term=1 // if_seq_no要与查询到的_seq_no一致 
{
    "doc": {
        "price": 21
    }
}

// 变更后
{
    "_index": "zbp_index",
    "_type": "product",
    "_id": "1",
    "_version": 4,
    "_seq_no": 23,
    "_primary_term": 2,
    "found": true,
    "_source": {
        "name": "better gaolujie yagao",
        "desc": "youxiao fangzhu",
        "price": 21,
        "producer": "gaolujie producer",
        "tags": [
            "fangzhu",
            "meibai"
        ]
    }
}


此时_version 和 _seq_no都会发生改变。


步骤2:客户端2后更新该数据并带上版本号16。结果报错,原因是客户端2带上的版本号小于document的新版本号23:

{
    "error": {
        "root_cause": [
            {
                "type": "version_conflict_engine_exception",
                "reason": "[1]: version conflict, required seqNo [16], primary term [2]. current document has seqNo [28] and primary term [2]",
                "index_uuid": "bT6wETrTSOG_-2wHxLh0OA",
                "shard": "0",
                "index": "zbp_index"
            }
        ],
        "type": "version_conflict_engine_exception",
        "reason": "[1]: version conflict, required seqNo [16], primary term [2]. current document has seqNo [28] and primary term [2]",
        "index_uuid": "bT6wETrTSOG_-2wHxLh0OA",
        "shard": "0",
        "index": "zbp_index"
    },
    "status": 409
}

此时如果客户端2要完成更改,需要重新获取id=1的文档的_seq_no并重新做更新请求。


根据批量获取文档


GET /zbp_index/product/_mget
{
    "ids":[1,2,3,4,5]
}

或者

GET /zbp_index/_mget
{
    "docs":[
        {
            "_type":"product",
            "_id":1
        },
        {
            "_type":"product",
            "_id":2
        },
        {
            "_type":"product",
            "_id":3
        }
    ]
}

或者

http://81.71.136.86:9200/_mget
{
    "docs":[
        {
            "_index":"zbp_index",
            "_type":"product",
            "_id":1
        },
        {
            "_index":"zbp_index",
            "_type":"product",
            "_id":2
        },
        {
            "_index":"zbp_index",
            "_type":"product",
            "_id":3
        }
    ]
}



批量增删改 _bulk


使用_bulk接口可以进行批量操作,每个操作都对应_bulk请求的body体中的2个json串,语法如下:

{"操作": {"元数据"}}
{"要操作的数据"}


操作的类型大致有这4种:

delete:删除一个文档,该操作只需要一个json串
create:相当于 PUT /index/type/id/_create 强制创建一个文档
index:创建或覆盖一个文档
update:相当于 POST /index/type/id/_update 部分更新一个文档


举个例子,批量插入5条语句:

POST /_bulk
{"create":{"_index":"zbp_index","_type":"product","_id":1}}
{"name":"better gaolujie yagao","desc":"youxiao fangzhu","price":25,"producer":"gaolujie producer","tags":["fangzhu"]}
{"create":{"_index":"zbp_index","_type":"product","_id":2}}
{"name":"heimei yagao","desc":"hei heimei","price":20,"producer":"heimei producer","tags":["fangzhu","meibai"]}
{"create":{"_index":"zbp_index","_type":"product","_id":3}}
{"name":"yunnanbaiyao yagao","desc":"zhong yao yagao","price":40,"producer":"guangzhou bai yun shan","tags":["jianghuo","meibai","fangzhu","qingxin","chijiu"]}
{"create":{"_index":"zbp_index","_type":"product","_id":4}}
{"name":"heiren yagao","desc":"waiguo pai zi","price":27,"producer":"heiren zhizao","tags":["pohe","meibai","liangli"]}
{"create":{"_index":"zbp_index","_type":"product","_id":5}}
{"name":"zhonghua yagao","desc":"leizhi pai zi","price":15,"producer":"zhonghua zhizao","tags":["meibai"]}


其他操作

POST /_bulk
{"create":{"_index":"zbp_index","_type":"product","_id":6}}
{"name":"tianqi yagao","desc":"laji pai zi","price":15,"producer":"tianqi zhizao","tags":["qingxiang"]}
{"index":{"_index":"zbp_index","_type":"product","_id":1}}
{"name":"gaolujie yagao","price":15,"tags":["qingxiang"]}
{"delete":{"_index":"zbp_index","_type":"product","_id":2}}
{"update":{"_index":"zbp_index","_type":"product","_id":1}}
{"doc":{"description":"nothing"}}


上面的写法等价于下面这两种写法:

POST /zbp_index/product/_bulk
{"create":{"_id":6}}
{"name":"tianqi yagao","desc":"laji pai zi","price":15,"producer":"tianqi zhizao","tags":["qingxiang"]}
{"index":{"_id":1}}
{"name":"gaolujie yagao","price":15,"tags":["qingxiang"]}
{"delete":{"_id":2}}
{"update":{"_id":1}}
{"doc":{"description":"nothing"}}
POST /zbp_index/_bulk
{"create":{"_type":"product","_id":6}}
{"name":"tianqi yagao","desc":"laji pai zi","price":15,"producer":"tianqi zhizao","tags":["qingxiang"]}
{"index":{"_type":"product","_id":1}}
{"name":"gaolujie yagao","price":15,"tags":["qingxiang"]}
{"delete":{"_type":"product","_id":2}}
{"update":{"_type":"product","_id":1}}
{"doc":{"description":"nothing"}}


需要注意:

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值的字段。


http://81.71.136.86:9200/zbp_index/product/_create?routing=name
{
    "name": "tianqi2 yagao",
    "desc": "laji pai zi",
    "price": 15,
    "producer": "tianqi zhizao",
    "tags": [
        "qingxiang"
    ]
}


路由的规则是 目标节点 = 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参数检索。例如电商场景满足搜索关键词的数据很多,但用户可能只要看前面几十条就够了,而不是让用户等几十秒或几分钟检索所有数据。


例如:

GET /_search?timeout=10ms

如果10ms内ES的所有shard检索出 1000条数据,而实际上可能满足搜索条件的文档有100w条。此时ES会只返回1000条记录。


multi-index和multi-type搜索模式

/_search    // 搜索范围:所有index 的所有type的数据
/index1/_search    // 搜索范围:index1下的所有type的数据
/index1,index2/_search  // 搜索范围:index1+index2下的所有type的数据
/*index1,*index2/_search    // 搜索范围:满足通配符的所有index的所有type数据
/index1/type1,type2/_search    // 搜索范围:index1下的type1和type2的数据
/index1,index2/type1,type2/_search  // 搜索范围:index1、2下的type1和type2的数据
/_all/type1,type2/_search     // 搜索范围:所有index下的type1、2的数据



deep paging性能问题

deep paging就是偏移量大的分页查询。

GET /index/type/_search?from=10000&size=10


看一个场景,假如某个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。




更多内容请关注微信公众号
zbpblog微信公众号

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > Elasticsearch入门基础系列(三) ES document文档解析

热门推荐
推荐新闻