elasticsearch之九文档存储机制及优化

个人专题目录


1. 文档存储机制

1.1 数据路由

文档存储如何路由到相应分片

一个文档,最终会落在主分片的一个分片上,到底应该在哪一个分片?这就是数据路由。

路由算法

shard = hash(routing) % number_of_primary_shards

哈希值对主分片数取模。

举例:

对一个文档经行crud时,都会带一个路由值 routing number。默认为文档_id(可能是手动指定,也可能是自动生成)。

存储1号文档,经过哈希计算,哈希值为2,此索引有3个主分片,那么计算2%3=2,就算出此文档在P2分片上。

决定一个document在哪个shard上,最重要的一个值就是routing值,默认是_id,也可以手动指定,相同的routing值,每次过来,从hash函数中,产出的hash值一定是相同的

无论hash值是几,无论是什么数字,对number_of_primary_shards求余数,结果一定是在0~number_of_primary_shards-1之间这个范围内的。0,1,2。

手动指定 routing number

PUT /test_index/_doc/15?routing=num
{
  "num": 0,
  "tags": []
}

场景:在程序中,架构师可以手动指定已有数据的一个属性为路由值,好处是可以定制一类文档数据存储到一个分片中。缺点是设计不好,会造成数据倾斜。

所以,不同文档尽量放到不同的索引中。剩下的事情交给es集群自己处理。

主分片数量不可变

涉及到以往数据的查询搜索,所以一旦建立索引,主分片数不可变。

1.2 文档的增删改内部机制

增删改可以看做update,都是对数据的改动。一个改动请求发送到es集群,经历以下四个步骤:

  • 客户端选择一个node发送请求过去,这个node就是coordinating node(协调节点)

  • coordinating node,对document进行路由,将请求转发给对应的node(有primary shard)

  • 实际的node上的primary shard处理请求,然后将数据同步到replica node。

  • coordinating node,如果发现primary node和所有replica node都搞定之后,就返回响应结果给客户端。

1.3 文档的查询内部机制

  • 客户端发送请求到任意一个node,成为coordinate node

  • coordinate node对document进行路由,将请求转发到对应的node,此时会使用round-robin随机轮询算法,在primary shard以及其所有replica中随机选择一个,让读请求负载均衡

  • 接收请求的node返回document给coordinate node

  • coordinate node返回document给客户端

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

1.4 bulk api奇特的json格式

POST /_bulk
{"action": {"meta"}}\n
{"data"}\n
{"action": {"meta"}}\n
{"data"}\n

[
    {
        "action":{
            "method":"create"
        },
        "data":{
            "id":1,
            "field1":"java",
            "field1":"spring",
        }
    },
      {
        "action":{
            "method":"create"
        },
        "data":{
            "id":2,
            "field1":"java",
            "field1":"spring",
        }
    }       
]
  1. bulk中的每个操作都可能要转发到不同的node的shard去执行

  2. 如果采用比较良好的json数组格式

允许任意的换行,整个可读性非常棒,读起来很爽,es拿到那种标准格式的json串以后,要按照下述流程去进行处理

  • 将json数组解析为JSONArray对象,这个时候,整个数据,就会在内存中出现一份一模一样的拷贝,一份数据是json文本,一份数据是JSONArray对象

  • 解析json数组里的每个json,对每个请求中的document进行路由

  • 为路由到同一个shard上的多个请求,创建一个请求数组。100请求中有10个是到P1.

  • 将这个请求数组序列化

  • 将序列化后的请求数组发送到对应的节点上去

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

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

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

另外的话,占用内存更多,就会导致java虚拟机的垃圾回收次数更多,跟频繁,每次要回收的垃圾对象更多,耗费的时间更多,导致es的java虚拟机停止工作线程的时间更多。

  1. 现在的奇特格式
POST /_bulk
{ "delete": { "_index": "test_index",  "_id": "5" }} \n
{ "create": { "_index": "test_index",  "_id": "14" }}\n
{ "test_field": "test14" }\n
{ "update": { "_index": "test_index",  "_id": "2"} }\n
{ "doc" : {"test_field" : "bulk test"} }\n
  • 不用将其转换为json对象,不会出现内存中的相同数据的拷贝,直接按照换行符切割json

  • 对每两个一组的json,读取meta,进行document路由

  • 直接将对应的json发送到node上去

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

1.5 深度剖析document写入原理(buffer,segment,commit)

  1. 数据写入buffer
  2. commit point
  3. buffer中的数据写入新的index segment
  4. 等待在os cache中的index segment被fsync强制刷到磁盘上
  5. 新的index sgement被打开,供search使用
    1. buffer被清空

每次commit point时,会有一个.del文件,标记了哪些segment中的哪些document被标记为deleted了
搜索的时候,会依次查询所有的segment,从旧的到新的,比如被修改过的document,在旧的segment中,会标记为deleted,在新的segment中会有其新的数据

1.6 优化写入流程实现NRT近实时(filesystem cache,refresh)

每次都必须等待fsync将segment刷入磁盘,才能将segment打开供search使用,这样的话,从一个document写入,到它可以被搜索,可能会超过1分钟!!!这就不是近实时的搜索了!!!主要瓶颈在于fsync实际发生磁盘IO写数据进磁盘,是很耗时的。

写入流程别改进如下:

  1. 数据写入buffer
  2. 每隔一定时间,buffer中的数据被写入segment文件,但是先写入os cache
  3. 只要segment写入os cache,那就直接打开供search使用,不立即执行commit

数据写入os cache,并被打开供搜索的过程,叫做refresh,默认是每隔1秒refresh一次。也就是说,每隔一秒就会将buffer中的数据写入一个新的index segment file,先写入os cache中。所以,es是近实时的,数据写入到可以被搜索,默认是1秒。

POST /my_index/_refresh,可以手动refresh,一般不需要手动执行,没必要,让es自己搞就可以了

比如说,我们现在的时效性要求,比较低,只要求一条数据写入es,一分钟以后才让我们搜索到就可以了,那么就可以调整refresh interval

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

1.7 继续优化写入流程实现durability可靠存储(translog,flush)

再次优化的写入流程

  1. 数据写入buffer缓冲和translog日志文件
  2. 每隔一秒钟,buffer中的数据被写入新的segment file,并进入os cache,此时segment被打开并供search使用
  3. buffer被清空
  4. 重复1~3,新的segment不断添加,buffer不断被清空,而translog中的数据不断累加
  5. 当translog长度达到一定程度的时候,commit操作发生
    1. buffer中的所有数据写入一个新的segment,并写入os cache,打开供使用
    2. buffer被清空
    3. 一个commit ponit被写入磁盘,标明了所有的index segment
    4. filesystem cache中的所有index segment file缓存数据,被fsync强行刷到磁盘上
    5. 现有的translog被清空,创建一个新的translog

基于translog和commit point,如何进行数据恢复

fsync+清空translog,就是flush,默认每隔30分钟flush一次,或者当translog过大的时候,也会flush

POST /my_index/_flush,一般来说别手动flush,让它自动执行就可以了

translog,每隔5秒被fsync一次到磁盘上。在一次增删改操作之后,当fsync在primary shard和replica shard都成功之后,那次增删改操作才会成功

但是这种在一次增删改时强行fsync translog可能会导致部分操作比较耗时,也可以允许部分数据丢失,设置异步fsync translog

PUT /my_index/_settings
{
    "index.translog.durability": "async",
    "index.translog.sync_interval": "5s"
}

1.8 最后优化写入流程实现海量磁盘文件合并(segment merge,optimize)

每秒一个segment file,文件过多,而且每次search都要搜索所有的segment,很耗时

默认会在后台执行segment merge操作,在merge的时候,被标记为deleted的document也会被彻底物理删除

每次merge操作的执行流程

  1. 选择一些有相似大小的segment,merge成一个大的segment
  2. 将新的segment flush到磁盘上去
  3. 写一个新的commit point,包括了新的segment,并且排除旧的那些segment
  4. 将新的segment打开供搜索
    (5)将旧的segment删除

POST /my_index/_optimize?max_num_segments=1,尽量不要手动执行,让它自动默认执行就可以了

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容