Elastic Search 存储方式

个人习惯,一般学习数据库,主要学习其数据存储方式和索引结构。理解了这些,碰到很多优化方面的问题会事半功倍。


1 es写入数据过程

先从数据如何被写入开始讲起,es是分布式的数据库,所以其写入流程也会相对复杂一点。

在es中,节点主要可分为master节点和数据节点(还有其他的节点类型,但只有这两类节点集群就能运行),其中数据节点承担了数据写入的工作。

  1. 请求路由:当写入请求被任一数据节点接收后(数据节点默认一般也担任着协调节点的功能),然后通过其要写入的文档id计算要写入到那个主分区中(分区策略算法为 shard = hash(id) % number_of_primary_shards)

    再通过查询分区元信息,确认该主分区Shared1位于数据节点Node1

    随后将请求转发到该数据节点Node1

  2. 具体写入:Node1收到转发的请求后,首先对写入请求进行校验和预处理(像动态mapping就是这一步操作的),然后将文档数据写入到主分片Shared1。但由于es对读性能做出了巨大优化,导致写性能相对变的很差,所以在具体的写入文档时,es也做了很多优化来尽可能提升写入速度。

    首先会把文档数据先写入内存,经过索引中指定的refresh_interval,会定时将数据写入到底层的Lucene中(这时数据可以被检索到),Lucene也不是直接将数据写入到磁盘的,而是写到了os cache,该步骤又称refresh操作。

    但是这样做的话,系统宕机会产生丢失数据的风险(因为不可预知os cache什么时候会刷新到磁盘上),所以es引入了WAL预写日志机制,在一开始写入内存时,会先写入translog buffer,经过index.translog.sync_interval时间间隔后(默认5s),translog buffer会写入到磁盘上的translog文件。

    当文档写入内存后,主分片Shared1会将写请求转发到其他的备份分片上,默认等待一半分片写入成功后,会返回给协调节点写入成功的响应(可以通过wait_for_active_shards参数调节确认写入的分片数量)。

es_write_flow

2 es存储结构

es中最高层结构为索引,是同类比文档的集合,可以类比于数据库中的表。

  1. shard:索引中包含着分片shard,每个索引最低拥有一个主分片和不限制数量的备份分片,可以在索引的setting中指定。

    其底层其实是Lucene中的索引,即每个分片其实都是一个Lucene实例,所以每个分片都能执行具体的CURD操作。

  2. translog:上面有提及过,基本上所有的数据库都有类似的WAL机制,用于防止机器突然宕机后的数据丢失。在文档数据写入内存buffer的同时,会将该条操作写入translog buffer,并在一定的时间内,持久化到磁盘中,默认是每次request操作结束后进行一次持久化操作。

    并且translog也会定时的并清空掉的,该步叫flush操作(对应了Lucene中的commit操作),意味着Lucene真正的将索引数据保存到了磁盘上。在机器宕机的时候,Lucene由于没有持久化导致数据丢失了,这时es也可以使用translog去恢复数据。

  3. segment: 每一次refresh_interval间隔后,都会在相应索引下生产segment文件,其实segment文件就是指Lucene中的倒排索引,由于每次间隔都会生成一个小文件,当文件数目过多时,会影响查询效率和占用过多内存,所以segment文件定期会进行合并来减小数量(有点消耗CPU和磁盘),当然也可以调用API进行强制归并/_forcemerge?max_num_segments=1

segment再往下就要到Lucene中的结构了(当然了,Lucene中用到的数据结构非常多,这里只讲目前我知道的一部分)

  1. document:文档,也就是我们向es写入的每一条数据,一个segment包含了许多文档,主要存储了文档所有的元信息、原始文档内容、DocValues。

    DocValues为文档的正向索引(使用列式储存),基本上可以等同我们平时所使用的数据库,Lucene额外存储DocValues是为了能够快速的做聚合计算使用的,比方说用倒排索引sum文档中某一个字段的值,消耗是无比巨大的,但是使用列式储存,则十分快捷。

  2. term:词条,也是倒排索引整个的核心以及主要实现方式。通过将文档内容进行分词后 + 词频、位置等元信息就可以构成倒排索引了。通过倒排索引,可以快速根据term反查所在的文档,而所有的term就构成了词典。

    由于词典中的词可能有几百万、上千万,为了能够使查询尽可能的快速,Lucene使用了多种数据结构进行优化。

    为了使词典能够放到内存中进行查询,使用了FST进行存储空间的压缩。

    为了能够根据查询的term快速定位到对应的词典,在词典上使用词典的前缀进一步构建了Trie Tree的term index。

    为了能使多查询结果进行合并,倒排索引中的doc列表使用跳表进行维护。

3 es写入优化

了解这么多底层的实现,当然是为了我们调优进行服务的了。

我之前做过一个日志收集,每天日志写入量在5000W左右,其实也不算很大,但是由于机器配置太差,所以无奈只能想办法进行写入优化。

首先,对于日志记录场景,不需要有高可用、持久化等方面的要求,日志没写进来就算了,后期还可以通过原始日志文件再进行导入,因此首先要去掉的就是分片副本(去掉一个就少几次写入)。

ES中的translog是用于保障数据不会丢失的,追求日志高吞吐量的话也可以忽略掉,translog.durability改成异步写入,并且增加translog的刷盘时间。

如果不需要有实时查询日志要求的话,也可以降低refresh_interval时间,也可以比较明显的增加写入速度,减少磁盘压力。

日志场景查询压力并不是很大,所以segment合并时间可以放大或者直接去掉改成每日凌晨定时合并,也可以明显缓解机器压力。同时ES默认用于segment合并的空间使用限制为每秒20M,如果ES上的机器磁盘条件可以,可以进行调大。

然后在日志写入方,使用批量写入功能,并权衡好每批写入的数量和时间延长(每批写入数量无脑调大反而可能会减少写入速度),同时在ES中也可以调大批量写入线程和队列大小,加速写入。

由于日志基本是只追加操作,所以ES中的docId使用自增id或者es默认的id,来保证不会触发version变更的操作。

可以使用以下脚本,观察ES集群的写入速度

https://editor.csdn.net/md/?articleId=114587419