美文网首页读书Java 学习分布式服务
数据在Elasticsearch的完整读写流程,掌握到这个程度就

数据在Elasticsearch的完整读写流程,掌握到这个程度就

作者: 技术栈 | 来源:发表于2022-06-17 14:01 被阅读0次
    image.png

    前言

    看到标题以后大家有些人可能感觉有点小题大做,毕竟cilent端几行代码就能解决的问题,没必要兴师动众的来仔细讲一下。其实如果你仅仅想使用一下elasticsearch的功能,并不追求性能以及高可用性,那么这么想无可厚非。

    但是如果想在生产环境下使用elasticsearch,尤其是高并发高吞吐量的场景下,那么性能优化和高可用性就不可或缺了,要做到上面两点那么数据读写这两个操作的优化是必不可少的。古语有云:“工欲善其事,必先利其器”。想要优化这两个操作,必须先了解这两个操作的原理。

    曾经有一个大神说过:“完全理解了数据库的读写流程,这个数据库一半的内容已经掌握了”。既然如此重要且势在必行,那就先在网上学习下,但是网上的博客写得不太详细而且很多地方还有些小问题,于是仔细逛了一遍官网以及看了部分源码后,将相关的内容整理了出来,供自己以及需要的兄弟姐妹们观摩学习。

    正文

    写数据

    先把我画的流程图拿出来,大家可以照着图来理解接下来的流程:

    image.png

    client写入数据时首先先连接到elasticsearch cluster的coordinator node(如果对elasticsearch的node不了解的话,可以参考下这篇文章),coordinator node根据需要写入数据的doc id字段(默认使用该字段,如果数据写入指定了routing的话,则使用routing进行分片路由)进行路由到对应的分片。路由计算方式:shard=hash(document id)%(num_of_primary_shards),然后根据节点的cluster state找到该shard所在的data node,请求就被转发到了该data node

    refresh

    下面就开始正式地写数据流程了。首先数据被写入到elasticsearch的index buffer中,该buffer在elasticsearch的heap中。写完index buffer后,数据才会写入到translog中,translog写入完毕后即可以返回客户端写入成功。此处有几个问题需要强调下,也是网上的资料经常出问题的地方,划重点

    index buffer和translog到底孰先孰后

    • 和数据库不同,数据库是先写CommitLog,然后再写内存,而Elasticsearch是先写内存,最后才写TransLog。这个有悖常理,笔者也曾经很污理解,后来考虑到可能是因为Lucene的内存写入很重很复杂,很容易失败,比如分词,字段类型不匹配且无法转型,字段长度超过限制等,为了避免TransLog中有大量无效记录,减少recover的复杂度并提升效率,所以就把写Lucene放在了写translog前面。
    • 此处需要说明下,网上很多博客都说先写translog后写index buffer或者同时写translog和index buffer的,这些优势有文艺的,为了证明,拿出最好的证据-源码来说明问题:
        public IndexResult index(Index index) throws IOException {
           .......
                    final IndexResult indexResult;
                    if (plan.earlyResultOnPreFlightError.isPresent()) {
                        indexResult = plan.earlyResultOnPreFlightError.get();
                        assert indexResult.getResultType() == Result.Type.FAILURE : indexResult.getResultType();
                    } else if (plan.indexIntoLucene || plan.addStaleOpToLucene) {
                     // 将数据写入lucene,最终会调用lucene的文档写入接口
                        indexResult = indexIntoLucene(index, plan);
                    } else {
                        indexResult = new IndexResult(
                            plan.versionForIndexing, getPrimaryTerm(), plan.seqNoForIndexing, plan.currentNotFoundOrDeleted);
                    }
                    if (index.origin().isFromTranslog() == false) {
                        final Translog.Location location;
                        if (indexResult.getResultType() == Result.Type.SUCCESS) {
                            location = translog.add(new Translog.Index(index, indexResult)); 
                        ......
                        // 将数据写入lucene后才开始写translog
                        indexResult.setTranslogLocation(location);
                    }
                  .......
            }
    

    多副本时如何写入

    • 上面的是单副本情况下的写入,如果是多副本写入可以参考下面的图。默认情况下,当primary shard写入成功后,即返回写入成功,后续replica shard通过异步的方式同步数据或者translog恢复数据。
    • 该机制可以通过设置index.write.wait_for_active_shards参数进行设置。该参数可以使用all或者在1到副本数加1(number_of_replicas+1)之间任何的一个整数值。如果是all也就是等待主分片和副本都写入成功,请求才返回,这样可以保证elasticsearch的强一致性,但是代价一是写入线程会阻塞,影响吞吐量;二是会影响elasticsearch的可用性。有一个节点不正常写入就会阻塞,直到节点恢复或者写入超时。
    image.png

    index buffer refresh

    • 数据写入到这里的时候还无法被搜索到,默认1s后执行refresh操作,将内容写入文件系统缓冲区(os cache)中的新段(segment)。此segment的内容尚未被fsynced(未被写入到硬盘),但是segment是打开的,内容可被搜索。
    • 但是1s一次的refresh会导致频繁生成新文件,在占用大量句柄以及系统资源的同时也会影响到查询数据的效率。所以对于实时性要求不是很高的业务场景,可以将refresh的时间拉长,比如30s,"index.refresh_interval":"30s"
    • 最后在refresh写完segment后会更新shard的commit point。commit point在shard中以segments_xxx名字的文件存在。用来记录每个shard中segment相关的信息。

    flush

    • 当os cache中的segments数据积累到一定时间(默认30分钟)或者translog达到一定大小时(默认512M),os cache中的segments会被flush到硬盘上进行持久化,持久化成功后,对应的translog由于失去存在的意义而被删除。
    • 此处有几个参数和flush相关,大家可以根据需求进行配置: index.translog.flush_threshold_ops:当发生多少次操作时进行一次flush。默认是 unlimited。 index.translog.flush_threshold_size:当translog的大小达到此值时会进行一次flush操作。默认是512mb。 index.translog.flush_threshold_period:在指定的时间间隔内如果没有进行flush操作,会进行一次强制flush操作。默认是30m。 index.translog.interval:多少时间间隔内会检查一次translog,来进行一次flush操作。es会随机地在这个值到这个值的2倍大小之间进行一次操作,默认是5s。

    segment merge

    • segments被持久化后写入硬盘,硬盘上的segments的数量就会越来越多,又会引发句柄占用以及影响查询效率,所以此时会触发segment merge操作。由于这个操作比较复杂,受限于本文长度就先不在这里说了,后续会专门写一篇文章来说明。

    Delete&&Update操作

    Delete&&Update操作也是一种特殊的写操作,但是由于Delete&&Update操作并不是即时生效,而是通过标记删除的方式来实现,最终通过segment merge操作实现真删。所以和标准的写入还是有一定的差别,下面来说一下具体差别:

    • Delete:磁盘上的每个分段(segement)都有一个.del文件与它关联。当客户端发送删除请求时,该文档未被真正删除,而是在.del文件中标记为已删除。此文档仍然可能被搜到,但会从结果中过滤掉。当分段合并时,在.del文件中标记为删除的文档不会包括在新的合并段中。
    • Update:创建新文件,Elasticsearch将该文档分配一个版本号。对文档的每次更改都会产生一个新的版本号,版本号使用versionMap来进行管理,用以减少热点数据的多次磁盘IO开销。当执行更新时,旧版本在.del文件中标记为已删除,并且并且在新版本的分片中编入索引。旧版本可能仍然与搜索查询匹配,但是从结果中将其过滤掉。

    读数据

    elasticsearch中的读操作包含get操作以及search操作,下面就根据这两个操作来详细讲解elasticsearch是如何进行读数据操作的。为了节省篇幅,此处统一说明一下,client从coordinator节点路由到对应数据所在节点的过程与写入数据的流程相同,请大家参考上面写数据相关的内容。下面直接就从数据节点开始讲解。

    名词解释

    • 正排索引:doc id -> value,方向是正常方向,所以称为正排索引。使用场景是get这种通过doc id找value的场景。在shard中以后缀名为fdx与fdt结尾的文件存储,dfx是索引文件,fdt是数据文件。这两部分数据是硬盘消耗的大户,elasticsearch中的source字段即存储在fdt文件中。
    • 倒排索引:index -> doc id,方向与上面的相反,所以称为倒排索引。使用场景是search通过查询条件匹配对应的倒排索引拿到对应数据的doc id,拿到doc id后查询正排索引拿到真正的数据。在shard中以后缀名除了正排索引外绝大部分都是各种类型的倒排索引,每一种倒排索引也分为索引文件和数据文件。这两部分数据是内存消耗的大户,elasticsearch中的倒排索引都会加载到off heap中用来加速查询,这也是要留给lucene一半内存最主要的原因。

    get

    get操作即使用doc id字段进行单条数据的查询,查询流程图如下:

    image.png
    • 首先使用doc id字段从os cache中的translog中查询,如果能查询到就直接返回客户端;
    • 如果os cache中的translog没有查询到的话,再去disk上的tranlog中查询,如果能查询到就直接返回客户端;
    • 如果reanslog中没有查询到对应的数据,再去segment中查询对应的数据。首先把正排索引的fdx数据加载到off heap中去查询doc id。如果查询不到则直接返回null;如果能查询到doc id,根据查询结果中的偏移量直接去硬盘上查询对应的原始数据并返回。
    • 此处大家思考下get操作(其实update以及delete操作也会优先查询translog)为什么要优先查询translog后再去查询segment数据?这里说明一下原因:上文已经说过了delete以及update操作在elasticsearch中都是先打删除标记然后通过segment merge操作进行真删,所以一条数据可能在elasticsearch中有几个版本,而最新的版本可能会存在于translog中。所以如果在translog中查询到目标数据直接返回即可,一定是最新的数据;如果translog中没有目标数据,再去segments中查询。

    search

    search也是elasticsearch中比较复杂的流程。总体分为term search以及分词search。分词search内容较多,考虑到篇幅,后续会单独写一篇文章讲述。此处仅仅是为了讲解search的原理和流程,所以使用term search来进行讲解。

    search操作的阶段取决于search type的选择,后续单独写一篇文章来说明elasticsearch的search type。而search type默认使用query then fetch类型,该类型由两个阶段组成:查询阶段(query)和获取阶段(fetch)阶段。

    • 查询阶段:在此阶段,协调节点将搜索请求路由到索引中的所有分片(包括:主分片和副本分片)。分片独立执行搜索,并根据相关性分数创建一个优先级排序结果.所有分片将匹配到的文档和相关性分数的文档id返回给协调节点。协调节点创建一个新的优先级队列,并对全局结果进行排序。可以有很多文档匹配结果,但默认情况下,每个分片将前10个结果发送到协调节点,协调节点创建优先级队列,从所有分片中分选结果并返回前10个匹配结果。
    • 获取阶段:在协调节点对所有的结果进行排序,生成全局排序的文档列表后,它将所有分片请求原始文档。所有的分片都会丰富文档并将其返回到协调节点。

    term search的流程如下:


    image.png
    • 首先client会根据search中的term去elasticsearch heap中的Segment Cache中查询FST数据(如果不知道FST数据建议参考之前的博文),简单来说就是倒排索引的前缀树。
    • 根据FST数据来查询对应的倒排索引tip文件,将tip文件加载到off heap中进行查询,将查询后的索引数据查询倒排索引数据tim文件,得到对应的doc id列表。
    • 根据doc id列表将对应的正排索引文件fdx数据当如到off heap中查询对应doc id的地址,查询之后去fdt文件中取出对应的原始数据中必要的信息返回到协调节点(完整的数据在fetch阶段获取)。

    后记

    elasticsearch读写不仅是将数据写入或者读出数据库,更包含了数据在elasticsearch中的流转过程,并触发elasticsearch的相关机制,这些机制都是elasticsearch的核心机制,也是性能优化以及高可用性配置的优先考虑对象。

    考虑到篇幅,该篇文章也只是将主要流程解读了一下,还有很多细节没能完整讲解,这些就需要大家在以后的工作中按需了解了,但是如果能将上述篇幅中的内容完全理解,基本上也能应对绝大多数场景了。

    最后,笔者长期关注大数据通用技术,通用原理以及NOSQL数据库的技术架构以及使用。如果大家感觉笔者写的还不错,麻烦大家多多点赞和分享转发,也许你的朋友也喜欢。

    相关文章

      网友评论

        本文标题:数据在Elasticsearch的完整读写流程,掌握到这个程度就

        本文链接:https://www.haomeiwen.com/subject/aoxhvrtx.html