ElasticSearch数据存储内容

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ElasticSearch数据存储内容相关的知识,希望对你有一定的参考价值。

参考技术A 很多使用Elasticsearch的同学会关心数据存储在ES中的存储容量,会有这样的疑问:xxTB的数据入到ES会使用多少存储空间。这个问题其实很难直接回答的,只有数据写入ES后,才能观察到实际的存储空间。比如同样是1TB的数据,写入ES的存储空间可能差距会非常大,可能小到只有300~400GB,也可能多到6-7TB,为什么会造成这么大的差距呢?究其原因,我们来探究下Elasticsearch中的数据是如何存储。文章中我以Elasticsearch 2.3版本为示例,对应的lucene版本是5.5,Elasticsearch现在已经来到了6.5版本,数字类型、列存等存储结构有些变化,但基本的概念变化不多,文章中的内容依然适用。

Elasticsearch对外提供的是index的概念,可以类比为DB,用户查询是在index上完成的,每个index由若干个shard组成,以此来达到分布式可扩展的能力。比如下图是一个由10个shard组成的index。

shard是Elasticsearch数据存储的最小单位,index的存储容量为所有shard的存储容量之和。Elasticsearch集群的存储容量则为所有index存储容量之和。

一个shard就对应了一个lucene的library。对于一个shard,Elasticsearch增加了translog的功能,类似于HBase WAL,是数据写入过程中的中间数据,其余的数据都在lucene库中管理的。

所以Elasticsearch索引使用的存储内容主要取决于lucene中的数据存储。

下面我们主要看下lucene的文件内容,在了解lucene文件内容前,大家先了解些lucene的基本概念。

lucene包的文件是由很多segment文件组成的,segments_xxx文件记录了lucene包下面的segment文件数量。每个segment会包含如下的文件。

下面我们以真实的数据作为示例,看看lucene中各类型数据的容量占比。

写100w数据,有一个uuid字段,写入的是长度为36位的uuid,字符串总为3600w字节,约为35M。

数据使用一个shard,不带副本,使用默认的压缩算法,写入完成后merge成一个segment方便观察。

使用线上默认的配置,uuid存为不分词的字符串类型。创建如下索引:

首先写入100w不同的uuid,使用磁盘容量细节如下:

可以看到正排数据、倒排索引数据,列存数据容量占比几乎相同,正排数据和倒排数据还会存储Elasticsearch的唯一id字段,所以容量会比列存多一些。

35M的uuid存入Elasticsearch后,数据膨胀了3倍,达到了122.7mb。Elasticsearch竟然这么消耗资源,不要着急下结论,接下来看另一个测试结果。

我们写入100w一样的uuid,然后看看Elasticsearch使用的容量。

这回35M的数据Elasticsearch容量只有13.2mb,其中还有主要的占比还是Elasticsearch的唯一id,100w的uuid几乎不占存储容积。

所以在Elasticsearch中建立索引的字段如果基数越大(count distinct),越占用磁盘空间。

我们再看看存100w个不一样的整型会是如何。

从结果可以看到,100w整型数据,Elasticsearch的存储开销为13.6mb。如果以int型计算100w数据的长度的话,为400w字节,大概是3.8mb数据。忽略Elasticsearch唯一id字段的影响,Elasticsearch实际存储容量跟整型数据长度差不多。

我们再看一下开启最佳压缩参数对存储空间的影响:

结果中可以发现,只有正排数据会启动压缩,压缩能力确实强劲,不考虑唯一id字段,存储容量大概压缩到接近50%。

我们还做了一些实验,Elasticsearch默认是开启_all参数的,_all可以让用户传入的整体json数据作为全文检索的字段,可以更方便的检索,但在现实场景中已经使用的不多,相反会增加很多存储容量的开销,可以看下开启_all的磁盘空间使用情况:

开启_all比不开启多了40mb的存储空间,多的数据都在倒排索引上,大约会增加30%多的存储开销。所以线上都直接禁用。

然后我还做了其他几个尝试,为了验证存储容量是否和数据量成正比,写入1000w数据的uuid,发现存储容量基本为100w数据的10倍。我还验证了数据长度是否和数据量成正比,发现把uuid增长2倍、4倍,存储容量也响应的增加了2倍和4倍。在此就不一一列出数据了。

文件名为:segments_xxx

该文件为lucene数据文件的元信息文件,记录所有segment的元数据信息。

该文件主要记录了目前有多少segment,每个segment有一些基本信息,更新这些信息定位到每个segment的元信息文件。

lucene元信息文件还支持记录userData,Elasticsearch可以在此记录translog的一些相关信息。

文件后缀:.si

每个segment都有一个.si文件,记录了该segment的元信息。

segment元信息文件中记录了segment的文档数量,segment对应的文件列表等信息。

文件后缀:.fnm

该文件存储了fields的基本信息。

fields信息中包括field的数量,field的类型,以及IndexOpetions,包括是否存储、是否索引,是否分词,是否需要列存等等。

文件后缀:.fdx, .fdt

索引文件为.fdx,数据文件为.fdt,数据存储文件功能为根据自动的文档id,得到文档的内容,搜索引擎的术语习惯称之为正排数据,即doc_id -> content,es的_source数据就存在这

索引文件记录了快速定位文档数据的索引信息,数据文件记录了所有文档id的具体内容。

索引后缀:.tip,.tim

倒排索引也包含索引文件和数据文件,.tip为索引文件,.tim为数据文件,索引文件包含了每个字段的索引元信息,数据文件有具体的索引内容。

5.5.0版本的倒排索引实现为FST tree,FST tree的最大优势就是内存空间占用非常低 ,具体可以参看下这篇文章: http://www.cnblogs.com/bonelee/p/6226185.html

http://examples.mikemccandless.com/fst.py?terms=&cmd=Build+it 为FST图实例,可以根据输入的数据构造出FST图

生成的 FST 图为:

文件后缀:.doc, .pos, .pay

.doc保存了每个term的doc id列表和term在doc中的词频

全文索引的字段,会有.pos文件,保存了term在doc中的位置

全文索引的字段,使用了一些像payloads的高级特性才会有.pay文件,保存了term在doc中的一些高级特性

文件后缀:.dvm, .dvd

索引文件为.dvm,数据文件为.dvd。

lucene实现的docvalues有如下类型:

其中SORTED_SET 的 SORTED_SINGLE_VALUED类型包括了两类数据 : binary + numeric, binary是按ord排序的term的列表,numeric是doc到ord的映射。

1.elasticsearch文档存储(保存|修改|删除)

【README】

0.本文部分内容(数据)总结自 es 开发文档, Document APIs | Elasticsearch Guide [7.2] | Elastic

1.本文的es版本是7.2.1;

2.elasticsearch 是一个数据存储,检索和分析引擎;本文介绍的是 es数据存储开发方式;

  • es是以文档为单位存储数据的,数据被序列化为json文档进行存储;

3.文档存储包括文档保存,修改,删除;(文档查询的开发方式比较复杂,单独新开一篇阐述)

  • 保存文档:使用 put 或 post请求;
  • 修改文档:使用put和post请求;
    1. put请求更新文档是全量替换;
    2. post请求路径不带 _update 更新文档是 全量替换
    3. post请求路径带 _update 更新文档是 部分更新;且 带了 _update 时,会比较新老数据是否相同,若相同则不执行更新,返回noop;
  • 删除文档:用delete请求(包括删除文档,删除索引);

【1】保存文档

保存文档可以用put请求或post请求;

  • Put 与 post请求保存文档的区别:put必须带文档id,post可以带 或者不带id;

【1.1】put请求保存文档

1) 在customer索引下的 external类型下保存2号文档,文档id设置为2

Put localhost:9200/customer/external/2


  "name":"zhangsan2"



    "_index": "customer",
    "_type": "external",
    "_id": "2",
    "_version": 1,
    "result": "created", // 这里表示创建
    "_shards": 
        "total": 2,
        "successful": 1,
        "failed": 0
    ,
    "_seq_no": 2,
    "_primary_term": 1
 

2)再执行一次,result就是更新 updated 的了;


    "_index": "customer",
    "_type": "external",
    "_id": "2",
    "_version": 2,
    "result": "updated", // 第2次执行就是更新 
    "_shards": 
        "total": 2,
        "successful": 1,
        "failed": 0
    ,
    "_seq_no": 3,
    "_primary_term": 1


【1.2】post请求保存文档

1) 在customer索引下的 external类型下保存文档(不带文档id);

Post localhost:9200/customer/external 


  "name":"zhangsan_post_1"



    "_index": "customer",
    "_type": "external",
    "_id": "Qb1Gq4MBJc7j47GNcnTa", // 自动生成id,请求路径不需要设置id 
    "_version": 1,
    "result": "created",  // 新增数据
    "_shards": 
        "total": 2,
        "successful": 1,
        "failed": 0
    ,
    "_seq_no": 4,
    "_primary_term": 1

【Post请求小结】

  • 不带id就是永远新增,带了id的规则是无则新增,有则更新

2)post请求保存文档,带文档id

Post  localhost:9200/customer/external/Qr1Jq4MBJc7j47GNp3RF 

  "name":"zhangsan_post_3"



    "_index": "customer",
    "_type": "external",
    "_id": "Qr1Jq4MBJc7j47GNp3RF",
    "_version": 5,  // 版本号递增 
    "result": "updated",  // 更新 
    "_shards": 
        "total": 2,
        "successful": 1,
        "failed": 0
    ,
    "_seq_no": 9,
    "_primary_term": 1


【2】es文档简单查询

为了方便验证es文档保存,修改,删除的效果,本文介绍了一个简单查询。

【2.1】get请求带文档id查询文档

Get localhost:9200/customer/external/1



    "_index": "customer", // 在哪个索引 
    "_type": "external", // 在哪个类型 
    "_id": "1",  // 记录id 
    "_version": 2,  // 表明数据被更新过,因为新建数据的版本号是1 
    "_seq_no": 1,  // 并发控制字段,每次更新就会+1;用来做乐观锁;
    "_primary_term": 1,  // 同上,主分片重新分配,如重启,就会变化 
    "found": true,  // 
    "_source":   // 真正的内容 
        "name": "zhangsan"
    

【2.2】seq_no 和 primary_term 做并发控制(乐观锁)

步骤1)查询id等于1的文档


    "_index": "customer",
    "_type": "external",
    "_id": "1",
    "_version": 4,
    "_seq_no": 11,
    "_primary_term": 1,
    "found": true,
    "_source": 
        "name": "zhangsan2_update_seq_11"
    

步骤2)把 seq_no 和 primary_term 作为更新条件

客户端1:(修改成功

Post localhost:9200/customer/external/1?if_seq_no=11&if_primary_term=1

  "name":"zhangsan_post_update_c1"



    "_index": "customer",
    "_type": "external",
    "_id": "1",
    "_version": 5,
    "result": "updated",
    "_shards": 
        "total": 2,
        "successful": 1,
        "failed": 0
    ,
    "_seq_no": 12,
    "_primary_term": 1

客户端2(修改失败),因为 seq_no 因客户端1修改文档的事件而递增了,即不等于11

Post  localhost:9200/customer/external/1?if_seq_no=11&if_primary_term=1 

  "name":"zhangsan_post_update_c2"



    "error": 
        "root_cause": [
            
                "type": "version_conflict_engine_exception",
                "reason": "[1]: version conflict, required seqNo [11], primary term [1]. current document has seqNo [12] and primary term [1]",
                "index_uuid": "IIq46lyCQMisw9_iPDheiw",
                "shard": "0",
                "index": "customer"
            
        ],
        ......
    ,
    "status": 409


【3】修改文档

1)post 或 put请求都可以修改文档;

  • post请求修改文档又分为 请求路径带 _update 和 不带_update;(post不带_update更新文档是全量替换,带_update 是部分更新)
  • put请求修改文档 时的请求路径不能带 _update ;(put更新文档是全量替换)

【3.1】post请求路径带_update更新文档

1)更新文档id为1的文档

Post localhost:9200/customer/external/1/_update

    "doc": // 注意 post请求路径带update的更新的请求体不同
        "name":"post_update_202210061135"
    



    "_index": "customer",
    "_type": "external",
    "_id": "1",
    "_version": 6,
    "result": "updated",
    "_shards": 
        "total": 2,
        "successful": 1,
        "failed": 0
    ,
    "_seq_no": 13,
    "_primary_term": 1

2)再发送一次相同请求,且报文体也相同,则得到 noop ;

Post localhost:9200/customer/external/1/_update

    "doc":
        "name":"post_update_202210061135"
    



    "_index": "customer",
    "_type": "external",
    "_id": "1",
    "_version": 6,
    "result": "noop", // 没有做任何操作 
    "_shards": 
        "total": 0,
        "successful": 0,
        "failed": 0
    


【3.2】post请求路径不带_update更新文档

1)更新id等于1的文档

Post localhost:9200/customer/external/1

  "name":"zhangsan_post_update_noupdate"



    "_index": "customer",
    "_type": "external",
    "_id": "1",
    "_version": 7,
    "result": "updated",
    "_shards": 
        "total": 2,
        "successful": 1,
        "failed": 0
    ,
    "_seq_no": 14,
    "_primary_term": 1

2)再发一次相同请求,相同报文体,如下:


    "_index": "customer",
    "_type": "external",
    "_id": "1",
    "_version": 8,
    "result": "updated",
    "_shards": 
        "total": 2,
        "successful": 1,
        "failed": 0
    ,
    "_seq_no": 15,
    "_primary_term": 1

【小结】

  • Post请求路径带update,会检查新老数据是否相同,若相同,则无需更新;即version, seq_no 都不会增加;
  • Post请求路径不带update,则不会检查新老数据是否相同,无论是否相同,都会更新;即 version 和 seq_no 都会增加;

【3.3】put请求路径带_update更新文档(API不存在

put请求更新文档时,其请求路径不能带 _update ;

【3.4】put请求路径不带_update更新文档 (全量替换)

0)初始数据:id为2的文档;

post localhost:9200/customer/external/2


    "_index": "customer",
    "_type": "external",
    "_id": "2",
    "_version": 6,
    "_seq_no": 8,
    "_primary_term": 2,
    "found": true,
    "_source": 
        "name": "post_zhangsan_02",
        "addr": "post_addr_02"
    

1)更新id为2的文档

Put localhost:9200/customer/external/2

    "doc":
        "name":"put_update_202210071519"        
    

// 更新后的结果 

    "_index": "customer",
    "_type": "external",
    "_id": "2",
    "_version": 7,
    "_seq_no": 9,
    "_primary_term": 2,
    "found": true,
    "_source": 
        "doc": 
            "name": "put_update_202210071519"
        
    

【小结】

  • put请求路径不带_update可以更新成功; 不会检查新老数据是否相同,都会更新
  • put请求更新文档是全量覆盖(虽然put请求仅更新了name字段,但结果是addr字段被删除了);

【3.5】post请求为索引新增字段

1)为id为1的文档新增 addr 字段;

Post localhost:9200/customer/external/1 

    "doc":
        "name":"post_noupdate_202210061151"
        , "addr":"成都高新区01_post_新增属性"
    



    "_index": "customer",
    "_type": "external",
    "_id": "1",
    "_version": 11,
    "result": "updated",
    "_shards": 
        "total": 2,
        "successful": 1,
        "failed": 0
    ,
    "_seq_no": 18,
    "_primary_term": 1


【3.6】put请求为索引新增字段

1)为id为1的文档新增 addr 字段;

Put localhost:9200/customer/external/1  

    "doc":
        "name":"post_noupdate_202210061154"
        , "addr":"成都高新区02_put_新增属性"
    



    "_index": "customer",
    "_type": "external",
    "_id": "1",
    "_version": 12,
    "result": "updated",
    "_shards": 
        "total": 2,
        "successful": 1,
        "failed": 0
    ,
    "_seq_no": 19,
    "_primary_term": 1


【3.7】post请求更新文档时带_update与不带_update的区别

1)post请求路径带_update,仅部分更新(我想大部分场景应该选择带 _update)

  • 如文档有字段1,字段2;
  • 通过post带update的更新,可以仅更新字段1 或 字段2; 

2)post请求路径不带update,是全量覆盖

  • 如文档有字段1,字段2;
  • 通过post不带update的更新,是全量覆盖;

3)例子(post请求带与不带_update 更新文档的区别):

3.0)初始数据

get localhost:9200/customer/external/2 


    "_index": "customer",
    "_type": "external",
    "_id": "2",
    "_version": 6,
    "_seq_no": 24,
    "_primary_term": 1,
    "found": true,
    "_source": 
        "name": "zhangsan2_insert",
        "addr": "post_带update_成都04"
    

3.1)Post请求路径带update的部分更新

Post localhost:9200/customer/external/2/_update 

    "doc":
        "addr":"post_带update_成都05"
    


// 更新后的结果(部分更新): 

    "_index": "customer",
    "_type": "external",
    "_id": "2",
    "_version": 7,
    "_seq_no": 25,
    "_primary_term": 1,
    "found": true,
    "_source": 
        "name": "zhangsan2_insert",
        "addr": "post_带update_成都05"
    

3.2)post请求路径不带_update 的全量替换

Post localhost:9200/customer/external/2
    
    "addr":"post_不带update_成都06"    


// 更新后的结果(全量替换):

    "_index": "customer",
    "_type": "external",
    "_id": "2",
    "_version": 8,
    "_seq_no": 26,
    "_primary_term": 1,
    "found": true,
    "_source": 
        "addr": "post_不带update_成都06" // (显然,这里是全量替换)
    


【4】删除数据(删除文档和索引)

【4.1】删除文档

1)删除文档id为2的文档

Delete localhost:9200/customer/external/2


    "_index": "customer",
    "_type": "external",
    "_id": "2",
    "_version": 9,
    "result": "deleted", // 删除成功 
    "_shards": 
        "total": 2,
        "successful": 1,
        "failed": 0
    ,
    "_seq_no": 27,
    "_primary_term": 1

2)删除完成后,再次查询id为2的文档,如下:

Get localhost:9200/customer/external/2 



    "_index": "customer",
    "_type": "external",
    "_id": "2",
    "found": false  // 显然没有找到 


【4.2】删除索引

注意: es中没有提供删除类型的api  ;

1)删除customer索引;

Delete localhost:9200/customer



    "acknowledged": true

2)删除索引文档后,,再次查询(报索引不存在);

Get localhost:9200/customer/external/2


    "error": 
        "root_cause": [
            
                "type": "index_not_found_exception",
                "reason": "no such index [customer]",
                "resource.type": "index_expression",
                "resource.id": "customer",
                "index_uuid": "_na_",
                "index": "customer"
            
        ],
       ...... 
    ,
    "status": 404

以上是关于ElasticSearch数据存储内容的主要内容,如果未能解决你的问题,请参考以下文章

京东把 Elasticsearch 用得真牛逼!日均5亿订单查询完美解决!

Elasticsearch学习4-数据修改

ElasticSearch数据存储内容

ElasticSearch是什么?为什么快?倒排索引是什么?ElasticSearch的应用?

ElasticSearch第一天

python 查询Elasticsearch的小例子